What’s new in the Platform API
New endpoints, improvements and breaking changes — newest first. Breaking changes are always announced here before they ship.
- New feature
Pairing codes — connect a number without scanning a QR
New endpoint
POST /connections/{id}/pairing-code(scopewhatsapp.manage). Body{ "phoneNumber": "40740267964" }— E.164 without the leading+, digits only — returns{ "code": "7S59-1KZP" }. Your customer types it into WhatsApp → Settings → Linked devices → Link with phone number. This is the path for the customer who is completing your onboarding on the same phone that runs WhatsApp: there is no second screen to photograph, so the QR simply cannot be used. A pairing code is not always available — the engine can decline with 502pairing_code_unavailable, so keep the QR flow on screen as a fallback. Requesting another code on the same connection is allowed (a new code, same session) and capped at 5 per connection per 15 minutes, then 429rate_limitedwithRetry-After. Nothing extra is consumed: the same paid slot and the same 7-day trial as QR pairing, claimed when the number actually pairs. Monitoring is unchanged — pollGET /connections/{id}and/or listen forconnection.connected. QR/WAPP connections only; a Meta/WABA number answers 409not_supported_for_provider. - Improvement
developers.wasync.app is the canonical host — cloudapi.wasync.app keeps working forever
The developer portal, the docs and the REST API are now documented under
https://developers.wasync.app, and the REST base URL ishttps://developers.wasync.app/api/v1. Nothing you already built has to change.https://cloudapi.wasync.app/api/v1is a permanent alias serving exactly the same API — same keys, same paths, same responses — and it remains valid indefinitely. This is a documentation change, not a migration: there is no cut-off date and no deprecation. The OAuth endpoints, the/.well-known/discovery documents and the MCP server URL are protocol identifiers your clients have already registered, so they are unchanged and still readcloudapi.wasync.app— keep using them exactly as issued. - New feature
Idempotency-Key on POST /connections — a retried create no longer costs you a second slot
Creating a connection provisions a WhatsApp session and consumes a paid slot, so a client that timed out and retried used to end up with two connections and two slot charges for one customer.
POST /connectionsnow accepts an optionalIdempotency-Keyheader — the only endpoint that does; omit it and behaviour is exactly as before. Within a 24 h TTL: the same key with the same body returns the SAME connection with200and the response headerIdempotency-Replayed: true; the same key with a different body is400 idempotency_key_reuse; a retry while the first call is still in flight is409 idempotency_in_progresswithRetry-After: 1; a key over 255 characters is400 idempotency_key_invalid. Keys are namespaced per workspace, and the body comparison is over a canonical hash, so JSON key order is irrelevant. Mint one fresh UUID per customer-onboarding attempt — never one per process, per API key, per day, or a constant, or the second customer you onboard is handed the first customer’s connection. On a replay theqris re-fetched fresh, because pairing codes rotate roughly every 20 seconds and a stale one would not scan. - New feature
API keys are the default credential — plus IP allowlists and webhook self-service
Authorization: Bearer wsk_live_…now works on every endpoint. Create and revoke keys at developers.wasync.app/keys — the key is shown once and stored only as a hash, scopes are per key, and scopes plus the reachable connection list are resolved live on every request. If you are automating your own account this replaces the entire OAuth flow: no redirect URI, no PKCE, no consent screen, no token refresh. OAuth 2.0 remains, unchanged, for apps that OTHER WASync customers install and consent to. A key can create a workspace’s FIRST connection, so the old “make the first one in the portal UI” bootstrap step is gone. Optional hardening: pin a key to IPv4/IPv6 addresses or CIDR ranges — a call from an address that is not on the list is refused with403 ip_not_allowed, deliberately distinct from401 invalid_key(unknown or revoked key), because the fix is to add an address, not to rotate a good key. New endpointsGET /webhook,PUT /webhookandPOST /webhook/rotatelet you point WASync at your endpoint and manage its signing secret without registering an app:PUTissues the secret,GETnever returns it (it reportssecretSetonly), and rotation kills the old secret immediately. Nothing changed for existing OAuth clients, and the webhook signature scheme is unchanged. - New feature
Connection lifecycle events + GET /account slot quota
Two webhook events were added for partners who run our connections inside their own product:
connection.disconnected(a number stopped working and its owner must scan a fresh QR) andconnection.connected(it recovered). Both fire once per real transition, on QR/WAPP connections, and the QR is deliberately NOT in the payload — it is a scan-to-login credential, so fetch it from your authenticatedGET /connections/{id}.GET /account(scopewhatsapp.read) answers “can I onboard another number right now?” before you take the order:{ slots: { total, used, available, nextExpiry }, connections: { total, connected, needsReconnect } }. A slot is capacity, not a phone number — it attaches when a number actually pairs and returns to your pool with whatever time is left on it when the connection is deleted, so a paid slot moves from a departing customer to a new one without paying twice. You map our connectionidto your own customer record; we mint the id, you map it. Subscriptions created before 10 August 2026 must re-consent to receive theconnection.*events. - New feature
Read receipts — POST /messages/read gives your customers blue ticks
If your operators read WhatsApp inside your own CRM, nobody ever opens the chat in WhatsApp — the customer’s messages stay on double grey ticks and the conversation feels ignored.
POST /messages/readsends the WhatsApp read receipt for you. Body:{ connectionId, phone?, messageId? }— at least one ofphone/messageId(the latter is WASync’s cuid of an incoming message, notwaMessageId). It uses the existingwhatsapp.sendscope, because marking read transmits a receipt to the customer’s device — no re-consent needed. Idempotent by nature: noidempotencyKey, no rate limit, safe to retry. Response{ ok: true, marked }, wheremarkedcounts the stored inbound messages newly flagged as read. - Breaking changeImprovement
messageId is now WASync's stable id — WhatsApp's id moves to waMessageId
POST /messagespreviously returned WhatsApp’s own message id (true_…@c.us_3EB0…) asmessageId, whileGET /messagesand themessage.received/message.statuswebhooks used WASync’s id — so status events could never be matched back to a send. They are now the SAME id everywhere:messageId(POST) =id(GET) =message.id(webhooks), a WASync cuid. WhatsApp’s own id is still returned, alongside, aswaMessageId(REST),wa_id(webhooks) andwa_message_id(media send) — use it for support tickets, never as a join key (it can benull).GET /messagesrows now also carrywaMessageId, so you can map both ways. Action required if you stored the value ofmessageIdto match webhooks: it now changes format — join on the new value, and readwaMessageIdif you specifically needed the WhatsApp id. One new edge case: if a send reaches WhatsApp but cannot be persisted, the 200 body is{ messageId: null, waMessageId, status, persisted: false }— the message was delivered, so do not retry it. - New featureImprovement
Webhooks reference page + docs payload fix
A dedicated Webhooks reference now documents the full delivery contract: event catalog, exact payload, signature verification with a complete receiver example, retry policy (3 attempts, exponential backoff, 10s per-attempt timeout, no long redelivery queue — backfill via
GET /messages), duplicate/ordering semantics and a troubleshooting checklist. Docs fix: the webhook example previously showedmessage.timestampas an ISO string — deliveries actually carry epoch milliseconds (a number). The payload itself is unchanged; only the documentation was wrong. - Improvement
Machine-readable docs surface: llms-full.txt + CORS-open spec
The complete documentation is now published as a single markdown file at /llms-full.txt for LLM ingestion, alongside the existing
/llms.txtindex./openapi.json,/llms.txtand/llms-full.txtare now served withAccess-Control-Allow-Origin: *, so browser-based spec viewers, SDK generators and AI agents can fetch them cross-origin. This changelog page is also new. - Improvement
402 responses with a payment link when the API add-on is inactive
Sending through a connection whose Platform API add-on is not active now returns
HTTP 402with an actionable body instead of a bare 403:{ error: "license_inactive", code, message, payment_url }. The original core code (addon_requiredorlicense_inactive) is preserved incode, andpayment_urlpoints where the connection owner can activate the add-on. MCP tool results return the same information as plain text so an AI agent can relay it to the human. Every other error status is still relayed verbatim — branch on theerrorcode, never on message text. - New feature
WhatsApp Platform API launch
First public release. REST v1 at
https://cloudapi.wasync.app/api/v1:GET /connections,GET /messages(cursor-paginated) andPOST /messages(text, idempotent viaidempotencyKey). Auth is OAuth 2.0 Authorization Code + PKCE (S256 required,client_secret_post) with discovery under/.well-known/. A remote MCP server at/api/mcpexposeslist_whatsapp_connections,send_whatsapp_messageandread_whatsapp_messages. Webhooks delivermessage.receivedwith HMAC-SHA256 signatures (X-WASync-Signature). App registration lives at developers.wasync.app; the AI integration kit (/docs,/openapi.json,/llms.txt) shipped the same day.