> ## 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.

# CRM provisioning (merchants)

> Create merchants (acc_xxx) and mint CRM Keys for the public API. A merchant needs no TPA — CRM-only is valid.

# CRM provisioning (merchants)

A **merchant** (`acc_xxx`) is the CRM organization: it owns stores, products, orders, customers, subscriptions, and **CRM Keys**. This page covers provisioning merchants and minting CRM Keys on the **CRM domain** (`/api/public/v1/partner/*`).

The full onboarding lifecycle, in one pass:

1. **Create** the merchant with your Partner Key (`merchants.create`), passing your own `externalRef` for idempotency and — if you want the client to log in — their `email`.
2. The contact receives an **invitation email fully branded as you** (your name, logo, colors — TagadaPay never appears), **sets their own password**, and lands on **your CRM host** as admin of their organization.
3. **Mint a CRM Key** (`merchants.keys.create`) whenever your backend needs to read or manage that merchant's data on their behalf.

<Note>
  **CRM-only is valid.** A merchant does **not** need a TPA. If your product only uses the merchant control plane (catalog, orders, customers, subscriptions) and processes payments elsewhere, you never have to touch the [Processing domain](/developer-tools/partners/accounts).

  When you *do* want TagadaPay to process cards: **CRM-only partners** submit an entity application (`partners.processing.applications.create({ accountId })`) or [embed](/developer-tools/partners/embed-onboarding); **payfac partners** use `tpas.create()`. See [Processing provisioning](/developer-tools/partners/accounts).
</Note>

***

## Create a merchant

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

const merchant = await partner.partners.crm.merchants.create({
  legalName: 'Acme SAS',
  country: 'FR',                // ISO 3166-1 alpha-2 (optional)
  currency: 'EUR',              // ISO 4217 (optional)
  externalRef: 'merchant_42',   // YOUR id — used for idempotency
  email: 'owner@acme.com',      // optional — invite the merchant to the dashboard
  metadata: { plan: 'pro' },
});

