Webhooks push purchase, buyback, redemption and payout events to your backend so you don't
have to poll. Manage them with the register, list, delete and delivery log endpoints
(scope webhooks:manage).
POST /api/v1/webhooks with { url, event_types? }. Omit event_types to subscribe to everything. Up to 20 webhooks per
partner. Private or internal URLs are rejected.
The response includes the HMAC signing_secret, a 64-hex string that is shown exactly once, at creation. Store it immediately, because GET /webhooks never returns secrets.
Each event is delivered as a POST to your URL with these headers:
X-Mystery-Event: purchase.fulfilled X-Mystery-Delivery: <event_id> # idempotency key, dedupe on this X-Mystery-Timestamp: <unix_ms> X-Mystery-Signature: t=<unix_ms>,sha256=<hex>
And this body shape:
{ "event": "purchase.fulfilled", "id": "purchase.fulfilled:5012",
"data": { "purchase_id": 5012, "status": "FULFILLED", "onchain_request_id": "0xabc…" } } HMAC_SHA256(signing_secret, "<timestamp>.<raw_body>") using the raw request body.sha256= value in X-Mystery-Signature.X-Mystery-Delivery, since retries re-send the same id.Any non-2xx response (or timeout) is retried up to 6 attempts with exponential backoff. Use the delivery log to debug a consumer that is not receiving events.
| Event | Fires when |
|---|---|
deposit.credited | An on-chain deposit was confirmed and credited to an end-user's custodial wallet |
purchase.reserved | A custodial purchase reserved the funds from the wallet balance (202 accepted) |
purchase.submitted | On-chain purchase broadcast |
purchase.fulfilled | The random reveal finished and the purchase is complete — payload includes the hydrated items[] when available (see below) |
purchase.refunded | Purchase refunded (e.g. machine empty) |
purchase.failed | Pre-broadcast failure |
instant_purchase.reserved | A custodial instant purchase reserved the funds |
instant_purchase.submitted | The buy-and-open transaction was broadcast on-chain |
instant_purchase.fulfilled | The VRF settled and the cards were delivered — the payload is the whole serialized purchase, pack and cards included (see below) |
instant_purchase.refunded | The on-chain purchase was refunded |
instant_purchase.failed | Pre-broadcast failure |
buyback.confirmed | A holder accepted a pool-model buyback offer and the OfferAccepted event settled (the fronted USDC lands on your bill) |
buyback.transfer_held | The bought-back card reached the pool signer and is held pending forwarding |
buyback.card_transferred | The bought-back card was forwarded to your pool wallet |
buyback.transfer_failed | Forwarding the bought-back card failed — the card is held; contact support |
pool.item_pulled | A pull delivered an item from YOUR pool inventory (pool model — the item lands on your bill) |
sellback.confirmed | A holder sold cards back to the pool. Includes the tokens, the USDC they were paid and the tx hash — this amount is the deduction in your net revenue |
redemption.prepared | Redemption prepared and the transaction to send is ready (emitted inline by prepare) |
redemption.updated | Every redemption status transition thereafter (burn, fulfillment, completion, failure) |
payout.statement_ready | A revenue-share period closed. Carries the frozen statement: gross, sell backs, net, your share |
payout.paid | Your share was paid out. Includes the amount and the on-chain tx hash |
The buyback and payout payloads carry everything you need to reconcile state without a second
call. sellback.confirmed includes the token_ids, the total_usdc the pool paid and the transaction_hash; the two payout.* events carry the whole frozen statement, so your ledger can mirror ours
from the webhook alone.
purchase.fulfilled also carries the hydrated items[] array — the same
shape GET /purchase/:id returns, including the
canonical card_id — so a reveal needs no follow-up call at all. Hydration is
best-effort and the key is omitted entirely rather than sent empty when it
fails, so treat a delivery with no items as "fetch the purchase by purchase_id", not as a pull that yielded nothing.
instant_purchase.fulfilled goes further: its data is the entire
serialized instant purchase, the same object GET /instant/purchase/:id returns,
with the delivered pack and its cards. If hydration fails it degrades to the core purchase
fields, and in the worst case to just id, purchase_id and status. Read the keys you need defensively and fall back to the purchase endpoint;
only those three are guaranteed.