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 free