
# API for Agencies

As an agency, you can use the same REST API your clients use, but point individual requests at one of your managed **sub-accounts** instead of your own account. This lets you build tooling that onboards a client end to end — creating their campaigns, training their AI on a knowledge base, importing their contacts, connecting their messaging channels, and buying phone numbers — all without logging into each sub-account by hand.

This page covers only the agency-specific behaviour: how to act on behalf of a sub-account with the `sub_account_id` parameter. For the basics (generating a key, authentication, base URL, error format, rate limits), start with the [API Access](../integrations/api-access.md) guide. Everything there applies here too — you authenticate with **your agency account's** API key.

::: note
**Note:** This page is technical. If you are not a developer, share it with the person building your integration.
:::


***

## How "acting on behalf of" works

By default, every API request acts on the account that owns the API key — your agency account. To act on a managed client account instead, add the optional `sub_account_id` parameter to the request, set to that client's account id.

- **Omit `sub_account_id`** → the request acts on your own agency account.
- **Include `sub_account_id`** → the request acts on that sub-account, but only after the platform confirms the sub-account is really yours.

You always authenticate with your **agency account's** API key. You never need the sub-account's own key, and you never handle the sub-account's credentials.

### Where to put it

- **GET / DELETE endpoints** → pass it as a query parameter: `?sub_account_id=THE_SUB_ACCOUNT_ID` (alongside your `apiKey`, if you authenticate by query).
- **POST / PUT / PATCH endpoints** → include it in the JSON request body as `"sub_account_id": "THE_SUB_ACCOUNT_ID"`.
- **AI assistants** → nothing to configure. The [MCP server](../integrations/connect-ai-clients.md) carries the same setting on its read tools, so one connection with your agency key can report on every client: just name the client in your request ("how many contacts does Bella's Bistro have?"). Write actions are available too: every endpoint that accepts `sub_account_id` is exposed as a tool, so you can create, change and send on a client's behalf from the same connection.

### Finding a sub-account's id

The `sub_account_id` is the client account's unique id. You can get the list of your sub-accounts and their ids from the **SubAccounts** API endpoints (see the [Sub-Accounts](sub-accounts.md) guide) or from the **Sub Accounts** page in the sidebar.

***

## Ownership is always verified

When you pass a `sub_account_id`, the platform checks that the account is a real sub-account **and** that it belongs to your agency. Only then does the request go through.

If the id is unknown, is not a sub-account, or belongs to a different agency, the request fails with a **`404`** response:

```json
{
  "success": false,
  "error_code": 404,
  "error": "Sub-account not found."
}
```

> **Why 404 and not 403?** A "forbidden" response would tell an outsider that the id exists but isn't theirs. Returning the same `404` for "doesn't exist" and "isn't yours" means the endpoint can't be used to discover which account ids belong to other agencies. Treat a `404` here as "this isn't a sub-account you manage."

***

## Where `sub_account_id` is supported

`sub_account_id` is accepted on essentially every **resource** endpoint — any call that creates, reads, updates, or deletes an account's own data. In practice you can provision and run a sub-account's whole setup with your agency key:

- **AI setup** — campaigns, agents, FAQs, knowledge-base sources (website crawl **and** document upload), knowledge-base groups, broadcasts, custom functions, MCP servers
- **Contacts & CRM** — contacts (including import), lists, tags, tasks, deals, appointments, events
- **Channels & numbers** — connect WhatsApp / WhatsApp Web / Telegram / Instagram & Messenger / LINE, search / purchase / manage phone numbers, WhatsApp templates, channel routing
- **Messaging & content** — send messages, chat sessions, chat exports, daily summaries
- **Settings & integrations** — webhooks, chat-widget config, white-label config, BYOK SMS and other account settings, analytics

On every one of these the parameter is **optional** — leave it out and the call acts on your own agency account, so one integration serves both. Credits and usage always come from the account you target: charges for a sub-account's campaigns, messages, tags, and numbers hit **the sub-account's** balance.

### Where it does NOT apply

A few endpoints are agency-level or self-addressed and ignore `sub_account_id`:

- **Managing the sub-accounts themselves** — the SubAccounts endpoints (create / list / update a sub-account) and the BYOK spending-limit endpoint already name the sub-account in their own URL path. The [pricing and policy endpoints](#set-per-client-ai-pricing-and-policy) and [chat-monitoring endpoints](#read-a-sub-accounts-conversations) follow the same pattern.
- **Copying an Agent between accounts** — `POST /v1/subaccounts/agents/copy` names both accounts itself, taking the destination as `targetUserId`. See the [worked example](#worked-example-ship-a-template-agent-into-every-new-client) below. (The older `POST /v1/subaccounts/campaigns/copy` works the same way but is deprecated with the rest of the [Campaigns API](../api/campaigns.md).)
- **Adjusting credits, and the two agency-wide rollups** — [`POST /v1/subaccounts/credits`](#grant-or-deduct-credits-directly) identifies the sub-account by `email` instead; [`GET /v1/subaccounts/credit-usage`](#read-credit-usage-and-campaign-health-across-your-book) and `GET /v1/subaccounts/campaign-status` report on every sub-account at once, so there's no single account to target.
- **Your agency's own account** — API-key management, agency usage reporting, team management and your [pricing tiers](#manage-your-pricing-tiers-over-the-api) always act on your agency account.
- **Inbound message webhooks** — endpoints that external systems post *into* are tied to the account whose credentials configured them, so there's nothing to redirect.

> The always-current, machine-readable list of which parameters each endpoint accepts lives in your dashboard API reference (**Settings → Integrations → API Key**) and the OpenAPI spec at `GET /v1/docs/openapi.yaml`. We ship API changes often — treat those as the source of truth.

::: master-only
<figure><img src="../.gitbook/assets/v2-api-access-key-section.png" alt="API Key settings page with masked key and Regenerate control"><figcaption><p>Settings → Integrations → API Key — your agency's key lives here, alongside the link to the full API reference.</p></figcaption></figure>
:::

***

## Worked example: connect Instagram & Messenger for a sub-account

Connecting Instagram & Messenger is a browser-based flow. You start it with the API, hand the returned consent URL to the client (or open it for them), wait for them to authorize in their browser, then pick which page to connect — all while targeting their sub-account with `sub_account_id`.

### Step 1 — Start the connection

Call the connect endpoint with the client's `sub_account_id` in the body. No credentials are sent here; the platform returns a consent URL the client must open in a browser, plus a one-time correlation token.

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/channels/meta/connect?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sub_account_id": "abc123def456"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.dmchamp.com/v1/channels/meta/connect", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    sub_account_id: "abc123def456",
  }),
});

const data = await res.json();
// data.oauth_url -> open this in the client's browser
```

**Python**

```python
import requests

res = requests.post(
    "https://api.dmchamp.com/v1/channels/meta/connect",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "sub_account_id": "abc123def456",
    },
)