// →
// {
//   object: 'merchant',
//   id: 'acc_xxx',
//   legalName: 'Acme SAS',
//   externalRef: 'merchant_42',
//   country: null,        // not stored on the merchant — see note below
//   currency: null,
//   managementMode: 'partner_managed',
//   createdAt: '2026-04-29T…',
//   portalInvite: {       // present when `email` was supplied
//     object: 'merchant_portal_invite',
//     status: 'invited',        // 'added' when they already have an account with you
//     email: 'owner@acme.com',
//     clerkOrgId: 'org_…',
//     clerkUserId: null,
//     invitationId: 'orginv_…',
//     emailMode: 'branded',     // email carries YOUR partner branding
//   },
// }
```

Creating a merchant also auto-provisions a default **store** (`store_xxx`) so the CRM is immediately usable.

<Note>
  `country` / `currency` you pass on `create()` are **provisioning inputs** — `currency` sets the auto-created store's base currency. They are **not** stored on the merchant object itself, so the response always returns `country: null` and `currency: null`. Read per-store currency back via the CRM stores API.
</Note>

### Idempotency

A second `create()` with the same `externalRef` returns the **same** merchant — retries are safe:

```ts theme={null}
const a = await partner.partners.crm.merchants.create({ legalName: 'Acme', externalRef: 'merchant_42' });
const b = await partner.partners.crm.merchants.create({ legalName: 'Acme', externalRef: 'merchant_42' });
console.log(a.id === b.id); // true
```

The idempotency key is the `(partnerId, externalRef)` pair and is permanent. You can also pass it via the `Idempotency-Key` header — the body value wins.

***

## Invite the merchant to the dashboard

By default, a merchant you create by API is **API-only**: nobody can log in to it, and it does not appear in the CRM dashboard. To hand the account over to your client, pass `email` on `create()` (as above) or invite them later:

```ts theme={null}
const invite = await partner.partners.crm.merchants.invite('acc_xxx', {
  email: 'owner@acme.com',
});
// {
//   merchantId: 'acc_xxx',
//   object: 'merchant_portal_invite',
//   status: 'invited',            // see the four statuses below
//   email: 'owner@acme.com',
//   clerkOrgId: 'org_…',
//   clerkUserId: null,            // set once they are in the organization
//   invitationId: 'orginv_…',     // null unless status is 'invited'
//   emailMode: 'branded',         // sent with YOUR partner branding
// }
```

**If the email already has a TagadaPay account with one of your merchants, nobody is emailed.** We add that person to the merchant's organization on the spot and answer `status: 'added'`. There is no link, so there is nothing to expire, nothing to click, and no way for them to end up creating a second account by mistake. This is the case that used to hurt: a merchant who had signed up on their own — often into a duplicate organization — could only be reached through a link that had usually already died.

Everyone else gets the invitation flow: a **brand-new email**, and also an existing TagadaPay account that has no organization of yours. Adding someone to an organization is silent, so we only do it for people already inside your account; anyone else has to accept, which keeps you from attaching a stranger who never agreed to it.

1. The contact receives an **invitation email branded with your partner identity** (your name, logo and colors from your partner profile — TagadaPay does not appear anywhere in the email), with an accept link.
2. On acceptance they **choose their own password** and land in the CRM dashboard. Partners with a dedicated CRM host (e.g. `crm.yourbrand.com`) get the acceptance page skinned with their brand and the user is dropped onto **their** host, not TagadaPay's.
3. From that point the account behaves like a regular self-serve merchant — dashboard access, notifications, team management.

The four statuses:

| `status`          | Meaning                                                                                                   |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `added`           | Already had a TagadaPay account with one of your merchants — now in this organization too. No email sent. |
| `already_member`  | Was in the organization already. Nothing changed.                                                         |
| `invited`         | Brand-new email, or an account with no organization of yours — invitation sent.                           |
| `already_invited` | An invitation for that email is still pending. No second email sent.                                      |

The call is **idempotent** in every direction: repeat it as often as you like, it never fails for work that is already done. Note that sending a **new** invitation (e.g. after a previous one expired) invalidates earlier links — only the most recent email works.

### Developers and team members

`role` defaults to `admin`, the merchant's own contact. Pass `member` for the developers and team members you add alongside them:

```ts theme={null}
await partner.partners.crm.merchants.invite('acc_xxx', {
  email: 'dev@acme.com',
  role: 'member',
});
```

<Note>
  **One email, one person.** If several TagadaPay users share the address you pass, we refuse with `409 ambiguous_email` rather than guess which one to add to your merchant's organization. Contact your account manager to get them merged.
</Note>

<Note>
  **Branding comes from your partner profile.** The email name, logo, colors, dashboard host and reply-to address are configured once on your partner profile by your TagadaPay account manager. Until they're set, the email falls back to your partner name with a neutral look. Optionally, configure your own DKIM-verified sending domain so even the technical From address is yours.
</Note>

<Note>
  **Silent merchants stay silent.** If your partner agreement uses the silent-merchant model (Tagada never contacts your merchants directly — `merchantPortalAccess: 'never'`), invites are rejected with `403 portal_access_never`. Contact your account manager to change the policy.
</Note>

***

## Retrieve / list

```ts theme={null}
const merchant = await partner.partners.crm.merchants.retrieve('acc_xxx');
const byRef = await partner.partners.crm.merchants.retrieveByExternalRef('merchant_42'); // null if none

