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

# Worked channels samples

> The AMB agent CRUD chain — list connected agents, register one, transition its status, remove it — with full response envelopes and the errors worth branching on.

## Worked channels samples

The generated blocks below document every parameter and response shape; this
overlay walks the chain an Apple Messages for Business integration actually
ships: **list connected agents → register one → transition its status →
remove it**, plus the WhatsApp Flow inventory your inbox composer polls.
Every authenticated call on this page carries `X-API-Key`; the two webhook
routes (`POST .../channels/amb/webhook`, `POST .../channels/whatsapp/flows/endpoint`)
are provider-facing and authenticate by signature, not by key — you never
call them from your own backend.

Every JSON response follows the `{ data, meta }` envelope — `data` carries
the payload, `meta` carries the `request_id` and `timestamp`. Quote
`meta.request_id` when you report a bad payload.

The Node SDK does not wrap the channels namespace yet, so the typed tabs
below use the SDK's `orbit.request` escape hatch (the same convention the
[API recipes guide](/guides/api-recipes) uses for uncovered resources) —
you get the SDK's auth, retries, and envelope unwrapping against the raw
REST path.

### 1. List connected AMB agents

`GET /api/v1/channels/amb/agents` returns the tenant's Apple Messages for
Business agents, newest first (capped at 200). Poll it on your
channel-settings screen to render connection state: the `status` field is
the approval lifecycle (`pending` → `approved` → `suspended`), and
`has_secret: true` confirms the agent carries a credential. The secret
itself is encrypted at rest and never serialised — a rotated secret still
reads as `has_secret: true` with no material exposed.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.orbit.devotel.io/api/v1/channels/amb/agents" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const agents = await orbit.request("GET", "/api/v1/channels/amb/agents");
  // agents.data — array of agent rows (see the 200 envelope below)
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": [
    {
      "id": "6f1c9b2e-3a44-4a8c-9d2b-7e5f0a1c2d34",
      "business_id": "urn:ib:business:084a3f21-77cc-4c0e-9a1b-0d2f5c8b93a7",
      "display_name": "Acme Support",
      "msp_id": "msp_3f9k2",
      "status": "approved",
      "capabilities": {
        "text": true,
        "listPicker": true,
        "form": true,
        "timePicker": false,
        "applePay": false
      },
      "logo_url": "https://cdn.acme.example/amb-logo.png",
      "has_secret": true,
      "created_at": "2026-08-30T09:14:22Z",
      "updated_at": "2026-09-02T11:40:05Z"
    },
    {
      "id": "a2e7d4c1-5b90-4e1f-b3d8-9c0a2e6f1b55",
      "business_id": "urn:ib:business:11b4e6d2-0a9c-4f77-8c2d-4e9a5b1c07d2",
      "display_name": "Acme Sales",
      "msp_id": null,
      "status": "pending",
      "capabilities": { "text": true },
      "logo_url": null,
      "has_secret": true,
      "created_at": "2026-09-04T16:02:11Z",
      "updated_at": "2026-09-04T16:02:11Z"
    }
  ],
  "meta": {
    "request_id": "req_chamb_list_1",
    "timestamp": "2026-09-08T10:20:00Z"
  }
}
```

An empty tenant answers `{"data": []}` — render the connect prompt rather
than branching on null. A first-booted tenant whose AMB tables have not
finished provisioning also answers `[]` (a clean 200), so an empty list
never means the endpoint is down.

### 2. Register an AMB agent

`POST /api/v1/channels/amb/agents` creates the agent. You need the
`business_id` Apple Business Register issued and the base64 `secret_key`
assigned to the agent — the secret is encrypted with the platform envelope
before it persists and is never returned by any read. The idempotent
answer on a repeated submit is driven by the `business_id`: it is unique
per tenant, so re-POSTing the same one conflicts — see the 409 below.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.orbit.devotel.io/api/v1/channels/amb/agents" \
    -H "X-API-Key: $ORBIT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "business_id": "urn:ib:business:084a3f21-77cc-4c0e-9a1b-0d2f5c8b93a7",
      "display_name": "Acme Support",
      "secret_key": "QkM4M2Y5MWItMjdkNC00ZTlhLWJkYzEtOGY3ZTBhMmM1ZDZl",
      "status": "pending",
      "capabilities": { "text": true, "listPicker": true },
      "logo_url": "https://cdn.acme.example/amb-logo.png"
    }'
  ```

  ```typescript Node.js theme={null}
  const agent = await orbit.request("POST", "/api/v1/channels/amb/agents", {
    business_id: "urn:ib:business:084a3f21-77cc-4c0e-9a1b-0d2f5c8b93a7",
    display_name: "Acme Support",
    secret_key: "QkM4M2Y5MWItMjdkNC00ZTlhLWJkYzEtOGY3ZTBhMmM1ZDZl",
    capabilities: { text: true, listPicker: true },
  });
  // agent.data.id — the agent id you PATCH and DELETE against
  ```
