developers
ES Sign up

Guides

Migrate from Twilio, 360dialog, YCloud or Kapso

If you send WhatsApp today with Twilio, 360dialog, YCloud or Kapso, moving to heysteff means changing one call: everything goes through the same POST /v1/messages, and that same endpoint also reaches Instagram, Messenger, SMS and email. This guide collects the endpoint, webhook and concept equivalents, and the steps to switch without downtime.

The details for each provider come from their public documentation (reviewed in November 2026). Where we could not confirm a detail, we describe it in general terms: check your provider's documentation before deleting code.

What changes#

From the Meta Cloud API directly#

If you call graph.facebook.com today with your own Meta token, you don't have to rewrite anything: change the base URL to https://app.heysteff.com/api/meta/whatsapp/v21.0 (any version works) and the token to your heysteff API key (Authorization: Bearer sk_live_…). The phone_number_id stays the same. For your webhook receiver, create the webhook with format: "meta": the same Meta payload arrives, signed with X-Hub-Signature-256 and the webhook's secret. Details in Compatible with Meta. If you come from a provider that exposes the Cloud API (360dialog, Kapso), you can also use that same URL instead of moving to /v1.

Endpoint equivalents#

OperationheysteffTwilio360dialogYCloudKapso
Base and authhttps://app.heysteff.com/api/v1
Authorization: Bearer
https://api.twilio.com/2010-04-01
Basic auth
https://waba-v2.360dialog.io
D360-API-KEY
https://api.ycloud.com/v2
X-API-Key
https://api.kapso.ai/meta/whatsapp/v24.0
X-API-Key
Send a messagePOST /v1/messagesPOST /Accounts/{AccountSid}/Messages.json (form)POST /messagesPOST /whatsapp/messages or /whatsapp/messages/sendDirectlyPOST /{phone_number_id}/messages
Send a templateThe same POST /v1/messages with content_type: "template"ContentSid and ContentVariables in the same POSTThe same POST /messages with type: "template"The same POST with type: "template"The same POST with type: "template"
List templatesGET /v1/templatesGET https://content.twilio.com/v1/ContentGET /message_templatesCheck their documentation (create: POST /whatsapp/templates)GET /{business_account_id}/message_templates
Upload a filePOST /v1/media (multipart, field file), or a public https URL in media.urlPublic URL in MediaUrlPOST /media (multipart)POST /whatsapp/media/{phoneNumber}/uploadPOST /{phone_number_id}/media
ContactsGET /v1/contacts, POST /v1/contactsHandled in your own systemCheck their documentationPOST /contact/contactsGET /{phone_number_id}/contacts
Conversations and historyGET /v1/conversations, GET /v1/conversations/{id}/messagesCheck their documentationCheck their documentationCheck their documentationCheck their documentation

Before and after, with a text message:

Twilio (before)
curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Messages.json" \
  -u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
  --data-urlencode "From=whatsapp:+14155550100" \
  --data-urlencode "To=whatsapp:+56912345678" \
  --data-urlencode "Body=Hola Ana, tu pedido ya va en camino"
heysteff (after)
curl https://app.heysteff.com/api/v1/messages \
  -H "Authorization: Bearer $HEYSTEFF_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{ "channel": "whatsapp_cloud", "to": "+56912345678", "content": "Hi Ana, your order is on its way" }'

And a template in the Cloud API format (360dialog, YCloud and Kapso use this body or one very close to it):

Cloud API (before)
{
  "messaging_product": "whatsapp",
  "to": "56912345678",
  "type": "template",
  "template": {
    "name": "pedido_despachado",
    "language": { "code": "es" },
    "components": [{ "type": "body", "parameters": [
      { "type": "text", "text": "Ana" }, { "type": "text", "text": "#1042" }
    ] }]
  }
}
heysteff (after)
{
  "channel": "whatsapp_cloud",
  "to": "+56912345678",
  "content_type": "template",
  "template": { "name": "pedido_despachado", "language": "es", "body_params": ["Ana", "#1042"] }
}

If your template has an image header or buttons with variables, send template.components in the Meta Graph API shape: heysteff passes it through and it wins over body_params. More in Message types.

Webhooks and signatures#

