> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionicfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a new merchant with Account Links

> Send a merchant from your app to Ionic to sign up and connect, then save their merchant ID when they return.

An Account Link brings a merchant onto Ionic from inside your app. Your server
creates the link and redirects the merchant to it. On Ionic, the merchant
verifies their email, agrees to connect to your platform, and completes their
application. Ionic then sends them back to your app, and your server saves
their merchant ID. You use that ID to take payments for the merchant.

This guide follows a platform at `https://yourapp.com` onboarding a dental
practice, Bayview Dental. For every field and status, see the API reference for
[creating a link](/api-reference/account-links/create-a-hosted-onboarding-link)
and [retrieving one](/api-reference/account-links/retrieve-hosted-onboarding-progress).

## Before you begin

Every Account Link follows a configuration. The configuration is your
standing offer to merchants: what your platform can do on their account, what
they pay, and which accounts they can use. You set it up once in the
Dashboard, and each link you create uses it. Changing your pricing or
permissions later means editing the configuration, not your code.

1. In **Connect → Domains**, add `https://yourapp.com` under **Redirect
   domains**. Ionic only sends merchants back to origins you list here.
2. In **Connect → Onboarding → Links**, choose **New link**. This is your
   configuration.
   * Choose **Connect + live application** and a pricing plan. The merchant
     signs an application with that plan's rates.
   * Choose the permissions your platform needs, such as taking payments.
   * Under **More options**, set **Accounts merchants can use** to **New
     accounts only**.
3. Choose **Create link**, which publishes it. Then choose **Copy link ID**
   from its menu. This `ccfg_` ID is the `configuration_id` for your requests.
4. In **Connect → API keys**, create a platform [secret key](/guides/authentication) with `platform:act`
   and the same permissions.

