Sign in
Introducción/Notificaciones Webhook

Notificaciones por webhook

Recibe actualizaciones en tiempo real del estado de pagos y retiros mediante webhooks firmados con HMAC.

El sistema de 2328.io envía un webhook a tu url_callback cada vez que cambia el estado de un pago. Esta es la forma recomendada de recibir notificaciones sobre pagos exitosos.

Formato de la solicitud

  • Método: POST
  • Content-Type: application/json
  • Firma: campo sign en el cuerpo de la solicitud

Payload

El cuerpo del webhook sigue el formato de la respuesta de /v1/payment/info y añade tx_explorer_url, además del campo sign utilizado para verificar la firma.

Pago exitoso

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"
}

Pago cancelado / fallido

Cuando el pago no está en un estado paid final, los campos txid, payment_amount y merchant_amount son 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"
}

Referencia de campos

CampoTipoDescripción
uuidstringUUID del pago
order_idstringTu ID de pedido
amountdecimal (8 dp)Monto fiat en currency
currencystringDivisa fiat solicitada por el comerciante
urlstringURL del checkout alojado
expires_atstring (ISO 8601)Cuándo expira la sesión de pago
created_atstring (ISO 8601)Cuándo se creó la sesión de pago
payer_currencystringCripto en la que paga el pagador
payer_amountdecimal (8 dp)Monto de cripto esperado
networkstringRed blockchain
addressstringDirección de depósito
payment_statusstringUno de: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (consulta References)
txidstring | nullHash de la transacción en la blockchain, presente solo después de un pago confirmado
tx_explorer_urlstring | nullURL de la transacción en el explorador de blockchain. Es null si no hay txid o si la transferencia es P2P interna.
payment_amountdecimal | nullMonto realmente pagado, presente solo después del pago
merchant_amountdecimal (18 dp) | nullMonto acreditado al comerciante después de comisiones
amount_usddecimal (8 dp)Monto en USD al momento de la creación
exchange_ratedecimalTipo de cambio cripto / fiat utilizado
signstring (hex)Firma HMAC-SHA256 del payload

Verificar la firma

Para verificar la firma de un webhook:

  1. Extrae el campo sign del payload
  2. Quita el campo sign del objeto
  3. Codifica los campos restantes como JSON
  4. Codifica el JSON en Base64
  5. Calcula HMAC-SHA256 a partir de la cadena Base64 usando tu API_KEY
  6. Compara la firma calculada con el valor de sign usando una comparación de tiempo constante
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);

Verifica siempre la firma antes de acreditar fondos a un usuario. Un webhook sin firma o con firma incorrecta podría ser una solicitud falsificada.

Webhooks de retiro

Cuando cambia el status de un retiro, el sistema envía un webhook POST a la URL url_callback indicada al crear el retiro. Si no se proporcionó url_callback, no se envían webhooks para ese retiro.

Los webhooks de retiro deben verificarse con tu Payout API key — no con la API key habitual. El algoritmo de firma es idéntico al de los webhooks de pago (quitar sign, codificar en JSON, base64, HMAC-SHA256), solo cambia la clave.

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"
}

Referencia de campos

CampoTipoDescripción
uuidstringUUID del retiro
order_idstringTu ID de idempotencia / referencia, si lo proporcionaste
statusstringpending, completed, failed, cancelled (consulta References)
currencystringDivisa de retiro
networkstringRed blockchain
amountdecimalMonto del retiro (en currency)
merchant_amountdecimalMonto cobrado del saldo del comerciante
network_amountdecimalMonto realmente enviado en la blockchain
amount_usddecimalValor en USD al momento del retiro
to_addressstringDirección blockchain del destinatario
memostring | nullMemo / destination tag, si se utilizó
txidstring | nullHash de la transacción en la blockchain, definido al estado completed
tx_explorer_urlstring | nullURL de la transacción en el explorador de blockchain. Es null si no hay txid o si la transferencia es P2P interna.
block_numberinteger | nullAltura del bloque de la transacción on-chain
error_typestring | nullMotivo cuando status = failed (p. ej., aml_risk, consulta References)
created_atstring (ISO 8601)Cuándo se creó el retiro
updated_atstring (ISO 8601)Cuándo cambió por última vez el estado
from_currencystringSaldo de origen del que se debitó el retiro cuando se usó conversión automática (p. ej., USDT para un retiro en BTC)
debited_amountdecimalMonto debitado del saldo from_currency
debited_currencystringDivisa del débito
signstring (hex)Firma HMAC-SHA256 del payload, firmada con la Payout API key

