Aller au contenu

Envoyez votre premier message en cinq minutes.

L'API REST travaille sur les numéros que vous avez liés dans la messagerie. Listez-les, trouvez une discussion, envoyez, et suivez chaque statut grâce à des webhooks signés.

POST /v1/messages
Idempotency-Key: 3f1c9a52-7d43-4bb8-9a3e-1e2f5c6d7a80
{ "chat_id": "chat_…", "type": "text",
"text": "Friday works, your order is updated." }
HTTP/1.1 202 Accepted
{ "id": "msg_…", "status": "pending", "chat_id": "chat_…" }
Puis, sur votre webhook
message.sentsent09:12:41
message.statusdelivered09:12:44
message.statusread09:14:10
URL de base
https://api.wrelay.site/v1
Authentification
Bearer rl_live_…
Erreurs
application/problem+json
Spécification
OpenAPI 3.1

Démarrage rapide

Cinq étapes, de la clé API au message distribué. Choisissez un langage une fois : tous les exemples suivent.

  1. Créez une clé API

    Dans le tableau de bord, ouvrez Paramètres › Développeurs › Clés API et créez une clé. Elle agit pour toute l'organisation et ne s'affiche qu'une fois : nous n'en gardons que l'empreinte.

    Les scopes ne se cumulent pas. Ce démarrage rapide demande read et send ; enregistrer un endpoint webhook via l'API demande admin.

    Authorization: Bearer rl_live_…
    # Settings › Developers › API keys › Create key
    export RELAY_API_KEY="rl_live_…"
    Avec une clé absente, révoquée ou expirée401
    {
    "type": "urn:relay:problem:unauthorized",
    "title": "Authentication required",
    "status": 401,
    "code": "unauthorized",
    "detail": "Send a valid API key as `Authorization: Bearer rl_live_…`."
    }
  2. Listez vos numéros

    GET /v1/numbers renvoie les numéros de l'organisation, du plus ancien au plus récent. Gardez l'id d'un numéro dont le status vaut working : can_send indique qu'il est prêt.

    Les listes sont paginées avec limit et cursor : renvoyez next_cursor jusqu'à ce qu'il vaille null.

    GET/v1/numbers
    curl "https://api.wrelay.site/v1/numbers?status=working" \
    -H "Authorization: Bearer $RELAY_API_KEY"
    Réponse200
    {
    "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
    }
  3. Trouvez ou ouvrez une discussion

    Cherchez parmi les discussions d'un numéro par nom ou par chiffres du téléphone, et gardez l'id de la discussion.

    Pas encore de conversation ? Passez cette étape et envoyez avec number_id et to au lieu de chat_id : la discussion s'ouvre avec le premier message.

    GET/v1/chats
    curl "https://api.wrelay.site/v1/chats?number_id=num_01a1a1c6c2007a3f9c41e2b7d5a60813&search=Maison" \
    -H "Authorization: Bearer $RELAY_API_KEY"
    Réponse200
    {
    "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
    }
  4. Envoyez un message

    POST /v1/messages enregistre le message en pending et le met en file. Il part sur WhatsApp en quelques secondes, à quelques secondes d'intervalle par numéro.

    Relancez un appel en échec ou expiré avec la même Idempotency-Key et le même corps : vous récupérez le même message avec son statut actuel, jamais un second envoi.

    POST/v1/messages
    curl 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."}'
    Réponse202
    {
    "id": "msg_01a1cf07d7407d84b2a619f3c8e05d47",
    "status": "pending",
    "chat_id": "chat_01a1a1d0f4a87c128e5d4b9a2f61c37e"
    }
  5. Recevez les webhooks

    Ajoutez un endpoint pour message.received et message.status, dans le tableau de bord ou avec une clé qui a le scope admin. La réponse contient le secret de signature, une seule fois.

    Chaque événement arrive ensuite en POST signé. Répondez par un 2xx en moins de 10 secondes, et vérifiez la signature avant de vous fier au corps.

    POST/v1/webhooks/endpoints
    curl 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"]}'
    Réponse201
    {
    "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_…"
    }
    Sur votre endpoint
    POST /hooks/relay
    Content-Type: application/json
    User-Agent: relay-webhooks/1
    X-Relay-Event: message.status
    X-Relay-Delivery: dlv_01a1cf07f1a27b6e9d20c3a85e17f4b9
    X-Relay-Timestamp: 1794474764
    X-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"
    }
    }