data = res.json()
# data["oauth_url"] -> open this in the client's browser
```

**Response:**

```json
{
  "success": true,
  "oauth_url": "https://www.facebook.com/v21.0/dialog/oauth?client_id=...&state=...",
  "state_token": "8sFq2yV0kQ7m4n1pZr3tWb6cXe9hJl2aD5gK7uN0oI",
  "expires_at": "2026-06-10T12:30:00.000Z"
}
```

Send the client to `oauth_url` in a browser to authorize. The `state_token` correlates this attempt and is a short-lived secret — don't log it. The attempt expires at `expires_at`; if it lapses, start again.

### Step 2 — Poll until the pages load

After the client authorizes, poll the status endpoint (with the same `sub_account_id`, this time as a query parameter) until the connectable pages appear.

**cURL**

```bash
curl "https://api.dmchamp.com/v1/channels/meta/status?apiKey=YOUR_API_KEY&sub_account_id=abc123def456"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.dmchamp.com/v1/channels/meta/status?sub_account_id=abc123def456",
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);

const data = await res.json();
// Wait until data.status === "pages_loaded", then read data.pages
```

**Python**

```python
import requests

res = requests.get(
    "https://api.dmchamp.com/v1/channels/meta/status",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"sub_account_id": "abc123def456"},
)

data = res.json()
# Wait until data["status"] == "pages_loaded", then read data["pages"]
```

**Response:**

```json
{
  "success": true,
  "status": "pages_loaded",
  "pages": [
    {
      "id": "1098765432101234",
      "name": "Acme Studio",
      "category": "Hair Salon",
      "instagram_business_account": {
        "id": "17841400000000000",
        "username": "acme.studio"
      }
    }
  ],
  "selected_page": null
}
```

The `status` field moves through `pending` → `token_received` → `pages_loaded` → `connected`. Wait for `pages_loaded` before selecting a page. Two terminal error states can also appear instead of progressing: `failed` and `expired` (the client declined consent, or the state token's ~30-minute window lapsed) — a `reason` field is included when either occurs. Stop polling and restart at Step 1 if you see one; don't wait on `pending` forever. Page access tokens are never returned.

### Step 3 — Select the page to connect

Pick one of the page ids from Step 2 and select it. Selecting a page connects both Instagram and Messenger for that page. Include `sub_account_id` in the body again.

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/channels/meta/select-page?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "page_id": "1098765432101234",
    "sub_account_id": "abc123def456"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.dmchamp.com/v1/channels/meta/select-page", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    page_id: "1098765432101234",
    sub_account_id: "abc123def456",
  }),
});

const data = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.dmchamp.com/v1/channels/meta/select-page",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "page_id": "1098765432101234",
        "sub_account_id": "abc123def456",
    },
)

data = res.json()
```

**Response:**

```json
{
  "success": true,
  "page_id": "1098765432101234",
  "instagram_business_account_id": "17841400000000000"
}
```

That's it — Instagram and Messenger are now connected on the client's sub-account. You only ever supplied the `page_id`; the underlying credential is resolved on the server and never passed through your integration.

***

## Worked example: buy a number for a sub-account

Purchasing a number works the same way: search with `sub_account_id` in the query, then purchase with it in the body. Credits are deducted from **the sub-account's** balance, and the number is provisioned on the sub-account.

**Search (cURL):**

```bash
curl "https://api.dmchamp.com/v1/phone-numbers/available?apiKey=YOUR_API_KEY&country_code=US&sub_account_id=abc123def456"
```

**Purchase (JavaScript):**

```javascript
const res = await fetch("https://api.dmchamp.com/v1/phone-numbers", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phone_number: "+14155551234",
    country_code: "US",
    display_name: "Support line",
    sub_account_id: "abc123def456",
  }),
});

const data = await res.json();
```

**Purchase (Python):**

```python
import requests

res = requests.post(
    "https://api.dmchamp.com/v1/phone-numbers",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "phone_number": "+14155551234",
        "country_code": "US",
        "display_name": "Support line",
        "sub_account_id": "abc123def456",
    },
)

data = res.json()
```

**Response:**

```json
{
  "success": true,
  "phone_number": "+14155551234",
  "channel": "whatsapp",
  "whatsapp_status": "PURCHASED",
  "outgoing_status": "PURCHASED",
  "status": "PURCHASED",
  "purchase_credits": 11.5,
  "monthly_credits": 11.5
}
```

The number is provisioned in the `PURCHASED` state and WhatsApp sender registration continues in the background. Poll `GET /v1/phone-numbers/{phoneNumber}/status?sub_account_id=abc123def456` until the status reaches `ONLINE` before sending.

***

## Worked example: ship a template Agent into every new client

The usual agency pattern is to keep one master Agent on your agency account, tuned the way you want every client to start, and stamp a copy of it into each new sub-account at provisioning time. That is three calls, and nothing needs repeating afterwards: the copy keeps its settings until you change them.

### Step 1 — Copy the Agent in

`POST /v1/subaccounts/agents/copy`

```bash
curl -X POST "https://api.dmchamp.com/v1/subaccounts/agents/copy?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "YOUR_TEMPLATE_AGENT_ID",
    "targetUserId": "abc123def456",
    "newName": "Inbound Instagram Leads",
    "copyFaqs": true
  }'
```

