Skip to main content
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:
The “event” you’re handling is 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 the X-AssetPay-Signature header — not in the body.

Header format

If your API secret was recently rotated, the header may also include 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 the id= field in the header
  • timestamp — value from the t= 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

You must verify against the raw request body bytes, not the parsed-then-re-stringified JSON. Most HTTP frameworks need explicit configuration to expose the raw body (e.g. express.raw(), fastify-raw-body).
The 5-minute timestamp tolerance protects against replay attacks. Reject anything older.

Handling Events

The event you’re handling is the new value of trade.status. AssetPay dispatches a webhook on every status transition.

Deposit Callbacks

Withdrawal Callbacks

A withdrawal moves 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:
Identify this as the approval gate by checking 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:
  1. Verify the signature
  2. Look up the user by trade.clientSteamID or trade.externalClientUserId
  3. Check if they can afford trade.totalPrice
  4. If yes: deduct the balance and respond with 2xx
  5. 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):
Any 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:
Also accepted: { "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
If a callback URL fails continuously for 72 hours, it is automatically disabled and removed from rotation. Delivery records are retained for 14 days for debugging — you can view callback history and manually retry failed deliveries from the AssetPay dashboard.

Callback Example: Complete Deposit Flow

Here’s the sequence of callbacks you’d receive for a typical CS2 deposit:
If something goes wrong at any step, you’ll get a 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:
If you reject the 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.