heysteff sends one format for every channel: a JSON POST with the envelope { id, tenant_id, event, ts, data }. An inbound message carries in data everything you need to reply (channel, external_user_id, channel_connection_id).

heysteffTwilio360dialogYCloudKapso
Where you set it upConsole, Webhooks sectionTwilio Console (inbound) and StatusCallback per messagePOST /v1/configs/webhook or POST /waba_webhookPOST /v2/webhookEndpointsPOST /platform/v1/whatsapp/phone_numbers/{id}/webhooks
Inbound messagemessage:receivedForm POST to your inbound URLmessages object in the Meta payloadwhatsapp.inbound_message.receivedwhatsapp.message.received
Status of a sent messagemessage:sent and message.failedMessageStatus on StatusCallbackstatuses objectwhatsapp.message.updatedwhatsapp.message.sent, .delivered, .read, .failed
Signature headerX-Heysteff-Signature (v1= + hex) and X-Heysteff-TimestampX-Twilio-SignatureNo documented signature: authenticate with custom headers you set when creating the webhookYCloud-Signature (t=…,s=…)X-Webhook-Signature
What is signedHMAC-SHA256 of ${timestamp}.${raw body}HMAC-SHA1 of the URL plus the POST parameters, with your auth tokenNot applicableHMAC-SHA256 of t + "." + raw bodyHMAC-SHA256 of the raw body
Retries and duplicatesDeduplicate by X-Heysteff-Delivery-Id, repeated on every retryCheck their documentationMeta retries if you do not answer 200Retries with growing delaysDeduplicate by X-Idempotency-Key

Coming from YCloud, the signature is almost the same: change the header and the v1= prefix. Coming from Kapso, add the timestamp to the signed string. Coming from Twilio, stop signing the URL: heysteff signs the raw body.

Node.js
import crypto from 'node:crypto';

// rawBody: the body exactly as received, unparsed.
function isFromHeysteff(rawBody, timestamp, signature, secret) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The SDK does it for you with verifyWebhook, and heysteff webhooks trigger in the CLI sends a signed event to localhost for testing. Details in Webhooks.

Concepts#

ConceptIn heysteffWhat it was called before
Number or accountA connected channel (channel_connection_id). One workspace holds WhatsApp numbers, Instagram and Messenger accounts, SMS and email.Twilio: the From (whatsapp:+…) or a Messaging Service. 360dialog: the API key of each number. YCloud: the from. Kapso: the phone_number_id in the URL.
TemplateSent by name and language (template.name, template.language) with body_params in order. GET /v1/templates lists the approved ones.Twilio: a ContentSid from its Content API. With Cloud API format providers, the same Meta name and language.
24-hour windowSame as everywhere: WhatsApp only allows free-form text within 24 hours of the customer's last message; outside it, a template. Outside the window the API answers with a clear error instead of accepting a message that will not be delivered.It is a WhatsApp rule, not a provider rule: it does not change.
SandboxAn sk_test_ key and the shared heysteff number: you join up to 5 phones with a code from the console. See Sandbox.Twilio: shared number and join <code>. 360dialog: a START message to their sandbox number. Kapso: a 6-character code to their sandbox number.
Inbox and peopleEvery conversation also shows up in the inbox on the web and in the mobile app. POST /v1/conversations/{id}/escalate hands it to a person.Depends on the provider.

Steps to switch#

  1. Create your account and API key. Create a developer account, run curl -fsSL https://developers.heysteff.com/install.sh | bash and then heysteff setup: it takes you to the console, stores the key and sends a test message.
  2. Test in the sandbox. With an sk_test_ key, switch your calls to POST /v1/messages without touching your current number.
  3. Move your webhook receiver. Create the webhook in the console, store the secret and change the signature check (see above). While you migrate, your server can accept both formats.
  4. Connect a number. In the console, Numbers section, connect your number through Meta's signup window or use a heysteff number. If your number lives with another provider today, follow Meta's process to move it to another account or provider, and check with your current provider what happens to its templates.
  5. Check your templates. GET /v1/templates shows the approved ones for the connected number. Coming from Twilio, replace each ContentSid with the template name and language.
  6. Switch to sk_live_ and turn off the old provider once your messages show up in the inbox and in the delivery log.

Using Claude Code, Cursor or Codex? Install the heysteff skill (see Coding agents) and ask it to migrate your integration with this guide.