Skip to main content

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é un widget

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

1. Backend — emití el widget token

// /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 });
}

2. Frontend — abrí el widget

'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) => {},
});
ParamTipoSignifica
username.valuestringRUT con guión (XX.XXX.XXX-X o sin puntos)
username.editablebooleanSi false, el campo aparece bloqueado. Default true.
holderId.valuestringRUT del titular si difiere del username (raro). Default = username.value.
holderId.editablebooleanDefault 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

CallbackCuándo disparaRecibe
onSuccess(et)User conectó OKet_* para canjear server-side
onExit(reason)User cerró sin terminar'user_close' | 'invalid_credentials' | 'bank_error' | 'timeout'
onEvent(event)Cualquier evento del widgetVer tabla abajo

Listen to widget events

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 typeCuándo
OPENWidget se abrió
BANK_SELECTEDUser eligió un banco
CREDENTIALS_SUBMITTEDUser submiteó RUT + clave
MFA_PROMPTEDEl banco pidió MFA
MFA_SUBMITTEDUser submiteó el código MFA
LINK_CREATEDEl link fue creado OK
EXITWidget se cerró (cualquier razón)
ERRORError 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.

Re-abrir el widget para MFA

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.