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"const response = await fetch('https://sandbox-api.ariari.xyz/api/v1/webhooks', {
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': process.env.ARI_SANDBOX_KEY!,
},
});
if (!response.ok) {
const problem = await response.json();
throw new Error(`${problem.error_code}: ${problem.detail}`);
}
const result = await response.json();using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key",
Environment.GetEnvironmentVariable("ARI_SANDBOX_KEY"));
var response = await client.GetAsync("https://sandbox-api.ariari.xyz/api/v1/webhooks");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();val client = HttpClient(CIO) {
install(ContentNegotiation) { json() }
}
val response: HttpResponse = client.get("https://sandbox-api.ariari.xyz/api/v1/webhooks") {
header("Ocp-Apim-Subscription-Key", System.getenv("ARI_SANDBOX_KEY"))
}
val result: String = response.bodyAsText()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_type — fx, 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:
- 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.
- Use un comparador de tiempo constante (
timingSafeEqual/hmac.compare_digest) —===filtra bytes de la firma vía timing. - 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 HTTPS —
http://se rechaza en el registro con400. 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:
- Actualice su verificador para aceptar transitoriamente ambos secretos, el viejo y el nuevo.
- Llame rotate-secret. Capture el nuevo valor.
- Despliegue el nuevo secreto a su entorno.
- 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
2xxen 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 sobreevent_id) en lugar de en código de aplicación — sobrevive reinicios de proceso y entregas concurrentes. - Registre
X-Correlation-Idjunto conevent_idcuando 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
200hasta haber persistido el evento. Un200a ARi es un contrato de que el evento está durablemente aceptado; si su escritura a BD falla, retorne5xxpara que la entrega se reintente.
Vea también
- Autenticación — scope
webhooks.manage,Ocp-Apim-Subscription-Key - Idempotencia — el mismo modelo de
Idempotency-Keyaplica a los endpoints de registro - Errores — valores de
error_codeque puede ver en cargas detransfer.failedytrading.order_rejected - Órdenes FX, Transferencias — endpoints cuyos ciclos de vida disparan los eventos listados arriba