Skip to main content
Webhooks push events to your server as they happen, so you don’t have to poll. Register an endpoint, subscribe to the events you care about, and Agentcard delivers a signed JSON payload for each one — a card being used, a transaction settling, a balance running low. Events fired by sandbox activity carry livemode: false; live activity carries livemode: true. A destination belongs to one mode: it’s created in the mode you’re viewing and receives only that mode’s events — register one in each mode to run both.

Register an endpoint

Webhook destinations live in the dashboard: open your organization → WebhooksAdd destination, pick the events, and paste your endpoint URL. Subscribe to All events, a whole family like transaction.*, or individual events — and change the selection any time. Creating a destination reveals its signing secret (prefixed whsec_) — store it, you use it to verify every delivery’s signature. You can reveal it again from the destination’s page, and rotate it there too (for example after a suspected leak). The same page sends test events so you can prove your handler before any real traffic, pauses or deletes the destination, and shows a live view of recent deliveries with their response codes.

The event payload

Every delivery is a POST with this envelope. Dedupe on id — the same event can be delivered more than once.
{
  "id": "evt_…",
  "type": "transaction.authorized",
  "created": 1751976000,
  "livemode": true,
  "data": {
    "id": "…",
    "card_id": "card_…",
    "last4": "4242",
    "amount_cents": 675,
    "merchant": "COFFEE SHOP #42 SAN FRANCISCO",
    "status": "PENDING",
    "balance_cents": 1825
  }
}
FieldMeaning
idStable, idempotent event id (evt_…). Dedupe on this.
typeThe event type (see the catalog below).
createdUnix epoch seconds.
livemodefalse for sandbox/test events, true for live.
dataEvent-specific payload; its shape depends on type.

Verify the signature

Every delivery carries an AgentCard-Signature header so you can confirm it came from us and wasn’t replayed. The header looks like:
AgentCard-Signature: t=1751976000,v1=<hex-hmac>
v1 is an HMAC-SHA256 of "{t}.{raw_request_body}" keyed with your endpoint secret. Verify it against the raw body (before any JSON parsing), and reject timestamps that are too old to blunt replays.
import crypto from "node:crypto";

// `rawBody` must be the exact bytes we sent — do not re-serialize parsed JSON.
// Returns false (never throws) on a missing or malformed header, so a random
// probe to your endpoint can't turn verification into a 500.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  if (typeof header !== "string" || !header) return false;
  const parts = Object.fromEntries(
    header.split(",").map((kv) => {
      const i = kv.indexOf("=");
      return i === -1 ? [kv, ""] : [kv.slice(0, i), kv.slice(i + 1)];
    }),
  );
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const given = Buffer.from(String(parts.v1 ?? ""), "utf8");
  const want = Buffer.from(expected, "utf8");
  return given.length === want.length && crypto.timingSafeEqual(given, want);
}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    t = int(parts.get("t", 0))
    if not t or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
A legacy X-AgentCard-Signature: sha256=<hex-hmac> header (an HMAC of the body alone, no timestamp) is also sent for backward compatibility. New integrations should verify AgentCard-Signature — the timestamp gives you replay protection.
Respond 2xx to acknowledge. Any other status (or a timeout) is treated as a failure and retried.

Delivery and retries

We attempt each delivery up to 5 times with backoff on any non-2xx response, timeout, or network error. Requests time out after 10 seconds. The destination’s page in the dashboard shows every recent delivery — status, attempt count, the response code we got, and when the next retry is scheduled. An endpoint that keeps failing is eventually disabled; fix your handler, then re-enable the destination from its page.

Event catalog

Subscribe to any of these by name (or "*" / a "family.*" prefix).
EventFires when
card.createdA card is issued.
card.updatedA card’s balance or status changes.
card.closedA card is closed (one-time-use spend, manual close, or expiry).
cardholder.createdA cardholder is created.
cardholder.updatedA cardholder’s details change.
cardholder_onboarding_session.completedA hosted onboarding session finishes.
transaction.authorizedA charge is authorized (funds held).
transaction.clearedA charge settles.
transaction.declinedA charge is declined.
transaction.voidedAn authorization is reversed.
balance.lowA card’s remaining balance drops into the low range.
merchant.connectedA cardholder links a merchant (e.g. DoorDash).
card_flow.startedA user’s company-funded card creation reserved your pool (company wallet).
transfer.approvedYou (or auto-approve) confirmed collection for a company-funded card.
transfer.initiatedThe pool-to-user transfer was submitted on-chain.
transfer.completedThe funds became the user’s spending power. Capture your customer here.
transfer.failedCompany funding aborted before the card was created.
transfer.releasedA company-funded card closed with unused balance.
card_flow.failedThe mint failed after funds moved; the allocation stays as user spending power.
wallet.fundedA deposit landed in your company wallet.
wallet.balance.lowYour company wallet dropped under its threshold.