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

# Hosted account links (Plaid & card)

Some internal accounts can't be created from raw details - a Plaid bank account or a card. For these, you mint a short-lived, Melio-hosted link and embed it, so bank and card details never touch your servers. **The outcome of this flow is a new internal account for the entity** (the funding source it pays from). This page shows the Plaid flow end to end, and the card flow that works the same way.

## Flow at a glance

<Frame>
  <img src="https://mintcdn.com/melio/YszTGs6Gx650_Iqt/images/hosted-links-flow.png?fit=max&auto=format&n=YszTGs6Gx650_Iqt&q=85&s=e1f2402412756aa7a89abd66625ef619" alt="Hosted Links Flow" width="1480" height="720" data-path="images/hosted-links-flow.png" />
</Frame>

## 1. Mint the link

Create the link with the accounts endpoint, passing `type: "plaid"` (or `"card"`). See the [API reference](https://developers.melio.com/docs/api-reference/accounts/create-a-link-to-add-a-plaid-or-card-account).

```http theme={null}
POST /v2/accounts/link
api-key: YOUR_API_KEY
Melio-Entity-Id: ent_4kQz9mXpR2wL
Content-Type: application/json

{ "type": "plaid" }
```

The response returns a short-lived `url` and its `expiration`. Mint a fresh link if it expires before the user opens it.

```json theme={null}
{ "data": { "url": "https://portal.melio.com/...", "expiration": "2026-07-08T16:04:05Z" } }
```

<Note>
  This is a UI screen, so embed the [Sonar SDK](/docs/docs/sonar) and forward the `Melio-Sonar-Token` on the call (see [UI requirements](/docs/docs/ui-requirements)).
</Note>

**If the link can't be minted**, the call returns:

* `400` - the request is invalid.
* `403 BUSINESS_NOT_ELIGIBLE` - a risk or compliance limitation blocks the entity from adding an internal account (check `GET /limitations`).
* `404` - the entity was not found.

## 2. Embed it

Embed the returned `url` in an iframe (or redirect the user to it). The hosted page runs the flow and creates the internal account for you.

```html theme={null}
<iframe src="{data.url}" allow="payment"></iframe>
```

<Frame>
  <img src="https://mintcdn.com/melio/o94hw4-DRHnzxmpx/images/plaid-flow.gif?s=7903c31681ac6ff88d2e1de9cf03aeb4" alt="Plaid Flow" width="1000" height="801" data-path="images/plaid-flow.gif" />
</Frame>

<Note>
  **Choosing an account:** if the connected bank has more than one account, Plaid Link lists them and the user selects which one to link. That single account becomes the internal account. To add another, mint another link.
</Note>

<Tip>
  **Testing in the simulator:** to run the full Plaid flow in the sandbox, choose **"Continue without phone number"** in Plaid Link. This skips phone verification so you can complete the flow end to end.
</Tip>

## 3. Listen for the result

When the flow finishes, the embedded page posts a `melio:accounts:internal` message to the parent window. Listen for it, and always filter on `event.data.type`:

```js theme={null}
window.addEventListener("message", (event) => {
  if (event.data?.type !== "melio:accounts:internal") return;

  const { flow, kind, status, accountId, message } = event.data;

  if (status === "success") {
    // Account added. Confirm via the api.account.created webhook (source of truth).
  } else if (status === "cancelled") {
    // The user closed the Plaid flow without finishing (Plaid only).
  } else if (status === "error") {
    // Show message and let the user retry.
  }
});
```

The event uses one shared shape for every outcome: `{ type, flow, kind, status, accountId?, message? }`.

* `flow` is `"add"` (adding a new account) or `"verify"` (verifying an existing one - Plaid only; see [Verify links](#verify-links)).
* `kind` is `"plaid"` or `"card"`.
* `status` is `"success"`, `"cancelled"`, or `"error"`.
* `accountId` is present **only** when `status` is `"success"`; `message` is present **only** when `status` is `"error"`.

Not every combination occurs. The events that can actually fire are:

| flow     | kind    | success | cancelled | error |
| -------- | ------- | :-----: | :-------: | :---: |
| `add`    | `plaid` |    ✅    |     ✅     |   ✅   |
| `add`    | `card`  |    ✅    |     ❌     |   ✅   |
| `verify` | `plaid` |    ✅    |     ✅     |   ✅   |

The card flow has no `cancelled` event (there's no Plaid Link to exit), and there is no `verify` + `card` combination.

<Note>
  As a best practice, also verify `event.origin` matches Melio's hosted domain before acting on the message.
</Note>

### Success

The user completed the flow. `accountId` is the new `acct_...` internal account. (A Plaid-backed account is returned with `type: "ach"`.)

```json theme={null}
{
  "type": "melio:accounts:internal",
  "flow": "add",
  "kind": "plaid",
  "status": "success",
  "accountId": "acct_71b271cc-0977-4279-bc51-b24982926ea1"
}
```

For a Plaid bank account the account is either **instantly verified**, or returned as `isVerified: false` with micro-deposits to follow (complete it later with a verify link - see below). You can't originate a payment from an unverified account.

### Cancelled (the exit event)

This is the exit event, and it's **Plaid only**. If the user exits Plaid Link inside the flow - closes it, taps back, or abandons a step - the flow reports `cancelled`. There is no `accountId`, and no account is created. The card flow never emits this status.

```json theme={null}
{
  "type": "melio:accounts:internal",
  "flow": "add",
  "kind": "plaid",
  "status": "cancelled"
}
```

<Warning>
  A `cancelled` event only arrives if the exit happens **inside** the flow. If the user closes the browser or tab entirely, or loses connectivity, **no window message is delivered at all**. Never rely on the window message alone: reconcile with the `api.account.created` webhook, or poll `GET /accounts`. If no account was created, the flow did not complete, and you can offer the user a fresh link.
</Warning>

### Error

Something went wrong during the flow. `message` describes what; let the user retry.

```json theme={null}
{
  "type": "melio:accounts:internal",
  "flow": "add",
  "kind": "card",
  "status": "error",
  "message": "..."
}
```

## When the account is created, and the webhooks

**Melio creates the internal account** when the user completes the hosted flow (for Plaid, after they select an account and confirm; for card, after the card is tokenized). You do not call a create endpoint yourself.

The window message is a UI signal; the webhooks are the source of truth. Subscribe to:

| Webhook               | Fires when                                                           | Payload                                                                      |
| --------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `api.account.created` | The internal account is created                                      | `ownershipType` (`internal`) and `accountType`                               |
| `api.account.updated` | The account changes, including when a Plaid account becomes verified | No body - fetch `GET /accounts/{accountId}` to read the current `isVerified` |

So the reliable pattern is: on the `success` message, reconcile against `api.account.created` (or `GET /accounts`); then watch `api.account.updated` to know when an unverified account clears micro-deposit verification.

**How this affects status:**

* **Completed (success):** the internal account exists. `isVerified` is `true` (usable right away) or `false` (micro-deposits pending - not usable for payments until it verifies).
* **Cancelled or browser closed:** no account is created and no status changes. Nothing to clean up.

## Get and use the account

On `success`, use the `accountId` to fetch the full account:

```http theme={null}
GET /v2/accounts/acct_71b271cc-0977-4279-bc51-b24982926ea1
api-key: YOUR_API_KEY
Melio-Entity-Id: ent_4kQz9mXpR2wL
```

```json theme={null}
{
  "id": "acct_71b271cc-0977-4279-bc51-b24982926ea1",
  "ownershipType": "internal",
  "type": "ach",
  "nickname": "Business checking",
  "isVerified": true,
  "bankAccount": {
    "accountType": "checking",
    "routingNumber": "011401533",
    "accountNumberLast4": "0000"
  },
  "createdAt": "2026-07-08T15:04:30Z",
  "updatedAt": "2026-07-08T15:04:30Z"
}
```

You can also list the entity's internal accounts with `GET /accounts?ownershipType=internal`.

Once `isVerified` is `true`, use the account as the funding source on a payment by passing its id as `originatingAccountId`:

```json theme={null}
{
  "originatingAccountId": "acct_71b271cc-0977-4279-bc51-b24982926ea1",
  "receivingAccountId": "acct_9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "amount": 125000,
  "deductionDate": "2026-07-10",
  "deliveryPreference": "standard-ach",
  "compliance": { "type": "goods-and-services" }
}
```

<Note>
  Set `externalId` on the account to map it to your own ledger id, then look it up later with `GET /accounts?externalId=...` - no need to store Melio ids.
</Note>

## Card flow

The card flow works the same way: mint the link with `type: "card"`, embed the `url`, and listen for the same `melio:accounts:internal` message (with `kind: "card"`). The hosted page shows PCI-compliant fields for the card number, expiry, and CVC, plus cardholder and billing address; the card is tokenized in the browser, so full card details never reach your servers.

Because there's no Plaid Link to exit, **the card flow emits only `success` or `error` - never `cancelled`.**

```json theme={null}
{
  "type": "melio:accounts:internal",
  "flow": "add",
  "kind": "card",
  "status": "success",
  "accountId": "acct_2d9b2f42-6061-4194-86ce-d49da9abae8a"
}
```

<Frame>
  <img src="https://mintcdn.com/melio/o94hw4-DRHnzxmpx/images/card-flow.gif?s=46989d2bc18d8844d036e06faffd963d" alt="Card Flow" width="1000" height="703" data-path="images/card-flow.gif" />
</Frame>

## Verify links

An internal Plaid account that came back unverified (`isVerified: false`) is completed with a verify link: `POST /accounts/{accountId}/verify/link`. It reuses the Plaid flow and emits the same message, with `flow: "verify"` and `kind: "plaid"` - `success`, `cancelled`, or `error`, just like the add flow. When it clears, `api.account.updated` fires.

```json theme={null}
{
  "type": "melio:accounts:internal",
  "flow": "verify",
  "kind": "plaid",
  "status": "success",
  "accountId": "acct_71b271cc-0977-4279-bc51-b24982926ea1"
}
```