The response carries the new Agent's id at `data.agent_id`. The FAQs, knowledge base and media library come along; the source account's WhatsApp templates, connected social posts and contacts deliberately do not. Full field list in the [AI Agents API](../api/agents.md#copy-an-agent-into-a-sub-account-agencies).

Note this endpoint takes `targetUserId` rather than `sub_account_id` — it names both accounts itself. The two calls below use the normal `sub_account_id` parameter.

### Step 2 — Switch it on

The copy always arrives paused, so it cannot message anyone until you say so. This is also the moment to pin the AI tier you want the client on; it stays there, so there is no need to re-apply it on a schedule.

```bash
curl -X PATCH "https://api.dmchamp.com/v1/agents/NEW_AGENT_ID/active?apiKey=YOUR_API_KEY&sub_account_id=abc123def456" \
  -H "Content-Type: application/json" \
  -d '{ "active": true }'

curl -X PUT "https://api.dmchamp.com/v1/agents/NEW_AGENT_ID?apiKey=YOUR_API_KEY&sub_account_id=abc123def456" \
  -H "Content-Type: application/json" \
  -d '{ "anthropic_model": "max" }'
```

To stop the client changing the tier afterwards, [lock the allowed tiers](#set-per-client-ai-pricing-and-policy) on the sub-account instead of re-sending the value.

### Step 3 — Point the client's channels at it

The copy arrives with no routing either, so nothing reaches it until you make it the answerer on the channels the client has connected. One call per channel:

```bash
curl -X PUT "https://api.dmchamp.com/v1/entry-points/channel-defaults?apiKey=YOUR_API_KEY&sub_account_id=abc123def456" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "instagram", "agent_id": "NEW_AGENT_ID" }'
```

From here, a first-time message from an unknown contact on that channel is picked up by the copied Agent automatically. See [Point a channel at an Agent](../api/entry-points.md#point-a-channel-at-an-agent) for the other channels and per-number routing.

> **Set the client's time zone when you create the sub-account.** Pass `time_zone_id` on `POST /v1/subaccounts`. Campaign active hours are evaluated in the sub-account's own time zone, so a client created without one has its schedule read against UTC — which quietly shifts when the assistant is allowed to reply.

***

## Skip the setup wizard for a client you configure yourself

`POST /v1/subaccounts`

By default, the first time a new sub-account owner signs in they are walked through the guided Setup Wizard. For done-for-you clients — where you build the campaign and connect the channels before the client ever logs in — pass `guided_onboarding: false` when you create the account. They land on the dashboard instead, and the **Setup Wizard** entry is hidden from their sidebar.

```bash
curl -X POST "https://api.dmchamp.com/v1/subaccounts" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "client@example.com",
    "first_name": "Alex",
    "last_name": "Client",
    "business_name": "Client Co",
    "guided_onboarding": false,
    "usage_limits": { "monthly_credits": 500 }
  }'
```

Omit the field (or send `true`) and the wizard behaves exactly as it always has, so existing integrations need no change. To give a client the wizard back later, re-show the `guided_onboarding` item with `PUT /v1/subaccounts/{subAccountUid}/menu-visibility` (below) — menu visibility controls whether the wizard is reachable, `guided_onboarding` controls only the first-sign-in redirect.

***

## Turn Tasks, Daily Summaries or the Media Library off for a client

`POST /v1/subaccounts`

These three are on for every new client unless you say otherwise, and they behave differently from every other feature in this guide: they are **opt-out**, not opt-in. Leaving them out of `features` is not enough on its own, because an older integration's `features` list simply never mentioned them — we can't tell "the agency switched this off" from "this list was written before the option existed".

So state it outright with `feature_settings`:

```bash
curl -X POST "https://api.dmchamp.com/v1/subaccounts" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "client@example.com",
    "first_name": "Alex",
    "last_name": "Client",
    "business_name": "Client Co",
    "feature_settings": {
      "tasks": false,
      "daily_summaries": false,
      "ai_media_library": true
    }
  }'
```

Every key is optional; anything you leave out stays on. With `tasks: false` the AI stops creating tasks for that client and no "New Task Created" emails go out; with `daily_summaries: false` the nightly summary is never generated or emailed.

`feature_settings` is the only thing that switches these three off at creation. Leaving them out of `features` does nothing on its own, no matter how the rest of your list looks — that is deliberate, so an older integration doesn't silently lose all three.

To change any of this afterwards, send the full `features` list to `PUT /v1/subaccounts/{subAccountUid}/features` — there, presence in the list turns a feature on and absence turns it off.

***

## Auto-login your clients into their sub-account (SSO)

`POST /v1/subaccounts/{subAccountUid}/sso-link`

One call with your agency API key returns a ready-to-open URL that logs the client straight into their own sub-account — no login screen, no password step, nothing to build on top. Open it in a new tab, a redirect, or an iframe inside your own product.

| Field | Required | Description |
|---|---|---|
| `redirect` | No | In-app page you want the client to end up on, e.g. `"/chats"` or `"/agents"`. Returned as `deep_link_url` in the response. |
| `app_base_url` | No | Dashboard host for the link. Defaults to your white-label app domain (or the platform domain if you have none). Must be `https`. |

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/sso-link" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "redirect": "/chats" }'
```

**Response**

```json
{
  "success": true,
  "url": "https://app.yourdomain.com/auth?redirect=%2Fchats#token=eyJhbGciOi…",
  "deep_link_url": "https://app.yourdomain.com/chats",
  "expires_at": "2026-07-22T15:04:05.000Z",
  "sub_account_uid": "SUB_ACCOUNT_UID"
}
```

How to use it well:

- **One hop.** Opening `url` signs the client in and lands them directly on the `redirect` page of the dashboard — no login screen, no intermediate page. `deep_link_url` names the same destination, for integrators that prefer navigating a frame explicitly after login; once the session exists, any dashboard path works in that browser context.
- **Mint on demand, open immediately.** The link contains a login credential and expires after about an hour. Request it server-side at the moment the client clicks, and never store or email it.
- The login token travels in the URL fragment (`#…`), which browsers never send to servers, and it is removed from the address bar the moment it's consumed.
- **Only your own sub-accounts.** The endpoint refuses any account your agency doesn't own.
- An expired link shows a clear error with a retry path — mint a fresh one.

***

## Hide navigation items on a sub-account

`PUT /v1/subaccounts/{subAccountUid}/menu-visibility`

Controls which sidebar and settings items a sub-account sees — useful when you embed the dashboard and want only the surfaces your product doesn't already cover. Anything not listed stays visible; send `null` as the whole `menuVisibility` value to reset everything to visible. Hiding an item hides the menu entry — pair it with the features you grant the sub-account for hard gating.

**cURL**

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/menu-visibility" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "menuVisibility": {
      "side_nav": { "Dashboard": false, "Campaigns": false, "Automations": false },
      "settings_nav": { "team": false }
    }
  }'
```

**Response**

```json
{
  "success": true,
  "data": {
    "subAccountUid": "SUB_ACCOUNT_UID",
    "menuVisibility": {
      "side_nav": { "Dashboard": false, "Campaigns": false, "Automations": false },
      "settings_nav": { "team": false }
    }
  }
}
```

`side_nav` accepts these 13 keys, which match the sidebar item names: `Dashboard`, `DailySummaries`, `Chats`, `Contacts`, `Deals`, `Tasks`, `Automations`, `Campaigns`, `Appointments`, `Settings`, `Help`, `CreditsCounter` (the credit balance shown in the sidebar) and `guided_onboarding` (the Setup Wizard). Three further keys — `AiInsights`, `Sub Accounts` and `Agency Reselling` — are accepted but do nothing: they only ever applied to the retired classic dashboard, so setting them has no effect on your sub-accounts. Missing keys mean visible; when you sign in to the sub-account yourself, hidden items are temporarily shown so you can always change things back.

Hiding a page from the menu never grants access to it. `Automations` needs the `automations` feature granted on the sub-account — set the key to `true` without it and the page still won't appear. `Tasks` and `DailySummaries` work the other way round: they are on for every client unless you switch them off (see [Turn Tasks, Daily Summaries or the Media Library off for a client](#turn-tasks-daily-summaries-or-the-media-library-off-for-a-client)).

***

## Choose which channel types a client can connect

`PUT /v1/subaccounts/{subAccountUid}/features`

The **Channel Types** switches you see on a plan tier are ordinary feature IDs, so you can set them per client from the API instead of the dashboard. This is one of the endpoints that names the sub-account in its own URL, so it takes no `sub_account_id`.

| Feature ID | Channel |
|---|---|
| `channel_chat_widget` | Website Chat Widget |
| `channel_whatsapp_api` | WhatsApp Business API |
| `channel_whatsapp_web` | WhatsApp Web (QR-linked number) |
| `channel_instagram` | Instagram |
| `channel_messenger` | Facebook Messenger |
| `channel_telegram` | Telegram |
| `channel_line` | LINE |
| `channel_viber` | Viber |
| `channel_email` | Email mailbox |
| `channel_sms` | SMS |
| `channel_imessage` | iMessage |

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/features" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "features": [
      "channels_3",
      "channel_chat_widget",
      "channel_whatsapp_web",
      "channel_instagram",
      "image_understanding",
      "contact_tagging",
      "incoming_campaigns",
      "webhooks"
    ]
  }'
```

Three things to get right:

- **The call replaces the whole feature list.** Send every feature the client should keep, not only the ones you're changing. The same IDs work as `features` on `POST /v1/subaccounts` when you create the account.
- **Channel types and channel count are separate gates, and both apply.** `channels_1` / `channels_3` / `channels_unlimited` control *how many* connections; the `channel_*` IDs control *which types*. The example above means "up to 3 connections, and only Chat Widget, WhatsApp Web or Instagram".
- **Sending no `channel_*` IDs at all means no channel restriction.** That's the original behaviour, which is why existing clients were unaffected when this shipped. Send one or more and everything else shows as locked on the client's Channels page with an upgrade note instead of a Connect button. Channels the client already connected keep working.

> Setting the channel list on a **plan tier**, so every client who buys that tier inherits it, is done in the dashboard under your agency plan settings. This endpoint sets it on one specific sub-account.

***

## Set an exact team-member limit for a client

`PUT /v1/subaccounts/{subAccountUid}/limits`

The `team_seats_*` features only offer preset ladder steps (3 / 5 / 10 / unlimited). To give a client an **exact** number of team seats — 2, 7, 15, anything — set `usage_limits.team_seats_limit` instead. It wins over the presets, and the platform enforces it on every invite, direct add and invite acceptance: once the limit is reached, further invitations are refused server-side.

- A positive integer is the exact cap.
- `0` means team members are **not included** — the client cannot invite anyone.
- `-1` means unlimited.
- `null` clears the custom limit and falls back to whichever `team_seats_*` preset is on the feature list.

Lowering the limit never removes existing team members; it only stops new ones being added.

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/limits" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "usageLimits": { "team_seats_limit": 7 }
  }'
