# WA BRIDGES API Reference WhatsApp-as-a-service. One bridge per phone number. ## Conventions **Phone numbers:** E.164 digits only, no `+`. Example: `15550001234`. **JIDs:** WhatsApp identifiers. Individual: `15550001234@s.whatsapp.net`. Group: `120363XXXXXX@g.us`. The `chat` field in messaging endpoints accepts either a bare phone number or a full JID. **Timestamps:** All timestamp fields are Unix epoch seconds (integer). **Errors:** All error responses use `{"error": "", "code": ""}` - see the Errors section for the code vocabulary. Common statuses: 400 bad request, 401 unauthorized, 404 not found, 429 rate limited, 502 WhatsApp upstream failure, 503 bridge not connected, 500 server error. ## Authentication All requests require: `Authorization: Bearer $WA_API_KEY` **Deprecated:** the `?apikey=` query parameter still authenticates but is deprecated and will be removed; the `Authorization` header is the only supported method going forward. Requests using the query parameter receive a `Deprecation: true` response header. ## Rate Limits All limits per API key. Exceeded requests → `429` with `Retry-After` header. | Endpoint | Limit | Window | |----------|-------|--------| | `POST /api/instances` (provision) | 10 req | per minute | | `/api/instances/:customer_ref/proxy/*` | 120 req | per minute | | `/api/*` (all other endpoints) | 120 req | per minute | | All endpoints (per IP) | 300 req | per minute | ## Errors Errors are JSON: `{"error": "", "code": ""}`. Branch on `code`, not the message text. Codes: `unauthorized` (401) · `validation_failed`, `invalid_chat`, `invalid_sender`, `media_fetch_failed` (400) · `contact_not_found`, `media_not_found`, `avatar_not_found` (404) · `already_paired` (409) · `not_connected` (503, carries a `Retry-After` header — wait and retry) · `upstream_error` (502, WhatsApp-side failure, retryable) · `qr_timeout` (504) · `internal_error` (500). Every response carries an `X-Request-Id` header — include it when contacting support. ## Idempotency Message-producing endpoints (`/send/text`, `/send/media`, `/send/location`, `/send/poll`, `/send/event`, `/send/reaction`, `PATCH /messages/:id/edit`, `DELETE /messages/:id`) accept an optional `Idempotency-Key` header (unique per logical message, max 255 chars) that makes retries safe: - First use of a key: the send happens and the response is stored for 24h. - Retry with the same key: the stored response is replayed with `X-Idempotency-Replayed: true`; nothing is re-sent to WhatsApp. - Concurrent duplicate: `409` with code `idempotency_conflict` while the first request is in flight. - Failed sends do not consume the key, so retrying them works normally. Without the header, behavior is unchanged — fully opt-in. Recommended for all production sends. ## Bridges Manage bridge instances. All routes require `Authorization: Bearer $WA_API_KEY`. Proxy calls use: `https://wabridges.com/api/instances/{customer_ref}/proxy` ### POST /api/instances Provision a new bridge. **Body:** `{"customer_ref": "user-123", "webhook_url": "https://yourbackend.com/hook"}` `customer_ref`: your internal identifier (e.g. user ID). Alphanumeric + dashes only. **Response 201:** `{"id": "7d09aa9c-…", "customer_ref": "user-123", "state": "running", "created_at": 1777180605, "webhook_secret": "whs_…"}` Outbound webhooks carry `Authorization: Bearer ` and an HMAC `X-Webhook-Signature` header - store the secret to verify them. If customer_ref already exists, returns the existing record unchanged with status 200. **Response 402:** `{"error": "…"}` - no active subscription, or account payment past due. **Response 403:** `{"error": "bridge_limit_reached", "limit": 10, "message": "…"}` - accounts are capped at 10 bridges by default; email hello@wabridges.com to raise it. ### GET /api/instances List all bridges. Response: array of `{id, customer_ref, state, created_at}`. ### GET /api/instances/:customer_ref Get single bridge with live manager state. Includes `webhook_url`, `webhook_secret`, `on_trial`, and `manager.health`, `manager.started_at`, `manager.fail_count`. Outbound webhooks carry `Authorization: Bearer ` and an HMAC `X-Webhook-Signature` header - verify before trusting payloads. ### GET /api/instances/:customer_ref/logs Stream bridge logs as server-sent events (SSE). Query param `tail` sets trailing lines to start from (default 100, max 10000). Response content type: `text/event-stream`. ## SESSION ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/status Connection status **Response:** `{"jid":"","name":"","phone":"","status":"unpaired"}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/qr QR code PNG for pairing **Response:** `(image/png)` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/pair Pair via link code sent to phone **Request body:** `{"phone":"15550001234"}` **Response:** `{"code":"ABCD-EFGH"}` ### DELETE https://wabridges.com/api/instances/{customer_ref}/proxy/logout Log out of WhatsApp session **Response:** `{"ok":true}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/webhook-logs Returns up to 200 most recent outbound webhook delivery attempts, newest first. Ring buffer — oldest entries are evicted once full. **Response:** `[{"attempt":0,"error":"","event":"","event_id":"","latency_ms":0,"status_code":0,"success":false,"timestamp":""}]` ## MESSAGING ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/text Send a text message **Request body:** `{"body":"hello","chat":"15550001234@s.whatsapp.net"}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/media Media type is inferred from the Content-Type of the URL response. **Request body:** `{"chat":"15550001234","url":"https://example.com/voice.ogg"}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/reaction React to a message with an emoji **Request body:** `{"chat":"15550001234","emoji":"👍","message_id":"ACE41E988162AB19A4A7BB7A9E663693"}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/poll Send a poll **Request body:** `{"chat":"15550001234","max_answers":3,"options":["Mon","Tue","Wed","Thu","Fri"],"question":"Which days work for you?"}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/location Send a location message **Request body:** `{"address":"Plaza de la Constitución, Mexico City","chat":"15550001234","lat":19.432608,"lng":-99.133209,"name":"Zócalo"}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/event Sends a WA EventMessage. The bridge embeds a fresh MessageSecret in MessageContextInfo so future EncEventResponseMessage RSVPs can be decrypted and delivered as event_response webhooks. **Request body:** `{"chat":"120363000000000001@g.us","description":"weekly status","end_time":1777338000,"has_reminder":true,"is_schedule_call":true,"join_link":"https://meet.example.com/team-sync","name":"Team sync","reminder_offset_sec":900,"start_time":1777334400}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/send/typing Send a typing indicator **Request body:** `{"chat":"15550001234","state":"composing"}` **Response:** `{"ok":true}` ### DELETE https://wabridges.com/api/instances/{customer_ref}/proxy/messages/:id Revoke a sent message for everyone **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### PATCH https://wabridges.com/api/instances/{customer_ref}/proxy/messages/:id/edit Edit a sent text message **Request body:** `{"body":"corrected text","chat":"15550001234@s.whatsapp.net"}` **Response:** `{"message_id":"ACE41E988162AB19A4A7BB7A9E663693","timestamp":1777180605}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/messages/:id/read Send a read receipt **Request body:** `{"chat":"15550001234@s.whatsapp.net"}` **Response:** `{"ok":true}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/messages/:id/media Download received media by message_id (cached 3h) **Response:** `(image/jpeg)` ## CONTACTS ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/contacts Returns a plain JSON array of contacts sorted by JID ascending. Without parameters the full list is returned (back-compat). To page, pass `limit` and use the last JID of the previous page as `after` on the next request; a page shorter than `limit` means you have reached the end. **Response:** `[{"about":"","business_address":"","business_email":"","business_name":"","first_name":"","full_name":"","is_business":false,"jid":"","name":"","phone":"","redacted_phone":"","registered":false,"verified_name":""}]` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/contacts/check Check which phone numbers are on WhatsApp **Request body:** `{"phones":["15550001234","00000000000"]}` **Response:** `[{"is_business":false,"is_on_whatsapp":false,"jid":"","phone":"","registered":false,"verified_name":""}]` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/contacts/:jid Get info for a single contact **Response:** `{"about":"","business_address":"","business_email":"","business_name":"","first_name":"","full_name":"","is_business":false,"jid":"","name":"","phone":"","redacted_phone":"","registered":false,"verified_name":""}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/contacts/:jid/avatar Download a contact's avatar **Response:** `(image/jpeg)` ## PROFILE ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/profile Your own profile **Response:** `{"about":"","jid":"","name":"","phone":""}` ### PATCH https://wabridges.com/api/instances/{customer_ref}/proxy/profile/status Set your About status text **Request body:** `{"status":"Hey there!"}` **Response:** `{"ok":true}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/profile/avatar Download your own avatar **Response:** `(image/jpeg)` ### PUT https://wabridges.com/api/instances/{customer_ref}/proxy/profile/avatar Server fetches the URL (must return JPEG) and uploads it as the account profile picture. **Request body:** `{"url":"https://example.com/me.jpg"}` **Response:** `{"picture_id":""}` ### DELETE https://wabridges.com/api/instances/{customer_ref}/proxy/profile/avatar Remove your profile picture **Response:** `{"ok":true}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/profile/qr Personal QR link to share your contact **Response:** `{"link":"https://wa.me/qr/ABCDEFGHIJKL"}` ## PRIVACY ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/status/privacy Get who can see your status updates **Response:** `[{"is_default":false,"list":[""],"type":"contacts"}]` ## PRESENCE ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/presence Set your online/offline presence **Request body:** `{"state":"unavailable"}` **Response:** `{"ok":true}` ### POST https://wabridges.com/api/instances/{customer_ref}/proxy/presence/subscribe WhatsApp only pushes ChatPresence (typing/recording) and Presence (online/offline) for contacts the client has subscribed to. Subscriptions decay after a few minutes; refresh periodically. Requires global presence to be `available` first. **Request body:** `{"chat":"15550001234"}` **Response:** `{"ok":true}` ## DEBUG ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/healthz Used by the container HEALTHCHECK. Always returns 200 while the process is up. Does NOT check WhatsApp connection — use /status for that. **Response:** `{"ok":true}` ### GET https://wabridges.com/api/instances/{customer_ref}/proxy/debug/stats Runtime stats (goroutines, heap, cache) **Response:** `{"gc_cycles":0,"goroutines":0,"heap_alloc_mb":0,"heap_objects":0,"heap_sys_mb":0,"media_cache_size":0,"next_gc_mb":0,"total_alloc_mb":0}` ## WEBHOOKS Every inbound event fires a POST to your `webhook_url`. **Delivery:** at-least-once, in order. Retries up to 5 attempts with backoff (10s/30s/1m/3m) on network errors, 429, and 5xx; other 4xx are not retried. Method: POST. Content-Type: application/json. **Timeout:** 10 seconds per attempt. Return 200 immediately — process async. Body is ignored. **Dedupe:** `X-Webhook-Event-Id` header is stable across retries — use it as an idempotency key. `X-Webhook-Attempt` is the 1-based attempt number. **Verification:** each delivery carries `Authorization: Bearer ` and `X-Webhook-Signature: t=,v1=` where v1 = HMAC-SHA256 over `.` keyed with webhook_secret. Verify v1 against the raw body and reject stale t (older than ~5 min). All events share a common shape: `{"event": "", ...event-specific fields}`. Switch on the `event` field to route to the correct handler. ### webhook: message **Fields:** event (message), message_id, contact_id, phone, chat_id, name, body, type (text|media|reaction|poll|location|event), media_type (|image|video|audio|voice|document|sticker), is_group, from_me, timestamp, caption?, edit (edit|revoke|pin)?, event_details?, forwarded?, is_broadcast?, max_answers?, mentions?, offline?, options?, quoted?, reaction?, verified_business? **Example payload:** ```json {"body":"Team sync","chat_id":"15550009999@g.us","contact_id":"15550001234@s.whatsapp.net","event":"message","event_details":{"description":"weekly status","end_time":1777338000,"join_link":"https://meet.example.com/team-sync","name":"Team sync","start_time":1777334400},"from_me":false,"is_group":true,"media_type":"","message_id":"ACEVENT0001","name":"John Doe","phone":"15550001234","timestamp":1777330600,"type":"event"} ``` ### webhook: connected **Fields:** event (connected), phone **Example payload:** ```json {"event":"connected","phone":"15550001234"} ``` ### webhook: disconnected **Fields:** event (disconnected), detail?, reason (logged_out|stream_replaced)? **Example payload:** ```json {"detail":"401: logged out","event":"disconnected","reason":"logged_out"} ``` ### webhook: typing **Fields:** event (typing), contact_id, chat_id, state (composing|paused), mode (text|voice) **Example payload:** ```json {"chat_id":"15550001234@s.whatsapp.net","contact_id":"15550001234@s.whatsapp.net","event":"typing","mode":"text","state":"composing"} ``` ### webhook: poll_vote **Fields:** event (poll_vote), contact_id, phone, chat_id, poll_id, message_id, name, from_me, timestamp, selected_options, offline? **Example payload:** ```json {"chat_id":"15550001234@s.whatsapp.net","contact_id":"15550001234@s.whatsapp.net","event":"poll_vote","from_me":false,"message_id":"ACVOTE0099","name":"John Doe","offline":true,"phone":"15550001234","poll_id":"ACPOLL0001","selected_options":["red"],"timestamp":1777180800} ``` ### webhook: event_response **Fields:** event (event_response), contact_id, phone, chat_id, event_id, message_id, name, from_me, timestamp, response (going|not_going|maybe|unknown), response_time_ms, extra_guest_count, offline? **Example payload:** ```json {"chat_id":"15550009999@g.us","contact_id":"15550001234@s.whatsapp.net","event":"event_response","event_id":"ACEVENT0001","extra_guest_count":1,"from_me":false,"message_id":"ACEVRESP0001","name":"John Doe","phone":"15550001234","response":"going","response_time_ms":1777330700123,"timestamp":1777330700} ``` ### webhook: call_incoming **Fields:** event (call_incoming), call_id, contact_id, platform, timestamp **Example payload:** ```json {"call_id":"ABCDEF123456","contact_id":"15550001234@s.whatsapp.net","event":"call_incoming","platform":"android","timestamp":1777180605} ``` ### webhook: call_terminated **Fields:** event (call_terminated), call_id, contact_id, reason (timeout|hangup|decline|busy|accept_elsewhere), timestamp **Example payload:** ```json {"call_id":"ABCDEF123456","contact_id":"15550001234@s.whatsapp.net","event":"call_terminated","reason":"hangup","timestamp":1777180720} ``` ### webhook: offline_sync_preview **Fields:** event (offline_sync_preview), total, messages, notifications, receipts, app_data_changes **Example payload:** ```json {"app_data_changes":0,"event":"offline_sync_preview","messages":120,"notifications":8,"receipts":14,"total":142} ``` ### webhook: offline_sync_completed **Fields:** event (offline_sync_completed), count **Example payload:** ```json {"count":142,"event":"offline_sync_completed"} ``` ### webhook: profile_picture_updated **Fields:** event (profile_picture_updated), remove, timestamp, picture_id? **Example payload:** ```json {"event":"profile_picture_updated","remove":true,"timestamp":1777180800} ```