migrate-to-sent

Plans and executes a migration from Twilio, Sinch, Infobip, Vonage, or MessageBird/Bird to Sent v3 — mapping send calls, status vocabularies, webhook signature schemes, opt-out stores, templates, and tenancy models, then cutting over safely with dual-run and rollback. Use when replacing an incumbent CPaaS provider, translating provider code or webhook handlers to Sent, or planning a phased cutover and its verification gates.

sentdm/sent-plugin1.8k installsMITSynced Aug 27

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: migrate-to-sent
description: Plans and executes a migration from Twilio, Sinch, Infobip, Vonage, or MessageBird/Bird to Sent v3 — mapping send calls, status vocabularies, webhook signature schemes, opt-out stores, templates, and tenancy models, then cutting over safely with dual-run and rollback. Use when replacing an incumbent CPaaS provider, translating provider code or webhook handlers to Sent, or planning a phased cutover and its verification gates.
license: MIT
---

# Migrate to Sent

Every migration from a major CPaaS provider hits the same five translation problems. Work them in this order, because the first one silently doubles cost and is invisible in tests.

## 1. Ordered fallback becomes automatic routing

Incumbent platforms express cross-channel delivery through different caller-side arrays, failover objects, messaging-service features, or application-level priority configuration. Do not assume those shapes have a direct Sent request-field equivalent.

**Sent's `channel` array is a broadcast list.** Porting an ordered array produces one message and one charge per recipient-channel pair, which passes tests and multiplies production spend. The correct translation is automatic routing — omit `channel` or send `["sent"]` — which lets the platform select a route and reroute across up to three channel-and-provider pairs on the same `message_id`. Details belong to `sent-routing-strategist`; the migration rule is simply: **never port an ordered channel list.**

## 2. Status vocabularies do not line up

Incumbent statuses map onto Sent's, but Sent adds two states that have no equivalent and that break naive retry logic.

| Sent status | Closest incumbent analogue | Migration note |
| --- | --- | --- |
| `QUEUED` | Twilio `queued`, Sinch `QUEUED_ON_CHANNEL` | Accepted, not sent |
| `ROUTED` | no analogue | Route chosen; fires again on reroute |
| `SENT` | Twilio `sent`, Sinch `MESSAGE_SUBMIT` | Provider handoff only |
| `DELIVERED` | `delivered` everywhere | The first proof of handset receipt |
| `READ` | Twilio `read`, Sinch `READ` | WhatsApp and RCS only |
| `FAILED` | `failed`, `undelivered` | May still reroute; not necessarily final |
| `FILTERED` | Twilio error 21610 (opt-out) | **Policy gate. Never retry** |
| `BLOCKED` | account-level errors | **Account precondition.** Fix the account, then resend |
| `SCHEDULED` | no analogue | Quiet-hours parking; resumes automatically |

Two consequences for ported code. Handlers that treat every non-delivered terminal state as retryable will retry consent blocks, which is a compliance failure rather than a bug. And handlers keyed on numeric provider error codes — Twilio's `21610` is the classic — must be rewritten against Sent's string `error.code` families.

## 3. Webhook verification is a rewrite, not a port

No two providers sign the same way, and no Sent SDK ships a verifier.

| Provider | Scheme |
| --- | --- |
| Twilio | `X-Twilio-Signature`, base64 HMAC-**SHA1** over the full URL plus sorted POST parameters |
| Sinch | HMAC-SHA256 over `body.nonce.timestamp`, four `x-sinch-webhook-signature*` headers, or OAuth 2.0 |
| Infobip | Basic, HMAC-SHA256 over the raw body, or OAuth on a notification profile; **the header name is account-configured** |
| Vonage | JWT in `Authorization: Bearer`, or a legacy `sig` parameter |
| MessageBird/Bird | `messagebird-signature`, base64 HMAC-SHA256 over timestamp, URL, and a SHA-256 body hash |
| **Sent** | `x-webhook-signature: v1,{base64}`, HMAC-SHA256 over `{x-webhook-id}.{x-webhook-timestamp}.{raw_body}` |

