Webhooks
An address, filters, a secret. AIMERICA POSTs you a signed body for every event.
Create a subscription
With the webhooks:manage scope. The address must be public and https://. Filters pick event families; secret is optional (we generate one otherwise); expires_in is in seconds, 30 days by default, 365 at most.
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}
The handshake
During that request AIMERICA POSTs an empty body to your address with the header X-AIMERICA-Validation-Token. Your server must answer 2xx within 3 seconds and echo the same header with the same value. Otherwise creation fails with 422 validation_error "the endpoint did not echo the validation token". That is how nobody can subscribe an address they do not own.
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
}
Keep secret: it is shown only at creation. GET /v1/webhooks, GET /v1/webhooks/{id}, PUT (address, filters — a new address redoes the handshake), DELETE, POST /v1/webhooks/{id}/renew (pushes expires_at out), POST /v1/webhooks/{id}/test (sends test.ping right away) and GET /v1/webhooks/{id}/deliveries ({id, event, status_code, attempt, delivered_at, error}) complete the set.
Filters
| Filter | Events delivered |
|---|---|
calls.sessions | calls.session.setup, .ringing, .answered, .hold, .ended — one per leg, as it happens |
calls.log | calls.log — a call is over and in the log, with its final result |
messages | messages.received, .sent, .delivered, .failed |
voicemails | voicemails.new |
faxes | faxes.received, .sent, .failed |
presence | presence.changed |
recordings.ready | recordings.ready — a recording can be fetched |
test | test.ping — sent by POST /v1/webhooks/{id}/test |
What you receive
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 has exactly the resource's shape in the API: a calls.* event carries a Call, messages.* a Message, and so on. Each delivery has a unique id — use it to ignore a repeat.
Verifying the signature
Take t from the header, join it to the raw body with a dot, compute HMAC-SHA256 with your secret, compare with v1 in constant time and refuse a timestamp older than five minutes. The format is the same for first-generation webhooks (Settings → Developer → Webhooks): one function serves both.
# 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));
}
Answers, retries, suspension, expiry
- Answer
2xxwithin ten seconds. Anything else, or no answer, counts as a failure. - After a failure we try again after 1 min, 5 min, 30 min, 2 h and 12 h, then give up on that delivery and increment
failure_count. A successful delivery resets the count. - 20 consecutive failures put the subscription in
suspended: nothing more is sent until you switch it back on (PUTor the button in the app). - A subscription expires at
expires_at(30 days by default) and then stops delivering: call/renewbefore that, weekly for instance. That is what keeps a forgotten address from receiving your data for years. - A call's events leave in order, but retries can arrive out of order: read
occurred_atand the content rather than trusting arrival order.
call.ended, sms.received…) keep working untouched. They have no handshake and no expiry, which makes them the convenient choice for Zapier.