Webhooks
FomoPay POSTs a signed event to your endpoint when a payment is created, submitted, confirmed or fails — and when a vault runs low. Verify the signature, answer 2xx quickly, and dedupe on the event id.
Events
| Type | When |
|---|---|
payment.created | A payment was recorded and the recipient snapshot taken. |
payment.submitted | The transaction was broadcast to Robinhood Chain. |
payment.confirmed | Confirmed on-chain and verified by FomoPay. The receipt is final. |
payment.failed | Reverted, dropped, replaced, or failed verification. |
recipient.unresolved | A handle couldn't be resolved to a verified payout wallet. |
vault.low_balance | A reward vault fell below its low-balance threshold. |
Subscribe to specific types or "*" (every event, including types added later). Events belong to a project: payments made with that project's keys, vaults and batches.
Every payload has the envelope { id, type, created_at, data }. Payment events carry the recipient handle, amount, asset, network, tx (the transaction hash once submitted), your metadata and idempotency_key, and the receipt. They never contain the recipient's wallet address.
Signatures
FomoPay-Signatureist=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. The HMAC key is your endpoint secret exactly as shown (whsec_…, as UTF-8).- During a secret rotation the header carries several
v1=entries — one per active secret. Accept the delivery if any of them matches. - Reject deliveries whose
tis more than 5 minutes from your clock: that is what makes a captured request useless for replay. - Sign and verify the raw bytes of the body. Parsing and re-serializing JSON changes whitespace and key order and the signature won't match.
Verify with the SDK
FomoPay.webhooks.verify(rawBody, headers, secret) is static (no API key needed), uses Web Crypto so it runs on Node, Bun, Deno and edge runtimes, accepts a fetch Headers or Node's req.headers, and returns the parsed event — or throws WEBHOOK_SIGNATURE_INVALID / WEBHOOK_TIMESTAMP_EXPIRED.
Verify by hand
Delivery and retries
A delivery succeeds when your endpoint answers 2xx within 10 seconds. Anything else — a non-2xx status, a timeout, a connection error — is retried on this schedule, then marked EXHAUSTED:
That is 9 attempts over roughly 45 hours. Every attempt — status, latency, a short response snippet — is in the delivery log under Dashboard → Webhooks, where you can also replay a delivery or send a test event (test events carry test: true and never include a real transaction).
Redirects are not followed. On mainnet, endpoint URLs must be https and resolve to public addresses — private, loopback and cloud-metadata addresses are refused when you save the endpoint and again at connect time. Locally, http://localhost receivers are allowed for testing.
Return 2xx as soon as the signature checks out and the event is stored; do slow work (emails, game state) asynchronously. Events can arrive more than once and, rarely, out of order — use data.status and created_at, not arrival order.
Manage endpoints
/v1/webhooksscope webhooks:write| Endpoint | Does |
|---|---|
GET /v1/webhooks | List this project's endpoints (no secrets). |
GET /v1/webhooks/:id | One endpoint with delivery health. |
PATCH /v1/webhooks/:id | Change url, events, description; status DISABLED pauses deliveries. |
DELETE /v1/webhooks/:id | Remove the endpoint. |
POST /v1/webhooks/:id/rotate | New secret. The old one keeps signing alongside it for grace_period_hours (default 24, max 168). |
Up to 16 endpoints per project. Secrets are derived on demand from a server key and never stored; the secret is returned on create and rotate, and project members who manage webhooks can reveal it again in the dashboard.