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

EventoSe manda cuando
invoice.createdSe crea una factura, por la API o desde un payment link.
invoice.detectedSe 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.paidEl 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.overpaidSe manda junto con invoice.paid cuando el comprador mandó más que el total. El excedente se convierte en un reembolso.
invoice.underpaidEl monto confirmado no llega al total. El comprador todavía puede mandar el resto antes de que termine la ventana.
invoice.expiredTerminó la ventana de pago: status es EXPIRED (no llegó nada) o EXPIRED_UNDERPAID (un pago parcial, retenido como reembolso).
payout.completedUna transacción de payout alcanzó el umbral de confirmaciones.
payout.failedUna transacción de payout se revirtió o se descartó. El monto vuelve al saldo disponible.
refund.claimableHay dinero retenido para un comprador y se puede reclamar. Trae claim_url.
refund.completedUn 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:

HeaderValor
X-Webhook-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-Webhook-TimestampEl mismo t, solo.
X-Webhook-EventEl 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"
  }
}
CampoDescripción
idIdentifica el evento. Es el mismo en cada reintento del mismo evento, así que usalo para procesar cada evento una sola vez.
typeEl nombre del evento.
occurredAtCuándo ocurrió el cambio (no cuándo se mandó la entrega).
dataEl 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

CampoDescripción
invoice_idLa factura.
statusPENDING, o AWAITING_SELECTION cuando el comprador elige la red.
livemodefalse en Sandbox (mode: "test" en la API), true en Live.
network, tokenEl par, o null mientras está AWAITING_SELECTION.
amount_baseEl precio en USD.
amount_totalLo que el comprador tiene que mandar, o null mientras está AWAITING_SELECTION.
merchant_order_idTal como se mandó en la factura, o null.
payment_link_idEl payment link del que salió la factura, o null.

invoice.detected, invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired

CampoDescripción
invoice_idLa factura.
statusEl estado de la factura después del cambio.
livemodefalse en Sandbox, true en Live.
merchant_order_idTal como se mandó en la factura, o null.
payment_link_idEl payment link del que salió la factura, o null.
network, tokenEl par. null para una factura que venció antes de que el comprador eligiera uno.
amount_totalLo que el comprador tenía que mandar.
amount_receivedLo 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

CampoDescripción
refund_id, invoice_idEl reembolso y la factura a la que pertenece.
statusCLAIMABLE.
reasonOVERPAYMENT, EXPIRED_UNDERPAID, LATE_PAYMENT o WRONG_TOKEN.
livemode, network, tokenDónde se va a pagar el reembolso, en este token.
amountEl monto retenido, antes de la comisión de red.
claim_urlEl link de reclamo privado del comprador. Reenviáselo al comprador; no lo publiques.
buyer_emailEl buyer_email de la factura. Si está, Aliquot ya mandó el link por email.
merchant_order_idEl 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 2xx en menos de 10 segundos. Cualquier otra cosa, un timeout o un error de conexión cuenta como falla.
  • Las redirecciones no se siguen: un 3xx es 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 id una 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):

EndpointDescripción
GET …/webhooks/endpointsLista los endpoints.
POST …/webhooks/endpointsCrea 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-secretReemplaza el secreto de firma. La respuesta incluye el nuevo secret, una sola vez.
GET …/webhooks/deliveriesIntentos de entrega, del más nuevo al más viejo. Filtros: endpointId, status (pending, success, failed), page, pageSize.
POST …/webhooks/deliveries/{id}/resendVuelve 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:

  1. Desplegá tu receptor de forma que acepte una firma que verifique con el secreto viejo o con el nuevo.
  2. Rotá: Rotar secreto en el endpoint, en el dashboard, o POST …/webhooks/endpoints/{id}/rotate-secret. Guardá el secret nuevo, que se muestra una sola vez, como el nuevo.
  3. Cuando las entregas verifiquen con el secreto nuevo, sacá el viejo de tu receptor.