```

You can also set it at creation time: `POST /v1/subaccounts` accepts `usage_limits.team_seats_limit` with the same semantics. To read the current value, fetch the sub-account with `GET /v1/subaccounts?email=...` and look at `usage_limits.team_seats_limit` (absent/`null` = the presets decide). The same endpoint also updates `credits`, `monthly_credits`, `roll_over_to_next_month`, `rollover_cap_months`, `rollover_expiry_days` and `byok_monthly_limit_usd` — send only the keys you want to change.

**How this interacts with SaaS plan seat limits.** Your SaaS plans can carry their own seat allowance (set in the plan editor — see [Team seats on a plan](agency-accounts.md#step-3--set-up-pricing-tiers)), which is applied automatically when a client subscribes. A limit you set through this endpoint counts as a **manual** grant: buying a plan replaces it with the plan's own seat allowance (that purchase is an explicit plan choice), but unattended monthly **renewals never overwrite a manual limit** — so a one-off exception you grant a client survives their billing cycle. Clearing the manual limit with `null` hands the field back to the plan at its next renewal.

**Cap what a client carries between renewals.** Two more `usage_limits` keys sit next to `roll_over_to_next_month`. Both are accepted by `POST /v1/subaccounts` at creation time as well, and `null` clears either one.

| Key | What it does |
|---|---|
| `rollover_cap_months` | Months of allowance the client may keep. A number from 0 to 120, fractions allowed (`0.5` = half a month). At each renewal the unused balance is trimmed to at most this many times the allowance that renewal grants, before the new credits are added; `0` carries nothing over. |
| `rollover_expiry_days` | A whole number of days, 1 to 3650. Credits left unused that long are dropped at the first renewal after they reach that age. Spending always comes off the oldest credits first, so a client who spends their allowance each month never loses any. |

Left unset, both fall back to the client's plan; a value sent here wins over the plan's. Only recurring credits (the monthly allowance and plan credits) are subject to them: top-ups, auto-recharges and one-time additions are never capped or expired. Each trim is written to the client's credit history as a **Rollover Cap Credit Adjustment** or an **Expired Credits Credit Adjustment** and never counts as usage. The plan-level equivalents are `rollover_cap_months` and `rollover_expiry_days` on a pricing tier — see [The fields on a tier](#the-fields-on-a-tier) and [Capping what rolls over](sub-accounts.md#capping-what-rolls-over).

***

## Set per-client AI pricing and policy

`PUT /v1/subaccounts/{subAccountUid}/max-tier` · `/ai-tiers` · `/max-rate` · `/action-pricing` · `/insider-rate` · `/locked-bot-fields` · `/notifications` · `/zero-credit-reply`

Eight more per-client switches, alongside `/limits`, `/features` and `/menu-visibility` above. Each takes the sub-account's uid in the URL (no `sub_account_id` body/query parameter — the target is already named in the path) and is scoped the same way: your agency key, and the sub-account must belong to your agency.

**Which AI models a client can use**

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/max-tier" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
```

`{ "enabled": boolean }` opts the client into (or out of) the Max AI tier — our infrastructure at the platform's list price. Turning this on for a BYOK client changes their AI cost from "free on my own key" to "charged against my credit pool," so it's a deliberate per-client decision rather than an agency-wide default.

To restrict WHICH tiers a client's campaigns and Agents may pick from at all (rather than just gating Max), use `ai-tiers`:

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/ai-tiers" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "allowed_ai_tiers": ["standard", "economy"] }'
```

`allowed_ai_tiers` is an array drawn from `standard`, `economy`, `max`, `mini` — it REPLACES the client's allow-list. Send `null` (or `[]`) to clear the restriction and let them pick any tier. This matters because a sub-account choosing its own AI tier spends from **your** credit pool, so it's the lever for pinning which models a reseller client may run up your bill on.

**A holding reply while the client is out of credits**

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/zero-credit-reply" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true, "message": "Thanks for your message, we will get back to you shortly." }'
```

When the client's balance (or your pool) is empty the AI cannot answer and the contact hears nothing. With `enabled: true`, every contact who writes in during the outage gets `message` once (max 500 characters, sent as-is on every channel), and the AI answers those conversations for real once credits are back. `enabled: false` keeps the saved text for later; `enabled: false` with no `message` removes the setting. Same switch as **Holding reply when out of credits** in the sub-account's Edit modal — see [A holding reply while a client is out of credits](sub-accounts.md#a-holding-reply-while-a-client-is-out-of-credits).

**What a client pays per AI action, and the WhatsApp fee markup**

Two ways to set your client-facing rate, from simplest to most granular:

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/max-rate" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rate": 0.35 }'
```

`rate` is the price in credits the sub-account's OWN balance burns per Max-model AI action — your client-facing markup on top of what your pool actually pays. `null` clears the override back to platform list price. The rate must be at least what a Max action costs your own pool (so you can never price a client below your cost) and no more than 10 credits; a request outside that window is rejected with the computed floor in the error message.

For per-action-type pricing instead of one flat Max rate, use `action-pricing`:

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/action-pricing" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "actionPricing": {
      "AI_MESSAGE": 0.6,
      "CHAT_SUMMARY": 0.15,
      "wa_carrier_multiplier": null
    }
  }'
```

`actionPricing` is a MERGE onto the client's existing map — a key you don't mention is left as it was, and `null` unsets that key back to its default. The recognised keys:

