Documentation Index
Fetch the complete documentation index at: https://docs.rail.cl/llms.txt
Use this file to discover all available pages before exploring further.
Por qué webhooks
En vez de polear /v1/refresh_intents/{id}, suscribite a eventos. Rail te avisa cuando un sync termina, un link expira, o aparecen movs nuevos.
Setup
Crea un endpoint en el dashboard (dashboard.rail.cl → Para desarrolladores → Webhooks) o via API:
curl https://api.rail.cl/v1/webhook_endpoints \
-X POST \
-H "Authorization: Bearer rail_sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-app.com/webhooks/rail",
"enabled_events": [
"link.refreshed",
"link.expired",
"refresh_intent.succeeded",
"refresh_intent.requires_mfa"
]
}'
Response incluye secret — guardalo, lo necesitás para verificar firmas.
{
"object": "webhook_endpoint",
"id": "we_xxx",
"url": "https://tu-app.com/webhooks/rail",
"secret": "whsec_abc123…",
"enabled_events": ["link.refreshed", ...]
}
Verificar firma
Cada delivery viene con header Rail-Signature: t=<timestamp>,v1=<hex>. Verificá con HMAC-SHA256:
import { createHmac } from 'crypto';
function verifyRailSignature(
rawBody: string,
header: string,
secret: string,
): boolean {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=')),
);
const timestamp = parts['t'];
const expectedSig = parts['v1'];
// Rechazar replays de más de 5min
const age = Date.now() / 1000 - Number(timestamp);
if (age > 300) return false;
const payload = `${timestamp}.${rawBody}`;
const actualSig = createHmac('sha256', secret)
.update(payload)
.digest('hex');
return actualSig === expectedSig;
}
// /webhooks/rail (Next.js route handler)
export async function POST(req: Request) {
const sig = req.headers.get('rail-signature') ?? '';
const body = await req.text();
if (!verifyRailSignature(body, sig, process.env.RAIL_WEBHOOK_SECRET!)) {
return new Response('invalid signature', { status: 400 });
}
const event = JSON.parse(body);
// ... handle event
return new Response('ok');
}
Eventos disponibles
| Event type | Cuándo |
|---|
link.created | Después de exchange exitoso (link nace) |
link.refreshed | Sync OK — vino con movs y/o balances actualizados |
link.expired | 3 fallos consecutivos sin validar → user debe re-conectar |
link.deleted | Cliente borró el link |
account.created | Apareció una cuenta nueva en el link |
account.removed | Una cuenta desapareció del banco (cerrada) |
refresh_intent.created | Intent encolado |
refresh_intent.succeeded | Sync on-demand terminó OK |
refresh_intent.failed | Sync on-demand falló — ver error_code |
refresh_intent.requires_mfa | Banco pidió MFA — emití wt y abrí widget |
Estructura del payload
{
"object": "event",
"id": "evt_xxx",
"type": "link.refreshed",
"created_at": "2026-06-03T15:00:45Z",
"data": {
"object": "link",
"id": "link_xxx",
"status": "active",
"last_synced_at": "2026-06-03T15:00:45Z",
"accounts_count": 3
}
}
data siempre contiene el recurso afectado en su forma actual (post-update).
Retry policy
Si tu endpoint devuelve != 2xx, Rail reintenta con backoff exponencial:
| Intento | Delay |
|---|
| 1 | inmediato |
| 2 | 1min |
| 3 | 5min |
| 4 | 30min |
| 5 | 2h |
| 6 | 6h |
Después del 6to fallo, marcamos la delivery como failed. Podés ver el historial en el dashboard o vía GET /v1/webhook_deliveries.
Redeliver manual
Si tu endpoint estuvo caído y querés re-recibir un evento específico:
curl https://api.rail.cl/v1/webhook_deliveries/{id}/redeliver \
-X POST \
-H "Authorization: Bearer rail_sk_live_…"
Idempotency
Tu handler debe ser idempotente: Rail puede entregar el mismo evento más de una vez (cuando hay timeouts, retries, redelivers). Usá el event.id como dedup key en tu DB.
const event = JSON.parse(body);
const { rowCount } = await db.query(
'INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING',
[event.id],
);
if (rowCount === 0) {
// Ya procesado, skip
return new Response('ok');
}
// ... procesar event