# Quickstart (/docs/empezar/quickstart)

Todo lo de esta página ocurre en **modo test**: no hay tráfico al SII, no hay certificado, no hay
CAF y no hay costo. Cuando cambies `sk_test_` por `sk_live_`, la misma llamada golpea el SII real.

## 1. Consigue tu primera llave [#1-consigue-tu-primera-llave]

Una cuenta recién creada no tiene ninguna llave todavía. El camino más corto es **una sola llamada**:
registrarte y recibir la llave de sandbox en la misma respuesta.

```bash
curl -sS https://api.facturia.cl/account/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "tu@empresa.cl",
    "password": "una-contraseña-larga",
    "bootstrap": {
      "sandbox_key": { "name": "primera llave" },
      "empresa": { "rut": "76000002-7", "legal_name": "Ejemplo SpA" }
    }
  }'
```

```json
{
  "status": 201,
  "data": {
    "token": "eyJhbGciOi…",
    "api_key": { "object": "api_key", "id": "key_01K2R7…", "secret": "sk_test_9lQpV3fA2mKcRt7wZxYb1Nde" },
    "empresa": { "object": "empresa", "id": "emp_01K2R7…", "rut": "76000002-7", "mode": "test" },
    "next": {
      "action": "emit_sandbox_document",
      "method": "POST",
      "url": "/v1/documents",
      "headers": { "Authorization": "Bearer <data.api_key.secret>", "Facturia-Empresa": "76000002-7" }
    }
  }
}
```

`data.api_key.secret` es la llave y **se devuelve exactamente una vez**. Siempre es `sk_test_`: no
hay variante live de este bloque y no la va a haber, porque mintear una llave live es justamente el
punto donde la verificación de correo tiene que apretar. `data.next` nombra la llamada que sigue,
con headers y todo; un valor entre `<…>` es un campo de esta misma respuesta que hay que sustituir.

Con `bootstrap.empresa` te saltas el paso 2. Si el RUT ya está tomado en otra cuenta, la cuenta y la
llave se crean igual y `data.empresa_error` dice por qué no hubo empresa — reportado, no lanzado.

```bash
export FACTURIA_KEY=sk_test_9lQpV3fA2mKcRt7wZxYb1Nde
export EMPRESA=76000002-7
```

> **Registrarte no exige confirmar el correo**
>
> La verificación cierra exactamente una puerta: mintear una llave **live**. El modo test, y todo lo
> que hay dentro, está abierto de inmediato — un agente puede llegar desde esta llamada hasta un DTE de
> sandbox emitido sin que nadie abra un buzón. Cuando llegue el momento de la llave live, un
> `403 email_verification_required` te dice qué falta.

### La forma en dos llamadas [#la-forma-en-dos-llamadas]

Sigue existiendo, y es la que necesitas si la cuenta ya existe o si quieres elegir los scopes. La
salida es la **sesión de cuenta**: el mismo JWT de una hora que devuelve `POST /account/login`,
presentado a la superficie del panel.

```bash
# Iniciar sesión (o registrarte sin `bootstrap`). No necesitas un navegador.
TOKEN=$(curl -sS https://api.facturia.cl/account/login \
  -H "Content-Type: application/json" \
  -d '{"email":"tu@empresa.cl","password":"..."}' | jq -r .data.token)

# Mintear la llave con la sesión, no con una llave que todavía no tienes.
curl -sS https://api.facturia.cl/dashboard/api_keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Facturia-Mode: test" \
  -H "Content-Type: application/json" \
  -d '{"name":"primera llave","scopes":["documents:read","documents:write","empresas:read","empresas:write","webhooks:read","webhooks:write"],"empresas":["*"]}'
```

**Estos seis scopes son el set de arranque, y son el mínimo que recorre esta página entera.** Son
también el valor por defecto de `bootstrap.sandbox_key.scopes`, así que la forma de una llamada
mintea exactamente esto. El paso 2 crea una empresa y `POST /v1/empresas` pide `empresas:write`; el
paso 6 suscribe un webhook y `POST /v1/webhook_endpoints` pide `webhooks:write`. Una llave minteada
solo con los dos de `documents:` emite documentos perfectamente y responde `403 insufficient_scope` a
la primera llamada de este quickstart. El build de este sitio cruza los scopes de esta llamada contra
el `requireScope` de cada endpoint que el quickstart invoca, así que no pueden volver a separarse.