Sent's key is the signing secret with `whsec_` stripped and the remainder base64-decoded, compared in constant time, with timestamps outside 300 seconds rejected. Because Sent provides no per-event id, dedupe keys must be derived from payload semantics. Build the receiver with `sent-webhook-engineer` rather than adapting the incumbent's verifier.

## 4. Opt-out stores must be reconciled, not migrated by copy

Every provider keeps its own suppression list — Twilio Advanced Opt-Out, Infobip Blocklist, Sinch OPT_IN/OPT_OUT events. Sent enforces consent at the platform level before events reach the application, stores it as `opt_out` on the contact, and applies it **channel-agnostically**: a `STOP` on SMS suppresses WhatsApp and RCS too.

Reconciliation rules: export the incumbent's suppression list before cutover, treat any opt-out on any incumbent channel as a global Sent opt-out, and never clear `opt_out` to "clean up" migrated data. Sent's ten default keywords are `STOP`, `CANCEL`, `UNSUBSCRIBE`, `QUIT`, `END`, `START`, `UNSTOP`, `SUBSCRIBE`, `HELP`, `INFO`, matched only when the entire trimmed body equals the keyword — so incumbent-specific keywords need custom keyword entries. Rewrite any incumbent keyword matcher as an exact local consent mirror and audit mechanism; the matcher must not write consent to Sent again. Consent semantics belong to `sent-two-way-messaging`.

## 5. Templates and tenancy are re-registered, not transferred

WhatsApp templates live with the WABA, so the migration question is whether the WABA moves. Positional placeholders (`{{1}}`, `{{2}}`) become **named** parameters in Sent, which means every call site that passed an ordered array must pass a named map. Approval is asynchronous and arrives as a `templates` webhook event, so build the template inventory before cutover rather than during it.

Tenancy maps as follows, with the boundary decision owned by `sender-profile-architect` and the API work by `sent-profile-provisioning`:

| Incumbent construct | Sent equivalent |
| --- | --- |
| Twilio subaccount | Sender Profile |
| Twilio Messaging Service | routing plus profile configuration, not a caller-side pool |
| Infobip Application or Entity | Sender Profile |
| Sinch Conversation API app | Sender Profile |
| Provider API credential per tenant | Profile-scoped API key, or organization key with `x-profile-id` |

## Migration sequence

1. **Inventory** every send call site, webhook handler, status branch, template, suppression list, and credential. Use `scripts/inventory_scan.py` to find them mechanically.
2. **Map** each item using [references/provider-mapping.md](references/provider-mapping.md), flagging ordered-fallback arrays and numeric error codes as required rewrites.
3. **Stand up Sent in parallel**: credentials, one webhook per environment, verified receiver, templates re-registered and approved.
4. **Prove equivalence in sandbox** with `"sandbox": true`, then with a small live cohort confirmed to `DELIVERED`.
5. **Dual-run** with a traffic split, comparing delivery rates, latency, and cost per message on the same message classes.
6. **Cut over** by message class — lowest-risk transactional first, marketing last — keeping the incumbent receiver live.
7. **Decommission** only after a full billing cycle of clean data, then revoke incumbent credentials.

Sequencing detail, verification gates, and rollback triggers are in [references/cutover-playbook.md](references/cutover-playbook.md).

## Mistakes that survive testing

- Porting an ordered channel array. Doubles cost, never errors.
- Treating `FILTERED` as retryable. Compliance exposure.
- Reusing the incumbent's signature verifier. Every delivery returns 401.
- Assuming `202` means delivered. Sent acknowledges acceptance only.
- Keeping positional template placeholders. Parameters silently mismatch.
- Retrying on `401`. Ten consecutive auth failures lock the credential with escalating lockout.
- Omitting `Idempotency-Key` during dual-run. A timeout retry sends twice.
- Sending `x-profile-id` with a profile-scoped key. Returns `403`.
- Copying an incumbent's `Authorization: Bearer` pattern. Sent authenticates with `x-api-key`.

## Boundaries

This skill owns provider mapping and line-by-line migration planning. Hand the resulting Sent client and resilience work to `sent-integration-starter`, channel semantics to `sent-routing-strategist`, receiver construction to `sent-webhook-engineer`, WhatsApp onboarding to `waba-embedded-signup`, and US campaign registration to `sms-10dlc-registration`.

More API Design skills

← All API Design skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY