Documentation menu
Docs / Build
Webhooks
Receive signed events for every change, verify them, and handle retries, duplicates and ordering.
Set up your endpoint#
Set the HTTPS address of your endpoint in Webhooks in the developer portal. Every event is a POST with an Event object:
{
"id": "evt_01Uj8Ik2Ol4",
"object": "event",
"type": "payment.completed",
"api_version": "v1",
"environment": "sandbox",
"created_at": "2026-10-15T14:04:10Z",
"data": {
"object": { "id": "pay_3As6Df9Gh2", "object": "payment", "status": "completed", "version": 3 },
"previous_attributes": { "status": "requires_confirmation" }
}
}data.object is the full record after the change (shortened above), and previous_attributes holds the earlier values of the fields that changed. All event types are in the webhook events reference.
Verify the signature#
Every request carries A2A-Signature: t=<unix seconds>,v1=<hex>. The v1 value is an HMAC-SHA256, with your webhook signing secret, of the timestamp, a full stop and the raw request body. During a secret rotation there is one v1 for each active secret; accept the event if any of them matches.
- Read the raw body before parsing JSON. Re-serialized JSON will not match.
- Reject a timestamp more than five minutes from your clock.
- Compare in constant time.
import crypto from "node:crypto";
export function verifyA2ASignature(rawBody, header, secrets, toleranceSeconds = 300) {
const parts = String(header).split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
return secrets.some((secret) => {
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
});
}import hashlib
import hmac
import time
def verify_a2a_signature(raw_body: bytes, header: str, secrets: list[str], tolerance: int = 300) -> bool:
items = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in items if k == "t"), None)
sigs = [v for k, v in items if k == "v1"]
if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
signed = t.encode() + b"." + raw_body
for secret in secrets:
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(expected, s) for s in sigs):
return True
return Falseusing System.Security.Cryptography;
using System.Text;
public static class A2AWebhooks
{
public static bool VerifySignature(string rawBody, string header, IEnumerable<string> secrets, int toleranceSeconds = 300)
{
var parts = header.Split(',').Select(p => p.Trim().Split('=', 2)).Where(p => p.Length == 2).ToList();
var t = parts.FirstOrDefault(p => p[0] == "t")?[1];
var sigs = parts.Where(p => p[0] == "v1").Select(p => p[1]).ToList();
if (!long.TryParse(t, out var ts) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > toleranceSeconds) return false;
var payload = Encoding.UTF8.GetBytes($"{t}.{rawBody}");
foreach (var secret in secrets)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Encoding.ASCII.GetBytes(Convert.ToHexString(hmac.ComputeHash(payload)).ToLowerInvariant());
if (sigs.Any(s => CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(s), expected))) return true;
}
return false;
}
}The portal's Webhooks page has a signature checker, and a drill that sends a tampered event: your endpoint must refuse it with a 4xx status.
Acknowledge quickly, then process#
Answer with any 2xx status within 10 seconds, then process the event in the background. Redirects are not followed. If the delivery fails, it is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, then every 12 hours until 72 hours after the first attempt.
Duplicates and ordering#
- Duplicates. The same event can arrive more than once. Store each
idyou have processed and skip repeats. - Ordering. Events can arrive out of order. Every record has a
versionthat increases with each change; ignore an event whose objectversionis lower than the one you hold. - Missed events. After downtime, list what you missed with
GET /v1/webhook-events?from=2026-10-15T00:00:00Z, or resend one withPOST /v1/webhook-events/{id}/resend.
Rotate your signing secret#
- In the portal, add a new secret. Both secrets now sign every event.
- Deploy the new secret to your servers, alongside the old one.
- Retire the older secret in the portal.
Receive webhooks on your own computer#
The hosted sandbox can deliver only to public HTTPS addresses. To develop locally without a tunnel, run the forwarding tool: it receives every webhook for your workspace and forwards it, with its original body and signature, to your local server.
A2A_CLIENT_ID=cli_sbx_... A2A_CLIENT_SECRET=sk_sbx_... \
node a2a-listen.mjs --base https://sandbox.a2ainstapay.com --forward-to http://localhost:3000/webhooksDownload a2a-listen.mjs. It needs Node.js 18 or later and no other software.
Test delivery problems#
In the portal settings, turn on the chaos options to receive duplicates, out-of-order events and a failed first attempt, or to receive 503 errors from the API. Use Send a sample event for any event type, and move the sandbox clock forward to see the retry schedule run.
Questions about this page? Contact developer support.