WASync · Developers
Changelog

What’s new in the Platform API

New endpoints, improvements and breaking changes — newest first. Breaking changes are always announced here before they ship.

  1. New feature

    Pairing codes — connect a number without scanning a QR

    New endpoint POST /connections/{id}/pairing-code (scope whatsapp.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 502 pairing_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 429 rate_limited with Retry-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 — poll GET /connections/{id} and/or listen for connection.connected. QR/WAPP connections only; a Meta/WABA number answers 409 not_supported_for_provider.

  2. 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 is https://developers.wasync.app/api/v1. Nothing you already built has to change. https://cloudapi.wasync.app/api/v1 is 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 read cloudapi.wasync.app — keep using them exactly as issued.

  3. 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 /connections now accepts an optional Idempotency-Key header — 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 with 200 and the response header Idempotency-Replayed: true; the same key with a different body is 400 idempotency_key_reuse; a retry while the first call is still in flight is 409 idempotency_in_progress with Retry-After: 1; a key over 255 characters is 400 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 the qr is re-fetched fresh, because pairing codes rotate roughly every 20 seconds and a stale one would not scan.

  4. 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 with 403 ip_not_allowed, deliberately distinct from 401 invalid_key (unknown or revoked key), because the fix is to add an address, not to rotate a good key. New endpoints GET /webhook, PUT /webhook and POST /webhook/rotate let you point WASync at your endpoint and manage its signing secret without registering an app: PUT issues the secret, GET never returns it (it reports secretSet only), and rotation kills the old secret immediately. Nothing changed for existing OAuth clients, and the webhook signature scheme is unchanged.

  5. 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) and connection.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 authenticated GET /connections/{id}. GET /account (scope whatsapp.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 connection id to your own customer record; we mint the id, you map it. Subscriptions created before 10 August 2026 must re-consent to receive the connection.* events.

  6. 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/read sends the WhatsApp read receipt for you. Body: { connectionId, phone?, messageId? } — at least one of phone / messageId (the latter is WASync’s cuid of an incoming message, not waMessageId). It uses the existing whatsapp.send scope, because marking read transmits a receipt to the customer’s device — no re-consent needed. Idempotent by nature: no idempotencyKey, no rate limit, safe to retry. Response { ok: true, marked }, where marked counts the stored inbound messages newly flagged as read.

  7. Breaking changeImprovement

    messageId is now WASync's stable id — WhatsApp's id moves to waMessageId

    POST /messages previously returned WhatsApp’s own message id (true_…@c.us_3EB0…) as messageId, while GET /messages and the message.received / message.status webhooks 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, as waMessageId (REST), wa_id (webhooks) and wa_message_id (media send) — use it for support tickets, never as a join key (it can be null). GET /messages rows now also carry waMessageId, so you can map both ways. Action required if you stored the value of messageId to match webhooks: it now changes format — join on the new value, and read waMessageId if 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.

  8. 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 showed message.timestamp as an ISO string — deliveries actually carry epoch milliseconds (a number). The payload itself is unchanged; only the documentation was wrong.

  9. 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.txt index. /openapi.json, /llms.txt and /llms-full.txt are now served with Access-Control-Allow-Origin: *, so browser-based spec viewers, SDK generators and AI agents can fetch them cross-origin. This changelog page is also new.

  10. 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 402 with an actionable body instead of a bare 403: { error: "license_inactive", code, message, payment_url }. The original core code (addon_required or license_inactive) is preserved in code, and payment_url points 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 the error code, never on message text.

  11. New feature

    WhatsApp Platform API launch

    First public release. REST v1 at https://cloudapi.wasync.app/api/v1: GET /connections, GET /messages (cursor-paginated) and POST /messages (text, idempotent via idempotencyKey). Auth is OAuth 2.0 Authorization Code + PKCE (S256 required, client_secret_post) with discovery under /.well-known/. A remote MCP server at /api/mcp exposes list_whatsapp_connections, send_whatsapp_message and read_whatsapp_messages. Webhooks deliver message.received with 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.