Volver a Documentacion

Webhooks

ARi entrega eventos asíncronos a un endpoint HTTPS controlado por usted. Use webhooks para cambios de estado que no puede sondear eficientemente — actualizaciones de tasas FX, completaciones de transferencias, resultados de órdenes de trading.

Para verificaciones de estado puntuales (p. ej. "¿se ejecutó esta orden específica?"), llame getStatus directamente. Los webhooks complementan al sondeo; no lo reemplazan.


Inicio rápido

# 1. Registre una URL de entrega con los eventos que le interesan
curl -fsS -X POST "https://sandbox-api.ariari.xyz/api/v1/webhooks" \
  -H "Ocp-Apim-Subscription-Key: $SU_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://partner.example.com/webhooks/ari",
    "events": ["fx.rate_changed", "transfer.completed"]
  }'

Respuesta:

{
  "webhook_id": "wh_a1b2c3d4e5f67890abcdef1234567890",
  "url": "https://partner.example.com/webhooks/ari",
  "events": ["fx.rate_changed", "transfer.completed"],
  "signing_secret": "whsec_dGhpcyBpcyBhIHRlc3Qgc2VjcmV0",
  "status": "active",
  "created_at": "2026-04-27T10:30:00.000Z"
}

El signing_secret se muestra una sola vez — guárdelo en su gestor de secretos de inmediato. Use el endpoint GET-secret para recuperarlo después si lo necesita (controlado por el mismo scope webhooks.manage, registrado en auditoría).

Ejemplo multi-lenguaje — `listWebhooks`

Generado desde el spec OpenAPI vivo. Elija su lenguaje; su elección persiste a través de otras páginas.

curl -fsS -X GET "https://sandbox-api.ariari.xyz/api/v1/webhooks" \
  -H "Ocp-Apim-Subscription-Key: $YOUR_KEY"

Catálogo de eventos

El conjunto completo de tipos de evento entregables a partners:

Tipo de evento Se dispara cuando La carga incluye
fx.rate_changed El bid/ask cotizado para un par de divisas cambia materialmente para su nivel currency_pair, old_rate, new_rate, bid_rate, ask_rate, tick_at
transfer.completed Una transferencia alcanza un estado terminal de éxito transfer_id, reference_code, status, amount, currency, completed_at
transfer.failed Una transferencia alcanza un estado terminal de fallo transfer_id, reference_code, status, error_code, failed_at
trading.order_filled Su orden de trading alcanza FILLED order_id, reference_code, filled_qty, avg_price, filled_at
trading.order_partially_filled La orden recibió un llenado parcial order_id, reference_code, filled_qty, cumulative_filled_qty, avg_price
trading.order_canceled La orden fue cancelada (por el partner o el venue) order_id, reference_code, canceled_at, cancel_reason
trading.order_rejected La orden fue rechazada antes de ejecutar order_id, reference_code, rejected_at, error_code
trading.order_expired Una orden con time-in-force expiró sin llenarse order_id, reference_code, expired_at

El array events en el registro acepta solo valores de esta lista. Suscribirse a un evento desconocido retorna 400 con error_code: GW-STC-001.


Forma de la entrega

Cada entrega de webhook es un POST con Content-Type: application/json. El cuerpo envuelve el evento en un sobre estable:

{
  "event_id": "8531c0ef-f24e-4fb8-9e3b-2a72df5c1488",
  "event_category": "fx",
  "event_type": "fx.rate_changed",
  "event_created_at": "2026-04-27T10:32:18.412Z",
  "event_delivery_id": "5e0c2a3b6e1c4d2e8f1a9b0c3d4e5f60",
  "event_object": {
    "currency_pair": "USDCRC",
    "old_rate": "508.91",
    "new_rate": "508.97",
    "bid_rate": "508.42",
    "ask_rate": "509.52"
  },
  "event_object_changes": {
    "rate": { "from": "508.91", "to": "508.97" }
  }
}

Referencia de campos:

Campo Propósito
event_id Identificador estable del evento (idempotente entre reintentos)
event_category Primer segmento del event_typefx, transfer, trading
event_type Tipo de evento totalmente cualificado del catálogo de arriba
event_created_at Timestamp UTC cuando ARi encoló el evento
event_delivery_id Identificador único de este intento de entrega — cambia en cada reintento
event_object Snapshot del recurso al momento del evento
event_object_changes Para eventos de tipo actualización, un diff {from, to} de los campos cambiados. null cuando el evento es una creación o una transición terminal

