Webhooks
Une adresse, des filtres, un secret. AIMERICA vous envoie un POST signé à chaque événement.
Créer un abonnement
Avec la permission webhooks:manage. L'adresse doit être publique et en https://. Les filtres choisissent les familles d'événements ; secret est facultatif (nous en générons un sinon) ; expires_in est en secondes, 30 jours par défaut, 365 au plus.
POST https://api.a1merica.ai/v1/webhooks
Authorization: Bearer aim_live_…
Content-Type: application/json
{"url": "https://example.com/aimerica", "filters": ["calls.log", "messages", "test"], "expires_in": 2592000}
La poignée de main
Pendant cette requête, AIMERICA envoie à votre adresse un POST vide avec l'en-tête X-AIMERICA-Validation-Token. Votre serveur doit répondre 2xx en moins de 3 secondes en renvoyant le même en-tête avec la même valeur. Sinon la création échoue avec 422 validation_error « the endpoint did not echo the validation token ». C'est ainsi que personne ne peut abonner une adresse qui ne lui appartient pas.
POST https://example.com/aimerica ← from AIMERICA, during the POST /v1/webhooks above
X-AIMERICA-Validation-Token: 7c1e…f0
Content-Length: 0
HTTP/1.1 200 OK ← what your receiver must answer, within 3 seconds
X-AIMERICA-Validation-Token: 7c1e…f0
{
"id": "0f9e…",
"url": "https://example.com/aimerica",
"filters": ["calls.log", "messages", "test"],
"secret": "whsec_…", ← shown once; every delivery is signed with it
"status": "active",
"expires_at": "2026-10-25T16:00:00Z",
"created_at": "2026-09-25T16:00:00Z",
"last_delivery_at": null,
"failure_count": 0
}
Gardez secret : il n'est montré qu'à la création. GET /v1/webhooks, GET /v1/webhooks/{id}, PUT (adresse, filtres — une nouvelle adresse refait la poignée de main), DELETE, POST /v1/webhooks/{id}/renew (repousse expires_at), POST /v1/webhooks/{id}/test (envoie test.ping tout de suite) et GET /v1/webhooks/{id}/deliveries ({id, event, status_code, attempt, delivered_at, error}) complètent le tout.
Filtres
| Filtre | Événements livrés |
|---|---|
calls.sessions | calls.session.setup, .ringing, .answered, .hold, .ended — un par branche, au moment où ça se passe |
calls.log | calls.log — un appel est terminé et dans le journal, avec son résultat final |
messages | messages.received, .sent, .delivered, .failed |
voicemails | voicemails.new |
faxes | faxes.received, .sent, .failed |
presence | presence.changed |
recordings.ready | recordings.ready — un enregistrement peut être récupéré |
test | test.ping — envoyé par POST /v1/webhooks/{id}/test |
Ce que vous recevez
POST https://example.com/aimerica
Content-Type: application/json
User-Agent: AIMERICA-Webhooks/1.0
X-AIMERICA-Event: calls.session.answered
X-AIMERICA-Delivery: 3e2d1c0b-…
X-AIMERICA-Signature: t=1758816000,v1=5f1a…c9
{
"id": "3e2d1c0b-…",
"event": "calls.session.answered",
"occurred_at": "2026-09-25T16:00:00Z",
"org_id": "a1b2…",
"data": { …the same shape GET /v1/calls/{id} returns… }
}
data a exactement la forme de la ressource dans l'API : un événement calls.* porte un Call, messages.* un Message, et ainsi de suite. Chaque livraison a un id unique — servez-vous-en pour ignorer un doublon.
Vérifier la signature
Prenez t dans l'en-tête, joignez-le au corps brut avec un point, calculez HMAC-SHA256 avec votre secret, comparez avec v1 en temps constant et refusez un horodatage de plus de cinq minutes. Le format est le même pour les webhooks de première génération (Réglages → Développeur → Webhooks) : une seule fonction sert aux deux.
# Python — one snippet verifies both generations of webhooks (same header, same format)
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1 or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, v1)
// Node.js — use the RAW request body, before any JSON parsing
const crypto = require("crypto");
function verify(secret, header, rawBody, tolerance = 300) {
const p = Object.fromEntries(header.split(",").map((s) => s.split("=")));
if (!p.t || !p.v1 || Math.abs(Date.now() / 1000 - Number(p.t)) > tolerance) return false;
const mac = crypto.createHmac("sha256", secret).update(`${p.t}.`).update(rawBody).digest("hex");
return mac.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(p.v1));
}
Réponses, reprises, suspension, expiration
- Répondez
2xxen moins de dix secondes. Autre chose, ou pas de réponse, compte comme un échec. - Après un échec, nous réessayons après 1 min, 5 min, 30 min, 2 h et 12 h, puis abandonnons cette livraison et augmentons
failure_count. Une livraison réussie remet le compteur à zéro. - 20 échecs consécutifs passent l'abonnement à
suspended: plus rien n'est envoyé jusqu'à ce que vous le réactiviez (PUTou le bouton dans l'application). - Un abonnement expire à
expires_at(30 jours par défaut) et cesse alors de livrer : appelez/renewavant, par exemple chaque semaine. C'est ce qui empêche une adresse oubliée de recevoir vos données pendant des années. - Les événements d'un même appel partent dans l'ordre, mais des reprises peuvent arriver dans le désordre : lisez
occurred_atet le contenu plutôt que l'ordre d'arrivée.
call.ended, sms.received…) continuent tels quels. Ils n'ont ni poignée de main ni expiration, ce qui les rend commodes pour Zapier.