Schemas
The objects the "Body" and "Responses" tables name: the empresa, onboarding, the handoff to a human, the recheck and the origen.
The "Body" and "Responses" tables in the chapters above name the type of every field. When that type is one of the objects below, the name is a link to this page. Here is its full shape, field by field, exactly as the contract declares it.
This is the same information that lives in /openapi.yaml under components.schemas; this page exists because a 271 KB file is not an answer for the person building the onboarding wizard.
| Schema | What it describes |
|---|---|
Empresa | The empresa, with capabilities, the emission channel and the environment. |
Origen | How the empresa issues today, as declared by you when it was created. |
Onboarding | The full onboarding: status, steps, requirements and the current handoff. |
PendingAction | The handoff to a human: who, where, what instructions, and how we find out. |
OnboardingRecheck | The response to "this is already done, go check". |
Empresa
| Field | Type | Required | Notes |
|---|---|---|---|
object | string | — | |
id | string | — | |
mode | live · test | — | |
rut | Rut | — | Canonical RUT — no dots, one hyphen, uppercase K. E.g.: "77928532-4". |
legal_name | string | — | |
giro | string,null | — | NULLABLE (#496). POST /v1/empresas does not require it and stores null when it is omitted, which is the ordinary create — the same class of lie #473 found on contact_email, caught by validating a real body against this schema instead of comparing the keys of a hand-written example. |
acteco | integer[] | — | |
address | EmpresaAddress | — | The address AS READ BACK off an empresa (#496). Structurally Address, with every member nullable, because POST /v1/empresas requires none of the three and stores null for whatever was omitted — which is the ordinary create. The REQUEST shape stays Address with non-null strings: what you may send and what you may read back are different statements, and collapsing them would either weaken the request or keep lying about the response. Same family of defect as #473's contact_email. |
codigo_sucursal | integer,null | — | The SII branch code (CdgSIISucur) this empresa emits from. null is casa matriz, which is most empresas — and it is REPORTED as null rather than omitted, because "we hold no sucursal for you" is a fact an SFG wizard has to be able to see: the Portal MIPYME's own preview refuses a document whose emisor origin is incomplete. |
contact_email | string,null | — | NULLABLE, and null is what an empresa created without one reports (#473). It was declared type: string while the wire sent null for every empresa that never supplied it — the ordinary case — so a strict generated client (pydantic, quicktype) rejected the response of GET /v1/empresas/\{id\}, the read that carries capabilities and the onboarding handoff. |
ambiente | certificacion · produccion | — | Which SII environment live emissions target. READ-ONLY AND DERIVED — it is not accepted on create or update, and sending it is refused with 400 invalid_field. The rule: emission_channel: sfg is always produccion, because the SII habilitated the empresa when it enrolled in the free system. emission_channel: mercado is certificacion until the SII issues its resolución, which is the only thing that moves it — or from the start when a stated origen: mercado resolución is verified by our own probe. It was a request field with default: produccion until 2026-09, and the applied default was the opposite, so a generated client that filled schema defaults created uncertified empresas in production. State origen and emission_channel; read this back. |
emission_channel | mercado · sfg | — | HOW THIS EMPRESA EMITS TODAY. mercado is Facturia's own certified engine; sfg drives the SII's free Sistema de Facturación Gratuito on the empresa's behalf, with folios the portal manages. The titular states it — it is a preference, not something we can read off the SII — and it decides which checklist, which tipos (sfg has no boleta form and no tipo 46) and which ambiente. A stored NULL reads as mercado, so this is never null. Default: "mercado". |
certification_track | mercado · sfg · null | — | WHICH CERTIFICATION CHECKLIST GOVERNS IT — a different question from emission_channel, and the two can legitimately disagree: an empresa can emit through the free portal while certifying for mercado, which is exactly what the pilot does. null is a real answer and the honest permanent state of a customer content on the free portal for ever: not pursuing certification. It folds the short SFG checklist. |
doc_types | 33 · 34 · 39 · 41 · 46 · 52 · 56 · 61[] | — | |
origen | gratuito · mercado · ninguno · null | — | How the empresa emits today, as you declare it. gratuito = it uses the SII's own Sistema de Facturación Gratuita; mercado = it already uses a market provider; ninguno = it does not emit electronically yet. null only on empresas created before the field existed — never declared, and deliberately not backfilled. It routes the onboarding copy we show and nothing else. Whether an empresa may skip certification is decided 100% empirically — a claimed resolución only earns the skip once matched against a real operation actually performed in that capacity, never a portal reading — and the value you declare here is never an input to that decision. Enrolment in the free system makes a RUT an emisor of real DTEs without a resolución de mercado, so a declared mercado we trusted would skip certification for exactly the clients who still need it. |
resolucion | ResolucionState | — | «¿YA EMITES CON OTRO PROVEEDOR?» — the resolución this empresa holds, and whether we have PROVED it (ruling 2026-09-07). The SII certifies the contribuyente, not the software. An empresa that already invoices through another provider holds a resolución the SII issued to its RUT, and that resolución does not stop being valid because it changed vendors — so it can state it on POST /v1/empresas or PATCH /v1/empresas/\{empresa_id\}, and a verified one skips the whole certification leg. verified NEVER COMES FROM THE CLAIM. Stating a resolución changes nothing on its own. The claim waits as verification_status: pending_verification until a Facturia operator confirms, for this empresa, the ONE production timbraje that verifies it (POST /v1/empresas/\{empresa_id\}/resolucion/verify; verification_request is that exact request). Palena refuses a RUT it has not authorized, so a granted range is the proof (verified) and a refusal is the answer too (refused, with the SII's words in refusal). Only that act — or the SII issuing the resolución to us at the end of a certification we ran — sets verified: true. Nothing is asked of the SII implicitly (sii-api#581 D1, 2026-09-23). Branch on verified, never on the presence of numero. While it is false the empresa is walking the ordinary checklist and onboarding.blocked_on says where it is; when the probe was refused, GET .../onboarding's blocked_reason.resolucion_claim carries the SII's own sentence. null for an empresa that never claimed one and has not been issued one. |
resolucion_boletas | ResolucionBoletasState | — | The boleta half of the same question (ruling 2026-09-07). The SII habilitates boleta electrónica separately from the resolución, and its answer is a DATE with no número: «habilitados los documentos de boleta electrónica a contar del …». verified is the same gate POST /v1/documents reads before building a 39 or 41, so it is true both for an empresa that arrived habilitated (its claim proved by a tipo-39/41 timbraje) and for one whose habilitación we ran. fecha: null with verified: true is the ordinary state of the second kind. null for an empresa that neither claimed a habilitación nor has one. |
reception_email | string,null | — | Per-empresa reception address, <rut>@dte.facturia.cl — the address the empresa registers with the SII as Mail Contacto Empresas. null FOR A SANDBOX EMPRESA (#450). A sandbox empresa reserves nothing: not the RUT (uniqueness is per mode) and not this alias. Publishing one for a scratch empresa would publish an address that routes a real company's supplier documents. Inbound reception is a live-mode fact. |
status | onboarding · active · suspended · archived | — | |
iva_shape | TaxpayerIvaShape | — | Which certification shape the SII put this taxpayer in, and how we know. Read-only — it is the SII's answer about your empresa, not a setting: no request body can write it. The SII decides, by itself, whether a contribuyente has activities afectas a IVA, and that decision issues a DIFFERENT set de pruebas. An exenta taxpayer is issued guías de despacho, facturas exentas, a libro de guías and a libro de compras para exentos — and no set básico at all — with boletas tipo 41. An afecta one is issued the set básico and the two libros, with boletas tipo 39. null means we have not established it yet. It is never a default: assuming afecta for an exento contribuyente would build eleven documents the SII will never review. |
onboarding | object | — | The same derivation GET .../onboarding uses (#414), so the two cannot disagree — they used to: status was the raw spine column and next_action_url filled in only when it read the literal string action_required, which the machine never stores. |
capabilities | EmissionCapabilities | — | «May this empresa emit, right now, and if not, who has to do what?» (#416, ADR-0011's 2026-09-02 addendum §2) — computed by the SAME function POST /v1/documents refuses emission with, so this object and a 422 can never disagree. blocked_by REPORTS subscription ON GET /v1/empresas/\{empresa_id\} and not on the LIST (#475, ruling 3): the single-object read asks the billing gate (one indexed read), the list does not, because that would be one billing lookup per row of every page. Absent means «not asked», never «fine». When it does report subscription, next.url is the checkout page for THAT empresa — the same URL the 402 publishes as details.checkout_url, so the object and the refusal name one page, not two. capabilities.tipos IS THE EMISSION TRUTH; doc_types IS THE DECLARATION (#493). They answer different questions and used to be published side by side with nothing saying which governs: doc_types is what the titular declared they intend to emit — it drives the CERTIFICATION checklist and which folios are requested — and tipos is what this empresa can emit right now, which is doc_types narrowed to what the current emission_channel supports (sfg has no boleta form and no tipo 46). Branch on tipos. To emit a tipo that is not in it, add it to doc_types with PATCH /v1/empresas/\{id\} (and, for 39/41, be on the mercado channel). |
created_at | string | — |
Empresa.onboarding
The same derivation GET .../onboarding uses (#414), so the two cannot disagree —
they used to: status was the raw spine column and next_action_url filled in only
when it read the literal string action_required, which the machine never stores.
| Field | Type | Required | Notes |
|---|---|---|---|
status | draft · mandato_pending · mandato_signed · certificate_pending · certificate_verified · sii_postulacion_pending · certification_in_progress · cumplimiento_pending · resolucion_pending · folios_pending · emitting · action_required · blocked · suspended | — | |
progress | number | — | |
blocked_on | string,null | — | The step key holding this empresa up, or null. Same field as GET .../onboarding's blocked_on. |
next_action_url | string,null | — | ALWAYS null here (contract (d) — never a URL that opens «Falta el enlace»). A capability-token onboarding page is a link scoped to ONE empresa and this object does not hold one; url below is where to get one. |
url | string | — | GET .../onboarding for this empresa — the full handoff (pending_action, including pending_action.link.mint to acuñar a live action URL) that this summary object does not carry. ABSOLUTE (#473, #419): https://api.facturia.cl/v1/empresas/\{id\}/onboarding, the same origin pending_action.link.mint.url and verification.recheck.url carry. It was a relative path under a format: uri declaration, which is a lie a strict client rejects and a URL an agent cannot dereference. |
Origen
How the empresa emits today, as you declare it. gratuito = it uses the SII's own
Sistema de Facturación Gratuita; mercado = it already uses a market provider;
ninguno = it does not emit electronically yet. null only on empresas created
before the field existed — never declared, and deliberately not backfilled.
It routes the onboarding copy we show and nothing else. Whether an empresa may skip
certification is decided 100% empirically — a claimed resolución only earns the skip
once matched against a real operation actually performed in that capacity, never a
portal reading — and the value you declare here is never an input to that decision.
Enrolment in the free system makes a RUT an emisor of real DTEs without a resolución
de mercado, so a declared mercado we trusted would skip certification for exactly
the clients who still need it.
| Value | |
|---|---|
"gratuito" | |
"mercado" | |
"ninguno" | |
null | undeclared |
Onboarding
| Field | Type | Required | Notes |
|---|---|---|---|
object | string | — | |
empresa_id | string | — | |
mode | live · test | null | — | test or live — which world this empresa is in (#418). A sandbox empresa is born emitting with a checklist of skipped steps, and a live one at the same emitting walked eleven of them; the two bodies are otherwise identical, so a screenshot, a log line, or a webhook payload could not tell which one produced it. null when the caller building this body did not say — never a guess, because live is the answer that gets acted on. |
status | draft · mandato_pending · mandato_signed · certificate_pending · certificate_verified · sii_postulacion_pending · certification_in_progress · cumplimiento_pending · resolucion_pending · folios_pending · emitting · action_required · blocked · suspended | — | |
progress | number | — | |
blocked_on | string,null | — | The step a human (or a failed requirement) is holding up. Always names a step present in steps[]. When status is action_required that step always carries a pending_action — which is where its link.mint lives, and where its action_url lives when one is live; when status is blocked it carries a blocked_reason instead, because a blocked empresa has nothing to click — a fact at the SII has to change. |
pending_action | PendingAction | null | — | The handoff for the step blocked_on names, hoisted so "what is blocking me and what do I tell my human" is one field read. null when the blocking step is OURS or the SII's, which is the honest answer and a different thing from "there is nothing to do": it means wait, not act. |
estimated_ready_at | string,null | — | Today plus the median business days the remaining steps take (Mon–Fri, no feriado calendar), each one counted as steps[].eta_business_days publishes it — so a step that still waits on a person of ours (execution: operator) contributes the sitting floor rather than its queue estimate (2026-09-07, #556). An estimate, not a promise: the rep-legal decisions are wall-clock unbounded, and it counts OUR time only — not what the SII takes to answer the postulación or to issue the resolución. null once emitting or suspended, while blocked, and while draft. |
requirements | object[] | — | What the SII requires of the contribuyente before it will authorize an emisor, as we last read it from the SII. EMPTY until the probe has run once — an empty list means "we have not looked", not "there is nothing to do". unknown means we looked and could not read the answer; it is never treated as met. |
steps | object[] | — | The checklist, in order. habilitacion is present only for empresas whose doc_types include a boleta type (39 or 41) — render what you are given rather than assuming a length. The four certification steps are evaluated PER DOCUMENT TYPE by the SII; each appears once here with the worst status across the declared types, and certifications[] breaks the same facts out per type. WHICH CHECKLIST YOU GET FOLLOWS certification_track, NOT emission_channel (#474, corrected 2026-09-04 — this paragraph named the channel and the code has always read the track). An empresa whose certification_track is sfg or null returns a SHORT, PARALLEL checklist — mandato, credential, clave_centralizado, usuario_autorizado, emisor_verificado, puesta_en_vivo, emitting — because a Sistema de Facturación Gratuito empresa is already a production emisor and never runs postulación, certification, cumplimiento, resolución or timbraje. For it certifications[] is empty. The two fields can legitimately disagree (ADR-0010 §1): the pilot emits through the free portal while certifying for mercado. A new empresa's track is set to its channel at creation; PATCH /v1/empresas/\{empresa_id\} changes each independently, and changing the channel alone deliberately leaves the track where it is. Render the steps you are given; do not assume which set you will receive. Two of those seven are the facts that ACTUALLY gate emission and used to appear on no checklist at all. clave_centralizado is the empresa's Sistema de Facturación Gratuito password: Portal MIPYME asks for it again at the irreversible step of every emission, so without it nothing can be issued even with the delegation live and a certificate in custody. puesta_en_vivo is OURS — the owner-present canary and the per-empresa enablement — and it is actor: facturia, rendered as a waiting state with an estimate, never as something a customer must do. |
certifications | object[] | — | Doc types certify separately; boletas certify in parallel with facturas. |
iva_shape | TaxpayerIvaShape | — | Which certification shape the SII put this taxpayer in, and how we know. Read-only — it is the SII's answer about your empresa, not a setting: no request body can write it. The SII decides, by itself, whether a contribuyente has activities afectas a IVA, and that decision issues a DIFFERENT set de pruebas. An exenta taxpayer is issued guías de despacho, facturas exentas, a libro de guías and a libro de compras para exentos — and no set básico at all — with boletas tipo 41. An afecta one is issued the set básico and the two libros, with boletas tipo 39. null means we have not established it yet. It is never a default: assuming afecta for an exento contribuyente would build eleven documents the SII will never review. |
updated_at | string | — |
Onboarding.requirements[]
| Field | Type | Required | Notes |
|---|---|---|---|
key | inicio_actividades · verificacion_actividades · representante_legal_certificate · usuario_administrador · sin_situaciones_pendientes | — | |
label | string | — | Spanish. The requirement as the person who has to satisfy it reads it. |
status | met · unmet · unknown | — |
Onboarding.steps[]
| Field | Type | Required | Notes |
|---|---|---|---|
key | mandato · certificate · sii_postulacion · set_de_pruebas · simulacion · intercambio · muestras_impresas · cumplimiento · resolucion · habilitacion · folios · credential · clave_centralizado · usuario_autorizado · emisor_verificado · puesta_en_vivo · emitting | — | |
label | string | — | Spanish. The step's name as a person reads it on a checklist. |
status | pending · in_progress · action_required · completed · failed · skipped | — | failed means something we run gave up after its retries. It is ours to fix and never moves the empresa to blocked. skipped means the step was never required — most often because the RUT was already an authorized emisor electrónico, so there was nothing to certify. |
actor | facturia · customer · sii | — | customer marks a step a PERSON PERFORMS: the mandato, the certificate upload, and on the SFG catalog the clave_centralizado and the usuario-autorizado grant. The certificate upload and the clave happen in OUR app rather than at the SII, and are the two an agent can complete on its principal's behalf (see pending_action.agent_actions). facturia DOES NOT MEAN «nothing for anybody to do». Three mercado steps are ours and still cannot finish without a person: sii_postulacion, cumplimiento and habilitacion. We perform each of them at the SII with the custodied certificate — which is the representante legal's own — after that person approves on a hosted page, and the field that says a decision is pending is pending_action.approval, never actor. Branch on the handoff. puesta_en_vivo is the opposite case and the one to watch: everything the customer owes is done and the canary is ours, so it carries no pending_action at all. |
help | string | — | Spanish. One or two sentences explaining the step to the person who owns it — written for them, not for you. Relay it verbatim rather than paraphrasing: several of these carry the legal reason an act cannot be delegated. |
action_url | string,null | — | A hosted page you can open RIGHT NOW, or null. Non-null only in the response that minted the link (POST .../onboarding/links, or POST .../mandato with an empty body): the pages are reached by a capability token stored only as a hash, so a read cannot reproduce the address. On GET .../onboarding it is null. It used to be non-null on every step at action_required, built from the step key — and that URL is rejected by the page it addresses. pending_action.link is the field to use: it names the call that mints a URL that works. |
link | object,null | — | WHAT THE HOSTED LINK FOR THIS STEP IS DOING (contract C3, 2026-09). Exactly the object documented at PendingAction.link — status, minted_at, expires_at, opened_at, requested_by, mint (null while a link is live or opened), ttl_seconds, reusable — published on EVERY step a person can be sent to, not only on the one blocked_on names. null on the steps that are ours, where there is nobody to send. It was published only inside pending_action, which exists on one step at a time, so an agent that had minted links for two steps could read the state of one of them. That is the exact question this contract exists to answer, because minting again REVOKES the URL somebody may already be holding. |
pending_action | PendingAction | null | — | The §7.4 invariant, in the field that can actually keep it: onboarding never asks for a human without telling them what to DO and how to reach the page. Non-null on every customer step that owes an action — the one at action_required and the one blocked_on names — and null on every step that owes none, and on the steps that are ours: instructions to a person about work we run unattended would be worse than silence. |
completed_at | string,null | — | |
evidence_source | sii_probe · own_record · no_producer · attested | — | How this step's completion becomes known (#395), so a caller never has to guess whether anything is actually watching. sii_probe — we re-read the SII and the step advances on what it says. own_record — the act lands in our own tables, so we see it as it happens. no_producer — NOTHING IN THIS BUILD OBSERVES IT (habilitacion today, the boleta habilitación at certBolElectDteInternet, an uncaptured SII SPA). Use POST .../onboarding/steps/\{step\}/attest — never a plain recheck, which cannot confirm what nothing observes. attested — a no_producer step that reached completed through exactly that atestación. The only way such a step can be completed in this build. |
execution | automatic · operator | — | HOW A STEP OF OURS ACTUALLY RUNS (2026-09, #487; derived per deployment since 2026-09-07, #556). actor: facturia says who owns the step and nothing about whether anybody is watching. Seven of ours reach the SII only when a fence is open — the postulación, the intercambio uploads, the muestras_impresas filing, the Declaración de Cumplimiento, the boleta habilitacion, and the «Avanzar Siguiente Paso» that closes set_de_pruebas and simulacion (one fence, one act, both pasos) — and while a fence is shut, that step is a person of ours at a keyboard. The first five published in_progress indistinguishably from a job on a schedule; the stage-advance pair was worse and published automatic outright while a person clicked in a browser. Either way it is the difference between "this finishes overnight" and "this finishes when somebody is at a desk". THIS IS NOT A CONSTANT OF THE PRODUCT. It is read per deployment, from the same environment variable the executor reads, so the SAME step can answer operator on one deployment and automatic on another, and can change from one day to the next without a new API version. Branch on the value you were given; never cache it as a fact about a step key. automatic on every step with no such fence, including the customer's, where it means "nothing of OURS is pending on it". The authority for WHICH fence belongs to which step is the lock register (worker/src/onboarding/locks.ts), whose rows name the step each fence holds; a test walks it against the catalog in both directions, so a fence that names a step nothing publishes as an operator's fails the build, and so does the reverse. |
eta_business_days | integer | — | This step's own p50, in business days (2026-09, #487). estimated_ready_at at the top of the body is the sum over everything still to run; this is the one line of it, so a checklist can put a number beside the row a person is actually looking at. A p50, not a promise. A STEP READING execution: operator NEVER PUBLISHES LESS THAN 2 (2026-09-07, #556). The catalog's base numbers were decomposed from a queue — 0 means "the SII answers this envelope in minutes" — and none of them describes a person of ours finding an hour for a sitting. So while a step waits on one, its p50 is floored at two business days, and it drops back to the base the moment that step's fence opens. Read it off the wire per step; it moves with execution. |
next_poll_at | string,null | — | WHEN WE LOOK AT THE SII AGAIN FOR THIS STEP (2026-09-07, #563), or null when no read is scheduled. Present on every step, and it is the field that makes an in_progress step honest. A step whose last probe learnt nothing — the SII did not answer, or answered "granted, not yet applied" — used to be indistinguishable on the wire from one that is about to finish, and an agent reading the checklist reported "we are doing fine" about a wait nobody had timed. Render it with blocked_reason: "el SII no contestó; volvemos a mirar a las 13:47". A SCHEDULE, not a promise: it is when the probe's own cadence falls due, and the reconciler notices within about five minutes of it. POST .../onboarding/steps/\{step\}/recheck asks now instead of waiting for it. |
blocked_reason | object | — | Why this step is where it is, as structured data with a code and a Spanish message. On the steps only a representante legal may perform it also carries a packet — the SII page to open, the empresa RUT, a checklist, and the reminder that the step needs THEIR certificate, not the one you custodied with us. Additional keys may appear; branch on code. ON failed, action_required AND in_progress (2026-09-07, #563). This said "failed or action_required", and the third case is the one a caller most needs: a step that is running publishes the LAST PROBE OUTCOME while that probe did not settle it — sii_unreachable (no answer from the SII), sii_propagation (the SII shows the grant and has not applied it yet), requirement_unmet (its own user table refuses the empresa's sender; details.key names the permiso). None of the three blocks the empresa, names it in blocked_on or asks for a person: they carry retriable: true and a Spanish hint, and next_poll_at beside them says when the next read is due. Absent while a running step has nothing to report. |
Onboarding.certifications[]
| Field | Type | Required | Notes |
|---|---|---|---|
tipo_dte | 33 · 34 · 39 · 41 · 46 · 52 · 56 · 61 | — | 33 factura afecta · 34 factura exenta · 39 boleta afecta · 41 boleta exenta · 46 factura de compra · 52 guía de despacho · 56 nota de débito · 61 nota de crédito. Boletas (39/41) travel a different SII transport; that is invisible through this API. |
status | not_started · in_progress · certified · failed | — | |
note | string | — |
PendingAction
The handoff for an act that still needs a person — the OAuth-consent analogue of this API.
WHY A PERSON, AND IT IS NOT WHAT THIS FIELD SAID UNTIL 2026-09-04. It said the SII "requires the taxpayer or their legal representative to authenticate personally, with THEIR OWN certificate", and that the custodied one "signs documents; it cannot authenticate a person". That is false, and it was the load-bearing sentence under three surfaces. A Chilean CA issues certificates to NATURAL PERSONS: the one in the vault is the representante legal's (mandato 1.3), and a TLS client-certificate session with the SII authenticates AS ITS HOLDER. Facturia ran an entire boleta certification with it, end to end, from code, on 2026-09-04.
What remains is a LEGAL line rather than a technical one, and it is narrower. Mandato
clausula 4.6 "Actos reservados al Cliente" reserves some acts to the customer; a few
others are decisions only they can take because of what the act COSTS — the month an
empresa stops being a free-system user, the moment a 24-hour SII clock starts. Those
arrive as an approval block below: we perform the act with the custodied certificate,
and a person authorises it on a hosted page of ours. The sii_postulacion step is
exactly that shape now — Facturia files the postulacion, after the titular approves.
Everything a consumer needs to relay the errand to a human is here, so no second call is needed and no click path has to be improvised. Customer-facing strings are in Spanish because the person who performs the act is Chilean and is frequently not the account holder; field names are English like everything else.
Present on steps[].pending_action for any step that asks a human to act, hoisted to
onboarding.pending_action for the step blocked_on names, and carried inside
error.details.pending_action when a /v1 write is refused because one is outstanding.
null whenever the wait is ours or the SII's - which means wait, not act.
| Field | Type | Required | Notes |
|---|---|---|---|
object | string | — | |
act | string | — | The ACT, which is not always the step. One step can park on two different errands - certificate means "buy a certificate from a CA" with no certificate on file and "grant «Enviar Doctos.» in Mantencion de usuarios" when the holder lacks the permission, which is a different portal and possibly a different person. Branch on this rather than on step when you care which. |
step | string | — | The onboarding step this handoff hangs off. |
empresa_id | string | — | |
title | string | — | Spanish, one line: what must happen. |
summary | string | — | Spanish, for someone who has never heard of the SII. |
action_url | string,null | — | A LIVE hosted page, or null. The same value as the step's action_url. The hosted pages are reached by a single-use capability token that is stored only as a SHA-256, so no read can reproduce one: this field is non-null only in the response that MINTED the link (POST /v1/empresas/\{id\}/onboarding/links, or POST /v1/empresas/\{id\}/mandato with an empty body). Everywhere else it is null and link below says how to get one. It used to be a URL built from the step key and never null - and that URL is REJECTED by the page it addresses, which answers "Esta pagina se abre desde el enlace que te mandamos" without a ?token=. Relaying it sent a representante legal to a page telling them to ask somebody for a link. Do not reconstruct it; mint it. |
link | object | — | What the hosted link for this step IS DOING, and how to obtain one. Always present. The refusal points at the endpoint that fixes it, the way RFC 9728's resource_metadata points a 401 at the consent flow. Issue mint exactly as given; the response carries the URL once. status first, then mint (2026-09, #459). action_url is null on every read by construction - the token is stored only as a SHA-256 - so an agent re-reading this body saw a null URL beside a mint recipe and minted again. For mandato that REVOKES the link the representante legal already has in their inbox; for any other step it retires a URL somebody may be about to open. mint is therefore null exactly while status is live or opened: there is already a link, and minting would destroy it. Branch on status, and treat a null mint as "you already have one", never as an error. |
agent_actions | object[] | — | What YOU can do here, as opposed to the human. Always present; usually empty, because these acts happen at the SII under a person's own certificate and a list full of hopeful-looking calls would be worse than none. The cumplimiento handoff is the one that carries a real entry: POST .../onboarding/go_live with an empty body, which mints the approval link for the titular. That call used to appear only as Spanish prose inside a prerequisites[].label - which this object tells you to relay VERBATIM - so agents read it out to a representante legal who has no API key. |
sii_url | string,null | — | The SII's own page, when we can name one that will not 404. null when we were not told which ambiente the empresa runs in - maullin is certification and palena is production, and a representante legal sent to the wrong one either bounces or rehearses the real thing, so we say nothing rather than guess. |
performed_by | object | — | WHO, specifically. Never "the customer". |
prerequisites | object[] | — | What to have in hand BEFORE opening the page. |
instructions | object[] | — | Ordered and concrete, contiguous from 1. Relay them VERBATIM - do not paraphrase and do not add steps. See evidence for how much these can be trusted. |
consequences | string[] | — | Spanish. What performing the act sets in motion, where that is not obvious and getting it wrong is expensive - the declaracion de cumplimiento starts the empresa's Informacion Electronica de Compras y Ventas obligation from the month it is made. Empty for acts with no such consequence. |
verification | object | — | How we find out it happened. Never "the customer tells us". |
estimated_minutes | integer | — | The human's time, at p50. |
next | string | — | Spanish. What happens after, and whether they must tell us. |
evidence | captured · documented · entry_point_only | — | How well-evidenced the click path is, published because a confidently wrong instruction is worse than an honest vague one. captured - we have driven this exact chain and the pages are fixtures in our repository; the instructions name real screens. documented - the SII's own documentation names the path and we have not walked it; you get the menu route, not a field-by-field script. entry_point_only - we can name the door and nothing behind it. |
caveat | string,null | — | Spanish. Non-null whenever evidence is not captured, saying exactly what we are unsure about. RELAY IT. Dropping it turns "the exact screen may differ" into a promise we cannot keep. |
copy | object,null | — | Copy by phase, for the one act whose hosted page and dashboard wizard used to write their own prose and drifted apart over who declares (#393). null for every act except cumplimiento, which publishes before_authorization, after_authorization and after_filed. THIS IS THE SINGLE AUTHOR. Both surfaces render this rather than keeping their own version — which is exactly how one used to say «cuando el SII abra el trámite lo hacemos nosotros» while this very object, right beside it, said the representante legal declares in person (cláusula 4.6 b). It says the truth of this build: Facturia does not declare (ALLOW_LIVE_CUMPLIMIENTO_WRITES is false, structurally); once the titular approves the month, the copy says to declare in the SII with the packet — never that Facturia will do it. |
PendingAction.link
What the hosted link for this step IS DOING, and how to obtain one. Always present.
The refusal points at the endpoint that fixes it, the way RFC 9728's resource_metadata
points a 401 at the consent flow. Issue mint exactly as given; the response carries
the URL once.
status first, then mint (2026-09, #459). action_url is null on every read by
construction - the token is stored only as a SHA-256 - so an agent re-reading this body
saw a null URL beside a mint recipe and minted again. For mandato that REVOKES the link
the representante legal already has in their inbox; for any other step it retires a URL
somebody may be about to open. mint is therefore null exactly while status is
live or opened: there is already a link, and minting would destroy it. Branch on
status, and treat a null mint as "you already have one", never as an error.
| Field | Type | Required | Notes |
|---|---|---|---|
status | none · live · opened · consumed · expired · revoked | — | - none - nobody has minted one. Mint. - live - minted, nobody has opened it. Re-send the URL you were given, or wait. - opened - somebody has the page open. Minting now breaks their tab. - consumed - somebody acted on it. The step moves on its own from here. - expired - the clock ran out. Mint a fresh one. - revoked - it was cancelled or superseded. Mint a fresh one. |
minted_at | string,null | — | |
expires_at | string,null | — | |
opened_at | string,null | — | When somebody first opened the page. Never moves on a reload, and always null for the mandato, whose page records none. |
requested_by | object,null | — | Who asked for the link, AS THEY DESCRIBED THEMSELVES. Optional on the mint (POST .../onboarding/links and POST .../mandato both accept requested_by: \{kind, name\}), and rendered to the person who receives the link - "Este enlace lo pidio Claude Desktop" instead of "lo genero una integracion". It is a CLAIM, never an authentication fact: the credential the mint actually arrived on is recorded separately and is not published here. |
mint | object,null | — | How to mint one - null while status is live or opened, because minting there REPLACES a link somebody is already holding. |
ttl_seconds | integer | — | How long the minted link lives. 7 days for the mandato, 30 for the rest. |
reusable | boolean | — | false for the mandato - its token is single use, because it produces a legal instrument and a second click must be told "ya se firmo" rather than offered a second signature. true for every other step: those produce no instrument of ours, so the link is a durable address for a checklist a person may re-read before acting. |
PendingAction.agent_actions[]
| Field | Type | Required | Notes |
|---|---|---|---|
method | string | — | |
url | string | — | Absolute; issue it as given. |
body | object | — | |
label | string | — | Spanish, one line: what this call does. |
when | string | — | Spanish, one line: when to make it, relative to the human's act. |
PendingAction.performed_by
WHO, specifically. Never "the customer".
| Field | Type | Required | Notes |
|---|---|---|---|
role | representante_legal · usuario_administrador · titular_certificado · titular_cuenta | — | |
label | string | — | Spanish. The person, not the party. |
why_not_delegable | string | — | Spanish, one sentence. Relay it: "the law requires you personally" persuades a reluctant signatory far better than "the software cannot do this". |
legal_basis | string[] | — | The citations that sentence stands on. Verbatim and checkable. |
PendingAction.prerequisites[]
| Field | Type | Required | Notes |
|---|---|---|---|
key | string | — | |
label | string | — |
PendingAction.instructions[]
| Field | Type | Required | Notes |
|---|---|---|---|
order | integer | — | |
text | string | — |
PendingAction.verification
How we find out it happened. Never "the customer tells us".
| Field | Type | Required | Notes |
|---|---|---|---|
method | sii_probe · own_record · no_producer | — | sii_probe - we re-read the SII and the step advances on what it says. own_record - the act lands in our own tables, so we see it as it happens. no_producer - NOTHING IN THIS BUILD OBSERVES IT. Named rather than hidden: a step whose completion nobody produces would otherwise sit action_required forever while everyone assumes something is watching. Do not tell a user we are watching for one of these. |
description | string | — | |
recheck_url | string,null | — | The recheck endpoint for this step, as POST <path> — a method glued to a relative path a caller has to parse apart and prefix a host onto. Deprecated — use recheck instead, which names the identical endpoint as \{method,url\}, absolute (#419). Null when there is nothing to ask. Kept, and still populated, for existing callers. |
recheck | object,null | — | The same endpoint as recheck_url, structured (#419, contract (d)) — the shape link.mint already ships. null under the exact same condition as recheck_url: nothing to ask (method above is no_producer). |
sii_latency | string | — | Spanish. Roughly how long the SII takes to show it. |
OnboardingRecheck
The answer to a recheck. It records that you asked and says what we queued; it verifies nothing by itself.
| Field | Type | Required | Notes |
|---|---|---|---|
object | string | — | |
empresa_id | string | — | |
step | string | — | |
status | pending · in_progress · action_required · completed · failed · skipped | — | The step's status as it stood when you called, read BEFORE anything was enqueued and returned UNCHANGED. Reading back the same status you posted against is the endpoint working correctly. |
accepted | boolean | — | The claim was recorded and the producers queued. |
verified | boolean | — | Always false. Verification is what the producer does, later, against the SII. This field exists so no caller can mistake acceptance for confirmation. |
evidence_source | sii_probe · own_record · no_producer · sandbox | — | How this step can be confirmed. See PendingAction.verification.method. |
contact | object,null | — | WHERE TO GO when evidence_source: 'no_producer' means this very call could not confirm anything (#395) — null for every other evidence_source. Named as data rather than left in message's prose, so a caller holding only this response can still act on it. |
queued | string[] | — | Job types this call enqueued. |
already_queued | string[] | — | Job types already scheduled for this five-minute window. Not a failure - "we are already about to look" is a true answer to "please look". |
message | string | — | Spanish, for the person who made the claim. |
pending_action | PendingAction | null | — | The handoff again, so a caller who got the errand wrong can re-read it. |
onboarding_url | string | — | Relative. DEPRECATED (2026-09, #497) in favour of next, which is the same read as \{method, url\} absolute - no method to split off the front, no host to prefix by hand. Still populated, unchanged. |
next | ApiCall | — | ONE SHAPE FOR "THE NEXT CALL" (#419, #497). A method plus an ABSOLUTE url, optionally with the body and headers to send, so an object carrying one is enough to issue the request — no method parsed off the front of a string, no host prefixed by hand, no relative path resolved against a base the caller has to know. Five different spellings of this idea shipped before it was named. Where an older field published one of them (capabilities.next.api, contact.api, onboarding_url), the old field is KEPT and populated and an ApiCall sibling is added beside it — additive, per rule #419 — and the prose on each says which one to branch on. |
requested_at | string | — |