Skip to main content
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

EventSession statusTriggerExample use case
session.processingprocessingDeposit detected, funds being routedShow “payment received” to user
session.succeededsucceededFunds delivered to destinationFulfill the order, send receipt
session.bouncedbouncedDelivery failed, funds refundedAlert 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:
  1. Read the raw body. Don’t parse JSON first — you need the exact bytes.
  2. Extract t and v1 from the Daimo-Signature header by splitting on , and =.
  3. Compute HMAC-SHA256 of ${t}.${rawBody} using your webhook secret.
  4. Compare the computed signature to v1 using crypto.timingSafeEqual.
  5. 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

FieldTypeDescription
idstringUnique event ID (UUID). Use for idempotency.
typestringOne of session.processing, session.succeeded, session.bounced
createdAtnumberUnix timestamp (seconds) when the event was created
data.sessionobjectSession snapshot at event time. Same shape as the session object, without clientSecret.
isTestEventbooleantrue 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:
HeaderDescription
Content-Typeapplication/json
Daimo-Signaturet=<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.