developers
ES Sign up

Guides

Compatible with the Meta Cloud API

If you already have code built for the WhatsApp Cloud API (graph.facebook.com/v21.0/{phone_number_id}/messages), you don't need to rewrite it: swap the base URL for heysteff's and the token for your API key, and it keeps working. Every message goes through heysteff just like one sent with POST /v1/messages: it shows up in the web and mobile inbox, counts toward your plan, respects the 24-hour window and appears in the API Logs.

The only change#

Meta directlyheysteff
Base URLhttps://graph.facebook.com/v21.0https://app.heysteff.com/api/meta/whatsapp/v21.0
TokenAuthorization: Bearer EAAG… (Meta system token)Authorization: Bearer sk_live_… (your heysteff API key)
VersionWhichever you useAny v<n>.<n>: v21.0, v22.0, v24.0… heysteff answers the same for all of them.
Before
curl https://graph.facebook.com/v21.0/106540352242922/messages \
  -H "Authorization: Bearer $META_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "messaging_product": "whatsapp", "to": "56912345678", "type": "text", "text": { "body": "Hi Ana" } }'
After
curl https://app.heysteff.com/api/meta/whatsapp/v21.0/106540352242922/messages \
  -H "Authorization: Bearer $HEYSTEFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "messaging_product": "whatsapp", "to": "56912345678", "type": "text", "text": { "body": "Hi Ana" } }'

The response has Meta's shape:

200
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "56912345678", "wa_id": "56912345678" }],
  "messages": [{ "id": "6f1d2c1e-6a1b-4c43-9a51-2f9f6a0e8b7d" }]
}

With a Cloud API client that lets you change the base URL (Meta's Node SDK, whatsapp-api-js or your own fetch wrapper) you only set that URL and the token.

The phone_number_id#

It is the same Meta id of your WhatsApp Cloud number, now connected to heysteff: see it in the console, Numbers section, and with GET /{phone_number_id}. Any WhatsApp Cloud number in your workspace works: one you connected, a heysteff number or one a customer of yours connected with their setup link. An id outside your workspace returns the same error Graph returns for someone else's object (code 100, subcode 33).

Sandbox. With an sk_test_ key, the phone_number_id is the heysteff shared number's (the console shows it in the Sandbox section) and you can only write to phones that joined. You also only see sandbox media: what you uploaded with an sk_test_ key and what was sent to the shared number; any other file answers 403 test_key_not_allowed. See Sandbox.

What it covers#

RouteWhat forScope
POST /{phone_number_id}/messagesSend text, image, audio, video, document, sticker, location, contacts, reaction, interactive (buttons, list, cta_url, location_request_message) and templates. Also mark as read with status: "read" and typing_indicator.messages:write
POST /{phone_number_id}/mediaUpload a file (multipart, file field). Returns { "id": "…" }.messages:write
GET /{media_id}url, mime_type, sha256 and file_size. The url downloads with the same Bearer.messages:write
DELETE /{media_id}Delete a file you uploaded with POST /{phone_number_id}/media. The rest of your workspace media cannot be deleted here.messages:write
GET /{phone_number_id}Verified name, display number, quality and platform.messages:write
GET and POST /{phone_number_id}/whatsapp_business_profileRead and change the business profile (about, address, description, email, websites, vertical).messages:write to read, numbers:write to change
GET /{waba_id}/phone_numbersThe numbers of that WABA connected to your workspace.messages:write
GET, POST and DELETE /{waba_id}/message_templatesList, create (they go to Meta review) and delete templates (?name=).messages:write to list, templates:write to create and delete

Meta account administration routes are not here (registering a number, two-step verification, app subscriptions, WABA analytics): heysteff does that for you when the number is connected. For everything that is not WhatsApp (Instagram, Messenger, SMS, email) use the heysteff API.

Message ids#

How sends behave#

Webhooks in Meta format#

To keep your Meta webhook receiver unchanged, create the webhook with format: "meta" (in the console, Webhooks section, choose "Meta Cloud API", or by API):

curl
curl https://app.heysteff.com/api/v1/webhooks \
  -H "Authorization: Bearer $HEYSTEFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://api.yourapp.com/whatsapp/webhook", "format": "meta" }'

That webhook receives the Cloud API payload: object: "whatsapp_business_account" and entry[].changes[].value with metadata.phone_number_id, contacts and messages (inbound) or statuses (sent, delivered, read, failed). Only WhatsApp Cloud message events arrive (message:received, message:sent, message.delivered, message.read, message.failed); other channels and events stay in the heysteff format.

Signature. It comes in X-Hub-Signature-256: sha256=<hex>, the HMAC-SHA256 of the raw body, like Meta's. The difference is the key: instead of your Meta app secret, use the secret heysteff gave you when you created the webhook.

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

// rawBody: the body as it arrived. secret: the heysteff webhook's.
function isFromHeysteff(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

heysteff does not run Meta's initial verification (the GET with hub.challenge): the URL is live once created. The console's "Send test event" button sends a sample text message in Meta's shape with the same signature.

Errors#

Errors have the Graph API shape, with Cloud API codes wherever there is an equivalent:

400
{
  "error": {
    "message": "(#100) Invalid parameter",
    "type": "OAuthException",
    "code": 100,
    "error_data": { "messaging_product": "whatsapp", "details": "text.body: Required" },
    "fbtrace_id": "Az8or2yhqkZfEZ-_4Qn_Bam",
    "heysteff_code": "validation_error"
  }
}
CodeWhen
190 (401)The API key is missing, unknown, revoked or expired.
200 (403)The key lacks the scope, or it is a test key used outside the sandbox.
100Invalid parameter; with subcode 33, an id that does not exist or is not in your workspace.
130429 (429)The key's per-minute call limit, or the sandbox daily cap.
131030Sandbox: the phone has not joined.
131042Your plan reached its monthly message cap.
131047The 24-hour window is closed.
131053The file does not exist or could not be downloaded.
132000, 132001The template does not exist, is not approved or is missing variables.
2 (424)Meta did not answer when reading or changing the profile or a template. Retry.

heysteff_code is heysteff's code for the same error (see Errors and limits); clients built for Meta ignore it.

Which API to use#

This API is for moving what you already have without touching it. For new code, prefer POST /v1/messages: with the same body you send through WhatsApp, Instagram, Messenger, SMS or email by changing only channel. Both live in the same workspace with the same key.