| Key | Prices |
|---|---|
| `AI_MESSAGE` | An AI reply |
| `AI_TOOL_USE` | An AI tool call |
| `EVALUATION_CALL` | A chat evaluation pass |
| `INTERRUPTION_HANDLING` | Handling an interruption mid-reply |
| `CONTACT_TAG` | An AI-assigned contact tag |
| `CHAT_SUMMARY` | A chat summary |
| `wa_carrier_multiplier` | A markup multiplier applied to every non-AI WhatsApp fee the client pays: monthly number rent, managed-lane delivery fees, and Meta/Twilio template pass-through costs. |

Per-action rates must be a number greater than 0 and up to 10; `wa_carrier_multiplier` must be at least `1` (no discount below cost) and up to 10. Sending an unrecognised key, or a value out of range, rejects the WHOLE request and names every offending key, so a typo can never silently save a price that isn't actually applied.

If you're a [Champions Circle](https://skool.com/dm-champions) member, `insider-rate` passes your 20%-off Max/Lead Finder rate down to one client instead of applying it agency-wide:

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/insider-rate" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
```

Turning it on requires your own agency account to actually hold Circle membership; turning it off never does, so a lapsed member can always wind a client back down.

**Lock sections of a client's playbook**

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/locked-bot-fields" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "locked_bot_fields": ["instructions", "rules"] }'
```

`locked_bot_fields` is an array drawn from `instructions`, `goal`, `rules`, `personality`, `conclude_unless` — it REPLACES the client's locked list. A locked section is rejected server-side if the SUB-ACCOUNT itself tries to change it (directly, or by API key), while you (via `sub_account_id`) and the client's own dashboard admin view can still edit anything. Send `null` (or `[]`) to unlock everything. Useful for done-for-you clients where you own the playbook and get judged on the result.

**Set a client's notification preferences on their behalf**

```bash
curl -X PUT "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/notifications" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notifications": {
      "settings": {
        "credit_alerts": { "enabled": true, "channels": ["email", "in_app"] },
        "new_contacts": { "enabled": false }
      }
    }
  }'
```

`notifications` replaces the client's whole notification preference set (not a per-key merge — send every category you want kept, matching how the sub-account's own Settings page saves). Each category under `settings` accepts `enabled` (boolean) and up to three `channels` from `email`, `in_app`, `webhook`. Send `null` to reset to platform defaults.

All seven endpoints respond `{ "success": true, "data": { "subAccountUid": "...", ...the field(s) you set... } }`, and are audit-logged with the before/after value. Common errors: `403` if your account isn't Agency/Dev or the sub-account isn't yours to manage, `400` if it is not an agency sub-account or a value is out of range.

***

## Pause a client who has suspended their subscription

`POST /v1/subaccounts/{subAccountUid}/pause` · `POST /v1/subaccounts/{subAccountUid}/unpause`

When a client suspends their subscription with you, pause their account instead of deleting it: everything they send stops immediately — outbound messages, broadcasts, AI replies on every channel — and when they sign in they see a full-screen **Account paused** lock (with your optional message) instead of the app. Nothing is deleted or disconnected: agents, campaigns, connected channels, contacts and chat history all stay exactly as they are, so unpausing puts the client back precisely where they left off — no setup to redo.

| Field | Required | Description |
|---|---|---|
| `message` | No | Shown to the client on their lock screen. Leave it out for the default wording. |
| `reason` | No | Agency-internal note stored with the pause and in the audit log — never shown to the client. |

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/pause" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Your account is on hold — contact us to reactivate it.", "reason": "Subscription suspended per client email" }'
```

**Response**

```json
{
  "success": true,
  "data": {
    "subAccountUid": "SUB_ACCOUNT_UID",
    "paused": true,
    "level": "hard_blocked"
  }
}
```

When the client comes back, `POST /v1/subaccounts/SUB_ACCOUNT_UID/unpause` (no body) lifts the lock — sending and AI replies resume straight away.

Worth knowing:

- **It's the same state as the dashboard's Hard blocked toggle** ([Blocking / Pausing a Sub-Account](sub-accounts.md#blocking-pausing-a-sub-account)) — a client paused over the API shows as blocked in the dashboard and vice versa, and unpause clears a block placed from either side. The current state is readable from the `agency_block` field on `GET /v1/subaccounts` (`level` of `"none"`, `"soft_blocked"` or `"hard_blocked"`).
- **Both calls are idempotent.** Pausing an already-paused client just refreshes the message, reason and timestamp; unpausing an active client changes nothing.
- **The client is not emailed automatically** — many agencies white-label, so telling the client is left to you.
- **Your own DM Champ billing is untouched.** Pausing a client only affects your relationship with them.
- **AI assistants can do this too**: the [MCP server](../integrations/connect-ai-clients.md) exposes these endpoints as the `pause_subaccount` and `unpause_subaccount` tools.

***

## Grant or deduct credits directly

`POST /v1/subaccounts/credits`

Adds or removes an exact amount of credits from one sub-account's balance — the API equivalent of the dashboard's manual credit adjustment. This is a one-off balance change, distinct from the recurring `monthly_credits`, `roll_over_to_next_month`, `rollover_cap_months` and `rollover_expiry_days` settings on [`PUT /v1/subaccounts/{subAccountUid}/limits`](#set-an-exact-team-member-limit-for-a-client).

This is the one endpoint on this page that identifies the sub-account by **email** rather than `sub_account_id`.

| Field | Required | Description |
|---|---|---|
| `email` | Yes | The sub-account's email, as it exists under your agency. |
| `amount` | Yes | Non-zero number of credits. Positive adds, negative deducts. |
| `description` | No | Shown against the adjustment in the client's credit history. Defaults to a generic "Adjusted by agency via API" line. |

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/subaccounts/credits" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "client@example.com", "amount": 500, "description": "Q3 bonus credits" }'
```

**Response**

```json
{
  "success": true,
  "data": {
    "email": "client@example.com",
    "previous_balance": 1200,
    "adjustment": 500,
    "new_balance": 1700
  }
}
```

A negative `amount` that would take the balance below zero is refused with `400`, telling you the available balance and what you tried to deduct. If the client is on their own Stripe billing (reselling mode), an added amount also counts as credits they purchased, so it survives their next monthly reset the same way a real top-up would; on a standard allocated client it's treated as part of their recurring allowance instead. Either way they are a one-time addition, so a roll-over cap or an expiry set on the account (or its plan) never trims them — only the recurring allowance and plan credits are subject to those.

***

## Read a sub-account's conversations

`GET /v1/subaccounts/{subAccountUid}/chats` · `GET /v1/subaccounts/{subAccountUid}/chats/{contactId}/messages`

Lets you build a monitoring or support view of a client's conversations without signing into their account. First list their contacts with a preview of the latest message, then read one contact's full message history.

**List contacts**

```bash
curl "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/chats?apiKey=YOUR_AGENCY_API_KEY&pageSize=25"
```

| Query parameter | Required | Description |
|---|---|---|
| `pageSize` | No | Contacts per page. Default 25, maximum 50. |
| `lastActivityAt` | No | Pagination cursor — pass the previous page's `lastActivityAt` to continue. |
| `searchQuery` | No | Filter by contact name or phone number. |