Une requête, un message.Il n'existe pas d'endpoint d'envoi en masse ni de diffusion : une requête envoie un message à une seule conversation, et chaque numéro envoie à quelques secondes d'intervalle. Écrivez aux personnes qui attendent de vos nouvelles.Lire la politique d'utilisation acceptable

Comment se comporte l'API

Les règles communes à tous les endpoints. La référence détaille chacun d'eux.

Identifiants publics

Chaque identifiant se compose d'un préfixe de ressource et de 32 caractères hexadécimaux. Un identifiant d'une autre organisation, mal formé ou au mauvais préfixe répond 404 <resource>_not_found, jamais une erreur distincte.

Anatomie d'un identifiant de message
msg_Préfixe de ressource01a1cf07d7407d84b2a619f3c8e05d4732 caractères hexadécimaux
  • num_Numéro
  • chat_Discussion ou groupe
  • msg_Message
  • ct_Contact
  • upl_Téléversement
  • media_Pièce jointe reçue
  • whk_Endpoint webhook
  • dlv_Livraison webhook
  • evt_Événement webhook
  • op_Opération

Scopes

Une clé porte un ou plusieurs scopes. Un appel hors de ces scopes répond 403 insufficient_scope ; GET /v1/operations/{id} accepte n'importe quelle clé.

readTous les GET : numéros, discussions, messages, groupes, contacts et téléchargement des médias.
sendEnvois, réactions, modifications et suppressions, état des discussions (archiver, épingler, mettre en sourdine, marquer comme lu), groupes, téléversements et vérification de numéros.
adminConnexion, association, déconnexion et suppression des numéros ; endpoints webhook et journal des livraisons.

Les scopes ne se cumulent pas : donnez à une clé chacun des scopes dont elle a besoin.

Idempotence

Chaque POST, PATCH et DELETE exige un en-tête Idempotency-Key : de 1 à 150 caractères ASCII visibles, idéalement un UUID.

  • Même clé et même corps sous 24 heures : la réponse d'origine revient avec Idempotent-Replayed: true, et rien ne se produit deux fois.
  • Même clé pour une autre requête : 422 idempotency_key_reused. Premier appel encore en cours : 409 idempotency_key_in_use.
  • Seules les réponses réussies sont conservées : après une erreur, la même clé relance la requête.
  • Pour les envois, la clé est aussi le client_key du message : relancer POST /v1/messages, même après 24 heures, renvoie le même message avec son statut actuel.
  • Une exception : POST /v1/numbers/{id}/pairing-code ne prend pas de clé, car son code n'est jamais conservé.

Limites de débit

Chaque clé a un plafond de rafale et un budget par minute. Chaque numéro et chaque organisation ont en plus leurs propres limites.

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 23
RateLimit-Policy: 20;w=1, 60;w=60
Retry-After: 23
{
"type": "urn:relay:problem:rate_limited",
"title": "Too many requests",
"status": 429,
"code": "rate_limited",
"detail": "This API key is over its rate limit: retry after the number of seconds in Retry-After."
}
LimiteValeurRéponse
Rafale, par clé20 requêtes par seconderate_limited
Débit soutenu, par cléLa limite par minute de votre offre, ou celle de la clérate_limited
Par numéro30 requêtes d'envoi par minuterate_limited
Requêtes API, par organisationLe quota mensuel de l'offre, remis à zéro le 1er (UTC)quota_exceeded
Messages envoyés, par organisationLe quota mensuel d'envois de l'offrequota_exceeded
Envois quotidiens, par numéroAugmentent pendant les deux premières semaines d'un nouveau numérosend_cap_reached
Nouvelles conversations, par numéro et par jourPlafonnées, pour les personnes qui n'ont jamais écrit au numérorecipient_never_wrote
OffreRequêtes / minute / cléRequêtes API / moisMessages envoyés / mois
Starter6010 00010 000
Growth300100 00060 000
Business1 2001 000 000300 000

Chaque réponse authentifiée porte RateLimit-Limit, RateLimit-Remaining et RateLimit-Reset (en secondes) pour la fenêtre la plus serrée, et RateLimit-Policy pour toutes. Un 429 ajoute Retry-After.

Erreurs

