Sign in
Introduction/Webhook Notifications

Webhook Notifications

Receive real-time payment and payout status updates via HMAC-signed webhooks.

The 2328.io system sends a webhook to your url_callback whenever a payment status changes. This is the recommended way to get notified about successful payments.

Request format

  • Method: POST
  • Content-Type: application/json
  • Signature: sign field in the request body

Payload

The webhook body is identical to the /v1/payment/info response, plus a sign field used for signature verification.

Successful payment

JSON
{
  "uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
  "order_id": "ORDER-12345",
  "amount": "180.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
  "expires_at": "2026-05-09T16:56:58+03:00",
  "created_at": "2026-05-09T15:56:58+03:00",
  "payer_currency": "TON",
  "payer_amount": "0.95256917",
  "network": "TON",
  "address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
  "payment_status": "paid",
  "txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "payment_amount": "0.95256917",
  "merchant_amount": "0.949711462490000000",
  "amount_usd": "2.41324380",
  "exchange_rate": "0.01340691",
  "sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}

Cancelled / failed payment

When the payment is not in a terminal paid state, txid, payment_amount, and merchant_amount are null:

JSON
{
  "uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
  "order_id": "ORDER-12345",
  "amount": "2800.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
  "expires_at": "2026-05-09T06:19:04+03:00",
  "created_at": "2026-05-09T05:19:04+03:00",
  "payer_currency": "ETH",
  "payer_amount": "0.01620968",
  "network": "ETH-ERC20",
  "address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
  "payment_status": "cancel",
  "txid": null,
  "payment_amount": null,
  "merchant_amount": null,
  "amount_usd": "37.53934800",
  "exchange_rate": "0.01340691",
  "sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}

Field reference

FieldTypeDescription
uuidstringPayment UUID
order_idstringYour order ID
amountdecimal (8 dp)Fiat amount in currency
currencystringFiat currency the merchant requested
urlstringHosted checkout URL
expires_atstring (ISO 8601)When the payment session expires
created_atstring (ISO 8601)When the payment session was created
payer_currencystringCrypto the payer is paying in
payer_amountdecimal (8 dp)Amount of crypto expected
networkstringBlockchain network
addressstringDeposit address
payment_statusstringOne of: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (see References)
txidstring | nullBlockchain tx hash, present only after a confirmed payment
payment_amountdecimal | nullActual paid amount, present only after payment
merchant_amountdecimal (18 dp) | nullAmount credited to merchant after fees
amount_usddecimal (8 dp)Amount in USD at the time of creation
exchange_ratedecimalCrypto / fiat exchange rate used
signstring (hex)HMAC-SHA256 signature of the payload

Verifying the signature

To verify a webhook signature:

  1. Extract the sign field from the payload
  2. Remove the sign field from the object
  3. Encode the remaining fields as JSON
  4. Encode the JSON in Base64
  5. Compute HMAC-SHA256 from the Base64 string using your API_KEY
  6. Compare the computed signature with the sign value using a constant-time comparison
PHP
<?php
function verifyWebhookSign(array $data, string $apiKey): bool {
    $receivedSign = $data['sign'] ?? '';
    unset($data['sign']);

    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $base64 = base64_encode($json);
    $calculated = hash_hmac('sha256', $base64, $apiKey);

    return hash_equals($calculated, $receivedSign);
}

$apiKey = 'YOUR_API_KEY';
$payload = json_decode(file_get_contents('php://input'), true);

if (!verifyWebhookSign($payload, $apiKey)) {
    http_response_code(401);
    exit;
}

switch ($payload['payment_status']) {
    case 'paid':
    case 'overpaid':
        // Credit the order — check idempotency by order_id first
        break;
    case 'underpaid_check':
    case 'underpaid':
    case 'cancel':
        break;
}

http_response_code(200);

Always verify the signature before crediting any funds to a user. An unsigned or incorrectly-signed webhook could be a spoofed request.

Payout webhooks

When a payout's status changes, the system sends a POST webhook to the url_callback URL passed when the payout was created. If url_callback was not provided, no webhooks are sent for that payout.

Payout webhooks must be verified with your Payout API key — not the regular API key. The signing algorithm is identical to payment webhooks (strip sign, JSON-encode, base64, HMAC-SHA256), only the key differs.

Payload

JSON
{
  "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
  "order_id": "4dfdcc84402b1185b71cbe399321533e",
  "status": "completed",
  "currency": "TRX",
  "network": "TRX-TRC20",
  "amount": "3.00",
  "merchant_amount": "3.00",
  "network_amount": "3.00",
  "amount_usd": "1.04",
  "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
  "memo": null,
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "block_number": null,
  "error_type": null,
  "created_at": "2026-05-07T00:08:38+03:00",
  "updated_at": "2026-05-07T00:08:54+03:00",
  "from_currency": "USDT",
  "debited_amount": "1.050735",
  "debited_currency": "USDT",
  "sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}

Field reference

FieldTypeDescription
uuidstringPayout UUID
order_idstringYour idempotency / reference ID, if you provided one
statusstringpending, completed, failed, cancelled (see References)
currencystringWithdrawal currency
networkstringBlockchain network
amountdecimalWithdrawal amount (in currency)
merchant_amountdecimalAmount charged from the merchant balance
network_amountdecimalAmount actually sent on-chain
amount_usddecimalUSD value at the time of the payout
to_addressstringRecipient blockchain address
memostring | nullMemo / destination tag, if used
txidstring | nullBlockchain transaction hash, set on completed
block_numberinteger | nullBlock height of the on-chain transaction
error_typestring | nullReason when status = failed (e.g. aml_risk, see References)
created_atstring (ISO 8601)When the payout was created
updated_atstring (ISO 8601)When the status last changed
from_currencystringSource balance the payout was debited from when auto-conversion was used (e.g. USDT for a BTC payout)
debited_amountdecimalAmount debited from from_currency balance
debited_currencystringCurrency of the debit
signstring (hex)HMAC-SHA256 signature of the payload, signed with the Payout API key

Best practices

  • Idempotency — Always check if the payment has already been processed (by order_id or uuid). Webhooks may arrive multiple times.
  • Fast response — Return HTTP 200 as quickly as possible. Offload heavy work to a background queue.
  • Retries — If the system doesn't receive an HTTP 200, the webhook is resent after 2 minutes. Maximum 5 retry attempts.
  • Async processing — Handle webhook events asynchronously to avoid blocking the response.
  • Security — ALWAYS verify the sign signature before trusting the payload.

Webhooks can arrive out of order. Don't assume the first webhook you receive is the final state — always re-fetch via /v1/payment/info (or /v1/payout/status/{uuid}) if you need certainty.