Documentation
Everything you need to receive and send WhatsApp messages with Kobry.
Quickstart
Kobry connects the number you already use on the WhatsApp Business app to the Cloud API (Meta calls this Coexistence). You keep chatting from your phone, and your automations get every message through a webhook and can reply through a simple REST API.
- Create your account and choose a plan (Vodafone Cash in Egypt, or Visa/Mastercard from any country).
- Open Connections → Connect a number and finish Meta’s signup with the number on your WhatsApp Business app.
- Paste your webhook URL (for example your n8n Webhook node) and press Send test event.
- Create an API key in API keys and send your first message.
Your WhatsApp Business app must be version 2.24.17 or newer. Keep the phone app — you keep using it as before.
Receiving messages (webhooks)
Every event Meta sends for your number — new messages, delivery and read statuses, messages you send from the phone app (smb_message_echoes), template updates — is forwarded to your URL as an HTTPS POST. The body is Meta’s original JSON, unchanged, so Meta’s documentation applies as-is.
{
"object": "whatsapp_business_account",
"entry": [{
"id": "102290129340398",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "201000000000", "phone_number_id": "106540352242922" },
"contacts": [{ "profile": { "name": "Mona" }, "wa_id": "201012345678" }],
"messages": [{
"from": "201012345678",
"id": "wamid.HBgM...",
"timestamp": "1700000000",
"type": "text",
"text": { "body": "Is the blue hoodie available?" }
}]
}
}]
}]
}- Answer with any 2xx status within 5 seconds. Do slow work after you respond.
- If your server is down or slow, we retry the event for up to 24 hours (after 10s, 1m, 5m, 15m, 30m, then hourly). It shows as Retrying in Webhook logs until it is delivered. Because of retries you may rarely see the same event twice — use the message
idto ignore duplicates. - Your URL must be HTTPS on port 443, must not redirect, and must point to a public address.
- We never store message content — except an event your server didn't accept yet: it is held encrypted while we retry, deleted the moment it's delivered, and never kept longer than 24 hours. Only metadata (event type, result, timing) is kept for 7 days in Webhook logs.
Verifying the signature
Every request we send carries X-Kobry-Signature: sha256=<hex> — an HMAC-SHA256 of the raw request body using your connection’s signing secret (Connections → Signing secret). Check it so nobody else can fake events to your URL.
const crypto = require('crypto');
app.post('/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.KOBRY_SECRET)
.update(req.body) // raw bytes, not re-serialized JSON
.digest('hex');
const received = req.get('X-Kobry-Signature') || '';
const ok = expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!ok) return res.sendStatus(401);
res.sendStatus(200); // answer fast…
handle(JSON.parse(req.body)); // …then do the work
});import hmac, hashlib
def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")Sending messages
Send with POST https://kobry.app/v1/messages and your API key in the Authorization header. The body is Meta’s message object (so every message type Meta supports works), plus an optional connection_id when your workspace has more than one number.
curl -X POST https://kobry.app/v1/messages \
-H "Authorization: Bearer kob_live_…" \
-H "Content-Type: application/json" \
-d '{
"to": "201012345678",
"type": "text",
"text": { "body": "Yes, size L is in stock ✅" }
}'curl -X POST https://kobry.app/v1/messages \
-H "Authorization: Bearer kob_live_…" \
-H "Content-Type: application/json" \
-d '{
"to": "201012345678",
"type": "template",
"template": {
"name": "order_ready",
"language": { "code": "ar" },
"components": [{
"type": "body",
"parameters": [{ "type": "text", "text": "Mona" }, { "type": "text", "text": "A-1001" }]
}]
}
}'{
"messaging_product": "whatsapp",
"contacts": [{ "input": "201012345678", "wa_id": "201012345678" }],
"messages": [{ "id": "wamid.HBgM..." }]
}WhatsApp only allows free-form messages within 24 hours of the customer’s last message. Outside that window, send an approved template.
GET /v1/connections lists your numbers and their ids. A key can be limited to one number when you create it.
Templates
Create and manage templates from the dashboard (Templates) or the API. Meta reviews every new template, usually within minutes.
| Request | What it does |
|---|---|
GET /v1/templates | List templates with their status (APPROVED, PENDING, REJECTED…). |
POST /v1/templates | Create a template — same body as Meta (name, language, category, components). |
DELETE /v1/templates/{name} | Delete a template by name. |
Add ?connection_id=… (or "connection_id" in the body) when your workspace has several numbers.
n8n guide
Receive WhatsApp messages in n8n and reply — no code needed for the basics.
- Webhook node — HTTP Method
POST, choose a path, Respond: Immediately. Copy the Production URL. - In Kobry: Connections → paste that URL → Save URL → Send test event. Activate your workflow first, or use the Test URL while testing.
- Read the message: the text is at
{{ $json.body.entry[0].changes[0].value.messages[0].text.body }}and the sender at…messages[0].from. - IF node — continue only when
messagesexists (status updates arrive on the same webhook). - HTTP Request node to reply — Method
POST, URLhttps://kobry.app/v1/messages, Authentication → Generic Credential → Header Auth (NameAuthorization, ValueBearer kob_live_…), Body JSON as in “Sending messages”.
To verify the signature in n8n, turn on Raw Body in the Webhook node options and add a Code node before your logic:
const crypto = require('crypto');
const item = $input.first();
const raw = Buffer.from(item.binary.data.data, 'base64');
const expected = 'sha256=' + crypto
.createHmac('sha256', 'PASTE_YOUR_SIGNING_SECRET')
.update(raw)
.digest('hex');
const received = item.json.headers['x-kobry-signature'] || '';
if (expected !== received) throw new Error('Invalid signature');
return [{ json: JSON.parse(raw.toString('utf8')) }];Self-hosted n8n blocks require('crypto') unless the environment variable NODE_FUNCTION_ALLOW_BUILTIN=crypto is set.
Errors and limits
Every error has the same shape: { "error": { "code": "…", "message": "…" } }.
| Status | code | Meaning |
|---|---|---|
| 400 | meta_rejected | Meta refused the request — the message explains why. |
| 401 | unauthorized | Missing, wrong or revoked API key. |
| 402 | plan_required | Your workspace has no active plan. |
| 403 | forbidden_connection | This key is limited to another number. |
| 404 | connection_not_found | No such number in your workspace. |
| 409 | connection_not_ready | The number is paused or not connected yet. |
| 413 | payload_too_large | Body over 64 KB. |
| 422 | invalid_request / connection_required | Fix the body, or pass connection_id. |
| 429 | rate_limited | Over 600 requests per minute for this key — see Retry-After. |
| 502 | meta_unavailable | Meta had a problem. Try again shortly. |
Good to know (Coexistence)
- Group chats don’t sync to the API.
- After connecting, disappearing messages, view once, live location and broadcast lists are turned off on the app.
- Coexistence numbers send up to about 20 messages per second.
- Open the WhatsApp Business app regularly (at least every two weeks) — Meta may disconnect inactive numbers.
- Meta’s messaging fees are paid by you directly to Meta. We add no markup.