# Para agentes (/docs/agentes) Facturia es la API de facturación electrónica multiempresa de Chile: emisión de DTE (facturas, boletas, notas de crédito y débito), recepción de documentos de proveedores, la suite tributaria (RCV, F29) y webhooks firmados, con un servidor MCP para que un agente la opere directo. Sandbox gratis para siempre, sin tarjeta ni certificado. ## El camino en cinco pasos [#el-camino-en-cinco-pasos] **1. Arranque en frío, una sola llamada.** `POST /account/register` con `bootstrap.sandbox_key` y opcionalmente `bootstrap.empresa` devuelve, en la misma respuesta, la cuenta, una llave `sk_test_` y `data.next` — la llamada exacta que sigue, con método, URL y headers. Nunca hace falta confirmar un correo para llegar hasta acá. Contrato completo: `https://docs.facturia.cl/openapi-bootstrap.yaml`. **2. Emite en el sandbox.** `POST /v1/documents` con `Authorization: Bearer sk_test_…` y `Facturia-Empresa: ` crea un documento sin tocar el SII: `folio: null`, aceptado en menos de un segundo, PDF y XML al toque. La llave de arranque trae los seis scopes que recorren esto entero: `documents:read`, `documents:write`, `empresas:read`, `empresas:write`, `webhooks:read`, `webhooks:write`. `keys:write` es aparte (una llave root, solo mintea otras llaves) y los cuatro scopes de puesta en marcha (`mandato:write`, `mandato:attest`, `onboarding:write`, `onboarding:attest`) nunca vienen por defecto: se piden explícitamente cuando hacen falta. **3. Producción es una máquina de estados que se lee con un GET.** `GET /v1/empresas/{empresa_id}/onboarding` contesta siempre con la lista de pasos que faltan y, si el que sigue necesita a una persona, un `pending_action`. No hay que adivinar el flujo completo: cada lectura dice el siguiente paso, uno a la vez. **4. El patrón de handoff.** Cuando `pending_action` aparece, dice **quién** (`performed_by.role`: `representante_legal`, `usuario_administrador`, `titular_certificado` o `titular_cuenta` — un rol, nunca una persona), **dónde** (`action_url`, o `link.mint` con `{method, url, body}` si hay que acuñar el enlace primero), **qué hacer** (`instructions[]`, en español, textual — no la parafrasees) y **cómo nos enteramos** (`verification.recheck`: llámalo cuando la persona termine y volvemos a mirar). Relaya el objeto entero a tu principal; nunca resumas ni inventes un campo que no vino. **5. Por MCP.** `https://mcp.facturia.cl/mcp` expone las mismas capacidades como herramientas, con OAuth para la conexión y consentimiento explícito por scope. El descubrimiento vive en `/.well-known/oauth-protected-resource`. ## Las dos reglas [#las-dos-reglas] * **Sigue `next` (o `pending_action`/`link.mint`/`recheck`), nunca construyas la URL a mano.** Cada respuesta trae la llamada exacta que sigue — método, URL absoluta, headers y a veces el cuerpo — y eso es lo único que hay que ejecutar. Adivinar una ruta o inventar un campo es exactamente el error que este patrón existe para evitar. * **Relaya `pending_action` verbatim.** El `title`, las `instructions[]` y el `performed_by.role` son la comunicación con una persona real que va a hacer un trámite ante el SII; resumirlos, traducirlos de más o saltarte el rol correcto le entrega a la persona equivocada una instrucción distinta de la que a nosotros nos consta que es correcta. ## Para seguir [#para-seguir] * [OpenAPI 3.1 — arranque en frío](https://docs.facturia.cl/openapi-bootstrap.yaml): el contrato del paso 1. * [OpenAPI 3.1 — /v1](https://docs.facturia.cl/openapi.yaml): el contrato completo. * [Quickstart](https://docs.facturia.cl/docs/empezar/quickstart): los pasos 1 y 2, con las respuestas completas. * [De sandbox a producción](https://docs.facturia.cl/docs/empezar/produccion): los pasos 3 y 4, con cada handoff documentado. * [Servidor MCP](https://docs.facturia.cl/docs/mcp): qué ve un agente que se conecta por MCP. * [Diccionario de errores](https://docs.facturia.cl/docs/errores/codigos): todos los códigos, su causa y si conviene reintentar. La API sirve el mismo diccionario en `GET /v1/error_codes`. * [Todo lo anterior en un solo archivo](https://docs.facturia.cl/llms-core.txt): quickstart, producción, autenticación y MCP, sin las \~200 páginas de códigos de error. # Quickstart (/docs/empezar/quickstart) Todo lo de esta página ocurre en **modo test**: no hay tráfico al SII, no hay certificado, no hay CAF y no hay costo. Cuando cambies `sk_test_` por `sk_live_`, la misma llamada golpea el SII real. ## 1. Consigue tu primera llave [#1-consigue-tu-primera-llave] Una cuenta recién creada no tiene ninguna llave todavía. El camino más corto es **una sola llamada**: registrarte y recibir la llave de sandbox en la misma respuesta. ```bash curl -sS https://api.facturia.cl/account/register \ -H "Content-Type: application/json" \ -d '{ "email": "tu@empresa.cl", "password": "una-contraseña-larga", "bootstrap": { "sandbox_key": { "name": "primera llave" }, "empresa": { "rut": "76000002-7", "legal_name": "Ejemplo SpA" } } }' ``` ```json { "status": 201, "data": { "token": "eyJhbGciOi…", "api_key": { "object": "api_key", "id": "key_01K2R7…", "secret": "sk_test_9lQpV3fA2mKcRt7wZxYb1Nde" }, "empresa": { "object": "empresa", "id": "emp_01K2R7…", "rut": "76000002-7", "mode": "test" }, "next": { "action": "emit_sandbox_document", "method": "POST", "url": "/v1/documents", "headers": { "Authorization": "Bearer ", "Facturia-Empresa": "76000002-7" } } } } ``` `data.api_key.secret` es la llave y **se devuelve exactamente una vez**. Siempre es `sk_test_`: no hay variante live de este bloque y no la va a haber, porque mintear una llave live es justamente el punto donde la verificación de correo tiene que apretar. `data.next` nombra la llamada que sigue, con headers y todo; un valor entre `<…>` es un campo de esta misma respuesta que hay que sustituir. Con `bootstrap.empresa` te saltas el paso 2. Si el RUT ya está tomado en otra cuenta, la cuenta y la llave se crean igual y `data.empresa_error` dice por qué no hubo empresa — reportado, no lanzado. ```bash export FACTURIA_KEY=sk_test_9lQpV3fA2mKcRt7wZxYb1Nde export EMPRESA=76000002-7 ``` > **Registrarte no exige confirmar el correo** > > La verificación cierra exactamente una puerta: mintear una llave **live**. El modo test, y todo lo > que hay dentro, está abierto de inmediato — un agente puede llegar desde esta llamada hasta un DTE de > sandbox emitido sin que nadie abra un buzón. Cuando llegue el momento de la llave live, un > `403 email_verification_required` te dice qué falta. ### La forma en dos llamadas [#la-forma-en-dos-llamadas] Sigue existiendo, y es la que necesitas si la cuenta ya existe o si quieres elegir los scopes. La salida es la **sesión de cuenta**: el mismo JWT de una hora que devuelve `POST /account/login`, presentado a la superficie del panel. ```bash # Iniciar sesión (o registrarte sin `bootstrap`). No necesitas un navegador. TOKEN=$(curl -sS https://api.facturia.cl/account/login \ -H "Content-Type: application/json" \ -d '{"email":"tu@empresa.cl","password":"..."}' | jq -r .data.token) # Mintear la llave con la sesión, no con una llave que todavía no tienes. curl -sS https://api.facturia.cl/dashboard/api_keys \ -H "Authorization: Bearer $TOKEN" \ -H "Facturia-Mode: test" \ -H "Content-Type: application/json" \ -d '{"name":"primera llave","scopes":["documents:read","documents:write","empresas:read","empresas:write","webhooks:read","webhooks:write"],"empresas":["*"]}' ``` **Estos seis scopes son el set de arranque, y son el mínimo que recorre esta página entera.** Son también el valor por defecto de `bootstrap.sandbox_key.scopes`, así que la forma de una llamada mintea exactamente esto. El paso 2 crea una empresa y `POST /v1/empresas` pide `empresas:write`; el paso 6 suscribe un webhook y `POST /v1/webhook_endpoints` pide `webhooks:write`. Una llave minteada solo con los dos de `documents:` emite documentos perfectamente y responde `403 insufficient_scope` a la primera llamada de este quickstart. El build de este sitio cruza los scopes de esta llamada contra el `requireScope` de cada endpoint que el quickstart invoca, así que no pueden volver a separarse. Lo que **no** entra: `keys:write` —el bootstrap lo rechaza con `400` y `errors: ["invalid_bootstrap"]`, porque mintear es un poder de llave root y una llave de sandbox no debe ser una autoridad de minteo— ni los scopes nuevos de la puesta en marcha (`mandato:write`, `mandato:attest`, `onboarding:write`, `onboarding:attest`), que se piden por separado y a propósito. Todo eso está en [Scopes](/docs/autenticacion/scopes). `/dashboard/*` es la superficie de sesión de cuenta. Sirve los mismos recursos que `/v1` — los mismos serializadores, los mismos filtros, el mismo sobre de error — autenticada con un JWT de cuenta en vez de una llave `sk_`, porque un navegador nunca debe sostener una llave `sk_`. > **El modo viaja en un header en la sesión** > > Una sesión no tiene prefijo que cargue su modo, así que lo hace `Facturia-Mode: test|live`, con > `live` por defecto. Un valor no reconocido es un `400`, nunca una caída silenciosa a `live`. Guarda el secreto: se devuelve exactamente una vez. Y ojo con los límites del arranque en frío: `/account/register` y `/account/login` están topados en **10 por minuto y por IP** y **5 por minuto por correo**, con `429` y `errors: ["auth_throttled"]`. Están en [Límites](/docs/autenticacion/limites). ## 2. Crea una empresa de sandbox [#2-crea-una-empresa-de-sandbox] Si usaste `bootstrap.empresa` en el paso 1, esto ya está hecho — salta al paso 3. Si no, es una llamada. En modo test la empresa nace lista para emitir: sin certificado, sin mandato y sin certificación. ```bash curl -sS https://api.facturia.cl/v1/empresas \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Content-Type: application/json" \ -d '{"rut":"76000002-7","legal_name":"Ejemplo SpA"}' ``` Solo `rut` y `legal_name` son obligatorios. El resto lo leemos del registro de contribuyentes del SII una vez cargado el certificado, y puedes sobrescribir cualquier campo después. ## 3. Emite una boleta electrónica [#3-emite-una-boleta-electrónica] Dos campos. #### curl ```bash curl -sS https://api.facturia.cl/v1/documents \ -H "Authorization: Bearer $FACTURIA_KEY" \ -H "Facturia-Empresa: $EMPRESA" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "tipo_dte": 39, "items": [{ "description": "Servicio de mantención mensual", "quantity": 1, "unit_price": 25000 }] }' ``` #### JavaScript ```js // Node 18+ · sin SDK: la API es HTTP y nada más const res = await fetch('https://api.facturia.cl/v1/documents', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTURIA_KEY}`, 'Facturia-Empresa': process.env.EMPRESA, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({ tipo_dte: 39, items: [{ description: 'Servicio de mantención mensual', quantity: 1, unit_price: 25000 }], }), }); const documento = await res.json(); console.log(documento.folio, documento.status); // null queued ``` #### Python ```python # Python 3.9+ · requests — sin SDK: la API es HTTP y nada más import os, uuid, requests res = requests.post( "https://api.facturia.cl/v1/documents", headers={ "Authorization": "Bearer " + os.environ["FACTURIA_KEY"], "Facturia-Empresa": os.environ["EMPRESA"], "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json", }, json={ "tipo_dte": 39, "items": [ {"description": "Servicio de mantención mensual", "quantity": 1, "unit_price": 25000} ], }, ) documento = res.json() print(documento["folio"], documento["status"]) # None queued ``` Si prefieres un cliente tipado en vez de `requests` o `fetch`, el contrato se genera con una línea: [Generar un cliente tipado](/docs/referencia#generar-un-cliente-tipado). La respuesta es `201` y llega de inmediato: ```json { "id": "doc_01K2R7Q4XW9M3B8ZC5YHTVJD6N", "object": "document", "mode": "test", "status": "queued", "tipo_dte": 39, "folio": null, "issue_date": "2026-08-14", "totals": { "net": 21008, "iva": 3992, "exempt": 0, "total": 25000 }, "pdf_url": "https://api.facturia.cl/v1/documents/doc_01K2R7.../pdf", "xml_url": "https://api.facturia.cl/v1/documents/doc_01K2R7.../xml", "sii": { "environment": "sandbox", "track_id": null, "estado": null }, "created_at": "2026-08-14T13:04:11Z" } ``` Fíjate en tres cosas, y la primera es la que sorprende: **el `folio` viene `null`**. El `POST` no asigna folios. Escribe la fila, encola el trabajo y responde; un par de segundos después el worker saca el folio del contador — en modo test parten en 1 por cada `(empresa, tipo_dte)` — y recién ahí el documento tiene número. Lo segundo, los totales sí vienen calculados de inmediato, porque son aritmética nuestra y no dependen del SII: una boleta trae los precios brutos, así que los 25.000 se descomponen en neto e IVA. Y lo tercero, el `status` es `queued`, **no** `accepted`. > **Trampa — No guardes el folio desde la respuesta del POST** > > Siempre va a ser `null` ahí. El folio aparece en el evento `document.folio_allocated`, en la > siguiente lectura de `GET /v1/documents/{document_id}` y en el objeto que viaja dentro de los > webhooks del ciclo de vida — nunca en la respuesta que creó el documento. ## 4. El ciclo de vida honesto: `queued` → `sent` → `accepted` [#4-el-ciclo-de-vida-honesto-queued--sent--accepted] El `POST` es síncrono. La ida y vuelta al SII no lo es, y no fingimos lo contrario. ``` ┌───────────► rejected (terminal) │ queued ──► sent ──────┼───────────► reparo (terminal-ish; válido pero observado) │ │ │ └───────────► accepted ──► annulled (una NC lo anuló) │ └──► failed ──(retry)──► queued ``` En el sandbox el documento se asienta en `accepted` en aproximadamente un segundo, disparando cada webhook que dispararía un documento real. En producción el trayecto típico a un estado terminal es de 30 segundos a unos pocos minutos, y el SII puede tardar horas en sus peaks o en mantención. ```bash curl https://api.facturia.cl/v1/documents/doc_01K2R7Q4XW9M3B8ZC5YHTVJD6N \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA" ``` ```bash # Los eventos del documento, del más antiguo al más nuevo curl https://api.facturia.cl/v1/documents/doc_01K2R7.../events \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA" ``` ```json { "object": "list", "data": [ { "type": "document.folio_allocated", "at": "2026-08-14T13:04:13Z", "detail": { "folio": 1, "sandbox": true } }, { "type": "document.signed", "at": "2026-08-14T13:04:14Z", "detail": { "status": "signed" } }, { "type": "document.submitted", "at": "2026-08-14T13:04:19Z", "detail": { "status": "submitted", "track_id": "9000000001" } }, { "type": "document.accepted", "at": "2026-08-14T13:04:20Z", "detail": { "status": "accepted", "sii_estado": "DOK" } } ] } ``` Este listado es el **diario del motor**, y su vocabulario es más fino que el de los cuatro estados públicos: `folio_allocated` y `signed` son los dos pasos que desde afuera se leen como `queued`, y `submitted` es el paso que se lee como `sent`. Ahí está, en un solo lugar y con hora, el resto de la secuencia que el `201` no te podía contar: folio asignado → firmado → transmitido → aceptado. El diario empieza en el folio y no en la creación porque la creación ya la tienes — es la respuesta que acabas de leer, con su `created_at`. Es un vocabulario distinto del de los **webhooks**, y conviene no confundirlos. Un endpoint suscrito recibe `document.created`, `document.sent` y `document.accepted` — el catálogo de [Webhooks](/docs/webhooks) — porque un webhook anuncia un cambio sobre el que se puede actuar, y «ya le puse folio» y «ya lo firmé» no cambian nada que el suscriptor pueda observar: los dos siguen siendo `queued`. El diario los guarda igual, para cuando estás depurando y sí importa saber en cuál de los dos se quedó. > **No construyas sobre polling apretado** > > Los webhooks son la integración prevista. Si igual necesitas hacer polling: no consultes > `GET /v1/documents/{document_id}` más seguido que cada **10 segundos** durante los primeros 2 > minutos, y luego cada 60 segundos. Mejor aún, consulta el listado con > `status=sent&created_at[gte]=…` una vez por minuto y reconcilia en bloque. El header > `Facturia-Poll-After` en la respuesta del documento te dice el próximo momento útil, calculado por > nuestro propio scheduler. > > Ese header **desaparece cuando el documento llega a un estado terminal** (`accepted`, `reparo`, > `rejected`, `annulled`): ya no queda nada que observar, y decirte «vuelve en 60 segundos» sería > mandarte a consultar algo que no va a cambiar nunca más. Trata su ausencia como la señal de que > terminaste, no como un header perdido. > > En el sandbox eso significa que casi nunca lo vas a ver: el documento se asienta en \~1 segundo, así > que para cuando lo lees ya es terminal y la respuesta viene sin `Facturia-Poll-After`. Es el > comportamiento correcto y es exactamente el mismo código que corre en producción — no lo tomes como > que el header no existe. Ojo también con que solo viaja en `GET /v1/documents/{document_id}`: el > listado no lo lleva, porque una página de cien documentos no tiene un único próximo momento útil. ## 5. Baja el PDF [#5-baja-el-pdf] ```bash curl -L https://api.facturia.cl/v1/documents/doc_01K2R7.../pdf \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA" -o boleta.pdf ``` En modo test el PDF trae el layout real con una marca de agua `SIN VALOR TRIBUTARIO`. ## 6. Recibe el webhook en vez de preguntar [#6-recibe-el-webhook-en-vez-de-preguntar] ```bash curl -sS https://api.facturia.cl/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Content-Type: application/json" \ -d '{ "url": "https://api.miempresa.cl/hooks/facturia", "enabled_events": ["document.accepted","document.rejected","document.reparo"] }' ``` El `secret` (`whsec_…`) se devuelve una sola vez. Cómo verificar la firma está en [Webhooks · Firma](/docs/webhooks/firma). ## Y ahora, en serio [#y-ahora-en-serio] Cambiar `sk_test_` por `sk_live_` cambia lo que pasa en el cable, no tu código. Lo que sí cambia es que la empresa necesita recorrer una puesta en marcha real: mandato, certificado o clave centralizada, los pasos que hace una persona en el SII, la aprobación del mes y los folios. El camino completo, numerado y con los dos carriles lado a lado, está en [De sandbox a producción](/docs/empezar/produccion); lo maneja un solo endpoint que puedes dibujar como asistente: `GET /v1/empresas/{empresa_id}/onboarding`. - [De sandbox a producción](/docs/empezar/produccion): El camino real: origen y canal, mandato, pasos humanos con handoff, `go_live` y folios. - [Conceptos](/docs/empezar/conceptos): Las cuatro ideas que hacen entendible todo lo demás. - [El sandbox a fondo](/docs/empezar/sandbox): Garantías de determinismo, cómo forzar un rechazo y los test helpers. # De sandbox a producción (/docs/empezar/produccion) El quickstart termina con una boleta emitida en el sandbox. Esta página es lo que falta entre eso y una factura con valor tributario: **una máquina de estados que puedes recorrer entera por API**, con tres o cuatro momentos en que la ley chilena exige que actúe una persona y nosotros te damos la URL para dársela. Todo lo de acá vive detrás de un solo recurso de lectura: ```bash curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R7Q4XW9M3B8ZC5YHTVJD6N/onboarding \ -H "Authorization: Bearer $FACTURIA_KEY" ``` La forma exacta del cuerpo está en [`Onboarding`](/docs/referencia/esquemas#onboarding), y la del handoff a una persona en [`PendingAction`](/docs/referencia/esquemas#pendingaction). ## Los dos carriles [#los-dos-carriles] Una empresa emite por **uno** de dos canales, y el canal decide qué lista de pasos recorre. No son dos productos: son el mismo endpoint, los mismos campos y la misma respuesta. | | **mercado** | **SFG** (Sistema de Facturación Gratuito) | | ------------- | -------------------------------------------------------------- | ------------------------------------------------------------ | | Quién emite | Nuestro motor propio, certificado ante el SII | El portal gratuito del SII, operado por nosotros | | Certificación | Sí: set de pruebas, simulación, intercambio, muestras impresas | **Ninguna** — el SII ya habilitó a la empresa al inscribirla | | Ambiente | `certificacion` hasta la resolución, después `produccion` | Siempre `produccion` | | Folios | Timbraje propio (CAF) | Los asigna el SII en cada emisión | | Pasos | 11 (10 sin boletas) | 7 | | Copia cedible | Sí (tipos 33 y 34) | No | `emission_channel` es **cómo emite hoy** la empresa. `certification_track` es **para qué se está certificando**, y puede ser otra cosa o `null`: una empresa puede emitir en SFG mientras certifica para mercado, y una empresa contenta con el portal gratuito para siempre tiene la pista en `null`. Los dos campos vienen en el objeto [`Empresa`](/docs/referencia/esquemas#empresa) y son de solo lectura salvo el canal, que declaras tú. > **Dibuja los pasos que te entregan** > > `onboarding.steps[]` trae una lista o la otra según el carril activo. No supongas el largo ni las > llaves: recorre lo que venga, lee `actor` de cada paso y muestra `help`. Un paso con una llave que > tu código no conoce se dibuja igual; uno que desapareció no era exigible. ### Los pasos, con su `actor` [#los-pasos-con-su-actor] `actor` dice de quién es el paso: `customer` es tuyo o de tu cliente, `facturia` lo corremos nosotros, `sii` es esperar. Tres pasos del carril de mercado son nuestros y **aun así no avanzan sin una persona** —`sii_postulacion`, `cumplimiento` y `habilitacion`: los presentamos con el certificado en custodia, después de una aprobación—, y lo que lo dice es `pending_action.approval`, no `actor`. `execution` es el otro campo del cable ([`Onboarding`](/docs/referencia/esquemas#onboarding)): `automatic` es un trabajo en cola y `operator` es una persona nuestra frente a un terminal, que es la diferencia entre «queda durante la noche» y «queda cuando alguien se siente a hacerlo». No es un dato fijo del producto: **`execution` vuelve a `operator` en el API si la cerca de ese paso se cierra en producción**, y con ella sube el `eta_business_days` del paso y la fecha de `estimated_ready_at`. La tabla de abajo muestra el estado de producción —todas las cercas abiertas, `automatic` en las once filas—, así que léelo del cable y no de acá. Mientras un paso diga `operator`, su `eta_business_days` no baja de 2 días hábiles: una sesión de trabajo se agenda, no se encola. **Carril mercado** — once pasos, o diez si no declaraste boletas: | # | `key` | `actor` | `execution` | Qué es | | -- | ------------------- | ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | `mandato` | `customer` | `automatic` | El representante legal nos autoriza a actuar ante el SII. Revocable | | 2 | `certificate` | `customer` | `automatic` | El `.pfx` del representante legal, a la bóveda | | 3 | `sii_postulacion` | `facturia` | `automatic` | La presentamos nosotros con el certificado en custodia; el titular autoriza **el momento** | | 4 | `set_de_pruebas` | `facturia` | `automatic` | Emitimos el set que el SII exige por cada tipo | | 5 | `simulacion` | `facturia` | `automatic` | La simulación previa a la autorización | | 6 | `intercambio` | `facturia` | `automatic` | Probamos que la empresa recibe y responde documentos de terceros | | 7 | `muestras_impresas` | `facturia` | `automatic` | Enviamos las muestras impresas de cada tipo | | 8 | `cumplimiento` | `facturia` | `automatic` | La presentamos nosotros con el certificado en custodia; el representante legal aprueba el acto y, si no estaba fijado antes, el mes | | 9 | `resolucion` | `sii` | `automatic` | El SII dicta la resolución. Acá solo se espera | | 10 | `habilitacion` | `facturia` | `automatic` | Solo si declaraste boletas: las habilitamos nosotros de principio a fin con el certificado en custodia; el representante legal decide cuándo empezar y aprueba la declaración final | | 11 | `folios` | `facturia` | `automatic` | Pedimos el primer timbraje (CAF) de cada tipo | **Carril SFG** — siete pasos, sin certificación, más dos si declaras boletas: | # | `key` | `actor` | `execution` | Qué es | | - | -------------------- | ---------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | `mandato` | `customer` | `automatic` | El mismo mandato, el mismo registro, la misma revocación | | 2 | `credential` | `customer` | `automatic` | Certificado en custodia **o** delegación: nuestro RUT como usuario autorizado | | 3 | `clave_centralizado` | `customer` | `automatic` | La clave del Sistema de Facturación Centralizado, por `POST /v1/empresas/{empresa_id}/sfg_credential` | | 4 | `usuario_autorizado` | `customer` | `automatic` | Comprobamos que nuestro certificado puede actuar por la empresa en el Portal MIPYME | | 5 | `emisor_verificado` | `facturia` | `automatic` | Confirmamos que la empresa está inscrita y autorizada a emitir | | 6 | `puesta_en_vivo` | `facturia` | `automatic` | **Nuestro**: el canario y la habilitación de emisión real por empresa | | 7 | `emitting` | `facturia` | `automatic` | Listo: emitimos y recibimos por su cuenta del sistema gratuito | | 8 | `habilitacion` | `facturia` | `automatic` | Solo si declaraste boletas: el portal gratuito no tiene formulario de boleta, así que las emite nuestro motor y antes las habilitamos ante el SII. Dos decisiones tuyas: cuándo empezar y la declaración final | | 9 | `folios` | `facturia` | `automatic` | Solo si declaraste boletas: el primer timbraje (CAF) de la 39/41. Las facturas del carril gratuito no llevan CAF | > **Trampa — Las boletas del carril gratuito salen por nuestro motor** > > El Portal MIPYME nunca tuvo formulario de boleta, así que una 39 o una 41 **no** viaja por el > sistema gratuito: la construye, la firma y la envía el motor propio de Facturia, exactamente igual > que la de una empresa del carril mercado. El `emission_channel` de la empresa no decide esto — lo > decide el tipo. Lo que sí decide si puedes emitirla es la habilitación de boletas del SII (paso 8) y > el primer CAF (paso 9); hasta que estén, `POST /v1/documents` responde `422 empresa_not_emitting` > nombrando el paso `habilitacion`, y `capabilities.tipos` no lista la 39 ni la 41. **Tus facturas no > esperan a nada de esto**: salen en cuanto termina el paso 7. > **Trampa — `puesta_en_vivo` nunca es tuyo** > > Es un paso `actor: facturia` con `stall_actor: facturia`: una espera con estimación, no un > `action_required`. Si tu asistente lo dibuja como algo que el cliente puede apretar, va a mandar a > una persona a mirar una página donde no hay nada que hacer. `habilitación` está reservado para actos > del SII; **puesta en vivo** es el nuestro, y ocurre en los dos carriles. ## ¿Ya emites con otro proveedor? [#ya-emites-con-otro-proveedor] El SII certifica al **contribuyente**, no al software. La resolución que tu empresa ya tiene sigue valiendo aunque cambies de proveedor, y volver a certificarse sería ganarse de nuevo algo que ya se tiene. Así que se declara al crear la empresa — o después, con `PATCH /v1/empresas/{empresa_id}` — y nosotros la comprobamos. ```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` y `fecha` son el `NroResol` y el `FchResol` de la carátula: están impresos en cualquier documento que tu empresa ya haya emitido con su proveedor actual. No hay que ir al SII a buscarlos. > **Advertencia** > > **Declarar no salta nada.** El campo escribe dos columnas y no mueve ni un paso. Léelo de vuelta: > `resolucion.verified` es `false` hasta que lo comprobemos, y mientras tanto la empresa camina la > lista completa. ```json { "resolucion": { "numero": 80, "fecha": "2026-06-21", "verified": false, "verified_at": null } } ``` ### Cómo lo comprobamos [#cómo-lo-comprobamos] Pidiéndole folios al SII. En cuanto tenemos tu certificado en custodia, la máquina le pide al portal que timbre **un** tipo de documento de los que declaraste: el SII rechaza el timbraje de un RUT que no autorizó para ese tipo, así que un rango otorgado es la prueba y un rechazo es la respuesta. * **Te los da** → `resolucion.verified` pasa a `true`, los pasos de certificación quedan `skipped` con la nota que dice por qué, y `estimated_ready_at` es hoy: lo único que falta es el resto de tus folios. * **Te los niega** → `verified` se queda en `false` y el paso `sii_postulacion` vuelve a ser tuyo, con `blocked_reason.resolucion_claim` llevando la frase textual del SII. Nunca te dejamos sin explicación. Es un acto, no una lectura, y no hay otra forma: ninguna consulta del SII distingue una resolución de mercado de una inscripción en el sistema gratuito, y por eso lo comprobamos usándola. ### Boletas [#boletas] La habilitación de boleta electrónica es otra autorización, con su propia fecha y sin número — el correo del SII dice «habilitados los documentos de boleta electrónica a contar del …» y eso es todo. Se declara aparte, junto a `doc_types` con 39 o 41: ```json { "resolucion_boletas": { "fecha": "2026-09-01" } } ``` Se comprueba igual, con un timbraje de 39 o 41. Si resulta, tus boletas quedan habilitadas sin pasar por nuestra cadena de habilitación; si no, aparecen los pasos de habilitación normales. ### La casilla de intercambio sigue siendo tuya [#la-casilla-de-intercambio-sigue-siendo-tuya] Cambiar el correo de intercambio en Mi SII es un acto de administración de tu cuenta y el mandato nos prohíbe expresamente tocarlo. Mientras no lo cambies, **tus proveedores le siguen enviando los documentos a tu proveedor anterior**: nada se pierde, pero no llega acá. Tu dirección con nosotros la publica cada empresa en `reception_email`, y el acto `casilla_intercambio` de `GET /v1/empresas/{empresa_id}/onboarding` trae las instrucciones para la persona que puede hacerlo. ## El camino, paso por paso [#el-camino-paso-por-paso] ### Cuenta y primera llave [#cuenta-y-primera-llave] Lo mismo que el [quickstart](/docs/empezar/quickstart), con un cambio: `Facturia-Mode: live` en vez de `test`, y el set de scopes completo del arranque en frío. ```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":["*"]}' ``` Mintear una llave `sk_live_` desde una cuenta sin verificar responde `403 email_verification_required`. ### Construye entero contra el sandbox primero [#construye-entero-contra-el-sandbox-primero] Una llave `sk_test_` recorre el ciclo completo — emisión, estados, webhooks, PDF — sin tocar el SII y sin costo. La empresa de sandbox nace `emitting`: la máquina de esta página **se salta entera** en modo test, y eso es a propósito. Termina la integración ahí y cambia el prefijo después. ### La empresa real: `origen` y canal de emisión [#la-empresa-real-origen-y-canal-de-emisión] ```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` es **obligatorio en modo real** ([`Origen`](/docs/referencia/esquemas#origen)): `gratuito` si la empresa ya emite por el portal del SII, `mercado` si ya usa un proveedor, `ninguno` si todavía no emite electrónicamente. Omitirlo con una llave `sk_live_` es `400 invalid_field`. `origen` **no elige el canal** y no decide qué se puede saltar. Es una declaración sobre el presente que enruta la copia del asistente; el salto de certificación se decide contra el SII, empíricamente. El canal de emisión es una preferencia distinta —seguir en el portal gratuito o pasar al motor certificado— y por eso se declara aparte. `ambiente` es **derivado y de solo lectura**: SFG es siempre producción; mercado es certificación hasta que llega la resolución. No lo mandes. ### Mandato: un enlace de aprobación, no una casilla [#mandato-un-enlace-de-aprobación-no-una-casilla] El mandato lo firma el **representante legal**, que muy seguido no es quien tiene la cuenta. Así que el camino por defecto es acuñar un enlace y dárselo: ```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 '{}' ``` La respuesta trae el `signing_url`. **Se devuelve una sola vez** — de ahí en adelante solo guardamos su hash. Para saber cómo va, se lee: ```bash curl -sS https://api.facturia.cl/v1/empresas/$EMPRESA_ID/mandato \ -H "Authorization: Bearer $FACTURIA_KEY" ``` que responde `status`, `terms_current` y `acceptance_method` (disponible desde v1.1). Scopes: acuñar y leer el mandato pide `mandato:write`. Postear una **aceptación con cuerpo** —el caso del integrador que recogió la firma en su propio flujo— pide `mandato:attest`, se registra visible como `acceptance_method: api` y nunca entra en el set por defecto de una llave nueva ni se concede por implicación: `empresas:write` implica `mandato:write` (así una llave que ya existe sigue pudiendo acuñar y leer el mandato) pero no `mandato:attest`, que hay que pedirlo explícitamente (ADR-0012 §4). Ver [Scopes](/docs/autenticacion/scopes). `GET /v1/mandato_terms` es público (sin autenticación) y hoy publica la versión vigente: ```json { "object": "mandato_terms", "version": "1.0", "status": "active", "effective_date": "2026-08-24" } ``` Una aceptación contra una versión distinta a la vigente —la tuya quedó vieja porque el texto cambió— se rechaza con `422 mandato_terms_changed`, no con `mandato_terms_draft`: no hay ningún estado de borrador que bloquee la aceptación en modo real. `GET …/mandato` publica `terms_current` con la versión vigente y su `content_hash`; si no coincide con lo que firmaste, toca re-firmar con un `signing_url` nuevo (ver [«Revocar y re-firmar»](#revocar-y-re-firmar) más abajo). ### Revocar y re-firmar [#revocar-y-re-firmar] El mandato es revocable «en cualquier tiempo, sin expresión de causa, sin sanción ni cobro alguno» (cláusula 7.1), y hay dos puertas: * **Con tu llave o tu sesión:** `DELETE /v1/empresas/{empresa_id}/mandato` — `204` incluso si ya no había un mandato vivo, porque pediste un estado («ya no tengo autorización tuya») y ese es el resultado en los dos casos. * **El representante legal, sin llave:** el correo-recibo de la firma lleva un enlace perpetuo a `.../onboarding/{empresa_id}/mandato_revocation?token=…`. La página pide el RUT del representante legal **tal como quedó registrado en el mandato que firmó** —el enlace no vence, así que el RUT es lo que evita que un correo reenviado sea un botón de parada— y llama `POST https://api.facturia.cl/onboarding/mandato/revoke` con `{"token": "…", "rep_legal_rut": "…", "revoke": true}`. Si el enlace se perdió, se remite con `POST .../onboarding/mandato/revocation_link` (mismos dos factores). **Qué pasa al revocar:** la empresa queda `suspended` de inmediato —el mismo tick, no en cinco minutos— y deja de poder emitir; los documentos ya emitidos con valor tributario siguen existiendo, la revocación no los borra. La custodia del certificado es una decisión aparte (cláusula 7.1.1): revocar el mandato no destruye el `.pfx`, eso es `DELETE /v1/empresas/{empresa_id}/certificate`. **Volver a firmar** restablece la posición que la empresa tenía antes de revocar (cláusula 7.1.3): si ya había certificado, no vuelve a certificar solo porque re-firmó. El mismo flujo del signing link de arriba sirve para volver a firmar. **Cambio de versión del texto**, el otro motivo para re-firmar: si `GET /v1/mandato_terms` publica una versión distinta a la que el mandato vigente aceptó, cualquier acto que dependa del mandato responde `422 mandato_terms_changed` en vez de proceder. La salida es la misma: acuña un `signing_url` nuevo con `POST …/mandato {}` y que el representante legal lo firme de nuevo; el resto de la posición de la empresa no se mueve. ### Certificado (mercado) o clave centralizada (SFG) [#certificado-mercado-o-clave-centralizada-sfg] En el carril **mercado** el paso 2 es el certificado digital del representante legal: ```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=…" ``` Queda cifrado en una bóveda por empresa y no vuelve a salir; no es un argumento de la llamada de emisión. Se puede purgar con `DELETE /v1/empresas/{empresa_id}/certificate`, lo que revoca la custodia y suspende la emisión. En el carril **SFG** el paso equivalente es la credencial del portal: la clave del certificado **centralizado** del SII (no la clave tributaria) que el Portal MIPYME vuelve a pedir en el paso final de cada emisión. ```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` del mismo recurso responde la misma forma —solo si la tenemos guardada y desde cuándo, nunca la clave misma— y `DELETE /v1/empresas/{empresa_id}/sfg_credential` la destruye (`204`, también cuando no había nada que borrar: pediste un estado, «no tengo nada tuyo», y ese es el resultado en los dos casos). El paso `clave_centralizado` (disponible desde v1.1) es el que nombra ese handoff. Es `actor: customer`, su `performed_by.role` es `usuario_administrador` y su evidencia es `own_record`: cae en nuestras propias tablas en el mismo acto del `POST`, así que lo vemos ocurrir sin preguntarle al 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": "…" } } ] } } ``` > **Trampa — Nunca pedimos la clave tributaria** > > Toda la suite tributaria funciona con una sesión autenticada por certificado contra la zona de datos > del propio SII. La clave del Sistema de Facturación Centralizado no es la clave tributaria y no la > sustituye. ### Los pasos humanos: el patrón de handoff [#los-pasos-humanos-el-patrón-de-handoff] Cuando un paso necesita a una persona, la respuesta no dice «bloqueado». Dice quién, dónde, qué hacer y cómo nos vamos a enterar — la misma forma con que OAuth entrega la autenticación. ```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` está deprecado** > > `recheck_url` sigue viniendo, tal cual, para no romper a nadie que ya lo lee. Es un string `"POST "` relativo — hay que separar el método a mano y anteponer el host. Úsalo como el `recheck` > de arriba: el mismo endpoint, como `{method, url}`, absoluto. Lo mismo pasa con > `error.details.onboarding_url` más abajo: `error.details.onboarding` es la versión estructurada. Tres campos deciden qué hace tu agente con esto: * **`performed_by.role`** — a quién se lo entregas. `representante_legal`, `usuario_administrador`, `titular_certificado` o `titular_cuenta`. Un handoff se dirige a un **rol**, nunca a una persona: nosotros no sabemos cuál de ellos es tu principal. * **`verification.method`** — cómo nos enteramos. `sii_probe` (releemos el SII), `own_record` (cae en nuestras tablas) o `no_producer`, que significa que **nada en esta versión lo observa** y que acá una persona tiene que confirmarlo. No le digas a nadie que estás vigilando un `no_producer`. * **`evidence`** y **`caveat`** — qué tan respaldada está la ruta de clics. Si `evidence` no es `captured`, `caveat` no es nulo y dice de qué no estamos seguros. **Trasládalo.** `instructions[]` va en español y se traslada **textual**: no la parafrasees ni le agregues pasos. #### El enlace: `action_url` o `link.mint` [#el-enlace-action_url-o-linkmint] `pending_action.action_url` (y `steps[].action_url`) es la URL de un enlace **vivo** —no vencido, no revocado— si existe uno para ese paso. Si no existe, es `null`. Nunca es una URL que abra «falta el enlace». Para acuñar uno, el propio handoff dice cómo (disponible desde 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` acepta **cualquier** paso `actor: customer` del catálogo activo de la empresa, sea del carril mercado o del SFG. La única excepción es `mandato`, que tiene su propio endpoint (`POST …/mandato` con cuerpo vacío) y por eso su `link.mint` apunta ahí con `body: {}`. Acuñar pide `onboarding:write`. La `url` se devuelve **una sola vez**; re-acuñar revoca la anterior. Se revoca con `DELETE /v1/empresas/{empresa_id}/onboarding/links/{step}`, que responde `204` haya existido o no. #### La vuelta: `recheck` [#la-vuelta-recheck] Cuando la persona terminó, tu agente nos lo dice y nosotros vamos a mirar: ```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" ``` Esto **acepta una declaración y no produce ningún hecho**. No lleva cuerpo, no escribe el estado de ningún paso y no mueve la empresa. Encola al productor que lee el SII y devuelve el estado tal como estaba al momento de llamar. Si posteas sobre un paso en `action_required` y lees `action_required` de vuelta, eso es el endpoint funcionando: `verified` viene en la respuesta y **siempre es `false`**. La forma completa está en [`OnboardingRecheck`](/docs/referencia/esquemas#onboardingrecheck). No es obligatorio: `sii_probe` y `own_record` nos llegan solos. `recheck` sirve para no esperar el próximo barrido. #### Cuando el paso es NUESTRO y sólo falta que alguien diga que sí: `approve` [#cuando-el-paso-es-nuestro-y-sólo-falta-que-alguien-diga-que-sí-approve] Tres pasos los ejecutamos nosotros y aun así no arrancan solos, porque cada uno tiene una consecuencia que alguien tiene que decidir primero: la **postulación** (una empresa del Sistema de Facturación Gratuito pierde su calidad de usuario activo en cuanto el SII la autorice), la **Declaración de Cumplimiento** (cierra la certificación y abre, desde el mes que se declare, la obligación de Información Electrónica de Compras y Ventas) y la **habilitación de boletas** (abre un reloj de 24 horas del SII). Esos pasos traen un bloque `approval` en su `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" } ``` La forma canónica de tomar esa decisión es la página alojada (ADR-0012 §1): acuña el enlace del paso y entrégaselo al titular. Si tu integración recogió la decisión en su propio flujo, la puerta es: ```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"}' ``` Pide `onboarding:attest` —nunca implicado por `empresas:write`—, el `act` tiene que ser el que el paso está pidiendo ahora (si no, `409 onboarding_approval_not_offered` o `400 invalid_field`), y `decision: "reject"` se registra igual que una aprobación: «no todavía» es una respuesta, y los ejecutores leen la MÁS RECIENTE. ### La Declaración de Cumplimiento: la presentamos nosotros [#la-declaración-de-cumplimiento-la-presentamos-nosotros] La Declaración de Cumplimiento cierra la certificación de mercado. Al presentarla, la empresa queda operando en el sistema de facturación electrónica y debe la Información Electrónica de Compras y Ventas **desde el mes que se declare**, emita o no emita ese mes. El SII permite postergarla, así que el mes sigue siendo una decisión y no un efecto automático del resto de la certificación. **La presentamos nosotros, con el certificado del representante legal que tenemos en custodia** — el mismo con el que firmamos tus documentos. No tienes que entrar al sitio del SII ni completar ningún formulario allá. Lo único que falta es que el representante legal apruebe, en su propia página. El rol que puede aprobar este acto es `rep_legal`, no cualquier `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" } ``` Ese único clic aprueba dos cosas a la vez: el acto ante el SII, y —si el titular no había fijado el mes antes por la vía de la sección siguiente— el mes mismo. Antes de presentar comprobamos, contra el propio SII, que el titular del certificado en custodia es el mismo representante legal que la empresa tiene autorizado allá: es lo que exige la Res. Ex. SII N° 81 de 2005, resolutivo CUARTO d) — que sea el representante legal quien declare «con su certificado digital» —, y la misma comprobación que ya corrimos para la habilitación de boletas. #### Fijar el mes antes, por separado: `go_live` [#fijar-el-mes-antes-por-separado-go_live] Si el titular quiere decidir el mes antes de que exista algo que aprobar —o es una persona distinta del representante legal—, puede fijarlo por adelantado: ```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 '{}' ``` La respuesta trae `{ "approval_url": "…" }` (disponible desde v1.1): la página donde el titular ve exactamente qué está aprobando —las consecuencias del mes arriba, y el botón de autorizar debajo—. Acuñar pide `onboarding:write`. **Tiene que haber algo que decidir.** Si la empresa todavía no llegó a la declaración, no acuñamos un enlace que abriría una página vacía: * `409 go_live_not_ready` — el carril de mercado aún no llega a la decisión. `error.details.blocked_on` dice qué paso lo está deteniendo y `error.details.next` es el `{method, url}` que avisa cuando se mueve. Vuelve a intentarlo: va a estar listo. * `409 go_live_not_applicable` — la empresa no se está certificando para mercado, así que no tiene declaración de cumplimiento. Lo que abre su emisión es la `puesta_en_vivo`, que es nuestra. Ninguno de los dos aplica a `{"authorized": false}`: una decisión que existe siempre se puede retirar. La forma con cuerpo —`{"authorized": true}` posteado por un integrador que recogió la aprobación en su propio flujo— queda detrás de `onboarding:attest`, el mismo trato que `mandato:attest`. Mandar `{"authorized": false}` retira la autorización mientras no se haya declarado nada; después de declarada no queda nada que retirar y se responde `409 cumplimiento_already_filed`. **Esta autorización es gratis** (#530). Estuvo entre los actos que exigían suscripción activa hasta el 2026-09-04, y solo por esta puerta: la página alojada de `cumplimiento` para el representante legal respondía `200` para la misma empresa sin suscripción. Es una decisión, no una emisión. La suscripción se exige donde está el valor: emitir, la nota de crédito, pedir folios y enviar. Una empresa autorizada que no ha pagado no emite nada. > **La presenta Facturia; el representante legal la aprueba** > > Desde ADR-0013 (2026-09-06) el acto ante el SII lo presentamos nosotros con el certificado en > custodia — nunca el representante legal en persona. Lo que él hace es aprobar, en su propia página, > un acto que ya lo identifica: la Res. Ex. SII N° 81 de 2005 exige que sea el representante legal quien > declare «con su certificado digital», y ese es exactamente el certificado que tenemos en custodia bajo > el mandato. ### Puesta en vivo [#puesta-en-vivo] Después de que el carril terminó lo suyo, queda un paso nuestro: habilitar la emisión real de esa empresa. En SFG es el paso `puesta_en_vivo`, después del canario; en mercado es lo que sigue a los folios. Se muestra como una **espera con estimación** y nunca como algo que el cliente deba hacer. Mientras tanto, `Empresa.capabilities` dice la verdad en una lectura (disponible desde v1.1): ```json "capabilities": { "can_emit": false, "tipos": [], "blocked_by": "puesta_en_vivo", "next": { "who": "facturia" } } ``` Es la **misma función** que usa la puerta de emisión para responder `422 empresa_not_emitting`, así que el bloque y el rechazo no se pueden contradecir. `blocked_by` toma `onboarding`, `subscription`, `status`, `puesta_en_vivo` o `null`, y `next.who` es `agent`, `human` o `facturia`. ### Folios [#folios] En el carril **mercado** los folios son un CAF que le pedimos al SII y que vive en la empresa: ```bash curl -sS https://api.facturia.cl/v1/folios \ -H "Authorization: Bearer $FACTURIA_KEY" -H "Facturia-Empresa: $EMPRESA" ``` `auto_request_folios` viene **encendido** por defecto — es la única automatización que sí lo está — y pide un timbraje nuevo cuando la disponibilidad baja del umbral. También puedes pedirlo tú con `POST /v1/folios/requests`, o cargar un CAF propio con `POST /v1/cafs`. En el carril **SFG** no hay timbraje: el SII asigna el folio en cada emisión. `GET /v1/folios` responde con la forma `sfg_managed` y `folio_preview` es `null` en los borradores. Eso no es una carencia de datos; es cómo funciona ese canal. Detalles en [Folios y CAF](/docs/emision/folios). ### Webhooks [#webhooks] Lo último que conviene tener listo antes de la primera emisión real, porque es lo que evita el 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"] }' ``` El `secret` (`whsec_…`) se devuelve una sola vez. Cómo verificar la firma está en [Webhooks · Firma](/docs/webhooks/firma). ## Cuando la puerta se cierra igual [#cuando-la-puerta-se-cierra-igual] Si emites antes de tiempo, el rechazo trae el handoff adentro: ```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` es la misma dirección de siempre, pero relativa — hay que anteponerle el host. `onboarding` (#419) trae el mismo endpoint absoluto, como `{method, url}`, igual que `pending_action.link.mint`. Los dos siguen viniendo juntos; `onboarding_url` queda marcado `deprecated` en el contrato y es el que hay que dejar de leer. Es el mismo objeto [`PendingAction`](/docs/referencia/esquemas#pendingaction) del onboarding, así que tu manejo de errores y tu asistente pueden compartir el código. Los códigos que vas a ver: `422 empresa_not_emitting`, `422 mandato_required`, `422 postulacion_required`, `422 onboarding_requirement_unmet` y, si la empresa ya puede emitir por lo demás y no tiene suscripción, `402 subscription_required`. La elegibilidad se evalúa primero (#475): mientras el checklist esté incompleto el rechazo es `422 empresa_not_emitting` con su handoff, nunca «paga». Cada uno tiene su ficha en [Errores](/docs/errores). ## Siguiente paso [#siguiente-paso] - [Empresas (referencia)](/docs/referencia/empresas): Todos los endpoints de la empresa, generados desde el contrato. - [Esquemas](/docs/referencia/esquemas): `Onboarding`, `PendingAction`, `OnboardingRecheck`, `Origen` y `Empresa`, campo por campo. # Llaves de API (/docs/autenticacion) La autenticación es un solo bearer token. ```bash curl https://api.facturia.cl/v1/documents \ -H "Authorization: Bearer sk_live_9lQpV3fA2mKcRt7wZxYb1Nde" ``` | Prefijo | Modo | ¿Habla con el SII? | Costo | | ---------- | ----------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------ | | `sk_test_` | **test** — sandbox completamente simulado | Nunca | Gratis para siempre | | `sk_live_` | **live** — emisión real | Sí (certificación o producción del SII, según la empresa) | El plan único: $19.990 CLP + IVA al mes **por empresa**, documentos ilimitados | Las llaves tienen alcance de cuenta, se muestran una sola vez, se guardan hasheadas y se revocan individualmente. Puedes crearlas en el panel o por API. Una llave carga tres cosas: * un **modo** (`test` / `live`) — inmutable, codificado en el prefijo; * un **conjunto de scopes** — ver [Scopes](/docs/autenticacion/scopes); * una **lista blanca de empresas** — `["*"]` (toda empresa de la cuenta, incluidas las que se creen después) o una lista explícita de RUT. > **Trampa — No hay llave publicable en la v1** > > La API es servidor a servidor. Nunca pongas una llave secreta en un navegador, una app móvil ni el > prompt de un agente que un tercero pueda leer. Si una llave se filtra, revócala: la revocación es > inmediata e irrecuperable. > > Para darle un artefacto a alguien sin llave — el receptor, su contador, un hilo de WhatsApp — existen > los [enlaces públicos](/docs/emision/artefactos#enlaces-publicos), que son firmados, inadivinables, > con vencimiento y revocables, y no pueden escalarse a una credencial de API. ## Quién soy [#quién-soy] ```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 } } ``` Es la llamada barata para un health check: te dice qué llave estás usando, con qué scopes, sobre qué empresas y con qué límites. ## Token de empresa [#token-de-empresa] Una llave cuya lista blanca tiene exactamente una empresa es un **token de empresa**: el header `Facturia-Empresa` pasa a ser opcional, y si lo mandas debe coincidir o recibes `400 empresa_mismatch`. Es la forma recomendada cuando integras una sola compañía o cuando le entregas una llave al sistema de tu cliente: la llave no puede direccionar nada más que ese RUT. ## Verificación de correo [#verificación-de-correo] Registrarte no exige probar tu dirección, y tampoco lo exige iniciar sesión, leer nada, ni usar el producto completo en **modo test**. La verificación bloquea exactamente una cosa: crear una credencial **live**. ```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" ``` El enlace es de un solo uso, vive 24 horas y está ligado a la dirección a la que se envió: si el correo de la cuenta cambia en el intertanto, el token se rechaza en vez de confirmar una dirección a la que nunca se entregó. Hacer clic dos veces es un éxito, no un error. Los reenvíos tienen límite de tasa y responden `429` con `Retry-After`. Mintear una llave `sk_live_` desde una cuenta sin verificar devuelve `403 email_verification_required`. - [Scopes y empresas](/docs/autenticacion/scopes): Qué concede cada scope y cómo se combina con la lista blanca. - [Llaves root y rotación](/docs/autenticacion/llaves-root): Mintear una llave por cliente sin que un humano abra un panel. - [Límites de tasa](/docs/autenticacion/limites): Los buckets publicados y los headers que te dicen dónde estás. # Scopes y empresas (/docs/autenticacion/scopes) ## La tabla de scopes [#la-tabla-de-scopes] | Scope | Concede | | ---------------------------------- | -------------------------------------------------------------------------------------------------- | | `documents:read` | Leer documentos emitidos, PDF, XML y eventos | | `documents:write` | Emitir documentos, reintentar, enviar por correo | | `received:read` | Leer los DTE recibidos de proveedores | | `received:write` | Aceptar / reclamar / acusar recibo | | `empresas:read` | Leer empresas, puesta en marcha, configuración, folios | | `empresas:write` | Crear empresas, cargar certificados, cambiar configuración | | `folios:write` | Solicitar CAF al SII, cargar CAF | | `tax:read` | RCV, propuestas y declaraciones de F29, sugerencias | | `tax:write` | Caracterizar, declarar F29, confirmar sugerencias, ingresar gastos | | `webhooks:read` / `webhooks:write` | Endpoints de webhook y replay de eventos | | `usage:read` | Libro de consumo | | `keys:write` | Mintear, rotar y revocar llaves de API. Solo llaves root; nunca combinado con los scopes de arriba | Si falta un scope recibes `403 insufficient_scope`, con el scope requerido nombrado en `error.message`. No tienes que adivinar cuál era. ### El set de arranque [#el-set-de-arranque] La primera llave de una cuenta se mintea con estos seis, que son exactamente lo que recorre el [quickstart](/docs/empezar/quickstart) de punta a punta: ``` documents:read, documents:write, empresas:read, empresas:write, webhooks:read, webhooks:write ``` Ni uno más. `keys:write` queda fuera porque es el poder de una llave root, y los cuatro scopes de la puesta en marcha quedan fuera porque conceden actos con efecto legal. ### Los scopes de la puesta en marcha [#los-scopes-de-la-puesta-en-marcha] Cuatro scopes estrechos gobiernan los actos que autorizan a Facturia ante el SII (disponibles desde v1.1). **Ninguno entra en el set de arranque, ni por defecto ni por conveniencia**, y se piden uno por uno: | Scope | Concede | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mandato:write` | Acuñar el enlace de firma del mandato y leerlo (`GET …/mandato`) | | `mandato:attest` | Postear una aceptación **con cuerpo** (el integrador recogió la firma en su propio flujo; queda registrado como `acceptance_method: api`) y **revocar** el mandato con `DELETE …/mandato` — la revocación es el acto de mayor efecto del contrato y desde v1.1 no la concede `empresas:write` | | `onboarding:write` | Acuñar enlaces de pasos humanos, pedir una re-lectura, acuñar el enlace de aprobación de `go_live`, cargar el certificado | | `onboarding:attest` | Postear la autorización de `go_live` **con cuerpo** | `empresas:write` implica los dos scopes `:write` (`mandato:write`, `onboarding:write`) — así una llave que ya existe sigue pudiendo acuñar el enlace de firma y los enlaces de los pasos humanos. **No implica ninguno de los dos `:attest`**: esos se piden explícitamente o no se conceden nunca (ADR-0012 §4). Una llave con solo `empresas:write` que intenta `POST …/mandato` con cuerpo recibe `403 insufficient_scope: mandato:attest`, exactamente como si no tuviera ningún scope de la puesta en marcha. La diferencia entre `:write` y `:attest` es la diferencia entre **pedirle a una persona que apruebe** y **afirmar que ya aprobó en otra parte**: lo segundo es una atestación, se registra visiblemente como tal, y por eso vive detrás de su propio scope que nunca se concede por implicación ni por defecto. El camino completo está en [De sandbox a producción](/docs/empezar/produccion). > **Trampa — Una llave root no puede leer documentos** > > `keys:write` no se combina con ningún otro scope. Una llave root (`sk_live_root_…` / > `sk_test_root_…`) **no puede emitir, no puede leer documentos y no puede tocar datos tributarios**: > su único poder es mintear y revocar otras llaves. > > Es la confusión más común al empezar: minteas la llave root, la pruebas contra `/v1/documents` y > recibes un `403`. No está rota — está haciendo exactamente lo que debe. Mintea con ella una llave > ordinaria y usa esa. ## Lista blanca de empresas [#lista-blanca-de-empresas] Además de los scopes, cada llave lleva una lista de empresas: * `["*"]` — toda empresa de la cuenta, incluidas las que se creen después. * Una lista explícita de RUT — `["77928532-4", "78281439-7"]`. Una llave con exactamente un RUT es un [token de empresa](/docs/empezar/conceptos#token-de-empresa) y el header `Facturia-Empresa` se vuelve opcional. Los dos ejes se aplican juntos: una llave con `documents:write` sobre `["77928532-4"]` puede emitir para esa empresa y para ninguna otra. Nombrar otra empresa devuelve `403 empresa_not_allowed` — no un `404`, porque ya conoces su RUT y fingir que no existe no protege a nadie. ## Combinaciones que conviene tener en la cabeza [#combinaciones-que-conviene-tener-en-la-cabeza] | Forma | Scopes | Empresas | Para qué | | -------------------------------------- | ------------------------------------------ | ------------------------- | ------------------------------------------------------------ | | Backend de un producto propio | `documents:*`, `received:*`, `tax:*` | `["*"]` | Una sola llave que opera todas las empresas de la cuenta | | Llave por cliente (ERP, SaaS vertical) | lo mínimo que ese cliente necesita | un solo RUT | El sistema del cliente no puede direccionar nada más | | Solo lectura para BI o conciliación | `documents:read`, `tax:read`, `usage:read` | `["*"]` | Una llave que no puede emitir aunque se filtre | | Root | `keys:write` | según lo que va a delegar | Mintear y revocar llaves. Nada más | | Exploración y demos | cualquiera, pero `sk_test_` | cualquiera | Una llave de test es un límite de capacidad, no una política | ## Modo de sesión: `Facturia-Mode` [#modo-de-sesión-facturia-mode] La superficie `/dashboard/*` se autentica con un JWT de cuenta en vez de una llave `sk_`, porque un navegador nunca debe sostener una llave `sk_`. Una sesión no tiene prefijo que cargue su modo, así que el modo viaja en un header: ``` Facturia-Mode: test ``` Por defecto es `live`. El backend lo aplica exactamente igual que aplicaría el modo de una llave: en modo live una empresa de sandbox no existe, y viceversa. Un valor no reconocido es un `400`, nunca una caída silenciosa a `live`. Dos reglas aplican a una sesión que no aplican a una llave: * **Una sesión puede mintear llaves Y leer datos.** Una llave root no puede lo segundo. La diferencia es intencional: una llave root es una credencial delegada, y una sesión es el dueño de la cuenta en persona. * **Una sesión de cuenta es la única credencial que mintea una llave root:** `POST /dashboard/api_keys` con `{"root": true}` (sin `scopes` ni `empresas`) devuelve una `sk_live_root_…` / `sk_test_root_…` sin abrir ningún panel. Una llave —root u ordinaria— que pide `root: true` recibe `403 privilege_escalation`: la delegación llega hasta un nivel y una llave root no mintea otra llave root. Ver [Llaves root y rotación](/docs/autenticacion/llaves-root). # Llaves root y rotación (/docs/autenticacion/llaves-root) Si estás incrustando Facturia en tu propio producto — un SaaS vertical, un ERP, un estudio contable — vas a querer mintear una llave por empresa cliente sin que un humano abra un panel. Eso es un flujo de primera clase en la v1. ## La llave root [#la-llave-root] Los endpoints de gestión de llaves se autentican con una **llave root** (`sk_live_root_…` / `sk_test_root_…`), portadora del scope `keys:write`. La mintea una **sesión de cuenta** —la única credencial que puede— con `POST /dashboard/api_keys {"root": true}`; ni el panel ni un humano abriendo una pantalla son un requisito, es la misma llamada que haría tu propio backend en el arranque en frío: ```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}' ``` Sin `scopes` ni `empresas`: una llave root los lleva fijos (`["keys:write"]`, sin empresas) y mandarlos igual a `["keys:write"]` no es error, mandar cualquier otra cosa sí lo es — `400 invalid_field`. Una llave —root u ordinaria— que pide `{"root": true}` recibe `403 privilege_escalation`: solo una sesión de cuenta está por encima de la delegación. Una llave root no puede emitir, no puede leer documentos y no puede tocar datos tributarios. Su único poder es mintear y revocar otras llaves. Ten exactamente una, guárdala donde guardas las credenciales de tu base de datos, y nunca la despliegues a un runtime de edge. ## Mintear [#mintear] ```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` se devuelve **exactamente una vez**. Como esta llave nombra una sola empresa, es un token de empresa: entrégasela al sistema de ese cliente y no podrá direccionar nada más. ## Las tres barreras [#las-tres-barreras] Se aplican en el servidor, no en tu código: **Una llave minteada nunca puede alcanzar más que quien la mintea.** Sus `empresas` deben ser un subconjunto de la lista blanca de la llave minteadora, y sus `scopes` un subconjunto de los suyos. Una llave root con `["*"]` puede mintear cualquier cosa de la cuenta; una restringida a tres RUT solo puede producir llaves dentro de esos tres. Las violaciones devuelven `403 privilege_escalation` nombrando el RUT o el scope ofensor. **El modo se hereda.** Una llave `sk_test_root_` solo mintea llaves de test. No hay camino de una credencial de test a una live. **Una llave root no puede mintear llaves root.** La delegación es de un nivel de profundidad, por diseño. Si necesitas una segunda llave root, mintéala con una sesión de cuenta (`POST /dashboard/api_keys {"root": true}`), como arriba. ## Listar, rotar, revocar [#listar-rotar-revocar] ```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" ``` Una llave ordinaria (no root) que llame a `GET /v1/api_keys` se ve solo a sí misma — útil como health check, inútil como inventario. Todo esto queda auditado: `api_key.created`, `api_key.rolled` y `api_key.revoked` caen en el libro de eventos con el id de la llave que las minteó. > **Rotar no es revocar** > > `POST /v1/api_keys/{key_id}/roll` deja el secreto anterior vivo 24 horas para que puedas desplegar > sin ventana de caída. Si estás rotando **porque una llave se filtró**, rota y después revoca — o > revoca directamente y asume el corte. Las 24 horas son una comodidad de despliegue, no una respuesta > a incidentes. ## El arranque en frío [#el-arranque-en-frío] Todo lo anterior necesita una llave root, y una cuenta nueva no tiene ninguna. La salida es la **sesión de cuenta**: el mismo JWT de una hora que devuelve `POST /account/login`. ```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":["*"]}' ``` Esos seis son el **set de arranque**, el mismo en las tres superficies que lo publican (esta página, el [quickstart](/docs/empezar/quickstart) y el contrato del arranque en frío). Es el mínimo con el que se llega desde acá hasta un documento emitido: crear la empresa pide `empresas:write` y suscribir un webhook pide `webhooks:write`, así que una llave con solo los dos de `documents:` muere en el paso siguiente con `403 insufficient_scope`. Ver [Scopes](/docs/autenticacion/scopes). Para dejar de sostener la contraseña de un humano, la sesión mintea una **llave root** en la misma puerta: ```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_…"} ``` La root **hereda el alcance de la sesión** (`["*"]`: toda empresa de la cuenta, presentes y futuras). Ese alcance es lo que la hace útil: la barrera de subconjunto compara las `empresas` de la llave minteada contra las de quien mintea, así que una root que no alcanzara ninguna empresa no podría producir ninguna llave que alcance alguna. Ella misma no puede usarlo — su único scope es `keys:write` —; lo usan las llaves que mintea. `empresas` en un mint root puede ir ausente, `[]` o `["*"]`; una lista más estrecha es `400 invalid_field` (se acota la llave minteada, no la root). `/dashboard/*` es una superficie deliberadamente **estrecha**: empresas, documentos, documentos recibidos, consumo, llaves de API y certificados, y nada más. En particular, no puede emitir. ### Si se perdió la contraseña [#si-se-perdió-la-contraseña] La sesión es la única puerta a la primera llave, así que perder la contraseña dejaba a una persona sin ninguna. Hay recuperación: ```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":"..."} ``` Pedir un enlace nuevo invalida los anteriores, y cambiar la contraseña invalida los que queden. Un token que no sirve responde `400 password_reset_token_invalid` sin decir cuál de las cuatro razones fue: distinguirlas sería un oráculo para adivinar una credencial, y el remedio es el mismo — pedir otro enlace. `POST /account/login` y `POST /account/register` reportan ambos `emailVerified`, así que un cliente puede mostrar el estado sin una segunda llamada. Mintear una llave `sk_live_` desde una cuenta sin verificar devuelve `403 email_verification_required`; el modo test no se ve afectado. # Límites de tasa (/docs/autenticacion/limites) ## Los headers [#los-headers] Cada respuesta **autenticada** lleva: ``` RateLimit-Limit: 300 RateLimit-Remaining: 287 RateLimit-Reset: 41 ``` Reportan el **bucket más ajustado que aplica**, no el general: un request que está a una emisión de agotar su cuota por segundo lo dice, aunque le queden 280 requests generales. En un `429`, el header `Retry-After` (en segundos) te dice exactamente cuánto esperar. No adivines con backoff exponencial cuando la respuesta trae la cifra. Los endpoints sin autenticación no se cuentan y no llevan estos headers: `GET /v1/error_codes` sirve una constante sin datos de nadie. ## Los buckets publicados [#los-buckets-publicados] Por defecto, **por credencial** — una llave o una sesión de cuenta, cada modo por separado: | Bucket | Límite | Ventana | Se cuenta por | | -------------------------------------------- | ----------- | ------------------ | -------------------- | | API general | 300 req/min | minuto deslizante | credencial | | `POST /v1/documents` | 10 req/s | segundo deslizante | credencial | | `POST /v1/tax/f29/filings` | 10 req/min | minuto deslizante | credencial + empresa | | `POST /v1/tax/sync` | 10 req/min | minuto deslizante | credencial + empresa | | `POST /v1/documents/{document_id}/send` | 20 req/min | minuto deslizante | credencial | | Descarga de artefactos (PDF, XML, recibidos) | 60 req/min | minuto deslizante | credencial | | `https://ver.facturia.cl/d/{slug}` | 60 req/min | minuto deslizante | **el enlace** | Lo que importa más que los números: * **Declaración y sincronización se cuentan aparte.** Comparten el límite, no el contador: uno solo dejaría que una ráfaga de sincronizaciones se gastara la cuota que la declaración necesita el 20. * **El envío por correo tiene bucket propio** porque recibe una lista de direcciones del cliente y adjunta un PDF — la forma exacta de un cañón de correo. 20/min es generoso para facturar e inservible para cualquier otra cosa. * **Los enlaces públicos se cuentan por slug, no por quien llama.** `/d/{slug}` es la única ruta sin credencial, así que el enlace es lo único estable que contar. No hay conteo por IP. * **No hay ráfaga.** Una versión anterior de esta tabla traía una columna "Ráfaga" que contradecía su propio texto; el limitador aplica las ventanas deslizantes de arriba y nada más. Dimensiona tus reintentos al límite, no a una ráfaga. Estos son **límites de partida**, y se suben cuando lo pides: cuéntanos tu volumen y ajustamos. No necesitas cambiar de plan para tener más throughput. Los publicamos para que puedas dimensionar reintentos y backoff, no como una promesa competitiva. El modo test aplica los mismos límites que live, así que una prueba de carga es representativa. ## El arranque en frío tiene sus propios topes [#el-arranque-en-frío-tiene-sus-propios-topes] `POST /account/register` y `POST /account/login` no llevan credencial, así que no se pueden contar por credencial. Se cuentan por dos ejes a la vez, y los dos están publicados porque una flota de agentes detrás de un mismo NAT llega al primero en su primera llamada y merece saber el número en vez de descubrirlo: | Bucket | Límite | Ventana | Se cuenta por | | ------------------------------------------------ | ----------------- | ------- | ------------------------------------- | | `POST /account/register` · `POST /account/login` | **10 por minuto** | 60 s | dirección IP, sobre todas las cuentas | | `POST /account/register` · `POST /account/login` | **5 por minuto** | 60 s | dirección de correo, desde donde sea | El tope por IP acota la CPU: cada una de esas puertas cuesta un bcrypt completo con costo 12, y la máquina es de una vCPU. El tope por correo es la mitad que una botnet no puede esquivar rotando direcciones. **Las dos ventanas cuentan todos los intentos, también los exitosos.** Pasado cualquiera de los dos recibes un `429` con `Retry-After` en segundos y este cuerpo: ```json { "status": 429, "message": "Demasiados intentos. Vuelve a intentar en un momento.", "errors": ["auth_throttled"] } ``` Ese es el sobre del arranque en frío (`{status, message, data, errors}`), no el de `/v1` — son dos superficies distintas y cada una responde el suyo. **Programa contra `errors[0] === 'auth_throttled'`, nunca contra el `message`.** > **Trampa — El rechazo no te dice cuál de los dos topes tocaste** > > Y es a propósito: decir «demasiados intentos para este correo» convertiría el limitador en un oráculo > de enumeración de cuentas — cualquiera podría averiguar qué direcciones tienen cuenta. No hay captcha, > también a propósito: esta superficie es para agentes. Si una flota legítimamente necesita más de 10 > registros por minuto desde una misma salida, háblanos antes de construir un loop de reintentos > alrededor: la respuesta es un arreglo de plataforma. ## Cómo se comporta un `429` [#cómo-se-comporta-un-429] Un `429` es del tipo `rate_limit_error`. La emisión está encolada de nuestro lado: una ráfaga sobre 10 req/s es un `429` del lado del cliente, **nunca un documento perdido**. Reintenta con el `Retry-After` que te devolvimos y el documento sale. ```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'); } ``` Reutilizar el mismo `Idempotency-Key` en cada reintento es lo que garantiza que cinco intentos produzcan un documento y no cinco. ## No hay una cuota de plan que se pueda agotar [#no-hay-una-cuota-de-plan-que-se-pueda-agotar] Los límites de arriba son de **tasa**. El plan es uno solo, mensual, **por empresa** y con **documentos ilimitados**, así que no existe un saldo de documentos que se consuma ni un tramo que se pase: si algo te frena, es el ritmo, no el volumen. Conectar una segunda empresa tampoco te acerca a ninguna cuota: es una suscripción más, al mismo precio, con los mismos límites de tasa. El diccionario publica un `402 plan_limit_reached` y hoy **nada lo emite**. Sigue ahí porque un código publicado no se retira sin doce meses de aviso, y porque un cliente que ya sabe manejar un `402` merece tener contra qué programar. Lo dice su propia ficha: [`plan_limit_reached`](/docs/errores/plan_limit_reached). El otro `402`, `subscription_required`, **sí se aplica** — pero no es una cuota: es una empresa sin suscripción activa intentando uno de cinco actos reales. Está detallado en [El sobre de error](/docs/errores#el-402). ## Las dos realidades de arriba [#las-dos-realidades-de-arriba] Por encima de nuestros límites hay dos realidades del SII para las que conviene diseñar: 1. **El SII limita tasa y no publica sus cifras.** 2. **El SII tiene ventanas de mantención programadas.** Ninguna de las dos te va a perder un documento — la emisión está encolada — pero un documento que ya aceptamos va a quedarse en `queued` más tiempo durante una caída del SII. Lo vas a notar en el propio documento antes que en ninguna otra parte: el `status` no avanza y `Facturia-Poll-After` te sigue devolviendo un próximo intento en vez de desaparecer. La disponibilidad de nuestros componentes está en [status.facturia.cl](https://status.facturia.cl), y en JSON en [`status.facturia.cl/status.json`](https://status.facturia.cl/status.json); si algo lleva detenido más de lo razonable, escríbenos con el `request_id` y te decimos en cuál de las dos mitades estás. > **Construye sobre webhooks** > > Un integrador que hace polling apretado durante una caída del SII se auto-limita justo cuando menos > información hay. Un integrador que escucha `document.accepted` no nota la diferencia salvo por la > latencia. La página de [Webhooks](/docs/webhooks) tiene el catálogo completo. # Conectar un agente (/docs/mcp) El mismo motor que la API REST, otra puerta de entrada: en vez de que una persona escriba `curl`, un agente de IA de terceros llama herramientas tipadas. ``` https://mcp.facturia.cl ``` Un solo endpoint MCP (`/mcp`) hablando **Streamable HTTP**, y un solo conjunto de endpoints OAuth en el mismo origen. No hay subdominios por empresa ni por cuenta: la tenencia es un claim del token, no un hostname. ## El hecho arquitectónico [#el-hecho-arquitectónico] > **El servidor MCP es un cliente REST de /v1** > > No tiene lógica de negocio ni credenciales del motor: ni certificado del SII, ni llave de firma, ni > acceso al esquema del motor. Cada pregunta de dominio — cuáles son los totales de un documento, si > hay folios disponibles, qué dice el F29 — es un request a `/v1`, nunca una consulta. > > Eso significa que todo lo que vale en esta documentación sobre estados, errores y garantías vale > igual desde un agente. ## Por qué el agente no es un desarrollador [#por-qué-el-agente-no-es-un-desarrollador] Un desarrollador lee la documentación antes de escribir la primera línea; un agente decide qué llamar en el mismo turno en que se lo piden. Este servidor publica sus páginas como **recursos MCP** —las devuelve enteras, no como un enlace, así que un cliente sin capacidad de fetch también las lee— y aun así cada herramienta tiene que describirse sola: la documentación explica el modelo, la descripción de la herramienta es lo único que el modelo tiene a mano cuando decide. De ahí salen tres compromisos de diseño de esta capa: **Suite completa, no un wrapper delgado.** Emisión, recepción y la capa tributaria están todas aquí. **Ejecución directa, con una salida de emergencia para quien la quiera.** El paso de aprobación de herramientas del propio cliente MCP es el humano en el ciclo por defecto. Las empresas que quieran más pueden encender un doble paso borrador-y-confirmación. **Ser AI-first es resolver ambigüedad, no rechazarla.** `draft_document` acepta un receptor por nombre, porque así le habla una persona a un agente. Los ítems van estructurados (`[{description, quantity, unit_price}]`): la prosa la convierte el modelo, que es quien la entiende mejor y más barato, y `draft_document` devuelve los totales para que la persona los vea **antes** de que se firme nada. No hay un parser de prosa en la API, y no lo va a haber: un parser escondido detrás de un endpoint convierte una cantidad mal leída en un documento tributario. ## Agregar a tu cliente [#agregar-a-tu-cliente] Una sola dirección, `https://mcp.facturia.cl/mcp`, y ningún comando que instalar: el transporte es **Streamable HTTP**, así que no hay paquete que bajar ni proceso local que levantar. Tampoco hay un secreto que pegar en el archivo de configuración — el cliente descubre el OAuth solo (RFC 9728), se identifica con su documento de metadatos o se registra solo (RFC 7591), y te abre la pantalla de consentimiento en el navegador, donde tú decides qué puede hacer el agente y sobre qué empresas. #### Claude (mcp.json) En Claude Desktop: **Settings → Connectors → Add custom connector**, con la URL `https://mcp.facturia.cl/mcp`. Si prefieres editar el archivo a mano (`claude_desktop_config.json`), es una línea: ```json { "mcpServers": { "facturia": { "url": "https://mcp.facturia.cl/mcp" } } } ``` #### Claude Code ```bash claude mcp add --transport http facturia https://mcp.facturia.cl/mcp ``` La primera llamada a una herramienta abre el navegador para autorizar. Después, `claude mcp list` muestra la conexión y `claude mcp remove facturia` la saca de este cliente (para cortar el acceso de verdad, revoca desde el panel — ver [Revocación](#revocación)). #### claude.ai En la web: **Settings → Connectors → Add custom connector**, con la misma URL `https://mcp.facturia.cl/mcp`. No hay archivo que editar; la autorización se abre ahí mismo. #### Cursor En `~/.cursor/mcp.json` (global) o `.cursor/mcp.json` (por proyecto), el mismo objeto: ```json { "mcpServers": { "facturia": { "url": "https://mcp.facturia.cl/mcp" } } } ``` Cursor muestra el servidor en **Settings → MCP**; la autorización se abre en el navegador la primera vez que el agente llama una herramienta. #### VS Code Con GitHub Copilot en modo agente, en `.vscode/mcp.json` del proyecto (o en la configuración de usuario), el mismo servidor remoto: ```json { "servers": { "facturia": { "type": "http", "url": "https://mcp.facturia.cl/mcp" } } } ``` VS Code abre el navegador para autorizar la primera vez que el agente usa una herramienta. #### Windsurf En `~/.codeium/windsurf/mcp_config.json`, el mismo objeto que Claude y Cursor: ```json { "mcpServers": { "facturia": { "serverUrl": "https://mcp.facturia.cl/mcp" } } } ``` #### ChatGPT En ChatGPT los servidores MCP se agregan como **conector**: ajustes → conectores → agregar, apuntando a `https://mcp.facturia.cl/mcp`. La disponibilidad depende del plan y del modo (los conectores personalizados están en los planes de pago y en modo desarrollador). Cualquier cliente que hable MCP sobre Streamable HTTP con OAuth sirve igual: no hay nada específico de un cliente en este servidor. > **¿Nunca has creado una cuenta?** > > La pantalla de consentimiento te deja iniciar sesión ahí mismo y crear la llave del grant por ti — > no tienes que traerla lista. Y si todavía no tienes cuenta, el enlace de esa pantalla te lleva a > crearla y **te devuelve a la misma autorización** cuando terminas: no hay que reconectar el agente. > > ¿Tampoco tienes empresas todavía? Tampoco es un problema: marca «Todas las empresas de esta cuenta, > incluidas las que cree más adelante» junto con `empresas:write`, y el agente crea la primera con > `create_empresa`. Escribiendo `https://mcp.facturia.cl` en el navegador sale la misma información en una página: qué es, el snippet y el enlace a esta documentación. **Cómo se identifica el cliente.** La especificación de MCP fija un orden de prioridad para que un cliente diga quién es: un cliente pre-registrado primero, después un documento de metadatos del `client_id` (CIMD), y el registro dinámico al final, como respaldo. * **Documento de metadatos del `client_id` (CIMD).** El cliente puede usar una URL https como `client_id` — por ejemplo `https://claude.ai/.well-known/oauth-client.json` — en vez de registrarse antes de conectarse. El servidor la obtiene al momento de la conexión (GET, 3 s de plazo, 32 KB máximo, sin redirecciones, nunca hacia una dirección privada o loopback) y valida el JSON: el `client_id` que declara el documento tiene que ser exactamente esa URL, `client_name` y `redirect_uris` son obligatorios, y solo se aceptan clientes públicos (`token_endpoint_auth_method` ausente o `"none"`, PKCE S256 como siempre). El resultado se cachea respetando `Cache-Control: max-age`, acotado entre 1 y 24 horas — un error nunca se cachea. El `redirect_uri` de cada autorización tiene que calzar exacto con uno de los que declara el documento, la misma regla que un cliente registrado — con una sola excepción, la de RFC 8252 §7.3: si el documento declara `http://localhost/…` o `http://127.0.0.1/…`, el puerto no se compara, porque un cliente nativo como Claude Code abre uno distinto en cada sesión. * **Registro dinámico de cliente (RFC 7591).** El respaldo para el cliente que todavía no publica un CIMD — Cursor, VS Code, Windsurf, ChatGPT y similares siguen aquí. Cualquier cliente MCP se registra en la primera conexión, sin aprobación previa ni lista blanca de aplicaciones. El registro devuelve un `client_id` público; no hay `client_secret`, porque PKCE es el mecanismo de confidencialidad de un cliente público que no puede guardar un secreto. ``` POST https://mcp.facturia.cl/register (RFC 7591) GET https://mcp.facturia.cl/.well-known/oauth-authorization-server ``` Ese mismo `GET` declara si el servidor acepta CIMD: ```json { "client_id_metadata_document_supported": true } ``` **Qué ve la persona en la pantalla de consentimiento.** Con un cliente CIMD la pantalla muestra su nombre y el origen de su documento de metadatos («Claude · claude.ai») — ese origen es verificable, porque ahí vive el documento. Un cliente que se registró dinámicamente sigue con la advertencia «Esta aplicación se registró sola y eligió su propio nombre — nosotros no lo verificamos». En los dos casos la pantalla sigue mostrando el host al que va el código de autorización («A dónde va esto»), con una advertencia extra si ese host es `localhost` o `127.0.0.1`. **Autorización y consentimiento.** Flujo estándar de authorization-code + PKCE. La pantalla de consentimiento no es un timbre de goma: ahí decides qué credencial de Facturia queda envuelta en el grant. ``` 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` cambia el código (con el verificador PKCE) por un access token de vida corta y un refresh token de vida más larga, revocable individualmente. ```json { "access_token": "mcpat_9lQpV3fA2mKcRt7wZxYb1Nde", "token_type": "bearer", "expires_in": 3600, "refresh_token": "mcprt_qT8vN2wLmR5xKpJ7hYdCbA3s", "scope": "documents:read documents:write tax:read" } ``` ## Qué carga el token [#qué-carga-el-token] El mismo modelo de autorización que una llave de API, porque debe comportarse igual una vez emitido: * una **cuenta**, * un **modo** — el token puede ser solo-sandbox, live o mixto, según lo que se concedió; * un **conjunto de scopes**, con el mismo vocabulario que los scopes REST: `documents:read`, `documents:write`, `received:read`, `received:write`, `tax:read`, `tax:write`, `empresas:read`, `usage:read` y, para la puesta en marcha, `onboarding:write`, `mandato:write` y `empresas:write`; * una **lista blanca de empresas** — RUT explícitos, o `"*"`. > **Los dos scopes de atestación no existen en esta superficie** > > `/v1` también define `mandato:attest` y `onboarding:attest`: aceptar el mandato con un cuerpo, y > autorizar la puesta en vivo con uno. Esos son el momento en que una **persona** dice «sí, acepto» y > «sí, este mes», y quedan registrados como evidencia de esa decisión. > > Un agente no puede pedirlos acá, y no es una omisión: las herramientas MCP acuñan el enlace y se > detienen; la persona decide en la página. Si el agente pudiera atestar, el registro de tu aprobación > sería la palabra del agente. ## Nombrar la empresa en una llamada de herramienta [#nombrar-la-empresa-en-una-llamada-de-herramienta] La lista de un token puede abarcar varias empresas (el agente de un contador que lleva varios clientes, digamos), así que cada herramienta con alcance de empresa toma un argumento `empresa` explícito — el análogo directo del header `Facturia-Empresa`. | Situación | Comportamiento | | ----------------------------------------------------------------------- | -------------------------------------------------------- | | Omitido donde se requiere | La herramienta devuelve `empresa_required` | | Presente pero fuera de la lista | `empresa_not_allowed` — nunca un «no encontrado» a secas | | Un token con exactamente una empresa | Puedes omitirlo; el servidor lo rellena | | Herramientas de nivel cuenta (`list_empresas`, `get_usage` sin empresa) | Rechazan el argumento | ## El límite del sandbox [#el-límite-del-sandbox] > **Trampa — Un grant de sandbox es una capacidad, no una política** > > Un token con alcance de sandbox **no puede** alcanzar los datos de una empresa live ni emitir nada > con efecto legal. No es una preferencia que se pueda cambiar en caliente: es un límite estructural. > > Por eso es el default correcto para cualquier cosa exploratoria o de cara al público. El registro > dinámico de cliente significa que el software que llama **no está verificado por construcción**, y el > alcance del token es la defensa principal contra un agente comprometido o demasiado confiado. La pantalla de consentimiento defiende lo mismo desde el otro lado: no viene nada premarcado. Eliges explícitamente qué empresas puede tocar este agente. Conceder `"*"` es posible pero exige un clic extra de confirmación — la asimetría es intencional, porque un grant a un agente de terceros demasiado confiado es un riesgo materialmente distinto de una llave de API de primera parte guardada en tu propio backend. ## Revocación [#revocación] En el panel, **Agentes conectados** lista cada grant MCP de la cuenta: qué cliente se conectó, con qué scopes, sobre qué empresas, en qué modo, cuándo y cuándo lo usó por última vez. Revocar uno invalida de inmediato sus access y refresh tokens. Hay un segundo camino que sirve igual: cada grant envuelve una llave de API tuya, así que revocar esa llave en **Llaves** mata el grant en el mismo instante. ## Límites de tasa [#límites-de-tasa] Las llamadas MCP pegan a los mismos servicios de backend que las REST y comparten los mismos buckets publicados. No hay cuota MCP separada, porque no hay un pipeline de emisión separado por debajo. | Bucket | Límite | Qué herramientas | | ---------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | General | 300 req/min | `draft_document`, todas las `list_*` / `get_*`, `query_rcv`, `get_periodo_summary`, y las escrituras que no emiten ni declaran (`update_*`, `set_*`, `characterize_purchases`, `send_document`, `request_folios`, …) | | Emisión | 10 req/s, ráfaga 30 | `emit_document`, `emit_tipo46`, `emit_credit_note` | | Declaración F29 y sincronización del RCV | 10 req/min por empresa | `file_f29`, `sync_rcv` (una llamada por período; un rango de diez meses gasta el bucket entero) | | Artefactos | 60 req/min | `get_document_file` (el minteo de la URL firmada, no la descarga posterior, que es pública y no medida) | Una llamada limitada devuelve el tipo `rate_limit_error` con un campo `retry_after_seconds` sobre el que un agente puede actuar directamente. - [Catálogo de herramientas](/docs/mcp/herramientas): Todas las herramientas del servidor, con sus anotaciones y cuáles pasan por confirmación. - [Confirmaciones](/docs/mcp/confirmaciones): El doble paso para emitir, declarar y reclamar. # Catálogo de herramientas (/docs/mcp/herramientas) Esta página se genera desde el registro real de herramientas del servidor: si una herramienta existe, está acá, con las mismas anotaciones que ve tu cliente MCP. **Hasta 90 herramientas**, según los scopes del grant. ## Convenciones que comparten todas [#convenciones-que-comparten-todas] * El primer argumento de cada herramienta con alcance de empresa es `empresa`. Las de nivel cuenta lo rechazan. * Un argumento con `?` es opcional. * **Las anotaciones son las del protocolo**, leídas del mismo objeto `annotations` que recibe tu cliente: «solo lectura» es `readOnlyHint`, «irreversible» es `destructiveHint`, «idempotente» es `idempotentHint`. Tu cliente decide con eso si te pregunta cada vez o no. * **La idempotencia es trabajo del servidor, no del agente.** Un cliente REST manda su propio `Idempotency-Key`; un agente MCP no debería tener que pensar en seguridad de reintento. El servidor genera y administra la llave por debajo — anclada al `preview_id` cuando la llamada vino de `draft_document`, o a un UUID nuevo por llamada — así que un reintento tras una conexión cortada reutiliza el folio ya asignado en vez de quemar un segundo. * **Esta página lista todas. TU CONEXIÓN puede ver menos.** `tools/list` viene filtrado por los permisos que otorgó la persona: una herramienta aparece solo si todo lo que está en su columna **Permisos** está en el grant. Una conexión con `documents:read empresas:read` no recibe `emit_document`, porque prometer una factura que no puede emitir es peor que no ofrecerla. Cada herramienta publica además su requisito en `_meta.requiresScope`, para que un cliente pueda decir POR QUÉ falta alguna. Llamar a una que el grant no cubre sigue respondiendo `insufficient_scope`, con el permiso que falta y cómo conseguirlo: oculta no es lo mismo que inexistente. * **Casi todas necesitan `empresas:read`**, incluso las que parecen solo de escritura: nombrar una empresa implica leer la lista de empresas que esta conexión alcanza, que es también donde se chequea el límite del sandbox. * **Los errores son errores REST.** El contenido de error de una herramienta es el mismo objeto `{ type, code, message, hint, retriable, param, doc_url, request_id, empresa }` del [sobre de errores](/docs/errores), devuelto como contenido de error MCP. * Plata, fechas y RUT usan exactamente las convenciones de la API: enteros en CLP, fechas civiles `YYYY-MM-DD` en America/Santiago, RUT canónico. ## Emisión [#emisión] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ----------------------------------- | -------------- | | `draft_document` | Previsualiza antes de firmar nada: resuelve el receptor, valoriza los ítems, calcula totales y previsualiza (nunca reserva) el folio. Sin efectos. | `empresa?`, `tipo_dte`, `receptor?`, `items`, `issue_date?`, `references?` | `preview` con `resolved_receptor`, `resolved_items`, `totals`, `folio_preview`, `warnings[]`, `preview_id` y `expires_at`. | escribe · reversible | `empresas:read` + `documents:write` | no | | `emit_document` | Emite de verdad. Sandbox: simulado y determinista. Live: ida y vuelta real al SII, folio real, registro tributario real. | `empresa?`, `preview_id?`, `tipo_dte?`, `receptor?`, `items?`, `issue_date?`, `references?`, `external_id?`, `receptor_details?` | el objeto `document` — o un sobre `pending_confirmation` si la empresa exige confirmación. | escribe · irreversible · idempotente | `empresas:read` + `documents:write` | **sí** | | `get_document_status` | Chequeo de estado del tamaño de una narración: no el documento completo, para que un agente que hace polling no arrastre ítems y totales a su contexto cada vez. | `empresa?`, `document_id?`, `external_id?`, `tipo_dte?`, `folio?` | `status`, bloque `sii`, `rejection_reasons` y los timestamps clave. | solo lectura | `empresas:read` + `documents:read` | no | | `get_document_file` | Un PDF o XML como `resource_link` con vida de 10 minutos, nunca como bytes embebidos. `copy: "cedible"` elige la copia cedible (solo tipos 33 y 34, solo en PDF). | `empresa?`, `document_id`, `format?`, `copy?` | un bloque `resource_link` más la URL en texto como respaldo. | escribe · reversible | `empresas:read` + `documents:read` | no | | `list_documents` | Buscar y navegar lo emitido. También sirve para «tráeme un documento completo» filtrando por id o folio, que es por qué no hay una herramienta `get_document` aparte. | `empresa?`, `status?`, `tipo_dte?`, `folio?`, `receptor_rut?`, `external_id?`, `issue_date_gte?`, `issue_date_lte?`, `cursor?`, `limit?` | sobre de lista de `document`. | solo lectura | `empresas:read` + `documents:read` | no | | `get_folio_status` | ¿Va a alcanzar la próxima emisión? Muestra el stock de folios por tipo antes de gastar una llamada en descubrir que no hay. | `empresa?`, `tipo_dte?` | stock por tipo de DTE, con el rango vigente y cuántos quedan. | solo lectura | `empresas:read` | no | | `send_document` | Le manda por correo al receptor un documento ya emitido, en PDF y/o XML firmado. Siempre la copia ORIGINAL, nunca la cedible. Sin `to` va a las direcciones guardadas en el receptor del documento; con `to` va a donde le digan — y si no hay ninguna guardada ni entregada, `no_recipient` lo dice y hay que preguntar, jamás inventar una dirección. | `empresa?`, `document_id?`, `to?`, `attach?`, `job_id?` | Un `job` de tipo document\_send. El 202 es «encolado», nunca «llegó»: con `job_id` se lee `result.delivered`, y en sandbox `result.simulated` es true y no llegó a ningún buzón real. Repetir el mismo envío a las mismas direcciones devuelve el trabajo ORIGINAL y no manda un segundo correo — eso es el deduplicado funcionando, no una falla. | escribe · reversible | `empresas:read` + `documents:write` | no | | `emit_credit_note` | Emite una nota de crédito (tipo 61) contra un documento que ya está en el SII. En Chile no existe «borrar una factura»: el código 1 la ANULA, el 2 corrige el texto y el 3 corrige los montos (y ahí sí van los `items` corregidos). Es un segundo documento tributario REAL, con su propio folio. Exige `confirm_emit: true`, y si la empresa pidió aprobación humana para las escrituras del agente esta herramienta se NIEGA en vez de emitir. | `empresa?`, `document_id`, `code`, `reason`, `items?`, `external_id?`, `confirm_emit?` | La nota de crédito emitida, con su folio, su total y su estado, y qué documento anula o corrige. Si quedó `queued` avisa que sigue en vuelo en el SII y que no se emita una segunda: el folio ya se gastó. | escribe · irreversible · idempotente | `empresas:read` + `documents:write` | no | | `retry_document` | Vuelve a encolar un documento atascado en `failed` — se acabaron los folios, venció el certificado, el SII estaba caído. REUSA el folio ya asignado y nunca crea un segundo documento, que es justamente por qué reintentar es la respuesta correcta a una falla y re-emitir no lo es. Solo `failed` se puede reintentar; cualquier otro estado se rechaza nombrando el estado real. | `empresa?`, `document_id` | El documento de vuelta en `queued`. La narración dice que se reusa el MISMO folio, que no existe un segundo documento y que hay que mirar get\_document\_status en vez de darlo por aceptado. | escribe · reversible · idempotente | `empresas:read` + `documents:write` | no | | `get_document_events` | La línea de tiempo de un documento, de lo más viejo a lo más nuevo: cuándo se firmó, cuándo se transmitió, qué contestó el SII y cuándo, si se envió por correo, si una nota de crédito lo anuló. Es la herramienta para «¿qué pasó con esta factura?»; get\_document\_status responde «dónde está ahora», que es otra pregunta y mucho más corta. | `empresa?`, `document_id` | Los eventos con su tipo, su hora y su detalle. Una lista vacía no es un error: un documento emitido hace segundos todavía no tiene ninguno, y eso no es lo mismo que el documento no existir. | solo lectura | `empresas:read` + `documents:read` | no | | `check_cesion` | Le pregunta al SII si una factura se puede ceder hoy. Es un REPORTE, nunca un cálculo nuestro y nunca la cesión misma: la Ley 19.983 la hace cedible con el acuse del receptor, o con ocho días corridos de silencio, y una venta al contado nunca lo es — solo el SII sabe cuál aplica. Es un POST que hace una LECTURA: la operación del registro que consulta no lleva código de acción y no puede cambiar nada allá. | `empresa?`, `document_id` | El documento con su bloque `cesion`. `cedible` en null significa «nadie ha preguntado todavía», JAMÁS «no es cedible» — la narración lo dice con esas palabras. Termina diciendo que no se cedió ni se registró nada: cede\_document es el acto. | solo lectura | `empresas:read` + `documents:read` | no | | `cede_document` | Cede el crédito de una factura a un tercero, registrándolo en el Registro Electrónico de Transferencia de Crédito del SII. ASIGNA PLATA que le deben a otra empresa y NO SE PUEDE DESHACER. Ninguna de las cuatro acciones de la puerta de confirmación cubre una cesión, así que exige `confirm_irreversible: true` y todos los datos del cesionario sin adivinar ninguno: un RUT equivocado le cede el crédito a la empresa equivocada. | `empresa?`, `document_id?`, `cesionario_rut?`, `cesionario_razon_social?`, `cesionario_direccion?`, `cesionario_email?`, `monto?`, `ultimo_vencimiento?`, `otras_condiciones?`, `email_deudor?`, `job_id?`, `confirm_irreversible?` | Un `job` de tipo cesion\_submit. UN ACUSE NO ES UNA ANOTACIÓN: mientras el trabajo esté `running` con `registration: pending`, el AEC fue RECIBIDO por el registro con su TrackID y la anotación está pendiente — la narración lo dice así y prohíbe liquidar plata sobre eso. Solo `registration: confirmed` es cesión registrada. Un segundo intento tras uno que ya puso un AEC firmado en el cable se rechaza y no se reintenta. | escribe · irreversible · idempotente | `empresas:read` + `documents:write` | no | | `revoke_document_links` | Revoca todos los enlaces públicos vivos de un documento — las URL firmadas y sin credencial que mintea get\_document\_file. Sirve cuando un enlace se fue a la persona equivocada: dejan de resolver al tiro. | `empresa?`, `document_id` | La confirmación de que quedaron revocados, diciendo lo que NO hace: no recupera una copia ya descargada ni un correo ya entregado, y el documento sigue emitido y en el SII. get\_document\_file mintea uno nuevo cuando haga falta. | escribe · reversible | `empresas:read` + `documents:write` | no | `draft_document` devuelve `receptor_incomplete` con el campo exacto que falta, nunca una adivinanza silenciosa. `emit_document` puede devolver `totals_mismatch` si el agente mandó totales explícitos que no cuadran — la descripción de la herramienta le dice que nunca calcule totales a mano salvo que esté seguro, precisamente porque ese campo es una aserción, no una pista. ## Recepción [#recepción] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------- | -------------- | | `list_received_documents` | Navegar los DTE que te enviaron tus proveedores, filtrables por estado de respuesta comercial. | `empresa?`, `status?`, `tipo_dte?`, `issue_date_gte?`, `issue_date_lte?`, `cursor?`, `limit?` | sobre de lista de resúmenes. | solo lectura | `empresas:read` + `received:read` | no | | `get_received_document` | El detalle completo de uno, incluido el plazo del acuse. | `empresa?`, `received_document_id` | `received_document` completo, con `commercial_response` (`deadline_at`, `days_remaining`, `available_actions`). | solo lectura | `empresas:read` + `received:read` | no | | `accept_received_document` | Registrar la aceptación explícita (ACD) de un documento recibido. | `empresa?`, `received_document_id` | `commercial_response`. | escribe · irreversible · idempotente | `empresas:read` + `received:write` | no | | `reclaim_received_document` | Registrar un reclamo: contenido, parcial o total. Irreversible una vez registrado en el SII. | `empresa?`, `received_document_id`, `reason`, `note?` | `commercial_response` — o un sobre `pending_confirmation`. | escribe · irreversible · idempotente | `empresas:read` + `received:read` + `received:write` | **sí** | | `acknowledge_received_document` | Otorga el acuse de recibo (acción SII ERM) de un documento recibido: registra que la mercadería o el servicio realmente llegó. NO es aceptación del contenido — esa es accept\_received\_document (ACD). Un documento puede llevar ERM, ACD, ambos o ninguno. Nunca se gatilla por require\_confirmation, igual que la aceptación. | `empresa?`, `received_document_id` | El CommercialResponse registrado (sii\_code ERM, state receipt\_acknowledged) con una frase que aclara explícitamente que no es aceptación y que no hay forma de deshacerlo. | escribe · irreversible · idempotente | `empresas:read` + `received:write` | no | > **Trampa — Las descripciones de estas herramientas dicen «8 días corridos» en su propio texto** > > `get_received_document` y `reclaim_received_document` declaran la ventana de 8 días en su propia descripción y > exponen `days_remaining` de forma prominente. Un agente narrando «te quedan 2 días para responder» solo funciona > si el plazo es imposible de pasar por alto en el esquema. > > Errores frecuentes: `commercial_response_window_closed` (pasado el día 8), > `commercial_response_already_registered` (un solo tiro), `commercial_response_not_applicable` (tipo equivocado — > solo 33/34/43 llevan respuesta comercial). ## Impuestos [#impuestos] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------ | -------------- | | `get_periodo_summary` | La posición de IVA de un período, lista para narrar: débito, crédito, retenido, remanente, PPM y `total_payable`, con un párrafo en español citable tal cual. | `empresa?`, `period`, `compare_previous?` | las cifras crudas más un `narrative` de un párrafo y, si se pide, el delta del período anterior. | solo lectura | `empresas:read` + `tax:read` | no | | `query_rcv` | Leer filas del Registro de Compras y Ventas, compras y/o ventas, con filtros por período, contraparte, tipo y estado contable. | `empresa?`, `direction?`, `period?`, `estado_contable?`, `counterparty_rut?`, `tipo_dte?`, `issue_date_gte?`, `issue_date_lte?`, `cursor?`, `limit?` | sobre de lista de `rcv_entry` más un bloque `sync` de frescura. | solo lectura | `empresas:read` + `tax:read` | no | | `sync_rcv` | Pide al SII una lectura fresca del Registro de Compras y Ventas para un período — o para un rango de hasta diez meses, que es cómo se llega a la historia que ni el barrido diario (últimos tres meses) ni el relleno único de doce meses tras la primera sincronización leyeron. Con `job_id` consulta cómo va una sincronización ya pedida. | `empresa?`, `period?`, `from?`, `to?`, `job_id?` | lista de `job` de tipo `rcv_sync` (uno por período) con `status`; el `sync.last_synced_at` de `query_rcv` se mueve cuando cada uno termina. | escribe · reversible · idempotente | `empresas:read` + `tax:write` | no | | `get_f29_propuesta` | El F29 calculado del período con cada código explicado en lenguaje llano, y el archivo de carga al SII como enlace. | `empresa?`, `period` | el objeto propuesta; cada fila de `codes[]` lleva `explanation` y `contributing_entries`. | escribe · reversible | `empresas:read` + `tax:read` | no | | `file_f29` | Declarar el F29 — o, pasando `filing_id`, consultar una declaración ya hecha en vez de crear un duplicado. | `empresa?`, `period`, `confirm_total_payable?`, `filing_id?` | job `f29_filing` — o un sobre `pending_confirmation`. | escribe · irreversible · idempotente | `empresas:read` + `tax:read` + `tax:write` | **sí** | | `list_tipo46_suggestions` | Compras de servicios digitales extranjeros que están esperando una factura de compra. | `empresa?`, `status?`, `period?` | lista de `suggestion`. | solo lectura | `empresas:read` + `tax:read` | no | | `emit_tipo46` | Confirmar una sugerencia: emite la factura de compra tipo 46 y deja el IVA retenido como crédito fiscal. | `empresa?`, `suggestion_id` | `document` — o un sobre `pending_confirmation`. | escribe · irreversible · idempotente | `empresas:read` + `tax:read` + `tax:write` | **sí** | | `characterize_purchases` | Fija el tipo de compra de una o varias entradas del registro de compras — la decisión que dice si el IVA de cada compra es crédito fiscal recuperable y, por lo tanto, qué cargan los códigos 519/520 del F29. Una entrada es el caso simple; hasta 500 por llamada para «ordenar el mes completo». | `empresa?`, `entries`, `confirmation_token?` | El lote (`characterization_batch`): cuántas instrucciones quedaron encoladas, la acción de cada entrada con su tipo\_compra anterior, y las que fallaron con su razón. En esta build las escrituras en vivo están apagadas: cada acción termina en `dry_run` y el registro del SII NO se mueve — la narración lo dice con todas sus letras. Si la empresa tiene la puerta de confirmación, devuelve el sobre `pending_confirmation` con su URL y no encola nada. | escribe · reversible | `empresas:read` + `tax:write` | no | | `revert_characterization` | Deshace la última caracterización asentada de una compra, devolviéndola al `previous_tipo_compra` que quedó registrado. Encola una acción NUEVA enlazada a la que deshace: no edita ni borra la original, porque una escritura a un registro tributario que ocurrió, ocurrió, y su fila es la evidencia. | `empresa?`, `entry_id`, `iva_no_recuperable_code?`, `confirmation_token?` | La acción encolada (`characterization_action`) con `reverts` apuntando a la acción deshecha. Igual que la caracterización: en esta build no llega al SII y termina en `dry_run`. Pasa por la misma puerta de confirmación. | escribe · reversible | `empresas:read` + `tax:write` | no | | `set_estado_contable` | Anota el estado contable LOCAL de una compra —REGISTRO, PENDIENTE o NO\_INCLUIR— al lado del valor del SII y nunca encima: el `estado_contable` del registro del SII lo escribe la sincronización y manda siempre. RECLAMADO no se puede fijar acá: eso es consecuencia de un reclamo registrado, no una elección. | `empresa?`, `entry_id`, `estado_contable` | La entrada actualizada (`rcv_entry`), con el valor del SII intacto, el override y su `honored`, y `estado_contable_effective` — el que la propuesta F29 va a usar de verdad. Si la empresa no tiene `honor_estado_contable_override`, la narración dice explícitamente que el crédito fiscal no cambió. | escribe · reversible · idempotente | `empresas:read` + `tax:write` | no | | `list_f29_propuestas` | Lista los períodos que tienen propuesta F29 calculada y en qué estado está cada uno (`ready`, `computing`, `stale`, `blocked`), del mes más nuevo al más viejo. Responde «qué meses están listos» sin sondear doce períodos y cosechar once 404. | `empresa?`, `cursor?`, `limit?` | Una lista de propuestas con período, estado, total a pagar y vencimiento. Un mes que NO aparece nunca fue calculado — la narración lo llama «desconocido», jamás «no se debía nada». | solo lectura | `empresas:read` + `tax:read` | no | | `recompute_f29_propuesta` | Pide recalcular la propuesta F29 de un período con los libros como están ahora, guardando opcionalmente overrides por código. No declara nada y no toca al SII. Cada override exige una razón escrita: una cifra tributaria sobrescrita sin razón registrada es inauditable. | `empresa?`, `period`, `overrides?` | La propuesta en `computing` — está ENCOLADA, no recalculada. `get_f29_propuesta` del mismo período la refleja en un momento; hasta entonces no se cita un total ni se llama a file\_f29. | escribe · reversible · idempotente | `empresas:read` + `tax:write` | no | | `list_f29_filings` | Todas las declaraciones F29 registradas para la empresa, de la más nueva a la más vieja, con el período, el total declarado y hasta dónde llegó cada una. Es la forma de lista del `filing_id` de file\_f29, sin declarar nada. | `empresa?`, `period_gte?`, `period_lte?`, `status?`, `cursor?`, `limit?` | Una lista de `f29_filing`. La narración cuida el vocabulario: `submitted` no es `accepted`, `rejected` significa que NO quedó declarada, y `dry_run` es un ensayo que nunca salió. Un período ausente no fue declarado POR FACTURIA — no dice nada sobre lo que alguien haya declarado a mano en el SII. | solo lectura | `empresas:read` + `tax:read` | no | | `list_expenses` | Los cargos que Facturia tiene para la empresa —cargados por API, traídos de un feed bancario o parseados de una cartola—, que son la materia prima del detector de facturas de compra tipo 46. | `empresa?`, `period?`, `status?`, `source?`, `cursor?`, `limit?` | Una lista de `expense` con proveedor, monto original y su equivalente en pesos, origen y la sugerencia o el documento que cuelga de cada uno. Un cargo sin sugerencia no es necesariamente inelegible: la detección es asíncrona. | solo lectura | `empresas:read` + `tax:read` | no | | `add_expense` | Registra un cargo para que el detector de tipo 46 pueda verlo — la puerta por la que entra una compra de servicios digitales del exterior cuando no hay feed bancario de la tarjeta con que se pagó. No emite nada ni toca al SII. Un cargo en pesos NUNCA es candidato a tipo 46; uno en moneda extranjera EXIGE `fx_rate`, porque la tasa fija la base imponible del documento y Facturia no la inventa. | `empresa?`, `provider_name`, `provider_country?`, `description?`, `amount_original`, `currency`, `date`, `fx_rate?`, `fx_source?`, `external_id?` | El cargo guardado (`expense`) con su monto convertido a pesos y su período. La sugerencia llega después, de forma asíncrona: se lee con list\_tipo46\_suggestions y se confirma con emit\_tipo46. | escribe · reversible | `empresas:read` + `tax:write` | no | | `list_statements` | Las cartolas subidas para la empresa y cómo resultó cada importación. Es donde se mira cuando alguien dice «subí mi cartola y no pasó nada». | `empresa?`, `cursor?`, `limit?` | Una lista de `statement` con filas leídas, cargos creados, duplicados saltados y los rechazos fila por fila con su número de línea. El parser rechaza en vez de adivinar, así que esos rechazos son la instrucción de arreglo, no un detalle. | solo lectura | `empresas:read` + `tax:read` | no | | `upload_statement` | Sube una cartola bancaria o de tarjeta (base64, porque una llamada de herramienta no puede llevar un archivo) para que sus líneas en moneda extranjera se conviertan en cargos. En una cartola no hay credencial: son los propios cargos del usuario. | `empresa?`, `file_base64`, `filename?`, `format?`, `default_currency?` | Un job de importación: está IMPORTANDO, no importada. Si los bytes son idénticos a una subida anterior, la API reconoce el sha256 antes de guardar nada y devuelve el job ORIGINAL con 200 — la herramienta lo narra como «ya estaba», sin segundo parseo ni doble conteo (#206). | escribe · reversible · idempotente | `empresas:read` + `tax:write` | no | | `dismiss_tipo46_suggestion` | Descarta una sugerencia de factura de compra tipo 46 con una razón de la que el detector aprende. No emite nada y el cargo queda en ficha. Es la otra mitad del par con emit\_tipo46. | `empresa?`, `suggestion_id`, `reason?` | La sugerencia en `dismissed`. La narración dice cuánto IVA de crédito se está dejando de recuperar, porque descartar cuesta plata de verdad y esa es la consecuencia de la llamada. | escribe · reversible | `empresas:read` + `tax:write` | no | | `list_bank_connections` | Los feeds bancarios y de tarjeta enlazados a la empresa, con el estado de cada uno y cuándo sincronizó por última vez. Nunca devuelve credenciales: viven en el proveedor y Facturia solo guarda un token de enlace. | `empresa?`, `cursor?`, `limit?` | Una lista de `connection`. El estado es lo importante: `pending_link` significa que falta que una persona complete el enlace y no está llegando nada; `requires_relink` es la falla típica de los feeds bancarios y explica un mes vacío que no es vacío; solo `active` sincroniza. | solo lectura | `empresas:read` + `tax:read` | no | | `link_bank_connection` | Crea una conexión bancaria o de tarjeta y devuelve el `link_url` que una PERSONA abre una vez para autorizarla. Ninguna credencial pasa por la conversación: el login ocurre donde el proveedor, en esa página. | `empresa?`, `provider?`, `account_types?`, `sync_frequency?` | La conexión en `pending_link` con su `link_url`. Todavía no llega nada: la herramienta lo dice y prohíbe describir el feed como funcionando hasta que list\_bank\_connections lo muestre `active`. En sandbox nace `active` sin link y sin nada detrás. | escribe · reversible | `empresas:read` + `tax:write` | no | | `sync_bank_connection` | Pide a un feed enlazado que traiga sus movimientos ahora en vez de esperar la sincronización programada. Cada movimiento en moneda extranjera se convierte en un cargo y el detector de tipo 46 corre sobre los nuevos. No emite nada. | `empresa?`, `connection_id?`, `job_id?` | Un job `connection_sync`: está ENCOLADO. Con `job_id` la misma herramienta consulta el job en vez de arrancar otro (GET /jobs/\{id}, sin cabecera de empresa). Una conexión que no está `active` se rechaza en vez de encolar un job que correría sin hacer nada. | escribe · reversible · idempotente | `empresas:read` + `tax:write` | no | | `delete_bank_connection` | Desconecta un feed bancario: se revoca el enlace y deja de entrar movimiento. Los cargos ya creados se CONSERVAN — un gasto que se convirtió en tipo 46 es parte del F29 de la empresa y su procedencia no deja de importar porque se apagó el feed. | `empresa?`, `connection_id` | Confirmación de la desconexión. La narración advierte que reconectar no es algo que un agente pueda hacer: link\_bank\_connection crea una conexión nueva y una persona tiene que completar el enlace en su banco. | escribe · irreversible | `empresas:read` + `tax:write` | no | `confirm_total_payable` sigue siendo obligatorio a través de la capa MCP. La API REST ya se niega a declarar sin él, y esa barrera vale doble acá: obliga a que la cifra aparezca explícita en la llamada de herramienta, lo que obliga a que aparezca explícita en lo que el agente le diga a la persona antes de declarar. ## Cuenta [#cuenta] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | --------------- | -------------- | | `list_empresas` | Qué empresas puede ver esta conexión — la herramienta de desambiguación de un grant multiempresa. Marca cuáles son sandbox y cuáles no están emitiendo todavía. | `status?` | lista escueta: `id`, `rut`, `legal_name`, `ambiente`, `mode`, `status`. | solo lectura | `empresas:read` | no | | `select_empresa` | Fija la empresa por defecto del resto de la sesión, para no repetir el RUT en cada llamada. Un argumento `empresa` explícito siempre gana. | `empresa` | confirmación de la empresa elegida, o del borrado del default. | escribe · reversible · idempotente | `empresas:read` | no | | `get_usage` | Cuántos documentos aceptó el SII en un período de facturación, por cuenta o por empresa. Los documentos de sandbox no se facturan nunca. | `empresa?`, `period` | `usage_summary`. | solo lectura | `usage:read` | no | | `get_pending_confirmation` | Leer qué pasó con una aprobación que le entregaste a una persona: si sigue esperando, si la aprobó, si la rechazó, si venció, o si la aprobó y la ejecución falló. Es la única lectura que sirve para eso — get\_document\_status pide un identificador que el sobre no trae. | `confirmation_id` | `status` (`pending`/`confirmed`/`rejected`/`expired`/`failed`), el resumen que vio la persona, cuándo y por dónde decidió, el `result` cuando ya corrió, y el `error` cuando fue rechazada por la API. | solo lectura | ninguno | no | | `poll_events` | Leer la bitácora de la cuenta hacia adelante desde un checkpoint, para enterarse de que algo pasó — que firmaron el mandato, que el SII aceptó un documento — sin repetir llamadas a ciegas. `wait_seconds` espera hasta 30 s; por defecto mira una vez y contesta. | `after?`, `type?`, `limit?`, `wait_seconds?` | Los eventos nuevos, en orden, con el `next_cursor` para la próxima vez. Sin novedad es una respuesta, no una falla. | solo lectura | `webhooks:read` | no | Son deliberadamente escuetas. `select_empresa` está acotada para que no rompa el requisito de que el servidor sea capaz de operar sin estado: un argumento `empresa` explícito siempre gana, y perder la sesión pierde un default, no produce una respuesta equivocada. ## Puesta en marcha [#puesta-en-marcha] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------ | -------------- | | `create_empresa` | Agrega una empresa a la cuenta. En sandbox nace `emitting`; en live nace al principio de la puesta en marcha y necesita varios trámites de una persona ante el SII antes de emitir. | `rut`, `legal_name`, `giro?`, `emission_channel?`, `origen?`, `contact_email?`, `doc_types?`, `resolucion?`, `resolucion_boletas?` | el objeto `empresa` recién creado, con su `id`, su `onboarding_status` y su `emission_channel`. | escribe · reversible | `empresas:write` | no | | `get_onboarding_status` | Dónde está una empresa en la puesta en marcha y qué la tiene detenida: el reporte de avance. | `empresa?` | el objeto de puesta en marcha, con `steps[]`, `blocked_on` y los requisitos del SII. | solo lectura | `empresas:read` | no | | `get_pending_human_action` | El traspaso a la persona: qué trámite falta, quién exactamente puede hacerlo, qué tener a mano y cómo verificamos que ocurrió, con las instrucciones en español para relatarlas tal cual. Cuando el paso trae `approval` no hay trámite que hacer: la persona autoriza en la página y lo presentamos nosotros con el certificado en custodia — así va la Declaración de Cumplimiento, que aprueba el representante legal. Lo que sigue siendo del cliente (permisos «Enviar Doctos.», casilla de intercambio) lo es porque ocurre dentro de la sesión del usuario administrador en el SII, donde el certificado que custodiamos no tiene atribuciones. No inventes un tercero: la lista es la que trae `pending_action`, que ya sabe qué alcanza este despliegue y qué no. | `empresa?` | `pending_action` completo, o `null` cuando nadie está esperando a una persona. | solo lectura | `empresas:read` | no | | `request_mandato_signature` | Acuña el enlace donde el representante legal firma el mandato. No firma nada: devuelve una URL de un solo uso para entregarle a esa persona. | `empresa?` | el `mandato` con su `signing_url` — que se entrega una sola vez. | escribe · reversible · idempotente | `empresas:read` + `mandato:write` | no | | `get_mandato` | ¿Está firmado el mandato? Cuándo, por quién y contra qué versión del texto. Sirve para preguntar sin acuñar un enlace nuevo (acuñar revoca el vigente). | `empresa?` | el `mandato`, con `accepted_at` y `accepted_by` cuando existen. | solo lectura | `empresas:read` + `mandato:write` | no | | `mint_onboarding_link` | Acuña la página alojada de un paso que tiene que hacer una persona, para pasarle la URL. La clave del paso sale de `get_pending_human_action`; no se inventa. | `empresa?`, `step` | el `onboarding_link` con su `url` — que se entrega una sola vez — y su vencimiento. | escribe · reversible · idempotente | `empresas:read` + `onboarding:write` | no | | `recheck_onboarding_step` | «Ya lo hice»: pide que vayamos a leer el SII. Acepta una afirmación y no produce ningún hecho — no mueve el paso ni cambia el estado, y lo dice. | `empresa?`, `step` | el estado del paso tal como estaba ANTES de mirar, más `evidence_source` (cómo se confirma, o si no podemos observarlo). | escribe · reversible · idempotente | `empresas:read` + `onboarding:write` | no | | `upload_certificate` | Deja el certificado digital (.pfx) de la empresa en custodia de Facturia. Como una llamada de herramienta no puede llevar un archivo, recibe el .pfx en base64 y su contraseña — o `reuse_from_empresa` para reutilizar uno que la cuenta ya tiene. | `empresa?`, `file_base64?`, `password?`, `filename?`, `reuse_from_empresa?` | la metadata del `certificate`: titular, vencimiento y los RUT para los que el SII lo reconoce. | escribe · reversible | `empresas:read` + `onboarding:write` | no | | `set_sfg_credential` | Guardar la clave del certificado CENTRALIZADO que el SII ya tiene de la empresa — lo que autoriza al SII a firmar cada documento del Portal MIPYME. Llamada sin la clave, acuña la página alojada donde la persona la escribe ella misma: no se la pidas por chat. No es la clave tributaria, que Facturia nunca pide. | `empresa?`, `clave_centralizado?` | La metadata de custodia (`stored`, `stored_at`) — el valor no se puede volver a leer nunca — o el enlace a la página donde se escribe. | escribe · reversible | `empresas:read` + `onboarding:write` | no | | `upload_certification_set_file` | Subir el `.txt` del set de pruebas que el SII emitió para la empresa, en base64, para que se pueda armar la ronda de certificación. Nadie lo baja por ti. Los bytes van tal cual: el archivo es ISO-8859-1 y sus acentos son parte del documento. | `empresa?`, `file_base64`, `filename?` | El set como lo leímos: atención, codificación real, secciones y sus tipos, cantidad de casos y los problemas que no entendimos. | escribe · reversible | `empresas:read` + `empresas:write` | no | | `request_go_live_authorization` | Acuña la página donde el titular autoriza la Declaración de Cumplimiento. No la autoriza: declarar arranca la obligación de Información Electrónica de Compras y Ventas desde ese mes, y ese mes lo elige el cliente. | `empresa?` | la autorización con su enlace y su vencimiento. | escribe · reversible · idempotente | `empresas:read` + `onboarding:write` | no | | `get_account` | La cuenta autenticada y la credencial en uso: scopes, modo, allowlist de empresas, cuántas empresas alcanza y cuántas de esas pueden emitir hoy, y los límites de la cuenta. Es cómo un agente aprende qué puede hacer esta conexión antes de prometer algo. | ninguna | El objeto Account completo (incluida la ApiKey anidada) en structuredContent; el texto resume nombre de cuenta, scopes, alcance de empresas y límites. | solo lectura | ninguno | no | | `get_job` | Consulta cualquier job asíncrono por job\_id (folio\_request, document\_send, cesion\_submit, rcv\_sync, statement\_import, connection\_sync) — el chequeo genérico que sync\_rcv solo cubre para rcv\_sync. No manda cabecera de empresa (GET /jobs/\{id} la rechaza) y no fija un scope propio: el gate real es del lado de /v1, por tipo de job, así que una negativa llega como insufficient\_scope real desde /v1. | `job_id` | El Job completo en structuredContent; el texto narra por type+status — para rcv\_sync delega textualmente en describeSyncJob (tax.ts) para no mantener dos redacciones de lo mismo. | solo lectura | `empresas:read` | no | | `list_usage_records` | El ledger inmutable de uso: un registro por documento que el SII efectivamente aceptó (nunca al enviarlo), con reversas como registros compensatorios de cantidad negativa. Es el detalle detrás del resumen de get\_usage. `empresa` es opcional (por defecto, toda la cuenta) — sigue exactamente el patrón de get\_usage, incluida la decisión de NO declarar empresas:read en el catálogo porque el camino primario es a nivel de cuenta. | `empresa?`, `period?`, `metric?`, `cursor?`, `limit?` | Lista paginada de UsageRecord; el texto resume métrica, cantidad, RUT y período de cada fila. | solo lectura | `usage:read` | no | | `get_event` | El gemelo de poll\_events para UN evento por id — para reconciliar por id (un id citado en un ticket, o un webhook recibido fuera de orden) en vez de recorrer el ledger hacia adelante. | `event_id` | El Event completo en structuredContent; el texto resume tipo, fecha y empresa, y avisa qué sub-bloques (data.object/previous\_attributes/detail) están presentes. | solo lectura | `webhooks:read` | no | | `get_mandato_terms` | El texto y versión vigente del mandato — lo que request\_mandato\_signature hace firmar. GET /mandato\_terms es `security: []` en el YAML (sin scope, igual que /error\_codes): el cuerpo es una constante sin datos de tenant, por eso el manifest declara requiredScopes vacío. | `version?` | El objeto mandato\_terms completo (texto, versión, status, anexos) en structuredContent; el texto avisa si status es draft (no vinculante) y lista los anexos «parte integrante». | solo lectura | ninguno | no | | `list_webhook_endpoints` | Lista los endpoints de webhook de la cuenta — URL, eventos habilitados, empresas restringidas, estado. Nunca incluye el secreto de firma. Solo lectura: crear/editar/borrar un endpoint y rotar su secreto se quedan REST/panel-only, decisión del fundador (un agente no tiene dónde recibir un webhook propio). | `cursor?`, `limit?` | Lista paginada de WebhookEndpoint. | solo lectura | `webhooks:read` | no | | `list_webhook_deliveries` | Intentos de entrega de un endpoint de webhook, más reciente primero — para responder «¿de verdad disparó nuestro webhook para ese evento?». Solo lectura: reintentar una entrega se queda REST/panel-only. | `webhook_endpoint_id`, `status?`, `event_id?`, `cursor?`, `limit?` | Lista paginada de WebhookDelivery. | solo lectura | `webhooks:read` | no | | `get_certification` | El estado del set de pruebas de una empresa en pista mercado: ronda actual, si el archivo del SII está guardado, qué insumos (`slots`) faltan, problemas y avisos de la última construcción, y cada envío con su veredicto. | `empresa?` | El objeto `Certification` completo. `submittable: null` se narra como «desconocido, no hay corrida todavía», nunca como false. | solo lectura | `empresas:read` | no | | `set_certification_inputs` | Entrega los hechos que el set de pruebas del SII no trae por sí solo: receptor de las pruebas, corrección CodRef-2, quién emitió cada compra, factor de proporcionalidad, folio de notificación, boletas de muestra. Fusión por slot completo: omitir deja, `null` borra, un valor reemplaza entero. | `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?` | Un resumen de qué slots quedaron actualizados. No corre ninguna ronda ni gasta folios — get\_certification refleja el efecto. | escribe · reversible · idempotente | `empresas:read` + `empresas:write` | no | | `list_onboarding_evidence` | La rendición de cuentas de la cláusula 4.4 del mandato: cada acto que Facturia hizo por esta empresa, del más nuevo al más viejo, filtrable por `step` o `kind`. | `empresa?`, `step?`, `kind?`, `cursor?`, `limit?` | Una lista de `OnboardingEvidence`. El export en CSV completo NO se ofrece como herramienta — responde un archivo crudo sin mecanismo de enlace público. | solo lectura | `empresas:read` | no | | `get_certificate` | Metadatos del certificado digital en custodia de una empresa — sujeto, emisor, vencimiento, estado, RUTs autorizados — NUNCA el material. `null` es una respuesta válida: no hay certificado todavía. | `empresa?` | El objeto `Certificate`, o `null` narrado como «sube uno con upload\_certificate». | solo lectura | `empresas:read` | no | | `get_sfg_credential` | Si la clave del certificado centralizado SFG está guardada, y desde cuándo — nunca el valor, que esta API no puede devolver a nadie. | `empresa?` | El objeto `SfgCredential` (solo metadatos). | solo lectura | `empresas:read` | no | | `delete_certificate` | Destruye para siempre el certificado en custodia. LA EMISIÓN SE DETIENE al toque. Exige `confirm_delete: true` — sin eso, se rechaza antes de llamar a /v1. | `empresa?`, `confirm_delete?` | Confirmación de destrucción. Dice que hay que subir un certificado nuevo antes de poder emitir de nuevo. | escribe · irreversible · idempotente | `empresas:read` + `empresas:write` | no | | `delete_sfg_credential` | Destruye para siempre la clave SFG en custodia. A diferencia del certificado, NO retrocede el onboarding — la próxima emisión SFG simplemente se rechaza hasta que se ponga una clave nueva. Exige `confirm_delete: true`. | `empresa?`, `confirm_delete?` | Confirmación de destrucción. | escribe · irreversible · idempotente | `empresas:read` + `empresas:write` | no | | `revoke_onboarding_link` | Revoca el enlace hospedado que un mint\_onboarding\_link anterior generó para un paso, antes de que caduque solo. Sirve si el enlace equivocado salió, o si la persona ya no debería tenerlo. | `empresa?`, `step` | Confirmación de revocación (o de que no había ninguno vivo — mismo resultado). No mueve el estado del paso. | escribe · reversible | `empresas:read` + `onboarding:write` | no | | `request_mandato_revocation_link` | Acuña la página donde el representante legal ejerce la cláusula 7.1: revocar el mandato en cualquier momento, sin causa ni sanción. Esta herramienta SOLO acuña la página — la revocación es el acto de la persona, con su RUT como segundo factor, y ningún tool acá puede hacerlo por ella. | `empresa?` | El enlace de revocación (vive \~un siglo, se entrega una sola vez). La narración dice explícitamente que revocar detiene a Facturia de actuar ante el SII por esta empresa — nada se revocó por esta llamada. | escribe · reversible · idempotente | `empresas:read` + `mandato:write` | no | Ninguna de estas marca un paso como hecho, y no es una omisión: un paso avanza cuando leemos el hecho en el SII, nunca porque alguien lo diga. `get_pending_human_action` es la que hay que llamar cuando una emisión falla con `empresa_not_emitting` — devuelve el traspaso completo, en español, escrito para la persona que tiene que actuar, que muy seguido **no** es quien tiene la cuenta. Relátalo tal cual: si viene un `caveat`, esa frase también va. ## Sandbox [#sandbox] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------ | -------------------- | ---------------------------------- | -------------- | | `sandbox_set_onboarding_step` | Solo en sandbox: forzar un paso del onboarding a un estado, para ensayar el traspaso antes de hacerlo con la empresa de verdad de alguien. Poner `mandato` en `action_required` produce el mismo handoff que produciría una empresa real. Afirma un estado, no corre la máquina, y rechaza toda empresa live. | `empresa?`, `step`, `status`, `tipo_dte?` | El paso como quedó. Nada se verificó y nada llegó al SII. | escribe · reversible | `empresas:read` + `empresas:write` | no | | `sandbox_advance_clock` | Solo en sandbox: adelantar el reloj de la empresa en días enteros, para llegar a un plazo que está a días de distancia — sobre todo la ventana de acuse de 8 días de un documento recibido, que sin esto no se puede ver nunca antes de emitir de verdad. El desfase es acumulativo y no se puede retroceder. | `empresa?`, `days` | El desfase en días y qué fecha cree la empresa que es ahora. | escribe · reversible | `empresas:read` + `empresas:write` | no | Estas dos son de ENSAYO y sólo tocan empresas de prueba: la API las rechaza sobre una empresa real y las herramientas también, antes de llamarla. Existen porque una empresa sandbox nace `emitting` con todos los pasos omitidos, así que el traspaso —lo más importante que hace esta superficie— no se podía probar sin una empresa de verdad y el SII de verdad. Forzar un paso es afirmar un estado, no hacer avanzar nada: fuera del sandbox un paso avanza cuando leemos el hecho en el SII, nunca porque alguien lo diga. ## Folios y CAF [#folios-y-caf] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------- | -------------- | | `request_folios` | Le pide folios al SII para un tipo de documento — el timbraje, que es un acto REAL contra un recurso finito y no un contador que subimos nosotros. Antes de llamarla hay que mirar get\_folio\_status: bajo el umbral de la empresa la reposición es automática, así que pedir a mano encima registra un segundo rango que nadie va a usar y que igual hay que anular. El timbraje es un recorrido guionado por el portal del SII y demora minutos, no segundos. | `empresa?`, `tipo_dte?`, `quantity?`, `job_id?` | Un `job` de tipo folio\_request. Con `job_id` vuelve a consultarlo: si terminó bien narra el rango realmente registrado (`from`–`to`, `caf_id`) y avisa cuando el SII otorgó MENOS de lo pedido; si falló con `range_registered: true` dice explícitamente que ya hay un rango registrado y que NO se reintente, porque un reintento registra otro. En sandbox el trabajo llega ya `succeeded` sobre el rango virtual y la narración dice que no se le pidió nada al SII. | escribe · irreversible | `empresas:read` + `folios:write` | no | | `list_cafs` | Lista los CAF (autorizaciones de folios) que tiene la empresa: un rango por fila, con su tipo de documento, su primer y último folio, si sigue activo y de dónde salió — de un timbraje nuestro o de un archivo que alguien subió. Es la pregunta «qué rangos existen», distinta de get\_folio\_status, que responde «puedo emitir». | `empresa?` | La lista de CAF. Una empresa en el portal gratuito del SII no tiene ninguno —ni tendrá, porque el SII asigna el folio en cada emisión— y la lista vacía es la respuesta verdadera, no un hueco. | solo lectura | `empresas:read` | no | | `upload_caf` | Registra un CAF que la empresa ya bajó del portal del SII, para que Facturia pueda sacar folios de ese rango. Es la alternativa a request\_folios: acá el rango YA está autorizado y no se le pide nada al SII. El archivo se pasa en base64 y no se le pide a nadie que lo pegue en el chat: un CAF lleva `<RSASK>`, la llave privada que firma el timbre de cada documento del rango. | `empresa?`, `caf_xml_base64?`, `caf_xml?` | El `caf` registrado, con su rango y su estado. Si el mismo rango ya estaba en archivo devuelve la fila que ya teníamos —el SII no autoriza dos veces el mismo rango— así que reintentar tras un timeout es seguro. El contador de folios NO se mueve al rango nuevo hasta que el rango en uso se agota, así que subirlo antes no bota folios pagados. Nada del archivo se repite en la respuesta. | escribe · reversible · idempotente | `empresas:read` + `folios:write` | no | | `deactivate_caf` | Deja de sacar folios nuevos de un rango CAF. La historia se conserva: los documentos ya emitidos de ese rango mantienen su folio y el rango no se borra — pero deja de ofrecer folios y ESTO NO SE DESHACE desde ninguna parte. Exige `confirm_deactivate: true` y refuerza mirar list\_cafs y nombrarle al usuario el rango por sus folios antes de apagarlo. | `empresa?`, `caf_id`, `confirm_deactivate?` | La confirmación de que el rango quedó inactivo, diciendo que los documentos ya emitidos no se tocan y que un reemplazo necesita request\_folios o upload\_caf. | escribe · irreversible · idempotente | `empresas:read` + `folios:write` | no | ## Empresa [#empresa] | Herramienta | Para qué | Entradas | Salida | Anotaciones | Permisos | ¿Confirmación? | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ---------------------------------- | -------------- | | `get_empresa` | Lee la empresa completa como la tiene /v1: estado de onboarding, ambiente SII, qué puede emitir AHORA (`capabilities.tipos`, no `doc_types`) y cualquier resolución declarada o probada. Es la vista completa; list\_empresas da la misma fila para todas las empresas de la conexión. | `empresa?` | El objeto `Empresa` completo. La narración distingue explícitamente `capabilities.tipos` (la verdad de emisión) de `doc_types` (la declaración), y dice `verified: false/true` de cualquier resolución o habilitación de boleta reclamada. | solo lectura | `empresas:read` | no | | `update_empresa` | Corrige o declara hechos de una empresa ya existente: razón social, giro, dirección, doc\_types, canal de emisión, origen, certification\_track, status, y — la puerta del 2026-09-07 — `resolucion`/`resolucion_boletas` para una empresa que YA factura con otro proveedor. Declarar no prueba nada por sí solo: `verified` sigue en false hasta que Facturia pida un timbraje y el SII lo conceda. | `empresa?`, `legal_name?`, `giro?`, `acteco?`, `address?`, `codigo_sucursal?`, `contact_email?`, `doc_types?`, `certification_track?`, `origen?`, `emission_channel?`, `resolucion?`, `resolucion_boletas?`, `status?` | El `Empresa` actualizado. La narración marca un cambio de `emission_channel` como un cambio de ruteo real (antes→después) y nunca dice que una resolución declarada está confirmada. | escribe · reversible · idempotente | `empresas:read` + `empresas:write` | no | | `get_automation_settings` | Lee cada interruptor de automatización de la empresa: auto-emitir tipo 46, auto-declarar F29, auto-pedir folios, auto-aceptar recibidos, y `require_confirmation` — si un agente necesita que una persona apruebe antes de que un acto se ejecute. | `empresa?` | El objeto `AutomationSettings`, narrado con el estado ON/OFF y el umbral de cada uno. | solo lectura | `empresas:read` | no | | `update_automation_settings` | Prende o ajusta una automatización. REGLA QUE ESTA HERRAMIENTA HACE CUMPLIR: un agente NUNCA puede apagar `require_confirmation` — eso se rechaza acá mismo, antes de llamar a /v1, sea quien sea quien lo pida. Prenderlo sí está permitido. | `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?` | El `AutomationSettings` actualizado. La narración dice qué campos cambiaron y el estado final de `require_confirmation`. | escribe · reversible · idempotente | `empresas:read` + `empresas:write` | no | | `get_branding` | Lee el logo, el pie de página y si una factura sale cedible por defecto en los PDF de la empresa. | `empresa?` | El objeto `Branding`. | solo lectura | `empresas:read` | no | | `update_branding` | Cambia el logo (por URL — nunca bytes de imagen), el pie de página, o si una factura 33/34 sale cedible por defecto. `default_pdf_format` solo acepta `carta`. | `empresa?`, `logo_url?`, `default_pdf_format?`, `cedible_by_default?`, `footer_text?` | El `Branding` actualizado. | escribe · reversible · idempotente | `empresas:read` + `empresas:write` | no | | `list_counterparties` | A quiénes esta empresa les ha facturado (tipos 33/34/56/61 — una boleta es a un consumidor y nunca aparece). Es una DERIVACIÓN de los documentos ya emitidos, no una tabla propia. Sirve para encontrar el `rut` que piden list\_counterparty\_contacts y set\_counterparty\_contact. | `empresa?` | Una lista de `Counterparty`, cada una con sus contactos de WhatsApp si los tiene. No paginada — `has_more` siempre false. | solo lectura | `empresas:read` | no | | `get_counterparty` | Lee lo que Facturia tiene guardado de UNA contraparte: razón social, giro y dirección, de dónde salió cada dato (`source`) y cuándo se actualizó. Es lo que arma el bloque `<Receptor>` de una factura — el SII acepta el sobre y después rechaza el documento si faltan `GiroRecep`, `DirRecep` o `CmnaRecep`, y el folio se gasta igual. Qué campos exige cada documento lo decide la emisión, no esta herramienta. | `empresa?`, `counterparty_rut` | El `Counterparty` guardado, más una línea que dice qué campos ya están y cuáles siguen en blanco. | solo lectura | `empresas:read` | no | | `update_counterparty` | Guarda la razón social, el giro y la dirección de una contraparte para que TODOS los documentos futuros a ese RUT los lleven sin volver a preguntarle a nadie. Es la respuesta a `receptor_incomplete`. Nunca se inventan ni se deducen: lo que se guarda tiene que venir del usuario. Los topes son rechazos, no recortes — giro 40 caracteres, dirección 70, comuna 20, ciudad 20, razón social 100 — porque acortar un giro cambia lo que dice el documento. | `empresa?`, `counterparty_rut`, `legal_name?`, `giro?`, `address?` | El `Counterparty` ya guardado, con lo que quedó en ficha. | escribe · reversible · idempotente | `empresas:read` + `empresas:write` | no | | `list_counterparty_contacts` | Los contactos de WhatsApp guardados para una contraparte — la libreta de direcciones de esa empresa, primario primero. | `empresa?`, `counterparty_rut` | Una lista de `CounterpartyContact`. `notifiable` dice si de verdad se le avisaría (primario, con opt-in, sin opt-out) — v1 solo notifica al primario. | solo lectura | `empresas:read` | no | | `set_counterparty_contact` | Crea (sin `contact_id`) o edita (con `contact_id`) un contacto de WhatsApp de una contraparte — a quién avisarle de una factura. El opt-in es una ATESTACIÓN, no un trámite: se pasa solo cuando hay una base real, nunca por defecto. Un opt-out es de una sola vía y esta herramienta nunca lo revierte. | `empresa?`, `counterparty_rut`, `contact_id?`, `name?`, `phone?`, `counterparty_name?`, `primary?`, `opt_in?`, `opt_in_method?`, `opt_in_attested_by?`, `opted_out?` | El `CounterpartyContact` creado o editado, con `notifiable` calculado. | escribe · reversible | `empresas:read` + `empresas:write` | no | | `delete_counterparty_contact` | Borra un contacto de la libreta de una contraparte. Borrado real, sin deshacer — y no sirve para esquivar un opt-out: un contacto que ya dijo STOP se rechaza (`contact_opted_out`), porque esa fila es justamente lo que impide que se le vuelva a agregar. | `empresa?`, `counterparty_rut`, `contact_id` | Confirmación de borrado (o de que ya no existía — mismo resultado). | escribe · irreversible · idempotente | `empresas:read` + `empresas:write` | no | ## Cómo vuelven los archivos [#cómo-vuelven-los-archivos] MCP le da a una herramienta dos formas de devolver un archivo: embebido como blob base64, o como un `resource_link` que el cliente busca solo si y cuando necesita los bytes. **Facturia MCP siempre usa `resource_link`, nunca blobs embebidos.** ```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" } ] ``` Tres razones: * **Economía de contexto.** Un PDF firmado embebido en base64 puede ser cientos de kilobytes de contenido inyectado al contexto del modelo por un documento que nadie pidió que leyera en voz alta. Un enlace cuesta una frase. * **Compatibilidad de clientes.** Las URL `pdf_url` / `xml_url` de la API REST exigen una llave. La mayoría de los hosts MCP no tienen forma de adjuntar el token OAuth de Facturia al resolver un resource link — ni deberían tener que sostenerlo. * **Más seguro que el default REST.** `get_document_file` enciende el mecanismo que REST ya publica como opt-in (enlaces públicos firmados, con vencimiento, sin autenticación) y le acorta la vida a **10 minutos**, con alcance de un solo documento y sin listar. ## Lo que no está en la superficie de agente [#lo-que-no-está-en-la-superficie-de-agente] Pedir CAF, administrar webhooks, la marca y las escrituras de caracterización del RCV quedan solo en REST y panel: son acciones puntuales o raras con una UI mucho mejor que un turno de chat, y el registro dinámico de cliente significa que el software que llama no está verificado como sí lo está una sesión del panel. Tampoco hay herramientas de anulación (`credit_note`) ni de reintento (`retry`) en este corte. Además de herramientas, este servidor publica un **prompt** (`onboard_empresa`, la caminata completa de puesta en marcha) y **recursos** que apuntan a esta documentación y al `llms.txt` del sitio, para que un agente los descubra desde dentro de la conexión. # Confirmaciones de dos pasos (/docs/mcp/confirmaciones) ## El default: ejecución directa [#el-default-ejecución-directa] Cuando un agente llama `emit_document`, emite. Facturia no interpone su propio paso de aprobación, porque la UI de aprobación de herramientas del cliente MCP — eso que te muestra «Claude quiere llamar `emit_document`, ¿permitir?» — **es** el humano en el ciclo. Agregar una segunda confirmación del lado de Facturia sobre eso, para cada escritura, sería redundante en la enorme mayoría de las llamadas y derrotaría la mitad del punto de una interfaz de agente. ## Dónde eso no alcanza [#dónde-eso-no-alcanza] Existen dos modos de falla distintos, y la aprobación del cliente solo resuelve uno: **El agente intenta hacer algo que la persona no pidió.** Lo resuelve el paso de aprobación del propio cliente — para eso existe exactamente. **La persona aprueba por reflejo todo lo que su cliente de chat le pregunta, y algo con consecuencia financiera real — declarar un formulario tributario, un reclamo irreversible — sale porque alguien hizo clic en «permitir» en piloto automático y no porque haya revisado deliberadamente esa cifra.** La aprobación del cliente **no** resuelve esto: es el mismo clic en ambos casos. `require_confirmation` ataca el segundo. ## Encenderlo [#encenderlo] Es una bandera por empresa en `automation_settings`, al lado de `auto_emit_46` y compañía. Viene **apagada**, como toda automatización. ```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}' ``` Es un campo REST, no un ajuste solo de MCP: los llamadores REST y el panel ven y controlan la misma bandera. La API REST descrita en el resto de esta documentación no cambia de comportamiento por encenderla. ## Qué herramientas toca [#qué-herramientas-toca] `emit_document`, `file_f29`, `emit_tipo46` y `reclaim_received_document` — el conjunto «emitir / declarar / reclamar». Toda otra escritura (`accept_received_document`, por ejemplo) sigue siendo directa aunque la bandera esté encendida: aceptar ya coincide con el default legal que ocurriría por silencio de todas formas, así que ponerle un cerrojo no protege nada. ## Cómo se ve [#cómo-se-ve] ```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" } ``` Nada se declaró. El agente te entrega el resumen y el enlace, y ahí se detiene. ## Quién la completa: tú, en una página [#quién-la-completa-tú-en-una-página] Abres `confirmation_url`, ves qué empresa, qué acción y con qué cifra, y aprietas **Confirmar** o **Rechazar**. No hace falta iniciar sesión: el enlace lleva un token de un solo uso, válido esos 15 minutos y para esa acción y ninguna otra. Recién ahí se declara el F29 — con exactamente los datos que se resumieron cuando el agente lo planteó, no con lo que haya en el mundo en ese momento. > **El agente no puede completarla, y eso es el punto** > > No hay ninguna herramienta MCP que cierre una acción pendiente. La hubo — `confirm_action` — y se > retiró: si el agente puede completarla diciendo «el usuario me dijo que sí», entonces el registro de > tu aprobación es la palabra del agente, que es exactamente lo que esta bandera existe para no > aceptar. Si le dices al agente que ya aprobaste, que lo verifique leyendo (`get_document_status`, o > `file_f29` con un `filing_id`). Confirmar dos veces reproduce el resultado original en vez de ejecutar dos veces — la misma garantía que `Idempotency-Key` le da a un llamador REST. Rechazar dos veces tampoco es un error: querías que no pasara, y no está pasando. Una aprobación vence a los **15 minutos** sin recoger. Vencida, la página lo dice y el agente tiene que volver a plantear la acción para generar una nueva. ## Sandbox: se ensaya acá [#sandbox-se-ensaya-acá] El doble paso **funciona igual en sandbox**: si enciendes `require_confirmation` en una empresa de prueba, la emisión se aparca, se acuña el enlace y la página `/aprobar` se abre como en live — marcada como prueba, para que nadie confunda el ensayo con lo real. > **Antes esto no se podía ensayar** > > Hasta septiembre de 2026 el sandbox ignoraba la bandera: la API la aceptaba, la devolvía encendida > y la emisión salía directo igual. Es decir, el puente más delicado del producto — ese en el que un > agente le pasa una decisión a una persona — solo se podía ver por primera vez con documentos de > verdad. Ahora se prueba con cero riesgo. > > La demo de sesenta segundos no cambió: la bandera viene **apagada** por defecto, así que una empresa > de sandbox recién creada sigue emitiendo en una sola llamada. ## Los estados que no son errores [#los-estados-que-no-son-errores] Dos comportamientos de esta capa que conviene tener claros en el cliente: * **Límite de tasa y confirmación pendiente no son fallas.** El equivalente a un `429` y las respuestas `pending_confirmation` son *resultados* ordinarios de la herramienta con un campo `status` claro, no errores lanzados. Un agente debería leerlos y decidir qué decir a continuación, no tratarlos como algo roto. * **Cada descripción de herramienta declara sus propios modos de falla probables.** Un agente que elige entre `emit_document` y `draft_document`, o que decide si es seguro reintentar tras un `sii_error`, nunca debería tener que consultar documentación externa a media conversación. Por eso `retriable` viaja en cada motivo de rechazo.