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.
Tu app nunca debe ver las credenciales bancarias del user. El widget de Rail Connect es un iframe que las captura directamente, las manda encriptadas a Rail, y te devuelve un link_id opaco.
Flow completo
Setup
// /api/widget-token (Next.js API route)
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { bank_id } = await req.json();
const r = await fetch('https://api.rail.cl/v1/widget_tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RAIL_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ bank_id }),
});
const data = await r.json();
return NextResponse.json({ token: data.id });
}
'use client';
import Script from 'next/script';
import { useState } from 'react';
declare global {
interface Window {
Rail: {
openWidget: (opts: {
token: string;
publishableKey: string;
onSuccess: (et: string) => void;
onExit?: (reason: string) => void;
}) => void;
};
}
}
export function ConnectBank() {
async function handleClick() {
const r = await fetch('/api/widget-token', {
method: 'POST',
body: JSON.stringify({ bank_id: 'banco_estado' }),
});
const { token } = await r.json();
window.Rail.openWidget({
token,
publishableKey: process.env.NEXT_PUBLIC_RAIL_KEY!,
onSuccess: async (et) => {
await fetch('/api/rail-callback', {
method: 'POST',
body: JSON.stringify({ exchange_token: et }),
});
},
onExit: (reason) => console.log('user cerró:', reason),
});
}
return (
<>
<Script src="https://widget.rail.cl/v1/embed.js" />
<button onClick={handleClick}>Conectar banco</button>
</>
);
}
3. Backend — canjeá el exchange token
// /api/rail-callback
export async function POST(req: Request) {
const { exchange_token } = await req.json();
const r = await fetch(`https://api.rail.cl/v1/exchange_tokens/${exchange_token}`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.RAIL_SECRET_KEY}` },
});
const { link_id } = await r.json();
// Guardalo en tu DB asociado al user logueado
await db.from('user_bank_links').insert({
user_id: session.user.id,
link_id,
});
return NextResponse.json({ ok: true });
}
Configuración avanzada
Pre-fill de RUT y nombre
Si ya conocés el RUT del user (porque está logueado en tu app), pre-cargalo en el widget. Esto ahorra fricción y mejora conversión.
window.Rail.openWidget({
token: 'wt_…',
publishableKey: 'rail_pk_live_…',
username: {
value: '12345678-9',
editable: false, // true = el user puede cambiarlo
},
holderId: {
value: '12345678-9',
editable: true,
},
onSuccess: (et) => {},
});
| Param | Tipo | Significa |
|---|
username.value | string | RUT con guión (XX.XXX.XXX-X o sin puntos) |
username.editable | boolean | Si false, el campo aparece bloqueado. Default true. |
holderId.value | string | RUT del titular si difiere del username (raro). Default = username.value. |
holderId.editable | boolean | Default true. |
Pre-fillear el RUT mejora la conversión entre 2-4% según nuestros tests internos. Si tu app ya lo conoce, usalo siempre con editable: false.
Appearance (dark mode)
window.Rail.openWidget({
token: 'wt_…',
publishableKey: 'rail_pk_live_…',
appearance: 'dark', // 'light' | 'dark' | 'auto'
onSuccess: (et) => {},
});
'auto' respeta el prefers-color-scheme del browser del user.
Locale
window.Rail.openWidget({
// ...
locale: 'es-CL', // 'es-CL' (default) | 'es' | 'en'
});
Hoy solo soportamos español chileno y español neutro. Inglés en roadmap.
Callbacks
| Callback | Cuándo dispara | Recibe |
|---|
onSuccess(et) | User conectó OK | et_* para canjear server-side |
onExit(reason) | User cerró sin terminar | 'user_close' | 'invalid_credentials' | 'bank_error' | 'timeout' |
onEvent(event) | Cualquier evento del widget | Ver tabla abajo |
Para analytics o tracking del funnel, suscribite a todos los eventos del widget:
window.Rail.openWidget({
// ...
onEvent: (event) => {
// Mandá a tu analytics — Mixpanel, Segment, PostHog, etc.
analytics.track(`rail_widget_${event.type}`, event.payload);
},
});
| Event type | Cuándo |
|---|
OPEN | Widget se abrió |
BANK_SELECTED | User eligió un banco |
CREDENTIALS_SUBMITTED | User submiteó RUT + clave |
MFA_PROMPTED | El banco pidió MFA |
MFA_SUBMITTED | User submiteó el código MFA |
LINK_CREATED | El link fue creado OK |
EXIT | Widget se cerró (cualquier razón) |
ERROR | Error en algún paso |
Estos eventos son solo para UX/analytics. No los uses para confirmar que el link fue creado o que el sync funcionó — para eso usá webhooks o el onSuccess con el exchange token. Los eventos del widget pueden perderse (user cierra el browser, network) y no garantizan delivery.
Cuando un sync requiere MFA async, el refresh_intent.requires_mfa.widget_token contiene un ri_*_sec_* que abrís igual que un wt_*:
window.Rail.openWidget({
token: refreshIntent.requires_mfa.widget_token,
publishableKey: process.env.NEXT_PUBLIC_RAIL_KEY!,
onSuccess: () => {
// MFA resuelto, el sync sigue server-side
},
});
No hace falta onSuccess con et_* — el link ya existe, solo estamos resolviendo el MFA.