Lo que **no** entra: `keys:write` —el bootstrap lo rechaza con `400` y `errors: ["invalid_bootstrap"]`,
porque mintear es un poder de llave root y una llave de sandbox no debe ser una autoridad de
minteo— ni los scopes nuevos de la puesta en marcha (`mandato:write`, `mandato:attest`,
`onboarding:write`, `onboarding:attest`), que se piden por separado y a propósito. Todo eso está en
[Scopes](/docs/autenticacion/scopes).

`/dashboard/*` es la superficie de sesión de cuenta. Sirve los mismos recursos que `/v1` — los
mismos serializadores, los mismos filtros, el mismo sobre de error — autenticada con un JWT de
cuenta en vez de una llave `sk_`, porque un navegador nunca debe sostener una llave `sk_`.

> **El modo viaja en un header en la sesión**
>
> Una sesión no tiene prefijo que cargue su modo, así que lo hace `Facturia-Mode: test|live`, con
> `live` por defecto. Un valor no reconocido es un `400`, nunca una caída silenciosa a `live`.

Guarda el secreto: se devuelve exactamente una vez. Y ojo con los límites del arranque en frío:
`/account/register` y `/account/login` están topados en **10 por minuto y por IP** y **5 por minuto
por correo**, con `429` y `errors: ["auth_throttled"]`. Están en
[Límites](/docs/autenticacion/limites).

## 2. Crea una empresa de sandbox [#2-crea-una-empresa-de-sandbox]

Si usaste `bootstrap.empresa` en el paso 1, esto ya está hecho — salta al paso 3. Si no, es una
llamada. En modo test la empresa nace lista para emitir: sin certificado, sin mandato y sin
certificación.

```bash
curl -sS https://api.facturia.cl/v1/empresas \
  -H "Authorization: Bearer $FACTURIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rut":"76000002-7","legal_name":"Ejemplo SpA"}'
```

Solo `rut` y `legal_name` son obligatorios. El resto lo leemos del registro de contribuyentes del
SII una vez cargado el certificado, y puedes sobrescribir cualquier campo después.

## 3. Emite una boleta electrónica [#3-emite-una-boleta-electrónica]

Dos campos.

#### curl

```bash
curl -sS https://api.facturia.cl/v1/documents \
  -H "Authorization: Bearer $FACTURIA_KEY" \
  -H "Facturia-Empresa: $EMPRESA" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "tipo_dte": 39,
    "items": [{ "description": "Servicio de mantención mensual", "quantity": 1, "unit_price": 25000 }]
  }'
```

#### JavaScript

```js
// Node 18+ · sin SDK: la API es HTTP y nada más
const res = await fetch('https://api.facturia.cl/v1/documents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FACTURIA_KEY}`,
    'Facturia-Empresa': process.env.EMPRESA,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    tipo_dte: 39,
    items: [{ description: 'Servicio de mantención mensual', quantity: 1, unit_price: 25000 }],
  }),
});

const documento = await res.json();
console.log(documento.folio, documento.status); // null queued
```

#### Python

```python
# Python 3.9+ · requests — sin SDK: la API es HTTP y nada más
import os, uuid, requests

res = requests.post(
    "https://api.facturia.cl/v1/documents",
    headers={
        "Authorization": "Bearer " + os.environ["FACTURIA_KEY"],
        "Facturia-Empresa": os.environ["EMPRESA"],
        "Idempotency-Key": str(uuid.uuid4()),
        "Content-Type": "application/json",
    },
    json={
        "tipo_dte": 39,
        "items": [
            {"description": "Servicio de mantención mensual", "quantity": 1, "unit_price": 25000}
        ],
    },
)