**Response**

```json
{
  "success": true,
  "data": {
    "contacts": [
      {
        "contactId": "contact456",
        "firstName": "Jamie",
        "lastName": "Lee",
        "phoneNumber": "+14155551234",
        "email": "jamie@example.com",
        "channel": "whatsapp",
        "lastActivityAt": "2026-08-30T14:22:00.000Z",
        "lastMessage": { "body": "Thanks, that fixed it!", "direction": "inbound", "timestamp": "2026-08-30T14:22:00.000Z" },
        "isBotActive": true,
        "markChatClosed": false
      }
    ],
    "subAccountName": "Client Co",
    "subAccountEmail": "client@example.com",
    "hasMore": true,
    "lastActivityAt": "2026-08-30T14:22:00.000Z"
  }
}
```

Contacts are ordered by most recent activity first. Keep paging with `lastActivityAt` while `hasMore` is `true`.

**Read one contact's messages**

```bash
curl "https://api.dmchamp.com/v1/subaccounts/SUB_ACCOUNT_UID/chats/contact456/messages?apiKey=YOUR_AGENCY_API_KEY&pageSize=30"
```

| Query parameter | Required | Description |
|---|---|---|
| `pageSize` | No | Messages per page. Default 30, maximum 100. |
| `beforeTimestamp` | No | Pagination cursor — fetch messages older than this ISO timestamp. |

**Response**

```json
{
  "success": true,
  "data": {
    "messages": [
      {
        "messageId": "msg789",
        "body": "Thanks, that fixed it!",
        "direction": "inbound",
        "timestamp": "2026-08-30T14:22:00.000Z",
        "status": "received",
        "channel": "whatsapp",
        "botReply": false,
        "mediaUrl": null,
        "mediaContentType": null,
        "name": "Jamie Lee",
        "role": null
      }
    ],
    "contactInfo": { "firstName": "Jamie", "lastName": "Lee", "phoneNumber": "+14155551234", "channel": "whatsapp" },
    "hasMore": false,
    "oldestTimestamp": "2026-08-30T14:22:00.000Z"
  }
}
```

Messages come back newest first; page backwards through history with `beforeTimestamp`.

***

## Read credit usage and campaign health across your book

`GET /v1/subaccounts/credit-usage` · `GET /v1/subaccounts/campaign-status`

Two dashboard-style rollups over every sub-account you manage, for building your own agency reporting instead of clicking into each client one at a time.

**Credit usage**

```bash
curl "https://api.dmchamp.com/v1/subaccounts/credit-usage?apiKey=YOUR_AGENCY_API_KEY&from=2026-08-01&to=2026-08-31"
```

| Query parameter | Required | Description |
|---|---|---|
| `from` / `to` | Yes | ISO date range. |
| `subAccountId` | No | Omit for an agency-wide summary, one row per sub-account. Include to switch to detail mode: that sub-account's summary plus its raw, paginated usage records. |
| `limitCount` | No | Detail mode only. Default 500, maximum 2000. |
| `startAfterTimestamp` | No | Detail mode only — pagination cursor. |

```json
{
  "success": true,
  "data": {
    "subAccounts": [
      {
        "subAccountId": "abc123def456",
        "subAccountName": "Client Co",
        "subAccountEmail": "client@example.com",
        "totalCreditsUsed": 842,
        "totalCostUsd": 3.15,
        "byReason": { "AI reply": 620, "Chat summary": 80 },
        "topCampaigns": [{ "campaignName": "Inbound Leads", "creditsUsed": 500 }]
      }
    ],
    "totals": { "totalCreditsUsed": 842, "totalCostUsd": 3.15, "totalRecords": 214 },
    "dateRange": { "from": "2026-08-01", "to": "2026-08-31" },
    "hasMore": false,
    "lastTimestamp": null
  }
}
```

Pass `subAccountId` and the same response also carries `records`: individual charges with `amount`, `reason`, `campaignName`, `contactName` and `timestamp`. A client spending on their own BYOK key rather than your credits has cost/token figures withheld (`costsRedacted: true`) — that's platform-cost telemetry, not something to surface to a reseller viewer.

**Campaign status**

```bash
curl "https://api.dmchamp.com/v1/subaccounts/campaign-status?apiKey=YOUR_AGENCY_API_KEY&pageSize=20"
```

| Query parameter | Required | Description |
|---|---|---|
| `pageSize` | No | Sub-accounts per page. Default 10, maximum 50. |
| `lastDocumentId` | No | Pagination cursor. |
| `searchQuery` | No | Filter by sub-account name or email. |

```json
{
  "success": true,
  "data": {
    "totalSubAccounts": 34,
    "subAccountsWithIssues": 3,
    "totalLiveCampaigns": 51,
    "totalPausedCampaigns": 6,
    "subAccounts": [
      {
        "userId": "abc123def456",
        "email": "client@example.com",
        "displayName": "Jamie Lee",
        "businessName": "Client Co",
        "totalCampaigns": 2,
        "liveCampaigns": 1,
        "pausedCampaigns": 1,
        "hasIssues": true,
        "issueDetails": ["1 campaign paused"],
        "lastCampaignActivity": "2026-08-29T09:00:00.000Z"
      }
    ],
    "hasMore": true,
    "lastDocumentId": "abc123def456",
    "pageSize": 20
  }
}
```

`hasIssues` / `issueDetails` flag sub-accounts worth a look — a paused campaign, or one with no channel routed to it, for example. Use this to build a health-check dashboard across the whole book rather than opening each client to notice a stalled campaign.

> For time-series messaging and credit activity across every client (a chart-ready series rather than a point-in-time snapshot), see `GET /analytics/agency-rollup` in the Analytics API guide.

***

## Ship a client account that is already set up

`PUT /v1/snapshots/default` · `POST /v1/snapshots/{snapshotId}/apply`

A [snapshot](snapshots.md) is a reusable template: one or more AI agents plus their knowledge base, tools and media, captured from your own account. Two endpoints put it in your provisioning flow.

**Automatic — every new client is born with it.** Star a snapshot as your default once, and every account you create from then on arrives with it installed. That covers accounts made through `POST /v1/subaccounts`, accounts you create in the dashboard, and accounts created automatically when a client pays through your checkout link.

First, find the snapshot's id:

```bash
curl "https://api.dmchamp.com/v1/snapshots" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY"
```

Then set it as the default:

```bash
curl -X PUT "https://api.dmchamp.com/v1/snapshots/default" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "snapshot_id": "SNAPSHOT_ID" }'
```

That is the whole integration. Send `{"snapshot_id": null}` to turn it off again. You can do the same thing from the dashboard by clicking the star on the **Snapshots** page.

To read back what's currently starred (say, before a provisioning script decides whether to set one), `GET /v1/snapshots/default` returns `{ "success": true, "data": { "default_snapshot_id": "SNAPSHOT_ID" } }` — `null` when nothing is starred. `GET /v1/snapshots` (used to find the id above) returns the same `default_snapshot_id` alongside the full `snapshots` array, so most integrations only need the one call. Full snapshot object fields are in the [Snapshots](snapshots.md) guide.

