# Quickstart (/en/docs/empezar/quickstart)

Everything on this page happens in **test mode**: no traffic to the SII (Chile's tax authority), no
certificate, no CAF and no cost. Swap `sk_test_` for `sk_live_` and the same call hits the real SII.

## 1. Get your first key [#1-get-your-first-key]

A freshly created account holds no key yet. The shortest path is **a single call**: register and get
the sandbox key back in the same response.

```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` is the key, and it is **returned exactly once**. It is always `sk_test_`: there
is no live variant of this block and there never will be, because minting a live key is precisely the
point where email verification has to bite. `data.next` names the call that follows, headers and all;
a value inside `<…>` is a field of this same response that you substitute in.

`bootstrap.empresa` saves you step 2. If the RUT (the Chilean tax id) is already taken on another
account, the account and the key are still created and `data.empresa_error` says why there was no
empresa — reported, not thrown.

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

> **Registering does not require confirming your email**
>
> Verification closes exactly one door: minting a **live** key. Test mode, and everything inside it, is
> open immediately — an agent can get from this call to an issued sandbox DTE (*documento tributario
> electrónico*, Chile's electronic tax document) without anyone opening a mailbox. When the moment for
> the live key arrives, a `403 email_verification_required` tells you what is missing.

### The two-call form [#the-two-call-form]

It still exists, and it is the one you need if the account already exists or if you want to choose the
scopes. The way through is the **account session**: the same one-hour JWT that `POST /account/login`
returns, presented to the dashboard surface.

```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":["*"]}'
```

**These six scopes are the cold-start set, and they are the minimum that carries you through this
entire page.** They are also the default value of `bootstrap.sandbox_key.scopes`, so the one-call form
mints exactly this. Step 2 creates an empresa and `POST /v1/empresas` wants `empresas:write`; step 6
subscribes a webhook and `POST /v1/webhook_endpoints` wants `webhooks:write`. A key minted with only
the two `documents:` scopes emits documents perfectly well and answers `403 insufficient_scope` to the
first call of this quickstart. This site's build cross-checks the scopes of that call against the
`requireScope` of every endpoint the quickstart invokes, so the two can never drift apart again.

What is **not** in there: `keys:write` —the bootstrap refuses it with `400` and
`errors: ["invalid_bootstrap"]`, because minting is a root-key power and a sandbox key must not be a
minting authority— nor the newer go-live scopes (`mandato:write`, `mandato:attest`,
`onboarding:write`, `onboarding:attest`), which you ask for separately and on purpose. All of it is in
[Scopes](/en/docs/autenticacion/scopes).

`/dashboard/*` is the account-session surface. It serves the same resources as `/v1` — the same
serializers, the same filters, the same error envelope — authenticated with an account JWT instead of
an `sk_` key, because a browser must never hold an `sk_` key.

> **On a session, the mode travels in a header**
>
> A session has no prefix to carry its mode, so `Facturia-Mode: test|live` carries it, defaulting to
> `live`. An unrecognized value is a `400`, never a silent fall back to `live`.

Save the secret: it is returned exactly once. And mind the cold-start limits: `/account/register` and
`/account/login` are capped at **10 per minute per IP** and **5 per minute per email address**, with
`429` and `errors: ["auth_throttled"]`. They are in [Limits](/en/docs/autenticacion/limites).

## 2. Create a sandbox empresa [#2-create-a-sandbox-empresa]

If you used `bootstrap.empresa` in step 1 this is already done — skip to step 3. If not, it is one
call. In test mode the empresa is born ready to emit: no certificate, no mandato (the power of
attorney that lets us act before the SII on its behalf) and no certification.

```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"}'
```

Only `rut` and `legal_name` are required. We read the rest from the SII's taxpayer registry once the
certificate is uploaded, and you can overwrite any field afterwards.

## 3. Issue a boleta electrónica [#3-issue-a-boleta-electrónica]

Two fields.

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

If you would rather hold a typed client than `requests` or `fetch`, the contract generates one in a
single line: [Generate a typed client](/en/docs/referencia#generate-a-typed-client).

The response is a `201` and it arrives immediately:

```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"
}
```

Notice three things, and the first is the one that surprises: &#x2A;*the `folio` comes back `null`**. The
folio is the pre-authorized sequential number a DTE has to carry, and the `POST` does not assign one.
It writes the row, enqueues the work and answers; a couple of seconds later the worker draws the folio
from the counter — in test mode they start at 1 for each `(empresa, tipo_dte)` — and only then does
the document have a number. Second, the totals *are* computed immediately, because they are our own
arithmetic and do not depend on the SII: a boleta, Chile's consumer receipt, carries gross prices, so
the 25.000 breaks down into net and IVA (Chile's VAT). And third, the `status` is `queued`, **not**
`accepted`.

> **Trap — Do not store the folio from the POST response**
>
> It will always be `null` there. The folio shows up in the `document.folio_allocated` event, in the
> next read of `GET /v1/documents/{document_id}` and in the object carried inside the lifecycle
> webhooks — never in the response that created the document.

## 4. The honest lifecycle: `queued` → `sent` → `accepted` [#4-the-honest-lifecycle-queued--sent--accepted]

The `POST` is synchronous. The round trip to the SII is not, and we do not pretend otherwise.

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

In the sandbox the document settles to `accepted` in roughly a second, firing every webhook a real
document would fire. In production the typical trip to a terminal state is 30 seconds to a few
minutes, and the SII can take hours at its peaks or during maintenance.

```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" } }
  ]
}
```

This listing is the **engine's journal**, and its vocabulary is finer-grained than that of the four
public statuses: `folio_allocated` and `signed` are the two steps that read as `queued` from the
outside, and `submitted` is the step that reads as `sent`. There, in one place and with timestamps, is
the rest of the sequence the `201` could not tell you about: folio assigned → signed → transmitted →
accepted. The journal starts at the folio rather than at creation because you already hold the
creation — it is the response you have just read, with its `created_at`.

That is a different vocabulary from the one the **webhooks** use, and the two are worth keeping apart.
A subscribed endpoint receives `document.created`, `document.sent` and `document.accepted` — the
catalog in [Webhooks](/en/docs/webhooks) — because a webhook announces a change you can act on, and
"it has a folio now" and "I signed it" change nothing a subscriber can observe: both are still
`queued`. The journal keeps them anyway, for when you are debugging and it does matter which of the
two it stopped at.

> **Do not build on tight polling**
>
> Webhooks are the intended integration. If you need to poll anyway: do not read
> `GET /v1/documents/{document_id}` more often than every **10 seconds** for the first 2 minutes, and
> every 60 seconds after that. Better still, read the list with `status=sent&created_at[gte]=…` once a
> minute and reconcile in bulk. The `Facturia-Poll-After` header on the document response tells you the
> next useful moment, computed by our own scheduler.
>
> That header **disappears once the document reaches a terminal state** (`accepted`, `reparo`,
> `rejected`, `annulled`): there is nothing left to watch, and telling you to "come back in 60 seconds"
> would be sending you to poll something that will never change again. Treat its absence as the signal
> that you are finished, not as a lost header.
>
> In the sandbox that means you will almost never see it: the document settles in \~1 second, so by the
> time you read it it is already terminal and the response comes back without `Facturia-Poll-After`. It
> is the correct behavior and it is exactly the same code that runs in production — do not read it as
> the header not existing. Note as well that it only travels on `GET /v1/documents/{document_id}`: the
> list does not carry it, because a page of a hundred documents has no single next useful moment.

## 5. Download the PDF [#5-download-the-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
```

In test mode the PDF carries the real layout with a `SIN VALOR TRIBUTARIO` watermark.

## 6. Receive the webhook instead of asking [#6-receive-the-webhook-instead-of-asking]

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

The `secret` (`whsec_…`) is returned once only. How to verify the signature is in
[Webhooks · Signature](/en/docs/webhooks/firma).

## And now, for real [#and-now-for-real]

Swapping `sk_test_` for `sk_live_` changes what happens on the wire, not your code. What does change
is that the empresa has to walk a real go-live: mandato, certificate or centralized password, the
steps a person performs at the SII, the month approval and the folios. The full path, numbered and
with both lanes side by side, is in [From sandbox to production](/en/docs/empezar/produccion); a
single endpoint drives it, and you can render it as a wizard: `GET /v1/empresas/{empresa_id}/onboarding`.

- [From sandbox to production](/en/docs/empezar/produccion): The real path: origen and channel, mandato, human steps with a handoff, `go_live` and folios.

- [Concepts](/en/docs/empezar/conceptos): The four ideas that make everything else make sense.

- [The sandbox in depth](/en/docs/empezar/sandbox): Determinism guarantees, how to force a rejection, and the test helpers.
