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

# Embed onboarding (iframe)

> Mount TagadaPay’s KYB form inside your product via iframe — partner-branded, same backend as dashboard onboarding.

# Embed onboarding (iframe)

<Note>
  **Available.** Mint a session with your Partner Key, mount the returned URL in an iframe (or via `onboarding.js`). The form is partner-branded from your partner profile. Contact your account manager if branding fields are empty.
</Note>

Drop TagadaPay’s full KYB / onboarding form **inside your own app**, styled with **your** logo and colors. Merchants never leave your product chrome. TagadaPay still runs requirements, document intake, underwriting hooks, and provisioning — the same systems as the dashboard form.

This is **not** a white-label of the entire TagadaPay dashboard. A full branded dashboard host (`appHost` / partner CRM branding) is a separate product. Embed onboarding is **only the KYB form**, in an iframe.

***

## When to use which route

| Route                                                    | Merchant experience            | Who builds the form         | Branding  |
| -------------------------------------------------------- | ------------------------------ | --------------------------- | --------- |
| **A** — [tagadapay.io/apply](https://tagadapay.io/apply) | Leaves your product            | TagadaPay                   | TagadaPay |
| **B** — SDK intake (`requirements` / `documents`)        | Stays in your product          | **You** (push data via API) | Yours     |
| **C — Embed onboarding (this page)**                     | Stays in your product (iframe) | **TagadaPay** (hosted form) | **Yours** |

* Prefer **C** when you want our form UX (smart-fill, document kinds, validation) without rebuilding KYB.
* Prefer **B** when you already collect every field/document in your own UI and only need to push bytes + values.
* Prefer **A** when you want zero UI work and accept a TagadaPay-branded redirect.

Processing partners and CRM-only partners can both use Route C. Economics and acquirer assignment still follow the same rules as [Accounts (TPAs)](/developer-tools/partners/accounts).

***

## High-level flow

```
Your backend                         TagadaPay                         Your frontend
────────────                         ─────────                         ─────────────
1. createSession(…)           ───►   session + embed URL
2. return url to client       ───────────────────────────────►   3. mount <iframe src=url>
                                                                  4. merchant completes KYB
                              ◄── postMessage: complete / exit ── 5. listen + unmount
6. provision / poll TPA       ◄── webhooks (optional)
```

1. Your **server** creates a short-lived onboarding session (Partner Key).
2. Your **client** mounts the returned URL in an iframe (helper below, or raw `<iframe>`).
3. The form runs on TagadaPay’s origin, themed with your partner branding.
4. On success / exit, the iframe notifies the parent via `postMessage`.
5. Your backend continues with the usual TPA lifecycle (`retrieve` / `provision` / webhooks).

***

## 1. Create a session (server)

Authenticate with your **Partner Key**. Create (or resume) a session bound to the merchant / TPA you are onboarding.

```ts theme={null}
const partner = new Tagada(process.env.TAGADA_PARTNER_KEY!);

const session = await partner.partners.processing.onboarding.createSession({
  // Link to a CRM merchant you already created (recommended)
  accountId: 'acc_xxx',
  // Or let Tagada create/reuse a TPA from your external id
  externalRef: 'merchant_42_eu',
  legalName: 'Acme SAS',
  country: 'FR',
  currency: 'EUR',
  // Optional: pin acquirer when you are a processing partner
  // acquirer: 'adyen',
  // Domains allowed to frame the embed (CSP frame-ancestors)
  allowedOrigins: ['https://app.yourproduct.com'],
  // Optional deep-link back into your UI after complete (also signaled via postMessage)
  returnUrl: 'https://app.yourproduct.com/merchants/42/processing',
});

// →
// {
//   object: 'onboarding_session',
//   id: 'ons_…',                    // short-lived iframe ticket
//   url: 'https://dashboard.tagadapay.io/embed/onboarding/ons_…?token=…',
//   expiresAt: '2026-07-25T16:00:00.000Z',
//   tpaId: 'tpa_xxx',
//   accountId: 'acc_xxx',
//   entityId: 'ent_…',              // durable resume key — store this
// }
```

<Warning>
  Never create sessions or expose session tokens in browser-callable code with your Partner Key. Mint the session on your backend, pass only `session.url` (or a short-lived client token) to the frontend.
</Warning>

### Session rules

* **Entity id is the durable resume key** — `entityId` on the session response is the same id used by dashboard `?resume=` and abandoned-recovery emails. Store it.
* **`ons_…` is short-lived** — regenerate with the same `entityId` / `externalRef` / `accountId` if the merchant abandons and comes back (`createSession` is idempotent on those keys).
* **One active embed URL per session** — do not share URLs across merchants.
* **`allowedOrigins`** — required for iframe embedding; Tagada sets `Content-Security-Policy: frame-ancestors …` from this list. Origins must be HTTPS (localhost allowed in test).

Resume examples:

```ts theme={null}
// Prefer entity id when you already have it (abandoned email / prior createSession)
const resumed = await partner.partners.processing.onboarding.createSession({
  entityId: 'ent_…',
  allowedOrigins: ['https://app.yourproduct.com'],
});

// Or same externalRef as the first create — finds the partner_embed draft
const resumedByRef = await partner.partners.processing.onboarding.createSession({
  externalRef: 'merchant_42_eu',
  legalName: 'Acme SAS',
  allowedOrigins: ['https://app.yourproduct.com'],
});
```

Abandoned recovery emails for embed drafts remint a fresh embed URL for the **same entity** (not a new application). Dashboard `/apply` drafts keep using `/merchant/requests/new?resume={entityId}`.

### Draft vs submitted (same as tagadapay.io/apply)

`createSession` creates a real `tpa_entities` row immediately (`status: draft`) and returns its `entityId`. Mid-form autosave updates that row. **Drafts are not in the TagadaPay ops inbox** — operators only see applications after the merchant **submits** (`status: submitted`). Until then the entity id is only a resume / abandoned-cart key, identical to the Tagada dashboard onboarding form.

***

## 2. Mount the iframe (client)

### Option A — JS helper (recommended)

```html theme={null}
<div id="tagada-onboarding"></div>
<script src="https://api.tagadapay.io/onboarding.js"></script>
<script>
  const { unmount } = TagadaOnboarding.mount('#tagada-onboarding', {
    url: sessionUrlFromYourBackend,
    onEvent(event) {
      // event.type: 'ready' | 'height' | 'complete' | 'exit' | 'error'
      if (event.type === 'height') {
        // optional: grow the iframe with event.height
      }
      if (event.type === 'complete') {
        // event.tpaId, event.accountId — continue in your app
        unmount();
      }
      if (event.type === 'exit') {
        unmount();
      }
    },
  });
</script>
```

### Option B — raw iframe + `postMessage`

```html theme={null}
<iframe
  id="tagada-onboarding"
  src="https://dashboard.tagadapay.io/embed/onboarding/ons_…?token=…"
  title="Business onboarding"
  allow="camera; microphone; clipboard-read; clipboard-write"
  style="width:100%; min-height:720px; border:0;"
></iframe>
<script>
  window.addEventListener('message', (e) => {
    if (e.origin !== 'https://dashboard.tagadapay.io') return;
    const msg = e.data;
    if (!msg || msg.source !== 'tagada.onboarding') return;
    // msg.type: ready | height | complete | exit | error
  });
</script>
```

### `postMessage` envelope

| Field       | Type                             | Notes                       |
| ----------- | -------------------------------- | --------------------------- |
| `source`    | `'tagada.onboarding'`            | Always filter on this       |
| `type`      | string                           | See events below            |
| `sessionId` | string                           | `ons_…`                     |
| `tpaId`     | string \| null                   | Set once the TPA exists     |
| `accountId` | string \| null                   | CRM merchant id when linked |
| `height`    | number \| undefined              | For `type: 'height'` (px)   |
| `error`     | `{ code, message }` \| undefined | For `type: 'error'`         |

### Events

| `type`     | When                                             | Your action                                   |
| ---------- | ------------------------------------------------ | --------------------------------------------- |
| `ready`    | Form painted                                     | Optional analytics                            |
| `height`   | Content height changed                           | Resize iframe                                 |
| `complete` | KYB submitted successfully                       | Unmount; poll TPA / call `provision` as usual |
| `exit`     | Merchant closed / abandoned                      | Unmount; session may still be resumable       |
| `error`    | Fatal embed error (expired token, origin denied) | Show fallback; create a fresh session         |

Always verify `event.origin` is `https://dashboard.tagadapay.io` before trusting the payload.

***

## 3. Branding

The embed uses your partner branding from the TagadaPay partner profile (same fields as CRM / invite emails):

| Field                          | Effect in the form                           |
| ------------------------------ | -------------------------------------------- |
| `appName`                      | Title / header product name                  |
| `logoUrl` / `logoDarkUrl`      | Header logo                                  |
| `primaryColor` / `accentColor` | Buttons, links, focus rings                  |
| `faviconUrl`                   | Browser tab when opened full-page (fallback) |
| `fontFamily`                   | UI typeface when set                         |

Configure branding in the partner admin (or via your TagadaPay contact). No per-session theme override is required for the default path — the session inherits the partner’s branding automatically.

<Tip>
  Embed onboarding is **form-only chrome**: no TagadaPay dashboard navigation. That keeps the iframe feeling native inside your product while the full white-label dashboard remains a separate enablement.
</Tip>

***

## 4. After the merchant completes

Embed submit feeds the **same** entity / TPA pipeline as dashboard / SDK applications:

1. The application lands in the ops entity inbox (same as `applications.create`).
2. After Tagada promotes / assigns: **payfac partners** call [`tpas.provision(tpaId)`](/developer-tools/partners/accounts#provision--send-everything-to-the-acquirer); **CRM-only** wait for operator assignment, then mint a key with `partners.processing.enroll({ accountId })`.
3. Subscribe to [partner webhooks](/developer-tools/partners/webhooks) for status changes if you do not want to poll.

Idempotent resume: if the merchant returns later, `createSession` with the same `entityId` (preferred) or `accountId` / `externalRef` returns a fresh URL for the **same** `tpa_entities` draft when the previous token expired. Mid-form progress is stamped on the entity (`onboardingProgress`) — identical to the Tagada dashboard form — so abandoned reminders and iframe remounts land on the same step.

***

## 5. Security checklist

* Mint sessions **server-side** with the Partner Key only.
* Pass **`allowedOrigins`** that match the page hosting the iframe (scheme + host + port).
* Ignore `postMessage` events whose `origin` is not the Tagada onboard host.
* Do not put Partner Keys, long-lived secrets, or raw session minting in mobile/WebView JS bundles.
* Treat `session.url` as a **capability URL** — do not log it in public analytics.

***

## 6. Relation to API-only intake (Route B)

* **CRM-only:** prefer `partners.processing.applications.create({ accountId, … })` (same payload as merchant applications) **or** the embed — not shell `tpas.create()` (`403 payfac_required`).
* **Payfac:** you can mix `requirements.update` / `documents.upload` with the embed on the same TPA, then `provision`.

Do **not** dual-submit the same document from both paths unless you intend to replace it — prefer one writer per requirement.

***

## See also

* [Accounts (TPAs) — requirements, documents, provision](/developer-tools/partners/accounts)
* [CRM merchants](/developer-tools/partners/merchants)
* [Webhooks](/developer-tools/partners/webhooks)
* [Partners overview](/developer-tools/partners/introduction)