documento = res.json()
print(documento["folio"], documento["status"])  # None queued
```

Si prefieres un cliente tipado en vez de `requests` o `fetch`, el contrato se genera con una línea:
[Generar un cliente tipado](/docs/referencia#generar-un-cliente-tipado).

La respuesta es `201` y llega de inmediato:

```json
{
  "id": "doc_01K2R7Q4XW9M3B8ZC5YHTVJD6N",
  "object": "document",
  "mode": "test",
  "status": "queued",
  "tipo_dte": 39,
  "folio": null,
  "issue_date": "2026-08-14",
  "totals": { "net": 21008, "iva": 3992, "exempt": 0, "total": 25000 },
  "pdf_url": "https://api.facturia.cl/v1/documents/doc_01K2R7.../pdf",
  "xml_url": "https://api.facturia.cl/v1/documents/doc_01K2R7.../xml",
  "sii": { "environment": "sandbox", "track_id": null, "estado": null },
  "created_at": "2026-08-14T13:04:11Z"
}
```

Fíjate en tres cosas, y la primera es la que sorprende: &#x2A;*el `folio` viene `null`**. El `POST` no
asigna folios. Escribe la fila, encola el trabajo y responde; un par de segundos después el worker
saca el folio del contador — en modo test parten en 1 por cada `(empresa, tipo_dte)` — y recién ahí
el documento tiene número. Lo segundo, los totales sí vienen calculados de inmediato, porque son
aritmética nuestra y no dependen del SII: una boleta trae los precios brutos, así que los 25.000 se
descomponen en neto e IVA. Y lo tercero, el `status` es `queued`, **no** `accepted`.

> **Trampa — No guardes el folio desde la respuesta del POST**
>
> Siempre va a ser `null` ahí. El folio aparece en el evento `document.folio_allocated`, en la
> siguiente lectura de `GET /v1/documents/{document_id}` y en el objeto que viaja dentro de los
> webhooks del ciclo de vida — nunca en la respuesta que creó el documento.

## 4. El ciclo de vida honesto: `queued` → `sent` → `accepted` [#4-el-ciclo-de-vida-honesto-queued--sent--accepted]

El `POST` es síncrono. La ida y vuelta al SII no lo es, y no fingimos lo contrario.

```
                      ┌───────────► rejected  (terminal)
                      │
queued ──► sent ──────┼───────────► reparo    (terminal-ish; válido pero observado)
   │                  │
   │                  └───────────► accepted ──► annulled (una NC lo anuló)
   │
   └──► failed ──(retry)──► queued
```

En el sandbox el documento se asienta en `accepted` en aproximadamente un segundo, disparando cada
webhook que dispararía un documento real. En producción el trayecto típico a un estado terminal es
de 30 segundos a unos pocos minutos, y el SII puede tardar horas en sus peaks o en mantención.

```bash
curl https://api.facturia.cl/v1/documents/doc_01K2R7Q4XW9M3B8ZC5YHTVJD6N \
  -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA"
```

```bash
# Los eventos del documento, del más antiguo al más nuevo
curl https://api.facturia.cl/v1/documents/doc_01K2R7.../events \
  -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA"
