# For agents (/en/docs/agentes) Facturia is Chile's multi-company e-invoicing API: issuing DTEs (facturas, boletas, credit and debit notes), receiving suppliers' documents, the tax suite (RCV, F29), and signed webhooks, with an MCP server so an agent can operate it directly. Sandbox free forever, no card, no certificate. ## The path in five steps [#the-path-in-five-steps] **1. Cold start, one call.** `POST /account/register` with `bootstrap.sandbox_key` and optionally `bootstrap.empresa` returns, in the same response, the account, an `sk_test_` key, and `data.next` — the exact call that follows, with method, URL and headers. Confirming an e-mail is never required to get this far. Full contract: `https://docs.facturia.cl/openapi-bootstrap.yaml`. **2. Emit in the sandbox.** `POST /v1/documents` with `Authorization: Bearer sk_test_…` and `Facturia-Empresa: ` creates a document without touching the SII: `folio: null`, accepted in under a second, PDF and XML on the spot. The bootstrap key carries the six scopes that carry this whole path: `documents:read`, `documents:write`, `empresas:read`, `empresas:write`, `webhooks:read`, `webhooks:write`. `keys:write` is separate (a root key, mints other keys only), and the four go-live scopes (`mandato:write`, `mandato:attest`, `onboarding:write`, `onboarding:attest`) never come by default: they are requested explicitly when they are actually needed. **3. Production is a state machine you read with a GET.** `GET /v1/empresas/{empresa_id}/onboarding` always answers with the list of remaining steps and, if the next one needs a person, a `pending_action`. There is no need to guess the whole flow: every read names the one next step. **4. The handoff pattern.** When `pending_action` shows up, it names **who** (`performed_by.role`: `representante_legal`, `usuario_administrador`, `titular_certificado` or `titular_cuenta` — a role, never a person), **where** (`action_url`, or `link.mint` with `{method, url, body}` if the link has to be minted first), **what to do** (`instructions[]`, in Chilean Spanish, verbatim — do not paraphrase it) and **how we find out** (`verification.recheck`: call it once the person is done, and we check again). Relay the whole object to your principal; never summarize it or invent a field that was not there. **5. Over MCP.** `https://mcp.facturia.cl/mcp` exposes the same capabilities as tools, with OAuth for the connection and explicit per-scope consent. Discovery lives at `/.well-known/oauth-protected-resource`. ## The two rules [#the-two-rules] * **Follow `next` (or `pending_action`/`link.mint`/`recheck`), never build the URL by hand.** Every response carries the exact call that follows — method, absolute URL, headers and sometimes a body — and that is the only thing to execute. Guessing a route or inventing a field is exactly the mistake this pattern exists to prevent. * **Relay `pending_action` verbatim.** The `title`, the `instructions[]` and the `performed_by.role` are the communication with a real person who is about to do something in front of the SII; summarizing them, over-translating them, or handing the wrong role gives that person a different instruction from the one we actually know to be correct. ## To go further [#to-go-further] * [OpenAPI 3.1 — cold start](https://docs.facturia.cl/openapi-bootstrap.yaml): the contract for step 1. * [OpenAPI 3.1 — /v1](https://docs.facturia.cl/openapi.yaml): the full contract. * [Quickstart](https://docs.facturia.cl/en/docs/empezar/quickstart): steps 1 and 2, with the full responses. * [From sandbox to production](https://docs.facturia.cl/en/docs/empezar/produccion): steps 3 and 4, with every handoff documented. * [MCP server](https://docs.facturia.cl/en/docs/mcp): what an agent connecting over MCP sees. * [Error dictionary](https://docs.facturia.cl/en/docs/errores/codigos): every code, its cause, and whether retrying is worth it. The API serves the same dictionary at `GET /v1/error_codes`. * [All of the above in one file](https://docs.facturia.cl/en/llms-core.txt): quickstart, production, authentication and MCP, without the \~200 error-code pages. # 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 ", "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: **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. # From sandbox to production (/en/docs/empezar/produccion) The quickstart ends with a boleta — Chile's consumer receipt — issued in the sandbox. This page is what sits between that and a factura with real tax standing: **a state machine you can drive end to end over the API**, with three or four moments where Chilean law demands that a person act and we hand you the URL to give them. Everything here lives behind a single read resource: ```bash curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R7Q4XW9M3B8ZC5YHTVJD6N/onboarding \ -H "Authorization: Bearer $FACTURIA_KEY" ``` The exact shape of the body is in [`Onboarding`](/en/docs/referencia/esquemas#onboarding), and the shape of the handoff to a person in [`PendingAction`](/en/docs/referencia/esquemas#pendingaction). ## The two lanes [#the-two-lanes] An empresa emits through **one** of two channels, and the channel decides which list of steps it walks. They are not two products: it is the same endpoint, the same fields and the same response. | | **mercado** | **SFG** (Sistema de Facturación Gratuito) | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | | Who emits | Our own engine, certified before the SII (Chile's tax authority) | The SII's free portal, operated by us | | Certification | Yes: set de pruebas, simulación, intercambio, muestras impresas | **None** — the SII already enabled the empresa when it enrolled it | | Ambiente | `certificacion` until the resolución (the SII's authorizing ruling), `produccion` after it | Always `produccion` | | Folios | Our own timbraje — a CAF, the SII-signed block of folios | The SII assigns them on each emission | | Steps | 11 (10 without boletas) | 7 | | Copia cedible (the transferable copy used for factoring) | Yes (types 33 and 34) | No | `emission_channel` is **how the empresa emits today**. `certification_track` is **what it is certifying for**, and it can be something else, or `null`: an empresa can emit on SFG while it certifies for mercado, and an empresa happy with the free portal forever holds the track at `null`. Both fields come on the [`Empresa`](/en/docs/referencia/esquemas#empresa) object and are read-only except the channel, which you declare yourself. > **Render the steps you are handed** > > `onboarding.steps[]` carries one list or the other according to the active lane. Do not assume the > length or the keys: walk whatever arrives, read each step's `actor` and show its `help`. A step with a > key your code does not know is rendered all the same; one that disappeared was not required. ### The steps, with their `actor` [#the-steps-with-their-actor] `actor` says whose step it is: `customer` is yours or your client's, `facturia` we run, `sii` means waiting. Three steps of the mercado lane are ours and **still do not move without a person** — `sii_postulacion`, `cumplimiento` and `habilitacion`: we file them with the custodied certificate, after an approval — and what says so is `pending_action.approval`, not `actor`. `execution` is the wire's other field ([`Onboarding`](/en/docs/referencia/esquemas#onboarding)): `automatic` is a queued job and `operator` is a person of ours at a keyboard, which is the difference between «it lands overnight» and «it lands when somebody sits down to do it». It is not a fixed fact about the product: **`execution` goes back to `operator` in the API if that step's fence is closed in production** (`execution` vuelve a `operator` en el API si la cerca de ese paso se cierra en producción), and the step's `eta_business_days` and the body's `estimated_ready_at` go up with it. The table below shows the production state — every fence open, `automatic` on all eleven rows — so read it off the wire rather than from here. While a step says `operator`, its `eta_business_days` never drops below 2 business days: a sitting is scheduled, not queued. **mercado lane** — eleven steps, or ten if you did not declare boletas: | # | `key` | `actor` | `execution` | What it is | | -- | ------------------- | ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 1 | `mandato` | `customer` | `automatic` | The representante legal (the company's legal representative) authorizes us to act before the SII. Revocable | | 2 | `certificate` | `customer` | `automatic` | The representante legal's `.pfx`, into the vault | | 3 | `sii_postulacion` | `facturia` | `automatic` | We file it with the custodied certificate; the account holder authorizes **the moment** | | 4 | `set_de_pruebas` | `facturia` | `automatic` | We emit the set the SII requires for each type | | 5 | `simulacion` | `facturia` | `automatic` | The simulation that precedes authorization | | 6 | `intercambio` | `facturia` | `automatic` | We prove the empresa receives and answers third-party documents | | 7 | `muestras_impresas` | `facturia` | `automatic` | We send the printed samples of each type | | 8 | `cumplimiento` | `facturia` | `automatic` | We file it with the custodied certificate; the representante legal approves the act and, if it was not set before, the month | | 9 | `resolucion` | `sii` | `automatic` | The SII issues the resolución. Here you only wait | | 10 | `habilitacion` | `facturia` | `automatic` | Only if you declared boletas: we enable them end to end with the custodied certificate; the representante legal decides when to start and approves the final declaración | | 11 | `folios` | `facturia` | `automatic` | We request the first timbraje (CAF) of each type | **SFG lane** — seven steps, no certification, plus two if you declare boletas: | # | `key` | `actor` | `execution` | What it is | | - | -------------------- | ---------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | `mandato` | `customer` | `automatic` | The same mandato, the same record, the same revocation | | 2 | `credential` | `customer` | `automatic` | A custodied certificate **or** delegation: our RUT (the Chilean tax id) as a usuario autorizado | | 3 | `clave_centralizado` | `customer` | `automatic` | The Sistema de Facturación Centralizado password, through `POST /v1/empresas/{empresa_id}/sfg_credential` | | 4 | `usuario_autorizado` | `customer` | `automatic` | We check that our certificate can act for the empresa in Portal MIPYME | | 5 | `emisor_verificado` | `facturia` | `automatic` | We confirm the empresa is enrolled and authorized to emit | | 6 | `puesta_en_vivo` | `facturia` | `automatic` | **Ours**: the canary and per-empresa enabling of real emission | | 7 | `emitting` | `facturia` | `automatic` | Done: we emit and receive through its free-system account | | 8 | `habilitacion` | `facturia` | `automatic` | Only if you declared boletas: the free portal has no boleta form, so our own engine issues them and we get them habilitated at the SII first. Two decisions are yours: when to start, and the final declaración | | 9 | `folios` | `facturia` | `automatic` | Only if you declared boletas: the first CAF for types 39/41. The free lane's facturas carry no CAF | > **Trap — A free-lane empresa's boletas go through our own engine** > > Portal MIPYME never had a boleta form, so a 39 or a 41 does **not** travel the free system: our own > engine builds, signs and sends it, exactly as it does for a mercado empresa. The empresa's > `emission_channel` does not decide this — the type does. What decides whether you can issue one is > the SII's boleta habilitación (step 8) and the first CAF (step 9); until both are in, > `POST /v1/documents` answers `422 empresa_not_emitting` naming the `habilitacion` step, and > `capabilities.tipos` lists neither 39 nor 41. **Your facturas wait for none of it**: they go live as > soon as step 7 is done. > **Trap — `puesta_en_vivo` is never yours** > > It is an `actor: facturia` step with `stall_actor: facturia`: a wait with an estimate, not an > `action_required`. If your assistant renders it as something the client can press, it will send a > person to stare at a page where there is nothing to do. `habilitación` is reserved for acts of the > SII; **puesta en vivo** is ours, and it happens in both lanes. ## Already emitting with another provider? [#already-emitting-with-another-provider] The SII certifies the **taxpayer**, not the software. The resolución your empresa already holds stays valid when it changes vendors, and certifying again would mean re-earning something it already has. So you state it when you create the empresa — or later, with `PATCH /v1/empresas/{empresa_id}` — and we check it. ```bash curl -X POST https://api.facturia.cl/v1/empresas \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Content-Type: application/json" \ -d '{ "rut": "77928532-4", "legal_name": "Connect SpA", "origen": "mercado", "doc_types": [33, 34, 61], "resolucion": { "numero": 80, "fecha": "2026-06-21" } }' ``` `numero` and `fecha` are the `NroResol` and `FchResol` from the carátula: they are printed on every document your empresa has already issued with its current vendor. Nobody has to go to the SII for them. > **Warning** > > **Stating it skips nothing.** The field writes two columns and moves no step. Read it back: > `resolucion.verified` is `false` until we have checked it, and until then the empresa walks the whole > list. ```json { "resolucion": { "numero": 80, "fecha": "2026-06-21", "verified": false, "verified_at": null } } ``` ### How we check it [#how-we-check-it] By asking the SII for folios. As soon as your certificate is in custody, the machine asks the portal to timbrar **one** of the document types you declared: the SII refuses a timbraje for a RUT it has not authorized for that type, so a granted range is the proof and a refusal is the answer. * **It grants them** → `resolucion.verified` turns `true`, the certification steps report `skipped` with the note that says why, and `estimated_ready_at` is today: all that is left is the rest of your folios. * **It refuses** → `verified` stays `false` and the `sii_postulacion` step is yours again, with `blocked_reason.resolucion_claim` carrying the SII's own sentence. We never leave you without an explanation. It is an act, not a reading, and there is no other way: no SII consulta separates a mercado resolución from an enrolment in the free system, which is why we check it by using it. ### Boletas [#boletas] The boleta electrónica habilitación is a separate authorization, with its own date and no number — the SII's mail says «habilitados los documentos de boleta electrónica a contar del …» and that is all of it. State it separately, alongside `doc_types` containing 39 or 41: ```json { "resolucion_boletas": { "fecha": "2026-09-01" } } ``` It is checked the same way, with a 39 or 41 timbraje. If it works, your boletas are enabled without going through our habilitación chain; if not, the ordinary habilitación steps appear. ### The casilla de intercambio stays yours [#the-casilla-de-intercambio-stays-yours] Changing the exchange address in Mi SII is an act of administration on your own account, and the mandato expressly forbids us from touching it. Until you change it, **your suppliers keep sending documents to your previous provider**: nothing is lost, but nothing arrives here either. Your address with us is published per empresa as `reception_email`, and the `casilla_intercambio` act on `GET /v1/empresas/{empresa_id}/onboarding` carries the instructions for the person who can do it. ## The path, step by step [#the-path-step-by-step] ### Account and first key [#account-and-first-key] The same as the [quickstart](/en/docs/empezar/quickstart), with one change: `Facturia-Mode: live` instead of `test`, and the full cold-start scope set. ```bash curl -sS https://api.facturia.cl/dashboard/api_keys \ -H "Authorization: Bearer $TOKEN" \ -H "Facturia-Mode: live" \ -H "Content-Type: application/json" \ -d '{"name":"produccion","scopes":["documents:read","documents:write","empresas:read","empresas:write","webhooks:read","webhooks:write"],"empresas":["*"]}' ``` Minting an `sk_live_` key from an unverified account answers `403 email_verification_required`. ### Build the whole thing against the sandbox first [#build-the-whole-thing-against-the-sandbox-first] An `sk_test_` key walks the full cycle — emission, statuses, webhooks, PDF — without touching the SII and at no cost. A sandbox empresa is born `emitting`: the machine on this page **is skipped entirely** in test mode, and that is deliberate. Finish the integration there and change the prefix afterwards. ### The real empresa: `origen` and emission channel [#the-real-empresa-origen-and-emission-channel] ```bash curl -sS https://api.facturia.cl/v1/empresas \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "rut": "76000002-7", "legal_name": "Ejemplo SpA", "origen": "ninguno", "doc_types": [33, 34, 61] }' ``` `origen` is **required in live mode** ([`Origen`](/en/docs/referencia/esquemas#origen)): `gratuito` if the empresa already emits through the SII's portal, `mercado` if it already uses a provider, `ninguno` if it does not emit electronically yet. Omitting it with an `sk_live_` key is `400 invalid_field`. `origen` **does not choose the channel** and does not decide what can be skipped. It is a statement about the present that routes the wizard's copy; the certification skip is settled against the SII, empirically. The emission channel is a different preference — stay on the free portal or move to the certified engine — and that is why it is declared separately. `ambiente` is **derived and read-only**: SFG is always producción; mercado is certificación until the resolución arrives. Do not send it. ### Mandato: an approval link, not a checkbox [#mandato-an-approval-link-not-a-checkbox] The mandato is signed by the **representante legal**, who very often is not the person holding the account. So the default path is to mint a link and hand it over: ```bash # Cuerpo vacío = acuñar el enlace de firma. curl -sS -X POST https://api.facturia.cl/v1/empresas/$EMPRESA_ID/mandato \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` The response carries the `signing_url`. **It is returned once only** — from there on we keep nothing but its hash. To find out how it is going, you read it: ```bash curl -sS https://api.facturia.cl/v1/empresas/$EMPRESA_ID/mandato \ -H "Authorization: Bearer $FACTURIA_KEY" ``` which answers `status`, `terms_current` and `acceptance_method` (available from v1.1). Scopes: minting and reading the mandato wants `mandato:write`. Posting an **acceptance with a body** —the case of the integrator who collected the signature in their own flow— wants `mandato:attest`, is recorded visibly as `acceptance_method: api`, never enters the default set of a new key, and is never granted by implication: `empresas:write` implies `mandato:write` (so a key that already exists can still mint and read the mandato) but not `mandato:attest`, which has to be asked for explicitly (ADR-0012 §4). See [Scopes](/en/docs/autenticacion/scopes). `GET /v1/mandato_terms` is public (no authentication) and today publishes the current version: ```json { "object": "mandato_terms", "version": "1.0", "status": "active", "effective_date": "2026-08-24" } ``` An acceptance against a version that is not current — yours went stale because the text changed — is refused with `422 mandato_terms_changed`, not `mandato_terms_draft`: there is no draft state blocking live-mode acceptance. `GET …/mandato` publishes `terms_current` with the current version and its `content_hash`; if it does not match what you signed, you need to re-sign with a new `signing_url` (see [«Revoke and re-sign»](#revoke-and-re-sign) below). ### Revoke and re-sign [#revoke-and-re-sign] The mandato is revocable "at any time, without stating a reason, without penalty or charge" (cláusula 7.1), through two doors: * **With your key or your session:** `DELETE /v1/empresas/{empresa_id}/mandato` — `204` even when there was no live mandato, because you asked for a state ("I no longer have your authorisation") and that state is the outcome either way. * **The representante legal, without a key:** the signing receipt e-mail carries a perpetual link to `.../onboarding/{empresa_id}/mandato_revocation?token=…`. The page asks for the representante legal's RUT **exactly as recorded on the mandato they signed** — the link never expires, so the RUT is what stops a forwarded e-mail from being a stop button — and calls `POST https://api.facturia.cl/onboarding/mandato/revoke` with `{"token": "…", "rep_legal_rut": "…", "revoke": true}`. If the link was lost, it is re-minted with `POST .../onboarding/mandato/revocation_link` (same two factors). **What revoking does:** the empresa becomes `suspended` immediately — the same tick, not up to five minutes later — and stops being able to emit; documents already emitted with tax value keep existing, revocation does not delete them. Certificate custody is a separate decision (cláusula 7.1.1): revoking the mandato does not destroy the `.pfx` — that is `DELETE /v1/empresas/{empresa_id}/certificate`. **Re-signing** restores the position the empresa held before the revocation (cláusula 7.1.3): if it had already certified, it does not certify again just because it re-signed. The same signing-link flow above is what re-signs. **A terms version change**, the other reason to re-sign: if `GET /v1/mandato_terms` publishes a version different from the one the current mandato accepted, any act that depends on the mandato answers `422 mandato_terms_changed` instead of proceeding. The way out is the same: mint a fresh `signing_url` with `POST …/mandato {}` and have the representante legal sign it again; the rest of the empresa's position does not move. ### Certificate (mercado) or centralized password (SFG) [#certificate-mercado-or-centralized-password-sfg] In the **mercado** lane, step 2 is the representante legal's digital certificate: ```bash curl -sS -X POST https://api.facturia.cl/v1/empresas/$EMPRESA_ID/certificate \ -H "Authorization: Bearer $FACTURIA_KEY" \ -F "file=@certificado.pfx" -F "password=…" ``` It is encrypted into a per-empresa vault and never comes back out; it is not an argument of the emission call. It can be purged with `DELETE /v1/empresas/{empresa_id}/certificate`, which revokes the custody and suspends emission. In the **SFG** lane the equivalent step is the portal credential: the password of the SII's **centralizado** certificate (not the clave tributaria), which Portal MIPYME asks for again at the final step of every emission. ```bash curl -sS -X POST https://api.facturia.cl/v1/empresas/$EMPRESA_ID/sfg_credential \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Content-Type: application/json" \ -d '{"clave_centralizado": "…"}' ``` ```json { "object": "sfg_credential", "empresa_id": "emp_01K2R7Q4XW9M3B8ZC5YHTVJD6N", "clave_centralizado": { "stored": true, "stored_at": "2026-08-24T12:03:11Z" }, "purpose": "sfg_emission_centralizado_signing" } ``` `GET` on the same resource returns the same shape — only whether we hold it and since when, never the password itself — and `DELETE /v1/empresas/{empresa_id}/sfg_credential` destroys it (`204`, even when there was nothing to destroy: you asked for a state, "you hold nothing of mine", and that is the outcome either way). The `clave_centralizado` step (available from v1.1) is the one that names that handoff. It is `actor: customer`, its `performed_by.role` is `usuario_administrador`, and its evidence is `own_record`: it lands in our own tables in the same act as the `POST`, so we watch it happen without asking the SII: ```json { "blocked_on": "clave_centralizado", "pending_action": { "object": "pending_action", "act": "sfg_clave_centralizado", "step": "clave_centralizado", "title": "Entregarnos la clave de tu Sistema de Facturación Gratuito", "performed_by": { "role": "usuario_administrador", "label": "El usuario administrador de la empresa ante el SII, o quien tenga la clave del Sistema de Facturación Gratuito." }, "verification": { "method": "own_record", "recheck_url": "POST /v1/empresas/{empresa_id}/onboarding/steps/clave_centralizado/recheck" }, "agent_actions": [ { "method": "POST", "url": "https://api.facturia.cl/v1/empresas/{empresa_id}/sfg_credential", "body": { "clave_centralizado": "…" } } ] } } ``` > **Trap — We never ask for the clave tributaria** > > The whole tax suite runs on a certificate-authenticated session against the SII's own data area. The > Sistema de Facturación Centralizado password is not the clave tributaria — the taxpayer's own SII > password — and does not replace it. ### The human steps: the handoff pattern [#the-human-steps-the-handoff-pattern] When a step needs a person, the response does not say "blocked". It says who, where, what to do and how we are going to find out — the same shape with which OAuth hands over authentication. ```json { "object": "onboarding", "status": "action_required", "blocked_on": "sii_postulacion", "pending_action": { "object": "pending_action", "act": "sii_postulacion", "step": "sii_postulacion", "title": "Autorizar la postulación al sistema de facturación de mercado", "action_url": "https://app.facturia.cl/onboarding/…", "sii_url": "https://www.sii.cl/…", "performed_by": { "role": "titular_cuenta", "label": "El titular de la cuenta de Facturia", "why_not_delegable": "…", "legal_basis": ["Mandato Facturia, cláusula 4.2 a) «Certificación como emisor electrónico»", "…"] }, "approval": { "act": "postulacion_autorizacion", "role": "titular_cuenta", "headline": "¿Presentamos la postulación ahora?", "consequences": ["Si hoy emites en el Sistema de Facturación Gratuito, tu empresa pierde su calidad de usuario activo…"], "button_label": "Sí, preséntenla", "reject_label": "No todavía" }, "instructions": [{ "order": 1, "text": "…" }], "verification": { "method": "sii_probe", "recheck_url": "POST /v1/empresas/{empresa_id}/onboarding/steps/sii_postulacion/recheck", "recheck": { "method": "POST", "url": "https://api.facturia.cl/v1/empresas/{empresa_id}/onboarding/steps/sii_postulacion/recheck" }, "sii_latency": "…" }, "evidence": "captured", "caveat": null } } ``` > **`recheck_url` is deprecated** > > `recheck_url` still ships, unchanged, so nothing reading it today breaks. It is a relative `"POST "` string — you have to parse the method apart and prefix the host yourself. Use `recheck` > above instead: the same endpoint, as `{method, url}`, absolute. The same applies to > `error.details.onboarding_url` below: `error.details.onboarding` is the structured version. Three fields decide what your agent does with this: * **`performed_by.role`** — who you hand it to. `representante_legal`, `usuario_administrador`, `titular_certificado` or `titular_cuenta`. A handoff addresses a **role**, never a person: we do not know which of them your principal is. * **`verification.method`** — how we find out. `sii_probe` (we re-read the SII), `own_record` (it lands in our tables) or `no_producer`, which means that **nothing in this version observes it** and that here a person has to confirm it. Do not tell anyone you are watching a `no_producer`. * **`evidence`** and **`caveat`** — how well backed the click path is. If `evidence` is not `captured`, `caveat` is not null and says what we are unsure of. **Pass it on.** `instructions[]` comes in Spanish and is passed on **verbatim**: do not paraphrase it and do not add steps to it. #### The link: `action_url` or `link.mint` [#the-link-action_url-or-linkmint] `pending_action.action_url` (and `steps[].action_url`) is the URL of a **live** link —not expired, not revoked— if one exists for that step. If none exists, it is `null`. It is never a URL that opens on "the link is missing". To mint one, the handoff itself says how (available from v1.1): ```json "link": { "mint": { "method": "POST", "url": "https://api.facturia.cl/v1/empresas/{empresa_id}/onboarding/links", "body": { "step": "sii_postulacion" } }, "ttl_seconds": 2592000, "reusable": true } ``` `POST /v1/empresas/{empresa_id}/onboarding/links` accepts **any** `actor: customer` step in the empresa's active catalog, whether from the mercado lane or from SFG. The one exception is `mandato`, which has its own endpoint (`POST …/mandato` with an empty body), which is why its `link.mint` points there with `body: {}`. Minting wants `onboarding:write`. The `url` is returned **once only**; minting again revokes the previous one. You revoke it with `DELETE /v1/empresas/{empresa_id}/onboarding/links/{step}`, which answers `204` whether or not one existed. #### The way back: `recheck` [#the-way-back-recheck] When the person is done, your agent tells us and we go and look: ```bash STEP=sii_postulacion curl -sS -X POST \ https://api.facturia.cl/v1/empresas/$EMPRESA_ID/onboarding/steps/$STEP/recheck \ -H "Authorization: Bearer $FACTURIA_KEY" ``` This **accepts a claim and produces no fact**. It carries no body, it writes no step's status and it does not move the empresa. It enqueues the producer that reads the SII and returns the state exactly as it stood at the moment of the call. If you post about a step in `action_required` and read `action_required` back, that is the endpoint working: `verified` comes in the response and it is **always `false`**. The full shape is in [`OnboardingRecheck`](/en/docs/referencia/esquemas#onboardingrecheck). It is not mandatory: `sii_probe` and `own_record` reach us on their own. `recheck` is there so you do not have to wait for the next sweep. #### When the step is OURS and all that is missing is a yes: `approve` [#when-the-step-is-ours-and-all-that-is-missing-is-a-yes-approve] Three steps are ours to run and still do not start on their own, because each has a consequence someone has to decide first: the **postulación** (an empresa on the Sistema de Facturación Gratuito loses its active-user status the instant the SII authorizes it), the **Declaración de Cumplimiento** (it closes certification and, from the month it is filed for, opens the Información Electrónica de Compras y Ventas obligation) and the **boleta habilitación** (it opens a 24-hour SII clock). Those steps carry an `approval` block in their `pending_action`: ```json "approval": { "act": "postulacion_autorizacion", "role": "titular_cuenta", "headline": "¿Presentamos la postulación ahora?", "consequences": ["…lo que hay que leer ANTES de apretar…"], "button_label": "Sí, preséntenla", "reject_label": "No todavía" } ``` The canonical way to take that decision is the hosted page (ADR-0012 §1): mint the step's link and hand it to the account holder. If your integration collected the decision in its own flow, the door is: ```bash curl -sS -X POST \ https://api.facturia.cl/v1/empresas/$EMPRESA_ID/onboarding/steps/$STEP/approve \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Content-Type: application/json" \ -d '{"act": "postulacion_autorizacion", "decision": "approve"}' ``` It wants `onboarding:attest` — never implied by `empresas:write` — the `act` must be the one the step is currently asking for (otherwise `409 onboarding_approval_not_offered` or `400 invalid_field`), and `decision: "reject"` is recorded exactly like an approval: «not yet» is an answer, and executors read the MOST RECENT one. ### The Declaración de Cumplimiento: we file it [#the-declaración-de-cumplimiento-we-file-it] The Declaración de Cumplimiento closes mercado certification. Once it is filed, the empresa is operating in the electronic invoicing system and owes the Información Electrónica de Compras y Ventas **from the month it is filed for**, whether or not it emits that month. The SII allows postponing it, so the month stays a decision rather than an automatic effect of the rest of certification. **We file it ourselves, with the representante legal's certificate we hold in custody** — the same one we sign your documents with. You do not have to log in to the SII site or fill out anything there. All that is left is for the representante legal to approve, on their own page. The role that can approve this act is `rep_legal`, not just any `titular_cuenta`: ```json "approval": { "act": "cumplimiento_declaracion", "role": "rep_legal", "headline": "¿Presentamos la Declaración de Cumplimiento?", "consequences": [ "Tu empresa queda operando en el sistema de facturación electrónica desde el mes indicado abajo.", "Desde ese mes debes la Información Electrónica de Compras y Ventas, emitas o no." ], "button_label": "Sí, preséntenla", "reject_label": "No todavía" } ``` That one click approves two things at once: the act before the SII, and — if the account holder had not already set the month through the next section — the month itself. Before filing we check, against the SII itself, that the holder of the custodied certificate is the same representante legal the empresa has authorized there: that is what Res. Ex. SII N° 81 de 2005, resolutivo CUARTO d) requires — that the representante legal be the one to declare "con su certificado digital" — and the same check we already run for the boleta habilitación. #### Setting the month ahead of time, separately: `go_live` [#setting-the-month-ahead-of-time-separately-go_live] If the account holder wants to decide the month before there is anything to approve — or is a different person from the representante legal — they can set it in advance: ```bash # Cuerpo vacío = acuñar el enlace de aprobación para el titular. curl -sS -X POST https://api.facturia.cl/v1/empresas/$EMPRESA_ID/onboarding/go_live \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` The response carries `{ "approval_url": "…" }` (available from v1.1): the page where the account holder sees exactly what they are approving — the month's consequences above, the authorize button below. Minting wants `onboarding:write`. **There has to be something to decide.** If the empresa has not reached the declaración yet we do not mint a link that would open onto an empty page: * `409 go_live_not_ready` — the mercado lane has not reached the decision. `error.details.blocked_on` names the step holding it up and `error.details.next` is the `{method, url}` that reports when it moves. Retry: it will be ready. * `409 go_live_not_applicable` — this empresa is not certifying for mercado, so it has no declaración de cumplimiento at all. What gates its emission is `puesta_en_vivo`, which is ours. Neither applies to `{"authorized": false}`: a decision that exists can always be withdrawn. The form with a body —`{"authorized": true}` posted by an integrator who collected the approval in their own flow— sits behind `onboarding:attest`, the same treatment as `mandato:attest`. Sending `{"authorized": false}` withdraws the authorization as long as nothing has been filed; once it is filed there is nothing left to withdraw and the answer is `409 cumplimiento_already_filed`. **This authorization is free** (#530). It was one of the acts that required an active subscription until 2026-09-04, and only on this door: the hosted `cumplimiento` page for the representante legal answered `200` for the same unsubscribed empresa. It is a decision, not an emission. The subscription is required where the value is: emitting, the credit note, requesting folios and sending. An authorized empresa that has not paid emits nothing. > **Facturia files it; the representante legal approves it** > > Since ADR-0013 (2026-09-06) we file the act before the SII with the custodied certificate — never > the representante legal in person. What they do is approve, on their own page, an act that already > identifies them: Res. Ex. SII N° 81 de 2005 requires that the representante legal be the one to > declare "con su certificado digital", and that is exactly the certificate we hold in custody under > the mandato. ### Puesta en vivo [#puesta-en-vivo] Once the lane has finished its part, one step of ours is left: enabling real emission for that empresa. In SFG it is the `puesta_en_vivo` step, after the canary; in mercado it is what follows the folios. It is shown as a **wait with an estimate** and never as something the client has to do. In the meantime, `Empresa.capabilities` tells the truth in a single read (available from v1.1): ```json "capabilities": { "can_emit": false, "tipos": [], "blocked_by": "puesta_en_vivo", "next": { "who": "facturia" } } ``` It is the **same function** the emission gate uses to answer `422 empresa_not_emitting`, so the block and the refusal cannot contradict each other. `blocked_by` takes `onboarding`, `subscription`, `status`, `puesta_en_vivo` or `null`, and `next.who` is `agent`, `human` or `facturia`. ### Folios [#folios] In the **mercado** lane the folios — the pre-authorized numbers each document has to carry — are a CAF we request from the SII and that lives on the empresa: ```bash curl -sS https://api.facturia.cl/v1/folios \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA" ``` `auto_request_folios` ships **on** by default — it is the only automation that is — and requests a fresh timbraje when availability drops below the threshold. You can also ask for one yourself with `POST /v1/folios/requests`, or upload your own CAF with `POST /v1/cafs`. In the **SFG** lane there is no timbraje: the SII assigns the folio on each emission. `GET /v1/folios` answers with the `sfg_managed` shape and `folio_preview` is `null` on drafts. That is not missing data; it is how that channel works. Details in [Folios and CAF](/en/docs/emision/folios). ### Webhooks [#webhooks] The last thing worth having ready before the first real emission, because it is what saves you from polling: ```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). ## When the gate closes anyway [#when-the-gate-closes-anyway] If you emit ahead of time, the refusal carries the handoff inside it: ```json { "error": { "type": "unprocessable_error", "code": "empresa_not_emitting", "message": "…", "hint": "…", "retriable": false, "details": { "onboarding_status": "sii_postulacion_pending", "onboarding_url": "/v1/empresas/{empresa_id}/onboarding", "onboarding": { "method": "GET", "url": "https://api.facturia.cl/v1/empresas/{empresa_id}/onboarding" }, "pending_action": { "…": "…" } } } } ``` `onboarding_url` is the same address as always, but relative — you have to prefix the host yourself. `onboarding` (#419) carries the identical endpoint absolute, as `{method, url}`, the same shape as `pending_action.link.mint`. Both still ship together; `onboarding_url` is marked `deprecated` in the contract and is the one to stop reading. It is the same [`PendingAction`](/en/docs/referencia/esquemas#pendingaction) object the onboarding returns, so your error handling and your wizard can share the code. The codes you will see: `422 empresa_not_emitting`, `422 mandato_required`, `422 postulacion_required`, `422 onboarding_requirement_unmet` and, if the empresa is otherwise able to emit and has no subscription, `402 subscription_required`. Eligibility is evaluated first (#475): while the checklist is unfinished the refusal is `422 empresa_not_emitting` with its handoff, never «pay». Each has its own entry in [Errors](/en/docs/errores). ## Next step [#next-step] - [Empresas (reference)](/en/docs/referencia/empresas): Every endpoint of the empresa, generated from the contract. - [Schemas](/en/docs/referencia/esquemas): `Onboarding`, `PendingAction`, `OnboardingRecheck`, `Origen` and `Empresa`, field by field. # API keys (/en/docs/autenticacion) Authentication is a single bearer token. ```bash curl https://api.facturia.cl/v1/documents \ -H "Authorization: Bearer sk_live_9lQpV3fA2mKcRt7wZxYb1Nde" ``` | Prefix | Mode | Talks to the SII (Chile's tax authority)? | Cost | | ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | | `sk_test_` | **test** — a fully simulated sandbox | Never | Free forever | | `sk_live_` | **live** — real emission | Yes (SII certification or production, depending on the empresa — the company you emit for) | The single plan: $19.990 CLP + IVA (VAT) per month **per empresa**, unlimited documents | Keys are account-scoped, shown once, stored hashed, and revoked one at a time. Create them in the dashboard or through the API. A key carries three things: * a **mode** (`test` / `live`) — immutable, encoded in the prefix; * a **set of scopes** — see [Scopes](/en/docs/autenticacion/scopes); * an **empresa allowlist** — `["*"]` (every empresa in the account, including ones created later) or an explicit list of RUT, Chile's taxpayer id numbers. > **Trap — There is no publishable key in v1** > > The API is server to server. Never put a secret key in a browser, a mobile app, or the prompt of an > agent a third party can read. If a key leaks, revoke it: revocation is immediate and irreversible. > > To hand an artifact to someone who has no key — the recipient, their accountant, a WhatsApp thread — > there are [public links](/en/docs/emision/artefactos#enlaces-publicos): signed, unguessable, > expiring and revocable, and they cannot be escalated into an API credential. ## Who am I [#who-am-i] ```bash curl https://api.facturia.cl/v1/account \ -H "Authorization: Bearer $FACTURIA_KEY" ``` ```json { "object": "account", "id": "acct_01K2QW8V6ZR3T5NM", "name": "Connect Core", "key": { "id": "key_01K2QWA3F2", "name": "production-backend", "mode": "live", "last4": "1Nde", "scopes": ["documents:read", "documents:write", "tax:read"], "empresas": ["77928532-4", "78281439-7"], "created_at": "2026-08-01T17:22:04Z", "last_used_at": "2026-08-14T13:04:10Z" }, "limits": { "requests_per_minute": 300, "emissions_per_second": 10 } } ``` This is the cheap call for a health check: it tells you which key you are using, with which scopes, over which empresas, and under which limits. ## Empresa token [#empresa-token] A key whose allowlist holds exactly one empresa is an **empresa token**: the `Facturia-Empresa` header becomes optional, and if you do send it, it has to match or you get `400 empresa_mismatch`. This is the recommended shape when you integrate a single company, or when you hand a key to a customer's system: the key cannot address anything but that RUT. ## Email verification [#email-verification] Signing up does not require proving your address, and neither does logging in, reading anything, or using the whole product in **test mode**. Verification blocks exactly one thing: creating a **live** credential. ```bash # Confirmar — el token viene del enlace del correo. No necesita sesión: # quien hace clic tiene un enlace, posiblemente en otro dispositivo. curl -sS https://api.facturia.cl/account/verify \ -H "Content-Type: application/json" -d '{"token":"..."}' # Reenviar — este sí necesita la sesión, para que no nos puedan usar para mandarle correo a nadie. curl -sS -X POST https://api.facturia.cl/account/verification/resend \ -H "Authorization: Bearer $TOKEN" ``` The link is single-use, lives 24 hours, and is bound to the address it was sent to: if the account's email changes in the meantime, the token is rejected rather than confirming an address it was never delivered to. Clicking twice is a success, not an error. Resends are rate limited and answer `429` with `Retry-After`. Minting an `sk_live_` key from an unverified account returns `403 email_verification_required`. - [Scopes and empresas](/en/docs/autenticacion/scopes): What each scope grants and how it combines with the allowlist. - [Root keys and rotation](/en/docs/autenticacion/llaves-root): Mint one key per customer without a human opening a dashboard. - [Rate limits](/en/docs/autenticacion/limites): The published buckets and the headers that tell you where you are. # Scopes and empresas (/en/docs/autenticacion/scopes) ## The scope table [#the-scope-table] | Scope | Grants | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `documents:read` | Read emitted documents, PDF, XML and events | | `documents:write` | Emit documents, retry, send by email | | `received:read` | Read the electronic tax documents (DTE) received from suppliers | | `received:write` | Accept / claim / acknowledge receipt | | `empresas:read` | Read empresas, onboarding, configuration, folios (the SII-authorised document numbers) | | `empresas:write` | Create empresas, upload certificates, change configuration | | `folios:write` | Ask the SII (Chile's tax authority) for a CAF, the signed block of folios; upload a CAF | | `tax:read` | RCV (the SII's ledger of purchases and sales), F29 (the monthly return) proposals and filings, suggestions | | `tax:write` | Characterise, file F29, confirm suggestions, enter expenses | | `webhooks:read` / `webhooks:write` | Webhook endpoints and event replay | | `usage:read` | Usage ledger | | `keys:write` | Mint, rotate and revoke API keys. Root keys only; never combined with the scopes above | A missing scope gets you `403 insufficient_scope`, with the required scope named in `error.message`. You do not have to guess which one it was. ### The starter set [#the-starter-set] An account's first key is minted with these six, which are exactly what the [quickstart](/en/docs/empezar/quickstart) walks through end to end: ``` documents:read, documents:write, empresas:read, empresas:write, webhooks:read, webhooks:write ``` Not one more. `keys:write` is left out because it is a root key's power, and the four onboarding scopes are left out because they grant acts with legal effect. ### The onboarding scopes [#the-onboarding-scopes] Four narrow scopes govern the acts that authorise Facturia before the SII (available from v1.1). **None of them belongs to the starter set, not by default and not for convenience**, and they are requested one at a time: | Scope | Grants | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mandato:write` | Mint the signing link for the mandato — the power of attorney an empresa grants Facturia — and read it (`GET …/mandato`) | | `mandato:attest` | Post an acceptance **with a body** (the integrator collected the signature in its own flow; recorded as `acceptance_method: api`) and **revoke** the mandato with `DELETE …/mandato` — revocation is the act with the largest effect in the contract, and since v1.1 `empresas:write` no longer grants it | | `onboarding:write` | Mint links for human steps, ask for a re-read, mint the `go_live` approval link, upload the certificate | | `onboarding:attest` | Post the `go_live` authorisation **with a body** | `empresas:write` implies the two `:write` scopes (`mandato:write`, `onboarding:write`) — so a key that already exists can still mint the signing link and the human-step links. **It implies neither `:attest` scope**: those are granted explicitly or not at all (ADR-0012 §4). A key holding only `empresas:write` that tries `POST …/mandato` with a body gets `403 insufficient_scope: mandato:attest`, exactly as if it held no onboarding scope at all. The difference between `:write` and `:attest` is the difference between **asking a person to approve** and **asserting that they already approved somewhere else**: the second is an attestation, it is recorded visibly as one, and that is why it lives behind its own scope that is never granted by implication or by default. The full path is in [From sandbox to production](/en/docs/empezar/produccion). > **Trap — A root key cannot read documents** > > `keys:write` does not combine with any other scope. A root key (`sk_live_root_…` / > `sk_test_root_…`) **cannot emit, cannot read documents and cannot touch tax data**: its only power > is minting and revoking other keys. > > It is the most common confusion when starting out: you mint the root key, try it against > `/v1/documents` and get a `403`. It is not broken — it is doing exactly what it should. Use it to > mint an ordinary key, and use that one. ## The empresa allowlist [#the-empresa-allowlist] On top of its scopes, every key carries a list of empresas: * `["*"]` — every empresa in the account, including ones created later. * An explicit list of RUT, Chile's taxpayer id numbers — `["77928532-4", "78281439-7"]`. A key with exactly one RUT is an [empresa token](/en/docs/empezar/conceptos#token-de-empresa), and the `Facturia-Empresa` header becomes optional. The two axes apply together: a key with `documents:write` over `["77928532-4"]` can emit for that empresa and for no other. Naming a different empresa returns `403 empresa_not_allowed` — not a `404`, because you already know its RUT and pretending it does not exist protects nobody. ## Combinations worth keeping in your head [#combinations-worth-keeping-in-your-head] | Shape | Scopes | Empresas | What for | | ----------------------------------------- | ------------------------------------------ | -------------------------------- | ------------------------------------------------------- | | Backend of your own product | `documents:*`, `received:*`, `tax:*` | `["*"]` | A single key that operates every empresa in the account | | One key per customer (ERP, vertical SaaS) | the minimum that customer needs | a single RUT | The customer's system cannot address anything else | | Read-only for BI or reconciliation | `documents:read`, `tax:read`, `usage:read` | `["*"]` | A key that cannot emit even if it leaks | | Root | `keys:write` | whatever it is going to delegate | Mint and revoke keys. Nothing else | | Exploration and demos | anything, but `sk_test_` | anything | A test key is a capability boundary, not a policy | ## Session mode: `Facturia-Mode` [#session-mode-facturia-mode] The `/dashboard/*` surface authenticates with an account JWT instead of an `sk_` key, because a browser must never hold an `sk_` key. A session has no prefix to carry its mode, so the mode travels in a header: ``` Facturia-Mode: test ``` The default is `live`. The backend enforces it exactly as it would enforce a key's mode: in live mode a sandbox empresa does not exist, and vice versa. An unrecognised value is a `400`, never a silent fall back to `live`. Two rules apply to a session that do not apply to a key: * **A session can mint keys AND read data.** A root key cannot do the second. The difference is deliberate: a root key is a delegated credential, and a session is the account owner in person. * **An account session is the only credential that mints a root key:** `POST /dashboard/api_keys` with `{"root": true}` (no `scopes`, no `empresas`) returns a `sk_live_root_…` / `sk_test_root_…` without opening any dashboard. A key — root or ordinary — that asks for `root: true` gets `403 privilege_escalation`: delegation goes one level deep, and a root key cannot mint another root key. See [Root keys and rotation](/en/docs/autenticacion/llaves-root). # Root keys and rotation (/en/docs/autenticacion/llaves-root) If you are embedding Facturia in your own product — a vertical SaaS, an ERP, an accounting firm — you will want to mint one key per customer empresa without a human opening a dashboard. That is a first-class flow in v1. ## The root key [#the-root-key] The key-management endpoints authenticate with a **root key** (`sk_live_root_…` / `sk_test_root_…`), carrying the `keys:write` scope. It is minted by an **account session** — the only credential that can — with `POST /dashboard/api_keys {"root": true}`; neither the dashboard nor a human opening a screen is a requirement, it is the same call your own backend would make at cold start: ```bash curl -sS https://api.facturia.cl/dashboard/api_keys \ -H "Authorization: Bearer $TOKEN" \ -H "Facturia-Mode: live" \ -H "Content-Type: application/json" \ -d '{"name": "root — arranque en frío", "root": true}' ``` No `scopes`, no `empresas`: a root key carries them fixed (`["keys:write"]`, no empresas), and sending `["keys:write"]` explicitly is not an error — sending anything else is, `400 invalid_field`. A key — root or ordinary — that asks for `{"root": true}` gets `403 privilege_escalation`: only an account session is above delegation. A root key cannot emit, cannot read documents and cannot touch tax data. Its only power is minting and revoking other keys. Keep exactly one, store it where you store your database credentials, and never deploy it to an edge runtime. ## Minting [#minting] ```bash curl -sS https://api.facturia.cl/v1/api_keys \ -H "Authorization: Bearer $FACTURIA_ROOT_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "cliente — Comercial Andina", "mode": "live", "scopes": ["documents:read", "documents:write", "received:read"], "empresas": ["76543212-K"] }' ``` ```json { "id": "key_01K2QWB7T4N9", "object": "api_key", "name": "cliente — Comercial Andina", "mode": "live", "secret": "sk_live_qT8vN2wLmR5xKpJ7hYdCbA3s", "last4": "bA3s", "scopes": ["documents:read", "documents:write", "received:read"], "empresas": ["76543212-K"], "created_at": "2026-08-14T13:12:40Z" } ``` `secret` is returned **exactly once**. Because this key names a single empresa, it is an empresa token: hand it to that customer's system and it cannot address anything else. ## The three guardrails [#the-three-guardrails] They are enforced on the server, not in your code: **A minted key can never reach further than the key that minted it.** Its `empresas` must be a subset of the minting key's allowlist, and its `scopes` a subset of that key's scopes. A root key with `["*"]` can mint anything in the account; one restricted to three RUT (Chile's taxpayer ids) can only produce keys inside those three. Violations return `403 privilege_escalation`, naming the offending RUT or scope. **Mode is inherited.** An `sk_test_root_` key mints test keys only. There is no path from a test credential to a live one. **A root key cannot mint root keys.** Delegation is one level deep, by design. If you need a second root key, mint it with an account session (`POST /dashboard/api_keys {"root": true}`), as above. ## List, rotate, revoke [#list-rotate-revoke] ```bash # Listar (solo metadatos — los secretos no son legibles, ni siquiera por una llave root) curl https://api.facturia.cl/v1/api_keys -H "Authorization: Bearer $FACTURIA_ROOT_KEY" # Rotar: emite un secreto de reemplazo; el anterior sigue funcionando 24 h, # así que puedes desplegar sin corte curl -X POST https://api.facturia.cl/v1/api_keys/key_01K2QWB7T4N9/roll \ -H "Authorization: Bearer $FACTURIA_ROOT_KEY" # Revocar de inmediato curl -X DELETE https://api.facturia.cl/v1/api_keys/key_01K2QWB7T4N9 \ -H "Authorization: Bearer $FACTURIA_ROOT_KEY" ``` An ordinary (non-root) key calling `GET /v1/api_keys` sees only itself — useful as a health check, useless as an inventory. All of it is audited: `api_key.created`, `api_key.rolled` and `api_key.revoked` land in the event ledger with the id of the key that minted them. > **Rotating is not revoking** > > `POST /v1/api_keys/{key_id}/roll` leaves the previous secret alive for 24 hours so you can deploy > without a window of downtime. If you are rotating **because a key leaked**, rotate and then revoke — > or revoke outright and take the outage. The 24 hours are a deployment convenience, not an incident > response. ## The cold start [#the-cold-start] Everything above needs a root key, and a new account has none. The way out is the **account session**: the same one-hour JWT that `POST /account/login` returns. ```bash # 1. Registrarte (o iniciar sesión). No se necesita navegador: es una llamada normal. TOKEN=$(curl -sS https://api.facturia.cl/account/register \ -H "Content-Type: application/json" \ -d '{"email":"tu@empresa.cl","password":"..."}' | jq -r .data.token) # 2. Mintear la primera 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":["*"]}' ``` Those six are the **starter set**, the same across the three surfaces that publish it (this page, the [quickstart](/en/docs/empezar/quickstart) and the cold start's own contract). It is the minimum that gets you from here to an emitted document: creating the empresa needs `empresas:write` and subscribing a webhook needs `webhooks:write`, so a key holding only the two `documents:` scopes dies at the next step with `403 insufficient_scope`. See [Scopes](/en/docs/autenticacion/scopes). To stop holding a human's password, the session mints a **root key** at the same door: ```bash curl -sS https://api.facturia.cl/dashboard/api_keys \ -H "Authorization: Bearer $TOKEN" \ -H "Facturia-Mode: test" \ -H "Content-Type: application/json" \ -d '{"name":"root de integracion","root":true}' # → {"id":"key_…","root":true,"scopes":["keys:write"],"empresas":["*"],"secret":"sk_test_root_…"} ``` The root key **inherits the session's reach** (`["*"]`: every empresa on the account, present and future). That reach is what makes it usable: the subset guardrail compares a minted key's `empresas` against the minter's, so a root key that reached no empresa could mint no key that reaches one. It cannot use the reach itself — its only scope is `keys:write`; the keys it mints do. `empresas` on a root mint may be absent, `[]` or `["*"]`; a narrower list is `400 invalid_field` (narrow the minted key, not the root). `/dashboard/*` is a deliberately **narrow** surface: empresas, documents, received documents, usage, API keys and certificates, and nothing else. In particular, it cannot emit. ### If the password is lost [#if-the-password-is-lost] The session is the only door to the first key, so losing the password left a person with none. There is a recovery path: ```bash # 1. Pedir el enlace. SIEMPRE responde 202, exista o no la cuenta: una respuesta # distinta para una dirección desconocida sería una lista de clientes pública. curl -sS https://api.facturia.cl/account/password/reset_request \ -H "Content-Type: application/json" -d '{"email":"tu@empresa.cl"}' # → 202 {"object":"password_reset_request","status":"accepted"} # 2. Con el token del correo (vive 60 minutos, un solo uso): curl -sS https://api.facturia.cl/account/password/reset \ -H "Content-Type: application/json" -d '{"token":"...","password":"..."}' # → 200 {"object":"account","id":"...","email":"..."} ``` Asking for a new link invalidates the earlier ones, and changing the password invalidates whatever is left. A token that does not work answers `400 password_reset_token_invalid` without saying which of the four reasons it was: distinguishing them is a probe oracle over a bearer credential, and the remedy is the same — ask for another link. `POST /account/login` and `POST /account/register` both report `emailVerified`, so a client can show the state without a second call. Minting an `sk_live_` key from an unverified account returns `403 email_verification_required`; test mode is unaffected. # Rate limits (/en/docs/autenticacion/limites) ## The headers [#the-headers] Every **authenticated** response carries: ``` RateLimit-Limit: 300 RateLimit-Remaining: 287 RateLimit-Reset: 41 ``` They report the **tightest bucket that applies**, not the general one: a request that is one emission away from exhausting its per-second quota says so, even with 280 general requests left. On a `429`, the `Retry-After` header (in seconds) tells you exactly how long to wait. Do not guess with exponential backoff when the response carries the number. Unauthenticated endpoints are not counted and do not carry these headers: `GET /v1/error_codes` serves a constant that holds nobody's data. ## The published buckets [#the-published-buckets] By default, **per credential** — a key or an account session, each mode counted separately: | Bucket | Limit | Window | Counted by | | --------------------------------------- | ----------- | -------------- | -------------------- | | General API | 300 req/min | sliding minute | credential | | `POST /v1/documents` | 10 req/s | sliding second | credential | | `POST /v1/tax/f29/filings` | 10 req/min | sliding minute | credential + empresa | | `POST /v1/tax/sync` | 10 req/min | sliding minute | credential + empresa | | `POST /v1/documents/{document_id}/send` | 20 req/min | sliding minute | credential | | Artifact downloads (PDF, XML, received) | 60 req/min | sliding minute | credential | | `https://ver.facturia.cl/d/{slug}` | 60 req/min | sliding minute | **the link** | What matters more than the numbers: * **Filing and syncing are counted apart.** They share the limit, not the counter: a single one would let a burst of syncs spend the quota the filing needs on the 20th. * **Sending by email has its own bucket** because it takes a list of addresses from the caller and attaches a PDF — the exact shape of a mail cannon. 20/min is generous for invoicing and useless for anything else. * **Public links are counted per slug, not per caller.** `/d/{slug}` is the only route without a credential, so the link is the only stable thing to count. There is no per-IP counting. * **There is no burst allowance.** An earlier version of this table carried a "Burst" column that contradicted its own text; the limiter enforces the sliding windows above and nothing else. Size your retries to the limit, not to a burst. These are **starting limits**, and they go up when you ask: tell us your volume and we adjust. You do not need to change plan to get more throughput. We publish them so you can size retries and backoff, not as a competitive promise. Test mode enforces the same limits as live, so a load test is representative. ## The cold start has caps of its own [#the-cold-start-has-caps-of-its-own] `POST /account/register` and `POST /account/login` carry no credential, so they cannot be counted per credential. They are counted on two axes at once, and both are published because a fleet of agents behind one NAT hits the first on its very first call and deserves to know the number instead of discovering it: | Bucket | Limit | Window | Counted by | | ------------------------------------------------ | ----------------- | ------ | -------------------------------- | | `POST /account/register` · `POST /account/login` | **10 per minute** | 60 s | IP address, across every account | | `POST /account/register` · `POST /account/login` | **5 per minute** | 60 s | email address, from wherever | The per-IP cap bounds CPU: each of those doors costs a full bcrypt at cost 12, and the machine has one vCPU. The per-email cap is the half a botnet cannot dodge by rotating addresses. **Both windows count every attempt, the successful ones included.** Past either one you get a `429` with `Retry-After` in seconds and this body: ```json { "status": 429, "message": "Demasiados intentos. Vuelve a intentar en un momento.", "errors": ["auth_throttled"] } ``` That is the cold start's envelope (`{status, message, data, errors}`), not `/v1`'s — they are two different surfaces and each answers with its own. **Program against `errors[0] === 'auth_throttled'`, never against the `message`.** > **Trap — The rejection does not tell you which of the two caps you hit** > > And that is on purpose: saying "too many attempts for this email" would turn the limiter into an > account-enumeration oracle — anyone could find out which addresses have an account. There is no > captcha, also on purpose: this surface is for agents. If a fleet legitimately needs more than 10 > registrations per minute out of one egress, talk to us before building a retry loop around it: the > answer is a platform fix. ## How a `429` behaves [#how-a-429-behaves] A `429` is of type `rate_limit_error`. Emission is queued on our side: a burst above 10 req/s is a client-side `429`, **never a lost document**. Retry with the `Retry-After` we handed back and the document goes out. ```js async function emitir(payload, idempotencyKey) { for (let intento = 0; intento < 5; intento += 1) { 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': idempotencyKey, // el mismo en cada reintento 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (res.status !== 429) return res; const esperar = Number(res.headers.get('Retry-After') ?? 1); await new Promise((r) => setTimeout(r, esperar * 1000)); } throw new Error('rate limit persistente'); } ``` Reusing the same `Idempotency-Key` on every retry is what guarantees that five attempts produce one document and not five. ## There is no plan quota to run out of [#there-is-no-plan-quota-to-run-out-of] The limits above are **rate** limits. There is a single plan, monthly, **per empresa**, with **unlimited documents**, so there is no document balance to burn down and no tier to cross: if something is slowing you, it is the pace, not the volume. Connecting a second empresa does not move you toward a quota either: it is one more subscription, at the same price, with the same rate limits. The dictionary publishes a `402 plan_limit_reached` and today **nothing emits it**. It stays because a published code is not withdrawn without twelve months' notice, and because a client that already knows how to handle a `402` deserves something to program against. Its own entry says so: [`plan_limit_reached`](/en/docs/errores/plan_limit_reached). The other `402`, `subscription_required`, **is enforced** — but it is not a quota: it is an empresa with no active subscription attempting one of five real acts. It is spelled out in [The error envelope](/en/docs/errores#el-402). ## The two realities upstream [#the-two-realities-upstream] Above our own limits there are two realities of the SII (Chile's tax authority) worth designing for: 1. **The SII rate-limits, and does not publish its numbers.** 2. **The SII has scheduled maintenance windows.** Neither one will lose you a document — emission is queued — but a document we already accepted will sit in `queued` longer during an SII outage. You will see it on the document itself before anywhere else: `status` does not advance and `Facturia-Poll-After` keeps handing back a next attempt instead of disappearing. The availability of our own components is at [status.facturia.cl](https://status.facturia.cl), and in JSON at [`status.facturia.cl/status.json`](https://status.facturia.cl/status.json); if something has been stuck longer than is reasonable, write to us with the `request_id` and we will tell you which of the two halves you are in. > **Build on webhooks** > > An integrator polling tightly through an SII outage rate-limits itself exactly when there is least > information to be had. An integrator listening for `document.accepted` notices no difference beyond > the latency. The [Webhooks](/en/docs/webhooks) page has the full catalogue. # Connect an agent (/en/docs/mcp) The same engine as the REST API, a different front door: instead of a person typing `curl`, a third-party AI agent calls typed tools. ``` https://mcp.facturia.cl ``` One MCP endpoint (`/mcp`) speaking **Streamable HTTP**, and one set of OAuth endpoints on the same origin. There are no per-empresa or per-account subdomains: tenancy is a claim in the token, not a hostname. ## The architectural fact [#the-architectural-fact] > **The MCP server is a REST client of /v1** > > It holds no business logic and none of the engine's credentials: no SII (Chile's tax authority) > certificate, no signing key, no access to the engine's schema. Every domain question — what a > document's totals are, whether folios (the pre-authorized numbers a document must carry) are > available, what the F29 (the monthly VAT return) says — is a request to `/v1`, never a query. > > Which means everything in these docs about statuses, errors and guarantees holds exactly the same > from an agent. ## Why an agent is not a developer [#why-an-agent-is-not-a-developer] A developer reads the documentation before writing the first line; an agent decides what to call in the same turn it is asked. This server publishes its pages as **MCP resources** — returned whole, not as a link, so a client with no fetch capability reads them too — and even so every tool has to describe itself: the documentation explains the model, and the tool description is all the model has in hand at the moment it decides. Three design commitments for this layer follow from that: **The full suite, not a thin wrapper.** Emission, reception and the tax layer are all here. **Direct execution, with an escape hatch for whoever wants one.** The MCP client's own tool-approval step is the default human in the loop. Empresas that want more can turn on a two-step draft-and-confirm. **Being AI-first means resolving ambiguity, not refusing it.** `draft_document` takes a receptor (the recipient) by name, because that is how a person talks to an agent. Line items are structured (`[{description, quantity, unit_price}]`): the model does the prose, because it understands prose better and more cheaply than any parser we could write, and `draft_document` returns the totals so the person sees them **before** anything is signed. There is no prose parser in the API and there will not be one: a parser hidden behind an endpoint turns a misread quantity into a tax document. ## Add it to your client [#add-it-to-your-client] One address, `https://mcp.facturia.cl/mcp`, and nothing to install: the transport is **Streamable HTTP**, so there is no package to download and no local process to run. There is no secret to paste into a config file either — the client discovers OAuth on its own (RFC 9728), identifies itself with its metadata document or registers itself (RFC 7591), and opens the consent screen in your browser, where you decide what the agent may do and over which empresas. #### Claude (mcp.json) In Claude Desktop: **Settings → Connectors → Add custom connector**, with the URL `https://mcp.facturia.cl/mcp`. If you would rather edit the file by hand (`claude_desktop_config.json`), it is one line: ```json { "mcpServers": { "facturia": { "url": "https://mcp.facturia.cl/mcp" } } } ``` #### Claude Code ```bash claude mcp add --transport http facturia https://mcp.facturia.cl/mcp ``` The first tool call opens the browser to authorize. After that, `claude mcp list` shows the connection and `claude mcp remove facturia` drops it from this client (to actually cut off access, revoke it from the dashboard — see [Revocation](#revocación)). #### claude.ai On the web: **Settings → Connectors → Add custom connector**, with the same URL `https://mcp.facturia.cl/mcp`. There is no file to edit; authorization opens right there. #### Cursor In `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project), the same object: ```json { "mcpServers": { "facturia": { "url": "https://mcp.facturia.cl/mcp" } } } ``` Cursor lists the server under **Settings → MCP**; authorization opens in the browser the first time the agent calls a tool. #### VS Code With GitHub Copilot in agent mode, in the project's `.vscode/mcp.json` (or in user settings), the same remote server: ```json { "servers": { "facturia": { "type": "http", "url": "https://mcp.facturia.cl/mcp" } } } ``` VS Code opens the browser to authorize the first time the agent uses a tool. #### Windsurf In `~/.codeium/windsurf/mcp_config.json`, the same object as Claude and Cursor: ```json { "mcpServers": { "facturia": { "serverUrl": "https://mcp.facturia.cl/mcp" } } } ``` #### ChatGPT In ChatGPT, MCP servers are added as a **connector**: settings → connectors → add, pointing at `https://mcp.facturia.cl/mcp`. Availability depends on the plan and the mode (custom connectors are on the paid plans and in developer mode). Any client that speaks MCP over Streamable HTTP with OAuth works just as well: there is nothing client-specific in this server. > **Never created an account?** > > The consent screen lets you sign in right there and mints the grant's key for you — you do not have > to bring one ready. And if you have no account yet, the link on that screen takes you to create one > and **brings you back to the same authorization** when you are done: no reconnecting the agent. > > No empresas yet either? Also fine: tick "Todas las empresas de esta cuenta, incluidas las que cree > más adelante" together with `empresas:write`, and the agent creates the first one with > `create_empresa`. Typing `https://mcp.facturia.cl` into a browser gives you the same information as a page: what it is, the snippet, and the link to these docs. **How the client identifies itself.** The MCP spec sets a priority order for how a client says who it is: a pre-registered client first, then a client ID metadata document (CIMD), and dynamic registration last, as the fallback. * **Client ID metadata document (CIMD).** The client can use an https URL as its `client_id` — for example `https://claude.ai/.well-known/oauth-client.json` — instead of registering before it connects. The server fetches it at connect time (GET, 3 s timeout, 32 KB max, no redirects, never to a private or loopback address) and validates the JSON: the `client_id` the document declares has to be exactly that URL, `client_name` and `redirect_uris` are required, and only public clients are accepted (`token_endpoint_auth_method` absent or `"none"`, PKCE S256 as always). The result is cached honoring `Cache-Control: max-age`, clamped between 1 and 24 hours — an error is never cached. The `redirect_uri` of every authorization has to match one the document declares exactly, the same rule as a registered client — with one exception, RFC 8252 §7.3's: when the document declares `http://localhost/…` or `http://127.0.0.1/…`, the port is not compared, because a native client such as Claude Code opens a different one every session. * **Dynamic client registration (RFC 7591).** The fallback for a client that does not yet publish a CIMD — Cursor, VS Code, Windsurf, ChatGPT and similar still register this way. Any MCP client registers itself on first connection — no prior approval, no application allowlist. Registration returns a public `client_id`; there is no `client_secret`, because PKCE is the confidentiality mechanism for a public client that cannot keep a secret. ``` POST https://mcp.facturia.cl/register (RFC 7591) GET https://mcp.facturia.cl/.well-known/oauth-authorization-server ``` That same `GET` declares whether the server accepts CIMD: ```json { "client_id_metadata_document_supported": true } ``` **What the person sees on the consent screen.** With a CIMD client the screen shows its name and the origin of its metadata document ("Claude · claude.ai") — that origin is verifiable, because that is where the document lives. A dynamically registered client still gets the warning "This application registered itself and picked its own name — we do not verify it." In both cases the screen keeps showing the host the authorization code will be sent to ("Where this is going"), with an extra warning when that host is `localhost` or `127.0.0.1`. **Authorization and consent.** Standard authorization-code + PKCE flow. The consent screen is not a rubber stamp: that is where you decide which Facturia credential the grant wraps. ``` GET https://mcp.facturia.cl/authorize ?client_id=mcpc_01K2SC9X4M &redirect_uri=https://claude.ai/api/mcp/auth_callback &response_type=code &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &scope=documents:read documents:write tax:read &state=af0ifjsldkj ``` **Token.** `POST /token` exchanges the code (with the PKCE verifier) for a short-lived access token and a longer-lived refresh token, revocable on its own. ```json { "access_token": "mcpat_9lQpV3fA2mKcRt7wZxYb1Nde", "token_type": "bearer", "expires_in": 3600, "refresh_token": "mcprt_qT8vN2wLmR5xKpJ7hYdCbA3s", "scope": "documents:read documents:write tax:read" } ``` ## What the token carries [#what-the-token-carries] The same authorization model as an API key, because it has to behave the same once issued: * an **account**, * a **mode** — the token can be sandbox-only, live or mixed, depending on what was granted; * a **set of scopes**, with the same vocabulary as the REST scopes: `documents:read`, `documents:write`, `received:read`, `received:write`, `tax:read`, `tax:write`, `empresas:read`, `usage:read` and, for going live, `onboarding:write`, `mandato:write` and `empresas:write`; * an **empresa allowlist** — explicit RUTs (Chile's taxpayer ids), or `"*"`. > **The two attestation scopes do not exist on this surface** > > `/v1` also defines `mandato:attest` and `onboarding:attest`: accepting the mandato — the power of > attorney that lets Facturia act for a company before the SII — with a request body, and authorizing > the go-live with one. Those are the moment a **person** says "yes, I accept" and "yes, this month", > and they are recorded as the evidence of that decision. > > An agent cannot ask for them here, and that is not an oversight: the MCP tools mint the link and > stop; the person decides on the page. If the agent could attest, the record of your approval would > be the agent's word for it. ## Naming the empresa in a tool call [#naming-the-empresa-in-a-tool-call] A single token's list can span several empresas (an accountant's agent handling several clients, say), so every empresa-scoped tool takes an explicit `empresa` argument — the direct analogue of the `Facturia-Empresa` header. | Situation | Behavior | | --------------------------------------------------------------------- | ------------------------------------------------ | | Omitted where it is required | The tool returns `empresa_required` | | Present but outside the list | `empresa_not_allowed` — never a bare "not found" | | A token with exactly one empresa | You may omit it; the server fills it in | | Account-level tools (`list_empresas`, `get_usage` without an empresa) | Reject the argument | ## The sandbox limit [#the-sandbox-limit] > **Trap — A sandbox grant is a capability, not a policy** > > A sandbox-scoped token **cannot** reach a live empresa's data or issue anything with legal effect. > It is not a preference that can be flipped at runtime: it is a structural limit. > > That is why it is the right default for anything exploratory or public-facing. Dynamic client > registration means the calling software is **unverified by construction**, and the token's scope is > the primary defense against a compromised or over-trusting agent. The consent screen defends the same thing from the other side: nothing comes pre-checked. You pick explicitly which empresas this agent may touch. Granting `"*"` is possible but takes an extra confirmation click — the asymmetry is deliberate, because a grant to an over-trusting third-party agent is a materially different risk from a first-party API key kept in your own backend. ## Revocation [#revocación] In the dashboard, **Connected agents** lists every MCP grant on the account: which client connected, with which scopes, over which empresas, in which mode, when, and when it last used it. Revoking one invalidates its access and refresh tokens immediately. There is a second path that works just as well: every grant wraps one of your API keys, so revoking that key under **Keys** kills the grant in the same instant. ## Rate limits [#rate-limits] MCP calls hit the same backend services as the REST ones and share the same published buckets. There is no separate MCP quota, because there is no separate emission pipeline underneath. | Bucket | Limit | Which tools | | ----------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | General | 300 req/min | `draft_document`, all the `list_*` / `get_*`, `query_rcv`, `get_periodo_summary`, and the writes that neither emit nor file (`update_*`, `set_*`, `characterize_purchases`, `send_document`, `request_folios`, …) | | Emission | 10 req/s, burst 30 | `emit_document`, `emit_tipo46`, `emit_credit_note` | | F29 filing and RCV sync | 10 req/min per empresa | `file_f29`, `sync_rcv` (one call per period; a ten-month range spends the whole bucket) | | Artifacts | 60 req/min | `get_document_file` (minting the signed URL, not the download afterwards, which is public and unmetered) | A throttled call returns the `rate_limit_error` type with a `retry_after_seconds` field an agent can act on directly. - [Tool catalog](/en/docs/mcp/herramientas): Every tool on the server, with its annotations and which ones go through confirmation. - [Confirmations](/en/docs/mcp/confirmaciones): The two-step for emitting, filing and claiming. # Tool catalog (/en/docs/mcp/herramientas) This page is generated from the server's real tool registry: if a tool exists, it is here, with the same annotations your MCP client sees. **Up to 90 tools**, depending on the grant's scopes. ## Conventions every one of them shares [#conventions-every-one-of-them-shares] * The first argument of every empresa-scoped tool is `empresa`. Account-level tools reject it. * An argument with `?` is optional. * **Annotations are the protocol's own**, read off the same `annotations` object your client receives: "read-only" is `readOnlyHint`, "irreversible" is `destructiveHint`, "idempotent" is `idempotentHint`. Your client decides whether to ask you every time from those. * **Idempotency is the server's job, not the agent's.** A REST client sends its own `Idempotency-Key`; an MCP agent should not have to think about retry safety. The server generates and manages the key underneath — anchored to the `preview_id` when the call came from `draft_document`, or to a fresh UUID per call — so a retry after a dropped connection reuses the folio already assigned instead of burning a second one. * **This page lists every tool. YOUR CONNECTION MAY SEE FEWER.** `tools/list` is filtered by the scopes the person granted: a tool appears only when everything in its **Scopes** column is in the grant. A connection holding `documents:read empresas:read` is not offered `emit_document`, because promising an invoice it cannot issue is worse than not offering it. Each tool also publishes its requirement in `_meta.requiresScope`, so a client can say WHY something is missing. Calling a tool the grant does not cover still answers `insufficient_scope`, naming the scope and how to get it — hidden is not the same as absent. * **Almost every tool needs `empresas:read`**, even the ones that look like they only write: naming a company means reading the list of companies this connection may reach, which is also where the sandbox boundary is checked. * **Errors are REST errors.** A tool's error content is the same `{ type, code, message, hint, retriable, param, doc_url, request_id, empresa }` object from the [error envelope](/en/docs/errores), returned as MCP error content. * Money, dates and RUT follow exactly the API's own conventions: integers in CLP, civil dates `YYYY-MM-DD` in America/Santiago, canonical RUT. ## Issuing [#issuing] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------- | ------------- | | `draft_document` | Preview a document before anything is signed: resolves the receptor, prices items, computes totals, and previews (never reserves) the folio that would be used. No side effects — nothing is emitted, no folio is consumed, and the SII is not contacted. Call this first whenever the user described a document in prose, then pass the returned preview\_id to emit\_document. The preview reports how it identified the receptor (`resolution_method`: rut\_exact, anonymous, or needs\_clarification) — if it is needs\_clarification, ask the user rather than guessing. `folio_preview` is the folio this document WOULD take. It is `null` on the SII free portal (`emission_channel: sfg`), where the portal assigns the folio during the emission itself — that is normal and not a shortage. A WARNING IS NOT A REFUSAL. `warnings[]` carry a stable `code` — BRANCH ON THAT, never on the English message — and the draft stays valid whatever they say: emit\_document works. `receptor_unresolved` means a name was given where a RUT belongs, and `param` says which field to ask for. `no_folios_available` means the emission would fail LATER for want of folios, not that this draft is wrong: check get\_folio\_status for the exact stock and, if the tipo really is out, request\_folios asks the SII for a range (#575 put it on this surface — check first, because replenishment below the company’s threshold is already automatic). Do not silently stop. It cannot come from a sandbox company, whose folios are virtual and unlimited, nor from the SII free portal. An unrecognised code is not a fault: relay it verbatim. Likely errors: `receptor_incomplete` — the counterparty has no giro/dirección/comuna on file and a factura cannot be filed without them. The refusal names exactly which, with the length each caps at; ASK THE USER, never invent one, then store the answer with update\_counterparty and draft again. Answering once is enough: it goes into this company’s address book and every later document to that RUT resolves from it. Also `empresa_not_allowed`. | `empresa?`, `tipo_dte`, `receptor?`, `items`, `issue_date?`, `references?` | writes · reversible | `empresas:read` + `documents:write` | no | | `emit_document` | Emit for real. Sandbox: mocked, deterministic, settles in about a second. Live: real SII round-trip, a real folio, a real tax record. Pass either the preview\_id from draft\_document (strongly preferred — the numbers the user approved are the numbers that go out) or the full explicit fields. RETRY SAFETY: if you pass neither preview\_id nor external\_id, this tool drafts a preview itself first — the same call draft\_document makes — purely so the emission has a stable identity to retry against. Without one, a dropped connection or a client that calls this tool twice for the same invoice has no way to be recognised as a retry, and on a live empresa that means a second real folio for one invoice. Prefer calling draft\_document yourself (the user sees the numbers first) or passing your own external\_id (an order id, an invoice number) — both give the same protection without the extra round trip this tool otherwise makes on your behalf. Do not hand-compute and pass `totals`: that field is an assertion, not a hint, and a disagreement with our computation returns `totals_mismatch` rather than being quietly accepted. If the company has require\_confirmation on, this returns a pending\_confirmation envelope instead of a document — that is a normal result, not an error. Give the user the summary and the confirmation\_url and stop: they approve on that page themselves, and there is no tool that finishes it for them. Repeat calls with the same preview\_id, or the same external\_id, return the original document rather than emitting twice. So does a repeat of a keyless call whose fields are identical — within 24 hours, the same tipo\_dte, receptor, items, issue\_date and references means the same invoice. SO A BATCH OF GENUINELY IDENTICAL DOCUMENTS NEEDS TELLING APART, and this case is common rather than exotic: several cuotas of a payment plan, a monthly retainer billed twice, two identical charges to the same customer on the same day. Emitting those from identical fields would collapse them into ONE document and you would report six invoices where four exist. Two ways through, both fine: call draft\_document once per document and emit each returned preview\_id (each draft is its own identity — and the user sees the numbers first, which is the preferred flow anyway), or give each emission its own external\_id. Do NOT tell them apart by editing the glosa: the wording on a document is what the customer reads, and changing it to defeat a retry check puts a bookkeeping artefact on a legal record. IF THE RECEPTOR IS INCOMPLETE, THIS ASKS. A factura’s receptor needs a giro, a dirección and a comuna: they are optional in the SII’s schema and mandatory to its business validation, so a document without them is accepted as an envelope and then REFUSED as a document — with the folio spent either way. Facturia resolves them from what it holds (this company’s address book, and its own earlier documents to the same RUT) and, when it cannot, refuses with `receptor_incomplete` BEFORE a folio is allocated, naming exactly which fields and how long each may be. Ask the user for those and send them back in `receptor_details` — never invent them. ANSWER IN `receptor_details`, NOT BY ENLARGING `receptor`. The two look interchangeable and are not: `receptor` is part of this emission’s retry identity, so adding fields to it makes the retry look like a second, different invoice, while `receptor_details` is deliberately outside that identity — the answering call and the call it answers are the same action. `receptor_details` is also STORED, in this company’s address book for that RUT, so the next document to them needs nobody asked anything. Likely errors: `receptor_incomplete` (see above — supply `receptor_details`, never a guessed address), `totals_mismatch`, `no_folios_available` (get\_folio\_status says which tipos are out), `external_id_exists`, `empresa_not_emitting` — the company has not finished onboarding, and it is almost always waiting on a PERSON: call get\_pending\_human\_action and relay the handoff it returns verbatim. Do not improvise the steps and do not tell the user to "go to the dashboard". `subscription_required` means the company has no active subscription; `error.details.checkout_url` is the page the ACCOUNT HOLDER opens to fix it, and the error text hands you that link — pass it on verbatim rather than describing it. | `empresa?`, `preview_id?`, `tipo_dte?`, `receptor?`, `items?`, `issue_date?`, `references?`, `external_id?`, `receptor_details?` | writes · irreversible · idempotent | `empresas:read` + `documents:write` | **sí** | | `get_document_status` | Cheap, narration-sized status check — not the full document, so polling one does not drag its items and totals back into your context every time. Identify the document by document\_id, by your own external\_id, or by tipo\_dte + folio. It does NOT carry the glosa or the line items; list\_documents does, filtered by folio or external\_id. Statuses run queued → sent → accepted, with rejected and failed as terminal alternatives. Every rejection reason carries `retriable`, which tells you whether calling again could help. Likely errors: `document_not_found` (no document matches that external\_id or tipo\_dte+folio pair for this company), `resource_not_found` (wrong document\_id), `empresa_required`. | `empresa?`, `document_id?`, `external_id?`, `tipo_dte?`, `folio?` | read-only | `empresas:read` + `documents:read` | no | | `get_document_file` | A PDF or XML as a resource link — not embedded bytes. The link is signed, unguessable, needs no credential to open, and is valid for ten minutes; hand it to the user rather than trying to read the file yourself. PDF is for a human to read; XML is the legal artifact the receptor’s accounting system ingests. `copy` picks which PDF copy the link points at — `original` (default, no acuse-de-recibo box, no CEDIBLE legend) or `cedible` (both, the copy that makes the invoice assignable). Only meaningful when `format` is `pdf`. A cedible copy exists only for facturas — tipo 33 and 34 (Ley 19.983 gives the tercera copia to facturas, not to boletas or notas); asking for it on any other type is `422 cedible_not_available`. A document that is still `queued` or `sent` has no rendered file yet, and this refuses with `artifact_not_ready` and `retry_after_seconds` RATHER than handing you a link that would open «este enlace no es válido, venció o fue revocado» in the person’s browser. Wait that long and call again, or check get\_document\_status. Likely errors: `artifact_not_ready` (retriable — the file is still being produced), `cedible_not_available` (wrong tipo for `copy: cedible`). | `empresa?`, `document_id`, `format?`, `copy?` | writes · reversible | `empresas:read` + `documents:read` | no | | `list_documents` | Search and browse documents this company has issued, newest first. Also the way to fetch one document in full — filter by folio or external\_id — which is why there is no separate get\_document tool. EACH DOCUMENT COMES BACK WITH ITS GLOSA — the line items, word for word — plus its references, its id, its external\_id and, on a rejection, the reason. So this is the tool that answers «what did we bill them last time», «use the same wording as the invoice from Tuesday» or «what was that nota de crédito against»: read the wording off a previous document rather than asking the person to type it again. Use get\_document\_status instead when you only need to know whether something was accepted; this tool returns whole documents and is much heavier in context. Likely errors: `invalid_limit` (1–100), `empresa_required`. | `empresa?`, `status?`, `tipo_dte?`, `folio?`, `receptor_rut?`, `external_id?`, `issue_date_gte?`, `issue_date_lte?`, `cursor?`, `limit?` | read-only | `empresas:read` + `documents:read` | no | | `get_folio_status` | How many documents of each type this company can still issue — the check that answers "will my next invoice go through?" before you try. Every document consumes one folio from a range the SII grants, and running out is the `no_folios_available` error the emission tools warn about. `available` COUNTS EVERY RANGE THE COMPANY HOLDS, including the ones queued behind the one it is currently drawing from, because that is what the emission path draws from too. Do not compute a shortage yourself from `next_folio` and a range end. Each row reports `status`: `ok`, `low` (below this company’s own top-up threshold — replenishment is automatic and already in hand), `exhausted` (the next emission of that tipo WILL fail), or `sfg_managed`. `sfg_managed` means the company emits through the SII’s free portal, which assigns a folio at the moment of each emission — there is no stock, `available` is null rather than zero, and `no_folios_available` cannot happen to it. Never report a null as "none left". A tipo the company has never been granted folios for shows as `exhausted` with no ranges; that is not the same as running low, and the fix is a first CAF rather than a top-up. DO NOT RELAY `low` TO THE PERSON. Folios are stock this platform keeps for them; a threshold this platform chose, crossed on a tipo that can still emit, is not news, and repeating it as a warning turns routine housekeeping into something they think they have to act on. Say something only when `exhausted` actually blocks what they asked for — and then say what is already being done about it. request\_folios asks the SII for more, and it is on this surface now (#575) — but read this first: replenishment below the threshold is AUTOMATIC, so a `low` tipo needs nothing said and nothing done. A timbraje registers a real range at the SII, so one request and then wait — never a loop. Likely errors: `empresa_required`, `empresa_not_allowed`. | `empresa?`, `tipo_dte?` | read-only | `empresas:read` | no | | `send_document` | Email an emitted document to the receptor, as PDF and/or signed XML. The XML is the artifact their accounting system ingests; the PDF is what a person reads. Always the ORIGINAL printed copy, never the cedible one — that is an instrument for assigning the debt, not the invoice you send someone to be paid. Omit `to` and it goes to the addresses stored on the document’s receptor. Pass `to` to send it somewhere else; do NOT invent an address, and if the document has none stored and the user gave none, `no_recipient` says exactly that — ask. A REPEAT IS SAFE: the same document to the same addresses with the same attachments is one email and one job — a retry after a timeout returns the ORIGINAL job rather than mailing twice. A DIFFERENT address is a different send and does mail again, so do not "re-send to make sure" with a changed recipient list. This is asynchronous: you get a `job` back and the mail is delivered afterwards. Call this tool again with `job_id` to see how it went — `result.delivered` is the fact, and in a sandbox company `result.simulated` is true and nothing reached a real mailbox. Never tell a user the mail arrived on the strength of the 202 alone. A document still `queued` refuses with `artifact_not_ready` because the signed XML does not exist yet; a document whose PDF has not rendered refuses the same way, and `attach: ["xml"]` works immediately in that case. Likely errors: `no_recipient` (nothing stored, nothing supplied — ask the user for an address), `artifact_not_ready` (retriable — the file is still being produced), `too_many_recipients` (at most 10 addresses), `invalid_email`, `subscription_required` (delivery is a paid capability; `error.details.checkout_url` is the ACCOUNT HOLDER’s page — pass the link on verbatim). | `empresa?`, `document_id?`, `to?`, `attach?`, `job_id?` | writes · reversible | `empresas:read` + `documents:write` | no | | `emit_credit_note` | Emit a nota de crédito (tipo 61) against a document that is already at the SII. There is no "delete an invoice" in Chile: `code: 1` ANNULS the original, `2` corrects its text, `3` corrects its amounts. This creates a REAL second tax document with its own folio, and it cannot be undone. With code 1 or 2 the note mirrors the original’s amounts — you do not restate them, and you must not, because an annulment has to reverse exactly what was issued. With code 3 you pass `items` carrying the CORRECTED values, and the difference is what gets credited. `reason` is required and is limited to 90 characters: it is the SII’s own RazonRef field and it is printed on the document, so write what a tax auditor should read, not a note to yourself. Requires `confirm_emit: true` — say what you are about to annul (tipo, folio, total, receptor) and get a real answer first. If the company requires human approval for agent writes, this tool refuses instead of emitting: an annulment cannot be parked on the approval page, so the account holder does it from the panel. The document must have a folio (a `queued` one has not been issued yet) and must not already be annulled. Likely errors: `document_already_annulled` (a nota de crédito already annulled this one — there is nothing left to do), `document_not_referenceable` (no folio yet: wait for the original to leave `queued`), `invalid_field` (`code` must be 1, 2 or 3; `reason` is required and ≤ 90 chars; `items` is required with code 3), `subscription_required`. | `empresa?`, `document_id`, `code`, `reason`, `items?`, `external_id?`, `confirm_emit?` | writes · irreversible · idempotent | `empresas:read` + `documents:write` | no | | `retry_document` | Re-queue a document stuck in `failed` — the folio was exhausted when it was first sent, the certificate had expired, the SII was down. It REUSES the folio already assigned and never creates a second document, so this is the right answer to a failure and re-emitting is not. ONLY `failed` can be retried. A document that is `queued` or `sent` is still in flight (wait), and `accepted` or `rejected` is terminal — retrying either is refused with `document_not_retryable`, naming the status it actually has. Check get\_document\_status first if you are unsure. Fix the cause before retrying a document that failed for a reason retrying cannot change: get\_document\_status carries `rejection_reasons[]` with a `retriable` flag on each, and get\_folio\_status says whether the tipo has folios again. Retrying into the same wall spends nothing but achieves nothing. It comes back `queued`: the worker picks it up and the SII round trip happens afterwards, so do not report it as accepted. Likely errors: `document_not_retryable` (the document is not in `failed` — the message names the status it is in), `resource_not_found` (wrong document\_id, or it belongs to another company), `empresa_required`. | `empresa?`, `document_id` | writes · reversible · idempotent | `empresas:read` + `documents:write` | no | | `get_document_events` | Everything that happened to one document, oldest first: when it was signed, when it was transmitted, what the SII answered and when, whether it was emailed, whether a nota de crédito annulled it. This is the tool for "what happened to this invoice?" — get\_document\_status answers "where is it now", which is a different question and much shorter. The timeline is the record of what Facturia PUBLISHED about the document, independent of whether any webhook delivery succeeded, so it is complete even for an account with no webhook endpoint at all. An empty timeline is not an error: a document emitted seconds ago may have no event yet. Say that rather than reporting the document as missing. Likely errors: `resource_not_found` (wrong document\_id, or it belongs to another company), `empresa_required`, `empresa_not_allowed`. | `empresa?`, `document_id` | read-only | `empresas:read` + `documents:read` | no | | `check_cesion` | Ask the SII whether one factura can be assigned to a factoring company right now. A REPORT, never a computation and never the assignment itself: under Ley 19.983 a factura becomes assignable when the receptor gives acuse de recibo, or when eight días corridos pass with no reclamo, and one declared paid al contado never does — only the SII knows which applies. Use cede\_document to actually assign it. The answer is asynchronous: this queues the question and the SII answers afterwards. Read the result from the document itself (list\_documents with the folio, or get\_document\_file’s sibling reads) under `cesion`: `cedible` true/false, `sii_status_code` the SII’s raw code, `checked_at` when the SII last ANSWERED. All three are null until the first answer lands — null means "nobody has asked yet", never "not cedible". Facturas only: the registro answers about tipos 33, 34 and 43, because Ley 19.983 gives the cedible copy to facturas and not to boletas or notas. And only about a document it has recorded, so a `queued` or `sent` one is refused with `cesion_status_not_available` — wait until it is `accepted`. In a sandbox company the answer is simulated in-process against the simulated clock and no SII socket is opened; say so rather than reporting it as the SII’s word. Likely errors: `cesion_not_applicable` (wrong tipo — a boleta or a nota has no cesibilidad status at all), `cesion_status_not_available` (the document has not reached the SII yet; retriable once it is accepted), `resource_not_found`. | `empresa?`, `document_id` | read-only | `empresas:read` + `documents:read` | no | | `cede_document` | Assign the credit of one factura to a third party, registering it in the SII’s Registro Electrónico de Transferencia de Crédito. This ASSIGNS MONEY OWED to somebody else and IT CANNOT BE UNDONE. Check check\_cesion first: the SII refuses a factura that is not cedible. THERE IS NO APPROVAL PAGE FOR THIS ONE. `require_confirmation` covers four actions — emitting a document, filing an F29, emitting a tipo 46 and reclaiming a supplier document — and a cesión is not among them, so a company that gated agent writes did NOT gate this. That is why `confirm_irreversible: true` is required and why you must name the cesionario, the amount and the factura to the user, in their own words, before calling. If they hesitate, do not call. Facturas only (tipos 33, 34, 43 — Ley 19.983). Every field of `cesionario` is required and none may be guessed: a wrong RUT assigns the credit to the wrong company. `monto` defaults to the document’s full total and must be between 1 and that total. A RECEIPT IS NOT A REGISTRATION. The furthest the upload itself reaches is `submitted` — the SII answered «Envío recibido OK» and issued a TrackID, and its validation happens hours later by email to the cedente. Check with `job_id`: while the job is `running` with `registration: pending`, say the AEC was RECEIVED and the anotación is pending. Only `registration: confirmed` means the SII recorded the cesión, and only that is worth settling money on. If a previous attempt already put a signed AEC on the wire, a second one is refused with `cesion_attempt_may_have_registered` — whether the credit was already assigned cannot be decided from here, and the refusal carries the last registry answer this system holds. Do not retry it; a person has to check the SII’s registro for that folio. Likely errors: `cesion_tipo_not_cedible` (only 33, 34 and 43 can be ceded), `cesion_cedente_email_missing` (the company has no contact\_email, and the declaración jurada names it), `cesion_attempt_may_have_registered` (a previous attempt may already have assigned this credit — never retry), `missing_field` / `invalid_field` (the cesionario’s RUT fails the módulo-11 check, the dirección is under 5 characters, or `monto` is outside 1..total). | `empresa?`, `document_id?`, `cesionario_rut?`, `cesionario_razon_social?`, `cesionario_direccion?`, `cesionario_email?`, `monto?`, `ultimo_vencimiento?`, `otras_condiciones?`, `email_deudor?`, `job_id?`, `confirm_irreversible?` | writes · irreversible · idempotent | `empresas:read` + `documents:write` | no | | `revoke_document_links` | Revoke every live public link for one document — the signed, credential-free URLs get\_document\_file mints. Use it when a link went to the wrong person: the URLs stop resolving immediately, and anyone opening one afterwards sees «este enlace no es válido, venció o fue revocado». It does NOT unsend anything: a copy already downloaded stays downloaded, and an email that already carried the PDF still carries it. Say that rather than letting a user believe a document was recalled. The document itself is untouched — still emitted, still at the SII, still with its folio. get\_document\_file mints a fresh link whenever one is needed again. Likely errors: `resource_not_found` (wrong document\_id, or it belongs to another company), `empresa_required`, `empresa_not_allowed`. | `empresa?`, `document_id` | writes · reversible | `empresas:read` + `documents:write` | no | `draft_document` returns `receptor_incomplete` naming the exact field that is missing, never a silent guess. `emit_document` can return `totals_mismatch` if the agent sent explicit totals that do not add up — the tool's own description tells it never to compute totals by hand unless it is sure, precisely because that field is an assertion, not a hint. ## Reception [#reception] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------- | ------------- | | `list_received_documents` | Browse the DTEs suppliers have sent this company, filterable by commercial-response state. Use `status: pending` to find the ones still inside their 8-day response window — those are the ones with a deadline attached. Chilean law gives the receiver 8 calendar days to reject a received factura (33/34/43); silence is acceptance and after day 8 it is irrevocable. Each row reports days\_remaining. An empty list on a sandbox company is normal and does not mean anything is broken: reception is driven entirely from outside — a document appears when a supplier actually sends one. There is no tool here that fabricates one, and none is coming; seeding a sandbox supplier document is a REST-only test helper. Say that rather than looking for a tool to create one. Likely errors: `invalid_limit` (1–100), `empresa_required`. | `empresa?`, `status?`, `tipo_dte?`, `issue_date_gte?`, `issue_date_lte?`, `cursor?`, `limit?` | read-only | `empresas:read` + `received:read` | no | | `get_received_document` | Full detail on one supplier document, including the acuse deadline. THE DEADLINE MATTERS: Chilean law gives the receiver 8 calendar days from receipt to reject a 33/34/43. Doing nothing is legally meaningful — it is aceptación tácita, and after day 8 the document is irrevocably accepted and becomes freely transferable. The response reports `days_remaining` and `available_actions`; say the remaining days to the user whenever the state is still pending. Likely errors: `resource_not_found` (wrong received\_document\_id, or wrong company), `empresa_required`. | `empresa?`, `received_document_id` | read-only | `empresas:read` + `received:read` | no | | `accept_received_document` | Register explicit acceptance (the SII’s ACD event) of a supplier document’s content. This matches what would happen anyway if nobody acted — silence accepts at day 8 — so it creates an explicit record rather than changing the outcome. It executes directly even on companies that gate other agent writes, for exactly that reason. One shot: a second commercial response on the same document is refused. Likely errors: `commercial_response_already_registered` (already answered), `commercial_response_window_closed` (past day 8), `commercial_response_not_applicable` (only 33/34/43 carry a commercial response). | `empresa?`, `received_document_id` | writes · irreversible · idempotent | `empresas:read` + `received:write` | no | | `reclaim_received_document` | Register a reclamo against a supplier document — by content, partial missing goods, or total missing goods. IRREVERSIBLE: there is no un-reclamar. It tells the SII and the supplier that this company disputes the document, and it stops the invoice becoming freely transferable. Only do this when the user has said clearly which document and why. The window is 8 calendar days from receipt; after that the document is irrevocably accepted and this tool refuses. Companies with require\_confirmation on get a pending\_confirmation envelope instead of an immediate reclamo — relay the summary and the confirmation\_url; the person decides on that page and no tool here completes it. Likely errors: `commercial_response_window_closed` (past day 8, with the deadline in the error), `commercial_response_already_registered`, `commercial_response_not_applicable` (only 33/34/43). | `empresa?`, `received_document_id`, `reason`, `note?` | writes · irreversible · idempotent | `empresas:read` + `received:read` + `received:write` | **sí** | | `acknowledge_received_document` | Register the acuse de recibo (SII acción ERM) for a supplier document — that the goods or service it describes were actually received. THIS IS NOT ACCEPTANCE OF THE DOCUMENT'S CONTENT. It is the OTHER commercial response Ley 19.983 art. 3 recognises, independent of accept\_received\_document's ACD: a document can carry an ERM, an ACD, both, or neither. Acknowledging receipt says the merchandise or service arrived — it says nothing about whether the invoice itself is correct, and does not stop a later reclamo on the same document. Like acceptance, this is NEVER gated behind a company's require\_confirmation setting: it registers a fact about delivery rather than disputing the document, so it executes directly even on companies that gate a reclamo. One shot: a second acuse (or any other commercial response) already registered for this document is refused, and so is an attempt on a document whose 8-day window has no anchor yet — the clock starts at the SII's own recorded receipt timestamp, and until that has been read there is no window to be inside of. Likely errors: `commercial_response_already_registered` (already acknowledged, or a conflicting response already recorded), `commercial_response_anchor_pending` (we have not yet read this document's SII receipt timestamp), `commercial_response_window_closed` (past day 8 — the document is irrevocably accepted by silence already), `commercial_response_not_applicable` (only 33/34/43 carry a commercial response). | `empresa?`, `received_document_id` | writes · irreversible · idempotent | `empresas:read` + `received:write` | no | > **Trap — These tools' own descriptions say "8 calendar days"** > > `get_received_document` and `reclaim_received_document` declare the 8-day window in their own description and > surface `days_remaining` prominently. An agent narrating "you have 2 days left to respond" only works if the > deadline is impossible to miss in the schema. > > Common errors: `commercial_response_window_closed` (past day 8), > `commercial_response_already_registered` (one shot only), `commercial_response_not_applicable` (wrong type — > only 33/34/43 carry a commercial response). ## Tax [#tax] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------ | ------------- | | `get_periodo_summary` | The company’s IVA position for one month, ready to narrate: débito, crédito, retenido, remanente, PPM, total payable and the due date — plus a `narrative` paragraph written in correct Chilean tax Spanish. QUOTE THE NARRATIVE RATHER THAN COMPOSING YOUR OWN. Getting débito/crédito/remanente right in Spanish is easy to get subtly wrong and impossible for the user to catch, because a wrong version still reads fluently. Set compare\_previous to answer "why do I owe more/less than last month". This is a read: nothing is filed and nothing changes. A period with no synced data yet comes back as zeros with `sync` describing the gap, not as an error — check `sync` before presenting the figures as current. Likely errors: `invalid_period` (`period` is not a real YYYY-MM month), `empresa_required`. | `empresa?`, `period`, `compare_previous?` | read-only | `empresas:read` + `tax:read` | no | | `query_rcv` | Read RCV rows — purchases, sales, or both — as the SII holds them. This is the ledger the F29 is computed from, so it is where to look when a number in a propuesta is surprising. Every response carries a `sync` block saying how fresh the data is; if `status` is `degraded`, say so rather than presenting the figures as current. `estado_contable` is the important filter: REGISTRO rows count toward the F29, PENDIENTE and NO\_INCLUIR do not. An unsynced period returns an empty list with `sync` describing the gap, not an error. Likely errors: `invalid_period` (`period` is not a real YYYY-MM month), `invalid_limit` (1–100). | `empresa?`, `direction?`, `period?`, `estado_contable?`, `counterparty_rut?`, `tipo_dte?`, `issue_date_gte?`, `issue_date_lte?`, `cursor?`, `limit?` | read-only | `empresas:read` + `tax:read` | no | | `sync_rcv` | Ask Facturia to read the Registro de Compras y Ventas from the SII for one period — or for a range of periods, which is how the history the daily sync never read gets filled in. WHEN TO CALL IT: whenever query\_rcv or get\_periodo\_summary answers with `sync.last_synced_at: null` for the period you asked about. That means the month was never fetched, and an empty list there is "unknown", not "no documents" — the daily sync covers the last three months, and a one-time backfill of the twelve before that runs only after a company’s first successful sync, so anything older (or anything asked about before that backfill ran) has never been read. Never tell a user a sale or purchase does not exist from an unsynced period; call this, wait, read again. HOW IT RUNS: each period becomes one `job` (type `rcv_sync`) the worker executes against the SII with the company’s certificate; one takes roughly 5–60 seconds. Pass `job_id` to check on one instead of starting another, or simply re-read query\_rcv after a minute — `sync.last_synced_at` for that period moves when its job lands. A call for a period whose sync is still queued or running attaches to that job rather than starting a second one; a call after it finished starts a fresh read, whatever the daily sync did this morning. LIMITS: `from`/`to` may span at most ten months per call (the endpoint is metered at ten calls per minute per company). For a longer backfill call again with the next range; if the meter trips mid-range the answer lists exactly which periods were queued and which were not, with `retry_after_seconds`. Omitting every period argument syncs the PREVIOUS month, which is the one "sync my RCV" means on any day before the 20th. Sandbox companies never open a session against the SII: the job settles at once over whatever test data was seeded. Likely errors: `invalid_period` (not a real YYYY-MM month), `invalid_range` (`to` before `from`, or more than ten months), `rate_limit_error` (wait `retry_after_seconds` and continue with the periods not yet queued), `empresa_not_emitting` (the company has no certificate in custody yet — nothing can read the SII on its behalf; get\_pending\_human\_action says what is missing). | `empresa?`, `period?`, `from?`, `to?`, `job_id?` | writes · reversible · idempotent | `empresas:read` + `tax:write` | no | | `get_f29_propuesta` | The computed F29 for one period, every código explained in plain language, with the RCV rows behind each figure. This is what would be filed — read it before recommending file\_f29. PAY ATTENTION TO `observations`: any entry with severity `error` means the proposal is not safe to file, and you should tell the user what it says rather than proceeding. Warnings are worth mentioning too (three purchases sitting in PENDIENTE means real credit is being left behind). Also returns a link to the SII-format .txt upload file, valid ten minutes, in case the user prefers to submit it by hand. Likely errors: `resource_not_found` (no propuesta has been computed for that period yet — nothing to sync it from), `invalid_period` (`period` is not a real YYYY-MM month). | `empresa?`, `period` | writes · reversible | `empresas:read` + `tax:read` | no | | `file_f29` | File the F29 for a period — or, if you pass filing\_id, report on one already filed rather than creating a duplicate. FILING IS A LEGALLY BINDING DECLARATION with money attached. `confirm_total_payable` is mandatory: it must equal the propuesta’s figure, which forces the number to appear explicitly in this call and therefore in whatever you told the user before making it. If they disagree, it fails with f29\_amount\_confirmation\_mismatch rather than filing something else. Read get\_f29\_propuesta first, every time. If it carries an observation with severity `error`, do not call this. Most companies gate this: expect a pending\_confirmation envelope rather than a filing. Relay the summary and the confirmation\_url and stop — the person approves on that page, and nothing here can complete it on their behalf. There is no un-filing — a filed F29 is corrected by a rectificatoria, which Facturia does not support. Likely errors: `f29_amount_confirmation_mismatch` (the RCV moved since you read the propuesta — re-read it), `f29_already_filed`, `resource_not_found` (no propuesta computed for that period — call get\_f29\_propuesta first). | `empresa?`, `period`, `confirm_total_payable?`, `filing_id?` | writes · irreversible · idempotent | `empresas:read` + `tax:read` + `tax:write` | **sí** | | `list_tipo46_suggestions` | Foreign digital-services purchases we detected that need a self-issued factura de compra (tipo 46). Since Ley 21.210 the Chilean buyer of foreign digital services IS the taxpayer: the provider charges no IVA, you self-issue a tipo 46 with IVA totalmente retenido, and you take that IVA as crédito fiscal. Almost nobody does it, so this is usually free money the user did not know about — each suggestion reports `estimated_benefit.iva_credit`. Read-only. Confirming one is emit\_tipo46. Likely errors: `empresa_required`, `invalid_limit`. | `empresa?`, `status?`, `period?` | read-only | `empresas:read` + `tax:read` | no | | `emit_tipo46` | Confirm a foreign-digital-services suggestion, which emits the tipo-46 factura de compra with IVA totalmente retenido and books the crédito fiscal. This issues a real tax document in the company’s name. Show the user the suggestion’s proposed\_document and estimated\_benefit first — particularly the exchange rate, which determines the peso base of the document and is the figure that would be questioned if the criterion is ever challenged. Companies with require\_confirmation on get a pending\_confirmation envelope instead. Repeat calls with the same suggestion\_id return the original document rather than emitting twice. Likely errors: `suggestion_already_resolved` (already confirmed or dismissed), `resource_not_found` (no such suggestion), `no_folios_available` (tipo 46 needs its own CAF — get\_folio\_status shows whether one exists, and a company that has never been granted tipo-46 folios shows as `exhausted` with no ranges). | `empresa?`, `suggestion_id` | writes · irreversible · idempotent | `empresas:read` + `tax:read` + `tax:write` | **sí** | | `characterize_purchases` | Set the tipo de compra on RCV purchase entries — the decision that says whether each purchase’s IVA is recoverable crédito fiscal, and therefore what códigos 519/520 carry on the F29. Pass one entry for the single case; up to 500 in one call for "clear the whole month". WHEN TO CALL IT: when query\_rcv shows purchases whose `characterization` is empty or wrong, or when get\_f29\_propuesta counts crédito the user says is not theirs to take (a supermercado charge, an activo fijo, an expense outside the giro). WHAT YOU MUST NOT CLAIM: in this build the SII’s own register DOES NOT MOVE. Live caracterización writes are fenced off, the answer says so on every action (`live_writes_enabled: false`, `dry_run: true`, and a `note` in Spanish), and each action settles as `dry_run`. Relay that: the instruction is recorded against the entry and the tipo de compra at the SII is unchanged. Never say "characterized at the SII". THE API HAS ITS OWN CONFIRMATION GATE. A company with caracterización gated answers a `pending_confirmation` envelope instead of queueing anything — that is a normal result, not an error. Relay its summary and confirmation\_url and STOP. Only if the user then tells you to proceed, call again with the identical entries plus `confirmation_token` (the token is bound to the NUMBER of entries, so the second call must carry exactly the same count). `no_recuperable` is not a complete instruction on its own: it needs `iva_no_recuperable_code` (1, 2, 3, 4 or 9), because the SII does not accept a non-recoverable IVA without a reason. A bulk call is per entry: one bad id lands in `failed[]` with its own error and the other 499 still go through — read `failed` before reporting success. Changing a caracterización invalidates the F29 propuesta for that period, which is recomputed. Likely errors: `invalid_tipo_compra` (not one of the taxonomy’s values), `iva_no_recuperable_code_required` (you asked for `no_recuperable` without a reason code), `characterization_already_pending` (an instruction for that entry is still in flight — wait for it rather than sending a second), `caracterizacion_automation_disabled` (this company has not opted in; a person turns it on, you cannot). | `empresa?`, `entries`, `confirmation_token?` | writes · reversible | `empresas:read` + `tax:write` | no | | `revert_characterization` | Put a purchase’s tipo de compra back to what it was before the last settled caracterización. It queues a NEW action restoring `previous_tipo_compra`, linked to the one it undoes by `reverts` — it does not edit or delete the original, because a write to a tax register that happened, happened, and its row is the evidence. WHEN TO CALL IT: the user says a caracterización was wrong. Only an action that already SETTLED can be reverted; an instruction still in flight is superseded by calling characterize\_purchases again, not by this. WHAT YOU MUST NOT CLAIM: as with characterize\_purchases, in this build the SII’s register does not move — the answer carries `live_writes_enabled: false` and settles as `dry_run`. Say so rather than reporting an undo at the SII. It passes the identical gate: a gated company answers a `pending_confirmation` envelope, which is a normal result — relay its summary and confirmation\_url and stop, and only send `confirmation_token` back after the user says to go ahead. If the value being restored is `no_recuperable`, the SII needs a reason: pass `iva_no_recuperable_code` unless an earlier action on the same document already recorded one. Likely errors: `characterization_nothing_to_revert` (no settled action on this purchase, or the last one recorded no previous value — set the tipo you want with characterize\_purchases instead), `iva_no_recuperable_code_required` (the restored value is `no_recuperable` and no reason is on file), `characterization_previous_not_revertible` (the recorded previous value is not one this API can set — choose one explicitly). | `empresa?`, `entry_id`, `iva_no_recuperable_code?`, `confirmation_token?` | writes · reversible | `empresas:read` + `tax:write` | no | | `set_estado_contable` | Record YOUR bookkeeping estado for one RCV purchase — REGISTRO, PENDIENTE or NO\_INCLUIR. It is written to `estado_contable_override`, beside the SII’s own `estado_contable`, and NEVER over it: the SII’s register is refreshed by every sync and stays authoritative, so a resync can always correct us. THE OVERRIDE IS IGNORED BY THE F29 unless this company has `honor_estado_contable_override` turned on. The answer says which value the propuesta will actually use, in `estado_contable_effective`, and `estado_contable_override.honored` says whether the F29 obeys you at all. Read those two before telling anyone their crédito changed — on a company that does not honour overrides, this call changes a label and nothing else. RECLAMADO is deliberately not settable here: an entry is RECLAMADO because a reclamo was registered against the document, and asserting it here would claim a commercial action that never happened. Use reclaim\_received\_document for that. This is NOT the same act as caracterización — that is characterize\_purchases, which decides whether the IVA is recoverable. Every write is recorded with the key that made it and the SII value it did not replace. Likely errors: `invalid_field` (`estado_contable` is not REGISTRO, PENDIENTE or NO\_INCLUIR), `resource_not_found` (no such purchase entry for this company), `empresa_required`. | `empresa?`, `entry_id`, `estado_contable` | writes · reversible · idempotent | `empresas:read` + `tax:write` | no | | `list_f29_propuestas` | List the periods that have an F29 propuesta and what state each one is in — `ready`, `computing`, `stale` or `blocked` — newest month first. This is how to answer "which months are done" and "is anything still outstanding" without probing twelve periods one by one and collecting eleven 404s. READ-ONLY: nothing is computed, nothing is filed, nothing changes. The rows deliberately omit the validation block and the filing id; for one month in full — every código explained, the observations, the upload file — call get\_f29\_propuesta with that period. A period MISSING from this list has never been computed, which is not the same as "nothing was owed": call recompute\_f29\_propuesta for it, or check that the RCV for that month was ever synced (sync\_rcv). Likely errors: `invalid_cursor` (the cursor did not come from a previous page of this same list), `invalid_limit` (1–100), `empresa_required`. | `empresa?`, `cursor?`, `limit?` | read-only | `empresas:read` + `tax:read` | no | | `recompute_f29_propuesta` | Ask Facturia to rebuild one period’s F29 propuesta from the ledgers as they stand now, optionally storing overrides for individual códigos. Call it after a caracterización, after a sync\_rcv that moved a month, or when the user says a figure is stale. It does NOT file anything and nothing reaches the SII. IT IS QUEUED, NOT DONE. The answer comes back with `status: computing`: the computation is a worker job, so say "recomputing", never "recomputed". get\_f29\_propuesta for the same period reflects it after a moment — read it again before quoting any number, and do not call file\_f29 off a propuesta that is still computing. AN OVERRIDE IS A HUMAN OVERRULING A COMPUTED TAX FIGURE, so every one needs a `reason` in the user’s own words. It is stored, not applied here — the number in the propuesta and the number in the SII upload file come from the same arithmetic, which is why this layer refuses to do the sum itself. Do not invent a reason; if the user did not give one, ask. Likely errors: `override_reason_required` (an override arrived with no reason — unauditable, so it is refused), `invalid_field` (`code` and `value` must be integers), `invalid_period` (`period` is not a real YYYY-MM month). | `empresa?`, `period`, `overrides?` | writes · reversible · idempotent | `empresas:read` + `tax:write` | no | | `list_f29_filings` | Every F29 filing recorded for this company, newest first, with the period, the declared total and where each one got to. This is the read that answers "have we filed August yet" and "what happened to the one we sent" without filing anything: it is the list form of file\_f29’s `filing_id` check. READ-ONLY. Nothing is filed, nothing is retried and nothing reaches the SII. MIND THE VOCABULARY when you narrate a row: `submitted` means the SII has the file and has not answered; `accepted` is filed; `rejected` means the SII refused it and it is NOT filed; `dry_run` is a rehearsal that never left the building and does not hold the period. A period with no row here has not been filed through Facturia — it says nothing about whether somebody filed it at the SII by hand, which we cannot see. Likely errors: `invalid_period` (a `period_gte`/`period_lte` filter that is not a real YYYY-MM month), `invalid_cursor`, `empresa_required`. | `empresa?`, `period_gte?`, `period_lte?`, `status?`, `cursor?`, `limit?` | read-only | `empresas:read` + `tax:read` | no | | `list_expenses` | The charges Facturia holds for this company — posted through add\_expense, pulled from a bank feed, or parsed out of an uploaded cartola. These are the raw material the tipo-46 detector reads: a foreign-currency charge from a provider with no domicile in Chile is what becomes a factura de compra. READ-ONLY. Use it to answer "what did we import", "did that Anthropic charge land", and to see which charges already have a suggestion (`suggestion_id`) or a document (`document_id`) hanging off them. A charge with no suggestion is not necessarily ineligible: detection runs asynchronously after intake, and a CLP charge is never a tipo-46 candidate at all. list\_tipo46\_suggestions is the read that answers eligibility. Likely errors: `invalid_period` (`period` is not a real YYYY-MM month), `invalid_limit` (1–100), `empresa_required`. | `empresa?`, `period?`, `status?`, `source?`, `cursor?`, `limit?` | read-only | `empresas:read` + `tax:read` | no | | `add_expense` | Put one charge on file so the tipo-46 detector can see it — the way a foreign digital-services purchase (Anthropic, AWS, Google, Figma…) enters Facturia when there is no bank feed for the card it was paid with. It records a charge; it does NOT emit anything and nothing reaches the SII. WHAT MAKES IT A TIPO-46 CANDIDATE: the charge must be in a FOREIGN CURRENCY. A CLP charge is never one — a Chilean supplier issues their own document, and self-issuing a second would duplicate the purchase. The provider’s name is what the detector matches on, so send it as it appears on the statement; you do not send a RUT, because the SII’s generic foreign-provider RUT is what the document will carry. `fx_rate` IS REQUIRED whenever the currency is not CLP and Facturia will never invent one: the rate sets the peso base of a tax document, and guessing it would make us the author of the user’s tax position. Ask the user for the rate their card actually settled at (that is `fx_source: card_rate`) rather than looking one up. `amount_original` is the charge in its own currency; every CLP figure in this product is an integer with no decimals. The suggestion follows asynchronously — this call does not return one. Read list\_tipo46\_suggestions a moment later, then emit\_tipo46 to confirm. Pass `external_id` if your source has a stable id for the charge: it is what stops the same charge being counted twice when a feed and a person both send it. Likely errors: `fx_rate_required` (a non-CLP charge with no rate), `invalid_amount` (`amount_original` must be positive), `invalid_date` (`date` must be YYYY-MM-DD), `external_id_exists` (this charge is already on file — it was NOT duplicated). | `empresa?`, `provider_name`, `provider_country?`, `description?`, `amount_original`, `currency`, `date`, `fx_rate?`, `fx_source?`, `external_id?` | writes · reversible | `empresas:read` + `tax:write` | no | | `list_statements` | The bank and card statements (cartolas) uploaded for this company, newest first, with how each import went: rows read, charges created, duplicates skipped, and the per-row errors the parser refused. This is where to look when a user says "I uploaded my cartola and nothing happened". READ-ONLY. Nothing is re-parsed and nothing is imported. A row with `errors[]` is the important one: the parser REJECTS a line whose amount, currency, date or decimal convention is ambiguous rather than guessing it, because a guessed row looks imported and nothing announces the mistake. Relay those reasons and line numbers to the user; they are the fix. Likely errors: `invalid_cursor` (the cursor did not come from a previous page of this list), `invalid_limit` (1–100), `empresa_required`. | `empresa?`, `cursor?`, `limit?` | read-only | `empresas:read` + `tax:read` | no | | `upload_statement` | Import a bank or card statement so its foreign-currency lines become charges the tipo-46 detector can read. A tool call cannot carry a file, so pass the cartola as base64 in `file_base64`; a cartola is a list of the user’s own charges, and there is no credential in it. IT IS QUEUED, NOT PARSED HERE. The answer is a job: say "importing", not "imported". list\_statements for this company reports rows read, charges created, duplicates skipped and the per-row refusals once the job lands, and list\_expenses shows what actually came in. RE-UPLOADING THE SAME FILE IS SAFE, and the answer tells you which happened: byte-identical bytes are recognised by their sha256 BEFORE anything is stored, and the ORIGINAL import job comes back — no second parse, no second copy, nothing double-counted. Overlapping (not identical) cartolas are safe too: rows are deduplicated on date, amount, currency and provider. THE PARSER REJECTS RATHER THAN GUESSES. A line whose amount, currency, date or decimal convention is ambiguous comes back as an error with its line number instead of a charge, because a guessed row looks imported and nothing would announce the mistake. Pass `default_currency` only when the file has no currency column, and it must be a FOREIGN currency — a CLP default would turn every row into a peso row, and a peso row never becomes a tipo 46. Likely errors: `file_too_large` (the limit is 8 MB), `invalid_format` (`format` is not a layout we know — leave it at `auto` unless the user told you the bank), `default_currency_not_foreign` (you passed CLP), `file_empty` (the base64 decoded to nothing — NOTHING was stored). | `empresa?`, `file_base64`, `filename?`, `format?`, `default_currency?` | writes · reversible · idempotent | `empresas:read` + `tax:write` | no | | `dismiss_tipo46_suggestion` | Reject a foreign-digital-services suggestion without emitting anything, with a reason the detector learns from. Use it when the user says a charge is not a foreign digital service, or that they already declared it. Nothing is emitted, nothing reaches the SII, and the charge itself stays on file. DISMISSING GIVES UP REAL MONEY, so say what it costs before you call it: the suggestion carries `estimated_benefit.iva_credit`, which is IVA the user could have taken as crédito fiscal. Read it back to them first — list\_tipo46\_suggestions has it — and dismiss only when they have said so. The other half of this pair is emit\_tipo46, which confirms rather than rejects. The reason is stored on the row rather than logged, because a detector that is never told it was wrong stays wrong. `other` exists so the enum never blocks a dismissal you were told to make. Likely errors: `suggestion_already_resolved` (it was already confirmed or dismissed — nothing changed), `resource_not_found` (no such suggestion for this company), `invalid_field` (`reason` is not one of the four). | `empresa?`, `suggestion_id`, `reason?` | writes · reversible | `empresas:read` + `tax:write` | no | | `list_bank_connections` | The bank/card connections linked to this company, with each one’s status and when it last synced. A feed is what removes the assumption add\_expense makes — that somebody already typed the charge in — by turning foreign-currency card movements into charges the tipo-46 detector reads. READ-ONLY. Nothing is linked, synced or disconnected, and no credential is ever returned: the bank credentials live with the provider, and Facturia holds only a link token. THE STATUS IS THE WHOLE POINT of this read. `pending_link` means a person still has to complete the link at `link_url` and nothing is arriving. `requires_relink` is the near-universal failure mode of bank feeds and means the feed went quiet — say so rather than reporting an empty month as "no charges". `active` is the only status that syncs. Likely errors: `invalid_cursor` (the cursor did not come from a previous page of this list), `invalid_limit` (1–100), `empresa_required`. | `empresa?`, `cursor?`, `limit?` | read-only | `empresas:read` + `tax:read` | no | | `link_bank_connection` | Create a bank or card connection for this company and get back the `link_url` a PERSON opens once to authorize it. From then on each sync turns foreign-currency movements into charges with the rate the issuer actually settled at, and the tipo-46 detector runs over them. NO CREDENTIAL PASSES THROUGH THIS CONVERSATION, and none may: the bank login happens at the provider, on the page this call mints, and Facturia stores a link token rather than a password. Never ask the user for a bank password here. IT IS NOT LINKED YET. A live connection comes back `pending_link` with a `link_url`: relay the link, and say that nothing arrives until somebody completes it. Do not report a feed as working until list\_bank\_connections shows it `active`. (A SANDBOX company has no bank to link, so its connection is born `active` with no link\_url and nothing behind it.) A second call creates a SECOND connection rather than reusing the first, so read list\_bank\_connections before calling this — a company that already has a `requires_relink` feed needs that one re-linked, not a new one. Likely errors: `invalid_provider` (`fintoc` is the only provider), `invalid_field` (`account_types` or `sync_frequency` is not one of the accepted values), `empresa_required`. | `empresa?`, `provider?`, `account_types?`, `sync_frequency?` | writes · reversible | `empresas:read` + `tax:write` | no | | `sync_bank_connection` | Ask a linked bank/card feed to pull its movements immediately instead of waiting for the scheduled sync. Each foreign-currency movement becomes a charge, and the tipo-46 detector runs over the new ones. Nothing is emitted and nothing reaches the SII. IT IS QUEUED, NOT DONE. The answer is a job (`type: connection_sync`): say "syncing", not "synced". Pass `job_id` to check on one instead of starting another, or read list\_expenses after a minute. A job that stays `queued` for a few minutes means the worker has not picked it up yet — that is "still running", not "failed". A connection that is not `active` is REFUSED rather than queued, on purpose: a sync job for an unlinked feed would run, do nothing, and report success. If you get `connection_not_linked`, read list\_bank\_connections — the fix is a person completing (or repeating) the link at its `link_url`, which you cannot do for them. In the SANDBOX the job comes back already `succeeded` with nothing imported: there is no bank to call, and a job left `queued` would never move. Likely errors: `connection_not_linked` (the feed is `pending_link` or `requires_relink` — a person must link it), `resource_not_found` (no such connection for this company), `rate_limit_exceeded`. | `empresa?`, `connection_id?`, `job_id?` | writes · reversible · idempotent | `empresas:read` + `tax:write` | no | | `delete_bank_connection` | Disconnect a bank or card feed: the link is revoked and no further movements are ingested. Charges the feed already created are KEPT — an expense that became a tipo 46 is part of the company’s F29, and its provenance does not stop mattering because the feed was turned off. ASK BEFORE YOU CALL IT. Reconnecting is not something you can do: link\_bank\_connection creates a new connection and a PERSON has to complete the bank link again, at their bank. From the moment this returns, foreign charges stop arriving on their own and nothing announces it — which is how a month quietly ends up with no crédito. If the feed merely went quiet, this is the wrong tool: a `requires_relink` connection needs re-linking, not disconnecting. Read list\_bank\_connections first. Likely errors: `connection_already_disconnected` (it was already off — nothing changed), `resource_not_found` (no such connection for this company), `empresa_required`. | `empresa?`, `connection_id` | writes · irreversible | `empresas:read` + `tax:write` | no | `confirm_total_payable` is still required through the MCP layer. The REST API already refuses to file without it, and that barrier is worth double here: it forces the figure to appear explicitly in the tool call, which forces it to appear explicitly in what the agent tells the human before filing. ## Account [#account] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------- | --------------- | ------------- | | `list_empresas` | Which companies this connection can see — the disambiguation tool. Call it first whenever the user names a company loosely ("the consultancy", "my other one") or whenever you are unsure which company a request is about, and ask rather than guessing. Reports each company’s `mode` (test = sandbox, nothing it does reaches the SII), its `status`, and — the field that decides whether you can do anything at all — its `onboarding_status`. Anything other than `emitting` means it CANNOT issue documents yet and is almost always waiting on a person: call get\_pending\_human\_action for that company and relay the handoff. Also reports `emission_channel` (how it emits: `mercado` = our engine, `sfg` = the SII free portal, which has a different setup entirely) and `certification_track` (what it is being certified for), plus `capabilities` when the engine supplies it — which document types it can issue RIGHT NOW, and what is blocking the rest. Prefer `capabilities` over your own inference: "it looks set up" is not the same fact as "the SII will accept a folio from it today". This is an account-level tool: it takes no `empresa` argument. | `status?` | read-only | `empresas:read` | no | | `select_empresa` | Remember one company as the default for the rest of this session, so you do not have to repeat its RUT on every call. Useful when this connection can reach several companies and the user has settled on one. An explicit `empresa` argument on any tool always overrides this, so the selection can never silently redirect a call that named its target. If the connection drops, the selection is lost and tools ask for the company again — nothing produces a wrong answer. Pass empresa: null to clear the selection. | `empresa` | writes · reversible · idempotent | `empresas:read` | no | | `get_usage` | How many documents this account has emitted in a billing period. Omit `empresa` for the whole account, or name one to see just that company. A document is billable when the SII accepts it — not when it is submitted. Rejected, failed and sandbox documents are never billed, so a sandbox-heavy month legitimately shows near-zero usage. Read-only; nothing here changes a plan or a bill. | `empresa?`, `period` | read-only | `usage:read` | no | | `get_pending_confirmation` | Read what happened to an approval you handed to a person. Call it with the `confirmation_id` from a `pending_confirmation` envelope — this is how you find out they came back, and it is the ONLY read that works for it: get\_document\_status needs an identifier the envelope does not carry. `status` is one of: `pending` (still waiting — the link is live and nothing has been done), `confirmed` (they approved it AND it executed; `result` is the document, filing or response the original tool would have returned), `rejected` (they said no — do not raise the same action again unless the user asks), `expired` (nobody decided in time; call the ORIGINAL tool again with the same arguments to raise a fresh approval), `failed` (they approved it and the execution was refused — `error` says why, and it will not change on a retry). Do NOT poll this in a loop. Call it when the user says they have done it, or when they ask you whether it went through. A person deciding on a page is a human-length wait, not a machine-length one. Likely errors: `confirmation_not_found` (no such approval for THIS connection — a confirmation belongs to the connection that raised it). | `confirmation_id` | read-only | none | no | | `poll_events` | The account’s event ledger, read forward from a checkpoint — the way to find out that something happened WITHOUT calling a read tool over and over hoping. Use it after handing a person a link ("did they sign it yet?"), after emitting ("did the SII accept it?"), or to catch up on everything since your last look. PASS `after` WITH THE `next_cursor` (or the last event id) YOU GOT LAST TIME. It pages FORWARD, oldest first, strictly newer than what you name, so you never re-read a row and never need to remember which ones you have seen. The first call may omit it. `wait_seconds` lets you wait for the next event instead of coming back empty: 0 (the default) looks once and answers immediately; up to 30 waits that long and returns as soon as something arrives. DO NOT CALL THIS IN A LOOP. Ask when the user asks, or once after they say they have done their part — a person deciding on a page is a human-length wait, and burning turns on an empty ledger helps nobody. This is an account-level tool: it takes no `empresa` argument. Filter with `type` if you only care about one kind. Likely errors: `insufficient_scope` (this connection was not granted `webhooks:read` — the user chooses that on the consent screen and an agent cannot widen its own grant), `invalid_cursor`. | `after?`, `type?`, `limit?`, `wait_seconds?` | read-only | `webhooks:read` | no | Deliberately sparse. `select_empresa` is scoped so it cannot break the requirement that the server be able to operate statelessly: an explicit `empresa` argument always wins, and losing the session loses a default, not a wrong answer. ## Onboarding [#onboarding] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------ | ------------- | | `create_empresa` | Add a company (empresa) to this account. In test mode it is born `emitting` — no mandato, no certificate, no certification — which is the whole point of the sandbox. In LIVE mode it starts at the beginning of onboarding and a person has to do several things at the SII before it can issue anything; call get\_pending\_human\_action right after to find out what. Ask the user for the RUT and the razón social rather than inferring them: a company created under the wrong RUT is a tenant whose documents would be issued in somebody else’s name. `emission_channel` decides HOW this company will emit — through Facturia’s own engine (`mercado`) or through the SII’s free portal (`sfg`) — and the two have different setup steps entirely. If the user has not said, ask; do not pick one. `origen` is REQUIRED for a live company and there is no default: it is the user’s own statement about how they emit TODAY, which nobody can read off the SII. ASK — do not infer it from `emission_channel`, which is a different question about the future. READ THE RESULT BEFORE PROMISING ANYTHING. It says whether the new company is inside THIS connection’s grant (`reachable_by_this_connection`): a grant scoped to a list of companies does not stretch to cover one created afterwards, and no agent can widen its own grant. When it is false, every other tool will answer `empresa_not_allowed` for this company and the fix is a human one, stated in the result. Likely errors: `invalid_field` (almost always `origen` on a live key — ask the user which of the three it is and call again), `empresa_already_exists` (this RUT is already on the account — list\_empresas will show it), `email_verification_required` (a live company needs a confirmed e-mail address on the account), `invalid_rut`. | `rut`, `legal_name`, `giro?`, `emission_channel?`, `origen?`, `contact_email?`, `doc_types?`, `resolucion?`, `resolucion_boletas?` | writes · reversible | `empresas:write` | no | | `get_onboarding_status` | Where a company is in the SII onboarding process, and what it is blocked on. Call this when emission fails with `empresa_not_emitting`, or when the user asks "can I start invoicing yet". YOU CAN DRIVE THIS, not just report on it. When `status` is `action_required`, call get\_pending\_human\_action for the handoff; mint the link with mint\_onboarding\_link (or request\_mandato\_signature for the mandato, upload\_certificate for the certificate page, set\_sfg\_credential for the SFG clave); and when the person tells you they have done it, call recheck\_onboarding\_step so we go and look. If there is no company yet, create\_empresa makes one. WHAT NO AGENT CAN DO, here or anywhere: the acts a person decides. The representante legal signs the mandato and the titular authorizes the go-live, each on a hosted page built for it — you carry the link to them and carry the result back, and you are never the record that they agreed. Facturia files the postulación itself, with the certificate it holds in custody, after the titular has approved it on a page. The declaración de cumplimiento starts the company's Información Electrónica de Compras y Ventas obligation FROM THE MONTH IT IS MADE, which is why choosing that month is a person's decision and not a progression. Never tell a user it has been scheduled or done. A step advances when WE READ THE FACT off the SII, never because somebody said so — including you. `recheck_onboarding_step` returns the status unchanged, and that is it working. | `empresa?` | read-only | `empresas:read` | no | | `get_pending_human_action` | The handoff for a company that is waiting on a PERSON. Call this whenever emission fails with `empresa_not_emitting`, whenever `get_onboarding_status` reports `action_required`, or whenever the user asks why they cannot invoice yet — and note that a company can be `emitting` AND still owe a decision, so read `pending_action` rather than deciding from `status`. TWO KINDS OF WAITING, ANSWERED DIFFERENTLY (rulings 2026-09-06). Most of these acts are OURS to perform: the certificate the empresa gave us belongs to its representante legal and does authenticate them at the SII, so Facturia files the postulación, runs the whole boleta habilitación and files the Declaración de Cumplimiento with it. What we need from the person is an AUTHORIZATION, not an errand — and when the handoff carries an `approval`, that is exactly what it is asking for: the act, who may take it (the titular, or the representante legal for `cumplimiento_declaracion`), and what approving causes. Send them to the page and let them decide; never approve it yourself and never say you did. The acts that DO stay the client’s stay so because the certificate cannot reach them, not because of a rule we chose: granting «Enviar Doctos.» in Mantención de usuarios and declaring the casilla de intercambio happen inside the usuario administrador’s own SII session, where a certificate we hold in custody has no standing. Say that — it is true, and it tells the user which of their people has to open it. Do not improvise a third: whatever `pending_action` names is the list, and it already accounts for what this deployment can and cannot reach. What comes back is a `pending_action`: a URL, ordered instructions in Spanish, what to have in hand first, exactly which person may do it, how we verify it happened, and the legal citation for why it cannot be delegated. RELAY THE SPANISH VERBATIM — it is written for the person who has to act, who is very often NOT the account holder (the account is the accountant’s; the certificate is a socio’s). Do not paraphrase the steps and do not invent any: if `evidence` is not `captured`, `caveat` says what we are unsure about, and that sentence must be relayed too. Read `verification` before promising anything. `sii_probe` means we will notice on our own. `no_producer` means we genuinely cannot see this one yet and a person has to tell us — never say it is being watched. What comes back also says how to GET the person a working link: `action_url` is a live one if there is one, and `null` if there is not — never send a user to a URL you built yourself. When it is null, `link.mint` names the call that mints one, and the tool for it is mint\_onboarding\_link (or request\_mandato\_signature for the mandato). You cannot mark any of this done. A step advances when we read the fact off the SII, never because someone said so — but when the user tells you they have finished, call recheck\_onboarding\_step, which asks us to go and look. It changes nothing by itself and says so. | `empresa?` | read-only | `empresas:read` | no | | `request_mandato_signature` | Mint the hosted page where the representante legal signs the mandato — the document that authorizes Facturia to act before the SII on this company’s behalf. Nothing is signed by calling this: it produces a URL to give to a person. THE URL COMES BACK ONCE and only its hash is stored, so relay it verbatim and do not summarise it away. It is SINGLE-USE: once the mandato is signed the link stops working, and calling this again revokes whatever link was outstanding. The signer is the representante legal of the company, who is very often NOT the account holder — the account may be the accountant’s. Say who has to open it. You cannot accept the mandato on anyone’s behalf and no tool here can: the acceptance is the person’s act, recorded against the exact text they were shown. Likely errors: `mandato_already_signed` (there is nothing left to sign — use get\_mandato to see when and by whom), `mandato_terms_draft` (the terms are not published yet — a live company cannot accept them and this is ours to fix, not the user’s). | `empresa?` | writes · reversible · idempotent | `empresas:read` + `mandato:write` | no | | `get_mandato` | Read the mandato for a company: whether it has been signed, when, by whom, and against which version of the text. THIS IS HOW YOU FIND OUT THE HUMAN CAME BACK — call it after handing over a signing link instead of minting a second one, because minting revokes the outstanding link. `status` is `none` (nobody has been asked yet), `pending` (a signing link is outstanding), `signed`, or `revoked`. None of those is an error; `none` in particular is a state, not a 404. Read `terms_current` before trusting a `signed` mandato: `false` means the text was amended and the acceptance on file no longer binds, so the pipeline has already stopped trusting it and a fresh signature is needed. `superseded_at` says when that happened. `acceptance_method` says how it reached us — `hosted` (a person completed the page), `dashboard` (the titular in session) or `api` (an integrator attested it). Likely errors: none in normal operation — an absent mandato answers `status: "none"` rather than failing. `empresa_not_allowed` if this connection cannot reach the company. | `empresa?` | read-only | `empresas:read` + `mandato:write` | no | | `mint_onboarding_link` | Mint the hosted page for a step that a PERSON has to perform, and give the URL to whoever has to perform it. The step key comes from get\_pending\_human\_action or get\_onboarding\_status — do not invent one; each company has its own checklist and it differs between the mercado and SFG channels. THE URL COMES BACK ONCE and only its hash is stored: relay it verbatim. It is reusable and lives about thirty days, and minting again REVOKES the previous link — so do not mint one to "check", use get\_pending\_human\_action, which reports whether a live link already exists. These pages exist because the person who must act often has no account here: the account belongs to the accountant and the certificate to a socio. A link works without a login; a dashboard does not. The mandato is the exception — its link comes from request\_mandato\_signature. Likely errors: `invalid_step` (not a step on this company’s checklist, or not one a person performs), `step_already_complete`. | `empresa?`, `step` | writes · reversible · idempotent | `empresas:read` + `onboarding:write` | no | | `recheck_onboarding_step` | The resume half of the handoff: the person says they have done it, and this asks us to go and find out whether the SII agrees. Call it when a user tells you a step is finished. IT ACCEPTS A CLAIM AND PRODUCES NO FACT. It writes no status, moves no company, and returns the step exactly as it stood when you called — read BEFORE the check was queued. If you recheck an `action_required` step and read `action_required` back, that is the endpoint working: a step advances when we see the fact at the SII, never because somebody said so. Never report to the user that the step is now done. Read `evidence_source` in the answer and relay what it means. `sii_probe`: we will go and read the SII. `own_record`: the act lands in our own tables and we will see it. `no_producer`: we genuinely cannot observe this step yet — say so, do NOT say it is being watched, and tell them a person here has to confirm it. `sandbox`: a test company never opens a session against the SII, so there is nothing to re-read. Likely errors: `invalid_step` (not a step on this company’s checklist), `step_already_complete`. | `empresa?`, `step` | writes · reversible · idempotent | `empresas:read` + `onboarding:write` | no | | `upload_certificate` | Put the company’s digital certificate (.pfx / PKCS#12) into Facturia’s custody. It is what signs this company’s documents; without it nothing can be emitted. DO NOT ASK FOR THE PASSWORD. Call this with only `empresa` and it MINTS THE HOSTED PAGE where the person uploads the file themselves — a URL you hand over, no login, no transcript. That is the right answer almost every time: a certificate password typed into a chat window is stored in that conversation’s history wherever that history lives, and the certificate is the company’s legal signature. The person who has the `.pfx` is usually the representante legal or a socio, who has no account here anyway. Only pass `file_base64` (the .pfx, base64-encoded) and `password` when the user has ALREADY volunteered both, unprompted. A tool call cannot carry a file, which is why they would have to be pasted. If this account already holds a certificate that the SII lists as an authorized user of this company’s RUT, pass `reuse_from_empresa` instead and no file or password is needed at all — prefer that whenever it applies. When the person has uploaded it on the page, call recheck\_onboarding\_step so we go and look. Likely errors: `certificate_invalid_password`, `certificate_expired`, `certificate_rut_mismatch` (the certificate does not belong to a person the SII authorizes for this RUT), `certificate_not_authorized_user` (for a reuse). | `empresa?`, `file_base64?`, `password?`, `filename?`, `reuse_from_empresa?` | writes · reversible | `empresas:read` + `onboarding:write` | no | | `set_sfg_credential` | For a company that emits through the SII’s free portal (`emission_channel: sfg`), Facturia needs the clave of the CERTIFICADO CENTRALIZADO the SII already holds for it — that is what authorizes the SII to sign each document server-side. Without it an SFG company cannot emit at all. DO NOT ASK FOR IT IN THIS CONVERSATION. Call this with only `empresa` and it MINTS THE HOSTED PAGE where the person types it themselves — no login, and the secret never reaches this transcript. That is the right answer unless the user has ALREADY volunteered the value unprompted. IT IS NOT THE CLAVE TRIBUTARIA. Facturia never asks for, holds or wants the taxpayer’s SII password. If a user offers you that instead, refuse it and say why: it is the key to their whole tax identity, and this is not it. The value is never readable back — `GET` reports only whether one is stored and since when. Likely errors: `invalid_field` (a synonym like `clave` or `password` is refused rather than coerced — the field is `clave_centralizado`), `422` on a `mercado` or sandbox company, which has no Portal MIPYME to sign anything. | `empresa?`, `clave_centralizado?` | writes · reversible | `empresas:read` + `onboarding:write` | no | | `upload_certification_set_file` | Put the `.txt` SET DE PRUEBAS the SII issued for this company into Facturia, so the certification round can be built from it. The SII issues one per taxpayer when it accepts the postulación and nothing downloads it for us — this is how the bytes get in, and certification cannot start without them. A tool call cannot carry a file, so pass it as base64 in `file_base64`. There is no secret in a set de pruebas — it is a list of test cases — so unlike a certificate password this is a reasonable thing to move through a tool call. THE BYTES MATTER. The file is ISO-8859-1 and its accents are part of the document: «Cajón» and «Pañuelo» are two of the items the SII has reported as reparos when they were lost. Base64 the file EXACTLY as downloaded; never open it in an editor and re-save it, and never retype its contents. This stores and parses the file. It does not run a round, does not contact the SII and does not spend a folio. Likely errors: `set_file_unrecognized` (those bytes are not a set de pruebas — a portal login page saved as .txt is the usual cause, and NOTHING is stored), `422` for a company whose lane has no certification. | `empresa?`, `file_base64`, `filename?` | writes · reversible | `empresas:read` + `empresas:write` | no | | `request_go_live_authorization` | Mint the page where the account holder authorizes the Declaración de Cumplimiento — the last SII step before a company can emit for real. This does NOT authorize it and no tool here can. WHY IT IS A HUMAN DECISION AND NOT A PROGRESSION: declaring puts the company into the electronic invoicing system immediately AND starts its Información Electrónica de Compras y Ventas obligation FROM THE MONTH OF THE DECLARACIÓN, whether or not it ever emits anything. Which month that is belongs to the customer and their accountant. The SII’s own page says the step may be postponed. So: relay the link, say plainly that declaring starts a monthly obligation from that month, and never tell a user it has been scheduled, requested or done. THE LINK IS SINGLE-USE and lives seven days: it records a decision with a tax consequence, so it stops working once the titular decides. Calling this again mints a fresh one and revokes whatever was outstanding. Likely errors: `go_live_not_ready` (the company has not reached the cumplimiento decision yet — `error.details.blocked_on` names what it is waiting on and `error.details.next` is the one call or link that moves it; relay that instead), `go_live_not_applicable` (an SFG company has no declaración to authorize), `cumplimiento_already_filed` (there is nothing left to authorize), `subscription_required` (the company has no active subscription — `error.details.checkout_url` is the page the ACCOUNT HOLDER opens to fix it; hand it over rather than describing it). | `empresa?` | writes · reversible · idempotent | `empresas:read` + `onboarding:write` | no | | `get_account` | The authenticated account and the credential in use: this key’s own scopes, mode and empresa allowlist, how many empresas it can reach (`empresas_total`) versus how many of those can actually emit right now (`empresas_ready`), and the account’s rate limits. Call this to learn what this connection can actually DO before promising a user anything — a grant missing `tax:write`, for instance, cannot file an F29 no matter how the rest of the conversation goes, and this is the one call that says so up front rather than after a refusal three turns later. Account-level: it takes no `empresa` argument, and a `Facturia-Empresa` header on it is refused. Likely errors: none beyond the ordinary authentication failures — a valid connection cannot make this call wrong. | none | read-only | none | no | | `get_job` | One endpoint for every asynchronous action this API starts: a folio request, sending a document by email, submitting a cesión, an RCV sync, a bank-statement import, or a bank-connection sync. Call it with the `job_id` a `202` response handed you. READ `type` FIRST — it decides the shape of `result` and how soon it is worth asking again: about 10s for `document_send`/`statement_import`, 15s for `connection_sync`, 30s for `rcv_sync`/`cesion_submit`, 60s for `folio_request` (the SII’s timbraje is a scripted portal walk end to end; asking sooner only earns a 429). Nothing on this surface can finish faster than about 10 seconds regardless of type — that is how often the worker claims new work. For `rcv_sync`, this narrates exactly what sync\_rcv’s own `job_id` check-back does — call either one, interchangeably. A job created in the OTHER mode (sandbox vs. live) does not exist for this connection: that is `resource_not_found`, not a refusal that would confirm it exists. This is account-level and takes no `empresa` argument — a job belongs to the account, not to one company, and a `Facturia-Empresa` header on it is refused. Likely errors: `resource_not_found` (wrong job\_id, or it belongs to the other mode), `insufficient_scope` (this connection can watch some job types and not others — the scope needed is whichever scope STARTS that kind of job, plus the read scope for what it acts on; a `documents:read`-only grant, for example, cannot watch an `rcv_sync` job). | `job_id` | read-only | `empresas:read` | no | | `list_usage_records` | One record per document the SII actually ACCEPTED — the raw ledger get\_usage rolls up into a summary. A document is recorded the moment it is accepted, never on submission; rejected, failed and sandbox documents never appear here, and a correction is a compensating record with negative quantity, never an edit of the original. This is a report, not a meter: the plan is flat and unlimited, so nothing here is priced per document — read it to reconcile "how many did we actually get billed for" against your own records, never to predict a bill. Omit `empresa` for every empresa this connection can reach; name one to filter to it. Likely errors: `invalid_period` (`period` is not a real YYYY-MM month), `invalid_limit` (1–100). | `empresa?`, `period?`, `metric?`, `cursor?`, `limit?` | read-only | `usage:read` | no | | `get_event` | One event from the account’s own ledger — the exact object poll\_events pages through and a webhook delivery would carry — for a caller reconciling by id (an id quoted in a support ticket, or a webhook payload received out of order) rather than by walking the ledger forward. Account-level: it takes no `empresa` argument. Likely errors: `resource_not_found` (no such event, or it belongs to the other mode), `insufficient_scope` (this connection was not granted `webhooks:read` — the user chooses that on the consent screen and an agent cannot widen its own grant). | `event_id` | read-only | `webhooks:read` | no | | `get_mandato_terms` | The exact text (and version) a representante legal is asked to sign when request\_mandato\_signature mints their link — quote it, or a faithful summary of it, rather than describing the mandato from memory. CHECK `status` FIRST. `draft` means the text has not finished attorney review and is not yet binding — never present a draft as terms someone is about to sign. `annexes[]` carries the documents the contract itself calls «parte integrante» (price, personal data, the automation matrix) and belongs alongside `text` for anyone actually deciding whether to sign. This needs no scope from the grant — `/v1` serves it unauthenticated, the same as the error-code dictionary, because the text is a constant with no tenant data in it. Omit `version` for the text in force today. Pass one from `available_versions` to read a draft of what is coming next, or the exact version an old acceptance recorded (its receipt names it). Likely errors: `invalid_field` (an unknown `version`). | `version?` | read-only | none | no | | `list_webhook_endpoints` | Webhook endpoints configured for the account: URL, which event types each receives, which empresas it is restricted to, and status. Signing secrets are never included. THIS SURFACE ONLY READS. Creating, updating or deleting an endpoint, and reading or rotating its secret, stay on REST and the panel — an agent has no HTTPS endpoint of its own to point one at, and the founder keeps webhook writes off the MCP regardless. Account-level: it takes no `empresa` argument. Likely errors: `insufficient_scope` (this connection was not granted `webhooks:read`). | `cursor?`, `limit?` | read-only | `webhooks:read` | no | | `list_webhook_deliveries` | Delivery attempts for one webhook endpoint, newest first — response status, attempt count, and whether a retry is scheduled. Use it to answer "did our webhook actually fire for that event" rather than guessing from silence on the receiving end. THIS SURFACE ONLY READS: redelivering an event stays on REST and the panel. Likely errors: `resource_not_found` (no such webhook\_endpoint\_id), `insufficient_scope` (this connection was not granted `webhooks:read`). | `webhook_endpoint_id`, `status?`, `event_id?`, `cursor?`, `limit?` | read-only | `webhooks:read` | no | | `get_certification` | The set de pruebas round for a mercado-track empresa: which round is running, whether the SII set file is stored, what inputs the round still needs (`slots[].supplied`), any problems or notices the last build hit, every envío sent so far and its verdict. One read for the whole thing — there is no separate endpoint per envío. `submittable` is null until a build has been attempted at all, which is not the same as false. An entry in `problems` blocks the next round; an entry in `notices` is something the build decided on the empresa’s behalf and is worth mentioning, never a blocker. Likely errors: `empresa_not_allowed`, `422` for a company whose lane has no certification (an SFG-only empresa, or one with no certification\_track). | `empresa?` | read-only | `empresas:read` | no | | `set_certification_inputs` | The set states several things by implication and never spells them out: who issued each purchase in the libro de compras, what the CodRef-2 correction says, which factura invoiced a guía the set marks “facturada”, the factor de proporcionalidad, the SII-issued folio de notificación, and — for a boleta round — the abbreviated unidades and ten everyday boletas. get\_certification’s `slots[]` says which of these THIS empresa’s round still needs. THE MERGE IS PER TOP-LEVEL SLOT: omit a slot to leave it as it is, send `null` to CLEAR it, send a value to REPLACE it whole. Restating `proveedores` restates every one of them — that is both what a form submits and the only way to remove one. NOTHING IS WRITTEN WHEN ANY SLOT IS INVALID, not even the valid ones — a partial write would leave the empresa in a state nobody asked for. This writes facts. It does not run a round, does not contact the SII and does not spend a folio — the round runs on its own schedule; call get\_certification afterwards to see it reflected. Likely errors: `certification_input_invalid` (every problem is listed in `error.details.problems`, not just the first). | `empresa?`, `receptor?`, `giro_recep_emisor?`, `text_correction?`, `proveedores?`, `iva_no_rec_codes?`, `libro_treatments?`, `libro_guias?`, `factor_proporcionalidad?`, `libro_boleta_folio_notificacion?`, `boleta_unidades?`, `boleta_muestras?`, `permiso_usuario_rut?` | writes · reversible · idempotent | `empresas:read` + `empresas:write` | no | | `list_onboarding_evidence` | Cláusula 4.4 of the mandato obliges Facturia to render account of every act performed on this empresa’s behalf: every probe, envelope, verdict, signature and authorization, newest first, with the date it happened and the date it will be purged (six years out — Código Tributario prescripción extraordinaria). Filter with `step` or `kind` to find one act. FOR THE WHOLE HISTORY AS A DOWNLOADABLE FILE: `GET …/onboarding/evidence/export` answers a raw CSV file with no public-link mechanism, so this server does not expose it as a tool — page through this list instead, or tell the user to ask a person with direct API access for the export. Likely errors: `empresa_not_allowed`, `invalid_cursor`, `invalid_limit`. | `empresa?`, `step?`, `kind?`, `cursor?`, `limit?` | read-only | `empresas:read` | no | | `get_certificate` | Metadata about the digital certificate (.pfx) in Facturia’s custody for this empresa — never the material itself, which this API never returns to anybody. Subject, issuer, expiry, status, and which RUTs the SII lists it as an authorized user of. `null` means no certificate has been uploaded yet — that is an answer, not an error; upload\_certificate mints the page for whoever holds the file. Likely errors: `empresa_not_allowed`. | `empresa?` | read-only | `empresas:read` | no | | `get_sfg_credential` | Whether Facturia holds the SII «certificado centralizado» clave for an SFG-channel empresa, and since when — never the value, which this API cannot return to anybody, not even the person who set it. Only meaningful for a LIVE `emission_channel: sfg` empresa; a `mercado` or sandbox one has nothing this could apply to. Likely errors: `not_applicable_for_channel` (this empresa emits through `mercado`, which signs with its own certificate and never asks the SII to sign), `not_applicable_in_test_mode`. | `empresa?` | read-only | `empresas:read` | no | | `delete_certificate` | Permanently destroy the digital certificate (.pfx) Facturia holds for this empresa. EMISSION STOPS the moment this runs — the empresa is walked back to `certificate_pending` and nothing can be signed until a new one is uploaded. This is cláusula 7.3’s custody-ending right, and it is irreversible: the bytes are destroyed, not archived. REQUIRES `confirm_delete: true`. Without it, this tool refuses before making any call, precisely because there is no undo once it runs. Likely errors: none blocking — this always succeeds (204) whether or not a certificate existed, because "you hold nothing of mine" is the same outcome either way. | `empresa?`, `confirm_delete?` | writes · irreversible · idempotent | `empresas:read` + `empresas:write` | no | | `delete_sfg_credential` | Permanently destroy the SII «certificado centralizado» clave Facturia holds for this SFG-channel empresa. The next SFG emission will refuse for want of a signing clave until a new one is set — unlike deleting the certificate, this does NOT walk the empresa’s onboarding backwards, because the clave can be re-supplied in one call. Irreversible: the bytes are destroyed. REQUIRES `confirm_delete: true`. Without it, this tool refuses before making any call. Likely errors: `not_applicable_for_channel`, `not_applicable_in_test_mode`. | `empresa?`, `confirm_delete?` | writes · irreversible · idempotent | `empresas:read` + `empresas:write` | no | | `revoke_onboarding_link` | Revoke the hosted page a previous mint\_onboarding\_link call minted for one step, before it expires on its own. Use it if the wrong link went out, or the person it was for should no longer have it. Succeeds (204) whether or not a live link existed — "nobody can use this any more" is the same outcome either way. This does not touch the step’s status: a person can still be walked through it again with a fresh mint\_onboarding\_link call. The mandato has no link here: request\_mandato\_revocation\_link is the mandato’s own revocation door, for the SIGNED mandato itself — a different thing from a hosted link. Likely errors: none blocking in normal use. | `empresa?`, `step` | writes · reversible | `empresas:read` + `onboarding:write` | no | | `request_mandato_revocation_link` | Mint the hosted page where the representante legal exercises cláusula 7.1: revoking the mandato at any time, without cause and without penalty. This tool only MINTS the page — the revocation itself is the representante legal’s own act, gated by their RUT as recorded on the mandato they signed, and this tool cannot perform it and cannot see whether they have. REVOKING STOPS FACTURIA FROM ACTING FOR THIS EMPRESA AT THE SII: every queued document is cancelled and the empresa is suspended the moment the page is completed. Say that plainly when you hand the link over — do not present it as a routine settings change. The link lives about a century, because the right it addresses never expires; it is returned EXACTLY ONCE, so relay it verbatim. Likely errors: `mandato_not_signed` (there is no mandato in force to revoke — get\_mandato says the current state), `mandato_revocation_link_unavailable` (the mint failed; the mandato still stands, retry). | `empresa?` | writes · reversible · idempotent | `empresas:read` + `mandato:write` | no | None of these mark a step done, and that is not an omission: a step advances when we read the fact from the SII, never because someone said so. `get_pending_human_action` is the one to call when an issuance fails with `empresa_not_emitting` — it returns the full handoff, written for the person who has to act, who very often is **not** the account holder. Relay it as written: if a `caveat` comes back, that sentence goes too. ## Sandbox [#sandbox] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------- | ---------------------------------- | ------------- | | `sandbox_set_onboarding_step` | REHEARSAL ONLY, and only for a SANDBOX company: force one onboarding step into a state, so you can practise the handoff before doing it with somebody’s real company. A sandbox empresa is born `emitting` with every step skipped, which means the most important thing this surface does — read what a person must do, mint the link, ask for the recheck — has no sandbox to be tried in without this. Set `mandato` to `action_required` and get\_pending\_human\_action returns the real mandato handoff; set `certificate` to `failed` and see what a failed step reads like. `completed` and `skipped` work too. IT ASSERTS A STATE, IT DOES NOT RUN ANYTHING. Nothing is verified, nothing is sent to the SII (a sandbox company never talks to it at all), and no real onboarding works this way: outside the sandbox a step advances only when we read the fact off the SII. Do not let a rehearsal teach you to describe the real thing as something an agent can move. A LIVE COMPANY IS REFUSED, here and again by the API. There is no version of this that touches real data. Likely errors: `sandbox_only` (you named a live company), `invalid_field` (not a step on this company’s checklist, or not a status the step model has). | `empresa?`, `step`, `status`, `tipo_dte?` | writes · reversible | `empresas:read` + `empresas:write` | no | | `sandbox_advance_clock` | REHEARSAL ONLY, and only for a SANDBOX company: move its clock forward by whole days, so deadlines that are days away can be reached in a conversation. THE CASE THIS EXISTS FOR is the 8-day acuse window on a received document. A supplier document injected into the sandbox sits at day 8 forever without this, so «you have 2 days left to respond» — the thing the reception tools are for — could not be seen at all before emitting for real. The offset is ADDITIVE and cumulative: advancing 5 days twice lands 10 days out, and real time keeps passing underneath. It cannot be moved backwards; a company whose clock has gone too far is one to recreate rather than rewind. A LIVE COMPANY IS REFUSED, here and again by the API — a clock that could be moved on real data would be a way to walk a real deadline past its date. Likely errors: `sandbox_only` (you named a live company), `invalid_field` (`days` must be a whole number from 1 to 400). | `empresa?`, `days` | writes · reversible | `empresas:read` + `empresas:write` | no | Both of these are REHEARSAL levers and touch test companies only: the API refuses them on a live empresa and so do the tools, before calling it. They exist because a sandbox empresa is born `emitting` with every step skipped, so the handoff — the most important thing this surface does — could not be tried without a live company and the real SII. Forcing a step ASSERTS a state; it does not advance anything. Outside the sandbox a step moves when we read the fact off the SII, never because somebody said so. ## undefined [#undefined] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------- | -------------------------------- | ------------- | | `request_folios` | Request a new folio range for one document type — the timbraje, which is a REAL act at the SII against a finite resource, not a counter we increment. Every document consumes a folio; when a tipo runs out, `no_folios_available` is what the emission tools answer. CHECK get\_folio\_status FIRST. A tipo reporting `ok` needs nothing, and a tipo reporting `low` needs nothing either: replenishment below the company’s own threshold is AUTOMATIC (`auto_request_folios`), so asking again by hand while the automatic request is already in flight registers a SECOND range that nobody will use. Call this when the user explicitly asks for folios, or when `exhausted` is blocking emission and nothing has been requested. AN UNUSED RANGE IS NOT FREE: it must be annulled, and enough unused ranges block further timbraje for the company. So one call, then wait — never a loop. IT TAKES MINUTES, NOT SECONDS: the SII has no timbraje webservice, so this is a scripted walk through its portal. You get a `job` back; check on it by calling this tool again with `job_id`, and wait 60 seconds between checks (that is the `Facturia-Poll-After` the API publishes for this job type). On `succeeded` the job’s result names the range actually registered — say the `from`–`to` folios and the caf\_id, and note that `quantity` there is what the SII GRANTED, which can be smaller than what was asked for. On `failed` read `range_registered`: if it is TRUE a range was already registered before the failure, so CALLING AGAIN WOULD ASSIGN ANOTHER ONE — do not retry, say what happened and let a person look. If it is false, the failure is safe to retry once the message says why. A sandbox company answers `succeeded` immediately over its virtual range (1–1.000.000) and nothing is asked of the SII; say that rather than reporting a timbraje that never happened. A company on the SII free portal (`emission_channel: sfg`) has no timbraje at all and is refused with `not_applicable_for_channel` — relay that refusal, it is the truth and not a bug. An `insufficient_scope` naming `folios:write` means this CONNECTION predates the scope: the user has to disconnect Facturia in their MCP client and connect it again, ticking «Pedirle folios al SII y administrar tus CAF». No panel screen widens an existing grant. Likely errors: `not_applicable_for_channel` (sfg company — there is nothing to request), `invalid_tipo_dte`, `subscription_required` (`error.details.checkout_url` is the page the ACCOUNT HOLDER opens — pass the link on verbatim). | `empresa?`, `tipo_dte?`, `quantity?`, `job_id?` | writes · irreversible | `empresas:read` + `folios:write` | no | | `list_cafs` | The folio authorizations (CAF) on file for this company: one row per range, with its tipo\_dte, its first and last folio, whether it is still active, and whether it came from a timbraje we ran (`source: sii_request`) or from a file somebody uploaded (`source: upload`). Use get\_folio\_status instead when the question is "can I emit" — that one reports remaining stock per tipo. This one answers "which ranges exist", which is what you need before deactivating one or before deciding whether a range is already on file. A company on the SII free portal (`emission_channel: sfg`) holds NO CAF — not "none yet", none ever, because the SII assigns each folio at emission. An empty list from an sfg company is the truthful answer, not a gap. Likely errors: `empresa_required`, `empresa_not_allowed`. | `empresa?` | read-only | `empresas:read` | no | | `upload_caf` | Register a CAF file the company downloaded from the SII itself, so Facturia can draw folios from that range. The alternative to request\_folios: this one registers a range the SII ALREADY granted, and asks the SII for nothing. Pass the file’s bytes as `caf_xml_base64` (base64 of the raw `<AUTORIZACION>` XML), or the XML itself as `caf_xml`. READ THE FILE, DO NOT ASK A PERSON TO PASTE IT: a CAF carries `<RSASK>`, the private key that signs the timbre of every document drawn from the range, so it belongs in a file the user points you at — never in the conversation, never echoed back, never quoted in your answer. Nothing of the file is repeated in this tool’s result. The range is the identity: re-registering a file already on file returns the existing range rather than a second one, so a retry after a timeout is safe. A new range never lowers the folio counter — it waits until the range in use is spent, so uploading early does not abandon folios you paid for. Refused for a sandbox company (`not_applicable_in_test_mode`): sandbox folios come from a virtual range no CAF backs, so a real CAF stored against one could never be drawn from — and the file is live SII key material. Refused for an sfg company for the same reason list\_cafs returns nothing there. An `insufficient_scope` naming `folios:write` means this CONNECTION predates the scope: the user disconnects Facturia in their MCP client and connects it again, ticking «Pedirle folios al SII y administrar tus CAF». A grant cannot be widened in place. Likely errors: `caf_belongs_to_another_empresa` (the CAF names a different RUT — every document drawn from it would be rejected AFTER the folio was spent), `not_applicable_in_test_mode`, `not_applicable_for_channel` (sfg company), `invalid_field` (not a CAF, or one that is not usable — the message says which). | `empresa?`, `caf_xml_base64?`, `caf_xml?` | writes · reversible · idempotent | `empresas:read` + `folios:write` | no | | `deactivate_caf` | Stop Facturia from drawing new folios from one CAF range. History is preserved — documents already emitted from it keep their folios, and the range is not deleted — but the range stops offering folios and THIS CANNOT BE UNDONE from here. Only do this when the user asked for it by name: a range with folios left that gets deactivated is authorized, paid for, and now unusable, and the folios in it still have to be annulled at the SII. Call list\_cafs first and tell the user which range you are about to switch off, by its folio numbers. Requires `confirm_deactivate: true` in the arguments — a deliberate second act, because the first one is not reversible. An `insufficient_scope` naming `folios:write` means this CONNECTION predates the scope: the user disconnects Facturia in their MCP client and connects it again, ticking «Pedirle folios al SII y administrar tus CAF». Nothing in the panel widens an existing grant. Likely errors: `resource_not_found` (no CAF with that id belongs to this company — list\_cafs has the real ids), `empresa_not_allowed`. | `empresa?`, `caf_id`, `confirm_deactivate?` | writes · irreversible · idempotent | `empresas:read` + `folios:write` | no | ## undefined [#undefined-1] | Tool | Description | Inputs | Annotations | Scopes | Confirmation? | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ---------------------------------- | ------------- | | `get_empresa` | The empresa exactly as /v1 holds it: onboarding status, which SII environment it emits into, what it can actually emit RIGHT NOW, and any resolución it has claimed or proven. list\_empresas shows the same row across every company on this connection; call this one when you need the full detail for a single empresa, or to re-read it after update\_empresa. Read `capabilities.tipos` for the emission TRUTH — `doc_types` is only the declaration, and the two can legitimately disagree while certification is in progress. `resolucion.verified` (and `resolucion_boletas.verified`) is what actually matters when this empresa arrived claiming a resolución: `false` means the claim is still unproven, however confident it sounded when it was declared. Likely errors: `empresa_not_allowed` (this connection cannot reach the company), `empresa_required` (ambiguous and no default set). | `empresa?` | read-only | `empresas:read` | no | | `update_empresa` | Update an existing empresa’s stated facts: `legal_name`, `giro`, `acteco`, `address`, `codigo_sucursal`, `contact_email`, `doc_types`, `certification_track`, `origen`, `emission_channel`, `resolucion`, `resolucion_boletas` or `status`. Every field is optional — send only what changes; everything else is left exactly as it was. `ambiente` cannot be sent here: it is derived, never stated. THIS IS THE DOOR TO DECLARE AN EXISTING SII RESOLUCIÓN, for an empresa that already invoices electronically through another provider. `resolucion.numero` (NroResol) and `resolucion.fecha` (FchResol) are printed on the carátula of any invoice it has ALREADY issued — read them off a recent one rather than sending anyone to Mi SII. DECLARING SKIPS NOTHING BY ITSELF: the empresa still needs its certificate and mandato, and `resolucion.verified` on the response stays `false` until Facturia proves the claim by asking the SII to timbrar one declared document type. NEVER tell the user the resolución is confirmed from this call — call get\_empresa afterwards and read `verified`. `resolucion_boletas` is the same claim for boleta electrónica: a date, no número, sent alongside `doc_types` containing 39 or 41. Send `null` for either to clear it. `emission_channel` IS A REAL ROUTING CHANGE, not a preference update: moving to `sfg` sets `ambiente` to `produccion` in the same write and switches which checklist, which folios and which driver govern this empresa from now on. Narrate the BEFORE and AFTER from the response, not just that the call succeeded. `doc_types` is the DECLARATION, never the emission truth — call get\_empresa and read `capabilities.tipos` for what can actually be emitted today. Adding 39/41 re-opens certification for boleta on `mercado`; sending them on `sfg` (or moving to `sfg` while they are declared) is refused, because the free portal has no boleta form. Likely errors: `invalid_field` (a value the SII layer refuses, or `doc_types` sent empty), `doc_type_not_supported_on_channel` (39/41 on `sfg`), `empresa_not_allowed`. | `empresa?`, `legal_name?`, `giro?`, `acteco?`, `address?`, `codigo_sucursal?`, `contact_email?`, `doc_types?`, `certification_track?`, `origen?`, `emission_channel?`, `resolucion?`, `resolucion_boletas?`, `status?` | writes · reversible · idempotent | `empresas:read` + `empresas:write` | no | | `get_automation_settings` | Every automatic-write switch this empresa has: auto-emitting tipo 46 documents, auto-filing the F29, auto-requesting folios, auto-accepting received documents, and `require_confirmation` — whether a person has to approve an agent-initiated write on this empresa before it runs. Everything defaults OFF except `auto_request_folios`. Likely errors: `empresa_not_allowed`. | `empresa?` | read-only | `empresas:read` | no | | `update_automation_settings` | Turn an automation on or off, or tune its threshold. Send only the fields that change. AN AGENT MAY NOT TURN `require_confirmation` OFF THROUGH THIS TOOL. That flag exists so a PERSON, not an agent, is the record that this empresa’s money-moving writes may run unattended — sending `require_confirmation: false` is refused right here, before anything reaches Facturia, no matter who is asking or why. Turning it ON is fine and safe at any time. To turn it OFF, a person does it from the panel themselves. `auto_accept_received` needs `auto_accept_received_after_days` (1–7) the first time it is turned on, in the SAME call. `auto_file_f29` files a binding declaration before day 20 with nobody reviewing it — check `auto_file_f29_max_payable` before assuming that is safe for this company. Likely errors: `unknown_setting` (not a field this object has), `invalid_field`, `invalid_automation_settings` (a combination the domain refuses, e.g. `auto_accept_received` with no day count). | `empresa?`, `auto_emit_46?`, `auto_emit_46_max_amount?`, `auto_file_f29?`, `auto_file_f29_day?`, `auto_file_f29_max_payable?`, `auto_request_folios?`, `auto_request_folios_threshold?`, `auto_accept_received?`, `auto_accept_received_after_days?`, `require_confirmation?`, `rcv_sync_frequency?`, `auto_characterize_purchases?`, `honor_estado_contable_override?` | writes · reversible · idempotent | `empresas:read` + `empresas:write` | no | | `get_branding` | The logo, footer text, and whether a factura defaults to cedible, that this empresa’s documents render with. `default_pdf_format` only ever reads `carta` — no thermal layout exists on this API. Likely errors: `empresa_not_allowed`. | `empresa?` | read-only | `empresas:read` | no | | `update_branding` | Set the logo URL, footer text, or whether a factura (tipo 33/34) defaults to cedible. `logo_url` MUST already be a URL this empresa hosts — this tool never accepts image bytes and a result here never inlines one; if the user only has a file, they need to host it somewhere first and give you the address. `default_pdf_format` accepts only the literal `carta`. Likely errors: `unknown_setting`, `invalid_field` (a `default_pdf_format` other than `carta`). | `empresa?`, `logo_url?`, `default_pdf_format?`, `cedible_by_default?`, `footer_text?` | writes · reversible · idempotent | `empresas:read` + `empresas:write` | no | | `list_counterparties` | The counterparties this empresa has emitted a factura, nota or guía to (tipos 33, 34, 56, 61 — a boleta’s receptor is a consumer and never appears here), each with whatever WhatsApp contacts are already on file. DERIVED from the documents already emitted, not a stored list: a counterparty this empresa stops invoicing simply stops appearing here, and one it has never invoiced can still carry a contact. Use it to find the `rut` for list\_counterparty\_contacts and set\_counterparty\_contact. NOT PAGINATED: the window is a bound on the last several hundred documents scanned, not a page of results — `has_more` is always false. Likely errors: `empresa_not_allowed`. | `empresa?` | read-only | `empresas:read` | no | | `get_counterparty` | The stored record for ONE counterparty RUT: razón social, giro and address, plus `source` (where each came from — the address book itself, a prior emission, or nothing yet) and when it was last updated. THIS IS WHAT A FACTURA’S `<Receptor>` BLOCK IS BUILT FROM. `GiroRecep`, `DirRecep` and `CmnaRecep` are optional in the SII’s schema and mandatory to its business validation: a document missing them is ACCEPTED as an envelope and then REFUSED as a document, with the folio spent either way. Which of them a given document actually needs depends on the tipo and the channel, and the emission is the authority on that — it names anything still missing as `receptor_incomplete` before a folio is allocated. Read this before promising a user their invoice will go out, and use update\_counterparty to fill anything blank. list\_counterparties is the DERIVED list of who has been invoiced; this is the STORED record of who they are, and a counterparty can have one without the other. Likely errors: `invalid_rut`, `resource_not_found` (nothing on file for that RUT yet — which is a normal state, not a fault), `empresa_not_allowed`. | `empresa?`, `counterparty_rut` | read-only | `empresas:read` | no | | `update_counterparty` | Write down what a counterparty’s razón social, giro and address ACTUALLY are, so every future document to that RUT carries them without anybody being asked again. Creates the record if there is none. Send only what you know; anything omitted is left exactly as it was. THIS IS THE ANSWER TO `receptor_incomplete`. A factura whose receptor has no giro/dirección/comuna is accepted by the SII as an envelope and then refused as a document — the folio is spent either way, and seven production folios were lost that way on 2026-09-08. Facturia refuses such a request before allocating a folio and names the missing fields; store them here (once) and the emission goes through. NEVER INVENT, GUESS OR INFER ANY OF THIS. Not from the company name, not from a web search, not from another counterparty with a similar name. A wrong dirección on a factura is a defect in a legal tax document. Everything you send must have come from the user or from a document they gave you. THE CAPS ARE REFUSALS, NOT TRUNCATIONS: giro 40 characters, dirección 70, comuna 20, ciudad 20, razón social 100. A longer value is refused right here, before anything is written — because a shortened name of trade changes what the document says. If a real giro is longer than 40, ask the user for their own shorter form of it. Likely errors: `invalid_field` (a value past its cap, or nothing to change), `invalid_rut`, `empresa_not_allowed`. | `empresa?`, `counterparty_rut`, `legal_name?`, `giro?`, `address?` | writes · reversible · idempotent | `empresas:read` + `empresas:write` | no | | `list_counterparty_contacts` | Every contact this empresa has recorded for one counterparty RUT — the address book, primary first. `notifiable` says whether a contact would actually be messaged (primary, opted in, not opted out); v1 notifies ONLY the primary. Likely errors: `invalid_rut`, `empresa_not_allowed`. | `empresa?`, `counterparty_rut` | read-only | `empresas:read` | no | | `set_counterparty_contact` | This is “who do I message about this invoice” for a counterparty: create a contact by omitting `contact_id`, or edit one by passing it. The FIRST contact for a counterparty becomes primary automatically unless you say otherwise; only the primary is ever notified. THE OPT-IN IS AN ATTESTATION, NOT A FORMALITY: pass `opt_in: true` and `opt_in_method` only when you have an actual basis for it — WhatsApp’s own policy is that merely holding a number is not consent. Omitting it stores the contact as an address-book entry that is never messaged, which is a legitimate and common state, not an error to fix. AN OPT-OUT IS ONE-WAY: pass `opted_out: true` only to RECORD one a person told you about through another channel. It can never be un-set through this tool, and a number that opted out can never be re-added under a new contact — that refusal is the point. Likely errors: `invalid_field` (creating without `name` or `phone`), `invalid_phone` (not E.164, e.g. `+56912345678`), `optin_method_required` (`opt_in: true` with no method), `contact_opted_out` (this number, or this contact, cannot be written to), `resource_not_found` (bad `contact_id`). | `empresa?`, `counterparty_rut`, `contact_id?`, `name?`, `phone?`, `counterparty_name?`, `primary?`, `opt_in?`, `opt_in_method?`, `opt_in_attested_by?`, `opted_out?` | writes · reversible | `empresas:read` + `empresas:write` | no | | `delete_counterparty_contact` | Delete a contact from a counterparty’s address book. A HARD DELETE with no undo — and it cannot be used to work around an opt-out: a contact that already opted out is refused (`contact_opted_out`), because the tombstone row is what keeps that number from being re-added later. Likely errors: `contact_opted_out` (this contact opted out and its record is kept on purpose). | `empresa?`, `counterparty_rut`, `contact_id` | writes · irreversible · idempotent | `empresas:read` + `empresas:write` | no | ## How files come back [#how-files-come-back] MCP gives a tool two ways to return a file: embedded as a base64 blob, or as a `resource_link` the client only fetches if and when it needs the bytes. **Facturia MCP always uses `resource_link`, never embedded blobs.** ```jsonc // → get_document_file { "empresa": "77928532-4", "document_id": "doc_01K2R7Q4XW9M3B8ZC5YHTVJD6N", "format": "pdf" } // ← contenido del resultado [ { "type": "text", "text": "Factura 33 folio 412, enlace válido 10 minutos." }, { "type": "resource_link", "uri": "https://ver.facturia.cl/l/3d9f2b8a1c?exp=1755184211", "name": "factura-33-412.pdf", "mimeType": "application/pdf" } ] ``` Three reasons: * **Context economy.** A base64-embedded signed PDF can be hundreds of kilobytes of content injected into the model's context for a document nobody asked to have read aloud. A link costs one sentence. * **Client compatibility.** The REST API's `pdf_url` / `xml_url` URLs require a key. Most MCP hosts have no way to attach Facturia's OAuth token when resolving a resource link — nor should they have to. * **Safer than the REST default.** `get_document_file` switches on the mechanism REST already publishes as opt-in (signed public links, with an expiry, unauthenticated) and shortens its life to **10 minutes**, scoped to a single document and never listable. ## What is not on the agent surface [#what-is-not-on-the-agent-surface] Requesting a CAF, managing webhooks, branding and RCV characterisation writes stay REST- and dashboard-only: they are one-off or rare actions with a UI far better than a chat turn, and dynamic client registration means the calling software is not verified the way a dashboard session is. There are also no void (`credit_note`) or retry (`retry`) tools in this cut. Besides tools, this server also publishes a **prompt** (`onboard_empresa`, the full onboarding walkthrough) and **resources** that point at this documentation and at the site's `llms.txt`, so an agent can discover them from inside the connection. # Two-step confirmations (/en/docs/mcp/confirmaciones) ## The default: direct execution [#the-default-direct-execution] When an agent calls `emit_document`, it emits. Facturia does not interpose an approval step of its own, because the MCP client's tool-approval UI — the thing that shows you "Claude wants to call `emit_document`, allow?" — **is** the human in the loop. Adding a second Facturia-side confirmation on top of that, for every write, would be redundant on the vast majority of calls and would defeat half the point of an agent interface. ## Where that is not enough [#where-that-is-not-enough] There are two distinct failure modes, and the client's approval only solves one: **The agent tries to do something the person did not ask for.** The client's own approval step solves it — that is exactly what it exists for. **The person reflexively approves everything their chat client asks them, and something with real financial consequence — filing a tax return, an irreversible claim — goes out because someone clicked "allow" on autopilot and not because they deliberately reviewed that figure.** The client's approval does **not** solve this: it is the same click in both cases. `require_confirmation` attacks the second. ## Turning it on [#turning-it-on] It is a per-empresa flag in `automation_settings`, next to `auto_emit_46` and friends. It ships **off**, like every automation. ```bash curl -X PATCH https://api.facturia.cl/v1/empresas/emp_01K2R6.../automation_settings \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Content-Type: application/json" \ -d '{"require_confirmation": true}' ``` It is a REST field, not an MCP-only setting: REST callers and the dashboard see and control the same flag. The REST API described in the rest of these docs does not change behavior because you turned it on. ## Which tools it covers [#which-tools-it-covers] `emit_document`, `file_f29`, `emit_tipo46` and `reclaim_received_document` — the "emit / file / claim" set. Every other write (`accept_received_document`, for instance) stays direct even with the flag on: accepting already matches the legal default that silence would produce anyway, so putting a lock on it protects nothing. ## What it looks like [#what-it-looks-like] ```jsonc // → file_f29 { "empresa": "77928532-4", "period": "2026-07", "confirm_total_payable": 1373000 } // ← pending_confirmation (la empresa tiene require_confirmation: true) { "status": "pending_confirmation", "confirmation_id": "conf_01K2SD3P7Q", "confirmation_url": "https://app.facturia.cl/aprobar/conf_01K2SD3P7Q?token=mcpapv_9lQpV3fA…", "action": "file_f29", "summary": "File F29 for período 2026-07 — CLP 1.373.000 payable, due 2026-08-20.", "expires_at": "2026-08-14T14:45:00Z" } ``` Nothing was filed. The agent hands you the summary and the link, and stops there. ## Who completes it: you, on a page [#who-completes-it-you-on-a-page] You open `confirmation_url`, see which empresa, which action and with what figure, and press **Confirm** or **Reject**. No sign-in needed: the link carries a single-use token, valid for those 15 minutes and for that action and no other. Only then is the F29 (Chile's monthly VAT return) filed — with exactly the data that was summarized when the agent proposed it, not with whatever the world looks like at that moment. > **The agent cannot complete it, and that is the point** > > There is no MCP tool that closes a pending action. There was one — `confirm_action` — and it was > withdrawn: if the agent can complete it by saying "the user told me yes", then the record of your > approval is the agent's word for it, which is exactly what this flag exists to refuse. If you tell > the agent you already approved, have it verify by reading (`get_document_status`, or `file_f29` with > a `filing_id`). Confirming twice replays the original result instead of executing twice — the same guarantee `Idempotency-Key` gives a REST caller. Rejecting twice is not an error either: you wanted it not to happen, and it is not happening. An approval expires after **15 minutes** unclaimed. Once expired, the page says so and the agent has to propose the action again to mint a new one. ## Sandbox: this is where you rehearse it [#sandbox-this-is-where-you-rehearse-it] The two-step **works the same in sandbox**: turn `require_confirmation` on for a test empresa and the emission is parked, the link is minted, and the `/aprobar` page opens exactly as it does live — marked as a test, so nobody mistakes the rehearsal for the real thing. > **This could not be rehearsed before** > > Until September 2026 the sandbox ignored the flag: the API accepted it, reported it as on, and > emitted directly anyway. Which meant the most consequential bridge in the product — the one where an > agent hands a decision to a person — could only be seen for the first time with real documents. Now > it can be exercised at zero risk. > > The sixty-second demo is unchanged: the flag defaults to **off**, so a freshly created sandbox > empresa still issues in a single call. ## The states that are not errors [#the-states-that-are-not-errors] Two behaviors of this layer worth being clear about in the client: * **A rate limit and a pending confirmation are not failures.** The equivalent of a `429` and the `pending_confirmation` responses are ordinary tool *results* with a clear `status` field, not thrown errors. An agent should read them and decide what to say next, not treat them as something broken. * **Every tool description declares its own likely failure modes.** An agent choosing between `emit_document` and `draft_document`, or deciding whether it is safe to retry after a `sii_error`, should never have to consult external documentation mid-conversation. That is why `retriable` travels with every rejection reason.