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

# Connect a customer's channels

> Let your customers connect WhatsApp, Messenger and Instagram from inside your own app, without signing in to DMLY.

Channel Connect lets your app start the Meta connection for one of your sub-accounts. Your
customer clicks **Connect WhatsApp** (or Messenger, or Instagram) in your product, authorizes with
Meta on a page carrying your brand, picks the account, and lands back in your app. They never see
the DMLY dashboard, and your backend gets the connected channel's id.

<Warning>
  Channel Connect is in **private beta** and is switched on per agency. Ask DMLY support to enable
  it for yours. Until it is on, `GET /channel-connect/settings` reports `"enabled": false` and
  creating a connection returns `403`.
</Warning>

## What it supports

| Platform | `platform` | How the customer connects |
| - | - | - |
| WhatsApp | `whatsapp` | Meta's Embedded Signup, then they pick the number |
| Messenger | `facebook` | Facebook Login, then they pick the Page |
| Instagram | `instagram` | Facebook Login, then they pick the professional account linked to one of their Pages |

Not available through Channel Connect: Instagram Login (an Instagram account with no Facebook Page)
and WhatsApp Business app co-existence. Customers who need those connect from the DMLY dashboard.

## How it works

<Steps>
  <Step title="Register your return URLs (once)">
    Tell DMLY where customers may be sent back to. Only these exact URLs are accepted.

    ```bash theme={"dark"}
    curl -X PUT https://dash.dmly.io/api/agency/v1/channel-connect/settings \
      -H "x-api-key: dmly_ag_xxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{"return_urls": ["https://app.example.com/integrations/callback"]}'
    ```

    Up to 20 HTTPS URLs, with no query string, fragment or credentials. Matching is exact,
    including the path and any trailing slash. The call replaces the whole list.
  </Step>

  <Step title="Start a connection from your backend">
    When your customer clicks Connect, your server creates a session for their sub-account.

    ```bash theme={"dark"}
    curl -X POST https://dash.dmly.io/api/agency/v1/workspaces/{workspace}/channel-connections \
      -H "x-api-key: dmly_ag_xxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: 6c1d0e9a-connect-whatsapp" \
      -d '{
        "platform": "whatsapp",
        "return_url": "https://app.example.com/integrations/callback",
        "state": "cust_8841"
      }'
    ```

    The response carries a `connect_url`. Send it to the browser and nowhere else.

    ```json theme={"dark"}
    {
      "data": {
        "id": "6f0e2c1a-8b7d-4c3e-9a1f-0d2b3c4e5f60",
        "workspace_id": "11111111-1111-1111-1111-111111111111",
        "platform": "whatsapp",
        "status": "pending",
        "channel_id": null,
        "error_code": null,
        "state": "cust_8841",
        "expires_at": "2026-10-06T12:30:00+00:00",
        "connect_url": "https://dash.dmly.io/agency-connect/6f0e2c1a-…?token=…"
      }
    }
    ```
  </Step>

  <Step title="Send your customer to the connect URL">
    Open `connect_url` as a top-level navigation (a link or a redirect, not an iframe or a
    background fetch). The customer sees your agency's name, clicks **Continue to Meta**,
    authorizes, and chooses the account to connect. They must finish in the same browser they
    started in, within 30 minutes.
  </Step>

  <Step title="Read the result when they come back">
    The browser returns to your `return_url` with `connection_id` and `state` in the query string.
    Match `state` to the customer who started, then ask DMLY for the result. Never trust the
    query string alone.

    ```bash theme={"dark"}
    curl https://dash.dmly.io/api/agency/v1/workspaces/{workspace}/channel-connections/{connection} \
      -H "x-api-key: dmly_ag_xxxxxxxxxxxx"
    ```

    When `status` is `connected`, `channel_id` is the new channel. Read it with
    `GET /workspaces/{workspace}/channels/{channel}`.
  </Step>
</Steps>

## Before you start

* **Keep the agency key on your server.** Create sessions from your backend only. An agency key can
  act on every sub-account you own, so your backend must check that the signed-in customer owns the
  workspace before it creates a session for it.
