Facturiadocs
Empieza aquí

Quickstart

De una cuenta nueva a una boleta emitida en el sandbox, con el ciclo queued → accepted completo y sin tocar el SII.

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

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.

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" }
    }
  }'
{
  "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.

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

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.

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

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

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.

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

Dos campos.

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 }]
  }'

Si prefieres un cliente tipado en vez de requests o fetch, el contrato se genera con una línea: Generar un cliente tipado.

La respuesta es 201 y llega de inmediato:

{
  "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: 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

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.

curl https://api.facturia.cl/v1/documents/doc_01K2R7Q4XW9M3B8ZC5YHTVJD6N \
  -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA"
# 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"
{
  "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 — 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

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

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.

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; lo maneja un solo endpoint que puedes dibujar como asistente: GET /v1/empresas/{empresa_id}/onboarding.

En esta página