Webhooks

How you find out money arrived.

Events

EventMeaning
payment.confirmedPaid in full and settled
payment.underpaidSomething arrived, less than invoiced
payment.overpaidMore than invoiced arrived
payment.expiredWindow closed with nothing received
payment.failedSettlement reverted; funds recoverable

Only payment.confirmed means you have been paid.

Payload

{
  "event": "payment.confirmed",
  "created": 1784483382,
  "data": {
    "id": "0x7f3a...",
    "status": "confirmed",
    "amount": "25.00",
    "amount_received": "25.00",
    "currency": "USDC",
    "reference": "ORD-1234",
    "metadata": { "orderId": "1234" },
    "tx_hash": "0x40ec...",
    "path": "address",
    "settled_at": "2026-07-20T11:04:00.000Z"
  }
}

Verifying

X-Specie-Signature: t=1784483382,v1=5257a869...

HMAC-SHA256 over {timestamp}.{raw body}, keyed with your endpoint secret. The timestamp is inside the signed material, so rewriting it invalidates the signature rather than extending its life.

Use the SDK, which does the constant-time comparison and the replay window for you. Verifying by hand:

import { createHmac, timingSafeEqual } from "node:crypto";

const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) throw new Error("Replay");

const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"))) {
  throw new Error("Bad signature");
}

Three things that are not optional: the raw body, a constant-time comparison, and the timestamp window. Skip the window and a captured request stays replayable forever.

Retries

Failures retry with backoff — roughly 10s, 1m, 5m, 30m, 2h, 6h, then a day — before being abandoned. Anything outside 2xx counts as a failure.

Every attempt is in Dashboard → Webhooks, and can be replayed by hand.

Make your handler idempotent

The same event can arrive twice: retries, replays, and at-least-once delivery all cause it. Key on data.id and make repeat deliveries a no-op.

Requirements

Endpoints must be https and publicly resolvable. Private, loopback, link-local and cloud metadata addresses are refused, at registration and again at delivery — see security.

Respond 2xx quickly and do the work asynchronously. Delivery times out after 10 seconds.