Webhooks
Starter kit & partner-webhook pointer
Want a runnable example? The webhook-starter-kit.zip is a Bun starter kit, ready to download and run: a bare-bones Express receiver that HMAC-verifies deliveries (one dependency, express) plus a zero-dependency fetch card-creation script.
Note the two distinct systems: this page documents the card SDK's webhook helpers, which use a single signature header and a different verifier. Partner self-subscribers using their API token (and the raw delivery/wire shape) belong in the Partner Card API guide, which uses the partner API's x-agio-timestamp + x-agio-signature scheme.
Receive real-time notifications for Agio events and verify their authenticity using HMAC-SHA256 signatures.
Webhook starter kit
A zero-dependency Bun starter kit — an HMAC-verifying webhook receiver plus an end-to-end card-creation script — is ready to download and run:
⬇ Download webhook-starter-kit.zip
For the raw Partner Card API delivery shape and verification reference, see the Partner Card API guide.
Webhook Event History
Query past webhook events delivered to your endpoint:
// Get all webhook events
const { data: events } = await cards.webhooks.getWebhooks();
// Filter by resource, action, or time range
const { data: events } = await cards.webhooks.getWebhooks({
resourceType: "transaction",
resourceAction: "created",
requestSentAtAfter: "2026-01-01T00:00:00Z",
limit: 50
});
// Get a specific webhook event
const { data: event } = await cards.webhooks.getWebhook("webhook-event-id");WebhookEvent Structure
interface WebhookEvent {
id: string;
requestBody: CardWebhook; // The webhook payload
requestSentAt: string; // When Agio sent the webhook
responseReceivedAt?: string; // When your server responded
responseStatus?: number; // Your server's HTTP status
responseBody?: unknown; // Your server's response
attemptCount?: number; // Delivery attempt number
}Verify Webhook Signatures
Agio signs every webhook payload with HMAC-SHA256 using your API key. Always verify signatures before processing.
const isValid = cards.webhooks.verifyWebhookSignature(
requestBody, // Raw request body (string or object)
signature // Value from the "signature" header
);The SDK uses timing-safe comparison to prevent timing attacks.
Parse Webhook Payloads
const webhook = cards.webhooks.parseWebhook(requestBody);
console.log(webhook.id); // Webhook event ID
console.log(webhook.resource); // "transaction", "card", "collateral", etc.
console.log(webhook.action); // "created", "updated", "completed", etc.
console.log(webhook.body); // Event-specific payloadWebhook Resources and Actions
| Resource | Actions | Description |
|---|---|---|
transaction | requested, created, updated, completed | Spend, collateral, payment, and fee transactions |
card | created, updated | Card status changes |
collateral | created, updated, completed | Collateral deposits and withdrawals |
company | created, updated | Company status changes |
user | created, updated | User status changes |
contract | created, updated | Smart contract events |
report | created, completed | Report generation events |
disputes | created, updated | Dispute lifecycle events |
Express.js Handler Example
import express from "express";
import { createClient } from "agio-card-sdk";
const app = express();
const cards = createClient({ baseURL, apiKey });
app.post("/webhooks/cards", express.json(), (req, res) => {
const signature = req.headers["signature"] as string;
if (!signature) {
return res.status(400).json({ error: "Missing signature header" });
}
const isValid = cards.webhooks.verifyWebhookSignature(req.body, signature);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const webhook = cards.webhooks.parseWebhook(req.body);
switch (`${webhook.resource}.${webhook.action}`) {
case "transaction.created":
handleNewTransaction(webhook.body);
break;
case "card.updated":
handleCardUpdate(webhook.body);
break;
case "collateral.completed":
handleCollateralDeposit(webhook.body);
break;
}
res.json({ received: true });
});Signing Key Rotation
Rotate webhook signing keys without downtime using the two-step process:
Step 1: Create a Secondary Key
const { data: keyResponse } = await cards.webhooks.createSecondarySigningKey();
console.log(keyResponse.currentSigningApiKey.name); // Current primary
console.log(keyResponse.newSecondarySigningApiKey.key); // New secondary keyUpdate your webhook handler to accept signatures from either key during the transition.
Step 2: Promote Secondary to Primary
Once you have verified the secondary key works:
const { data: promoted } = await cards.webhooks.promoteSecondaryKeyToPrimary();
console.log(promoted.oldPrimarySigningApiKey.name); // Deleted
console.log(promoted.newPrimarySigningApiKey.key); // Now primaryDANGER
Promoting the secondary key deletes the old primary key. Verify your handler works with the secondary key before promoting.
Signature Key Scopes
Signature keys can be managed at three scopes for payment and withdrawal operations:
// Company scope
await cards.signatures.getCompanyPaymentSignature("company-id", params);
await cards.signatures.getCompanyWithdrawalSignature("company-id", params);
// Tenant scope
await cards.signatures.getTenantPaymentSignature("tenant-id", params);
await cards.signatures.getTenantWithdrawalSignature("tenant-id", params);
// User scope
await cards.signatures.getUserPaymentSignature("user-id", params);
await cards.signatures.getUserWithdrawalSignature("user-id", params);SignatureParams
interface SignatureParams {
token: string; // Token contract address
amount: string; // Amount in token units
adminAddress: string; // Admin wallet address
chainId?: number; // Target chain ID
isAmountNative?: boolean;
cardCollateralContractId?: string;
recipientAddress?: string; // Required for withdrawals
}Signatures are returned as either ready (with data and salt) or pending (with optional retryAfter).