</CodeGroup>

```json 201 theme={null}
{
  "data": {
    "id": "6f1c9b2e-3a44-4a8c-9d2b-7e5f0a1c2d34",
    "business_id": "urn:ib:business:084a3f21-77cc-4c0e-9a1b-0d2f5c8b93a7",
    "display_name": "Acme Support",
    "msp_id": null,
    "status": "pending",
    "capabilities": { "text": true, "listPicker": true },
    "logo_url": "https://cdn.acme.example/amb-logo.png",
    "has_secret": true,
    "created_at": "2026-09-08T10:31:44Z",
    "updated_at": "2026-09-08T10:31:44Z"
  },
  "meta": {
    "request_id": "req_chamb_create_1",
    "timestamp": "2026-09-08T10:31:44Z"
  }
}
```

Keep the returned `data.id` — updates and deletes address the agent by
that id, not by `business_id`. Registration also mirrors the
`business_id → tenant` mapping into the webhook registry, so inbound Apple
Messages start routing to your inbox the moment the POST succeeds.

### 3. Transition status / rotate the secret

`PATCH /api/v1/channels/amb/agents/{id}` is a partial update — only the
fields you send change. The two calls you make in practice: flip `status`
when Apple approves (or when you suspend the channel), and send a new
`secret_key` to rotate credentials (the replacement is encrypted before it
persists).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH "https://api.orbit.devotel.io/api/v1/channels/amb/agents/6f1c9b2e-3a44-4a8c-9d2b-7e5f0a1c2d34" \
    -H "X-API-Key: $ORBIT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "status": "approved" }'
  ```

  ```typescript Node.js theme={null}
  const updated = await orbit.request(
    "PATCH",
    "/api/v1/channels/amb/agents/6f1c9b2e-3a44-4a8c-9d2b-7e5f0a1c2d34",
    { status: "approved" },
  );

  // Rotate the secret the same way:
  await orbit.request(
    "PATCH",
    "/api/v1/channels/amb/agents/6f1c9b2e-3a44-4a8c-9d2b-7e5f0a1c2d34",
    { secret_key: "TWV3YmFzZTY0a2V5LXJvdGF0ZWQtMjAyNi0wOS0wOA==" },
  );
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "id": "6f1c9b2e-3a44-4a8c-9d2b-7e5f0a1c2d34",
    "business_id": "urn:ib:business:084a3f21-77cc-4c0e-9a1b-0d2f5c8b93a7",
    "display_name": "Acme Support",
    "msp_id": null,
    "status": "approved",
    "capabilities": { "text": true, "listPicker": true },
    "logo_url": "https://cdn.acme.example/amb-logo.png",
    "has_secret": true,
    "created_at": "2026-09-08T10:31:44Z",
    "updated_at": "2026-09-08T11:02:17Z"
  },
  "meta": {
    "request_id": "req_chamb_patch_1",
    "timestamp": "2026-09-08T11:02:17Z"
  }
}
```

A PATCH against an id that does not exist on your tenant is a `404` —
re-list (section 1) and re-read the id instead of retrying the same one;
the roster is the source of truth after any delete or re-register.

### 4. Disconnect an agent

`DELETE /api/v1/channels/amb/agents/{id}` removes the agent and drops its
`business_id` from the webhook registry, so inbound Apple Messages stop
routing to your inbox immediately. The response is the deleted id plus a
`deleted: true` marker.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE "https://api.orbit.devotel.io/api/v1/channels/amb/agents/a2e7d4c1-5b90-4e1f-b3d8-9c0a2e6f1b55" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const gone = await orbit.request(
    "DELETE",
    "/api/v1/channels/amb/agents/a2e7d4c1-5b90-4e1f-b3d8-9c0a2e6f1b55",
  );
  // gone.data.deleted === true
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "id": "a2e7d4c1-5b90-4e1f-b3d8-9c0a2e6f1b55",
    "deleted": true
  },
  "meta": {
    "request_id": "req_chamb_del_1",
    "timestamp": "2026-09-08T11:20:02Z"
  }
}
```

### 5. Inventory WhatsApp Flows

