Webhooks
Event Types

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:

FieldTypeDescription
eventstringDEPOSIT
depositIdnumberInternal deposit ID
idempotencyKeystringUnique deduplication key for this deposit
externalUserIdstringThe user identifier you provided when creating the deposit ticket
sessionRefstring | nullOpaque 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
sessionAmountstring | nullThe 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
amountstringNet raw amount that reached the bankroll (gross − fee), token's smallest unit
amountInDollarstring | nullNet USD value — credit this to the user
amountInLocalCurrencystring | nullNet value in your platform's local fiat (e.g. TRY). Equals amountInDollar when your platform currency is USD
localCurrencyCodestringYour platform's fiat code used for the conversion ("USD" when none is configured)
currentRatestringUSD → localCurrencyCode rate used at settlement ("1" for USD)
pairstringConversion pair, e.g. "USD/TRY"
grossAmountstringRaw amount the user actually sent on-chain
feeAmountstringRaw fee deducted on settlement ("0" when no fee)
feeInDollarstring | nullUSD value of the fee ("0.000000" when no fee)
typestringERC20 or NATIVE
contractAddressstring | nullToken contract address (null for native transfers)
transactionHashstring | nullInbound (deposit) transaction hash
chainIdnumberChain ID where the deposit occurred
confirmedAtstringISO 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: 0 to approve, non-zero to deny. withdrawRequestId is 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 to POST /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_REQUESTWITHDRAW_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:

FieldTypeDescription
eventstringWITHDRAW_REQUEST
withdrawRequestIdstringThe 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
sessionRefstringOpaque 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
externalUserIdstringThe user identifier
typestringDIRECT_TRANSFER or OFF_RAMP
addressstringDestination wallet address
cryptoCurrencyCodestringThe token that will actually be sent, e.g. USDT, ETH
fiatCurrencyCodestringe.g. USD, EUR
fiatAmountstringThe operator-set fiat amount from the withdraw session (what you sent on /session/withdraw)
cryptoAmountstring | nullAlways "null" — the withdrawal is denominated in the operator-set fiat fiatAmount; the crypto to send is cryptoAmountSent
cryptoAmountSentstring | nullThe crypto amount that will actually be sent on-chain — reconcile your token ledger against this
amountInUsdstring | nullUSD value — check this against the user's balance
requestedAtstringISO 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:

CodeMessage Shown to User
0(approved)
1Withdrawal request declined by operator
2Insufficient balance for withdrawal
3Withdrawal temporarily unavailable
4User is not eligible for withdrawal
5Daily withdrawal limit exceeded
6Withdrawal amount too low
7Withdrawal amount too high
8Account verification required
9Withdrawal 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:

FieldTypeDescription
eventstringWITHDRAW_COMPLETE
withdrawRequestIdstringThe 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
sessionRefstringOpaque handle for the owning session — the same value returned when you created the withdraw session. Provided for integrations that group by session
externalUserIdstringThe user identifier
typestringDIRECT_TRANSFER or OFF_RAMP
statusstringCOMPLETED
addressstringDestination wallet address
cryptoCurrencyCodestringThe token sent, e.g. USDT, ETH
fiatCurrencyCodestringe.g. USD, EUR
fiatAmountstringThe operator-set fiat amount from the withdraw session
cryptoAmountstring | nullAlways "null" — the crypto sent is cryptoAmountSent
cryptoAmountSentstring | nullThe crypto amount actually sent on-chain (matches WITHDRAW_REQUEST.cryptoAmountSent) — reconcile your token ledger against this
amountInUsdstring | nullUSD value of the withdrawal
completedAtstringISO 8601 timestamp

Expected response:

{
  "error": 0,
  "description": "ok"
}