AIMERICAAPI · Developers

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

FilterEvents delivered
calls.sessionscalls.session.setup, .ringing, .answered, .hold, .ended — one per leg, as it happens
calls.logcalls.log — a call is over and in the log, with its final result
messagesmessages.received, .sent, .delivered, .failed
voicemailsvoicemails.new
faxesfaxes.received, .sent, .failed
presencepresence.changed
recordings.readyrecordings.ready — a recording can be fetched
testtest.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 2xx within 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 (PUT or the button in the app).
  • A subscription expires at expires_at (30 days by default) and then stops delivering: call /renew before 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_at and the content rather than trusting arrival order.
First-generation webhooks (24 September 2026; addresses under Settings → Developer → Webhooks, events call.ended, sms.received…) keep working untouched. They have no handshake and no expiry, which makes them the convenient choice for Zapier.