Empresas
The tenancy boundary: creation, the mandato (the power of attorney an empresa grants Facturia), certificate custody, onboarding and its automations.
Base: https://api.facturia.cl/v1. Every endpoint in this chapter authenticates with Authorization: Bearer unless stated otherwise, and the error envelope is the one in Errors.
This chapter is ordered the way it makes sense to read it, starting with the mandato: it is step 1 of both onboarding tracks, and the certificate (or the portal credential) is step 2. Uploading the certificate first works, but the step stays pending until the machine reaches certificate_pending. The full path is in From sandbox to production.
| Method | Path | What it does |
|---|---|---|
GET | /empresas | List empresas visible to this key |
POST | /empresas | Create an empresa |
GET | /empresas/{empresa_id} | Retrieve an empresa |
PATCH | /empresas/{empresa_id} | Update an empresa |
DELETE | /empresas/{empresa_id} | Delete a SANDBOX empresa |
GET | /empresas/{empresa_id}/mandato | Whether the representante legal has signed, against which terms, and how |
POST | /empresas/{empresa_id}/mandato | Record the mandato authorizing Facturia to act before the SII |
DELETE | /empresas/{empresa_id}/mandato | Revoke the mandato |
GET | /empresas/{empresa_id}/certificate | Retrieve certificate metadata |
POST | /empresas/{empresa_id}/certificate | Upload (or reuse) the empresa's digital certificate |
DELETE | /empresas/{empresa_id}/certificate | Purge the certificate from the vault (revokes custody, suspends emission) |
GET | /empresas/{empresa_id}/sfg_credential | Retrieve the SFG centralizado clave's custody metadata |
POST | /empresas/{empresa_id}/sfg_credential | Store (or replace) the SFG centralizado clave |
DELETE | /empresas/{empresa_id}/sfg_credential | Destroy the SFG centralizado clave (ends custody) |
POST | /empresas/{empresa_id}/mandato/revocation_link | Mint the hosted page where the representante legal revokes |
POST | /empresas/{empresa_id}/resolucion/verify | Verify a declared resolución with one production timbraje (Facturia operators only) |
GET | /empresas/{empresa_id}/counterparties | Counterparties you have invoiced, with the address and contacts on file for each |
GET | /contribuyentes/{rut} | What the SII publishes about a company, by RUT, and a giro that fits a factura |
GET | /empresas/{empresa_id}/counterparties/{counterparty_rut} | What you know about one counterparty, and whether you stated it or we inferred it |
PATCH | /empresas/{empresa_id}/counterparties/{counterparty_rut} | Put a counterparty's razón social, giro and address on file |
GET | /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts | The contacts on file for one counterparty |
POST | /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts | Put a contacto responsable on file |
PATCH | /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts/{contact_id} | Edit a contact, promote it, or record an opt-out |
DELETE | /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts/{contact_id} | Remove a contact from the address book |
GET | /empresas/{empresa_id}/onboarding | Onboarding lifecycle, steps and blocking human actions |
POST | /empresas/{empresa_id}/onboarding/links | Mint a link to a hosted representante-legal page |
DELETE | /empresas/{empresa_id}/onboarding/links/{step} | Revoke a hosted representante-legal link |
POST | /empresas/{empresa_id}/onboarding/go_live | Ask the titular to approve the go-live, or state the decision on their behalf |
POST | /empresas/{empresa_id}/onboarding/steps/{step}/recheck | Tell us a human step was performed, and we go and look |
POST | /empresas/{empresa_id}/onboarding/steps/{step}/attest | Record that a human performed a step this build cannot probe (#395) |
POST | /empresas/{empresa_id}/onboarding/steps/{step}/approve | Record the titular's authorization for a step Facturia then performs (#452, #453) |
GET | /empresas/{empresa_id}/onboarding/certification | The set de pruebas - what was uploaded, what is missing, what the SII said |
POST | /empresas/{empresa_id}/onboarding/certification/set_file | Upload the set de pruebas the SII issued for this empresa |
POST | /empresas/{empresa_id}/onboarding/certification/inputs | Supply the facts the set de pruebas does not carry |
GET | /empresas/{empresa_id}/onboarding/evidence | The rendicion de cuentas - what we did on the empresa's behalf |
GET | /empresas/{empresa_id}/onboarding/evidence/export | The rendicion de cuentas as one CSV file |
GET | /empresas/{empresa_id}/automation_settings | Retrieve per-empresa automation settings |
PATCH | /empresas/{empresa_id}/automation_settings | Update per-empresa automation settings |
GET | /empresas/{empresa_id}/branding | Read logo, default print format and footer text |
PATCH | /empresas/{empresa_id}/branding | Update logo, default print format and footer text |
GET /empresas
List empresas visible to this key
List every empresa this key can reach — its own allowlist resolved to real rows, or the whole account for an unrestricted key.
curl -sS https://api.facturia.cl/v1/empresas \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
limit | query | — | integer | |
cursor | query | — | string | Opaque cursor from a previous response's next_cursor. |
status | query | — | onboarding · active · suspended · archived |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | A list of empresas. | lista de Empresa |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas
Create an empresa
In test mode the empresa is born emitting: no mandato, no certificate, no certification.
In live mode it starts at mandato_pending and progresses through the onboarding state machine.
origen IS REQUIRED IN LIVE MODE and this operation refuses without it —
400 invalid_field with param: "origen" (#495). It is not in required above because
the mode is a property of the CREDENTIAL, not of the body, and a schema cannot say «required
when the bearer token starts with sk_live_»: declaring it unconditionally would make every
sandbox call — where it defaults to ninguno — fail a generated client's own validation
before it left the machine. So the rule lives on the 400 below, where a codegen'd client
can at least see it, and in the field's own description.
curl -sS -X POST https://api.facturia.cl/v1/empresas \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"rut":"77928532-4","legal_name":"Connect SpA"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
Idempotency-Key | header | — | string | Client-generated key, unique per logical operation, retained 24h and scoped to (account, empresa, endpoint). Replays return the original response with Facturia-Idempotent-Replay: true. Same key + different body → 409. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
rut | Rut | yes | Canonical RUT — no dots, one hyphen, uppercase K. E.g.: "77928532-4". |
legal_name | string | yes | |
giro | string | — | |
acteco | integer[] | — | |
address | Address | — | |
codigo_sucursal | integer,null | — | SII branch code (CdgSIISucur). Omit, or send null, for casa matriz. |
contact_email | string | — | |
emission_channel | mercado · sfg | — | How this empresa will emit. mercado runs Facturia's certified engine and the eleven-step certification; sfg drives the SII's free Sistema de Facturación Gratuito for an empresa that is already enrolled there, and runs a short checklist with no certification in it. Choosing sfg for an empresa that is NOT enrolled does not enrol it: the emisor_verificado step will say so. Choosing mercado for one that is happy on the free portal starts a certification it does not need. ambiente is DERIVED from this and is not accepted here — see Empresa.ambiente. Default: "mercado". |
doc_types | 33 · 34 · 39 · 41 · 46 · 52 · 56 · 61[] | — | WHAT THIS EMPRESA INTENDS TO EMIT — the declaration that drives certification and which folios are requested; capabilities.tipos is what it can emit NOW. Defaults to [33, 34, 61]; an explicit [] is 400 invalid_field, because an empresa with zero tipos can never finish certification. Declaring 39 or 41 (boletas) on emission_channel: sfg is 422 doc_type_not_supported_on_channel: the SII's free Portal MIPYME exposes no boleta form. Boletas need the mercado channel, where they also add the habilitacion step to the checklist. Default: [33,34,61]. |
origen | gratuito · mercado · ninguno | — | REQUIRED IN LIVE MODE — an sk_live_ key that omits it gets 400 invalid_field with param: "origen", which is the first empresa of every real customer. It is not in this schema's required list because the mode is a property of the credential and not of the body; the operation's 400 declares the refusal (#495). Optional in test mode, where it defaults to ninguno because a sandbox empresa is born emitting and never meets the wizard this field routes. See the Origen schema for what it does and does not decide. |
resolucion | ResolucionClaimRequest | — | THE RESOLUCIÓN THIS EMPRESA ALREADY HOLDS (ruling 2026-09-07). Send it when the empresa already invoices electronically through another provider; send null to clear it. Both halves are printed on the carátula of every documento tributario electrónico the empresa emits today (NroResol, FchResol), so an agent can read them off a recent invoice rather than sending anybody to Mi SII. IT IS A CLAIM, NOT A SKIP. It writes two columns and moves nothing else: the empresa is still created at mandato_pending, still needs the certificate and the mandato, and still certifies unless our own production timbraje proves the resolución. That timbraje is made only when a Facturia operator confirms it for this empresa; until then the claim reads resolucion.verification_status: pending_verification. Read resolucion.verified on the empresa object for the outcome. |
resolucion_boletas | ResolucionBoletasClaimRequest | — | THE BOLETA HABILITACIÓN THIS EMPRESA ALREADY HOLDS (ruling 2026-09-07). Send it alongside doc_types containing 39 or 41; send null to clear it. It carries no numero because the SII issues none for boletas: the habilitación arrives as «habilitados los documentos de boleta electrónica a contar del …», and that date is the whole of it. Verified by a tipo-39/41 production timbraje, exactly as the resolución claim is; refused, the ordinary habilitación steps appear on the checklist instead. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | The created empresa. | Empresa |
400 | Malformed input. The codes this operation answers: * invalid_rut — rut missing, or its check digit is wrong. * invalid_field with param: "legal_name" — absent or empty. * invalid_field with param: "origen" — absent, on a live key. The first empresa of every real customer hits this if the caller treated the field as optional because the schema does. See the operation description. * invalid_field with param: "doc_types" — not an array of supported tipos, or an explicit []. * invalid_field with param: "emission_channel" / "acteco" / "codigo_sucursal" — a value outside what the field admits. * invalid_field with param: "ambiente" — ambiente is derived and is refused rather than ignored. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
422 | doc_type_not_supported_on_channel — doc_types names a tipo the resolved emission_channel cannot build (39/41 on sfg). error.param is doc_types. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}
Retrieve an empresa
One empresa: legal identity, ambiente, emission_channel/certification_track, onboarding spine and capabilities.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The empresa. | Empresa |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
PATCH /empresas/{empresa_id}
Update an empresa
Update legal_name, giro, contact_email, acteco, origen, codigo_sucursal,
doc_types, emission_channel, certification_track or status. ambiente is
derived and rejected if sent (422); origen is the titular's own claim about how the
empresa emits today, correctable, never a canal preference.
Two of these move the onboarding machine and say so in their own descriptions:
doc_types (adding a tipo re-opens certification for it; 39/41 need the mercado
channel) and certification_track (which checklist governs the empresa).
THE PANEL MIRROR IS NARROWER. PATCH /dashboard/empresas/\{empresa_id\} — the same
handler behind an account session — accepts only doc_types and certification_track,
and refuses every other field with 400 invalid_field naming it. Those two are what the
puesta en marcha wizard has to record; the rest are the taxpayer identity each document
is printed from, and a browser session is not the credential that rewrites what the SII
sees on an invoice. An sk_ key on THIS path is unaffected.
curl -sS -X PATCH https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"legal_name":"Connect SpA","giro":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
legal_name | string | — | |
giro | string | — | |
acteco | array,null | — | SII activity codes, most specific first; the first one becomes <Acteco> on the emisor block of every document. An empty array — or null — clears it. Omitting the field leaves the stored value alone. |
address | Address | — | |
codigo_sucursal | integer,null | — | SII branch code (CdgSIISucur); null clears it back to casa matriz. Omitting the field leaves the stored value alone — a distinction that matters here, because an SFG empresa's Portal MIPYME preview refuses a document whose emisor origin is incomplete. |
contact_email | string | — | |
doc_types | 33 · 34 · 39 · 41 · 46 · 52 · 56 · 61[] | — | WHAT THIS EMPRESA INTENDS TO EMIT — the declaration that drives certification and which folios are requested. Read capabilities.tipos for what it can emit NOW. An empty array is 400 invalid_field: an empresa with zero tipos can never finish certification. Omitting the field leaves the stored value alone. ADDING a tipo to an empresa already emitting re-opens its checklist at certification_in_progress — certification and CAF are per tipo, and the SII authorizes only the types you certified. Adding 39 or 41 is what makes the habilitacion step appear, ON EITHER CHANNEL. BOLETAS ARE ALLOWED ON THE sfg CHANNEL, and declaring one is how you ask for the habilitación. The SII's free Portal MIPYME exposes no boleta form to anybody, so a 39/41 always leaves through our own engine whatever the empresa emits through — which means an empresa can keep issuing its facturas on the free portal and have us run the boleta certification for it in the background. The habilitacion and folios steps appear on the free lane's checklist the moment you declare a boleta, and capabilities.tipos gains 39/41 once the SII habilitates them. TIPO 46 ON THE sfg CHANNEL IS 422 doc_type_not_supported_on_channel: unlike a boleta, it is not a document our engine builds on that lane, so it needs emission_channel: "mercado". The same refusal fires when emission_channel is moved to sfg while a 46 is already declared. |
certification_track | mercado · sfg · null | — | WHICH CHECKLIST GOVERNS THIS EMPRESA — settable since #474, because the code has always read it and no surface could state it: PATCH \{emission_channel: "sfg"\} moved the tipos, the ambiente and the driver and left the empresa on the ten-step mercado checklist with blocked_on: mandato, a list an SFG emisor never runs. null means «not pursuing certification» and is a real, permanent answer. Setting it to null or to sfg HIDES the mercado checklist WITHOUT deleting anything: the step records stay exactly as they are and setting the track back to mercado shows them again, unchanged. It is a projection, not a destructive write. |
origen | gratuito · mercado · ninguno | — | Correctable, and the only way an empresa created before the field existed can acquire one. |
emission_channel | mercado · sfg | — | Correctable, like origen: an empresa can move onto the SFG lane or off it. Moving to sfg sets ambiente to produccion in the same write, because an empresa in the free system is a production emisor. Moving to mercado leaves ambiente alone — the resolución is the only thing that moves it the other way. It does NOT change certification_track on its own — that is a separate field on this same request body (#474). Those are two facts and each is stated where it belongs. |
resolucion | ResolucionClaimRequest | — | THE RESOLUCIÓN THIS EMPRESA ALREADY HOLDS (ruling 2026-09-07). Send it when the empresa already invoices electronically through another provider; send null to clear it. Both halves are printed on the carátula of every documento tributario electrónico the empresa emits today (NroResol, FchResol), so an agent can read them off a recent invoice rather than sending anybody to Mi SII. IT IS A CLAIM, NOT A SKIP. It writes two columns and moves nothing else: the empresa is still created at mandato_pending, still needs the certificate and the mandato, and still certifies unless our own production timbraje proves the resolución. That timbraje is made only when a Facturia operator confirms it for this empresa; until then the claim reads resolucion.verification_status: pending_verification. Read resolucion.verified on the empresa object for the outcome. |
resolucion_boletas | ResolucionBoletasClaimRequest | — | THE BOLETA HABILITACIÓN THIS EMPRESA ALREADY HOLDS (ruling 2026-09-07). Send it alongside doc_types containing 39 or 41; send null to clear it. It carries no numero because the SII issues none for boletas: the habilitación arrives as «habilitados los documentos de boleta electrónica a contar del …», and that date is the whole of it. Verified by a tipo-39/41 production timbraje, exactly as the resolución claim is; refused, the ordinary habilitación steps appear on the checklist instead. |
status | active · archived | — | |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | The updated empresa. | Empresa |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
DELETE /empresas/{empresa_id}
Delete a SANDBOX empresa
Throw away a scratch empresa. Sandbox only.
A sandbox empresa holds nothing that is a tax record: its documents are mocked, it never
reached the SII, and it reserves nothing — not the RUT (uniqueness is per mode) and not the
<rut>@dte.facturia.cl reception alias, which live empresas alone carry. So it can be
deleted, and the empresa plus everything hanging off it goes in one transaction.
A LIVE empresa is 409 empresa_delete_not_allowed, always. §7.1: emitted documents are tax
records. The two real exits are DELETE /v1/empresas/\{empresa_id\}/mandato (ends the
authorization, cláusula 7.1.3) and PATCH /v1/empresas/\{empresa_id\} with
\{"status": "archived"\} (stops emission and billing); both keep the history.
curl -sS -X DELETE https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
204 | Deleted. No body. | — |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
409 | empresa_delete_not_allowed — the empresa is live. Revoke the mandato and/or archive it. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/mandato
Whether the representante legal has signed, against which terms, and how
The read half of the hosted mode. Mint a signing_url with an empty POST, send it to
the representante legal, then poll this - completion is observable by the agent that
asked for it, without subscribing to a webhook or inferring it from steps[].status.
Two fields answer questions the onboarding checklist cannot. terms_current: false on a
signed mandato means the text has moved on and the pipeline has already stopped
trusting the acceptance - POST again to collect a fresh one. And acceptance_method
distinguishes an aprobacion (hosted: a human of the required role completed a page
that showed them what they were signing) from an atestacion (api: an integrator's
claim that one did). Those are different pieces of evidence.
signing_url is ALWAYS null here: only a SHA-256 of the token is stored, so no read
can reproduce it. POST with an empty body re-mints, revoking the previous link.
status: "none" for an empresa nobody has asked yet. It is a state, not a missing
resource - the empresa exists and you are authorized for it.
Scope: mandato:write (which empresas:write implies). The scope that mints the link
is the scope that reads whether it was used.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/mandato \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The mandato, or a none status when nobody has been asked yet. | Mandato |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/mandato
Record the mandato authorizing Facturia to act before the SII
Post the acceptance you collected yourself, or omit the body to receive a hosted
signing_url to send the representante legal.
We record that an acceptance happened, by whom, from where, and against which exact bytes of the mandato text. We do NOT verify a firma electrónica avanzada: whether one is required is an open legal question, and until it is answered this endpoint claims no more than the evidence it holds.
While GET /v1/mandato_terms reports status: "draft", a live-mode acceptance is
refused with 422 mandato_terms_draft — the same warning §7.3 prints, enforced.
Two forms, two scopes (ADR-0012 §4). No body — mint the enlace de firma for the
representante legal — needs mandato:write, which empresas:write implies. A body —
asserting a signature you collected in your own flow — is an atestación and needs
mandato:attest itself: empresas:write does NOT imply it (corrected 2026-09-03), so
a key that used to post a body under empresas:write alone now gets
403 insufficient_scope naming mandato:attest.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/mandato \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"requested_by":"…","representante_legal":{}}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
requested_by | RequestedBy + any | — | |
representante_legal | object | — | |
accepted_at | string | — | When the representante legal accepted. Clamped to now if it is in the future. |
accepted_ip | string | — | |
accepted_user_agent | string | — | |
terms_version | string | — | |
evidence | object | — | The same facts in the shape a signing page naturally produces. Accepted as an alternative to the flat fields above, and kept verbatim in the rendición de cuentas alongside whatever we recorded ourselves. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | The mandato. | Mandato |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
DELETE /empresas/{empresa_id}/mandato
Revoke the mandato
Cláusula 7.1: at any time, without cause, without penalty, and without ending the
contract. The empresa moves to suspended immediately, everything already queued is
cancelled in the same transaction, and the mandato step returns to action_required.
This is an atestación, not a write (ADR-0012 §4, ticket #465). It is the act with
the largest effect in the contract, so empresas:write is NOT enough: send it with
mandato:attest, which is granted explicitly, is never implied by empresas:write and
is never in the MCP vocabulary. A dashboard session may also call it — a session is the
account holder in person. Anything else gets 403 insufficient_scope, whose
error.details carries required_scope and the \{method, url\} that mints the hosted
revocation page for the representante legal
(POST /empresas/\{empresa_id\}/mandato/revocation_link).
It does NOT destroy the certificate — that is DELETE .../certificate, a separate
revocation with its own deadline. And it does not undo what preceded it: the empresa's
position is remembered, so a fresh acceptance returns it to where it was rather than
restarting onboarding.
The empresa stays READABLE while suspended (ticket #515): GET /empresas/\{id\},
GET .../onboarding and GET .../mandato all answer, onboarding.blocked_on names
mandato, and onboarding.pending_action is the mandato_revocado handoff whose
link.mint re-signs. active is untouched — a right the contract calls reversible is
never implemented as a disappearance.
curl -sS -X DELETE https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/mandato \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
204 | Revoked. The empresa moves to suspended, and stays readable. | — |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/certificate
Retrieve certificate metadata
Metadata only. The .pfx and its password are never readable through the API.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/certificate \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | Certificate metadata. | Certificate |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/certificate
Upload (or reuse) the empresa's digital certificate
Upload the empresa's .pfx (multipart/form-data with file and password) or reuse another empresa's valid certificate (reuse_from_empresa). An expired certificate is refused at ingest.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/certificate \
-H "Authorization: Bearer $FACTURIA_KEY" \
-F "file=…" \
-F "password=…"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body (multipart/form-data)
| Field | Type | Required | Notes |
|---|---|---|---|
file | string | — | PKCS#12 (.pfx) file. |
password | string | — | The .pfx password. |
reuse_from_empresa | string | — | Instead of uploading, reference a certificate already held for another empresa on this account. Valid only when that certificate is a SII-authorized user of this empresa's RUT. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | Certificate stored and verified. | Certificate + object |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
DELETE /empresas/{empresa_id}/certificate
Purge the certificate from the vault (revokes custody, suspends emission)
Remove the stored certificate metadata. The empresa returns to needing one before it can sign anything the SII will accept.
curl -sS -X DELETE https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/certificate \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
204 | Purged. | — |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/sfg_credential
Retrieve the SFG centralizado clave's custody metadata
Metadata only — whether we hold the clave and when it was stored. The value itself is never readable through the API: there is no code path in this endpoint that decrypts it.
Only an emission_channel: sfg empresa in live mode has one. A mercado empresa signs
with the certificate in custody and never asks the SII to sign for it, and a test-mode
empresa has no Portal MIPYME at all; both answer 422 naming the fix.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/sfg_credential \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The custody record. | SfgCredential |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/sfg_credential
Store (or replace) the SFG centralizado clave
The value goes straight to the per-tenant vault and its id to the empresa's credential row; there is no plaintext column. Replacing destroys the outgoing secret first, so the destruction is in the custody record rather than a side effect nobody can point at.
The response is the same metadata GET returns. The clave is not echoed back.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/sfg_credential \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"clave_centralizado":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
clave_centralizado | string | yes | The clave of the empresa's certificado CENTRALIZADO at the SII. ONE FIELD NAME, NO SYNONYMS: clave, password and clave_tributaria are refused with 400 invalid_field rather than coerced. The one mistake this endpoint exists to make impossible is a customer handing us their SII portal password under a field name we were relaxed about. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | Stored. | SfgCredential + object |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
DELETE /empresas/{empresa_id}/sfg_credential
Destroy the SFG centralizado clave (ends custody)
Destroys the bytes. 204 even when there was nothing to destroy — you asked for a state
("you hold nothing of mine"), and that state is the outcome either way.
Unlike DELETE .../certificate it does NOT walk the empresa's onboarding back: the next
SFG emission refuses for want of a signing clave, which the emit path already reports
honestly, and a clave can be re-supplied in one call.
curl -sS -X DELETE https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/sfg_credential \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
204 | Destroyed. | — |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/mandato/revocation_link
Mint the hosted page where the representante legal revokes
Cláusula 7.1 belongs to the person who signed, and this is how you put them in front of it (ticket #465). The acceptance already hands them this address once, and the receipt e-mail carries it; this endpoint is for every later moment — the e-mail was lost, or never went out, or the customer is asking their integrator to help them stop.
Holding the link is not holding the power: the page demands the representante legal's RUT as recorded on the mandato they signed, which this response deliberately does not echo. Minting revokes any previous revocation link, which is what single-use means when a link is reissued.
The URL is returned EXACTLY ONCE — only a SHA-256 of the token is stored. It does not expire: the right it addresses does not either.
409 mandato_not_signed when nothing is in force, because an address for a right that
cannot be exercised is worse than a refusal that says so.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/mandato/revocation_link \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
201 | The link. Returned once. | MandatoRevocationLink |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/resolucion/verify
Verify a declared resolución with one production timbraje (Facturia operators only)
OPERATOR ONLY — NOT CALLABLE WITH AN API KEY (sii-api#581 D1, founder decision
2026-09-23). Documented so an integrator knows what moves resolucion.verification_status
and who moves it.
A resolución stated on POST/PATCH /v1/empresas is a claim. It is verified by asking
the PRODUCTION SII for ONE folio range of one declared factura tipo: the SII refuses a
timbraje for a RUT it has not authorized for that tipo, so a granted range is the proof.
That request is irreversible (a registered range must be used or annulled), so it is made
only when a Facturia operator confirms it for this empresa. Nothing is requested
implicitly, nothing during POST /v1/empresas, and nothing against the certification
environment.
Without confirm: true the call refuses with 400 confirmation_required and names the
exact request in the message and in error.details.request. With it, the confirmation
(who, when, why) is recorded and ONE folio_request job is queued; the SII's answer then
moves resolucion.verification_status from pending_verification to verified or
refused. An attempt that gets no answer about the RUT (the SII's edge block, an unusable
certificate) stays pending_verification.
409 resolucion_verification_not_allowed names the blocking fact in
error.details.reason: sandbox, sfg_channel, no_claim, already_verified,
refused, in_flight, range_registered, not_ready (the empresa is not at
certificate_verified) or no_factura_tipo.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/resolucion/verify \
-H "X-Admin-Secret: $FACTURIA_OPERATOR_SECRET" \
-H "Content-Type: application/json" \
-d '{"confirm":true,"authorized_by":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
confirm | boolean | yes | Must be true. Anything else is 400 confirmation_required and nothing is sent. |
authorized_by | string | yes | The person confirming. Recorded beside the SII's answer. |
reason | string,null | — | Why, in a sentence (the ticket, the customer). Recorded. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
202 | Confirmed. The one production request is queued; the SII's answer lands on resolucion. | ResolucionVerification |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/counterparties
Counterparties you have invoiced, with the address and contacts on file for each
A DERIVED list, not a stored one: it reads the receivers of the documents this empresa has already emitted (tipos 33, 34, 56 and 61) and joins whatever contacts and address book entries exist, so the screen opens as a checklist rather than a blank form. A counterparty that has an address or a contact but no document yet is included.
The address fields come from the address book only. Where none is on file they are null
and so is source, which on this list means "nothing recorded" rather than "nothing
known" — the single-counterparty read is the one that falls back to what earlier
documents said.
has_more is always false. The window bounds the SCAN, not a page of results, and
promising a cursor this shape cannot honour would be worse than saying so.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The counterparties, newest first. | lista de Counterparty |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /contribuyentes/{rut}
What the SII publishes about a company, by RUT, and a giro that fits a factura
The razón social and economic activities the SII publishes for a persona jurídica, and a
suggested giro that fits the receptor of a factura (at most 40 characters, ISO-8859-1).
For prefilling a customer before the first invoice to them.
The source is the SII's public nómina of personas jurídicas
(https://www.sii.cl/sobre_el_sii/nominapersonasjuridicas.html), which the SII republishes
every few months and does not guarantee to be complete or current. source.published_at
says which edition answered. Personas naturales are not in it: their RUTs answer 404, like
a RUT that does not exist.
A SUGGESTION, not a statement: nothing here is written into a document on its own. Show it
to the person, and once confirmed, put it on file with
PATCH /v1/empresas/\{id\}/counterparties/\{rut\} — that is what the next emission reads.
suggested_giro.basis is curated when the text comes from Facturia's short wording for
a common activity code (the official descriptions are usually longer than 40 characters,
and a cut one would change what the factura says), sii_description when the official
description fits as it stands. Null when neither applies: ask the person.
Not empresa-scoped: the data is public and about a third party, so no Facturia-Empresa
is needed. Limited to 120 requests a minute per account.
curl -sS https://api.facturia.cl/v1/contribuyentes/{rut} \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
rut | path | yes | Rut | Any persona jurídica's RUT, canonical (76000002-7) or with dots, or without the hyphen. |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The contribuyente. | Contribuyente |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | The nómina does not list this RUT (a persona natural, or no such company). | ErrorResponse |
429 | Too many requests. | ErrorResponse |
503 | The nómina has not been loaded yet (contribuyentes_unavailable). Retry after Retry-After. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/counterparties/{counterparty_rut}
What you know about one counterparty, and whether you stated it or we inferred it
Your address book entry for this RUT — the razón social, giro and domicilio we will put on the next document to it.
Two sources, in the order the emission resolver uses them. If you have put an address on
file, that is the answer and source is api. If you have not, but you have already
emitted a document to this RUT, the answer is read off the newest one and source says
so — that is inference, not a statement, and it is the difference worth knowing before
you spend a folio. If neither exists, this is a 404.
The counterparty has no id: there is exactly one identity per RUT, so the RUT is the key.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties/{counterparty_rut} \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
counterparty_rut | path | yes | Rut | The counterparty's RUT, canonical (76000002-7). It is the key of the relationship and of the address book entry — a counterparty has no id of its own, because there is exactly one of each per RUT. |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The counterparty. | CounterpartyIdentity |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | Nothing is on file for this RUT and no document has ever been emitted to it. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
PATCH /empresas/{empresa_id}/counterparties/{counterparty_rut}
Put a counterparty's razón social, giro and address on file
The SII will not file a factura whose receptor has no giro, dirección and comuna: it accepts the ENVELOPE and then refuses the DOCUMENT, and the folio is spent either way. Recording the address here answers that once instead of on every emission — and it is the only source that can answer for a counterparty you have never invoiced, which is exactly when the question bites.
An upsert: the first call creates the entry, later ones edit it. There is no POST and no DELETE, because the entry has no lifecycle of its own.
Partial. A field you do not send keeps its value; a field sent as null is cleared.
Every value is held to the same SII schema limit a value sent on a document is held to —
giro 40 characters, address.street 70, address.comuna and address.city 20,
legal_name 100 — and to the same ISO-8859-1 rule. Over the limit is refused, never
truncated: a silently shortened name of trade would change what every document to this
counterparty says about who they are.
No earlier document is required. Recording an address for a RUT you have not invoiced yet is the point.
curl -sS -X PATCH https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties/{counterparty_rut} \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"legal_name":"Connect SpA","giro":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
counterparty_rut | path | yes | Rut | The counterparty's RUT, canonical (76000002-7). It is the key of the relationship and of the address book entry — a counterparty has no id of its own, because there is exactly one of each per RUT. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
legal_name | string,null | — | RznSocRecep, at most 100 characters. |
giro | string,null | — | GiroRecep, at most 40 characters — half of what the emisor's own giro is allowed, and the cap that bites most often. Refused over the limit, never truncated. |
address | CounterpartyAddress | — | The receptor's domicilio, as the SII's own elements: street is DirRecep, comuna is CmnaRecep, city is CiudadRecep. Always present as an object, even when every part of it is null, so a client never has to guard the parent before reading a part. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | The stored counterparty. | CounterpartyIdentity |
400 | A malformed RUT, a body naming no field at all, a value longer than the SII element it becomes (field_too_long) or one carrying a character ISO-8859-1 cannot represent (unencodable_character). | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts
The contacts on file for one counterparty
The primary contact comes first — it is the only one that is notified.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties/{counterparty_rut}/contacts \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
counterparty_rut | path | yes | Rut | The counterparty's RUT, canonical (76000002-7). It is the key of the relationship and of the address book entry — a counterparty has no id of its own, because there is exactly one of each per RUT. |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The contacts. | lista de CounterpartyContact |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts
Put a contacto responsable on file
A contact with no attestation is a legitimate state: it is stored, it is shown, and it
is never messaged. Sending opt_in: true without opt_in_method is refused, because
consent recorded without recording how it was obtained is not a record.
The first contact for a counterparty becomes the primary unless you say otherwise.
A number that has opted out cannot be re-added under a new id: that is 422 contact_opted_out, and it is what stops delete-and-recreate from undoing a STOP.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties/{counterparty_rut}/contacts \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"cliente — Comercial Andina","phone":"+56912345678"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
counterparty_rut | path | yes | Rut | The counterparty's RUT, canonical (76000002-7). It is the key of the relationship and of the address book entry — a counterparty has no id of its own, because there is exactly one of each per RUT. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | |
phone | string | yes | E.164. Anything that cannot be brought to that form is 400 invalid_phone. E.g.: "+56912345678". |
counterparty_name | string,null | — | Overrides the razón social the documents carried, for display only. |
primary | boolean | — | Defaults to true for the first contact of a counterparty, false afterwards. |
opt_in | boolean | — | Your attestation that this person agreed to receive WhatsApp notifications from you. Omitting it is a legitimate state: the contact is stored as an address-book entry and is never messaged (skip reason no_optin). |
opt_in_method | en_persona · telefono · email · escrito · otro · null | — | How the person actually agreed to be messaged. en_persona = in person or at the point of sale; telefono = by phone, recorded by whoever took the call; email = by email or a web form; escrito = on paper, signed; otro = something else, said plainly rather than forced into the nearest fit. WhatsApp's policy is that holding a number is not consent — "an affirmative, disclosed opt-in action is required" — so an attestation without a method is refused with 400 optin_method_required. |
opt_in_attested_by | string | — | Who collected the consent. Defaults to the credential that made the call. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | The contact. | CounterpartyContact |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
PATCH /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts/{contact_id}
Edit a contact, promote it, or record an opt-out
primary: true demotes whoever held it, in the same call — exactly one contact per
counterparty may be primary and the database is what enforces it.
opted_out: true records that the person asked to stop by some other channel.
opted_out: false does not exist: an opted-out contact is frozen and cannot be edited,
re-attested or re-enabled. Someone who asked to be left alone is not un-asked by a
colleague fixing a typo in their name.
curl -sS -X PATCH https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties/{counterparty_rut}/contacts/{contact_id} \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"cliente — Comercial Andina","phone":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
counterparty_rut | path | yes | Rut | The counterparty's RUT, canonical (76000002-7). It is the key of the relationship and of the address book entry — a counterparty has no id of its own, because there is exactly one of each per RUT. |
contact_id | path | yes | string | The cnt_… id. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | — | |
phone | string | — | |
counterparty_name | string,null | — | |
primary | boolean | — | true demotes whoever currently holds it, in the same call. Two primaries cannot exist even momentarily. |
opt_in | boolean | — | false clears the attestation — a consent record that can only ever be added to is worse for the person it exists to protect. |
opt_in_method | en_persona · telefono · email · escrito · otro · null | — | How the person actually agreed to be messaged. en_persona = in person or at the point of sale; telefono = by phone, recorded by whoever took the call; email = by email or a web form; escrito = on paper, signed; otro = something else, said plainly rather than forced into the nearest fit. WhatsApp's policy is that holding a number is not consent — "an affirmative, disclosed opt-in action is required" — so an attestation without a method is refused with 400 optin_method_required. |
opted_out | boolean | — | true records that the person asked to stop by another channel. false is not accepted: an opt-out is not reversible through this API. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | The updated contact. | CounterpartyContact |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
DELETE /empresas/{empresa_id}/counterparties/{counterparty_rut}/contacts/{contact_id}
Remove a contact from the address book
204 whether or not it was there. The exception is an opted-out contact, which is kept: its row is precisely what makes re-adding the same number refusable.
curl -sS -X DELETE https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/counterparties/{counterparty_rut}/contacts/{contact_id} \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
counterparty_rut | path | yes | Rut | The counterparty's RUT, canonical (76000002-7). It is the key of the relationship and of the address book entry — a counterparty has no id of its own, because there is exactly one of each per RUT. |
contact_id | path | yes | string | The cnt_… id. |
Responses
| Code | Meaning | Body |
|---|---|---|
204 | Removed. | — |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/onboarding
Onboarding lifecycle, steps and blocking human actions
Everything a wizard needs to render progress and tell the user what to do next.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | Onboarding state. | Onboarding |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/links
Mint a link to a hosted representante-legal page
The person who has to act is often not the person holding the account: the account belongs to the accountant, the certificate belongs to a socio, and the SII steps that need a person are the socio's. So the hosted pages are reached by a link, not by a login.
step is any step of THIS empresa's checklist that needs a person - actor: customer
(the step they perform) OR stall_actor: customer (a step of OURS that cannot finish
until they decide). Which ones exist depends on its certification_track:
certificate, sii_postulacion, cumplimiento and habilitacion on the mercado
catalog; credential, clave_centralizado and usuario_autorizado on the SFG one. A
step from the other catalog is refused with a 400 naming the ones this empresa does
have. The mandato's link is single-use and comes from
POST /empresas/\{empresa_id\}/mandato with an empty body instead.
The url is returned exactly once - only its SHA-256 is stored. The link is reusable
and lives thirty days, and re-minting revokes the previous one.
The response also carries the step's pending_action, with action_url set to the URL
just minted: the mint is the one moment both halves exist at once, so an agent gets the
live address and the instructions to relay with it in a single call.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/links \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"step":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
step | string | yes | Which hosted page the link opens: any step of THIS empresa's checklist that needs a human - actor: customer (they perform it) or stall_actor: customer (we perform it and it waits on their decision). mandato is deliberately absent: its link is single-use and comes from POST /empresas/\{empresa_id\}/mandato with an empty body. Read the valid values off GET /empresas/\{empresa_id\}/onboarding. |
requested_by | RequestedBy | — | Who is asking for this link, as YOU describe yourself (2026-09, #459). Optional, and rendered to the person who receives the link: "Este enlace lo pidio Claude Desktop el 4 de septiembre" instead of "lo genero una integracion o un agente en nombre de tu cuenta", which was the only sentence available for every agent that has ever existed. A CLAIM, NOT A CREDENTIAL. It changes nothing about authorization and is never trusted for a decision; the credential your request actually arrived on is recorded separately. Send \{"kind": "agent", "name": "<your client name>"\} when you are an agent acting for a person. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | The link. url carries the token and is never shown again. | OnboardingLink |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
DELETE /empresas/{empresa_id}/onboarding/links/{step}
Revoke a hosted representante-legal link
204 whether or not one existed. A revoked link answers exactly like one that never existed, so a token cannot be used to learn whether we ever asked a given company to sign something.
curl -sS -X DELETE https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/links/{step} \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
step | path | yes | string | The step whose link is being revoked. Any step either catalog can mint a link for - the wider set on purpose, so a link minted under a checklist the empresa has since left can still be taken back. |
Responses
| Code | Meaning | Body |
|---|---|---|
204 | Revoked. | — |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/go_live
Ask the titular to approve the go-live, or state the decision on their behalf
The Declaracion de Cumplimiento is a decision about a month, not a progression. On declaring, the empresa "queda operando automaticamente en el sistema de facturacion electronica" and owes the Informacion Electronica de Compras y Ventas FROM THE MONTH OF THE DECLARACION, whether or not it emits anything. The SII's own page says the step may be postponed.
So the month is the customer's to choose and a custody mandate does not carry it.
TWO FORMS, and the body is which one.
- No body mints an enlace de aprobacion for the titular and answers
201withapproval_urlandexpires_at. The human opens the page, sees the month and the IECV consequence in their own language, and decides. Single use, seven days, and the URL is returned exactly once - only its SHA-256 is stored. Scopeonboarding:write. \{"authorized": true|false\}states the decision on the titular's behalf: an atestacion, for an integrator that collected it in its own flow. Answers200with the authorization. Scopeonboarding:attest.
empresas:write implies onboarding:write — the no-body form — but NOT
onboarding:attest (corrected 2026-09-03): an atestación is a human granting that power
on purpose, every time, never a side effect of a wider grant a key already held. A key
that used to send \{"authorized": true|false\} under empresas:write alone now gets
403 insufficient_scope naming onboarding:attest.
This changed in 2026-09. An empty body used to DEFAULT to authorized: true, so a
caller who sent no body silently authorized a tax consequence. An omitted field is not a
decision; if you were relying on that, send \{"authorized": true\} explicitly.
\{"authorized": false\} withdraws while nothing has been filed. Once the declaracion is
filed there is nothing left to withdraw and the call is refused with
409 cumplimiento_already_filed.
Refusals arrive together. The status and code are the first blocker's own - 422 mandato_revoked, for instance - and error.details.blockers[] lists every one, so you
do not learn them one round trip at a time.
THE AUTHORIZATION IS FREE (2026-09-04, #530). It used to be gated on the
subscription, and only on this door: the hosted cumplimiento page for the
representante legal answered 200 for the same unsubscribed empresa, so the rule lived
in one channel and an agent that sent the human to the other one walked around it. It
is a DECISION, not an emission — the titular saying «start my obligation this month» —
and the gate stands where the value is: every act that spends a folio or leaves the
building is still gated, so an authorized empresa that has not paid emits nothing. This
operation no longer answers 402.
There has to BE a decision (2026-09, #451). Minting an approval link for an empresa that has not reached the Declaracion de Cumplimiento handed a human a page with nothing on it. Two refusals come first, before the blockers and before the mint:
409 go_live_not_ready- the mercado lane has not reached the decision.error.details.blocked_onnames the step holding it up anderror.details.nextis the\{method, url\}that reports when it moves. Retriable: it WILL be ready.409 go_live_not_applicable- this empresa is not certifying for mercado, so it has no Declaracion de Cumplimiento at all. What gates an SFG empresa's emission ispuesta_en_vivo, which is ours. Not retriable.
Neither applies to \{"authorized": false\}: a decision that exists can always be
withdrawn, whatever the lane has done since.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/go_live \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"authorized":true}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
authorized | boolean | — | Present at all, this states the decision on the titular's behalf - an atestacion, behind onboarding:attest. false withdraws an authorization that has not been acted on yet; once the declaracion is filed the call is refused with 409 cumplimiento_already_filed. No default. Omit the whole body to mint an approval link for the titular instead. It used to default to true, which meant a caller who sent no body silently authorized a tax consequence. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | The authorization, as it now stands. Answered for the body form. | GoLiveAuthorization |
201 | The approval link, answered for an empty body. approval_url is shown once. | GoLiveApproval |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/steps/{step}/recheck
Tell us a human step was performed, and we go and look
The resume half of the pending_action handoff. A person has performed one of the acts
Chilean law reserves to them (see pending_action.performed_by), and this asks us to
find out whether the SII agrees.
This endpoint accepts a claim and produces no fact. It takes no request body, writes
no step status and moves no empresa. What it does is enqueue the producer that READS the
SII, plus the tick that folds whatever that producer finds, and return the step's status
as it stood the moment you called — read before the enqueue and returned unchanged. If
you post a recheck on a step that is action_required and read action_required back,
that is the endpoint working: the step advances when we see the fact, not when you say
so. verified is on the response and it is always false.
evidence_source says how (or whether) the step can be confirmed at all. sii_probe
means we go and read the SII. own_record means the act lands in our own tables and we
will see it. no_producer means this build genuinely cannot observe that step yet — the
boleta habilitacion is the one — and contact names the door out:
POST .../onboarding/steps/\{step\}/attest (#395), which is a DIFFERENT endpoint that
writes a fact rather than merely recording a claim. Do not tell a user this call alone is
watching a no_producer step; nothing is.
queued and already_queued split the jobs this call created from the ones that were
already scheduled for the same five-minute window. Both mean "we are about to look".
For a sandbox empresa nothing is enqueued and evidence_source is sandbox: a test
empresa's onboarding never opens a session against the SII, so there is nothing to
re-read. Move its steps with the test helpers instead.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/steps/{step}/recheck \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
step | path | yes | string | An onboarding step key from this empresa's own checklist. |
Responses
| Code | Meaning | Body |
|---|---|---|
202 | The claim was accepted for verification. Nothing has been verified yet. | OnboardingRecheck |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/steps/{step}/attest
Record that a human performed a step this build cannot probe (#395)
The endpoint recheck's own contact field points at when evidence_source: 'no_producer'. Unlike recheck — which accepts a claim and produces no fact —
this endpoint writes one: a sii_onboarding_evidence row the next onboarding tick
reads as the fact that lets the step complete.
Refused with 422 invalid_field on any step whose evidence_source is not
no_producer — attesting a step the SII probe or our own records already confirm would
be a second, competing source of truth for a fact we can already verify. habilitacion
(boleta habilitación at certBolElectDteInternet, an uncaptured SII SPA) is the only
step this map names no_producer today.
Requires onboarding:attest itself: never in the bootstrap default set, never
implied by empresas:write alone (ADR-0012 §3-4). attested_by is required — who told
you this happened.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/steps/{step}/attest \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"attested_by":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
step | path | yes | string | An onboarding step key from this empresa's own checklist. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
attested_by | string | yes | A name, or the representante legal's RUT. Who told you this happened. |
note | string | — | |
| Responses |
| Code | Meaning | Body |
|---|---|---|
202 | The attestation was recorded. The step folds on the next tick. | OnboardingAttestation |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/steps/{step}/approve
Record the titular's authorization for a step Facturia then performs (#452, #453)
The sibling of attest, for the opposite situation. attest says «a person did
something we cannot see»; this says «a person authorized something WE are about to do».
Two acts in this pipeline are ours to perform once somebody chooses the moment. The
postulacion is presented with the representante legal's own custodied certificate under
mandato 4.2 a), and it costs an empresa on the Sistema de Facturacion Gratuito its
"calidad de usuario activo" the instant the SII authorizes it. The boleta habilitacion
opens a 24-hour SII clock. Neither is a decision a custody mandate carries, so each
waits for a row in sii_onboarding_step_approvals.
act must be the one the step is currently asking for. Read
steps[].pending_action.approval on GET .../onboarding: it names the act, whose
decision it is, the consequences to show first, and both button labels. A step asking
for nothing answers 409 onboarding_approval_not_offered; a mismatched act answers
400 invalid_field. One step carries different decisions at different points of its
life, and an approval recorded against the wrong one is consent that was never given.
A rejection is recorded, not ignored. decision: "reject" writes a row exactly as
an approval does, so the product can say "you left it for later" rather than "nobody
answered". Executors read the LATEST row for the act and branch on decision.
Requires onboarding:attest itself — never in the bootstrap default set, never implied
by empresas:write (ADR-0012 §3-4). The canonical form of an approval is the hosted
page (POST /onboarding/steps/\{step\}/approve with the link token); this is the
integrator door for a decision collected in their own flow.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/steps/{step}/approve \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"act":"…","decision":"approve"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
step | path | yes | string | An onboarding step key from this empresa's own checklist. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
act | string | yes | The act being decided, exactly as pending_action.approval.act names it. |
decision | approve · reject | yes | |
note | string | — | |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | The decision was recorded. On approve, the fold tick is enqueued. | OnboardingStepApproval |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
404 | No such object under this key. | ErrorResponse |
409 | State conflict — idempotency reuse, duplicate external_id, artifact not ready. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/onboarding/certification
The set de pruebas - what was uploaded, what is missing, what the SII said
One read, not four. A certification is a bounded thing - one set file, one input document, a handful of envios across at most three rounds - and splitting it would make the obvious screen four round trips whose answers can disagree.
TWO FIELDS THAT LOOK ALIKE AND ARE OPPOSITE. envios[].estado is the ENVIO estado,
where SOK means "envio recibido" - arrived, not judged. envios[].verdict is the
SET verdict, where SOK means the set was APPROVED. Same three letters, opposite
consequences, which is why they are two fields and never merged.
envios[].dry_run: true means the envelope was built and NOT sent. submittable is
null until a build has happened at all, which is not the same as false.
problems[].message may be null: the sentences live in the build artifact beside
the record, and when it cannot be read the response degrades to codes rather than
failing. The code is always there.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/certification \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The certification, as it now stands. | Certification |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/certification/set_file
Upload the set de pruebas the SII issued for this empresa
The SII issues the set .txt per taxpayer when it accepts the postulacion. Nothing
downloads it for you; this is how the bytes get in.
MULTIPART, NOT BASE64 IN JSON. The file is ISO-8859-1 and its accents are part of the
document - Cajon and Panuelo are two of the items the SII reported as reparos when
theirs were lost. Multipart hands us the bytes; a JSON string would already have been
through somebody's idea of a text encoding by the time we saw it. The bytes are stored
verbatim and never re-encoded. Limit: 1 MB.
Bytes that are not a set de pruebas are refused with 422 set_file_unrecognized and
NOTHING is stored - a portal login page parsed as an empty set would report the
empresa as having no casos, which is a different thing from having no set file.
A set that IS recognised is stored even when parts of it were not understood; those
appear in problems[].
This endpoint does not run a round, does not open a socket to the SII and does not spend a folio. The round is the engine's, on its own clock.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/certification/set_file \
-H "Authorization: Bearer $FACTURIA_KEY" \
-F "file=…"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body (multipart/form-data)
| Field | Type | Required | Notes |
|---|---|---|---|
file | string | yes | The .txt as the SII issued it. ISO-8859-1, at most 1 MB. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
201 | The set as we read it. | CertificationSetFile |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
422 | Well-formed but violates a domain rule — folios exhausted, acuse window closed. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
POST /empresas/{empresa_id}/onboarding/certification/inputs
Supply the facts the set de pruebas does not carry
The set states several things by implication and never spells them out: who issued each purchase in the libro de compras, what the CodRef-2 correction says, which factura invoiced a guia the set marks "facturada", the factor de proporcionalidad, the SII-issued folio de notificacion, and - for a boleta round - the abbreviated unidades and ten everyday boletas. Each one is a deliberate refusal in the engine: the round reports what it needs rather than inventing a value and spending folios on a document the SII will reject.
THE MERGE IS PER TOP-LEVEL SLOT: absent keeps, null clears, present replaces WHOLE.
Restating proveedores restates all of them - which is what a form submits and the
only way to REMOVE one. A deep merge would leave the row you deleted in the next
libro.
NOTHING IS WRITTEN WHEN ANYTHING IS INVALID, not even the valid half: a partial write
leaves the empresa in a state nobody asked for and a round that refuses for a reason
you believe you already fixed. 400 certification_input_invalid carries EVERY problem
in error.details.problems, not the first. It may also refuse over a value you did
not send, when the stored document your patch merges into is itself invalid.
ONE SLOT IS PREFILLED: libro_guias.folio_notificacion defaults to 3, the number
continuing the 1 and 2 the libro de ventas and the libro de compras carry.
LibroGuia_v10.xsd makes the element mandatory where the IECV's is optional, so it
cannot simply be omitted, and a round that goes out on the default says so in a
notice. libro_boleta_folio_notificacion has no default and will not get one: it is
an SII-issued number answering a specific notificacion and nothing can derive it.
curl -sS -X POST https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/certification/inputs \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"receptor":{"rut":"76543212-K"},"giro_recep_emisor":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
receptor | object,null | — | The party the test documents are issued to. giro is measured against GiroRecep (40), which is shorter than GiroEmis (80) - an emisor's own name of trade is frequently illegal in this slot. |
giro_recep_emisor | string,null | — | A ≤40-character abbreviation of the EMISOR's own giro, for the ONE place a traslado interno guia writes it back: the receptor is the emisor itself (a traslado interno moves goods between the empresa's own premises), so its giro is what would be written into GiroRecep - which caps at 40 where the emisor's own GiroEmis allows 80. Absent, and the emisor's giro fits within 40 characters anyway: written verbatim, same as always. Absent, and it does not fit: GiroRecep is optional in the schema, so the element is OMITTED rather than truncated, and the round raises giro_recep_omitted so the omission is visible rather than silent - this is the exact reparo the SII drew over the pilot's own 65-character giro. Present: written verbatim, and refused - never truncated - if it does not fit either. |
text_correction | object,null | — | The CodRef-2 correction. Written as "Dice: {says} y debe decir: {shouldSay}" into RazonRef, which caps at 90 - 21 of them are the scaffolding, so the two halves share 69. Measured on the composition, and never truncated for you. |
proveedores | object,null | — | Who issued each purchase, keyed <tipoDte>:<folio> - the two things the set prints for every row. A RUT whose verifier digit fails modulo 11 is one of the two documented causes of an LRH "Descuadrado", found after the folios are spent. razonSocial is measured against the LIBRO's RznSoc (50), not the DTE's 100. |
iva_no_rec_codes | object,null | — | Why a purchase loses its credito. LibroCV_v10.xsd enumerates 1,2,3,4,9. |
libro_treatments | object,null | — | The IVA treatment of a purchase the set does not classify. "unknown" is the planner's word for "the set said nothing" and "exento_sin_iva" is arithmetic, so neither is an input. |
libro_guias | object,null | — | |
factor_proporcionalidad | number,null | — | The set prints it as "EL FACTOR DE PROPORCIONALIDAD DEL IVA ES DE 0.60". It decides how much credito is claimed, so it is never guessed. |
libro_boleta_folio_notificacion | integer,null | — | The SII-issued "Folio de la Notificacion con que se Solicita el Libro Especial". NO DEFAULT, and there will not be one - nothing can derive it, and a boleta round refuses without it. |
boleta_unidades | object,null | — | UnmdItem by <casoId>:<nroLinDet>, for the one thing a set can demand that its own schema forbids - a unidad longer than four characters. Refused, never truncated: "mens" is not a unit. |
boleta_muestras | array,null | — | The ten everyday boletas the SII's boleta review asks for, describing what this empresa actually sells at prices it actually charges. Fewer than ten is an incomplete request and is refused; more is fine. |
permiso_usuario_rut | string,null | — | The natural person the permisos probe asks "Mantencion de usuarios" about, when the custodied certificate encodes no RUT of its own. THE ONLY SLOT HERE THAT IS NOT A DOCUMENT FACT, and the only one no builder reads. Estado 106 "Usuario sin permiso de envio" is about the RUT the SII token was minted for, so the probe reads it out of the certificate first and falls back to this. Absence does NOT refuse a round: the round still builds and signs, and it is the permisos gate that parks, reporting no_target with the remedy attached. Validated modulo 11 like every other RUT here. null clears it. Never the empresa's own RUT - that table lists natural persons, so an empresa RUT would find nobody and report the sender as unlisted. |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | The inputs as they now stand on the empresa. | object |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/onboarding/evidence
The rendicion de cuentas - what we did on the empresa's behalf
Clausula 4.4 of the mandato obliges us to render account of what we did on the customer's behalf. Every act the onboarding pipeline performs is kept as an artifact, and this reads them back, newest first.
retention_until is on every row and is six years from the act - the Codigo
Tributario's prescripcion extraordinaria plus its conservation duty, which is also the
retention purpose under Ley 21.719. On revocation or traspaso the record freezes
read-only and is offered as an export; it is never purged early.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/evidence \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
limit | query | — | integer | |
cursor | query | — | string | Opaque cursor from a previous response's next_cursor. |
step | query | — | string | Only artifacts of this onboarding step. |
kind | query | — | string | Only artifacts of this kind. |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The artifacts. | lista de OnboardingEvidence |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/onboarding/evidence/export
The rendicion de cuentas as one CSV file
The same rows the list endpoint serves, complete in one response, as a file you can hand to a customer, an accountant or a lawyer. The retention ruling says that on revocation or traspaso we offer the customer a full export as the formal clausula 4.4 rendicion; this is that export, and it is available at any time, not only on the way out.
RFC 4180, comma-separated, UTF-8 with NO byte-order mark, CRLF line endings. The columns
are the JSON object's own fields in the same order - id, empresa_id, step,
tipo_dte, kind, captured_at, retention_until, sha256, bytes, content_type,
detail - so storage_path is absent here exactly as it is absent there. detail
travels as its compact JSON, quoted.
Paged internally and capped at 50,000 rows. When the cap is reached the response says so
in Facturia-Export-Complete: false rather than serving a truncated file that looks
whole; step and kind are how you narrow an export that hit it.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/onboarding/evidence/export \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string | |
step | query | — | string | Only artifacts of this onboarding step. |
kind | query | — | string | Only artifacts of this kind. |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The rendicion, as a CSV attachment. | text/csv |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/automation_settings
Retrieve per-empresa automation settings
The empresa's automatic-emission toggles (auto_characterize_purchases, auto_emit_46, …), each defaulted where unset.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/automation_settings \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | Automation settings. | AutomationSettings |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
PATCH /empresas/{empresa_id}/automation_settings
Update per-empresa automation settings
Every automation defaults to OFF except auto_request_folios. auto_emit_46 and
auto_file_f29 are opt-in per empresa and capped.
curl -sS -X PATCH https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/automation_settings \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"object":"…","empresa_id":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
object | string | — | |
empresa_id | string | — | |
auto_emit_46 | boolean | — | Default: false. |
auto_emit_46_max_amount | Clp | — | Amount in CLP. Integer pesos, never decimals or strings. |
auto_file_f29 | boolean | — | Default: false. |
auto_file_f29_day | integer | — | Default: 18. |
auto_file_f29_max_payable | Clp | — | Default 0 — out of the box only a zero or refund position files itself, and any month you actually owe IVA waits for a human. Raise deliberately, per empresa. Default: 0. |
auto_request_folios | boolean | — | Default: true. |
auto_request_folios_threshold | integer | — | Default: 50. |
auto_accept_received | boolean | — | Default: false. |
auto_accept_received_after_days | integer,null | — | |
require_confirmation | boolean | — | When true, money-moving actions initiated from an agent surface — emitting a document, filing an F29, registering a reclamo through the MCP server's tools — require a draft → confirm two-step instead of executing on the first call. It exists primarily for the MCP surface (see mcp-server-spec.md); the REST API described here is unaffected, and the flag lives on this resource so one settings object governs the empresa on every surface. Default: false. |
rcv_sync_frequency | hourly · daily · weekly · manual | — | Default: "daily". |
honor_estado_contable_override | boolean | — | Whether the F29 propuesta obeys the LOCAL estado_contable_override set through POST /tax/purchases/\{entry_id\}/estado_contable instead of the estado_contable the SII's own register supplied. Off by default, and the direction of that default is the point: only REGISTRO purchases carry a crédito fiscal, so a local PENDIENTE → REGISTRO flip claims IVA the SII does not agree exists — and the SII's value is the one an audit reads. Turned on, every affected propuesta carries an explicit observation naming the documents, and every write is recorded on the document's event log. Default: false. |
updated_at | string | — | |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | Updated settings. | AutomationSettings |
400 | Malformed or missing input. | ErrorResponse |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
GET /empresas/{empresa_id}/branding
Read logo, default print format and footer text
Every field is filled in from the platform defaults where the empresa has set nothing, so the object always describes what a PDF will actually look like rather than only what was overridden.
curl -sS https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/branding \
-H "Authorization: Bearer $FACTURIA_KEY"Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Responses
| Code | Meaning | Body |
|---|---|---|
200 | The empresa's branding. | Branding |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |
PATCH /empresas/{empresa_id}/branding
Update logo, default print format and footer text
Update the panel and hosted-page branding fields (logo, colors, display name). An unknown field name is refused (422 unknown_setting), never silently ignored.
curl -sS -X PATCH https://api.facturia.cl/v1/empresas/emp_01K2R6ZC4P8QW1VN7T3MHDY9EK/branding \
-H "Authorization: Bearer $FACTURIA_KEY" \
-H "Content-Type: application/json" \
-d '{"object":"…","logo_url":"…"}'Parameters
| Parameter | In | Required | Type | Notes |
|---|---|---|---|---|
empresa_id | path | yes | string |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
object | string | — | |
logo_url | string | — | |
default_pdf_format | carta | — | carta is the only legal value — 80mm was removed in wave 9 along with the format parameter, and no thermal layout exists. PATCH refuses any other value with 400 invalid_field (ticket #18 wave 10). There is nothing further for it to select: it already names the only layout this API renders, so validating it is what "applying" it means. Default: "carta". |
cedible_by_default | boolean | — | Applies to facturas — tipo 33 and 34 (Ley 19.983; not "tipo 33 only", corrected in ticket #18 wave 10). Consumed by GET /documents/\{document_id\}/pdf's default-copy resolution: a per-document pdf.cedible (§9.3) overrides it; an explicit ?copy= overrides both. See that endpoint's copy parameter for the full precedence. Default: false. |
footer_text | string | — | |
| Responses |
| Code | Meaning | Body |
|---|---|---|
200 | Updated branding. | Branding |
401 | Missing, malformed or revoked key. | ErrorResponse |
403 | Valid key, not permitted — scope, empresa allowlist, or mode. | ErrorResponse |
429 | Too many requests. | ErrorResponse |
default | An error not otherwise enumerated for this operation, in the same §5.5 envelope as every other refusal. Branch on error.code, never on the HTTP status alone or on error.message — a code this operation has not been seen to throw yet is still safer to route on than a string meant for a human. | ErrorResponse |