Regla de idempotencia: deduplicar por event_id, no por event_delivery_id. Los reintentos reutilizan el mismo event_id para que su handler vea la segunda entrega como una repetición.


Encabezados

Cada entrega incluye:

Encabezado Valor
X-Webhook-Signature t=<unix-segundos>,v1=<hmac-sha256-en-hex-minúsculas> (vea verificación abajo)
X-Webhook-Event El event_type (también está en el cuerpo — el header es para enrutamiento sin parsear)
X-Webhook-Delivery-Id El mismo valor que event_delivery_id en el cuerpo (32 caracteres hex, sin guiones)
X-Correlation-Id UUID — propáguelo si llama de vuelta a ARi mientras maneja el evento
Content-Type application/json

Rechace cualquier entrega que no traiga X-Webhook-Signature — siempre lo enviamos, su ausencia indica una solicitud falsificada.


Verificación de firma HMAC

El esquema de firma es la defensa canónica contra manipulación de la carga y reproducción. Verifique siempre antes de confiar en el cuerpo.

Lo que se firma

La carga firmada se construye así:

<unix_timestamp> + "." + <bytes_crudos_del_cuerpo>

El HMAC se calcula con HMAC-SHA256 usando su signing_secret (firme con el prefijo whsec_ incluido — tal como aparece en la respuesta).

La salida se renderiza en hex minúsculas y se coloca después de v1= en el encabezado X-Webhook-Signature.

Snippet de verificación (Node.js)

import crypto from 'node:crypto';

function verifyWebhook(rawBody, headerValue, signingSecret) {
  // 1. Parsear t=<unix>,v1=<hex>
  const parts = Object.fromEntries(
    headerValue.split(',').map(p => p.split('=', 2))
  );
  const timestamp = parts.t;
  const provided = parts.v1;
  if (!timestamp || !provided) throw new Error('encabezado de firma malformado');

  // 2. Rechazar entregas más viejas que 5 minutos (protección anti-replay)
  const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (ageSeconds > 300 || ageSeconds < -60) {
    throw new Error(`entrega obsoleta o futura: edad=${ageSeconds}s`);
  }

  // 3. Recalcular HMAC
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', signingSecret)
    .update(signedPayload)
    .digest('hex');

  // 4. Comparar en tiempo constante
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(provided, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error('firma no coincide');
  }
}

Snippet de verificación (Python)

import hmac, hashlib, time

def verify_webhook(raw_body: bytes, header_value: str, signing_secret: str) -> None:
    parts = dict(p.split("=", 1) for p in header_value.split(","))
    timestamp = parts["t"]
    provided = parts["v1"]

    age = int(time.time()) - int(timestamp)
    if age > 300 or age < -60:
        raise ValueError(f"entrega obsoleta o futura: edad={age}s")

    signed = f"{timestamp}.".encode("utf-8") + raw_body
    expected = hmac.new(
        signing_secret.encode("utf-8"), signed, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, provided):
        raise ValueError("firma no coincide")

Tres reglas comunes a ambas implementaciones:

  1. Verifique contra los bytes crudos del cuerpo, no contra un objeto JSON re-serializado — cambios de espacios en blanco u orden de claves rompen la firma.
  2. Use un comparador de tiempo constante (timingSafeEqual / hmac.compare_digest) — === filtra bytes de la firma vía timing.
  3. Imponga una ventana de frescura — restrinja t=<timestamp> a ±5 minutos de su reloj de servidor. Sin esto, una entrega legítima filtrada puede ser repetida indefinidamente.

Política de reintentos

Las entregas que no devuelven 2xx dentro de un timeout de 35 segundos se reintentan con backoff exponencial:

Intento Demora antes del reintento
1 (inicial)
2 1 minuto
3 5 minutos
4 15 minutos
5 1 hora

Tras el último intento, la entrega se mueve al almacén de cartas muertas (dead-letter) del webhook (visible para el soporte de ARi; no expuesto actualmente en la API de partners). El event_id permanece estable a lo largo de todos los intentos para que la deduplicación funcione limpiamente de su lado.

