> ## 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.

# Widget Rail Connect

> Integrar el widget para que tus users conecten sus bancos sin compartir credenciales con tu app.

## 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

```mermaid theme={null}
sequenceDiagram
    participant App as Tu frontend
    participant Backend as Tu backend
    participant Widget as widget.rail.cl
    participant Rail

    App->>Backend: "Quiero conectar BancoEstado"
    Backend->>Rail: POST /v1/widget_tokens { bank_id }
    Rail-->>Backend: { id: "wt_..." }
    Backend-->>App: wt_*
    App->>Widget: Rail.openWidget({ token, pk })
    Widget->>Rail: validate
    Rail->>Widget: ok, mostrar form
    App->>Widget: user mete RUT + clave + MFA
    Widget->>Rail: sync inicial
    Rail-->>Widget: link_id, et_*
    Widget-->>App: onSuccess(et_*)
    App->>Backend: POST /callback { et_* }
    Backend->>Rail: POST /v1/exchange_tokens/{et}
    Rail-->>Backend: { link_id }
```

## Setup

### 1. Backend — emite el widget token

```ts theme={null}
// /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 });
}
```

<Note>
  **¿Cuenta de empresa?** Envía `holder_type: "business"` en el body del
  `POST /v1/widget_tokens`. El widget muestra el form de empresa (RUT empresa +
  RUT persona + clave) y filtra el picker a los bancos que soportan empresa. El
  frontend no cambia. Ver [Banca empresa](/guides/banca-empresa).
</Note>

### 2. Frontend — abre el widget

```tsx theme={null}
'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 — canjea el exchange token

```ts theme={null}
// /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 conoces el RUT del user (porque está logueado en tu app), pre-cárgalo en el widget. Esto ahorra fricción y mejora conversión.

```ts theme={null}
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`.                                                             |

<Tip>
  Pre-fillear el RUT mejora la conversión entre 2-4% según nuestros tests internos. Si tu app ya lo conoce, úsalo siempre con `editable: false`.
</Tip>

### Appearance (dark mode)

```ts theme={null}
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

```ts theme={null}
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                                                            |

### Listen to widget events

Para analytics o tracking del funnel, suscríbete a todos los eventos del widget:

```ts theme={null}
window.Rail.openWidget({
  // ...
  onEvent: (event) => {
    // Envía 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               |

<Warning>
  **Estos eventos son solo para UX/analytics.** No los uses para confirmar que el link fue creado o que el sync funcionó — para eso usa webhooks o el `onSuccess` con el exchange token. Los eventos del widget pueden perderse (user cierra el browser, network) y no garantizan delivery.
</Warning>

## Re-abrir el widget para MFA

Cuando un sync requiere MFA async, el `refresh_intent.requires_mfa.widget_token` contiene un `ri_*_sec_*` que abres igual que un `wt_*`:

```ts theme={null}
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.