`GET /api/v1/channels/whatsapp/flows` lists the tenant's Meta WhatsApp
Flows (up to 100, newest first). Your inbox composer polls this to offer
a local-flow picker while a completion event is in flight: each row
carries the remote Meta `flow_id`, the lifecycle `status`
(`draft` / `published` / `deprecated`), and `first_screen` for the launch
surface. Flows `POST /api/v1/channels/whatsapp/flows/endpoint` receives
are landings of these same flows — an unknown `flow_id` in a completion
event means the flow was deprecated upstream; re-list here before
surfacing an error.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.orbit.devotel.io/api/v1/channels/whatsapp/flows" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```typescript Node.js theme={null}
  const flows = await orbit.request("GET", "/api/v1/channels/whatsapp/flows");
  // flows.data — rows keyed by remote Meta flow_id
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": [
    {
      "id": "fl_9c2d4e6f8a",
      "name": "Order support intake",
      "flow_id": "1234567890123456",
      "status": "published",
      "version": 3,
      "json_schema": {
        "screens": [
          { "id": "INTAKE", "title": "What do you need help with?" }
        ]
      },
      "first_screen": "INTAKE",
      "created_at": "2026-08-21T14:03:51Z",
      "updated_at": "2026-09-01T09:44:10Z"
    },
    {
      "id": "fl_2b7a1c9d3e",
      "name": "Warranty claim",
      "flow_id": null,
      "status": "draft",
      "version": 1,
      "json_schema": { "screens": [{ "id": "CLAIM", "title": "Claim details" }] },
      "first_screen": "CLAIM",
      "created_at": "2026-09-05T17:22:40Z",
      "updated_at": "2026-09-05T17:22:40Z"
    }
  ],
  "meta": {
    "request_id": "req_chwa_flows_1",
    "timestamp": "2026-09-08T11:26:33Z"
  }
}
```

A `flow_id: null` row is a draft that has not been published to Meta yet —
filter it out of any picker that launches live flows.

## Errors worth branching on

The matrix below is scoped to what a channel-config client actually hits;
the platform-wide retry-vs-terminal decision table lives in the
[error handling guide](/guides/error-handling-examples).

### 409 — the channel is already connected

A second POST of a `business_id` your tenant already registered conflicts —
`business_id` is unique per tenant, and the create is not upsert-shaped.
The idempotent connect playbook is: POST once, and on 409 PATCH the
existing agent (rotate the secret, restate capabilities) instead of
re-POSTing.

```json 409 theme={null}
{
  "error": {
    "code": "CONFLICT",
    "message": "The resource already exists.",
    "status": 409
  },
  "meta": {
    "request_id": "req_chamb_conflict_1",
    "timestamp": "2026-09-08T10:33:09Z"
  }
}
```

### 403 — a read-scoped key tried to write

Create, update, and disconnect need a write-capable key (dashboard role
owner/admin/developer). A key scoped to read-only gets a 403 — fix the
key's scope set on the developer page; retrying with the same key never
heals it.

```json 403 theme={null}
{
  "error": {
    "code": "FORBIDDEN",
    "message": "The key is valid but does not have the scope this operation requires.",
    "status": 403
  },
  "meta": {
    "request_id": "req_chamb_forbid_1",
    "timestamp": "2026-09-08T10:33:12Z"
  }
}
```

### 422 — the provider fields did not validate

Body schema violations return `VALIDATION_ERROR` with `error.details`
naming the field — a `business_id` Apple rejects, a `secret_key` shorter
than 16 or longer than 2048 base64 chars, a non-enum `status`, or an agent
`id` path segment that is not a UUID. Branch on `error.details` and resend
with the corrected field; a blind retry repeats the same 422.

```json 422 theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "status": 422,
    "details": { "secret_key": "String must contain at least 16 character(s)" }
  },
  "meta": {
    "request_id": "req_chamb_valid_1",
    "timestamp": "2026-09-08T10:33:20Z"
  }
}
```

### Retry matrix

| Class                             | Meaning                                        | Branch response                                                                                                                                                                                              |
| --------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **401 `UNAUTHORIZED`**            | the API key itself fails before the route runs | **Surface.** Rotate the key or re-mint it; the two provider webhooks instead return `WEBHOOK_AUTH_FAILED` (401) when the inbound signature does not verify — that is the provider's handshake, not your key. |
| **403 `FORBIDDEN`**               | the key lacks this operation's write scope     | **Surface.** Fix the key's scope set on the developer page.                                                                                                                                                  |
| **422 `VALIDATION_ERROR`**        | the body or path payload failed the schema     | **Fix, then resend.** Branch on `error.details`; a blind retry repeats the same 422.                                                                                                                         |
| **429 `RATE_LIMITED`**            | the window quota is exhausted                  | **Retry after** `error.details.retry_after` (or the `Retry-After` header) with the same request body.                                                                                                        |
| **409 already-connected channel** | the `business_id` is unique per tenant         | **Branch on `error.code`.** PATCH the existing agent instead of re-POSTing the connect.                                                                                                                      |
| **404 unknown agent id**          | PATCH / DELETE addressed an id that is gone    | **Re-list and re-branch.** Read the roster again; a stale id never resolves.                                                                                                                                 |