* **Treat `connect_url` as a secret.** Anyone holding it can start that connection. Don't log it,
  put it in analytics, or send it by email. Only the create call returns it (and an
  `Idempotency-Key` replay of it while the session is still `pending`); status reads never do.
* **Use `state` for correlation only.** Up to 500 characters, returned unchanged on the return URL,
  in status responses and in webhooks. Don't put secrets or personal data in it.
* **Retry safely with `Idempotency-Key`.** Repeating the same request with the same key returns the
  same session (`200`). The same key with a different body or sub-account returns `409`. Use a new
  key to start over.

## Statuses

| Status | Meaning |
| - | - |
| `pending` | Created. The customer hasn't continued to Meta yet |
| `authorizing` | The customer is on Meta's consent screens |
| `selecting` | Meta approved; the customer is choosing the account |
| `connected` | Done. `channel_id` is set |
| `failed` | Something went wrong. See `error_code` |
| `cancelled` | The customer declined at Meta, or you cancelled the session |
| `expired` | 30 minutes passed without finishing |

`connected` means the account was saved and its Meta webhook subscription succeeded. Messages still
depend on the account itself: a WhatsApp number must be able to send, a Page must allow messaging,
and Meta's usual messaging rules apply.

To abandon a session, call `POST /workspaces/{workspace}/channel-connections/{connection}/cancel`.
It is safe to repeat and never disconnects a channel that already connected.

## Reconnecting a channel

To refresh an existing channel (an expired token, a revoked permission), pass its id as
`channel_id` when you create the session. The customer must pick the same number, Page or
Instagram account; anything else fails with `reconnection_target_mismatch`. The channel keeps its
id, so nothing changes on your side.

## Webhooks

Subscribe your [agency webhook](/api-reference/agency-webhooks) to any of:

* `channel.connection.completed`
* `channel.connection.failed`
* `channel.connection.cancelled`
* `channel.connection.expired`

`data` is the connection as `GET …/channel-connections/{connection}` returns it, without
`connect_url`. Events are sent within about a minute of the session ending, at least once, so
deduplicate on the envelope `id`. They report how onboarding ended, not later problems such as
an expired token. Reading the status when the customer returns is still the quickest way to show
them the result.

## Error codes

| `error_code` | What happened | What to do |
| - | - | - |
| `authorization_not_completed` | The customer declined or closed Meta's consent screens | Let them start again |
| `required_permissions_missing` | The customer unticked a permission messaging needs | Start again and keep every permission ticked |
| `no_eligible_accounts` | Nothing to connect: no WhatsApp number, no Page, or no Instagram professional account linked to a Page | Set the account up in Meta first, then retry |
| `account_already_connected_elsewhere` | That number, Page or account is connected to another DMLY workspace | Disconnect it there first |
| `channel_limit_reached` | The sub-account's plan has no room for another channel of this kind | Raise the plan or add an add-on, then retry |
| `reconnection_target_mismatch` | A reconnect picked a different account than `channel_id` | Pick the account the channel already uses |
| `subscription_failed` | Meta refused to send this account's messages to DMLY | Retry; if it repeats, contact support |
| `credentials_expired` | Meta's authorization expired before the customer chose an account | Start again |
| `token_exchange_failed` | Meta didn't complete the authorization | Start again |
| `asset_discovery_incomplete` | The customer has too many accounts for DMLY to list | Contact support |
| `provider_request_failed` | Meta rejected a request | Retry; if it repeats, contact support |
| `provider_unavailable` | Meta couldn't be reached | Retry in a few minutes |
| `provider_not_configured` | Connecting this platform isn't set up on DMLY's side | Contact support |
| `session_expired` | 30 minutes passed | Start a new session |

Meta's error details and tokens are never returned to you.

## What your customer sees

The connect page shows your agency's app name (or agency name) and nothing from the DMLY dashboard.
It is served from your verified custom domain when you have one, otherwise from your agency
subdomain, otherwise from DMLY's own domain. Meta's own consent screens show the name of the Meta app
your customer is authorizing, which DMLY controls, not yours.

For Messenger and Instagram the consent screen asks for the same permissions as a connection made in
the DMLY dashboard, including comment permissions, so comment automations work on the channel. Only
the messaging permissions are required; a customer who withholds the others still connects for
direct messages.


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