Documentation Index
Fetch the complete documentation index at: https://docs.daimo.com/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks let your server receive real-time notifications when a session transitions to a new status. Instead of polling, Daimo sends a POST request to your endpoint with the event payload.
Event types
| Event | Session status | Trigger | Example use case |
|---|
session.processing | processing | Deposit detected, funds being routed | Show “payment received” to user |
session.succeeded | succeeded | Funds delivered to destination | Fulfill the order, send receipt |
session.bounced | bounced | Delivery failed, funds refunded | Alert support, notify customer |
Subscribe to all events with ["*"] or pick specific types.
Quickstart
1. Register an endpoint
curl -X POST https://api.daimo.com/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/daimo",
"events": ["*"]
}'
The response includes a secret. Store it securely, you’ll use it to verify that incoming requests are from Daimo.
2. Handle events
Set up a route on your server to receive webhook events:
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.method === "POST" && req.url === "/webhooks/daimo") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const event = JSON.parse(body);
// TODO: verify signature (see below)
switch (event.type) {
case "session.succeeded":
// Handle successful delivery
break;
case "session.bounced":
// Handle failed delivery
break;
}
res.writeHead(200).end();
});
}
});
server.listen(4242);
3. Send a test event
Verify your endpoint is working by sending a test event:
curl -X POST https://api.daimo.com/v1/webhooks/{webhookId}/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"eventType": "session.succeeded"}'
Test events include isTestEvent: true in the payload so you can filter them out of your business logic.
Verify signatures
Every webhook delivery includes a Daimo-Signature header for verifying authenticity. Always verify signatures in production to ensure requests are from Daimo.
How it works
The signature header looks like this:
Daimo-Signature: t=1700000000,v1=5257a869...
To verify a webhook:
- Read the raw body. Don’t parse JSON first — you need the exact bytes.
- Extract
t and v1 from the Daimo-Signature header by splitting on , and =.
- Compute HMAC-SHA256 of
${t}.${rawBody} using your webhook secret.
- Compare the computed signature to
v1 using crypto.timingSafeEqual.
- Reject stale timestamps. If
t is more than 5 minutes old, discard the event to prevent replay attacks.
Full verification function
import * as crypto from "crypto";
const TIMESTAMP_TOLERANCE_SEC = 300; // 5 minutes
function verifyWebhookSignature(
secret: string,
signatureHeader: string,
rawBody: string,
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => {
const [k, ...v] = p.split("=");
return [k, v.join("=")];
}),
);
const ts = parts["t"];
const sig = parts["v1"];
if (!ts || !sig) return false;
const tsNum = parseInt(ts, 10);
if (isNaN(tsNum)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - tsNum);
if (age > TIMESTAMP_TOLERANCE_SEC) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex"),
);
} catch {
return false;
}
}
Complete handler with verification
import { createServer } from "node:http";
import * as crypto from "crypto";
const WEBHOOK_SECRET = process.env.DAIMO_WEBHOOK_SECRET!;
const server = createServer((req, res) => {
if (req.method === "POST" && req.url === "/webhooks/daimo") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const signature = req.headers["daimo-signature"] as string;
if (!verifyWebhookSignature(WEBHOOK_SECRET, signature, body)) {
res.writeHead(400).end("invalid signature");
return;
}
const event = JSON.parse(body);
switch (event.type) {
case "session.succeeded":
// Handle successful delivery
break;
case "session.bounced":
// Handle failed delivery
break;
}
res.writeHead(200).end();
});
}
});
server.listen(4242);
Event payload
Field reference
| Field | Type | Description |
|---|
id | string | Unique event ID (UUID). Use for idempotency. |
type | string | One of session.processing, session.succeeded, session.bounced |
createdAt | number | Unix timestamp (seconds) when the event was created |
data.session | object | Session snapshot at event time. Same shape as the session object, without clientSecret. |
isTestEvent | boolean | true for test events sent via /test endpoint. Omitted for real events. |
Here’s an example of a session.succeeded event:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "session.succeeded",
"createdAt": 1700000000,
"data": {
"session": {
"sessionId": "abcdef1234567890abcdef1234567890",
"status": "succeeded",
"destination": {
"type": "evm",
"address": "0x...",
"chainId": 8453,
"chainName": "Base",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenSymbol": "USDC",
"amountUnits": "10.00",
"delivery": {
"txHash": "0x...",
"receivedUnits": "10.00"
}
},
"display": {
"title": "Deposit to Acme",
"verb": "Deposit"
},
"paymentMethod": {
"type": "evm",
"receiverAddress": "0x...",
"createdAt": 1700000000
},
"metadata": { "myUserId": "user_123" },
"createdAt": 1700000000,
"expiresAt": 1700003600
}
}
}
Delivery behavior
Every delivery includes these headers:
| Header | Description |
|---|
Content-Type | application/json |
Daimo-Signature | t=<unix_seconds>,v1=<hmac_hex> (see Verify signatures) |
- Daimo waits 10 seconds for your server to respond.
- Any 2xx status code counts as success.
- Failed deliveries are retried with exponential backoff: the n-th retry waits 2^(n-1) minutes.
- After 10 failed attempts, the event is marked as failed and no further retries are made.
Test events
Use POST /v1/webhooks/{webhookId}/test to send a test event. You can optionally specify an eventType parameter (defaults to session.succeeded).
Test events contain isTestEvent: true in the payload. Use this flag to skip business logic during testing.
Best practices
- Return 200 quickly. Process events asynchronously if your handler does heavy work. Daimo times out after 10 seconds.
- Verify signatures. Always verify the
Daimo-Signature header in production to confirm requests are from Daimo.
- Handle test events. Check
event.isTestEvent and skip side effects (e.g. order fulfillment) for test events.
- Be idempotent. Daimo may deliver the same event more than once. Log processed event IDs and skip duplicates. The
event.id uniquely identifies each event.