Send your first message in five minutes.
The REST API works on the numbers you've linked in the inbox. List them, find a conversation, send, and follow every status through signed webhooks.
- Base URL
- https://api.wrelay.site/v1
- Authentication
- Bearer rl_live_…
- Errors
- application/problem+json
- Specification
- OpenAPI 3.1
Quickstart
Five steps from an API key to a delivered message. Pick a language once and every example follows.
Create an API key
In the dashboard, open Settings › Developers › API keys and create a key. It acts for the whole organization and is shown once: we only keep its hash.
Scopes aren't cumulative. This quickstart needs
readandsend; registering a webhook endpoint by API needsadmin.Authorization: Bearer rl_live_…# Settings › Developers › API keys › Create keyexport RELAY_API_KEY="rl_live_…"With a missing, revoked or expired key401{"type": "urn:relay:problem:unauthorized","title": "Authentication required","status": 401,"code": "unauthorized","detail": "Send a valid API key as `Authorization: Bearer rl_live_…`."}List your numbers
GET /v1/numbersreturns the organization's numbers, oldest first. Keep theidof a number whosestatusisworking:can_sendsays it's ready.Lists are paged with
limitandcursor: passnext_cursorback until it'snull.GET/v1/numberscurl "https://api.wrelay.site/v1/numbers?status=working" \-H "Authorization: Bearer $RELAY_API_KEY"Response200{"data": [{"id": "num_01a1a1c6c2007a3f9c41e2b7d5a60813","label": "Sales","phone": "+212600000001","display_name": "Atelier Exemple","avatar_url": null,"status": "working","status_reason": null,"status_changed_at": "2026-11-10T08:02:11.000Z","first_connected_at": "2026-11-03T14:20:05.000Z","connected_at": "2026-11-10T08:02:11.000Z","last_event_at": "2026-11-12T09:11:57.000Z","can_send": true,"daily_send_cap": 400,"history_import": {"status": "completed","started_at": "2026-11-03T14:20:09.000Z","finished_at": "2026-11-03T14:31:52.000Z","chats_total": 214,"chats_done": 214,"messages": 18342,"error_code": null},"created_at": "2026-11-03T14:18:40.000Z"}],"has_more": false,"next_cursor": null}Find or start a chat
Search one number's chats by name or by phone digits, and keep the chat's
id.No conversation yet? Skip this step and send with
number_idandtoinstead ofchat_id: the chat opens with the first message.GET/v1/chatscurl "https://api.wrelay.site/v1/chats?number_id=num_01a1a1c6c2007a3f9c41e2b7d5a60813&search=Maison" \-H "Authorization: Bearer $RELAY_API_KEY"Response200{"data": [{"id": "chat_01a1a1d0f4a87c128e5d4b9a2f61c37e","number_id": "num_01a1a1c6c2007a3f9c41e2b7d5a60813","type": "dm","jid": "212612345678@s.whatsapp.net","phone": "+212612345678","name": "Maison Exemple","description": null,"avatar_url": null,"unread_count": 1,"last_message_at": "2026-11-12T09:10:03.000Z","archived": false,"pinned": false,"pinned_at": null,"muted": false,"muted_until": null,"left_at": null,"only_admins_send": false,"only_admins_edit": false,"group_created_at": null,"created_at": "2026-11-03T14:24:17.000Z"}],"has_more": false,"next_cursor": null}Send a message
POST /v1/messagesrecords the message aspendingand queues it. It leaves WhatsApp within seconds, a few seconds apart per number.Retry a failed or timed-out call with the same
Idempotency-Keyand the same body: you get the same message back with its current status, never a second send.POST/v1/messagescurl https://api.wrelay.site/v1/messages \-H "Authorization: Bearer $RELAY_API_KEY" \-H "Idempotency-Key: $(uuidgen)" \-H "Content-Type: application/json" \-d '{"chat_id": "chat_01a1a1d0f4a87c128e5d4b9a2f61c37e", "type": "text","text": "Friday works, your order is updated."}'Response202{"id": "msg_01a1cf07d7407d84b2a619f3c8e05d47","status": "pending","chat_id": "chat_01a1a1d0f4a87c128e5d4b9a2f61c37e"}Receive webhooks
Add an endpoint for
message.receivedandmessage.status, in the dashboard or with a key that has theadminscope. The answer carries the signing secret, once.Every event then arrives as a signed POST. Answer any 2xx within 10 seconds, and check the signature before you trust the body.
POST/v1/webhooks/endpointscurl https://api.wrelay.site/v1/webhooks/endpoints \-H "Authorization: Bearer $RELAY_ADMIN_KEY" \-H "Idempotency-Key: $(uuidgen)" \-H "Content-Type: application/json" \-d '{"url": "https://example.com/hooks/relay","events": ["message.received", "message.status"]}'Response201{"id": "whk_01a1c5e0a9407e8db0c27f4a19d3e65b","url": "https://example.com/hooks/relay","description": null,"events": ["message.received","message.status"],"number_ids": [],"enabled": true,"disabled_at": null,"disabled_reason": null,"consecutive_failures": 0,"last_delivery_at": null,"last_success_at": null,"created_at": "2026-11-12T09:05:31.000Z","secret": "whsec_…"}On your endpointPOST /hooks/relayContent-Type: application/jsonUser-Agent: relay-webhooks/1X-Relay-Event: message.statusX-Relay-Delivery: dlv_01a1cf07f1a27b6e9d20c3a85e17f4b9X-Relay-Timestamp: 1794474764X-Relay-Signature: v1=…{"id": "evt_01a1cf07f1a27b6e9d20c3a85e17f4b9","event": "message.status","created_at": "2026-11-12T09:12:44.120Z","data": {"id": "msg_01a1cf07d7407d84b2a619f3c8e05d47","number_id": "num_01a1a1c6c2007a3f9c41e2b7d5a60813","chat_id": "chat_01a1a1d0f4a87c128e5d4b9a2f61c37e","wa_message_id": "3EB0A7C2F51D9E84B6C3","from_me": true,"client_key": "3f1c9a52-7d43-4bb8-9a3e-1e2f5c6d7a80","status": "delivered","error_code": null,"sent_at": "2026-11-12T09:12:41.000Z"}}
How the API behaves
The rules every endpoint shares. The reference documents each endpoint in full.
Public ids
Every id is a resource prefix and 32 hex characters. An id from another organization, malformed or with the wrong prefix answers 404 <resource>_not_found, never a separate error.
num_Numberchat_Chat or groupmsg_Messagect_Contactupl_Uploadmedia_Received attachmentwhk_Webhook endpointdlv_Webhook deliveryevt_Webhook eventop_Operation
Scopes
A key carries one or more scopes. A call outside them answers 403 insufficient_scope; GET /v1/operations/{id} accepts any key.
readEvery GET: numbers, chats, messages, groups, contacts and media downloads.sendSending, reactions, edits and revokes, chat state (archive, pin, mute, read), groups, uploads and number checks.adminConnecting, pairing, logging out and deleting numbers; webhook endpoints and the delivery log.Scopes aren't cumulative: give a key every scope it needs.
Idempotency
Every POST, PATCH and DELETE needs an Idempotency-Key header: 1 to 150 visible ASCII characters, a UUID is ideal.
- Same key and same body within 24 hours: the original answer comes back with
Idempotent-Replayed: true, and nothing happens twice. - Same key for a different request:
422 idempotency_key_reused. The first call still running:409 idempotency_key_in_use. - Only successful answers are kept: after an error, the same key runs the request again.
- For sends, the key is also the message's
client_key: a retry ofPOST /v1/messages, even after 24 hours, answers the same message with its current status. - One exception:
POST /v1/numbers/{id}/pairing-codetakes no key, since its code is never stored.
Rate limits
Each key has a burst cap and a per-minute budget. Each number and each organization have their own limits on top.
rate_limitedrate_limitedrate_limitedquota_exceededquota_exceededsend_cap_reachedrecipient_never_wroteEvery authenticated answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds) for the tightest window, and RateLimit-Policy for all of them. A 429 adds Retry-After.
Errors
Every non-2xx answer is an RFC 9457 problem, served as application/problem+json. Branch on code: codes are never renamed or removed, and new ones may appear. errors lists the fields to fix.
idempotency_key_requiredAdd an Idempotency-Key header to the write.unauthorizedCheck the key: unknown, revoked and expired keys all answer this.insufficient_scopeUse a key that has the endpoint's scope.chat_not_foundThe id isn't in this organization, or has the wrong prefix.number_not_workingThe number isn't connected: wait for number.status to read working.validation_failedFix the fields listed in errors.rate_limitedWait Retry-After seconds, then retry.gateway_<reason>WhatsApp didn't answer an in-request call: retry shortly.enqueue_failedSaved but not queued: retry with the same Idempotency-Key.Long operations
Calls that wait on WhatsApp (connecting or deleting a number, group commands, number checks) answer 202 with an operation_id and a Location header.
- Poll
GET /v1/operations/{id}every 1 to 2 seconds untilstatusissucceeded(readresult) orfailed(readerror.code), then read the resource again. - An operation that doesn't settle within an hour reads
failed, with the codeexpired.
Sending files
Files never travel through the API: they go straight to storage, then the message points at them.
Images and audio up to 16 MB, video up to 64 MB, documents up to 100 MB. A received attachment downloads with GET /v1/media/{id}, which answers a signed URL valid for 5 minutes.
POST /v1/media/uploadswithmime_typeandsize_bytes: you get anupl_id and a presignedupload.url.PUTthe raw bytes toupload.urlwith exactlyupload.headersand no Authorization header, within 15 minutes.POST /v1/media/uploads/{id}/finalize: the upload turnsready.POST /v1/messageswithtype,upload_idand an optionaltextcaption, within 24 hours.
Every endpoint, in the reference.
The hosted reference and the OpenAPI 3.1 file come from the same schemas that check your requests, so they can't drift from what the API accepts.