Webhooks and events
Every state change is an event. Events are listed in order at GET /v1/events (Direct Deposit partners: GET /api/partner/events), and when SET has a webhook URL for your account each one is also POSTed to it, signed.
Event types
| Object | Types |
|---|---|
| Customer | customer.updated |
| Bank account | bank_account.created |
| Cash-Out Address | cash_out_address.created, cash_out_address.updated |
| Virtual account | virtual_account.created, virtual_account.deactivated, virtual_account.activated |
| Deposit | deposit.<state> (for example deposit.funds_received, deposit.payment_processed), deposit.delivered, deposit.chain_mismatch, deposit.chain_failed |
| Cash-Out payment | cash_out_payment.<state> (for example cash_out_payment.payment_processed, cash_out_payment.returned) |
| Conversion | conversion_address.created, conversion_payment.<state>, conversion_payment.delivered, .chain_mismatch, .chain_failed |
| Wallet | wallet.created, wallet.activity, wallet_withdrawal.created, wallet_withdrawal.<state> |
| Invoice (Merchant API) | invoice.created, invoice.<state> (paid, short, over), invoice.voided, payment.matched, payment.unmatched, journal_entry.created |
| Account | return_addresses.updated, ping (test) |
| Direct Deposit partner | payout.<state>, payable_payment.<state>, payee.direct_allowed, payee.direct_revoked, ping, listed at GET /api/partner/events |
{ "id": "evt_…", "object": "event", "type": "deposit.payment_processed", "livemode": true, "createdAt": "…", "data": { … } }Verifying the signature
SET-Signature: t=1793072643,v1=5f2c…v1 is the hex HMAC-SHA256 of "<t>.<raw body>" keyed with your webhook secret (GET /v1/webhook_secret). Verify before parsing:
const [t, v1] = header.split(",").map((part) => part.split("=")[1]);
const expected = hmacSha256Hex(secret, `${t}.${rawBody}`);
if (!timingSafeEqual(expected, v1) || Math.abs(Date.now() / 1000 - Number(t)) > 300) reject();Reject a timestamp more than five minutes old, compare in constant time, and use the raw bytes of the body.
Delivery
- A
2xxanswer is delivered. Anything else is retried: after 1 minute, doubling, at most a day apart, 15 tries. - Delivery is at least once. Deduplicate on the event
id. - Order is not guaranteed across retries; use
createdAtand the object's forward-onlystate. GET /v1/events/{id}shows the delivery record;POST /v1/events/{id}/redeliversends one again;POST /v1/webhook_testsends a signedping.- Without a webhook, poll
GET /v1/events?after=<last id>.
What an event proves
deposit.payment_processed means the provider sent the stablecoin; deposit.delivered means SET read it on chain at the destination. cash_out_payment.payment_processed (paidToBank: true) means the provider paid the bank. Treat only these as outcomes; never a redirect or a status your front end saw.