```

```json
{
  "object": "list",
  "data": [
    { "type": "document.folio_allocated", "at": "2026-08-14T13:04:13Z", "detail": { "folio": 1, "sandbox": true } },
    { "type": "document.signed",          "at": "2026-08-14T13:04:14Z", "detail": { "status": "signed" } },
    { "type": "document.submitted",       "at": "2026-08-14T13:04:19Z", "detail": { "status": "submitted", "track_id": "9000000001" } },
    { "type": "document.accepted",        "at": "2026-08-14T13:04:20Z", "detail": { "status": "accepted", "sii_estado": "DOK" } }
  ]
}
```

Este listado es el **diario del motor**, y su vocabulario es más fino que el de los cuatro estados
públicos: `folio_allocated` y `signed` son los dos pasos que desde afuera se leen como `queued`, y
`submitted` es el paso que se lee como `sent`. Ahí está, en un solo lugar y con hora, el resto de la
secuencia que el `201` no te podía contar: folio asignado → firmado → transmitido → aceptado. El
diario empieza en el folio y no en la creación porque la creación ya la tienes — es la respuesta que
acabas de leer, con su `created_at`.

Es un vocabulario distinto del de los **webhooks**, y conviene no confundirlos. Un endpoint suscrito
recibe `document.created`, `document.sent` y `document.accepted` — el catálogo de
[Webhooks](/docs/webhooks) — porque un webhook anuncia un cambio sobre el que se puede actuar, y
«ya le puse folio» y «ya lo firmé» no cambian nada que el suscriptor pueda observar: los dos siguen
siendo `queued`. El diario los guarda igual, para cuando estás depurando y sí importa saber en cuál
de los dos se quedó.

> **No construyas sobre polling apretado**
>
> Los webhooks son la integración prevista. Si igual necesitas hacer polling: no consultes
> `GET /v1/documents/{document_id}` más seguido que cada **10 segundos** durante los primeros 2
> minutos, y luego cada 60 segundos. Mejor aún, consulta el listado con
> `status=sent&created_at[gte]=…` una vez por minuto y reconcilia en bloque. El header
> `Facturia-Poll-After` en la respuesta del documento te dice el próximo momento útil, calculado por
> nuestro propio scheduler.
>
> Ese header **desaparece cuando el documento llega a un estado terminal** (`accepted`, `reparo`,
> `rejected`, `annulled`): ya no queda nada que observar, y decirte «vuelve en 60 segundos» sería
> mandarte a consultar algo que no va a cambiar nunca más. Trata su ausencia como la señal de que
> terminaste, no como un header perdido.
>
> En el sandbox eso significa que casi nunca lo vas a ver: el documento se asienta en \~1 segundo, así
> que para cuando lo lees ya es terminal y la respuesta viene sin `Facturia-Poll-After`. Es el
> comportamiento correcto y es exactamente el mismo código que corre en producción — no lo tomes como
> que el header no existe. Ojo también con que solo viaja en `GET /v1/documents/{document_id}`: el
> listado no lo lleva, porque una página de cien documentos no tiene un único próximo momento útil.

## 5. Baja el PDF [#5-baja-el-pdf]

```bash
curl -L https://api.facturia.cl/v1/documents/doc_01K2R7.../pdf \
  -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA" -o boleta.pdf
```

En modo test el PDF trae el layout real con una marca de agua `SIN VALOR TRIBUTARIO`.

## 6. Recibe el webhook en vez de preguntar [#6-recibe-el-webhook-en-vez-de-preguntar]

```bash
curl -sS https://api.facturia.cl/v1/webhook_endpoints \
  -H "Authorization: Bearer $FACTURIA_KEY" -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.miempresa.cl/hooks/facturia",
    "enabled_events": ["document.accepted","document.rejected","document.reparo"]
  }'
```

El `secret` (`whsec_…`) se devuelve una sola vez. Cómo verificar la firma está en
[Webhooks · Firma](/docs/webhooks/firma).

## Y ahora, en serio [#y-ahora-en-serio]

Cambiar `sk_test_` por `sk_live_` cambia lo que pasa en el cable, no tu código. Lo que sí cambia es
que la empresa necesita recorrer una puesta en marcha real: mandato, certificado o clave
centralizada, los pasos que hace una persona en el SII, la aprobación del mes y los folios. El camino
completo, numerado y con los dos carriles lado a lado, está en
[De sandbox a producción](/docs/empezar/produccion); lo maneja un solo endpoint que puedes dibujar
como asistente: `GET /v1/empresas/{empresa_id}/onboarding`.

- [De sandbox a producción](/docs/empezar/produccion): El camino real: origen y canal, mandato, pasos humanos con handoff, `go_live` y folios.

- [Conceptos](/docs/empezar/conceptos): Las cuatro ideas que hacen entendible todo lo demás.

- [El sandbox a fondo](/docs/empezar/sandbox): Garantías de determinismo, cómo forzar un rechazo y los test helpers.
