Callbacks
AssetPay sends HTTP POST requests to your callback URL whenever a trade changes state. These webhooks are your primary mechanism for tracking trade progress and updating user balances.Setup
Configure your callback URL in the AssetPay dashboard under Settings. Your API secret is used to sign every webhook — keep it private. Your callback endpoint must:- Accept POST requests with JSON body
- Respond with HTTP 200 within 15 seconds
- Be publicly accessible (no auth required from our side)
Callback Payload
The body of every state-change callback is the full trade object directly:| Field | Description |
|---|---|
trade | Full Trade object with current state |
trade.status | The trade’s new status — this is the event you’re being notified about |
trade.source | "client" (end-user via /client/trading/*) or "self" (merchant self-trade via /secure/*). The same callback URL fires for both. |
trade.status — there is no separate event field on the body.
The withdrawal
initiated callback acts as an approval gate — your handler must explicitly approve or reject it. See The initiated callback for withdrawals below.Signature Verification
Every webhook is signed with HMAC-SHA256 using your API secret. The signature is delivered in theX-AssetPay-Signature header — not in the body.
Header format
s1=<previous-hex-signature> so callbacks signed with the prior secret remain verifiable during the grace window.
What’s signed
The HMAC-SHA256 message is the literal string:deliveryId— value from theid=field in the headertimestamp— value from thet=field (ISO 8601)rawBody— the raw request body bytes, exactly as received. Do not re-serialize the parsed JSON; parsers may reorder keys or change spacing and break the HMAC.
Verification example
Handling Events
Theevent you’re handling is the new value of trade.status. AssetPay dispatches a webhook on every status transition.
Deposit Callbacks
trade.status | What to Do |
|---|---|
initiated | Trade created, no Steam offer sent yet. Acknowledge with 200. No balance action. |
active | Steam trade offer sent to the user — awaiting their acceptance. Acknowledge with 200. |
hold | User accepted, items received, Steam hold started. Credit preCredit if using instant deposits; otherwise just acknowledge. |
completed | Hold ended cleanly (or no hold required for Rust). Credit pendingCredit (or totalPrice if not using instant deposits). |
failed | Trade did not go through. No credit needed. Optionally notify the user. |
canceled | Trade was canceled before any state was applied (incl. an unaccepted offer auto-canceled past its window). No action needed. |
declined | The end user declined the Steam trade offer. No items received, no credit needed. Optionally notify the user. |
reverted | A previously completed deposit was reversed. Reverse any preCredit + pendingCredit already credited. Check revertedBy. |
Withdrawal Callbacks
trade.status | What to Do |
|---|---|
initiated | Approval gate — see below. The merchant’s wallet hasn’t been debited yet; your response decides whether the withdrawal proceeds. |
pending | Supplier is sourcing the item. Acknowledge with 200. No balance action. |
active | Steam trade offer sent to the user — awaiting acceptance (offerID now present). Acknowledge with 200. |
hold | Items sourced and Steam trade offer accepted by the user — hold period running (offerID + holdEndDate present). Acknowledge with 200. |
completed | Hold ended cleanly. No action needed. |
failed | Trade did not go through. Refund totalPrice to the user’s balance if you previously deducted it. |
canceled | Trade was canceled before processing. Refund totalPrice to the user’s balance if you previously deducted it. |
reverted | A previously completed withdrawal was reversed (by supplier or user). Refund totalPrice. Check revertedBy. |
initiated → pending → active → hold → completed. The intermediate statuses (pending, active) are progress signals and may be skipped depending on the provider and delivery mode, so drive balance changes off initiated and the terminal states.
The initiated callback for withdrawals
This is the most important callback in the withdrawal flow. When a withdrawal is initiated, AssetPay sends you this callback before purchasing anything. Your backend decides whether the withdrawal proceeds.
The approval callback body has the same shape as every other state callback:
trade.type === "withdraw" and trade.status === "initiated". The signature header and signing rules are also identical to regular callbacks — same X-AssetPay-Signature: t=...,id=...,s=... format, same <deliveryId>.<timestamp>.<rawBody> HMAC input.
Your handler should:
- Verify the signature
- Look up the user by
trade.clientSteamIDortrade.externalClientUserId - Check if they can afford
trade.totalPrice - If yes: deduct the balance and respond with
2xx - If no: respond with a rejection (see below)
Self-trade withdrawals (
source === "self") also go through this gate. The merchant must have a callback URL configured — withdrawals fail immediately with MERCHANT_NO_CALLBACK_URL (1705) if none is registered. If you want to auto-approve all self-trades, your handler can return 200 whenever trade.source === "self".Rejecting a Withdrawal
The withdrawal approval gate accepts several rejection signals: Option 1 — HTTP 4xx (recommended):4xx status works (400, 402, 403, 409, etc.). The reason field is optional but recommended — it appears in AssetPay logs for debugging.
Option 2 — HTTP 2xx with a rejection body:
{ "status": "rejected" }, { "errorCode": "INSUFFICIENT_BALANCE" }, { "code": "INSUFFICIENT_BALANCE" }.
In either case, the trade is marked failed, the merchant wallet is refunded automatically, and you receive a follow-up failed callback.
Only
4xx responses or one of the explicit rejection bodies above are treated as intentional rejections. A plain 200 with any other body counts as approval. 5xx responses and timeouts are treated as delivery failures and retried.Idempotency
You may receive the same callback more than once (network retries, duplicate deliveries). Your handler should be idempotent. Track processed (trade, status) pairs and skip duplicates:Retry Behavior
If your endpoint returns a non-2xx status, times out, or doesn’t respond within 15 seconds, AssetPay retries the callback:- Total attempts: 11 (initial + 10 retries)
- Backoff schedule:
30s, 30s, 5m, 5m, 15m, 15m, 1h, 1h, 6h, 6h - Total window: ~14h 31m from the first attempt to the last retry
Callback Example: Complete Deposit Flow
Here’s the sequence of callbacks you’d receive for a typical CS2 deposit:failed callback instead of the next step. If the user declines the offer you’ll get declined, and if it’s auto-canceled after sitting unaccepted you’ll get canceled. If a reversal happens after completed, you’ll get reverted. Rust deposits skip the hold step entirely and go straight from active to completed.
Callback Example: Complete Withdrawal Flow
Here’s the sequence of callbacks you’d receive for a typical CS2 withdrawal:initiated callback or the supplier purchase fails, you’ll get a failed callback (refund the user’s balance). A reversal after hold surfaces as reverted. Rust withdrawals normally skip hold — Rust has no 7-day reversal window, so they usually go active → completed directly with no holdEndDate (a Steam security escrow is the exception and surfaces as hold). Act on initiated and the terminal states rather than expecting every step.