Configuring
Webhook URLs and signing secrets are dashboard-only operations (API-key auth can’t change them). In your merchant dashboard:- Webhooks tab → set the callback URL.
- Webhook secret tab → create a secret. The raw secret is shown once at creation; the server stores only an encrypted copy.
- Optionally hit “Test” to fire a one-shot delivery against the URL.
Event types
Trade lifecycle:trade.initiated— buy accepted, escrow held, items not yet placed at the marketplace.trade.pending— marketplace worker successfully placed the order; awaiting fulfillment.trade.active— marketplace is processing/delivering.trade.hold— items received, holding for the dispute window.trade.completed— hold released, trade settled.trade.failed— couldn’t be placed/delivered (insufficient supply, marketplace decline, etc.). Each item’serrorcarries aTradeFailureCode; user fully refunded.trade.canceled— canceled before delivery (by the platform, or by the user while stillpending); user fully refunded, no penalty.trade.declined— buyer’s fault after the Steam offer went out (declined or let it expire); user refunded minus a 2% penalty.trade.reverted— items recalled or refunded after the marketplace accepted.trade.settled— fee/earnings ledger postings completed (paired withtrade.completed).trade.refunded— full or partial refund posted to the user wallet; carries a per-itemrefundblock (amount refunded + any buyer-fault penalty, in USD).
deposit.initiateddeposit.pendingdeposit.completeddeposit.partialdeposit.expireddeposit.faileddeposit.refundeddeposit.cancelled
payout.crypto.deposit.completed— a deposit landed in the partner’s payout custody (per-(chain, token) sidecar credited). Distinct fromdeposit.completed— the latter is for spot wallet credits.payout.crypto.withdraw.approval— synchronous approval gate, see below. Special delivery semantics.payout.crypto.withdraw.broadcast— withdrawal broadcast on-chain. IncludestxHashandnonce.payout.crypto.withdraw.confirmed— final confirmation reached.payout.crypto.withdraw.refunded— the single terminal failure event: any approval rejection, broadcast failure, or on-chain revert re-credits the sidecar and restores the payout wallet. There is no separate.failedevent.
The approval callback — synchronous, not queued
payout.crypto.withdraw.approval uses the same envelope, headers, and signature scheme as
every other event — only the delivery semantics differ:
Because the envelope and signing match every other event, one handler with one Standard
Webhooks verifier can ingest approvals alongside lifecycle events — just dispatch on
event.type === 'payout.crypto.withdraw.approval' to apply the synchronous behavior below.
When your server receives payout.crypto.withdraw.approval:
- Verify the signature (same scheme as every other event).
- Look up
data.withdrawal.externalIdin your records. If you didn’t create this withdrawal — respond 4xx immediately to refund. - If you did create it, confirm
destination,amountCents, andforUserExternalIdmatch what you expect. - Return 2xx within 5 seconds → the worker proceeds to
queued→broadcast. - Return 4xx → withdrawal is immediately refunded.
1823 CRYPTO_PAYOUT_NO_CALLBACK_URL at submission time — it never reaches this stage.
Approval payload shape
payout.crypto.withdraw.broadcast, .confirmed,
.refunded) carry the exact same data.withdrawal shape — only status, txHash,
broadcastAt, confirmedAt, and failureReason differ across the lifecycle.
Envelope
Every delivery body is JSON in this shape — including the synchronous approval callback:Trade object you’d get from the trade
detail endpoint — id, status, currency, totalPrice, full
items array, etc. The items array reflects the trade’s state at
event-delivery time, including each item’s price, marketplace,
delivery mode, status, and display metadata as it becomes available.
Each refunded item also carries an inline refund breakdown (amount,
penalty, buyerFault, with amount + penalty == price) on the item
itself — present on every trade event once the item is refunded, so
prefer it over the top-level refund block when reconciling.
Settlement events (trade.settled, trade.completed) additionally
include the fee breakdown. trade.refunded events additionally include a
refund block — the per-item breakdown of how much went back to the
wallet (refunded) and any buyer-fault penalty withheld, both in USD.
One trade.refunded is emitted per refunded item; match it to the item
via refund.itemId. Deposit events carry a serialised deposit (gateway
or self-hosted crypto fields, depending on method).
Delivery headers
webhook-signature is space-separated-value style: t=... is the
timestamp, s=... is the current-secret signature, and s1=... (when
present) is the previous-secret signature for the rotation window.
Signature verification
The signature is HMAC-SHA256 over<id>.<timestamp>.<raw_body> keyed
with your secret, encoded as base64url (no padding).
Verify on the raw bytes, before JSON parsing — re-serialised JSON has
different whitespace and won’t match.
With @skinshark/sdk
verifyWebhook checks the timestamp tolerance (default 300s, configurable
via toleranceSeconds), validates the HMAC against s= and the rotation
slot s1=, and returns the parsed event. It throws SkinsharkError with
key: "INVALID_SIGNATURE" on any failure mode.
Without the SDK
If you don’t want the SDK runtime, the verification is ~30 lines ofnode:crypto:
Retries and disabling
- Deliveries enqueue immediately and run as a background processor.
- Failed deliveries (non-2xx, timeout, network error) retry on a fixed schedule — 11 attempts total spread over ~14¾ hours per event: 30s, 30s, 5m, 5m, 15m, 15m, 1h, 1h, 6h, 6h.
- After 11 attempts, the delivery is marked
exhausted— no further retries for that event. - The same
webhook-idis reused on every retry of the same event. - If your callback URL is failing continuously for 72 hours (no intervening success), the URL is auto-disabled. Fix the endpoint, then re-enable from the dashboard. Until then, new events are queued but not delivered.
- Use resend in the dashboard or via API to retry an exhausted / individual delivery on demand.
Testing locally
Use a public tunnel (ngrok, Cloudflare Tunnel) to expose your local handler, set the URL in the dashboard, then hit “Test” to fire a synthetic event — or trigger a real one with a tiny buy.Inspecting deliveries
The merchant dashboard’s Webhooks → Deliveries tab lists every attempt with the request payload, response status, response headers, truncated response body, and error if any. Useful for debugging signature verification because you can see exactly what we sent.Webhooks vs WebSocket
Most production setups use both: webhook fires source-of-truth, WS pushes
the same event for UI snappiness.