Most of what a WhatsApp API is asked to do comes down to four things: send a message, look something up, change a record, and be told when something happened. Spun exposes all four over plain HTTP against the same number your team answers in the inbox, authenticated with a key you create yourself and scope to exactly what the integration needs.
This page is the practical version: what the endpoints are, how authentication and scoping work, what happens when your WhatsApp line is offline at the moment you call, and where the machine-readable contract lives.
Keys, scopes and the blast radius of a leak
Authentication is a bearer token. You create a key in the app, it is shown to you once, and every request carries it in an Authorization header. Spun stores only a hash of the key, so a key that is lost cannot be recovered, only replaced.
The part worth spending a minute on is scoping. A key is not simply "access to the account": it carries a list of scopes, and every endpoint checks for the specific scope it needs. A key with only read_contacts cannot send a message. A key with only send_message cannot enumerate your contact list. On top of scopes, a key can carry an expiry date and an IP allowlist, so a key issued to a server that has a fixed address stops working the moment it is used from anywhere else.
- send_message covers text and media sends, and reading a queued send back.
- read_contacts, read_messages and read_labels are the read side, separately grantable.
- manage_contacts and manage_labels cover creating, updating and labelling.
- check_phone answers whether a number is reachable on WhatsApp.
- manage_webhooks creates, lists and deletes webhook subscriptions.
Request
curl -X POST https://api.spun.com/api/integrations/send-message \
-H "Authorization: Bearer wap_0f3c9a1d7b524e86a1c05d9e2f7b48ac" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155550142",
"text": "Your appointment is confirmed for Thursday at 10:00.",
"idempotency_key": "booking-8871"
}'Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"message_id": "yq0Zt9C1sK2mfE7x"
}Keys created for the Spun MCP server are deliberately refused on this API even when they are valid keys. The two surfaces have different approval semantics, and a key issued for one is not silently usable on the other.
What the WhatsApp API endpoints actually cover
The surface is deliberately small and stable rather than an exhaustive mirror of the product. It exists so an external system can do the things external systems really need to do.
- Send: a text message, or media as either a URL or base64, with an optional caption.
- Contacts: create a contact, update one, and list contacts with cursor pagination.
- Labels: list the labels in the workspace, apply one to a conversation, remove one.
- Messages: list messages with cursor pagination, for syncing into a warehouse or a CRM.
- Check phone: confirm a number is on WhatsApp before you build a list around it.
- Queue: look up a send that was queued because the line was offline.
- Webhooks: create, list and delete subscriptions.
The machine-readable contract is published at /api/integrations/openapi.json and needs no key to read, so an n8n node author, a Zapier app builder or your own generator can pull the spec before anyone has issued them credentials.
What happens when the WhatsApp line is offline
This is the part that surprises people, so it is worth being explicit. Spun sends through a live WhatsApp session, and a live session can be down at the exact second your code calls. The API does not pretend otherwise, and it behaves differently for text and for media on purpose.
A text send to an offline line returns 202 rather than an error, with a queue id. The message is held and delivered when the line reconnects, and you can poll the queue endpoint for its fate. A media send to an offline line returns a conflict instead, and sends nothing. That asymmetry is deliberate: the queue drains messages as text, so a queued image would arrive as a caption with no picture. Failing loudly is the better outcome, and your retry logic gets to decide.
Rate limiting is 60 requests per minute per key, with the standard rate-limit headers on every response, so a client can back off on the headers rather than on a 429 it did not expect.
Webhooks: being told instead of polling
Polling a messages endpoint on a timer is the wrong shape for a conversation, so the API can push instead. You register a URL and the events you care about, and Spun posts a JSON body to it as they happen.
Every delivery carries the event name and a unique delivery id in headers, and, when the subscription has a signing secret, an HMAC-SHA256 signature of the exact body. Verify the signature before you trust the payload: it is the only thing separating your endpoint from anyone who learns the URL. The secret is returned once, when you create the subscription, and never again.
- Message events, inbound and outbound.
- Contact created, updated and deleted.
- Label assigned.
- Chat flow completed, queued send processed, lead qualified.
- Conversation closed, call completed, and pipeline stage changed.
Related: Connecting Spun to Zapier and Make · Connecting Spun to n8n
Limits and honest caveats
- This is not the WhatsApp Business API. It is Spun's own API in front of your linked number, so there are no message templates to approve and no per-message fees, and equally no official Meta guarantees.
- Sixty requests per minute per key. Bulk work belongs in campaigns, which pace themselves properly, not in a tight API loop.
- The endpoint list above is the whole surface today. Anything not on it is not exposed, and no undocumented endpoint should be relied on.
- A key is scoped but not per-user. Treat it as workspace credentials and rotate it when someone with access leaves.
- Sending to people who never opted in will get the number limited by WhatsApp regardless of which route the message took.
Related: How Spun connects to WhatsApp