Event Types
All webhook bodies are application/x-www-form-urlencoded, so every value is a string. A field whose value is null arrives as the literal 4-character string "null" (not JSON null, not an absent key). Before parsing numbers, treat "null" and "" as absent — e.g. Number("null") is NaN, and if (contractAddress) is truthy for the string "null". Fields typed string | null below follow this rule.
Test events — the test field. A test webhook can be triggered for any event below — by you, via POST /session/:uuid/test-webhook, or by AbstraPay during onboarding. A test event is byte-for-byte identical to a real one — same fields, same HMAC signature — except it carries an extra test field with the string value "true". No funds have moved on-chain for a test event. Your handler MUST check for test === "true" and skip all real side effects (crediting a user, marking a withdrawal settled, updating balances) for those events — acknowledge with { "error": 0 } without applying ledger changes. Real (production) events never include the test field.
On a DEPOSIT test event the depositId is a negative sentinel (e.g. -1234567), derived deterministically from the session so repeat test sends of the same session carry the same id. Real deposits always have a positive depositId, so a negative value is an unambiguous "synthetic test" marker — never treat it as a real deposit id or persist it as one.
1. DEPOSIT
Sent when a user's deposit is confirmed on-chain and funds have been swept to the bankroll.
Delivery: Asynchronous, via a durable outbox — retried up to 5× with backoff, then dead-lettered (see Retries). Return { "error": 0 } to acknowledge; any non-zero error (or a non-2xx / non-JSON response) is treated as a failed delivery and retried — so acknowledge with 0 even for duplicates or events you choose to ignore.
Payload fields:
| Field | Type | Description |
|---|---|---|
event | string | DEPOSIT |
depositId | number | Internal deposit ID |
idempotencyKey | string | Unique deduplication key for this deposit |
externalUserId | string | The user identifier you provided when creating the deposit ticket |
sessionRef | string | null | Opaque handle for the session that produced this deposit — the same value returned when you created the session. Arrives as "null" when the deposit had no active session (a direct on-chain send); attribute by externalUserId + dedupe by idempotencyKey in that case |
sessionAmount | string | null | The expected deposit amount you set on /session/deposit, in your platform fiat (localCurrencyCode). Reconcile it against the received amountInLocalCurrency before crediting. "null" for a direct send with no session |
amount | string | Net raw amount that reached the bankroll (gross − fee), token's smallest unit |
amountInDollar | string | null | Net USD value — credit this to the user |
amountInLocalCurrency | string | null | Net value in your platform's local fiat (e.g. TRY). Equals amountInDollar when your platform currency is USD |
localCurrencyCode | string | Your platform's fiat code used for the conversion ("USD" when none is configured) |
currentRate | string | USD → localCurrencyCode rate used at settlement ("1" for USD) |
pair | string | Conversion pair, e.g. "USD/TRY" |
grossAmount | string | Raw amount the user actually sent on-chain |
feeAmount | string | Raw fee deducted on settlement ("0" when no fee) |
feeInDollar | string | null | USD value of the fee ("0.000000" when no fee) |
type | string | ERC20 or NATIVE |
contractAddress | string | null | Token contract address (null for native transfers) |
transactionHash | string | null | Inbound (deposit) transaction hash |
chainId | number | Chain ID where the deposit occurred |
confirmedAt | string | ISO 8601 timestamp |
Amounts: amount / amountInDollar are the net credited to you (fee already deducted). Reconciles as amount + feeAmount = grossAmount. With no fee configured, feeAmount = "0" and grossAmount = amount. Credit amountInDollar, not the raw amount.
Local currency: amountInLocalCurrency / currentRate are a snapshot taken at settlement in your platform's configured fiat, so you can book the deposit in your own currency without a second lookup. The rate can move afterwards — these values are the ones in effect when the deposit confirmed. When your platform currency is USD, localCurrencyCode = "USD", currentRate = "1", and amountInLocalCurrency = amountInDollar.
Expected response:
{
"error": 0,
"description": "ok",
"transactionId": "your-internal-id"
}Idempotency: Use idempotencyKey to deduplicate. You may receive the same deposit event more than once.
2. WITHDRAW_REQUEST
Sent to request your approval of a withdrawal. Delivered one of two ways depending on your operator's approval mode (see Withdrawals):
- Synchronous mode (default): sent before the withdrawal record is persisted, and the system waits for your response — return
error: 0to approve, non-zero to deny.withdrawRequestIdis populated here too. - External-approval mode: sent asynchronously (retried via our outbox) after the request is created as
AWAITING_APPROVAL. Your response only acknowledges receipt (error: 0); the real decision is a separate callback you make toPOST /withdraw/{withdrawRequestId}/decision.
Both modes send the identical event: "WITHDRAW_REQUEST" payload, and withdrawRequestId is always populated — use it as the single correlator across WITHDRAW_REQUEST → WITHDRAW_COMPLETE. In external-approval mode, always acknowledge with { "error": 0 } and drive the outcome from your callback.
Delivery: Synchronous (default mode) or asynchronous + retried (external-approval mode).
Payload fields:
| Field | Type | Description |
|---|---|---|
event | string | WITHDRAW_REQUEST |
withdrawRequestId | string | The withdrawal UUID — always populated (both modes). Use it in your /withdraw/{id}/decision callback (external-approval mode) and as the correlator to the later WITHDRAW_COMPLETE |
sessionRef | string | Opaque handle for the owning session — the same value returned when you created the withdraw session. A session-level grouping handle; for per-withdrawal correlation use withdrawRequestId |
externalUserId | string | The user identifier |
type | string | DIRECT_TRANSFER or OFF_RAMP |
address | string | Destination wallet address |
cryptoCurrencyCode | string | The token that will actually be sent, e.g. USDT, ETH |
fiatCurrencyCode | string | e.g. USD, EUR |
fiatAmount | string | The operator-set fiat amount from the withdraw session (what you sent on /session/withdraw) |
cryptoAmount | string | null | Always "null" — the withdrawal is denominated in the operator-set fiat fiatAmount; the crypto to send is cryptoAmountSent |
cryptoAmountSent | string | null | The crypto amount that will actually be sent on-chain — reconcile your token ledger against this |
amountInUsd | string | null | USD value — check this against the user's balance |
requestedAt | string | ISO 8601 timestamp |
To approve - return:
{
"error": 0,
"description": "ok"
}To deny - return a non-zero error code:
{
"error": 5,
"description": "Daily withdrawal limit exceeded"
}Error codes:
| Code | Message Shown to User |
|---|---|
0 | (approved) |
1 | Withdrawal request declined by operator |
2 | Insufficient balance for withdrawal |
3 | Withdrawal temporarily unavailable |
4 | User is not eligible for withdrawal |
5 | Daily withdrawal limit exceeded |
6 | Withdrawal amount too low |
7 | Withdrawal amount too high |
8 | Account verification required |
9 | Withdrawal suspended for this account |
Any other non-zero code will show the description field from your response as the error message.
3. WITHDRAW_COMPLETE
Sent after a withdrawal has been completed (funds paid out on-chain).
Delivery: Asynchronous, via a durable outbox — retried up to 5× with backoff, then dead-lettered (see Retries). Return { "error": 0 } to acknowledge; any non-zero error (or a non-2xx / non-JSON response) is treated as a failed delivery and retried, so acknowledge even for duplicates.
Payload fields:
| Field | Type | Description |
|---|---|---|
event | string | WITHDRAW_COMPLETE |
withdrawRequestId | string | The withdrawal UUID — the same id sent on the WITHDRAW_REQUEST event (always populated), so you can correlate a completion back to the request you approved |
sessionRef | string | Opaque handle for the owning session — the same value returned when you created the withdraw session. Provided for integrations that group by session |
externalUserId | string | The user identifier |
type | string | DIRECT_TRANSFER or OFF_RAMP |
status | string | COMPLETED |
address | string | Destination wallet address |
cryptoCurrencyCode | string | The token sent, e.g. USDT, ETH |
fiatCurrencyCode | string | e.g. USD, EUR |
fiatAmount | string | The operator-set fiat amount from the withdraw session |
cryptoAmount | string | null | Always "null" — the crypto sent is cryptoAmountSent |
cryptoAmountSent | string | null | The crypto amount actually sent on-chain (matches WITHDRAW_REQUEST.cryptoAmountSent) — reconcile your token ledger against this |
amountInUsd | string | null | USD value of the withdrawal |
completedAt | string | ISO 8601 timestamp |
Expected response:
{
"error": 0,
"description": "ok"
}