Webhooks
Los eventos que manda Aliquot, sus payloads, cómo verificar la firma, el calendario de reintentos, cómo reenviar una entrega, y cómo administrar endpoints y rotar el secreto de firma.
Última actualización 27 oct 2026
Aliquot le avisa a tu servidor de los cambios en facturas, payouts y reembolsos con requests HTTPS POST a los endpoints que registrás. Cada request va firmado con el secreto del endpoint.
Eventos
| Evento | Se manda cuando |
|---|---|
invoice.created | Se crea una factura, por la API o desde un payment link. |
invoice.detected | Se ve en la cadena la primera transferencia a la factura (también cuando una factura con pago incompleto recibe un pago complementario). El pago se está confirmando. |
invoice.paid | El pago alcanzó el umbral de confirmaciones y cubre el total. También se manda para las facturas pagadas de más, con "status": "OVERPAID". Entregá el pedido con este evento. |
invoice.overpaid | Se manda junto con invoice.paid cuando el comprador mandó más que el total. El excedente se convierte en un reembolso. |
invoice.underpaid | El monto confirmado no llega al total. El comprador todavía puede mandar el resto antes de que termine la ventana. |
invoice.expired | Terminó la ventana de pago: status es EXPIRED (no llegó nada) o EXPIRED_UNDERPAID (un pago parcial, retenido como reembolso). |
payout.completed | Una transacción de payout alcanzó el umbral de confirmaciones. |
payout.failed | Una transacción de payout se revirtió o se descartó. El monto vuelve al saldo disponible. |
refund.claimable | Hay dinero retenido para un comprador y se puede reclamar. Trae claim_url. |
refund.completed | Un reembolso reclamado se mandó y se confirmó. |
Anular una factura no manda ningún evento.
Formato de entrega
Cada entrega es un POST con Content-Type: application/json y estos headers:
| Header | Valor |
|---|---|
X-Webhook-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-Webhook-Timestamp | El mismo t, solo. |
X-Webhook-Event | El nombre del evento, como invoice.paid. |
El body es un sobre alrededor de los datos del evento:
{
"id": "evt_9b1f4c2a7d3e8f6a5b0c1d2e",
"type": "invoice.paid",
"occurredAt": "2026-10-27T14:03:11.482Z",
"data": {
"organization_id": "cm2k8x1qv0000l708h3d9f2ab",
"invoice_id": "inv_01JB7Q8M3X4K9TPZ2R6WVN5HDC",
"status": "PAID",
"livemode": false,
"merchant_order_id": "order-77310",
"payment_link_id": null,
"network": "POLYGON_AMOY",
"token": "USDC",
"amount_total": "102.50",
"amount_received": "102.50"
}
}| Campo | Descripción |
|---|---|
id | Identifica el evento. Es el mismo en cada reintento del mismo evento, así que usalo para procesar cada evento una sola vez. |
type | El nombre del evento. |
occurredAt | Cuándo ocurrió el cambio (no cuándo se mandó la entrega). |
data | El payload del evento, más abajo. Los montos son strings decimales en unidades del token. |
Todo objeto data usa nombres de campo en snake_case y trae organization_id (tu organización) y livemode (false en Sandbox, true en Live). Ignorá los campos que no uses; se pueden agregar campos nuevos.
Payloads
invoice.created
| Campo | Descripción |
|---|---|
invoice_id | La factura. |
status | PENDING, o AWAITING_SELECTION cuando el comprador elige la red. |
livemode | false en Sandbox (mode: "test" en la API), true en Live. |
network, token | El par, o null mientras está AWAITING_SELECTION. |
amount_base | El precio en USD. |
amount_total | Lo que el comprador tiene que mandar, o null mientras está AWAITING_SELECTION. |
merchant_order_id | Tal como se mandó en la factura, o null. |
payment_link_id | El payment link del que salió la factura, o null. |
invoice.detected, invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired
| Campo | Descripción |
|---|---|
invoice_id | La factura. |
status | El estado de la factura después del cambio. |
livemode | false en Sandbox, true en Live. |
merchant_order_id | Tal como se mandó en la factura, o null. |
payment_link_id | El payment link del que salió la factura, o null. |
network, token | El par. null para una factura que venció antes de que el comprador eligiera uno. |
amount_total | Lo que el comprador tenía que mandar. |
amount_received | Lo que la factura recibió en su token. |
Asociá el evento a tu orden con merchant_order_id, o con el id de la factura que guardaste al crearla (invoice_id). Los eventos de factura no traen metadata: si lo necesitás, leé la factura con GET /api/v1/crypto/invoices/{id}.
payout.completed, payout.failed
{
"organization_id": "cm2k8x1qv0000l708h3d9f2ab",
"payout_id": "po_01JB7SA2K9D4F6H8J0L2N4P6RT",
"status": "COMPLETED",
"livemode": false,
"account": "halden_type",
"network": "POLYGON_AMOY",
"token": "USDC",
"amount": "50.00",
"fee": "0.10",
"net_amount": "49.90",
"destination_address": "0x7a3B9c1D5e2F4a6B8c0D1e3F5a7B9c2D4e6F8a0B",
"tx_hash": "0x5c8e2f1a9b7d3c6e4f0a2b8d1c9e7f3a5b6c4d2e0f1a3b5c7d9e2f4a6b8c0d1e"
}account es la reference de la cuenta a la que se le hizo el payout. payout.failed agrega failure_reason.
refund.claimable
| Campo | Descripción |
|---|---|
refund_id, invoice_id | El reembolso y la factura a la que pertenece. |
status | CLAIMABLE. |
reason | OVERPAYMENT, EXPIRED_UNDERPAID, LATE_PAYMENT o WRONG_TOKEN. |
livemode, network, token | Dónde se va a pagar el reembolso, en este token. |
amount | El monto retenido, antes de la comisión de red. |
claim_url | El link de reclamo privado del comprador. Reenviáselo al comprador; no lo publiques. |
buyer_email | El buyer_email de la factura. Si está, Aliquot ya mandó el link por email. |
merchant_order_id | El merchant_order_id de la factura, o null. |
refund.completed
refund_id, invoice_id, status (COMPLETED), reason, livemode, network, token, gross_amount, network_fee, net_amount (lo que recibió el comprador), destination_address y tx_hash.
Verificar la firma
La firma es un HMAC-SHA256, con el secreto completo del endpoint como clave (incluido el prefijo whsec_), sobre el string <t>.<raw body>, donde t es el timestamp del header y el body crudo son los bytes exactos que recibiste. Calculala sobre el body crudo antes de parsear el JSON: volver a serializar un objeto parseado puede cambiar los bytes.
import { createHmac, timingSafeEqual } from 'node:crypto';
/**
* rawBody: el body del request tal como llegó (string).
* header: el header X-Webhook-Signature.
* secret: el secreto del endpoint, "whsec_…".
*/
export function verifySignature(rawBody, header, secret, toleranceSeconds = 300) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '');
if (!match) return false;
const [, t, v1] = match;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
}Con Express, leé el body con express.raw({ type: 'application/json' }) y pasá req.body.toString('utf8'). El Inicio rápido tiene un receptor completo.
Cada intento se firma con el momento en que se manda, así que un reintento trae un t nuevo. Rechazar timestamps de más de unos minutos te protege de requests repetidos sin rechazar los reintentos.
Respuesta y reintentos
- Respondé con cualquier status
2xxen menos de 10 segundos. Cualquier otra cosa, un timeout o un error de conexión cuenta como falla. - Las redirecciones no se siguen: un
3xxes una falla. - Una entrega fallida se reintenta con esperas cada vez más largas, hasta 7 intentos en total: cada reintento llega 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas y después 24 horas después del intento anterior. El último intento es unas 32 horas y media después del primero; si también falla, la entrega queda fallida. Igual la podés reenviar.
- Cada evento es una entrega separada y los reintentos se pueden intercalar, así que los eventos pueden llegar fuera de orden. Decidí a partir de
data.status, o leyendo el objeto desde la API, y no del orden de llegada. - Procesá cada
iduna sola vez: el mismo evento puede llegar más de una vez.
Los reintentos que tocan mientras el endpoint está deshabilitado cuentan como intentos fallidos. No dependas solo de los webhooks: si tu endpoint está caído más tiempo que el calendario de reintentos, o perdés un evento de tu lado, conciliá desde la API. GET /api/v1/crypto/invoices?status=PAID lista las facturas pagadas, y GET /api/v1/crypto/invoices/{id} devuelve el estado actual de una.
Reenviar una entrega
Cada intento queda registrado. Desarrolladores → Webhooks en el dashboard los lista con su status HTTP y una acción Reenviar. Reenviar manda el mismo evento al mismo endpoint otra vez: mismo id, type, occurredAt y data, firmado con el secreto actual del endpoint y un timestamp nuevo. Es un solo intento, sin el calendario de reintentos, y aparece en la lista como una entrega nueva. Como el id es el mismo, un receptor que ya procesó el evento lo saltea.
El equivalente en la API es POST …/webhooks/deliveries/{id}/resend (ver más abajo), que responde 202 cuando el intento queda en cola. Responde 409 CONFLICT si el endpoint está deshabilitado, o para un intento registrado antes de que existieran los reenvíos.
Administrar endpoints
Agregá endpoints en el dashboard, en Desarrolladores → Webhooks: la URL del endpoint y los eventos que recibe. invoice.paid, invoice.underpaid y refund.claimable vienen preseleccionados. La URL tiene que usar https:// y resolver a una dirección pública. El secreto de firma (whsec_ seguido de 64 caracteres hexadecimales) se muestra una sola vez, cuando se crea el endpoint.
Un endpoint recibe solo los eventos que tiene listados, y solo mientras está habilitado. Cada evento se entrega a todos los endpoints habilitados que están suscritos a él.
En la misma página, cada endpoint tiene Editar (URL, eventos, habilitado o deshabilitado), Rotar secreto y Eliminar. Eliminar un endpoint borra su historial de entregas y descarta sus reintentos pendientes.
Las mismas operaciones están disponibles como endpoints de la API bajo /api/v1/organizations/{organizationId}/webhooks. Aceptan una sesión del dashboard o una API key, y piden el permiso webhooks:manage (ver Autenticación):
| Endpoint | Descripción |
|---|---|
GET …/webhooks/endpoints | Lista los endpoints. |
POST …/webhooks/endpoints | Crea un endpoint. Body { "url", "events", "enabled" }. La respuesta incluye secret, una sola vez. |
PATCH …/webhooks/endpoints/{id} | Cambia url, events o enabled. |
DELETE …/webhooks/endpoints/{id} | Borra un endpoint. |
POST …/webhooks/endpoints/{id}/rotate-secret | Reemplaza el secreto de firma. La respuesta incluye el nuevo secret, una sola vez. |
GET …/webhooks/deliveries | Intentos de entrega, del más nuevo al más viejo. Filtros: endpointId, status (pending, success, failed), page, pageSize. |
POST …/webhooks/deliveries/{id}/resend | Vuelve a mandar el evento de ese intento, un solo intento. 202, sin body. |
Un objeto endpoint tiene id, organizationId, url, events, enabled, lastDeliveryAt, lastDeliveryStatus, createdAt y updatedAt. Un intento de entrega tiene id, endpointId, eventType, payloadHash (SHA-256 del body que se mandó), status, httpStatus, attemptCount, deliveredAt y createdAt. Estos objetos de administración usan camelCase, a diferencia de la API de pagos y de los payloads de los webhooks.
Rotar el secreto
Rotar reemplaza el secreto de inmediato: desde ese momento, toda entrega, incluidos los reintentos de eventos anteriores, se firma con el secreto nuevo, y el viejo ya no verifica. Para rotar sin rechazar entregas:
- Desplegá tu receptor de forma que acepte una firma que verifique con el secreto viejo o con el nuevo.
- Rotá: Rotar secreto en el endpoint, en el dashboard, o
POST …/webhooks/endpoints/{id}/rotate-secret. Guardá elsecretnuevo, que se muestra una sola vez, como el nuevo. - Cuando las entregas verifiquen con el secreto nuevo, sacá el viejo de tu receptor.