Documentation du pont WhatsApp
Un appel HTTP, et le message part de votre propre numéro WhatsApp. Aucune bibliothèque à installer.
Démarrer
Créez un compte sur l'espace développeurs, acceptez les conditions, connectez un numéro dédié aux notifications, puis générez une clé. Votre compte doit ensuite être validé par nous avant d'émettre — vous pouvez écrire toute votre intégration entre-temps, les erreurs vous diront exactement où vous en êtes.
Adresse de base : https://wapy.pro. Toutes les requêtes sont en application/json.
Mettez votre clé dans une variable d'environnement, jamais dans la commande
Les exemples de cette page lisent WAPY_PONT_CLE. Une clé écrite en clair dans une commande survit dans l'historique du terminal, dans les journaux du serveur et dans le premier message de support où on la recopie.
bash
export WAPY_PONT_CLE="wapy_pont_…"PowerShell
$env:WAPY_PONT_CLE = "wapy_pont_…"Sous Windows PowerShell, les exemples curl ne fonctionnent pas : curl y est un alias d'Invoke-WebRequest, qui ignore -X, -H et -d. Chaque section donne l'équivalent PowerShell juste en dessous.
Authentification
La clé se transmet dans l'en-tête Authorization, jamais dans l'URL : une clé en chaîne de requête finit dans les journaux du serveur, dans l'historique du navigateur et dans les référents sortants.
Authorization: Bearer $WAPY_PONT_CLEGardez-la côté serveur. Elle ne doit jamais atteindre un navigateur ni une application mobile : quiconque la détient peut écrire depuis votre numéro. Nous n'en conservons que l'empreinte — perdue, elle se remplace, elle ne se retrouve pas. Générer une nouvelle clé coupe immédiatement la précédente.
Idempotence : l'en-tête à ne pas oublier
Chaque envoi exige un en-tête Idempotency-Key unique. Rejouer une requête portant la même clé ne produit jamais un second message : la réponse initiale est renvoyée, avec rejeu: true.
Ce n'est pas une formalité. Sans elle, un délai réseau suivi d'une reprise annonce deux fois le même paiement au même parent. Utilisez un identifiant qui décrit l'évènement, pas l'instant : l'identifiant de la transaction, pas un horodatage.
Envoyer un message
POST /pont/v1/messages — pour un reçu, une confirmation, un rappel. Le champ consentement est obligatoire : une référence attestant que le destinataire a accepté d'être contacté. Nous ne pouvons pas la vérifier ; elle engage votre responsabilité.
curl
curl -X POST https://wapy.pro/pont/v1/messages \
-H "Authorization: Bearer $WAPY_PONT_CLE" \
-H "Idempotency-Key: paiement-2026-114" \
-H "Content-Type: application/json" \
-d '{
"destinataire": "+22990000001",
"texte": "Paiement reçu : 50 000 FCFA pour le 2e trimestre. Merci.",
"consentement": "inscription-2026-114"
}'Réponse
{
"id": 1042,
"message_id": "3EB0C8F2A1D4E5B6",
"statut": "envoye",
"remise": "acceptee",
"rejeu": false
}Numéro du destinataire. Avant tout envoi, nous demandons à WhatsApp si le numéro a un compte, et sous quelle forme. Un numéro sans compte est refusé (404 destinataire-inexistant), sans rien consommer. Les écritures d'un même numéro mènent au même compte et comptent pour un seul destinataire : au Bénin, +2290197101385 (10 chiffres) et +22997101385 (8 chiffres) sont la même personne. Envoyez une seule forme, celle que vous avez.
Sous Windows PowerShell, la commande ci-dessus ne fonctionne pas : curl y est un alias d'Invoke-WebRequest, qui ignore -X, -H et -d, et les \ de fin de ligne n'y sont pas des continuations. Utilisez plutôt :
PowerShell
$corps = @{
destinataire = "+22990000001"
texte = "Paiement reçu : 50 000 FCFA. Merci."
consentement = "inscription-2026-114"
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://wapy.pro/pont/v1/messages" `
-Headers @{
"Authorization" = "Bearer $env:WAPY_PONT_CLE"
"Idempotency-Key" = "paiement-2026-114"
} `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($corps))Les deux dernières lignes ne sont pas décoratives : sans elles, Windows PowerShell 5.1 encode le corps dans la page de codes du système et « Paiement reçu » arrive mutilé chez votre client. Le binaire curl.exe reste disponible si vous préférez la syntaxe Unix — mais appelez-le par son nom complet, sinon c'est l'alias qui répond.
Envoyer un code de vérification
POST /pont/v1/otp — le corps du message est composé par nous à partir d'un gabarit fixe : vous fournissez le code, pas le texte. Cela garantit qu'un point d'entrée conçu pour l'authentification ne serve pas à autre chose.
Le code n'est ni journalisé, ni stocké, ni renvoyé dans la réponse. Limite : deux codes par heure et par destinataire.
curl
curl -X POST https://wapy.pro/pont/v1/otp \
-H "Authorization: Bearer $WAPY_PONT_CLE" \
-H "Idempotency-Key: otp-9f3c1a" \
-H "Content-Type: application/json" \
-d '{
"destinataire": "+22990000001",
"code": "482913",
"service": "Mon École",
"minutes": 10
}'PowerShell
$corps = @{
destinataire = "+22990000001"
code = "482913"
service = "Mon École"
minutes = 10
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://wapy.pro/pont/v1/otp" `
-Headers @{
"Authorization" = "Bearer $env:WAPY_PONT_CLE"
"Idempotency-Key" = "otp-9f3c1a"
} `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($corps))Le destinataire reçoit :
Votre code de vérification Mon École est 482913.
Il expire dans 10 minutes. Ne le communiquez à personne.Prévoyez toujours un second canal. Si votre utilisateur n'a pas WhatsApp sur ce téléphone, un code envoyé ici est une porte fermée : gardez le SMS ou le courriel en repli.
Consulter l'état du compte
GET /pont/v1/statut — reste consultable même quand l'émission est bloquée : c'est là que vous lisez pourquoi.
Réponse
{
"statut": "actif",
"peut_emettre": true,
"blocage": "",
"conditions_acceptees": true,
"whatsapp": "WORKING",
"numero_connecte": true,
"webhook": { "url": "https://monapp.bj/wapy" },
"limites": {
"intervalle_min_s": 3,
"envois_par_heure": 60,
"envois_par_jour": 500,
"otp_par_heure_et_destinataire": 2,
"taille_max_message": 1000
},
"consommation": { "derniere_heure": 12, "dernier_jour": 87 }
}numero_connecte reflète l'état réel de la session WhatsApp (whatsapp : WORKING, SCAN_QR_CODE, STOPPED, FAILED…). Tant qu'il est faux, tout envoi répond 502.
Suivre la remise d'un message
Un 200 signifie que WhatsApp a accepté le message, pas qu'il est arrivé. Le champ remise progresse ensuite avec les accusés de WhatsApp : acceptee → serveur → appareil → lu, ou echec. Deux façons de le lire.
Interroger. GET /pont/v1/messages/{id} avec l'id renvoyé à l'envoi.
Réponse
{
"id": 1042,
"message_id": "3EB0C8F2A1D4E5B6",
"genre": "texte",
"statut": "envoye",
"motif": "",
"remise": "appareil",
"remise_le": "2026-09-14T10:32:07",
"cree_le": "2026-09-14T10:31:58"
}Être prévenu. POST /pont/v1/webhook avec { "url": "https://…" } (HTTPS obligatoire). La réponse contient un secret, montré une seule fois. À chaque changement d'état nous appelons votre URL en POST avec le même corps que ci-dessus plus "evenement": "remise", et l'en-tête X-Wapy-Signature: sha256=… : le HMAC-SHA256 du corps brut avec votre secret. Vérifiez-le avant de croire l'appel. DELETE /pont/v1/webhook retire l'adresse.
Vérifier la signature (Node)
import crypto from "node:crypto";
function signatureValide(corpsBrut, entete, secret) {
const attendu = "sha256=" + crypto.createHmac("sha256", secret).update(corpsBrut).digest("hex");
return crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(entete || ""));
}Aucun appel ne porte de destinataire ni de contenu : uniquement nos identifiants, l'état et l'heure. Un webhook qui ne répond pas n'est pas réessayé ; l'interrogation reste possible à tout moment.
Codes d'erreur
Le champ detail nomme la cause exacte. Un envoi refusé est tracé : rejouer la même clé d'idempotence après un refus renverra ce refus, pas un nouvel essai — changez de clé pour réessayer.
| Code | Sens | Cas |
|---|---|---|
| 400 | Requête invalide | Champ manquant, numéro illisible, message vide ou trop long, en-tête Idempotency-Key absent. |
| 401 | Clé refusée | Clé absente, inconnue, ou remplacée par une plus récente. |
| 403 | Envoi interdit | conditions-non-acceptees, compte-non-valide, compte-suspendu, aucune-cle, destinataire-desabonne. |
| 404 | Destinataire inexistant | destinataire-inexistant : ce numéro n'a pas de compte WhatsApp. Rien n'est envoyé ni compté. |
| 429 | Limite atteinte | trop-rapide, plafond-horaire, plafond-journalier, plafond-otp-destinataire. L'en-tête Retry-After indique le délai. |
| 502 | WhatsApp injoignable | Votre numéro n'a pas pu émettre alors qu'il semblait connecté. Réessayez avec la même clé d'idempotence. |
| 503 | Numéro déconnecté | numero-deconnecte : la session WhatsApp de votre compte est tombée ou attend un scan de QR. Rien n'est envoyé ni compté ; reconnectez le numéro depuis la console. Vous êtes aussi prévenu par e-mail. |
Limites et bon usage
Un intervalle minimal sépare deux envois. Cinquante paiements encaissés à midi ne doivent pas produire cinquante messages en dix secondes : mettez vos envois en file d'attente et respectez le Retry-After renvoyé sur un 429. C'est ce qui protège votre numéro, pas seulement notre service.
Un destinataire qui répond STOP cesse définitivement d'être joignable depuis votre compte : les envois suivants renvoient 403 destinataire-desabonne. Traitez ce code comme une désinscription dans votre base, pas comme une erreur passagère.
Exemples complets
Node.js
async function envoyerRecu(numero, montant, reference) {
const r = await fetch("https://wapy.pro/pont/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.WAPY_PONT_CLE}`,
"Idempotency-Key": `paiement-${reference}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
destinataire: numero,
texte: `Paiement reçu : ${montant} FCFA. Merci.`,
consentement: reference,
}),
});
if (r.status === 429) {
const attente = Number(r.headers.get("Retry-After") || 5);
throw new Error(`Limite atteinte, réessayer dans ${attente}s`);
}
if (!r.ok) throw new Error((await r.json()).detail);
return r.json();
}PHP
function envoyer_recu(string $numero, int $montant, string $reference): array {
$ch = curl_init("https://wapy.pro/pont/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("WAPY_PONT_CLE"),
"Idempotency-Key: paiement-" . $reference,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"destinataire" => $numero,
"texte" => "Paiement reçu : {$montant} FCFA. Merci.",
"consentement" => $reference,
], JSON_UNESCAPED_UNICODE),
]);
$corps = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
throw new RuntimeException("Envoi refusé : " . $corps);
}
return json_decode($corps, true);
}Python
import os, requests
def envoyer_recu(numero: str, montant: int, reference: str) -> dict:
r = requests.post(
"https://wapy.pro/pont/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['WAPY_PONT_CLE']}",
"Idempotency-Key": f"paiement-{reference}",
},
json={
"destinataire": numero,
"texte": f"Paiement reçu : {montant} FCFA. Merci.",
"consentement": reference,
},
timeout=20,
)
if r.status_code == 429:
raise RuntimeError(f"Limite atteinte, réessayer dans {r.headers.get('Retry-After', '5')}s")
r.raise_for_status()
return r.json()Ce que nous conservons
Aucun contenu de message, aucun code de vérification, aucun numéro en clair. Nous gardons des métadonnées d'envoi — date, genre, taille, issue — pendant quatre-vingt-dix jours, puis elles sont effacées. Les destinataires n'apparaissent que sous forme d'empreinte irréversible, servant uniquement à appliquer les limites.
Le détail des engagements est dans les conditions d'utilisation.