Buenas prácticas

  • Idempotencia — verifica siempre si el pago ya se ha procesado (por order_id o uuid). Los webhooks pueden llegar varias veces.
  • Respuesta rápida — devuelve HTTP 200 lo más rápido posible. Delega el trabajo pesado a una cola en segundo plano.
  • Reintentos — si el sistema no recibe HTTP 200, el webhook se reenvía después de 2 minutos. Máximo 5 intentos de reenvío.
  • Procesamiento asíncrono — gestiona los eventos de webhook de forma asíncrona para no bloquear la respuesta.
  • Seguridad — verifica SIEMPRE la firma sign antes de confiar en el payload.

Los webhooks pueden llegar fuera de orden. No asumas que el primer webhook que recibes es el estado final — vuelve a consultar mediante /v1/payment/info (o /v1/payout/status/{uuid}) si necesitas certeza.

Contrato de entrega y procesamiento

Use la siguiente orden dentro de su endpoint webhook:

  1. Lea el cuerpo de la solicitud sin registrar secretos ni la firma completa.
  2. Identifique si se trata de un evento de pago/cartera estática o de un evento de pago para seleccionar la clave API correcta.
  3. Elimine sign, reproduzca los bytes JSON documentados, calcule HMAC-SHA256 y compare en tiempo constante.
  4. Valide los identificadores requeridos, cadenas decimales y valores de estado.
  5. Inserte atómicamente un registro inbox/idempotency. Si ya existe, devuelva HTTP 200 sin repetir efectos secundarios.
  6. Confirme la mutación del pedido/libro mayor y encole correos electrónicos, análisis o notificaciones no críticos.
  7. Devuelva HTTP 200 rápidamente.

No llame a servicios de terceros lentos mientras mantenga la transacción idempotency. Un timeout después de confirmar pero antes de devolver puede causar un retry; el duplicado debe observar la clave inbox confirmada y convertirse en una operación nula.

EventoIdentidad principalNotas
Sesión de pagouuid + evidencia de estado/versiónEl mismo invoice puede emitir múltiples cambios de estado legítimos.
Recarga de pago parcialinvoice uuid + txidMás de una transferencia puede pertenecer al mismo invoice subpagado.
Depósito de cartera estáticared + txidorder_id se reutiliza en cada depósito a esa cartera.
Pagopago uuid + estadoNunca cree un segundo pago a partir de webhook retry lógica.

Si su esquema no tiene id de evento, almacene el hash verificado payload como evidencia adicional de auditoría, pero no reemplace las identidades comerciales anteriores con una marca de tiempo.

Ordenamiento y reconciliation

La entrega es al menos una vez y los mensajes de estado pueden competir. Implemente reglas comerciales monótonas en lugar de “la última solicitud gana”:

  • nunca vuelva a mover un pedido cumplido a check porque un evento anterior llegó tarde;
  • permita que underpaid_check reciba txids adicionales sin repetir créditos anteriores;
  • trate paid y overpaid como estados settlement exitosos, mientras conserva sus diferentes cantidades;
  • mantenga underpaid como un resultado final de pago parcial a menos que la API autorizada informe otro estado;
  • redirija aml_lock a revisión y no permita que un trabajador genérico retry lo cumpla;
  • consulte información de pago/pago siempre que la transición sea imposible, falte contexto o sea financieramente ambigua.

Ejecute reconciliation programado incluso cuando la entrega webhook parezca saludable. Compare su estado terminal local y la cantidad acreditada con /v1/payment/info, /v1/static-wallet/transactions o /v1/payout/status/{uuid} y alerten sobre diferencias en lugar de sobrescribir silenciosamente el historial del libro mayor.

Seguridad del endpoint Webhook

  • Requiera HTTPS y mantenga el callback públicamente accesible; los objetivos de callback privados/loopback se rechazan durante la creación del pago.
  • Haga cumplir un límite pequeño de cuerpo de solicitud y tipo de contenido JSON.
  • Limitación de tasa antes de trabajos costosos, pero deje suficiente margen para ráfagas legítimas y retries.
  • Nunca autorice un webhook solo por IP de origen. Las listas de permitidos de red son defensa en profundidad; la verificación HMAC es obligatoria.
  • Redacte sign, claves API, direcciones cuando la política lo requiera y metadatos personales de los registros de la aplicación.
  • Mantenga tanto las claves actuales como las de reemplazo programadas explícitamente disponibles durante una ventana de rotación controlada; nunca adivine qué clave firmó un evento.
  • Devuelva un cuerpo de error genérico en firmas inválidas para que el endpoint no se convierta en un oráculo de claves o cuentas.