**On demand — install into one account.** Useful for onboarding an existing client, or for giving a client a second template later.

```bash
curl -X POST "https://api.dmchamp.com/v1/snapshots/SNAPSHOT_ID/apply" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sub_account_id": "SUB_ACCOUNT_UID" }'
```

Omit `sub_account_id` and it installs into your own agency account instead. Like the `/subaccounts` endpoints, these name the target account in the path or body rather than through the ambient `sub_account_id` parameter.

Worth knowing before you build on it:

- **Installed agents start paused.** Connect the client's channels first, then activate the agent. That is true for both the automatic and the on-demand path.
- **Provisioning never fails because of a snapshot.** If the install can't complete, the client account is still created and usable — it just arrives empty and you can apply the snapshot afterwards.
- **Channels, calendars and OAuth connections are never copied.** Each account connects its own. Tools using a plain API key keep working immediately.
- **Applying twice creates a second copy.** Nothing is overwritten.

***

## Build the template itself over the API

`POST /v1/snapshots` · agents, custom functions and media over the API

The section above distributes a snapshot someone built in the dashboard. The authoring half is exposed too, so the whole loop — assemble the master setup once, capture it, hand it to every client — can run from code.

The pieces, in the order a provisioning script uses them:

1. **Create your custom functions.** `POST /v1/custom-functions` creates one; `GET /v1/custom-functions` lists what you have, and `GET`, `PUT` and `DELETE` on `/v1/custom-functions/{customFunctionId}` read, update and remove one. `POST /v1/custom-functions/test` dry-runs a definition before you save it.
2. **Create and shape the agent.** `POST /v1/agents` creates it, `PUT /v1/agents/{agentId}` updates it, and `PATCH /v1/agents/{agentId}/active` with `{ "active": false }` keeps it paused while you work (the same call with `true` goes live). `GET /v1/agents` lists them.
3. **Give the agent its abilities.** `POST /v1/agents/{agentId}/custom-functions` with `{ "custom_function_id": "..." }` attaches a function to the agent; the matching `DELETE /v1/agents/{agentId}/custom-functions/{customFunctionId}` detaches it.
4. **Fill the media library.** `POST /v1/agents/{agentId}/media-library` uploads an item (JSON with `base64Data`, `mimeType`, `title`, `description`); `GET` lists the agent's items, and `PATCH`/`DELETE` on `/{itemId}` update or remove one.
5. **Capture it as a snapshot.**

```bash
curl -X POST "https://api.dmchamp.com/v1/snapshots" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Master setup v1",
    "agent_ids": ["AGENT_ID"],
    "include_knowledge": true,
    "include_tools": true,
    "include_media": true
  }'
```

From there it's the previous section: star it as the default so every new client is born with it, or apply it on demand. Housekeeping lives alongside: `PATCH /v1/snapshots/{snapshotId}` with `{ "name": "..." }` renames one, `DELETE /v1/snapshots/{snapshotId}` deletes one (and un-stars it if it was the default), and `GET /v1/snapshots/apply-targets` lists every account you could install into.

The agent, custom-function and media endpoints all accept `sub_account_id`, so the same calls can also maintain an agent directly inside one client's account. Snapshot calls always act on your agency account — the template lives with you. Full request and response schemas for all of these are in the [API Reference](../api/reference.md).

***

## Manage your pricing tiers over the API

`GET /v1/agency/pricing-tiers` · `POST /v1/agency/pricing-tiers` · `PATCH /v1/agency/pricing-tiers/{tierIndex}` · `DELETE /v1/agency/pricing-tiers/{tierIndex}`

The plans you sell in **SaaS Mode → Pricing Tiers** can be read and changed from code, so your own admin panel or provisioning script can add a plan, adjust a price, or hand out a checkout link without anyone opening the dashboard. Authenticate with your agency API key like every other call on this page; these endpoints are agency-level, so they take no `sub_account_id`. Every write runs the same validation and the same Stripe product-and-price sync as the dashboard save, so a plan created here is indistinguishable from one you set up by hand.

### List your tiers

```bash
curl "https://api.dmchamp.com/v1/agency/pricing-tiers" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY"
```

**Response**

```json
{
  "success": true,
  "data": {
    "tiers": [
      {
        "tierIndex": 0,
        "credits": 1000,
        "price_cents": 2900,
        "currency": "usd",
        "label": "Starter",
        "billing_interval": "month",
        "trial_days": 14,
        "trial_credits": 250,
        "trial_card_required": false,
        "trial_hard_expiry": true,
        "stripe_price_id": "price_1PxAbC…",
        "stripe_product_id": "prod_QxAbC…",
        "checkout_url": "https://app.yourdomain.com/v1/checkout?id=YOUR_AGENCY_UID&tierIndex=0"
      }
    ],
    "count": 1,
    "max_tiers": 20
  }
}
```

Every tier comes back with its **`tierIndex`** — its position in your Plans list, which is how the other three calls address it — and a ready-to-share **`checkout_url`**, the same link the **Payments** tab gives you, already pointing at the [white label domain](white-labeling.md) that plan is sold on.

### Add a tier

The body is one tier object; it is appended to the end of your list.

```bash
curl -X POST "https://api.dmchamp.com/v1/agency/pricing-tiers" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Starter",
    "credits": 1000,
    "price_cents": 2900,
    "currency": "usd",
    "billing_interval": "month",
    "trial_days": 14,
    "trial_credits": 250,
    "trial_card_required": false,
    "trial_hard_expiry": true,
    "features": ["channels_3", "channel_whatsapp_web", "webhooks"]
  }'
```

The response carries the created tier, including the `tierIndex` it landed on and its `checkout_url`.

### Edit a tier

Send only the fields you want to change; everything else on the plan is left as it was.

```bash
curl -X PATCH "https://api.dmchamp.com/v1/agency/pricing-tiers/0" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "price_cents": 3900, "trial_hard_expiry": true }'
```

A field the API doesn't recognise is refused rather than ignored, and the error names it — so a typo can never quietly write a setting that looks live but does nothing. Changing the price, the credits, the currency or the billing cadence creates a new price in your Stripe; clients who already subscribed stay on what they signed up for.

### Delete a tier

```bash
curl -X DELETE "https://api.dmchamp.com/v1/agency/pricing-tiers/2" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY"
```

Same rule as the dashboard: a plan that still has active subscribers cannot be deleted. The request comes back refused, telling you how many subscribers are on it — cancel or migrate them first. A successful delete responds with your remaining tiers, already renumbered.

### The fields on a tier