const { data, hasMore } = await partner.partners.crm.merchants.list({
  limit: 50,
  cursor: '…',
  externalRef: 'merchant_42', // optional filter
});
```

***

## Mint a CRM Key

A **CRM Key** (`sk_crm_…`) grants access to `/api/public/v1/*` for **this one merchant**.

```ts theme={null}
const key = await partner.partners.crm.merchants.keys.create('acc_xxx');
// {
//   object: 'crm_key',
//   id: '…',
//   accountId: 'acc_xxx',
//   prefix: 'sk_crm_live_a1b2…',
//   token: 'sk_crm_live_a1b2c3…',   // RETURNED ONCE — store immediately
//   name: 'Your Partner — merchant_42',
//   createdAt: '2026-04-29T…',
// }

const keys = await partner.partners.crm.merchants.keys.list('acc_xxx');
await partner.partners.crm.merchants.keys.revoke(key.id);
```

<Warning>
  **The `token` is returned only on creation.** Store it in your secret manager immediately. Existing UUID tokens keep working — only newly minted keys use the `sk_crm_…` format.
</Warning>

Use the token as a normal merchant client:

```ts theme={null}
const merchantClient = new Tagada(key.token);    // sk_crm_live_…
const orders = await merchantClient.orders.list();
const customers = await merchantClient.customers.list();
```

See [API keys & authentication](/developer-tools/partners/api-keys) for rotation patterns and the full three-key model.

***

## Query the merchant's data

Once you hold a CRM Key, the client behaves exactly like a merchant's own key — the whole CRM read surface is available on behalf of that one merchant (`acc_xxx`). This is what you use to build a merchant dashboard, reconcile orders, or pull reporting into your own backend.

```ts theme={null}
const m = new Tagada(crmKey.token); // sk_crm_live_…

// Stores (a CRM-only merchant has one auto-created store)
const stores = await m.stores.list({ pageSize: 20 });
const storeId = stores.data[0].id;

// Orders — POST /orders/list under the hood, so filter + sort are rich
const orders = await m.orders.list({
  pagination: { page: 1, pageSize: 25 },
  sortBy: { field: 'orders.createdAt', direction: 'desc' },
  filters: { 'orders.status': ['paid'] },      // optional
});
console.log(orders.total, 'orders');            // e.g. 6280

// Payments (with their transactions), and the currencies / processors in use
const payments = await m.payments.list({ pagination: { page: 1, pageSize: 25 } });
const { currencies } = await m.payments.currencies();   // ['USD']
const { processors } = await m.payments.processors();

// Customers
const customers = await m.customers.list({ pageSize: 25 });

// Catalog
const products = await m.products.list({ storeId, page: 1, per_page: 25 });

// Subscriptions (recurring)
const subs = await m.subscriptions.list({ page: 1, per_page: 25 });

// Funnels, payment flows, processors config
const { funnels } = await m.funnels.list(storeId);
const flows = await m.paymentFlows.list();
const procs = await m.processors.list();
```

<Note>
  **`events.list` on high-volume merchants.** The behavioural event stream (`app_events`) can be very large. Always scope `events.list` with a `storeId` and a tight date window, or use `events.statistics` / `events.recent` — an unfiltered `events.list` on a big merchant can time out. For heavy analytics, prefer aggregated reporting over paging raw events.
</Note>

Every listing returns the same envelope: `{ data, total, page, pageSize, totalPages, hasMore }`. Retrieve single records with `orders.retrieve(id)`, `payments.retrieve(id)`, `customers.retrieve(id)`, `products.retrieve(id)`, etc.

***

## Common errors

| HTTP | Code                                  | When                                                            |
| ---- | ------------------------------------- | --------------------------------------------------------------- |
| 401  | `missing_api_key` / `invalid_api_key` | No / unknown / revoked key                                      |
| 403  | `partner_scope_required`              | Not a Partner Key (e.g. a CRM/Processing key used to provision) |
| 403  | `merchant_access_denied`              | Merchant exists but belongs to another partner                  |
| 403  | `portal_access_never`                 | Your partner policy forbids CRM invites (silent-merchant model) |
| 404  | `merchant_not_found`                  | `acc_xxx` does not exist                                        |

***

## Next step

<Card title="Add card processing (TPA)" icon="building-columns" href="/developer-tools/partners/accounts">
  When this merchant needs to take payments through TagadaPay, provision a TPA bound to its `acc_xxx` and mint a Processing Key.
</Card>
