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 follows the /v1/payment/info response and adds tx_explorer_url plus the 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",
  "tx_explorer_url": "https://tonviewer.com/transaction/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,
  "tx_explorer_url": 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
tx_explorer_urlstring | nullBlockchain explorer transaction URL. null when txid is absent or the transfer is internal P2P.
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",
  "tx_explorer_url": "https://tronscan.org/#/transaction/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
tx_explorer_urlstring | nullBlockchain explorer transaction URL. null when txid is absent or the transfer is internal P2P.
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.

Delivery and processing contract

Use the following order inside your webhook endpoint:

  1. Read the request body without logging secrets or the full signature.
  2. Identify whether it is a payment/static-wallet event or a payout event so you select the correct API key.
  3. Remove sign, reproduce the documented JSON bytes, calculate HMAC-SHA256, and compare in constant time.
  4. Validate required identifiers, decimal strings, and status values.
  5. Atomically insert an inbox/idempotency record. If it already exists, return HTTP 200 without repeating side effects.
  6. Commit the order/ledger mutation and enqueue non-critical email, analytics, or notifications.
  7. Return HTTP 200 quickly.

Do not call slow third-party services while holding the idempotency transaction. A timeout after you commit but before returning can cause a retry; the duplicate must observe the committed inbox key and become a no-op.

EventPrimary identityNotes
Payment sessionuuid + status/version evidenceThe same invoice can emit multiple legitimate status changes.
Partial-payment top-upinvoice uuid + txidMore than one transfer can belong to the same underpaid invoice.
Static-wallet depositnetwork + txidorder_id is reused by every deposit to that wallet.
Payoutpayout uuid + statusNever create a second payout from webhook retry logic.

If your schema has no event id, store the verified payload hash as additional audit evidence, but do not replace the business identities above with a timestamp.

Ordering and reconciliation

Delivery is at-least-once and status messages can race. Implement monotonic business rules rather than “last request wins”:

  • never move a fulfilled order back to check because an older event arrived late;
  • allow underpaid_check to receive additional txids without repeating earlier credits;
  • treat paid and overpaid as successful settlement states, while preserving their different amounts;
  • keep underpaid as a final partial-payment outcome unless the authoritative API later reports another state;
  • route aml_lock to review and do not let a generic retry worker fulfill it;
  • query payment/payout info whenever the transition is impossible, missing context, or financially ambiguous.

Run scheduled reconciliation even when webhook delivery appears healthy. Compare your local terminal state and credited amount with /v1/payment/info, /v1/static-wallet/transactions, or /v1/payout/status/{uuid} and alert on differences instead of silently overwriting ledger history.

Webhook endpoint security

  • Require HTTPS and keep the callback publicly reachable; private/loopback callback targets are rejected during payment creation.
  • Enforce a small request-body limit and JSON content type.
  • Rate-limit before expensive work, but leave enough headroom for legitimate bursts and retries.
  • Never authorize a webhook by source IP alone. Network allowlists are defense in depth; HMAC verification is mandatory.
  • Redact sign, API keys, addresses when required by policy, and personal metadata from application logs.
  • Keep both current and explicitly scheduled replacement keys available during a controlled rotation window; never guess which key signed an event.
  • Return a generic error body on invalid signatures so the endpoint does not become a key or account oracle.