Toute réponse hors 2xx est un problème RFC 9457, servi en application/problem+json. Basez-vous sur code : les codes ne sont jamais renommés ni retirés, et de nouveaux peuvent apparaître. errors liste les champs à corriger.

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "urn:relay:problem:validation_failed",
"title": "Unprocessable request",
"status": 422,
"code": "validation_failed",
"detail": "One or more fields are invalid.",
"errors": {
"text": "A text message needs text"
}
}
StatutCodeQue faire
400idempotency_key_requiredAjoutez un en-tête Idempotency-Key à l'écriture.
401unauthorizedVérifiez la clé : inconnue, révoquée ou expirée, elle répond la même chose.
403insufficient_scopeUtilisez une clé qui a le scope de l'endpoint.
404chat_not_foundL'identifiant n'appartient pas à cette organisation, ou son préfixe est faux.
409number_not_workingLe numéro n'est pas connecté : attendez que number.status indique working.
422validation_failedCorrigez les champs listés dans errors.
429rate_limitedAttendez Retry-After secondes, puis réessayez.
502gateway_<reason>WhatsApp n'a pas répondu à un appel immédiat : réessayez sous peu.
503enqueue_failedEnregistré mais pas mis en file : réessayez avec la même Idempotency-Key.
Voir tous les codes dans la référence

Opérations longues

Les appels qui attendent WhatsApp (connecter ou supprimer un numéro, commandes de groupe, vérification de numéros) répondent 202 avec un operation_id et un en-tête Location.

  • Interrogez GET /v1/operations/{id} toutes les 1 à 2 secondes jusqu'à ce que status vaille succeeded (lisez result) ou failed (lisez error.code), puis relisez la ressource.
  • Une opération qui ne se termine pas dans l'heure passe en failed, avec le code expired.
L'appel est accepté
POST /v1/contacts/check
Idempotency-Key: 3f1c9a52-7d43-4bb8-9a3e-1e2f5c6d7a80
{ "phone": "+212612345678" }
HTTP/1.1 202 Accepted
Location: /v1/operations/op_01a1cf07e6e07f028b3d6a1c94e2f507
{
"operation_id": "op_01a1cf07e6e07f028b3d6a1c94e2f507",
"operation": {
"id": "op_01a1cf07e6e07f028b3d6a1c94e2f507",
"kind": "number_check",
"status": "queued",
"progress": null,
"result": null,
"error": null,
"created_at": "2026-11-12T09:12:44.000Z",
"started_at": null,
"finished_at": null
}
}
Quelques secondes plus tard
GET /v1/operations/op_01a1cf07e6e07f028b3d6a1c94e2f507
HTTP/1.1 200 OK
{
"id": "op_01a1cf07e6e07f028b3d6a1c94e2f507",
"kind": "number_check",
"status": "succeeded",
"progress": null,
"result": {
"phone": "+212612345678",
"on_whatsapp": true
},
"error": null,
"created_at": "2026-11-12T09:12:44.000Z",
"started_at": "2026-11-12T09:12:45.000Z",
"finished_at": "2026-11-12T09:12:46.000Z"
}

Envoyer des fichiers

Les fichiers ne transitent jamais par l'API : ils vont directement dans le stockage, puis le message y fait référence.

Images et audio jusqu'à 16 Mo, vidéo jusqu'à 64 Mo, documents jusqu'à 100 Mo. Une pièce jointe reçue se télécharge avec GET /v1/media/{id}, qui répond une URL signée valable 5 minutes.

  1. POST /v1/media/uploads avec mime_type et size_bytes : vous obtenez un identifiant upl_ et une upload.url présignée.
  2. Envoyez les octets bruts en PUT sur upload.url avec exactement upload.headers et sans en-tête Authorization, dans les 15 minutes.
  3. POST /v1/media/uploads/{id}/finalize : le téléversement passe à ready.
  4. POST /v1/messages avec type, upload_id et une légende text facultative, dans les 24 heures.
POST /v1/media/uploads
Idempotency-Key: 3f1c9a52-7d43-4bb8-9a3e-1e2f5c6d7a80
{ "mime_type": "image/jpeg", "size_bytes": 284512, "file_name": "receipt.jpg" }
HTTP/1.1 201 Created
{
"id": "upl_01a1cf0901bc7a558f612d0c7e9b3a48",
"state": "pending",
"file_name": "receipt.jpg",
"mime_type": "image/jpeg",
"declared_bytes": 284512,
"size_bytes": null,
"expires_at": "2026-11-13T09:20:05.000Z",
"created_at": "2026-11-12T09:20:05.000Z",
"upload": {
"method": "PUT",
"url": "https://…",
"headers": {
"Content-Type": "image/jpeg"
},
"max_bytes": 284512,
"expires_at": "2026-11-12T09:35:05.000Z"
}
}

Chaque endpoint, dans la référence.

La référence en ligne et le fichier OpenAPI 3.1 sont générés à partir des schémas qui valident vos requêtes : ils ne peuvent pas diverger de ce que l'API accepte.