| Field | What it is |
|---|---|
| `label` / `description` | The plan's name, and the optional line shown on your checkout page. |
| `credits` | Credits the client gets **per month** on a monthly or yearly plan, and **per billing period** on a weekly one. |
| `price_cents` | Price per billing interval, in the smallest currency unit (`2900` = $29.00). On a yearly plan this is the price of the whole year. |
| `currency` | Lower-case ISO code — `usd`, `eur`, `gbp` and so on. |
| `billing_interval` / `billing_interval_count` | `month` (the default), `year`, or `week` with a count of 1–52 for "every N weeks". |
| `trial_days` | Free trial length, 0 to 90. `0` (or leaving it out) means no trial. |
| `trial_credits` | Credits the client starts the trial with. Defaults to the plan's `credits`. |
| `trial_card_required` | `false` lets the client start the trial without entering a card. Defaults to `true`. |
| `trial_hard_expiry` | `true` returns unused trial credits to your pool and locks the client's account when a trial ends without an upgrade. Defaults to `false` — see [Hard expiry after trial](agency-accounts.md#step-3--set-up-pricing-tiers). |
| `rollover_cap_months` | Months of allowance clients on this plan may carry between renewals — a number from 0 to 120, fractions allowed. `0` carries nothing over; `null` (the default) means no cap. See [Capping what rolls over](sub-accounts.md#capping-what-rolls-over). |
| `rollover_expiry_days` | Days after which unused credits are dropped at the next renewal — a whole number from 1 to 3650. `null` (the default) means they never expire. |
| `features` / `feature_settings` | What clients on this plan get — the same feature IDs as [Choose which channel types a client can connect](#choose-which-channel-types-a-client-can-connect). |
| `team_seats_limit` | Team seats the plan grants: an exact number, `0` for none, `-1` for unlimited. |
| `white_label_config` | Which of your [white label domains](white-labeling.md#up-to-three-white-labels) the plan is sold on. |

The trial fields only mean something on a plan that has a trial: save a tier with `trial_days: 0` and they are dropped. The plan's Stripe product and price ids are managed for you and can't be set by hand.

Three things to get right:

- **Tier indexes are positions, not permanent ids.** Deleting a plan shifts every plan after it down one, so re-fetch the list after any change — and re-copy the checkout links you've published, exactly as you would after deleting a plan in the dashboard.
- **SaaS Mode has to be set up first.** These endpoints need an agency account with white labeling and a Stripe key already saved; without one there is no Stripe account for the plan's product and price to live on.
- **Twenty plans is the cap**, the same as in the dashboard. The `max_tiers` field in the list response tells you the current limit.

Full request and response schemas are in the [API Reference](../api/reference.md), under **Agency**.

***

## Set your per-credit price over the API

`GET /v1/agency/credit-price` · `PATCH /v1/agency/credit-price`

The price clients pay for ad-hoc top-ups (**SaaS Mode → Per-Credit Pricing**) can be read and changed from code as well. This is built for the case where the price has to move on its own: an agency that sells credits in one currency but charges in another can let a scheduled job revise the price as the exchange rate changes, instead of someone editing it by hand every week.

### Read the current price

```bash
curl "https://api.dmchamp.com/v1/agency/credit-price" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY"
```

**Response**

```json
{
  "success": true,
  "data": {
    "price_per_credit_cents": 125,
    "price_per_credit_currency": "brl",
    "note": "USD 0.25 per credit at our reference rate",
    "minimum_cents": 60
  }
}
```

`minimum_cents` is the lowest price the platform allows in that currency, so a job can check a new price before sending it. All three values are `null` until a price has been set.

### Change it

```bash
curl -X PATCH "https://api.dmchamp.com/v1/agency/credit-price" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "price_per_credit_cents": 130, "currency": "brl" }'
```

Send only what you want to change. `price_per_credit_cents` is the price in the smallest currency unit (`130` = R$1.30); `currency` is a lower-case ISO code; `note` is an optional line of up to 200 characters shown to clients directly under the per-credit price on their Billing page — handy for a reference price in another currency, such as *"USD 0.25 per credit at our reference rate"*. Send `"note": ""` to remove it. The response is the same shape as the read above, so a job can compare and skip the write when nothing changed.

The same rules apply as in the dashboard: the price cannot go below the platform minimum for that currency, and the account needs white labeling. Unlike the pricing-tier endpoints, no Stripe key is required to read or change this value.

### Give a job a key that can do nothing else

Putting your full agency key into a scheduler is more access than a price update needs. Instead, mint a **scoped key** limited to the **Agency Credit Price** area: that key can read and change the per-credit price and nothing else — it cannot touch sub-accounts, plans, credits or your Stripe connection.

```bash
curl -X POST "https://api.dmchamp.com/v1/api-keys" \
  -H "X-API-Key: YOUR_AGENCY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "FX price updater", "scopes": { "read_only": false, "tags": ["Agency Credit Price"] } }'
```

The response carries the new key in `api_key` **once** — it is never shown again, so store it straight away. Set `"read_only": true` for a key that only needs to read the price, and add `"expires_at"` (an ISO date) if you want it to stop working on its own. Only the account owner's key can create scoped keys; list or revoke them with `GET /v1/api-keys` and `DELETE /v1/api-keys/{id}`.

***

## Let a sub-account read your pricing

`GET /v1/subaccounts/agency-pricing`

Every other endpoint on this page is called with **your agency key**, optionally targeting a client via `sub_account_id`. This one is the opposite: it's called with the **sub-account's own API key**, no `sub_account_id`, so a client's own top-up page (or an integration you build for them) can display what you charge them without ever seeing your agency account.

```bash
curl "https://api.dmchamp.com/v1/subaccounts/agency-pricing" \
  -H "X-API-Key: THE_SUB_ACCOUNTS_OWN_API_KEY"
```

**Response**

```json
{
  "success": true,
  "data": {
    "tiers": [{ "credits": 1000, "price_cents": 2900, "currency": "usd" }],
    "price_per_credit_cents": 125,
    "price_per_credit_currency": "brl",
    "price_per_credit_note": "USD 0.25 per credit at our reference rate",
    "agency_display_name": "Client Co's Growth Partner"
  }
}
```

This mirrors exactly what [`GET /v1/agency/pricing-tiers`](#list-your-tiers) and [`GET /v1/agency/credit-price`](#read-the-current-price) return for you as the agency, minus anything the client doesn't need to see (Stripe ids, `max_tiers`, and so on). It only works for an account that is actually a sub-account with a linked agency — calling it from your own agency account returns a permission error.

***

## Things to keep in mind

- **Use your agency key.** Authenticate every call with your agency account's API key — not the sub-account's. The `sub_account_id` parameter is what redirects the action.
- **Credits come from the sub-account.** Purchases and recurring charges hit the targeted sub-account's credit balance, not yours.
- **A `404` means "not your sub-account."** Double-check the id and that the account is one you manage.
- **The parameter is optional everywhere it's accepted.** Omit it and the same endpoint acts on your agency account, so you can reuse one integration for both.

***

## Related

- [API Access](../integrations/api-access.md) — authentication, base URL, errors, rate limits.
- [Sub-Accounts](sub-accounts.md) — list and manage the accounts you can target.
- [Sub-Account Auto-Recharge](sub-account-auto-recharge.md) — grant credits to a sub-account via webhook + API.
- [Campaigns API](../api/campaigns.md) — create, update, and copy campaigns, including the full field reference.
- [Channel Connection API](../api/channels.md) — connect a client's channels and route them to a campaign.
- Analytics API guide (in the API section) — the agency sub-account rollup and every other reporting endpoint.
- [Snapshots](snapshots.md) — what a snapshot captures and how to build one in the dashboard.
