Changelog
What changed in the API, with dates. `/v1` evolves additively; removals get 12 months of notice.
/v1 evolves additively: new endpoints, new optional fields, new response fields, new enum values, new event types. Your client must ignore what it does not recognise. A removal or a change of behaviour gets 12 months of notice, a Sunset header on the affected endpoints, and an entry here (§5.8).
There is an RSS feed at https://docs.facturia.cl/en/feed.xml. Subscribing to it is part of a good integration.
2026-10-06 — GET /v1/contribuyentes/\{rut\}: razón social, activities and a giro that fits the factura, from the SII's public nómina
Where it comes from. The SII's public nómina of personas jurídicas (https://www.sii.cl/sobre_el_sii/nominapersonasjuridicas.html), with no captcha and no login. Facturia reloads it when the SII publishes a new edition, and source.published_at says which one answered. Personas naturales are not in it: their RUT answers 404, like a RUT that does not exist.
The suggested giro. The official descriptions rarely fit GiroRecep's 40 characters (the one for buying and selling vehicles has 81), and cutting them would change what the factura says about the receptor. So suggested_giro uses a reviewed short text for the most common activities (basis: curated), the official description when it fits as it stands (sii_description), or nothing.
How to use it. Show it to the person and, once confirmed, store it with PATCH /v1/empresas/\{id\}/counterparties/\{rut\}: that is what the next emission reads. No Facturia-Empresa needed; it requires empresas:read and is limited to 120 requests a minute per account.
New error code. 503 contribuyentes_unavailable while the nómina has not been loaded; retry after Retry-After.
2026-09-23 — A natural person as a boleta receptor: the name reaches the document
What happened. POST /v1/documents stores the receptor in its public shape (legal_name, address) and the boleta builder reads another. The factura and SFG-portal lanes translated between the two; the boleta lane did not, so a boleta to «María José Pérez Soto» went out with her RUT in <RUTRecep> and Sin RUT in <RznSocRecep> and in the timbre. The SII accepts it — it is a valid document — but it does not name the person, and an Isapre or seguro complementario reimbursement reads exactly the name.
What changes. The name and address you send reach the boleta, and the timbre says what the document says. The anonymous boleta is unchanged: no receptor, 66666666-6 and «Sin RUT». There is no new field and no company flag: a natural person is a Party like any other.
And the credit note. A 61 to that same person is a factura-family document and carries giro, address.street and address.comuna like every 61, even when it annuls a boleta. POST …/credit_note copies the receptor from the original boleta, which has none of them: store them once in the address book (PATCH /v1/empresas/\{id\}/counterparties/\{rut\}) before annulling, or the note is refused with 400 receptor_incomplete before a folio is spent.
2026-09-23 — «Already emitting with another provider?»: a declared resolución is verified when an operator confirms it, and resolucion.verification_status says where it stands
What happened. An empresa that already invoices through another provider declares its resolución (resolucion: \{numero, fecha\}), and we verify it by asking the SII for one folio range of a declared factura tipo: the SII will not timbrar for a RUT it has not authorized. That request went out with the empresa's own ambiente, which starts in certification, and since 2026-09-08 an emission timbraje in certification is blocked before a connection opens. The local block was read as the SII's refusal, so no declared resolución could ever be verified, and the empresa fell into the full certification without the SII being asked anything.
What changes. Verification is made against the production SII, the only honest question. Because registering a range in production is irreversible, it never happens implicitly: not when the empresa is created, not by the onboarding process, not in certification. A Facturia operator confirms it, empresa by empresa, with POST /v1/empresas/\{id\}/resolucion/verify and confirm: true. That operation authenticates with the operator's secret; no API key can call it, root keys included.
What you see. resolucion gains three fields. verification_status is pending_verification (declared; waiting for the confirmation, in flight, or an attempt the SII did not answer about the RUT), verified (the SII granted the range: certification is skipped and the empresa moves to production) or refused (the SII answered without a range). verification_request is the exact request: tipo, quantity and ambiente. refusal carries the SII's words when it refused. Re-declaring the resolución with PATCH returns a refused claim to pending_verification. verified and verified_at keep their meaning.
New error codes. 400 confirmation_required, 401 operator_authentication_required and 409 resolucion_verification_not_allowed (the reason in error.details.reason). Only a caller of the operator operation receives them.
2026-09-10 — GET /v1/folios counts the folios you actually hold: queued ranges stop being invisible
What happened. Ever since the allocator learned to ROLL — when next_folio passes caf_hasta it takes the next active range starting above it and emits from there — the platform held two different truths: the emitter's, which saw every range, and the readers', which kept subtracting caf_hasta - next_folio + 1 over a single one. This endpoint was one of those readers, and its own header promised the opposite: «it reads the same tables allocate_dte_folio allocates from, so what it reports is what the next emission will actually get». The response even listed both ranges in ranges[] while reporting available: 1.
What changes in the object. available is the sum of the CHAIN the allocator will consume: what is left of the current range, plus each active range starting above the previous one, in order. A chain and not a sum of everything present: an overlapping range is never reached, so it is never counted. next_folio is the folio the next emission takes, and it is null when no emission of that type could succeed right now — it used to report the out-of-range number the counter was parked on. status keeps its meaning but is computed over the correct count: a company that already held its replenishment stops showing as low.
And a new block, stock. stock.chain carries every range that will be consumed, in order, with from, to, remaining and expired; stock.stops_at_expired_range says whether the walk ended on a lapsed range rather than on «nothing more was granted». That is not the same as available == 0: a chain can carry folios usable today and still stop behind them at a range whose vigencia has expired. Res. Ex. SII N° 58/2017 §1° makes such a range unusable for the documents that grant crédito fiscal, the allocator rolls onto it anyway and the emission path refuses it — so counting what sits behind it would promise folios no emission can reach.
The same mistake was costing ranges. Automatic replenishment read the same subtraction, so it saw low on a company that already had its replacement range — and it answers low by asking for a timbraje. An unused range has to be annulled and five of them block further timbraje for that document type, so the old arithmetic pushed a company toward that block one refill at a time. Replenishment now uses the same chain, so a waiting range counts and the request is not made.
Nothing changes about emission. The allocator was already right and was not touched: anyone holding queued ranges could always emit against them. What was wrong was what the API counted, and therefore what an agent or a dashboard repeated. The status enum also now documents sfg_managed, which this endpoint has returned since the SFG channel existed and the contract never declared.
2026-09-09 — The address book: record a customer’s address once and stop typing it on every document
Why it exists. On 8 September 2026 seven exempt facturas went out with a receptor carrying nothing but a RUT. GiroRecep, DirRecep and CmnaRecep are optional in the SII’s schema, so the envelope passed validation (EPR, with a TrackID) and the SII refused every document inside it: ACEPTADOS 0 / RECHAZADOS 1, seven times. Seven production folios burned silently. The two sources the resolver had that day — the empresa’s own address, only when the receptor is the emisor, and earlier documents to the same RUT — are no help for the first invoice to a new customer, which is exactly the case that burned the folios.
How you use it. PATCH /v1/empresas/\{empresa_id\}/counterparties/\{counterparty_rut\} with legal_name, giro and address is an upsert: the first call creates the entry and later ones edit it. It is partial — a field you do not send keeps its value — and null clears, which is how a value recorded by mistake is removed. No earlier document is required: recording the address of a RUT you have not invoiced yet is precisely the point. 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, and is refused over the limit rather than truncated: silently shortening a name of trade would change what every document to that counterparty says about who they are.
The order rules, and it runs from most deliberate to most inferred. What you send on the document wins; then your own empresa’s address, only when the receptor is you (the internal traslado guía); then the address book; and last an earlier document to the same RUT. The book beats the earlier document on purpose: it is where you go to CORRECT an address we have been getting wrong, and if last month’s won, the correction would be ignored until some emission happened to carry the new one. GET /v1/empresas/\{empresa_id\}/counterparties/\{counterparty_rut\} answers with what we actually know and source says which of the two it was: api if you recorded it, or the earlier document if we inferred it.
And the list is still derived. GET /v1/empresas/\{empresa_id\}/counterparties reads the receivers of the documents already emitted and joins whatever contacts and addresses exist; nothing copies a receiver into a row. Every entry now also carries legal_name, giro, address, source and updated_at, and on that list source: null means "nothing recorded", not "nothing known" — the single-counterparty read is the one that falls back to earlier documents.
2026-09-08 — An empresa that already invoices with another provider stops certifying twice: state the resolución and we check it by using it
The SII certifies the taxpayer, not the software. An empresa that already invoices electronically 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. Until today no surface could state it — the only door that ever wrote those two fields had been deleted as unsafe — so every empresa arriving from another provider walked all eleven certification steps.
The numbers are on your own invoices. numero and fecha are the NroResol and FchResol from the carátula of every documento tributario electrónico, so they can be read off any recent invoice. Nobody has to go to the SII for them.
Stating is not skipping. The field writes two columns and moves no step. resolucion.verified comes back false until we have checked it and the empresa walks its whole list meanwhile; branch on verified, never on the number being present.
We check it by using it. With your certificate in custody, the machine asks the SII's portal to timbrar one of the document types you declared. The SII refuses a timbraje for a RUT it has not authorized for that type, so a granted range is the proof: verified turns true, the certification steps report skipped with the note that says why, and estimated_ready_at is today. There is no consulta that answers this — no SII page separates a mercado resolución from an enrolment in the free system — which is why we check it by acting.
A refusal is never silent. If the SII refuses the timbraje, verified stays false, the full certification lane is offered, and the sii_postulacion step's blocked_reason.resolucion_claim carries the SII's own sentence, its code and the type it was asked about.
Boletas, with a date of their own. The boleta electrónica habilitación is a separate authorization and carries no number: the SII's mail says «habilitados los documentos de boleta electrónica a contar del …». State it in resolucion_boletas; it is checked with a 39 or 41 timbraje, and when it works the boletas are enabled without going through the habilitación chain.
The casilla de intercambio stays yours. The mandato forbids us from touching your empresa's contact details at the SII, so until you change the casilla in Mi SII your suppliers keep sending documents to your previous provider. Your address with us is published per empresa as reception_email.
2026-09-08 — A period nobody read is not an empty period: the sync block speaks of the month you asked for, POST /tax/sync really forces, the job settles, and the MCP gets sync_rcv
The RCV mirror is not the SII. What GET /v1/tax/sales, GET /v1/tax/purchases, GET /v1/tax/summary and the F29 proposal read is a copy filled by a daily sweep, and that sweep reads only the last three months. An empresa connected in August never had February read. Until today the sync block on those answers said "synced today at 05:25, fresh" for February anyway — it described the empresa's latest run, not the month asked for — and an agent read that as "February is current and empty" and told a person their sale did not exist. It existed; nobody had asked for it.
sync speaks of the month you asked for. With period, last_synced_at is the completion of the newest sync THAT READ THAT MONTH, status: syncing is one in flight for that month, and null is "never read". Without period the block still describes the whole empresa. The status vocabulary is now the real queue's too: degraded rests on failed_permanent, which is what the queue writes, instead of on two values that never existed.
POST /v1/tax/sync really forces. The queue row carried the same per-day key as the nightly sweep, so a call after 05:23 "collapsed" onto a run that had already finished and answered a job pointing at nothing. It now collapses only onto a sync for the same period that is still going to run or is running; if that one is done, it queues a fresh read. period defaults to the previous month in Santiago time and always rides in result.period.
The job settles. No rcv_sync job had ever moved past queued: nineteen in production on the day this was written, every one with its sync finished minutes earlier. The worker now moves it to running when it starts, to succeeded with purchases, sales, created, updated, withdrawn_at_sii and partial_errors when it lands, and to failed only when the queue stops retrying.
In the MCP: sync_rcv. One period, or from/to of up to ten months (the endpoint's bucket is ten per minute per empresa; if it trips mid-range the answer says exactly which periods were not queued and how long to wait), and job_id to check on one. query_rcv and get_periodo_summary now lead with the warning when the period was never read, and the server instructions carry rule 7: a period nobody read is not an empty period.
2026-09-08 — The MCP does everything the API does: 53 new tools, folios:write enters the vocabulary, and the RCV backfills twelve months on its own when a company connects
What comes in. Tax: characterize_purchases, revert_characterization, set_estado_contable, list_f29_propuestas, recompute_f29_propuesta, list_f29_filings, list_expenses/add_expense, list_statements/upload_statement, dismiss_tipo46_suggestion, and bank connections (list_bank_connections, link_bank_connection, sync_bank_connection, delete_bank_connection). Documents: send_document, emit_credit_note, retry_document, get_document_events, check_cesion, cede_document, revoke_document_links. Folios and CAFs, under the new folios:write scope: request_folios, list_cafs, upload_caf, deactivate_caf. Reception: acknowledge_received_document (the acuse de recibo, which is not acceptance). Account: get_account, get_job (generic, narrates every job type), list_usage_records, get_event, get_mandato_terms, list_webhook_endpoints, list_webhook_deliveries. Empresa: get_empresa, update_empresa (the door to declare a resolución you already hold from another provider and to change the channel), get/update_automation_settings, get/update_branding, counterparties and their contacts. Onboarding: get_certification, set_certification_inputs, list_onboarding_evidence, get_certificate, get_sfg_credential, delete_certificate, delete_sfg_credential, revoke_onboarding_link, request_mandato_revocation_link.
What stays out, and why. The person's acts (onboarding:attest, mandato:attest: signing, approving, authorizing the go-live) have no MCP scope; key minting (keys:write) would create a second authority behind a grant; webhook writes make no sense for an agent with no endpoint of its own (the reads are in); DELETE /empresas has no use case. And one rule of the MCP's own: update_automation_settings refuses to turn require_confirmation off — that flag exists so that the person, not the agent, is the record of approval.
The irreversible acts that do not pass through the approval page. The confirmation gate replays exactly four actions (emit_document, file_f29, emit_tipo46, reclaim_received_document). emit_credit_note is not one of them, so on an empresa with require_confirmation it REFUSES (confirmation_path_missing) rather than bypass the control; elsewhere it requires confirm_emit: true. cede_document, deactivate_caf, delete_certificate, delete_sfg_credential and delete_counterparty_contact require an explicit confirmation argument and never call the API without it. Each carries the destructiveHint that matches what it does, not what it is called.
folios:write is a new scope. A grant older than today does not hold it: if request_folios answers insufficient_scope naming it, reconnect the agent and consent again. The consent screen shows it as consequential (a timbraje spends a finite SII-authorized resource) and leaves it out of the one-click preset.
The RCV backfills itself. The daily sweep read the last three months and nothing else. Now, the first time an empresa has a successful (or partial) sync, the sweep queues the twelve months before that — once, derived from the queue itself (a row for a period older than the window, in any status, means the history was already asked for), honouring rcv_sync_frequency: manual, in one portal session. Both live empresas already had their history requested by hand, so neither is touched; the next one to connect gets it without anyone asking. sync_rcv remains the door for anything older.
The history emitted in the SII's free portal, once the portal has closed. The «Documentos emitidos» import (#45) ran only on the transition to emitting, and an empresa that got there by hand never had it. The daily sweep now queues it once for every empresa with an SFG footprint and a certificate in custody — including one that already migrated to mercado. And when the Portal MIPYME no longer shows the empresa (which is what graduating to mercado does — checked live today with both house empresas), the import does not fail loudly: it retires without an alert and takes the SII's other source, the RCV sales register, from which it brings every emitted document with receptor, legal name and the net/IVA/exempt split the portal never gave. When the receptor is itself a Facturia empresa, its intercambio copy of the signed EnvioDTE is already in our custody: the import links it, GET /documents/\{id\}/xml serves the real bytes and items carries the line detail. For an external receptor there is no XML or PDF we can hold: /xml and /pdf answer 409 artifact_not_available, permanent and honest, and the document lives at the SII. #46's claim ("the per-document download may re-send the DTE by email") remains untestable: no house document is reachable in the portal any more.
And query_rcv says everything the row carries. It used to narrate "neto $0 IVA $0" for a $13,916,387 exempt invoice and hide the receptor's RUT behind the legal name; an agent went looking on the internet for a RUT the SII had already given us. Every row now carries date, legal name with RUT, net, IVA, exempt and total.
2026-09-07 — A free-system empresa can issue boletas now — through our own engine, with the channel decided per type
The channel belongs to the document, not to the empresa. One function decides it everywhere (channelForTipo): types 39 and 41 → Facturia's own engine, any other type → the empresa's emission_channel, as before. Nobody has to switch channel to issue boletas, and doing so would be exactly the wrong move: it would take that empresa's facturas off the free lane.
What changes at POST /v1/documents. A boleta from an sfg empresa is no longer refused at parse time, and it is validated with our own engine's grammar (line discounts, global discounts, references) instead of the portal form's restrictions. The one type the free lane still refuses with tipo_not_available_for_channel is the factura de compra (46), where the SII declines to open its own form.
What gates it, and how you find out. Issuing a boleta needs the SII's boleta habilitación and the first CAF for 39/41. Until both are in, GET /v1/empresas/\{id\} lists neither 39 nor 41 in capabilities.tipos, and POST /v1/documents answers 422 empresa_not_emitting — the published code — naming the habilitacion step and carrying the pending_action with its two approvals. It does not answer tipo_not_available_for_channel: it is not that the lane cannot, it is that the SII has not authorized it yet.
Two new steps on the free lane's checklist, and only if you declared boletas. habilitacion and folios join SFG_ONBOARDING_STEPS with only_for_tipos: [39, 41], so an empresa that only invoices still sees exactly the seven it saw before. They are the same two steps the mercado lane has — same actor, same evidence, the same two approvals by the titular and the representante legal on pages of ours — and they come AFTER emitting: your facturas do not wait for the boleta habilitación, they go live as soon as the puesta en vivo is done.
One new per-empresa fact. boleta_emission_enabled_at (migration 155) records the SII's «habilitados los documentos de boleta electrónica a contar del …» plus the first CAF. The onboarding machine writes it itself, exactly as it writes the puesta en vivo, and it is what the emission door and capabilities both read — one function, one fact, no way for two surfaces to answer differently.
2026-09-07 — The Python recipe we publish now generates the whole client: the /account/* response types are back, and so are the five schemas the generator deleted
Nothing changes on the wire. Same JSON, same fields, same names, same values. What changes is how the document DESCRIBES what it already sent: an additive correction under the /v1 evolution rule (§5.8), and a client you already generated stays correct — regenerate it and it simply gains the types it used to lose.
The five /account/* responses have a type again. The envelope declared data: [object, "null"] and every operation redeclared its own typed data on top of it, through allOf. Those are two incompatible declarations of one property: «an object that is also null». The generator answered «EnvelopeDataType0 | None can't be merged with RegisterAccountResponse201Data, response will be omitted» and lost the register response, which is the only place the sk_test_ secret is ever returned. Envelope no longer declares data, and each operation declares it exactly once.
SendJob, StatementImportJob, RcvEntry, RcvEntryList and AutomationSettings are back. Two causes. One: allOf over a scalar $ref — the way a Clp or an EstadoContable got a description — which a generator reads as «I cannot intersect something that is not an object» and answers by deleting the schema and everything that returns it. In OpenAPI 3.1 a $ref may carry its description BESIDE it, and that is what it does now. Two: const: document_send on top of Job.type's enum, the same restriction written in a form that does not merge; it is enum: [document_send] now. With them come back PATCH /v1/empresas/\{empresa_id\}/automation_settings and POST /v1/test_helpers/empresas/\{empresa_id\}/onboarding/steps, which were not generated at all.
openapi-generator-cli -g python finishes on both documents. It aborted outright on two properties typed the bare "null" (data on the /account/* error, folio on a document preview). They are still always null — their description says so — and are now declared [object, "null"] and [integer, "null"], which the tool knows how to read.
And the contract cannot break this way again. test/contract-shapes.test.ts walks both published documents and fails on any of the four shapes: a property typed the bare "null", an allOf over a non-object schema, a const narrowing an inherited enum, or an allOf member redeclaring a base property incompatibly. The same rules run against a fixture document carrying all four, so the scan cannot pass by looking at nothing.
The one caveat left, said on the page. The four binary artifacts (/documents/\{id\}/pdf and /xml, and their twins under /received_documents) answer application/pdf and application/xml, and openapi-python-client only types JSON bodies: the method is generated all the same and the bytes arrive in response.content. We leave it alone in the document because the declared Content-Type is the one the server really sends.
2026-09-07 — The catalog publishes the declaración de cumplimiento as ours, and the step table can no longer contradict it
cumplimiento is actor: facturia. We file it at the SII with the representante legal's certificate held in custody — the one Res. Ex. SII N° 81 de 2005 CUARTO d) asks for — after that person approves the act cumplimiento_declaracion on the hosted page. stall_actor is customer, because what can hold the step for ever is that approval: the declaración's date fixes the month from which the Información Electrónica de Compras y Ventas obligation runs. The step's help says exactly that. The approval, the link and the pending_action.approval block are unchanged: they still reach the representante legal.
The step table in From sandbox to production is corrected and gains a column. habilitacion still said customer («the representante legal enables them at the SII») after it became ours; it now says facturia, with the same sentence the catalog carries. And both tables — mercado and SFG — publish execution, the field that separates a queued job (automatic) from a person of ours at a keyboard (operator).
actor stops being «the only field worth branching on». Three mercado steps are ours and still cannot finish without a person: sii_postulacion, cumplimiento and habilitacion. What says a decision is pending is pending_action.approval, not actor; the contract and the description of POST /v1/empresas/\{empresa_id\}/onboarding/links now say so, and a link is minted for actor: customer or stall_actor: customer.
The docs check diffs the catalog against the table, row by row. Position, actor and execution, in Spanish and in English, against ONBOARDING_STEPS, SFG_ONBOARDING_STEPS and STEP_EXECUTION. A row that moves in the code without moving on the page no longer builds the documentation.
2026-09-07 — A running step says what the last SII read saw and when we look again; uploading the certificate can no longer end with no probe
steps[].next_poll_at (new, on every step). An RFC 3339 instant — or null — for the next SII read on that step. A schedule, not a promise: it is when the probe's own cadence falls due, and the reconciler notices within about five minutes. The panel prints it on the row: «Volvemos a mirar a las 16:47».
blocked_reason on an in_progress step too. The field said «present on a failed or action_required step»; a running step now publishes the last probe outcome while that probe did not settle it. Three new dictionary codes: sii_unreachable (neither the session nor the consulta page answered), sii_propagation (the SII shows the grant and has not applied it yet — the ~1 business day lag, and it must NOT be granted again) and requirement_unmet (its user table does not yet grant the empresa's sender what it needs; details.key names the permiso). All three carry a Spanish hint and retriable: true, none blocks the empresa, and none asks anything of a person.
Uploading the certificate always triggers its verification. Opening the hosted certificate page enqueued the two probes keyed \{job\}:\{connection\}:\{5-min window\}; the upload seconds later took the same key and created nothing, so the certificate just stored was read by nobody and the step sat «in progress» with a four-day estimate. The upload now enqueues with a key that names the credential, and the five-minute sweep rescues any empresa with a certificate in the vault and no verdict on record — at most one open probe per empresa, so no SII session budget is spent.
2026-09-07 — The intercambio and the printed samples run on the queue, and you approve the samples before they go
The intercambio runs on the queue, end to end. OnboardingIntercambioExecutor downloads the Set de Intercambio, builds and signs the three responses (RecepcionEnvio, EnvioRecibos, ResultadoDTE) and uploads them, in one session and one handshake. The invariant that makes the paso meaningful stays where it was: the responses must answer the set the SII holds NOW, and every download mints a new one, so uploading responses to a superseded set is refused. A paso already accepted is never re-run. The instructions that asked a person to upload three files by hand are retired.
The printed samples file themselves, but only after somebody has looked at them. The engine assembles the kit — every document of the set de pruebas and one sample per tipo of the simulación — the hosted page lists them one by one with a link to open each, and the button reads «Aprobar y enviar al SII». Only then do we upload the PDFs and press «Enviar al SII». The act is muestras_revision, role titular_cuenta, recorded in sii_onboarding_step_approvals. A «not yet» is recorded too, and sends nothing.
A //OK from the SII means FILED, not approved. The revisión reads POR REVISAR and the SII resolves it on its own clock; the step stays in progress until it answers, and onboarding_verdict_poll reads that answer on its own. A revisión that already reads POR REVISAR is not filed again: it would spend a review round on a batch already under review.
Two new environment fences, closed in every build. ALLOW_LIVE_INTERCAMBIO_UPLOAD and ALLOW_LIVE_MUESTRAS_UPLOAD are read from the worker's environment instead of being source constants. With the fence shut the step opens no socket and says so in those words, instead of answering «no set has been ingested» about a set it was never allowed to fetch.
2026-09-07 — execution stops being hand-written: every step says how it runs TODAY, and the ETA says what a sitting costs
execution is derived from the step's fence. Five mercado steps reach the SII only with their fence open: the postulación, the intercambio, the printed samples, the Declaración de Cumplimiento and the boleta habilitación. Until yesterday the catalog hardcoded operator for all five, with a comment saying the fences were shut «everywhere this repository runs» — true when written and false from the day all five started reading the environment. The catalog now pairs each step with the NAME of its variable (STEP_FENCE_ENV_VARS) and stepExecution() reads it at serialize time, so both ends read exactly the same fact.
It is not a constant of the product: read it off the wire. The same step can answer operator on one deployment and automatic on another, and can change from one day to the next. Branch on the value you were given; never cache it as a fact about a step key.
The step's help tells the story that is actually true. The catalog carries both sentences and the serializer picks one: with the fence shut, «lo corre una persona de Facturia y te avisamos por correo y por evento cuando quede»; with it open, «lo hace el motor solo». The wire still carries ONE help string. The sentence «esta parte todavía no va sola» is retired from every surface — it was a permanent claim about the product, and today it changes with an environment variable.
The ETA says what a sitting costs. eta_business_days for a step with execution: operator never drops below 2 business days, and estimated_ready_at sums those projected numbers. The catalog's figures were decomposed for a queue — 0 means «the SII answers this envelope in minutes» — and none of them described a person of ours finding an hour. intercambio published 0 days with nothing scheduled at all. When the fence opens, the ETA goes back to the base number.
The printed-samples page can be minted now. muestras_approval_required joins CUSTOMER_REMEDY_REASON_CODES, the one piece that was missing: without it POST /v1/empresas/\{empresa_id\}/onboarding/links refused the muestras_impresas step, and the person who has to LOOK at the kit had nowhere to go.
Where it is configured. The five variables (ALLOW_LIVE_POSTULACION_WRITES, ALLOW_LIVE_INTERCAMBIO_UPLOAD, ALLOW_LIVE_MUESTRAS_UPLOAD, ALLOW_LIVE_CUMPLIMIENTO_WRITES, ALLOW_LIVE_BOLETA_ENROLMENT) belong on facturia-worker AND on facturia-api with the same value: the worker reads them to act, the API to publish. docs/deploy-fly.md carries the table and the fly secrets set line for both apps.
2026-09-07 — Onboarding estimates stop being a decomposition of a marketing sentence and start being measurements
The numbers are observations. The SII answers a set de pruebas in minutes (an SOK verdict within minutes of the declaración), a boleta review request in 26 seconds, and a Declaración de Cumplimiento with a mail one minute fifty-two later. A whole boleta habilitación — set, folios, envelopes, request, V°B° and declaración — ran end to end in 33 minutes. So set_de_pruebas, cumplimiento and habilitacion go from 1 business day to 0.
One went up, and it is the honest one. resolucion goes from 0 to 1: it is the only step that is an act of the SII's on its own calendar with nothing of ours in flight, and it has never arrived the same day. A 0 there read as «the resolución comes this afternoon».
What is left in the mercado lane's sum is three waits on the SII: the postulación being registered (read on a later poll, not in the submit's response), the printed-samples revisión, and the resolución. Total: 3 business days, which is why the surfaces say «around 2 to 3 business days».
The Sistema de Facturación Gratuito lane sums to 0. credential is an upload on a page of ours, usuario_autorizado is one read of the Portal MIPYME's list, and puesta_en_vivo is a predicate over facts we already hold — not the accompanied test emission its help promised and the engine never performed. You are emitting the same day.
«Avanzar Siguiente Paso» becomes a queued act. The POST to pe_avance4 that moves a postulación out of a certification paso sat behind a hand-written constant, so no automatic job could ever make it: the queue read the paso and a person pressed the button in a browser, while set_de_pruebas and simulacion published as automatic. The fence is now the ALLOW_LIVE_STAGE_ADVANCE variable and the lock register names the two steps it holds, so both steps' execution, help and estimate derive from it the way the other five do. The engine polls first and clicks only if the portal is waiting on it with every set conforme.
What to do about it. If you stored estimated_ready_at or eta_business_days as a constant, read them again: they came down. They are still p50s rather than promises, and whatever the SII takes beyond them is not something the catalog can put a number on.
2026-09-06 — The declaración de cumplimiento is approved on a page and filed by Facturia; cesión opens to mercado empresas; retries belong to the machine
The Declaración de Cumplimiento is an approval, not an errand at the SII (ADR-0013). steps[].pending_action.approval for cumplimiento carries the act cumplimiento_declaracion, role rep_legal: the representante legal reads the consequences — the empresa goes into production and the Información Electrónica de Compras y Ventas obligation runs from that month — and approves on the page; Facturia files the declaración with the custodied certificate, which is the representante's own and the one Res. Ex. SII 81/2005 requires. The same click records the month authorization (go_live_authorized_by = rep_legal_approval). Three code gates: the approval, certificate holder = representante legal per the SII itself, and the env-driven ALLOW_LIVE_CUMPLIMIENTO_WRITES fence with a mandato-version requirement for any empresa that is not a house one. New error code cumplimiento_approval_required.
A hand-placed empresa is offered the decisions its steps wait on. With an emitting spine and reason operator_grant, a step parked on action_required is no longer projected as completed: pending_action and its approval block reach the API, the panel and the MCP, and get_pending_human_action reads pending_action before saying nobody is waiting.
At the first production CAF the empresa switches channel by itself. An empresa certifying for mercado while emitting on the Portal MIPYME moves to emission_channel: mercado when it receives its first folios; between the resolución and that moment new emissions wait in the queue with reason channel_switch_pending.
POST /v1/documents/\{id\}/cede accepts mercado empresas. The channel guard and the cesion_channel_unsupported code are retired; the 409 for a previous attempt reports the Registro's last answer (rtc_estado, TrackID). The ALLOW_LIVE_CESION_SUBMITS fence is read from the environment and the submit is authorized per empresa (emitting, active, production).
What stays the client's says so for the real reason. The «Enviar Doctos.» permissions, the free portal's authorised user and the intercambio mailbox are set in the session of the account's usuario administrador at the SII, an authority the custodied certificate lacks exactly where the act is missing; the copy no longer speaks of «a legal decision of ours».
Dead-lettered jobs requeue themselves, and the mail says so. After an SII outage they retry in an hour; after an error of ours, once per deploy; at most three times per job. Operations alerts describe what the machine does and ask a person only for irreversible decisions.
2026-09-05 — A root key that finally inherits its reach, a sandbox that reserves no RUT, and an approval with a way back
A root key minted from a session finally does something. (#449) POST /dashboard/api_keys \{root:true\} used to return a root with empresas:[], and any key minted with it ended up with that same empty reach or a 403 privilege_escalation when asking for empresas:["*"] — a dead end documented as the main path for an integrator to stop holding a human's password. The root now inherits the session's real reach, and minting with it works.
A sandbox empresa reserves nothing. (#450) A test empresa's RUT no longer counts against the global uniqueness that does apply to a live empresa — the same account can hold the same RUT once in sandbox and once live — and DELETE /v1/empresas/\{empresa_id\} exists for sandbox ones (a live one answers 409 empresa_delete_not_allowed, naming the mandato revocation instead). The reception alias <rut>@dte.facturia.cl is never assigned to a sandbox empresa.
The subscription is charged after eligibility, never before. (#475, #530) capabilities.blocked_by may now say subscription, but only once the empresa could otherwise emit; the go-live authorization is free on every door, because it is the titular's decision, not an emission.
A revoked mandato is read off the real wire, and leaves a way back. (#515, #465) The panel used to read its own version of the revocation instead of the empresa's real state; the two now agree, and an empresa with a revoked mandato has a way to re-activate instead of being stranded.
Every human act has a generic hosted door, with its own record. A pending_action may now carry an approval block — who decides, what it costs, the two buttons — that calls POST /onboarding/steps/:step/approve; a row lands in sii_onboarding_step_approvals (migration 127) naming who decided what, when, and through which channel.
Boleta habilitación runs as Facturia, not a person clicking through it. (#452, #538, #543, #548) The whole sequence — set, CAF, round, review request, verdict, declaración — runs as a machine with exactly two human moments: the titular authorizes the 24-hour clock's start on a hosted page, and the declaración is a page for the representante legal with the form pre-filled.
The SII postulación is filed by Facturia, after the titular approves it. (#453) sii_postulacion becomes actor: facturia, always preceded by a titular approval on a hosted page that shows the consequence of losing the free facturador and lets them choose the moment; the production switch (ALLOW_LIVE_POSTULACION_WRITES) still defaults to false.
MCP tools read the attested contract, and tools/list shows only what the grant reaches. (#454-456, #550) tools/list used to offer the whole catalog regardless of the connection's real reach; a client now sees exactly the tools its scopes permit. The consent that authorizes that connection is in Spanish, with a label and a one-line explanation per permission, and a "Recomendado para emitir con tu agente" preset that checks nothing by default.
/aprobar speaks Spanish, names who asked, and the agent stops guessing. (#470, #471) The page used to show the summary in English and the tool's raw name (emit_document); it now drafts the summary in Spanish once and shows which OAuth client asked. On the other side, an agent can call get_pending_confirmation to learn whether the person has decided, instead of the only way to find out being a blind repeat of the original call.
Recovering a password no longer needs an e-mail to us. (#524) POST /account/password/reset_request sends a single-use link, valid 60 minutes, without confirming or denying whether the account exists; /recuperar and /restablecer in the panel complete the flow.
2026-09-04 — CONNECT is enabled for boleta electrónica, certified from code in 33 minutes
The test set ran and was approved from code. Sending set 0257510898 reached EPR with all five boletas; RCOF 0257510910 confirmed conforming receipt.
The portal's review request is an automatic act, not a two-week wait. certBolElectDteInternet — the procedure an August e-mail had left waiting 22 days in silence — turned out to be a portal action the SII resolves in seconds: the V°B° arrived in 26s, and set 4911005 landed SOK.
The declaración de cumplimiento closed the certification. Filed at 20:35:54; the SII's confirmation e-mail arrived at 20:37:46 — "habilitados los documentos de boleta electrónica a contar del 01-09-2026".
The whole sitting ran with no person in the loop. 33 minutes end to end, zero manual e-mails, zero clicks — the same discipline that already governed factura electrónica, now extended to the boleta.
2026-09-03 — Agent-first: an approval is a page, registration is one call, and the CAF stops promising what it does not keep
confirm_action retires; a human approval is always a page, never the agent's word (ADR-0012). An act that used to be confirmed with a boolean in the same call now parks a pending_confirmation and hands over a hosted /aprobar/\{id\} page: the person decides there, and no tool can claim on its own that they said yes.
Registration is one call. POST /account/register accepts bootstrap: \{sandbox_key, empresa\} and the same 201 carries the account, the session, the first sk_test_ key and a sandbox empresa ready to emit — the three calls it used to take are now one.
puesta_en_vivo runs on a single eligibility predicate. Before this, two different surfaces could ask "can it emit live?" and answer differently; now there is one function both of them consult.
Bilingual documentation, end to end. Every page on docs.facturia.cl has its twin under /en, with code-block and table parity checked on every build — not just the surrounding prose.
The CAF states its own vigencia. (#191) Res. Ex. SII N° 58/2017 sets 6 calendar months from the Fecha de Autorización; that date is now persisted and the vigencia is derived from it or absent — never invented — and an expired folio is refused at reception exactly as the SII would refuse it.
A cesión is assembled and registered natively. (#60) The AEC (Anexo Cesión de Créditos) is assembled with its three signatures — the cedente's, the cesionario's, and ours — and uploaded to the SII's Registro (RTC) via POST /v1/documents/\{document_id\}/cede; a cesión's status stops depending on a query that never said "cedido".
Printed samples speak the SII's protocol in raw HTTP. (#58) The upload (pdfdteInternet) and the review that follows it resolve in one handshake, with no browser session and no person in the loop.
2026-09-02 — The error envelope now carries what to do, and onboarding carries who to tell
The error envelope now carries hint and retriable. Both come out of the same dictionary that serves GET /v1/error_codes, looked up by code and never hand-written at the throw site, so the response and the dictionary cannot contradict each other. An agent no longer needs a second 75 KB call to know what to do with a code: the response it already has in hand carries it. They are only missing if the code is newer than your client — treat that as "read message and doc_url", never as a bug. hint is in English on purpose — the Spanish for each code lives on this site's own code page and in the dashboard.
Onboarding publishes the full handoff. GET /v1/empresas/\{empresa_id\}/onboarding carries pending_action on every step that needs a human, and hoists it to the top level for the step blocked_on names: exactly who has to act and why it cannot be delegated, what to have on hand, ordered instructions, the SII page when we can name one that does not 404, what sets the act in motion, and how we are going to find out it happened. That last field also publishes its own limit: verification.method: no_producer means nothing in this version observes that step, and it says so rather than hiding it. The same object also travels inside error.details.pending_action when a write is refused for a pending step.
New: POST /v1/empresas/\{empresa_id\}/onboarding/steps/\{step\}/recheck. The other half of the handoff: the person finished, and this asks us to go look. It accepts a claim and produces no fact — it takes no body, writes no step's state, and returns the state exactly as it was before anything was queued. verified comes back in the response and is always false. No API call can mark a human step done, this one included.
In the MCP: get_pending_human_action and get_folio_status. The first answers "what do I tell my human?" with the same pending_action as above. The second lets you see the wall before you hit it: how many folios are left per document type and what happens when they run out. On top of that, every tool in the catalog now carries annotations derived from what its handler actually does, not from its name — and two of them run against what the name suggests: get_document_file and get_f29_propuesta are not read-only, because they mint an unauthenticated public link to a signed tax document. A client that auto-approves reads would be handing out invoice links without asking anyone.
The SII now tells us which IVA shape your empresa is in, and we now store it. The empresa's new iva_shape reports whether the SII classified it as afecta or exenta, with what evidence and which test set applies to it. It is read-only: it is the SII's own answer about your empresa, not a setting. null means we have not established it yet, and afecta is never assumed — assuming it would build eleven documents the SII will never review.
/v1/cafs exists. The contract declared GET, POST and DELETE /v1/cafs, and production answered 404 unknown_endpoint to all three: the code had merged and the deploy had not. From now on no path reaches the published OpenAPI without having been probed against the real router — if the contract declared an endpoint the API does not mount, this site's build fails before publishing it, and the other way round too.
2026-08-24 — One plan, and a 402 at the door
One plan: $19,990 + VAT a month, per empresa, unlimited documents. The subscription belongs to the EMPRESA, not the account, so every action is decided against the subscription of the RUT the request names: one expired empresa never stops another one on the same account.
Exactly five actions are blocked, and they are written in one place: POST /v1/documents, POST /v1/documents/\{document_id\}/credit_note, POST /v1/documents/\{document_id\}/send, POST /v1/folios/requests and POST /v1/empresas/\{empresa_id\}/onboarding/go_live. Everything else stays open, and the list of what is NOT blocked is as explicit as the list of what is: every read, all of test mode, forever, the commercial response to a received document (the 8-day window is legal and we are not going to shorten it over an invoice of ours), and tax filings (same argument, with bigger consequences).
The 402 comes with what to fix it. error.details carries the empresa, its RUT, its name, the account's plan, the subscription's status, the grace instant if one was running, and a checkout_url that points at that empresa. An agent that gets a 402 does not have to improvise where to send its human to pay. A subscription that lapses keeps issuing through a 7-day grace period before any of this kicks in.
A note for anyone who already coded against the error-type table. 402 is not a tenth family: it is the one case where an invalid_request_error carries a status other than 400, because "you owe us money" is not a malformed request. plan_limit_reached stays published and nothing emits it — the plan has no quota to run out — and its page says so.
2026-08-21 — dispatch.dispatch_type is now the SII's TipoDespacho
dispatch.dispatch_type changes meaning — honestly, because it never did anything. The published vocabulary ("whose vehicle it is") did not match the SII XML's <TipoDespacho> element, so both channels rejected it. As of today the field is optional, guía-only (type 52), and takes exactly the SII's three values: receiver_account (1, on the receiver's account), issuer_to_customer_site (2, on the issuer's account to the customer's premises), issuer_to_other_site (3, on the issuer's account to other premises). The old names answer 400 invalid_field naming the replacement.
SII channel (the free issuer): still rejected. The portal's own form has no such field — we verified it against the capture — so sending it on that channel answers 422 dispatch_not_available_for_channel. On any type other than 52 it answers 422 dispatch_not_applicable, the same as transfer_type.
2026-08-21 — A zero-amount guía no longer goes through the SII channel
What happened. We issued the first real guía de despacho over the SII channel (the free issuer) with a $0 line and an internal transfer: a movement that generates no output VAT and that, until yesterday, both our documentation and the engine treated as a legitimate document. The portal rejected it, in these words: "Precio del primer item del detalle debe ser mayor a 0 En fila 1 no se cumple validacion [El precio del detalle está en blanco y debe ser mayor a 0] Valor Total Factura debe igual a la suma del Monto neto + IVA y mayor que 0." A retry with a price of 1,000 was accepted (folio 1, net 1,000 / IVA 190 / total 1,190). The portal validates its guía form with the invoice's own validations — the guía form is the invoice form under a different code — so the rule does not look at the transfer class.
What changes. In an empresa with emission_channel=sfg, a guía (type 52) with any unit_price line at or below 0, or whose total is 0, now answers 422 sfg_guia_zero_amount naming the line. It is rejected at the door, before the document is created and before anything is queued, and the message quotes the portal so it does not read as a limit of ours.
Nothing changes on the mercado channel. The same $0 document was accepted by the SII the same day through our own engine (guía t52 folio 1, internal transfer, accepted). The SII's own XSD and validator carry no such rule: it belongs to the portal's form. Both channels still issue a guía; now it is documented where they also differ on amounts.
While we were in there: SII-channel empresa drafts no longer warn no_folios_available. On that channel folios are assigned by the SII at issuance and POST /documents/previews has no counter to consult, so the warning fired on every draft and pointed at a management screen that channel refuses. folio_preview is still null, and it still means the same thing.
2026-08-19 — Cedibility you can query, a guía on both channels, and public links only when minted
New: cedibility lookup (Ley 19.983). POST /documents/\{document_id\}/cesion queues a read-only query against the SII's registry and the result lands in the document's new cesion object: \{cedible, sii_status_code, checked_at\} (null until the first query). It applies to the types the SII's own service supports (33, 34 and 43); on any other type it answers 422 cesion_not_applicable. The endpoint only asks for documents:read — it is a question, not an action — and in test mode it simulates the 8-calendar-day rule against the empresa's own clock. Facturia does not execute cesiones: to actually cede one, the channel is still the SII's own Registro (RPETC).
The guía de despacho (type 52) now issues over both channels. Until now the mercado channel rejected it at parse time (422 tipo_not_available_for_channel); now our own engine builds it with the same dispatch contract already published for the SII channel: transfer_type required (the SII's 9 transfer types), carrier, requester_rut. Transfers that are a sale (types 1 and 9) carry invoice totals; the rest may go out at zero amount. While we were in there, RUTs inside dispatch are now normalised on request acceptance — a RUT with dots used to be accepted at the door and die after spending the folio — and dispatch.destination is now validated as an object instead of silently truncated.
Retired: pdf_public_url and xml_public_url on the document object. They never worked: the link's secret is stored hashed and those fields were built without it, so they had always answered 404. Same rule as format=80mm: we retire what was published and did not exist rather than build it new behaviour on the spot. The real contract: the URL carrying ?k= is handed out exactly once — in the response of POST /documents/\{document_id\}/public_links, or in the public_links object of the issuance response when you asked for pdf: \{"public": true\} / xml: \{"public": true\}. Save it when you get it. document.created webhooks no longer carry secret-bearing URLs at all.
2026-08-16 — The cedible copy, on by default, and shareable
pdf.cedible at issuance is now stored and used. It used to be read off the request body and go nowhere. It now sets THIS document's default for GET /documents/\{document_id\}/pdf when the read does not send ?copy=: true makes it cedible by default, false pins it to the original explicitly. true on a type other than 33/34 is 422 cedible_not_available, rejected before a folio is spent, not silently ignored.
branding.cedible_by_default is now enforced. It is the EMPRESA's own default for when a document does not set its own. The full precedence for GET …/pdf with no ?copy=: the read's own explicit ?copy=, if any; otherwise the document's pdf.cedible; otherwise the empresa's cedible_by_default; otherwise original. Fixed along the way: it applies to invoice types 33 and 34, not only 33.
branding.default_pdf_format is now validated. PATCH /empresas/\{empresa_id\}/branding used to accept any value unchecked, 80mm included — the same value retired from ?format= in the previous wave. Any value other than carta is now 400 invalid_field.
Public links can now name cedible. POST /documents/\{document_id\}/public_links accepts cedible inside artifacts — a real, separate artifact, with its own cedible_public_url, not the pdf link under another name. It is not in the default set: asking for it requires naming it explicitly, and it only exists for invoices (422 cedible_not_available on any other type).
2026-08-15 — What was published and did not exist
Retired: format (carta | 80mm) on the PDF. No handler ever read it. ?format=80mm returned the same letter-size PDF as everything else, so a POS integration wired to it printed a full letter page on a thermal roll with nothing in the response saying so. No thermal layout exists, and we are not building one. Supplying the parameter — on the query string or as pdf: \{ "format": … \} at issuance — now answers 400 invalid_field.
Retired: copy=both. The SII's own Manual de Muestras Impresas allows exactly one page per file (p.12), so the two copies are two files and one HTTP response carries one. both never named anything that could exist. copy accepts original and cedible; any other value is 400.
copy=cedible now actually picks something. There used to be a single file per document, and for an invoice that file already had the acknowledgement-of-receipt box and the CEDIBLE legend drawn on top of it — so copy=original returned a cedible copy and the published parameter selected nothing. Both copies are now rendered and stored when the document is accepted. Documents rendered before today still have a single file: copy=cedible is served correctly off it, and copy=original returns those same bytes while a re-render is queued.
pdf_public_url and xml_public_url on reads are now null, and that is the fix. They used to return a URL built from the slug alone, without the ?k= secret /d/\{slug\} requires — meaning every one of them had always answered 404. The secret is stored hashed, the same as an API key, so we cannot reconstruct it: the working URL appears in the response that mints it (issuance with public: true, or POST …/public_links) and nowhere else. null is a value you can branch on; a broken URL is not.
GET /v1/mandato_terms exists. §7.3 has said since the first draft to present the mandato's text before posting acceptance, and the endpoint answered 404. It returns the text, its version, its SHA-256 hash and a status that today says draft: the contract's legal review is not finished, and saying so beats serving text that reads as binding when it is not. Unauthenticated, like the error dictionary.
expand on list endpoints is no longer ignored. GET /v1/documents?expand=… accepts events, references and received_document, and expands the whole page with one extra query, not one per row. xml and pdf_base64 are deliberately left out and answer 400: they would pull a whole artifact into every row. GET /v1/received_documents accepts no expansion at all, for the same reason.
expand=received_document is no longer always empty. An issued document links to a received one when it references it by tipo_dte and folio and the received document's issuer RUT is the issued document's receiver RUT, inside the same empresa, and only when that match is unique. If there are two candidates the link stays null: a wrong match is a claim, and an absence is not one.
The general rate limit is 300 req/min, as the table always said. The limiter was counting up to 100. The documentation was the correct one.
Rotating and revoking a key now respect the session's mode. A dashboard session in test mode used to see only its test keys when listing, yet could still rotate or revoke a live key by id. All three operations now apply the same filter.
CORS now exposes the headers the documentation promises. Facturia-Request-Id, the RateLimit-* headers, Retry-After, Facturia-Idempotent-Replay and Facturia-Poll-After had always been sent, and the browser was discarding them.
2026-08-14 — v1 — the published surface
Issuance. Eight DTE types (33, 34, 39, 41, 46, 52, 56, 61). POST answers 201 with status: queued and folio: null; the engine assigns the folio when the document moves to sent, and the trip to the SII is an observable state machine (document.sent, Facturia-Poll-After). POST /v1/documents/previews quotes and validates without issuing; preview_id issues what was approved, and issuing the same preview twice returns the original document, never a second folio.
Artifacts. PDF and signed XML, either by key or by a signed public link that expires in days or seconds. The XML is served as ISO-8859-1, byte for byte identical to what the SII received, on both the authenticated route and the public link.
The cedible copy, for invoice types 33 and 34. Ley 19.983 grants the third copy to the invoice without distinguishing taxed from exempt, and the SII's own Manual de Muestras Impresas requires it for both. copy=cedible on any other type answers 422 cedible_not_available.
Reception. Your suppliers' DTEs, the commercial response (ACD, RCD, RFP, RFT, ERM) and the 8-calendar-day window anchored on the SII's own reception mark, on Santiago's calendar. No anchor, no deadline: 422 commercial_response_anchor_pending instead of a made-up date.
Tax. Purchases and sales RCV, individual and bulk characterisation, an F29 proposal with an explanation and per-code traceability, the SII's own upload file, and filing as a job with honest states. Code 048 is declared as an external input, and every response says so whenever it is missing.
Webhooks. HMAC signatures over the raw bytes, at-least-once delivery, published retries, and an event log you can query and replay for 12 months.
Sandbox. Free forever, with not a single socket toward the SII: virtual folios, forceable outcomes, and the same rate limits as production, so a load test actually means something.