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

# Migrate from 360dialog

Migrate your WhatsApp numbers from 360dialog to Kapso.

## Step 1: Connect your number

1. In [WhatsApp Manager](https://business.facebook.com/latest/whatsapp_manager/phone_numbers), check the WABA for other numbers, templates, or assets you still need.
2. Turn off two-step verification, under **Phone numbers → Settings → Two-step verification**.
3. Confirm you are an admin of the owning Business Portfolio, and that the number has not sent a paid message in the last 30 days.
4. Remove the phone number from the WABA.
5. Wait about five minutes.
6. In Kapso, start embedded signup. Share your existing WABA, or create one when the flow asks.
7. Recreate your templates if you created a new WABA (see [Step 5](#step-5-templates)). Wait for Meta review.

The number stops sending after step 4, and on a new WABA it can send templates again once Meta approves them. Plan the migration accordingly.

See [Connect WhatsApp](/docs/how-to/whatsapp/connect-whatsapp) for the signup flow.

If the reconnect fails, see [coexistence troubleshooting](/docs/how-to/whatsapp/coexistence-troubleshooting) and [bring your own SIM troubleshooting](/docs/how-to/whatsapp/bring-your-own-sim-troubleshooting).

You can also start on a fresh number. [Instant setup](/docs/platform/phone-numbers/instant-setup) gives you a pre-verified US number, with no SMS verification step.

<Info>
  **Testing first?** The [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) replaces messaging `START` to 360dialog's sandbox number. Create a session for your test phone, then send the 6-character code from WhatsApp. Unlike 360dialog's sandbox, it uses the same paths as production, supports media, and is not capped at 200 messages.
</Info>

## Step 2: Get your phone number IDs

```bash theme={null}
curl https://api.kapso.ai/platform/v1/whatsapp/phone_numbers \
  -H "X-API-Key: YOUR_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "1234567890",
      "phone_number_id": "1234567890",
      "name": "Support Line",
      "business_account_id": "98765432109",
      "display_phone_number": "+1 555-123-4567",
      "quality_rating": "GREEN",
      "throughput_tier": "TIER_10K",
      "status": "CONNECTED"
    }
  ]
}
```

Store the mapping from each old channel key to its `phone_number_id`.

| 360dialog                      | Kapso                         | Scopes to        |
| ------------------------------ | ----------------------------- | ---------------- |
| `D360-API-KEY`, one per number | `X-API-Key`, one per project  | authentication   |
| The key itself                 | `phone_number_id` in the path | one phone number |
| The WABA behind the key        | `business_account_id`         | templates        |

If you monitor `GET /health_status`, its replacement is `GET /platform/v1/whatsapp/phone_numbers/{phone_number_id}/health`, which checks actual messaging availability rather than the stored record.

## Step 3: Update message sending

Both platforms pass the message body to Meta as-is, so it does not change. Two things do, in every call:

* The base URL: `https://waba-v2.360dialog.io` becomes `https://api.kapso.ai/meta/whatsapp/v24.0`.
* The sender: the API key no longer implies it, so it moves into the path, as in `POST /{phone_number_id}/messages`.

<Tabs>
  <Tab title="Text">
    Free-form replies, inside the 24-hour window. Only the URL and the auth header change.

    **360dialog:**

    ```bash theme={null}
    curl -X POST https://waba-v2.360dialog.io/messages \
      -H "D360-API-KEY: YOUR_CHANNEL_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "messaging_product": "whatsapp",
        "recipient_type": "individual",
        "to": "15551234567",
        "type": "text",
        "text": { "body": "Your order shipped!" }
      }'
    ```

    **Kapso:**

    ```bash theme={null}
    curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
      -H "X-API-Key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "messaging_product": "whatsapp",
        "recipient_type": "individual",
        "to": "15551234567",
        "type": "text",
        "text": { "body": "Your order shipped!" }
      }'
    ```

    Kapso forwards the body to Meta byte for byte, so optional fields like `recipient_type` pass through unchanged. See [Send text](/docs/whatsapp/send-messages/text).
  </Tab>

  <Tab title="Template">
    Business-initiated sends, outside the 24-hour window.

    ```bash theme={null}
    curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
      -H "X-API-Key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "messaging_product": "whatsapp",
        "to": "15551234567",
        "type": "template",
        "template": {
          "name": "order_update",
          "language": { "code": "en_US" },
          "components": [
            {
              "type": "body",
              "parameters": [{ "type": "text", "text": "John" }]
            }
          ]
        }
      }'
    ```

    See [Simple text templates](/docs/whatsapp/templates/simple-text).
  </Tab>

  <Tab title="Media">
    Images, video, audio, documents, and stickers.

    Uploads move from `POST /media` to `POST /{phone_number_id}/media`, with the same multipart form:

    ```bash theme={null}
    curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/media \
      -H "X-API-Key: YOUR_API_KEY" \
      -F "messaging_product=whatsapp" \
      -F "file=@receipt.png;type=image/png"
    ```

    Downloading loses the hostname-rewrite step. On 360dialog you fetch the media URL, replace `lookaside.fbsbx.com` with `waba-v2.360dialog.io`, strip backslashes, and fetch within five minutes. On Kapso, `GET /{media_id}?phone_number_id=1234567890` returns Meta's fields plus a `download_url` you fetch directly, valid for 4 minutes, with the auth embedded in the URL.

    For inbound media on [Kapso events](#kapso-events) you can skip the flow entirely: payloads carry a ready `message.kapso.media_url`. A meta webhook forwards Meta's raw payload, so keep fetching by media ID there. Kapso also ingests from a URL and hands back a Meta media ID via `POST /platform/v1/whatsapp/media`.
  </Tab>

  <Tab title="Interactive">
    Buttons, lists, CTAs, and Flows use Meta's `interactive` object in both platforms, so these payloads move across as-is:

    ```bash theme={null}
    curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
      -H "X-API-Key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "messaging_product": "whatsapp",
        "to": "15551234567",
        "type": "interactive",
        "interactive": {
          "type": "button",
          "body": { "text": "Confirm your appointment?" },
          "action": {
            "buttons": [
              { "type": "reply", "reply": { "id": "btn_yes", "title": "Yes" } },
              { "type": "reply", "reply": { "id": "btn_no", "title": "No" } }
            ]
          }
        }
      }'
    ```

    See [Send buttons](/docs/whatsapp/send-messages/buttons), [Send lists](/docs/whatsapp/send-messages/lists), and [Sending Flows](/docs/whatsapp/flows/sending-flows).
  </Tab>

  <Tab title="Read and typing">
    Both platforms mark a message read on the send endpoint, with Meta's status body, so this moves unchanged:

    ```bash theme={null}
    curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/1234567890/messages \
      -H "X-API-Key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "messaging_product": "whatsapp",
        "status": "read",
        "message_id": "wamid.HBgNMTU1NTE0OTU5Nzg1...",
        "typing_indicator": { "type": "text" }
      }'
    ```

    Keep `typing_indicator` to mark the message read and show the indicator. Drop it to only mark read. See [Mark as read](/docs/whatsapp/send-messages/mark-read).

    Reactions are their own message type, `type: "reaction"`, in both. See [Send reaction](/docs/whatsapp/send-messages/reaction).
  </Tab>
</Tabs>

The [TypeScript SDK](/docs/whatsapp/typescript-sdk/introduction) wraps all of this, so you do not have to write the envelopes by hand.

## Step 4: Update webhooks

360dialog sends you Meta's webhook payload as-is. Kapso can forward the same payload, send its own event format, or both: the two webhook kinds register independently on the same number.

### Keep your Meta parser

Register a webhook with `"kind": "meta"` and Kapso forwards Meta's exact payload with no reshaping. Your `entry[].changes[].value` parser keeps working unchanged:

```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "whatsapp_webhook": {
      "kind": "meta",
      "url": "https://yourapp.com/webhooks/whatsapp",
      "secret_key": "your-signing-secret"
    }
  }'
```

Kapso adds two headers on every delivery: `X-Webhook-Signature` (see [below](#signature-verification)) and `X-Idempotency-Key` for deduplication. If you authenticated 360dialog webhooks with custom headers, pass the same `headers` object here.

### Kapso events

Kapso's own webhook kind unwraps Meta's envelope into one event per occurrence, named in the `X-Webhook-Event` header, with the payload flattened to `message`, `conversation`, and `phone_number_id`:

```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/1234567890/webhooks \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "whatsapp_webhook": {
      "url": "https://yourapp.com/webhooks/whatsapp",
      "events": [
        "whatsapp.message.received",
        "whatsapp.message.sent",
        "whatsapp.message.delivered",
        "whatsapp.message.read",
        "whatsapp.message.failed"
      ],
      "secret_key": "your-signing-secret"
    }
  }'
```

What you parse out of Meta's envelope today maps to:

| Meta payload (what 360dialog forwards)               | Kapso                                                                                            |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `value.messages[]`, field `messages`                 | `whatsapp.message.received`, one event per message                                               |
| `value.statuses[].status: "sent"`                    | `whatsapp.message.sent`                                                                          |
| `value.statuses[].status: "delivered"`               | `whatsapp.message.delivered`                                                                     |
| `value.statuses[].status: "read"`                    | `whatsapp.message.read`                                                                          |
| `value.statuses[].status: "failed"`, with `errors[]` | `whatsapp.message.failed`, errors in `message.kapso.statuses[].errors[]`                         |
| `value.metadata.phone_number_id`                     | `phone_number_id`, at the top level                                                              |
| `entry[].id` (WABA ID)                               | Implicit in the registered number                                                                |
| `value.contacts[0].profile.name`                     | `conversation.contact_name`                                                                      |
| `messages[].image.id` and friends                    | `message.kapso.media_url`, ready to fetch                                                        |
| `messages[].referral`                                | On the payload when present. See [Referrals (CTWA)](/docs/platform/whatsapp-data#referrals-ctwa) |
| None                                                 | `conversation.id`, `message.kapso.transcript` for voice notes                                    |

Full payload shapes in [Message events](/docs/platform/webhooks/message-events).

Do not assume `message.from` is present. Some contacts have no visible phone number, and WhatsApp identifies them with a `business_scoped_user_id` instead. See [business-scoped user IDs](/docs/whatsapp/business-scoped-user-ids).

### Webhook configuration

| 360dialog                                                                        | Kapso                                                                                                                                    |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/configs/webhook` with `{url, headers}`                                 | `POST /platform/v1/whatsapp/phone_numbers/{id}/webhooks`                                                                                 |
| `GET /v1/configs/webhook`                                                        | `GET /platform/v1/whatsapp/phone_numbers/{id}/webhooks`                                                                                  |
| `POST /waba_webhook`, WABA-level fallback                                        | None. Register each number                                                                                                               |
| `/multi_webhook`, up to 3 URLs, secondaries fire only after the main returns 200 | Multiple webhooks per number, each delivered independently. Only one can be `kind: "meta"`; fan out extra raw-Meta destinations yourself |
| Account events mixed into the same payload                                       | [Project webhooks](/docs/platform/webhooks/project-webhooks), configured once per project                                                |

### Signature verification

360dialog authenticates webhooks with static custom headers, if you set them. Kapso signs every delivery: HMAC-SHA256 over the raw body, hex digest in `X-Webhook-Signature`, keyed with the `secret_key` you chose at registration.

```javascript theme={null}
const crypto = require('crypto');

// express.raw, not express.json: Kapso signs the exact bytes it sent, and
// re-serializing a parsed body does not reproduce them
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');

  // timingSafeEqual throws on length mismatch, so compare lengths first
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  res.sendStatus(200);
});
```

If you verified the partner-level `x-360dialog-signature`, the scheme is the same HMAC-SHA256 over the raw body. Only the header name and the secret change. See [Security](/docs/platform/webhooks/security) for Python and Ruby.

### Delivery behavior

|          | 360dialog                                       | Kapso                                                             |
| -------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| Retries  | Exponential backoff, documented as lasting days | 3 attempts in about 50 seconds: the first, then 10s and 40s later |
| Timeout  | 5s                                              | 10s                                                               |
| Dedup    | None. Their docs recommend you build it         | `X-Idempotency-Key` header                                        |
| Scope    | Per number, WABA fallback, 3 URLs max           | Per phone number, plus project webhooks                           |
| Batching | None                                            | Optional buffering on `whatsapp.message.received`                 |

See [Advanced](/docs/platform/webhooks/advanced) for buffering and ordering.

## Step 5: Templates

The main difference is how you tell the API which WABA you mean: 360dialog reads it from your API key, Kapso takes it in the path.

**360dialog:**

```bash theme={null}
curl -X POST https://waba-v2.360dialog.io/message_templates \
  -H "D360-API-KEY: YOUR_CHANNEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_update",
    "language": "en_US",
    "category": "UTILITY",
    "components": [
      {
        "type": "BODY",
        "text": "Hi {{1}}, your order is confirmed.",
        "example": { "body_text": [["John"]] }
      }
    ]
  }'
```

**Kapso:**

```bash theme={null}
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/98765432109/message_templates \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_update",
    "category": "UTILITY",
    "language": "en_US",
    "components": [
      {
        "type": "BODY",
        "text": "Hi {{1}}, your order is confirmed.",
        "example": { "body_text": [["John"]] }
      }
    ]
  }'
```

The lifecycle maps directly:

| 360dialog                                      | Kapso                                                           |
| ---------------------------------------------- | --------------------------------------------------------------- |
| `GET /message_templates`                       | `GET /{waba_id}/message_templates`                              |
| `POST /message_templates`                      | `POST /{waba_id}/message_templates`                             |
| `GET /message_templates/{template_id}`         | `GET /{waba_id}/message_templates/{template_id}`                |
| `POST /message_templates/{template_id}` (edit) | `POST /{waba_id}/message_templates?hsm_id=...`                  |
| `DELETE /message_templates` (by name or ID)    | `DELETE /{waba_id}/message_templates?name=...` or `?hsm_id=...` |

Templates belong to the WABA. If the number stayed on a WABA you already owned, they are still there and there is no re-review. If it landed on a new one, recreate them and wait for review.

## Multi-tenant setups

If you run 360dialog's Partner API, clients and channels become customers and phone numbers. Instead of the connect button or a direct signup link, each customer connects through a setup link:

```bash theme={null}
curl -X POST https://api.kapso.ai/platform/v1/customers \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer": {"name": "Acme Corporation", "external_customer_id": "CUS-12345"}}'

curl -X POST https://api.kapso.ai/platform/v1/customers/CUSTOMER_ID/setup_links \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "setup_link": {
      "success_redirect_url": "https://your-app.com/whatsapp/success",
      "meta_billing_mode": "partner_managed"
    }
  }'
```

Send your customer the returned `url`. They log in with Facebook and connect in about five minutes. Where 360dialog redirects back with `client` and `channels` query parameters, Kapso fires `whatsapp.phone_number.created` on your project webhook, with the `customer.id` and `phone_number_id`. See [Onboard customers](/docs/platform/customer-guide) and [Connection detection](/docs/platform/setup-links/detect-connection).

You create customers yourself, so there is no `client_created` event to wait for, and no per-channel API key generation. One API key covers every customer.

Partner webhook events map to [project webhooks](/docs/platform/webhooks/project-webhooks):

| 360dialog partner event                                                                        | Kapso                                                                                             |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `channel_created` / `channel_submitted` / `channel_ready` / `channel_live` / `channel_running` | `whatsapp.phone_number.created`, then `status` on the number                                      |
| `phone_number_migrated`                                                                        | `whatsapp.phone_number.created`                                                                   |
| `waba_disabled_update`                                                                         | `whatsapp.account.disabled` or `.reinstated`, by ban state                                        |
| `waba_account_restriction`                                                                     | `whatsapp.account.restricted`                                                                     |
| `waba_account_violation`                                                                       | `whatsapp.account.violation`                                                                      |
| `waba_account_bsp_removed`                                                                     | `whatsapp.phone_number.disconnected`                                                              |
| `waba_phone_number_removed`                                                                    | `whatsapp.phone_number.deleted`                                                                   |
| `account_offboarded`                                                                           | `whatsapp.phone_number.offboarded`                                                                |
| `user_preferences`                                                                             | `whatsapp.contact.marketing_preference_changed`, on the number's webhook, not the project webhook |
| `phone_number_quality_changed`                                                                 | None. Poll `GET /platform/v1/whatsapp/phone_numbers`                                              |
| `waba_template_status_changed` / `.category_changed` / `.quality_score_changed`                | None. Poll `GET /{waba_id}/message_templates`                                                     |
| `business_verification_status_update`                                                          | None. Dashboard, or a raw Meta webhook                                                            |
| `cancellation_requested` / `.revoked` / `.processed`                                           | None. No cancellation flow to track                                                               |

Partner balance and usage endpoints have no equivalent. Credits live in project settings, and message history is queryable per number.

## Billing

360dialog charges a per-number monthly license and prepays Meta from your conversation funds. Kapso runs on project credits, or direct Meta billing:

|                    | 360dialog                                         | Kapso                                                                                                |
| ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Who pays Meta      | 360dialog, from prepaid conversation funds        | Kapso from project credits, or your business directly                                                |
| Per-number license | 49 to 249 EUR/month by plan, renews on the 1st    | Numbers included by plan, then $5 to $10 per extra number. See [pricing](/docs/whatsapp/pricing-faq) |
| Marketing sends    | 7% surcharge over Meta rates via `POST /messages` | Meta rates, no surcharge                                                                             |
| Out of funds       | Sends fail                                        | Paid sends pause until credits are added, or Meta decides                                            |

Choose the billing mode when the WABA is connected. See [Meta message billing](/docs/whatsapp/meta-message-billing) and [pricing](/docs/whatsapp/pricing-faq).

## Feature map

| Feature                 | 360dialog                               | Kapso                                                                                                                 |
| ----------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| WhatsApp messaging      | `POST /messages`, key-scoped            | [`POST /{phone_number_id}/messages`](/docs/whatsapp/send-messages/text)                                               |
| Interactive messages    | Meta `interactive` object               | [Same object](/docs/whatsapp/send-messages/buttons)                                                                   |
| Templates               | `/message_templates`                    | [`/{waba_id}/message_templates`](/docs/whatsapp/templates/simple-text)                                                |
| WhatsApp Flows          | Full Flows API                          | [Full Flows API](/docs/whatsapp/flows/overview), including hosted data endpoints                                      |
| Bulk messaging          | Your own loop                           | [Broadcasts](/docs/platform/broadcasts/overview), with CSV upload and scheduling                                      |
| Conversation automation | None. API only                          | [Workflows](/docs/workflows/introduction) with AI steps and human handoff                                             |
| Team inbox              | None. API only                          | [Included](/docs/platform/inbox/overview), plus an [embeddable iframe](/docs/platform/inbox/embedded)                 |
| Contacts                | Block list only                         | [Contacts](/docs/platform/whatsapp-data#contacts) with `metadata`                                                     |
| Message history         | None. Stateless forwarding              | [`/whatsapp/messages`](/docs/platform/whatsapp-data), `/whatsapp/conversations`                                       |
| CTWA attribution        | `referral` in the Meta payload          | [Same, plus an Ads view](/docs/platform/whatsapp-data#referrals-ctwa)                                                 |
| WhatsApp calling        | Calling API                             | [Supported](/docs/whatsapp/typescript-sdk/calls), with call logs                                                      |
| Business profile        | `/whatsapp_business_profile`            | [Display names](/docs/whatsapp/display-names), [usernames](/docs/whatsapp/business-usernames)                         |
| Health check            | `GET /health_status`                    | `GET /platform/v1/whatsapp/phone_numbers/{phone_number_id}/health`                                                    |
| Multi-tenant onboarding | Partner API, connect button             | [Setup links](/docs/platform/setup-links/create-and-configure) your customer completes                                |
| Sandbox                 | `START` to a shared number, `/v1` paths | [6-character code](/docs/how-to/whatsapp/use-sandbox-for-testing), production paths                                   |
| Rate limits             | Undocumented                            | [100-2,000 req/min by plan](/api/rate-limits)                                                                         |
| SDKs                    | None                                    | [TypeScript](/docs/whatsapp/typescript-sdk/introduction), [MCP server](/docs/whatsapp/mcp), [CLI](/docs/whatsapp/cli) |

Things Kapso adds:

* Automatic voice-note transcription on inbound audio
* [Functions](/docs/functions/overview) on Cloudflare Workers, as workflow steps, agent tools, or plain endpoints
* [Findings](/docs/platform/findings), which reads ended conversations with AI and groups the recurring problems
* Conversation lifecycle webhooks, so you can act when a conversation goes quiet or ends

## What does not map

* **Groups API, Payments API, RCS.** WhatsApp one-to-one messaging only.
* **Template archiving.** `POST /message_templates/archive` is a 360dialog feature, not Meta's. Delete or keep.
* **Cascading multi-webhooks.** Kapso webhooks deliver independently; there is no primary that gates the secondaries.

## Cutover

Migrate one number at a time. A number's WhatsApp registration and webhook routing move as a unit, so there is no gradual split per number.

| Phase   | Actions                                                                                                                                                           |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prep    | Create the project and API key, build against a [sandbox](/docs/how-to/whatsapp/use-sandbox-for-testing) number, download any media you still need from 360dialog |
| Pilot   | Move one low-traffic number end to end. Budget for the time the number is down, and for Meta's template review on a new WABA                                      |
| Rollout | Migrate remaining numbers in batches; for multi-tenant, one customer at a time                                                                                    |
| Cutoff  | Cancel the channel subscription in the 360dialog Hub, then request the refund of unused conversation funds                                                        |

The license bills until you cancel, and it renews on the 1st, so time the cancellation after the number is confirmed sending on Kapso.

<Warning>
  360dialog stores no message history to export, but uploaded media expires 30 days after last use and webhook media after 7 days. Download anything you still reference by media ID before the old keys die.
</Warning>

## Troubleshooting

| Symptom                                | 360dialog equivalent       | Fix                                                                                                                                    |
| -------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized`                     | `D360-API-KEY` rejected    | Send `X-API-Key: YOUR_API_KEY`                                                                                                         |
| `404` on send                          | `POST /messages`           | The sender moved into the path: `POST /{phone_number_id}/messages`                                                                     |
| Media download fails                   | Lookaside hostname rewrite | Fetch `download_url` from `GET /{media_id}?phone_number_id=...`, or use `message.kapso.media_url` from the webhook                     |
| Number cannot be connected             | None                       | Two-step verification is still on, or the number is still on the old WABA. See [Step 1](#step-1-connect-your-number)                   |
| `(#131047)` re-engagement message      | Same Meta error            | Outside the 24-hour window. Send an approved template                                                                                  |
| Template not found                     | Same Meta error            | Templates are addressed by `name` + `language`, and must exist and be approved on this WABA                                            |
| Templates missing after the move       | None                       | They belonged to the old WABA. Recreate them on the new one and wait for review                                                        |
| Webhook signature mismatch             | No signature before        | HMAC-SHA256 over the raw body, checked against `X-Webhook-Signature`                                                                   |
| Webhook missed while endpoint was down | Days of redelivery         | 3 attempts in about 50 seconds. Return `200` fast, process from a queue, backfill from [message history](/docs/platform/whatsapp-data) |
| Marketing template refused with `422`  | None                       | The contact stopped marketing on that number. Check [marketing preferences](/docs/whatsapp/templates/marketing-opt-outs)               |
| `429`                                  | Undocumented               | Back off on `Retry-After`; see [rate limits](/api/rate-limits)                                                                         |

## Node.js example

The example keeps a raw Meta parser via a `kind: "meta"` webhook, which is the shortest path off 360dialog: the send function changes two lines, and the handler gains signature verification.

```javascript theme={null}
const crypto = require('crypto');
const express = require('express');

const app = express();

// Any durable queue works. 360dialog retried webhooks for days; Kapso
// stops after about 50 seconds, so the queue is what absorbs downtime.
const queue = require('./queue');

const KAPSO = 'https://api.kapso.ai/meta/whatsapp/v24.0';
const headers = {
  'X-API-Key': process.env.KAPSO_API_KEY, // was: D360-API-KEY per number
  'Content-Type': 'application/json',
};

// 360dialog: POST https://waba-v2.360dialog.io/messages, sender implied by key.
// Kapso: same body, sender in the path.
async function sendText({ phoneNumberId, to, text }) {
  const res = await fetch(`${KAPSO}/${phoneNumberId}/messages`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      messaging_product: 'whatsapp',
      to,
      type: 'text',
      text: { body: text },
    }),
  });
  const body = await res.json();

  if (!res.ok) {
    throw new Error(`Send failed (${res.status}): ${JSON.stringify(body.error ?? body)}`);
  }

  return body.messages[0].id; // wamid, same as before
}

// kind: "meta" webhook. The payload is Meta's envelope, exactly what
// 360dialog forwarded, so the parsing below is your existing code.
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), async (req, res) => {
  // New: 360dialog sent no signature. Kapso signs the raw body.
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(req.headers['x-webhook-signature'] ?? '', 'utf8');

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(req.body);
  const idempotencyKey = req.headers['x-idempotency-key'];

  for (const entry of payload.entry ?? []) {
    for (const change of entry.changes ?? []) {
      const value = change.value ?? {};

      for (const message of value.messages ?? []) {
        await queue.add('inbound', {
          idempotencyKey,
          // `from` can be absent on a BSUID-only contact, so carry the
          // scoped IDs too or the job cannot tell you who wrote in.
          from: message.from ?? null,
          fromUserId: message.from_user_id ?? null,
          fromParentUserId: message.from_parent_user_id ?? null,
          username: message.username ?? null,
          text: message.text?.body,
          phoneNumberId: value.metadata?.phone_number_id,
        });
      }

      for (const status of value.statuses ?? []) {
        await queue.add('status', {
          idempotencyKey,
          id: status.id,
          status: status.status,
          errors: status.errors ?? [],
        });
      }
    }
  }

  res.sendStatus(200);
});
```

## Need help

* [Send messages](/docs/whatsapp/send-messages/text) and [webhooks](/docs/platform/webhooks/overview)
* [API reference](/api/introduction)
* [WhatsApp support](https://wa.me/16266694464?text=Hi!%20I%20need%20help%20migrating%20from%20360dialog)