Una respuesta se considera exitosa solo en 2xx. Cualquier otro estado — incluidos los redireccionamientos 3xx — cuenta como fallo. No seguimos redirecciones.


Ciclo de vida del webhook

Los webhooks rotan a través de tres estados:

Estado Significado Transiciones de salida
active Entregando normalmente inactive (usted elimina vía API) → suspended (automático, tras fallos consecutivos)
suspended Auto-deshabilitado tras fallos consecutivos de entrega a través de múltiples eventos active vía POST /webhooks/{id}/reactivate
inactive Eliminación blanda; sin más entregas; registro retenido para auditoría terminal

El umbral de suspensión se configura por entorno (producción es más permisivo que sandbox). Cuando un webhook está suspendido no verá más entregas y los nuevos eventos para esa suscripción se descartan — los eventos no se encolan indefinidamente esperando reactivación.

Después de arreglar su endpoint, llame:

curl -fsS -X POST "https://sandbox-api.ariari.xyz/api/v1/webhooks/wh_a1b2.../reactivate" \
  -H "Ocp-Apim-Subscription-Key: $SU_CLAVE"

La reactivación reinicia el contador de fallos consecutivos. Fallos futuros suspenden de nuevo con el mismo umbral.


Restricciones de URL

La url registrada debe ser:

  • Solo HTTPShttp:// se rechaza en el registro con 400. No entregamos a endpoints en texto claro.
  • Públicamente alcanzable — los rangos privados RFC 1918, link-local, loopback, e IPs de servicios de metadata se bloquean en el registro y se re-validan en cada entrega (protección anti DNS rebinding).
  • Hostname estable — el DNS se re-resuelve en cada entrega; la rotación rápida de hostname está soportada, pero cada nueva IP se re-verifica contra la lista de bloqueo SSRF.

Si su endpoint está detrás de un proxy reverso, asegúrese de que el proxy preserve los bytes crudos del cuerpo de solicitud byte-por-byte; cualquier reescritura del cuerpo (gzip → identity, re-serialización JSON) rompe la verificación de firma.


Rotación de secreto

Rote cuando:

  • Un miembro del equipo con acceso al secreto deja la organización
  • Sospecha que el endpoint o el secreto se filtraron
  • En una cadencia regular según su política de seguridad (90 días es un piso común)
curl -fsS -X POST "https://sandbox-api.ariari.xyz/api/v1/webhooks/wh_a1b2.../rotate-secret" \
  -H "Ocp-Apim-Subscription-Key: $SU_CLAVE"

El secreto antiguo se invalida inmediatamente al rotar — la respuesta retorna el nuevo secreto, y la siguiente entrega se firma con él. Planifique la rotación con cuidado:

  1. Actualice su verificador para aceptar transitoriamente ambos secretos, el viejo y el nuevo.
  2. Llame rotate-secret. Capture el nuevo valor.
  3. Despliegue el nuevo secreto a su entorno.
  4. Una vez observe al menos una entrega firmada con el nuevo secreto, retire el viejo del verificador.

Consejos operacionales

  • Responda rápido — procese asíncronamente. Acuse recibo con 2xx en menos de 5 segundos, luego haga el trabajo pesado (escrituras a BD, llamadas downstream) en una cola en segundo plano. Un handler lento causa re-entregas que repiten lo que la primera entrega ya hizo.
  • Deduplique por event_id. Hágalo a nivel de base de datos (constraint UNIQUE sobre event_id) en lugar de en código de aplicación — sobrevive reinicios de proceso y entregas concurrentes.
  • Registre X-Correlation-Id junto con event_id cuando maneje una entrega. Si llama de vuelta a ARi (p. ej. para reconciliar el estado de una transferencia), reenvíe el mismo correlation id para que podamos correlacionar el viaje completo en nuestros logs.
  • No retorne 200 hasta haber persistido el evento. Un 200 a ARi es un contrato de que el evento está durablemente aceptado; si su escritura a BD falla, retorne 5xx para que la entrega se reintente.

Vea también

  • Autenticación — scope webhooks.manage, Ocp-Apim-Subscription-Key
  • Idempotencia — el mismo modelo de Idempotency-Key aplica a los endpoints de registro
  • Errores — valores de error_code que puede ver en cargas de transfer.failed y trading.order_rejected
  • Órdenes FX, Transferencias — endpoints cuyos ciclos de vida disparan los eventos listados arriba