POST firmada al endpoint de webhook que configuraste, con el cuerpo del evento.
Configuración del endpoint
Tu endpoint debe ser accesible públicamente por HTTPS y responder con cualquier código de estado 2xx. Configura su URL con tu contacto de Meru o desde el dashboard. Al crearlo recibes un secreto de firma (con prefijowhsec_) — guárdalo de forma segura; lo necesitas para verificar las firmas.
Estructura del evento
Todos los cuerpos de entrega tienen la misma forma de nivel superior:El tipo de evento. Uno de
payout.update o balance.updated.Marca de tiempo ISO 8601 de cuándo se emitió el evento.
El cuerpo del evento. Su forma depende de
type (ver más abajo).Headers de entrega
Cada solicitud incluye estos headers:| Header | Descripción |
|---|---|
Content-Type | application/json |
webhook-id | ID único del mensaje/entrega. Úsalo para idempotencia. |
webhook-timestamp | Marca de tiempo Unix (segundos) de la entrega, usada para verificar la firma. |
webhook-signature | Lista separada por espacios de firmas v1,<base64> (más de una durante la rotación del secreto). |
Verificación de firmas
Cada solicitud se firma con HMAC-SHA256 para que puedas confirmar que viene de nosotros. El contenido firmado es el string{webhook-id}.{webhook-timestamp}.{cuerpoCrudo}, con clave en tu secreto de firma (decodificado de base64 tras quitar el prefijo whsec_), y el resultado se codifica en base64 dentro del header webhook-signature.
Para verificar: recalcula la firma sobre el cuerpo crudo de la solicitud (antes de cualquier parseo de JSON) y compárala contra el header en tiempo constante. Rechaza las entregas cuyo webhook-timestamp tenga más de 5 minutos.
Evento: payout.update
Se envía cada vez que un payout cambia de estado. El type del evento es siempre payout.update, sin importar la transición — lee data.state (y data.previousState) para saber qué pasó.
Campos de data
El identificador del payout.
El identificador de tu empresa.
Tu referencia externa, si se proporcionó.
El usuario/entidad para quien se hizo el payout.
El estado previo a esta transición. Es
null en el primer evento (creación).El estado actual del payout. Ver la tabla de abajo.
El monto del payout, en unidades mínimas (por ejemplo, centavos).
La moneda de destino.
Uno de
wallet, bank o qr_code.Presente solo cuando el payout falló.
Hash de la transacción on-chain. Presente para destinos
wallet y bank.Metadatos del riel bancario. Presente solo para
destinationType = bank.Marca de tiempo de creación (ISO 8601).
Marca de tiempo de última actualización (ISO 8601).
Estados del payout (data.state)
| Estado | Significado |
|---|---|
created | Payout creado |
in_review | En revisión (pre-partner) |
awaiting_funds | Esperando fondos |
funds_received | Fondos recibidos |
payment_submitted | Enviado / en proceso |
payment_processed | Completado exitosamente |
undeliverable | No entregable |
returned | Devuelto |
refunded | Reembolsado |
canceled | Cancelado |
error | Error / fallido |
Evento: balance.updated
Se envía cuando un depósito crypto cambia de estado. Su data difiere del de payout.update: lleva detalles del depósito on-chain y no incluye onBehalfOf, destinationType ni extraData.
Campos de data
El identificador del depósito (se llama
payoutId por consistencia de transporte).El identificador de tu empresa.
Tu referencia externa, si se proporcionó.
Siempre
null para eventos de depósito.El estado actual del depósito.
El monto del depósito, en unidades mínimas.
La moneda del depósito (por ejemplo,
usdc).La blockchain donde se liquidó el depósito.
Hash de la transacción on-chain, cuando está disponible.
Motivo de la falla, cuando aplica.
Marca de tiempo de última actualización (ISO 8601).
Idempotencia
El mismo evento puede entregarse más de una vez. Usa el headerwebhook-id como clave de idempotencia: guarda los IDs procesados y descarta los duplicados.
Reintentos
Si tu endpoint no devuelve un estado2xx (o agota el tiempo de espera), reintentamos la entrega automáticamente con backoff exponencial a lo largo de varias horas, respetando Retry-After en respuestas de error. Confirma rápido (en pocos segundos) y procesa el trabajo pesado de forma asíncrona. Los endpoints que fallan de forma persistente pueden quedar deshabilitados.
Buenas prácticas
- Verifica siempre la firma contra el cuerpo crudo antes de procesar.
- Responde rápido con un
2xxy procesa de forma asíncrona. - Sé idempotente usando
webhook-id. - No asumas orden — apóyate en
state/previousStateyupdatedAt, no en el orden de entrega. - Registra el
webhook-id, eltypey eldata.statede cada entrega.
Ejemplo de handler
Pruebas
Expón tu servidor local con un túnel (por ejemplo, ngrok) y registra la URL pública como tu endpoint de webhook:Solución de problemas
- Firma inválida: verifica contra el cuerpo crudo (no un JSON re-serializado), usa el secreto
whsec_correcto y lee los headerswebhook-id/webhook-timestamp/webhook-signaturetal cual. - Eventos duplicados: deduplica usando
webhook-id. - Eventos perdidos: asegúrate de que tu endpoint devuelva
2xxrápido; las respuestasnon-2xxy los timeouts se reintentan, pero las fallas persistentes pueden deshabilitar la entrega. - Errores de SSL: tu endpoint necesita un certificado TLS válido.
webhook-id.