En esta página
Contenido
Lee el sobre versionado del webhook y la instantánea completa de la factura, el retiro o el depósito del canal que viaja en data.
Todo cuerpo de webhook es un sobre versionado. El objeto data sigue el mismo contrato de recurso orientado al comercio que devuelve la API de estado correspondiente, de modo que tu handler puede aplicar una instantánea completa en lugar de fusionar cambios parciales. Vale para los tres recursos: facturas, retiros y depósitos de canal comparten un mismo sobre y una misma regla de contenido.
Sobre
| Campo | Tipo | Contrato |
|---|---|---|
event_id |
string | Identificador estable con prefijo (evt_…), idéntico a X-Webhook-Id y sin cambios entre reintentos y reenvíos |
event_type |
string | Un valor del catálogo de eventos |
version |
integer | Versión del esquema del contenido. La versión actual es 1 |
occurred_at |
Unix seconds | Momento de la transición del recurso; a diferencia de la marca temporal de entrega, no cambia al reintentar |
data |
object | Instantánea completa de la factura, el retiro o el depósito del canal para este evento |
event_id identifica una entrega: la copia que un endpoint recibe de una transición. Dos endpoints suscritos al mismo evento reciben dos valores evt_ distintos para el mismo pago. La identidad de negocio es el id del recurso dentro de data — inv_…, wdr_… o pcd_… — y es el mismo entre endpoints, transiciones y reenvíos. Usa el id de negocio para no contar dos veces un pago, y event_id para no procesar dos veces una entrega.
Guarda event_id, event_type, version y el cuerpo bruto antes de empezar cualquier trabajo costoso. Rechaza una versión de esquema que tu integración no admita en lugar de adivinar su forma.
Contenido de una factura
Ejemplo: invoice.paid.
{
"event_id": "evt_J7EEYeL9pZJukfj2c5OQ44",
"event_type": "invoice.paid",
"version": 1,
"occurred_at": 1739281200,
"data": {
"invoice_id": "inv_5CcyDYmMUGtzYL10q0Iimr",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"status": "paid",
"is_final": true,
"is_test": false,
"payment_url": "https://checkout.paymos.io/invoice/inv_5CcyDYmMUGtzYL10q0Iimr",
"order": {
"external_id": "order-12345",
"client_id": "customer-67890",
"amount": "100.00",
"currency": "USD"
},
"payment": {
"currency": "USDT",
"network": "TRC20",
"chain_id": 728126428,
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"expected": "50.00",
"address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"exchange_rate": "2.00",
"paid": "50.00",
"remaining": "0",
"fee": "0.50",
"net": "49.50",
"transfers": [
{
"tx_hash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"amount": "50.00",
"status": "confirmed",
"created_at": 1739281020,
"confirmed_at": 1739281200,
"required_confirmations": 19,
"estimated_confirmation_at": 1739281080,
"explorer_url": "https://tronscan.org/#/transaction/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
]
},
"expires_at": 1739284200,
"completed_at": 1739281200,
"created_at": 1739277600,
"updated_at": 1739281200
}
}
El data de una factura sigue la respuesta de Obtener factura. Los campos que todavía no existen para el estado actual llegan como null o el contrato JSON los omite; usa status e is_final como autoridad sobre el estado.
Contenido de un retiro
Ejemplo: withdrawal.completed.
{
"event_id": "evt_CbYeOgBp9Kt2U3j74fxxLl",
"event_type": "withdrawal.completed",
"version": 1,
"occurred_at": 1739281200,
"data": {
"withdrawal_id": "wdr_7K2M9P4Q8R1X5Z3A0bC2dE",
"external_order_id": "payout-12345",
"status": "completed",
"is_final": true,
"is_test": false,
"amount": "25.00",
"fee": "0.50",
"currency": "USDT",
"network": "TRC20",
"destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"tx_hash": "7f8a9b0c1d2e3f4a5b6c7d8e9f00112233445566778899aabbccddeeff001122",
"explorer_url": "https://tronscan.org/#/transaction/7f8a9b0c1d2e3f4a5b6c7d8e9f00112233445566778899aabbccddeeff001122",
"created_at": 1739277600,
"completed_at": 1739281200
}
}
El data de un retiro sigue la respuesta de Obtener retiro. tx_hash y explorer_url se rellenan cuando hay una transacción en cadena bajo seguimiento; no sustituyen a status.
Contenido de un depósito del canal
Ejemplo: payment_channel.deposit.confirmed.
{
"event_id": "evt_J7EEYeL9pZJukfj2c5OQ44",
"event_type": "payment_channel.deposit.confirmed",
"version": 1,
"occurred_at": 1767225930,
"data": {
"id": "pcd_8ScRvL4jNq2XkB7mTfZdWu",
"payment_channel_id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"payment_channel_external_id": "customer-42",
"status": "confirmed",
"is_final": true,
"is_test": false,
"currency": "USDT",
"network": "TRC20",
"chain_id": 728126428,
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"gross": "100",
"fee": "1",
"net": "99",
"applied_fee_percent": 1.0,
"customer_fee_percent": 0,
"tx_hash": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
"transfer_id": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25:41a614f803b6fd780986a42c78ec9c7f77e6ded13c:41c9f6a2b7d0138e54ca3b91f6072ed48a5c1e93b7:0",
"source_address": "TW9s4RkAqBnLpVdX2ChYzUeGm7QfKt3NbZ",
"destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"block_height": 68421905,
"first_included_block_timestamp": 1767225900,
"explorer_url": "https://tronscan.org/#/transaction/9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
"created_at": 1767225870,
"updated_at": 1767225930,
"confirmed_at": 1767225930
}
}
El data de un depósito sigue la respuesta de Obtener depósito del canal. status e is_final son la autoridad sobre el estado; applied_fee_percent y customer_fee_percent son las tarifas congeladas para ese depósito y no se mueven cuando cambias tu precio.
Los tres eventos del depósito llevan el mismo objeto, así que un contenido con confirming ya te dice el canal, el identificador del pagador, el importe y las pruebas en cadena. confirming y reorged son informativos y pueden llegar en cualquier orden: que ninguno de los dos haga retroceder un depósito que ya sabes confirmed.
Compatibilidad
- Pueden aparecer campos nuevos sin cambio de versión. Ignora los campos que tu handler no use.
- El significado de los campos existentes no cambia dentro de una misma versión del contenido.
- Trata los identificadores y los importes decimales como cadenas. No interpretes los ID con prefijo como UUID ni el dinero como coma flotante binaria.
- Verifica la firma contra los bytes brutos antes de analizar el JSON. Volver a serializar el objeto altera la entrada firmada.