REST API
Send WhatsApp messages from your own app — order updates, sign-in codes, reminders — and receive replies by webhook. Keys, endpoints, errors and signed webhooks.
Updated September 25, 2026 · 5 min read
The REST API lets your app send and receive WhatsApp messages on its own, with the same rules as the dashboard and your AI assistant: opt-outs, the 24-hour window, plan limits and new-number warm-up all apply.
Base URL: https://sendfromchat.com/api/v1
Building with Claude Code? Connect SendFromChat (https://sendfromchat.com/api/mcp) and ask it to "add WhatsApp order notifications to my app". It fetches your approved templates and writes the integration for your language.
Authentication
Send an API key as a bearer token:
Authorization: Bearer sfc_live_…
With Claude Code, you never copy a key. Ask it to set up SendFromChat in your project: it creates the key and saves it straight into .env.local, or into Vercel, Netlify, Railway, Fly.io, Heroku, Cloudflare Workers or GitHub Actions secrets through that platform's CLI. The key never appears in the conversation — Claude only handles a one-time code that expires in ten minutes. Live keys are created only after you confirm.
You can also create keys on the dashboard's Developers page. Each key has a name and can be revoked on its own.
- Test keys (
sfc_test_…) run every check a real send would — plan, opt-outs, template, 24-hour window — then stop. Nothing is sent, saved or counted toward your plan. Build with one. - Live keys (
sfc_live_…) send for real.
Keep keys on your server. Never put one in browser or mobile-app code.
Check a key with GET /api/v1: it returns the workspace, whether the key is live or test, and the numbers it can send from.
Send a message
POST /messages
A template, which works any time:
{
"to": "+14155550132",
"template": { "name": "order_shipped", "language": "en_US", "variables": ["Ana", "#4821"] }
}
Free-form text, which only delivers within 24 hours of the person's last message to you:
{ "to": "+14155550132", "text": "Thanks — we're on it." }
to— international format with country code.template.variables— fills{{1}},{{2}}… in order. An object like{"1": "Ana"}works too.from— optional; the id of a connected number. Defaults to your first number.
Response 202:
{ "id": "wamid.HBgL…", "status": "accepted", "to": "+14155550132", "from": "…", "type": "template" }
With a test key the response is 200 with "status": "test".
Sign-in codes
Use a template in the Authentication category and pass the code as variable 1. SendFromChat fills in the copy-code button Meta requires.
{ "to": "+14155550132", "template": { "name": "login_code", "variables": ["482913"] } }
Retries without double-sending
Send an Idempotency-Key header, derived from the event in your app:
Idempotency-Key: order-4821-shipped
A retry with the same key and body returns the first response, with Idempotent-Replayed: true, instead of sending again. Reusing a key with a different body returns idempotency_mismatch.
Check a message
GET /messages/{id} returns its status: sent (Meta accepted it) → delivered → read, or failed with the reason. For updates as they happen, use webhooks.
Templates
GET /templates lists your templates, their status and the variables each needs. Add ?status=approved for only the ones you can send.
Contacts
PUT /contacts creates or updates a contact by phone number. Attributes merge into what's already there.
{ "phone": "+14155550132", "name": "Ana", "attributes": { "plan": "pro" }, "optedOut": false }
Only add people who agreed to hear from you on WhatsApp. Setting optedOut: true blocks template sends to them.
Sequences
Start a drip sequence for someone, for example when they sign up:
POST /sequences/{id}/enrollments with { "phone": "+14155550132" }
Stop it: DELETE /sequences/{id}/enrollments?phone=+14155550132
Webhooks
Add an endpoint on the Developers page, or with a live key:
POST /webhook-endpoints with { "url": "https://app.example.com/webhooks/sendfromchat", "events": ["message.received"] }
The response includes the signing secret, shown once.
| Event | When | data |
|---|---|---|
message.received |
A customer wrote in | messageId, from, name, type, text, receivedAt, whatsappAccountId, conversationId |
message.status |
A sent message changed status | messageId, to, status, error, whatsappAccountId, at |
contact.opted_out |
Someone replied STOP, or was opted out | phone, at |
contact.opted_in |
Someone opted back in | phone, at |
Every request body looks like { "id": "evt_…", "type": "…", "created": "…", "data": { … } }. Deliveries can repeat, so skip id values you've already handled. Answer with any 2xx within 5 seconds; failures are retried with backoff for about a day.
Verifying signatures
Each request carries:
SendFromChat-Signature: t=1790300000,v1=5f2b…
v1 is the hex HMAC-SHA256 of <t>.<raw request body>, keyed with your endpoint's secret. Compute it over the raw body, compare in constant time, and reject timestamps older than five minutes to stop replays.
import crypto from "node:crypto";
function verifySendFromChat(rawBody, header, secret) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Errors
Errors share one shape, with a stable type your app can branch on:
{ "error": { "type": "window_closed", "message": "…" } }
| Status | type |
Meaning |
|---|---|---|
| 400 | invalid_request, missing_variables |
Fix the request |
| 401 | unauthorized |
Missing, wrong or revoked key |
| 402 | sending_paused |
Trial ended or plan limit reached |
| 403 | live_key_required |
That action needs a live key |
| 404 | template_not_found, message_not_found, sequence_not_found |
Nothing with that name or id |
| 409 | no_number, request_in_progress |
No connected number, or the same Idempotency-Key is still running |
| 422 | opted_out, window_closed, template_not_approved, whatsapp_rejected |
WhatsApp's rules — whatsapp_rejected carries Meta's reason |
| 429 | warmup_limit |
A new number has reached today's warm-up cap; retry later |
Limits
Every message sent through the API counts toward your plan's monthly allowance, like messages from the dashboard and your assistant. New numbers are warmed up: template sends are capped at 50, 100, then 200 a day for the first three days, shared with campaigns.
Do this from your AI assistant
SendFromChat connects Claude, ChatGPT, Cursor or any MCP client to the WhatsApp Business API — templates, bulk sends, inbox, chatbots and sequences, just by asking. 30 days free, no card.
Start freeMore docs
Getting started with SendFromChat
From sign-up to your first WhatsApp message in a few minutes — connect a number in one click, connect your AI assistant, send.
Connect your AI assistant
Add SendFromChat to Claude, ChatGPT, Claude Code, Cursor, VS Code, Windsurf, Gemini CLI, Codex or any MCP client.
Sending messages
Free-form replies vs templates, the 24-hour window, check_recipient, and what "accepted" means.
Templates
Create WhatsApp message templates, pass Meta's review first time, and track approval.