Webhooks

Webhooks push purchase, buyback, deposit and redemption 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).

Registration

POST /api/v1/webhooks with { url, event_types? } — omit event_types to subscribe to everything. Up to 20 webhooks per partner. URLs that resolve to private or internal addresses are rejected (SSRF guard).

The response includes the HMAC signing_secret — a 64-hex string that is shown exactly once, at creation. Store it immediately; GET /webhooks never returns secrets.

Delivery

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…" } }

Verifying signatures

  1. Compute HMAC_SHA256(signing_secret, "<timestamp>.<raw_body>") using the raw request body.
  2. Compare it (constant-time) against the sha256= value in X-Mystery-Signature.
  3. Reject deliveries whose timestamp is older than ~5 minutes (replay protection).
  4. Dedupe on X-Mystery-Delivery — retries re-send the same id.

Retries

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 types

EventFires when
deposit.creditedA USDC deposit landed and was credited to an end-user balance
purchase.reservedCustodial purchase accepted — credits held
purchase.submittedOn-chain purchase broadcast
purchase.fulfilledVRF revealed the items — purchase complete
purchase.refundedPurchase refunded (e.g. machine empty) — hold returned
purchase.failedPre-broadcast failure — hold returned
buyback.confirmedThe holder accepted your buyback offer on-chain
buyback.transfer_heldBought-back NFT is waiting because no pool wallet is set (sent once)
buyback.card_transferredBought-back NFT delivered to your pool wallet
buyback.transfer_failedTerminal transfer failure after max retry attempts
redemption.preparedRedemption prepared — burn calldata issued (emitted inline by prepare)
redemption.updatedEvery redemption status transition thereafter (burn, fulfillment, completion, failure)

The buyback-transfer and redemption payloads carry everything needed to reconcile state — for example buyback.card_transferred includes the token_id, contract_address, to_address and tx_hash of the NFT delivery, and buyback.transfer_held tells you to set a pool wallet via PUT /mystery/pool-wallet.