The configuration doesn't store a return URL. You choose one for each link
when you create it, as described in [Choose a return URL](#choose-a-return-url).

## Start in test mode

Test mode is available as soon as Ionic enables Connect for your platform.
Create a configuration and key in [test mode](/concepts/key-concepts#test-mode-and-live-mode) and follow the same steps. Test
configurations use **Connect only**: the merchant verifies their email and
agrees to connect, with no application or pricing. Accounts created in test
mode can take test payments as soon as the merchant agrees, so
`payments_enabled` is `true` when they return.

In test mode, `return_url` can also point at your machine, such as
`http://localhost:3000/connect/callback`, without adding it under **Redirect
domains**. Ionic sends real verification codes, so use an email address you can
receive. When you are ready for live merchants, create a live configuration
with **Connect + live application** and a pricing plan, switch to your live
secret key, and use an HTTPS return URL on a redirect domain.

## 1. Create the Account Link

Add an endpoint behind your **Set up payments** button. It creates the link,
saves the link ID with the practice, and redirects the browser to Ionic.

```js theme={null}
// POST https://yourapp.com/settings/payments/connect
app.post("/settings/payments/connect", requireLogin, async (req, res) => {
  const practice = await db.practices.find(req.user.practiceId);

  const response = await fetch("https://api.ionicfi.com/v1/account_links", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.IONIC_PLATFORM_SECRET_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      configuration_id: "ccfg_4kT9mQ2xV7nB1cR8pL5wZ3yH",
      return_url: "https://yourapp.com/connect/callback",
      external_reference: practice.id,
      prefill: {
        business_name: "Bayview Dental",
        contact_first_name: "Ana",
        contact_last_name: "Lee",
        contact_email: "ana@bayviewdental.com",
        contact_phone: "+14155550100",
        address_line1: "1 Harbor Way",
        city: "San Francisco",
        state: "CA",
        postal_code: "94105",
        country: "US",
      },
    }),
  });
  if (!response.ok) throw new Error(`Ionic returned ${response.status}`);
  const link = await response.json();

  await db.accountLinks.insert({ id: link.id, practiceId: practice.id });
  await db.practices.update(practice.id, { currentAccountLink: link.id });
  res.redirect(303, link.url);
});
```

| Parameter | Required | Description |
| - | - | - |
| `Idempotency-Key` header | Yes | A new value for each link. Retrying with the same key and body returns the same link; the same key with a different body returns `409`. |
| `configuration_id` | Yes | The `ccfg_` ID you copied from the Dashboard. It must be published and in the same mode as your key. |
| `return_url` | Yes | Where Ionic sends the merchant when this visit ends. Any path and query on a redirect domain. See [Choose a return URL](#choose-a-return-url). |
| `prefill` | Yes, for new accounts | The business and its owner. Ionic creates the merchant's account from these values. |
| `external_reference` | No | Your own ID for the merchant, 1 to 256 characters. Ionic returns it when you read the link and on `account_link.*` events. It doesn't need to be unique. |
| `configuration_revision` | No | Pins a published revision of the configuration. Omit it to use the current one. |
| `resume_from` | No | An earlier link ID to continue. See [Restart an unfinished visit](#restart-an-unfinished-visit). |

`prefill` requires `business_name` (up to 200 characters),
`contact_first_name`, `contact_last_name`, `contact_email`, `contact_phone`,
`address_line1`, `city`, `state`, `postal_code`, and `country` (`US`).
`address_line2` is optional. A missing or invalid field returns `422` with a
message that names it.

<Info>
  `contact_email` becomes the account owner's login. Ionic sends the
  verification code to this address, and the merchant cannot change it during
  onboarding. Use the email of the person who will own the account.
</Info>

Ionic creates the merchant's account and responds:

```json theme={null}
{
  "id": "alink_8Fq2LmV0cX4nR7tYp1KsW3dA",
  "object": "account_link",
  "expires_at": "2026-10-06T18:30:00Z",
  "configuration_id": "ccfg_4kT9mQ2xV7nB1cR8pL5wZ3yH",
  "configuration_revision": 3,
  "account": "mer_7Hk2Pq9Lm4Xw1R",
  "url": "https://app.ionicfi.com/connect/l/alink_8Fq2LmV0cX4nR7tYp1KsW3dA#launch=iclaunch_…"
}
```

`account` is Bayview Dental's merchant ID. It exists now, but your platform
can't act for the merchant until they agree to connect in the next step. Until
then, reading the link returns `onboarding_status: "awaiting_consent"` and no
`connection_id`. The `url` works once and expires after 30 minutes, so redirect
to it right away. Don't email or message it, because link previews can open it
first.

## Choose a return URL

`return_url` is where Ionic sends the merchant's browser when their visit ends,
whether they finished or chose to leave. You set it on each link, so different
links can return to different pages of your app.

* **Origin.** The scheme, host, and port must match a redirect domain you
  added under **Connect → Domains**. `https://yourapp.com` allows
  `https://yourapp.com/anything`, but not `https://www.yourapp.com` or
  `https://yourapp.com:8443`.
* **Path and query.** Anything you like, such as
  `https://yourapp.com/settings/payments/done?from=onboarding`. Ionic keeps your
  query values and adds `account_link`. It may re-encode the query, so don't
  rely on parameter order.
* **`account_link` is reserved.** Ionic always sets it to the link ID and
  replaces any value you put there.
* **Not allowed.** Fragments (`#…`), credentials (`user:pass@`), and, in live
  mode, HTTP or loopback addresses. Write internationalized hostnames in
  punycode.
* **Test mode.** Also accepts `http://localhost:<port>`,
  `http://127.0.0.1:<port>`, and `http://[::1]:<port>` without registering them.
* **Checked twice.** Ionic checks the origin when you create the link and again
  when the merchant returns. Removing a redirect domain stops new links and
  returns to it. Reading the link still works, and
  [`resume_from`](#restart-an-unfinished-visit) can send the merchant to a new
  URL.

The return tells you which link to look at, not what happened. Anyone can open
your return URL with any `account_link` value, so always find the link in your
own records and read its result from the API, as in the next steps.

## 2. The merchant onboards on Ionic

Ana receives a code at `ana@bayviewdental.com`, enters it, and clicks
**Connect**. That one click creates her Ionic login as the account owner,
agrees to Ionic's terms, and connects the account to your platform. She then
reviews the prefilled application, which carries your pricing plan's rates,
and signs it. Ionic sends her back to your return URL.

If she chooses to leave instead, Ionic still sends her back to your return URL,
and the link's `outcome` records that she left.

## 3. Handle the return

Ionic adds the link ID to your return URL:

```text theme={null}
https://yourapp.com/connect/callback?account_link=alink_8Fq2LmV0cX4nR7tYp1KsW3dA
```

Add an endpoint at that URL. It finds the practice by the saved link ID, reads
the link from Ionic, and saves the merchant ID.

```js theme={null}
// GET https://yourapp.com/connect/callback?account_link=alink_…
app.get("/connect/callback", async (req, res) => {
  const saved = await db.accountLinks.find(req.query.account_link);
  if (!saved) return res.status(404).send("Unknown Account Link");

  // Only the practice's latest link may change its payment settings.
  const practice = await db.practices.find(saved.practiceId);
  if (practice.currentAccountLink !== saved.id) {
    return res.redirect(303, "/settings/payments");
  }

  const response = await fetch(
    `https://api.ionicfi.com/v1/account_links/${saved.id}`,
    { headers: { Authorization: `Bearer ${process.env.IONIC_PLATFORM_SECRET_KEY}` } },
  );
  if (!response.ok) throw new Error(`Ionic returned ${response.status}`);
  const link = await response.json();

  // Before consent there is no Connection to save.
  if (link.connection_id) {
    await db.practices.update(practice.id, {
      ionicAccount: link.connected_account_id,
      ionicConnection: link.connection_id,
      paymentsEnabled: link.payments_enabled,
    });
  }
  res.redirect(303, "/settings/payments");
});
```

The response:

```json theme={null}
{
  "id": "alink_8Fq2LmV0cX4nR7tYp1KsW3dA",
  "object": "account_link",
  "external_reference": "practice_4281",
  "livemode": true,
  "outcome": "merchant_actions_completed",
  "connected_account_id": "mer_7Hk2Pq9Lm4Xw1R",
  "connection_id": "conn_3Rv8Kd1Nq6Lp0T",
  "connection_status": "active",
  "onboarding_status": "under_review",
  "payments_enabled": false,
  ...
}
```

Bayview Dental is connected. Send `connected_account_id` as the
`Ionic-Account` header on requests you make for the practice, as described in
[Selecting a merchant](/guides/connect-overview#selecting-a-merchant).

Finding the practice by link ID, rather than by the signed-in user, keeps the
callback working when Ana finishes on a different device. Checking that it is
the practice's latest link stops an older, abandoned visit from replacing a
working connection, and requiring `connection_id` keeps a merchant who left
before connecting from clearing your saved one. Use a key in the same
mode that created the link.

Three fields answer different questions, so check each one:

| Field | Answers | Values |
| - | - | - |
| `outcome` | How did this visit end? | `in_progress`, `merchant_actions_completed`, `cancelled` (left before connecting), `incomplete` (connected, then left), `expired`, `blocked` |
| `onboarding_status` | Where is the application? | `awaiting_consent`, `collecting`, `awaiting_signature`, `under_review`, `approved`, `closed`. For Connect only configurations, absent once the owner consents. |
| `payments_enabled` | Can the account take payments now? | `true` or `false` |

`merchant_actions_completed` means Ana did everything Ionic needed from her in
this visit. It doesn't mean she can take payments yet. The
[API reference](/api-reference/account-links/retrieve-hosted-onboarding-progress)
describes every field and value.

## 4. Know when payments are enabled

Ana's application is `under_review`. Ionic reviews it after she returns, and
`payments_enabled` becomes `true` when the account can take payments. Ionic
sends an `account.updated` event to your
[connected-accounts webhook endpoint](/webhooks/endpoints#platform-webhooks-for-connected-accounts)
when that happens. You can also call
[`GET /v1/account_links/{id}`](/api-reference/account-links/retrieve-hosted-onboarding-progress)
at any time for the current state, including when the browser never returned.

## Restart an unfinished visit

If a link expires or the merchant leaves before finishing, create a new link
with `resume_from` set to the previous link ID. The merchant continues where
they stopped, with the same account and application. Send a new
`Idempotency-Key`, the same `configuration_id` and `external_reference`, and no
`prefill`. `return_url` can be the same or a new one on a redirect domain. Save
the new link ID as the practice's current link and redirect to its `url`.

Creating a fresh link without `resume_from` starts over and can create a second
account for the same merchant.

## Existing Ionic merchants

A configuration set to **Existing accounts only** or **New or existing**
accepts merchants who already have an Ionic account. They sign in instead of
verifying an email, and `prefill` is optional. A merchant who already takes
payments with Ionic keeps their current pricing and skips the application: the
link reports `onboarding_status: "approved"` and `existing_terms_retained: true`.

## What's next

<CardGroup cols={2}>
  <Card title="Register payment domains" icon="globe" href="/guides/platform-payment-domains">
    Add the websites your platform operates so connected accounts can take payments.
  </Card>

  <Card title="Send buyers to hosted checkout" icon="shopping-cart" href="/guides/connect-checkout">
    Take a first payment for the connected account with no payment form to build.
  </Card>

  <Card title="Take payments on your own page" icon="building" href="/guides/connect-blocks">
    Collect card payments for the connected account with Ionic Blocks.
  </Card>
</CardGroup>

* [Create a hosted onboarding link](/api-reference/account-links/create-a-hosted-onboarding-link):
  every request field, error, and example.
* [Retrieve hosted onboarding progress](/api-reference/account-links/retrieve-hosted-onboarding-progress):
  every result field and status value.
* [Connect overview](/guides/connect-overview): connected accounts and the
  `Ionic-Account` header.
* [Webhook endpoints](/webhooks/endpoints#platform-webhooks-for-connected-accounts):
  `account_link.*`, `connection.*`, and `account.*` events for your platform.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.