Facturiadocs

Changelog

Qué cambió en la API, con fecha. `/v1` evoluciona de forma aditiva; los retiros llevan 12 meses de aviso.

/v1 evoluciona de forma aditiva: endpoints nuevos, campos opcionales nuevos, campos de respuesta nuevos, valores de enum nuevos, tipos de evento nuevos. Tu cliente debe ignorar lo que no reconoce. Un retiro o un cambio de comportamiento lleva 12 meses de aviso, un header Sunset en los endpoints afectados y una entrada acá (§5.8).

Hay un feed RSS en https://docs.facturia.cl/feed.xml. Suscribirse es parte de una buena integración.

2026-10-06 — GET /v1/contribuyentes/\{rut\}: la razón social, las actividades y un giro que cabe en la factura, desde la nómina pública del SII

De dónde sale. De la nómina pública de personas jurídicas del SII (https://www.sii.cl/sobre_el_sii/nominapersonasjuridicas.html), sin captcha ni clave. Facturia la recarga cuando el SII publica una edición nueva, y source.published_at dice cuál respondió. Las personas naturales no están en ella: su RUT responde 404, igual que un RUT que no existe.

El giro sugerido. Las descripciones oficiales casi nunca caben en los 40 caracteres de GiroRecep (la de compraventa de vehículos tiene 81), y cortarlas cambiaría lo que la factura dice del receptor. Por eso suggested_giro usa un texto corto revisado para las actividades más comunes (basis: curated), la descripción oficial cuando cabe tal cual (sii_description), o nada.

Cómo usarlo. Muéstraselo a la persona y, una vez confirmado, guárdalo con PATCH /v1/empresas/\{id\}/counterparties/\{rut\}: eso es lo que lee la próxima emisión. No requiere Facturia-Empresa; pide empresas:read y tiene un límite de 120 consultas por minuto por cuenta.

Código de error nuevo. 503 contribuyentes_unavailable mientras la nómina no se ha cargado; reintenta después de Retry-After.

2026-09-23 — Una persona natural como receptor de boletas: el nombre llega al documento

Qué pasó. POST /v1/documents guarda el receptor en la forma pública (legal_name, address) y el constructor de boletas lee otra. Los carriles de factura y del portal SFG traducían entre las dos; el de boletas no, así que una boleta a «María José Pérez Soto» salía con su RUT en <RUTRecep> y Sin RUT en <RznSocRecep> y en el timbre. El SII la acepta —es un documento válido— pero no nombra a la persona, y un reembolso de Isapre o de seguro complementario lee justamente el nombre.

Qué cambia. El nombre y la dirección que mandas llegan a la boleta, y el timbre dice lo mismo que el documento. La boleta anónima no cambia: sin receptor, 66666666-6 y «Sin RUT». No hay campo nuevo ni bandera de empresa: una persona natural es un Party como cualquier otro.

Y la nota de crédito. Una 61 a esa misma persona es un documento de la familia de la factura y lleva giro, address.street y address.comuna, como toda 61, aunque anule una boleta. POST …/credit_note copia el receptor de la boleta original, que no los trae: guárdalos una vez en la libreta de direcciones (PATCH /v1/empresas/\{id\}/counterparties/\{rut\}) antes de anular, o la nota se rechaza con 400 receptor_incomplete antes de gastar un folio.

2026-09-23 — «¿Ya emites con otro proveedor?»: la resolución declarada se verifica cuando un operador lo confirma, y resolucion.verification_status dice dónde va

Qué pasó. Una empresa que ya factura con otro proveedor declara su resolución (resolucion: \{numero, fecha\}) y la verificamos pidiéndole al SII un rango de folios de un tipo de factura declarado: el SII no timbra para un RUT que no autorizó. Esa petición salía con el ambiente de la propia empresa, que nace en certificación, y desde el 2026-09-08 un timbraje de emisión en certificación se bloquea antes de abrir una conexión. El bloqueo local se leía como el rechazo del SII, así que ninguna resolución declarada podía verificarse, y la empresa caía a la certificación completa sin que al SII se le hubiera preguntado nada.

Qué cambia. La verificación se hace contra el SII de producción, que es la única pregunta honesta. Como registrar un rango en producción es irreversible, no ocurre nunca de forma implícita: ni al crear la empresa, ni por el proceso de habilitación, ni en certificación. Un operador de Facturia la confirma, empresa por empresa, con POST /v1/empresas/\{id\}/resolucion/verify y confirm: true. Esa operación se autentica con el secreto del operador; ninguna llave de API la puede llamar, tampoco una llave root.

Qué ves. resolucion gana tres campos. verification_status vale pending_verification (declarada; esperando la confirmación, en curso, o un intento que el SII no contestó sobre el RUT), verified (el SII entregó el rango: se salta la certificación y la empresa pasa a producción) o refused (el SII contestó sin rango). verification_request es la petición exacta: tipo, cantidad y ambiente. refusal trae las palabras del SII cuando rechazó. Volver a declarar la resolución con PATCH devuelve un refused a pending_verification. verified y verified_at no cambian de significado.

Nuevos códigos de error. 400 confirmation_required, 401 operator_authentication_required y 409 resolucion_verification_not_allowed (con el motivo en error.details.reason). Solo los recibe quien llama a la operación del operador.

2026-09-10 — GET /v1/folios cuenta los folios que de verdad tienes: los rangos en cola dejan de ser invisibles

Qué pasó. Desde que el asignador aprendió a ROTAR —cuando next_folio pasa caf_hasta toma el siguiente rango activo que empieza por encima y emite desde ahí— hubo dos verdades distintas en la plataforma: la del emisor, que veía todos los rangos, y la de los lectores, que seguían restando caf_hasta - next_folio + 1 sobre uno solo. Esta puerta era uno de esos lectores, y prometía en su propia cabecera lo contrario: «lee las mismas tablas de las que asigna allocate_dte_folio, así que lo que informa es lo que la próxima emisión va a obtener». La respuesta incluso listaba los dos rangos en ranges[] mientras informaba available: 1.

Qué cambia en el objeto. available es la suma de la CADENA que el asignador va a consumir: el tramo que queda del rango actual, más cada rango activo que empieza por encima del anterior, en orden. Una cadena y no una suma de todo lo que hay: un rango solapado nunca se alcanza, así que nunca se cuenta. next_folio es el folio que la próxima emisión toma, y es null cuando ninguna emisión de ese tipo puede salir ahora — antes informaba el número fuera de rango en el que el contador estaba estacionado. status no cambia de significado, pero se calcula sobre la cuenta correcta: una empresa que ya tenía su reposición deja de aparecer como low.

Y un bloque nuevo, stock. stock.chain trae cada rango que se va a consumir, en orden, con from, to, remaining y expired; stock.stops_at_expired_range dice si el recorrido terminó en un rango vencido en vez de terminar en «no hay más otorgado». No es lo mismo que available == 0: una cadena puede tener folios usables hoy y aun así detenerse detrás en un rango cuya vigencia caducó. Res. Ex. SII N° 58/2017 §1° hace inservible ese rango para los documentos que dan derecho a crédito fiscal, el asignador rota igual hacia él y la emisión lo rechaza — así que contar lo que hay detrás sería prometer folios que ninguna emisión alcanza.

El mismo error costaba rangos. La reposición automática leía la misma resta, así que veía low en una empresa que ya tenía su rango de repuesto — y a low responde pidiendo un timbraje. Un rango sin usar hay que anularlo y cinco bloquean el timbraje de ese tipo de documento, de modo que la aritmética vieja empujaba a la empresa hacia ese bloqueo de a una reposición por vez. La reposición usa ahora la misma cadena, así que un rango que espera cuenta y la petición no se hace.

Sin cambios en la emisión. El asignador ya hacía lo correcto y no se tocó: quien tenía rangos en cola siempre pudo emitir contra ellos. Lo que estaba mal era lo que la API contaba, y por lo tanto lo que un agente o un panel repetía. En el enum de status queda además documentado sfg_managed, que la puerta lleva devolviendo desde que existe el canal SFG y el contrato nunca declaró.

2026-09-09 — La libreta de direcciones: deja el domicilio de un cliente registrado una vez y deja de escribirlo en cada documento

Por qué existe. El 8 de septiembre de 2026 se emitieron siete facturas exentas cuyo receptor traía sólo un RUT. GiroRecep, DirRecep y CmnaRecep son opcionales en el esquema del SII, así que el sobre pasó la validación (EPR, con TrackID) y el SII rechazó cada documento que iba adentro: ACEPTADOS 0 / RECHAZADOS 1, siete veces. Siete folios de producción quemados en silencio. Las dos fuentes que la resolución tenía ese día —el domicilio de la propia empresa, sólo cuando el receptor es el emisor, y los documentos anteriores al mismo RUT— no sirven para la primera factura a un cliente nuevo, que es exactamente el caso que quemó los folios.

Cómo se usa. PATCH /v1/empresas/\{empresa_id\}/counterparties/\{counterparty_rut\} con legal_name, giro y address es un upsert: la primera llamada crea la entrada y las siguientes la editan. Es parcial —un campo que no mandas conserva su valor— y null borra, que es como se saca un dato registrado por error. No hace falta ningún documento previo: registrar el domicilio de un RUT que todavía no facturaste es precisamente el punto. Cada valor se somete al mismo tope del esquema del SII que un valor enviado en un documento (giro 40 caracteres, address.street 70, address.comuna y address.city 20, legal_name 100) y a la misma regla ISO-8859-1, y se rechaza pasado el tope en vez de truncarse: acortar un giro en silencio cambiaría lo que cada documento a esa contraparte dice sobre quién es.

El orden manda, y va de lo más deliberado a lo inferido. Lo que mandes en el documento gana; después el domicilio de tu propia empresa, sólo cuando el receptor eres tú (la guía de traslado interno); después la libreta; y al final un documento anterior al mismo RUT. La libreta le gana al documento anterior a propósito: es donde vas a CORREGIR un domicilio que veníamos poniendo mal, y si ganara el del mes pasado la corrección se ignoraría hasta que alguna emisión llevara el nuevo. GET /v1/empresas/\{empresa_id\}/counterparties/\{counterparty_rut\} responde con lo que efectivamente sabemos y source dice cuál de las dos fuentes fue: api si lo registraste tú, o el documento anterior si lo inferimos nosotros.

Y la lista sigue siendo derivada. GET /v1/empresas/\{empresa_id\}/counterparties lee los receptores de los documentos ya emitidos y les une los contactos y las direcciones que existan; nada copia un receptor a una fila. Ahora cada entrada trae también legal_name, giro, address, source y updated_at, y en esa lista source: null significa «no hay nada registrado», no «no se sabe nada» — la lectura de una sola contraparte es la que recurre a los documentos anteriores.

2026-09-08 — Si tu empresa ya emite con otro proveedor, deja de certificarse de nuevo: declaras la resolución y la comprobamos usándola

El SII certifica al contribuyente, no al software. Una empresa que ya factura electrónicamente con otro proveedor tiene una resolución que el SII le dio a su RUT, y esa resolución no deja de valer porque cambie de proveedor. Hasta hoy no había ninguna superficie donde decirlo — la única puerta que escribía esos dos campos se había borrado por insegura — así que cada empresa que llegaba desde otro proveedor caminaba los once pasos de certificación completos.

Los números están en tus propias facturas. numero y fecha son el NroResol y el FchResol de la carátula de todo documento tributario electrónico, así que se leen de cualquier factura reciente. Nadie tiene que ir al SII a buscarlos.

Declarar no es saltar. El campo escribe dos columnas y no mueve ni un paso. resolucion.verified sale false hasta que lo comprobemos y la empresa sigue su lista completa mientras tanto; ramifica por verified, nunca por que el número esté puesto.

Lo comprobamos usándola. Con tu certificado en custodia, la máquina le pide al portal del SII que timbre un tipo de documento de los que declaraste. El SII rechaza el timbraje de un RUT que no autorizó para ese tipo, así que un rango otorgado es la prueba: verified pasa a true, los pasos de certificación quedan skipped con la nota que dice por qué, y estimated_ready_at es hoy. No hay forma de leerlo en una consulta — ninguna página del SII distingue una resolución de mercado de una inscripción en el sistema gratuito — y por eso lo comprobamos actuando.

Un rechazo nunca es silencioso. Si el SII niega el timbraje, verified se queda en false, se ofrece el carril de certificación completo y blocked_reason.resolucion_claim del paso sii_postulacion lleva la frase textual del SII, el código y el tipo que se le pidió.

Boletas, con su propia fecha. La habilitación de boleta electrónica es otra autorización y no lleva número: el correo del SII dice «habilitados los documentos de boleta electrónica a contar del …». Se declara en resolucion_boletas y se comprueba con un timbraje de 39 o 41; si resulta, las boletas quedan habilitadas sin pasar por la cadena de habilitación.

La casilla de intercambio sigue siendo tuya. El mandato nos prohíbe tocar los datos de contacto de tu empresa ante el SII, así que hasta que cambies la casilla en Mi SII tus proveedores le siguen enviando los documentos a tu proveedor anterior. Tu dirección con nosotros la publica cada empresa en reception_email.

2026-09-08 — Un período que nadie leyó no es un período vacío: el bloque sync habla del mes que pediste, POST /tax/sync fuerza de verdad, el job se cierra, y el MCP tiene sync_rcv

El espejo del RCV no es el SII. Lo que leen GET /v1/tax/sales, GET /v1/tax/purchases, GET /v1/tax/summary y la propuesta de F29 es una copia que llena un barrido diario, y ese barrido lee sólo los últimos tres meses. Una empresa conectada en agosto nunca tuvo febrero leído. Hasta hoy el bloque sync de esas respuestas decía «sincronizado hoy a las 05:25, fresh» para febrero igual — describía la última corrida de la empresa, no el mes pedido — y un agente leyó eso como «febrero está al día y vacío» y le dijo a una persona que su venta no existía. Existía; nadie la había pedido.

sync habla del mes que pediste. Con period, last_synced_at es el cierre de la última sincronización QUE LEYÓ ESE MES, status: syncing es una en curso para ese mes, y null es «nunca leído». Sin period, el bloque sigue describiendo la empresa completa. El vocabulario de estados también quedó alineado con la cola real: degraded se apoya en failed_permanent, que es lo que la cola escribe, en vez de en dos valores que nunca existieron.

POST /v1/tax/sync fuerza de verdad. La fila de cola llevaba la misma clave por día que el barrido nocturno, así que una llamada después de las 05:23 «colapsaba» sobre una corrida ya terminada y devolvía un job que apuntaba a nada. Ahora sólo colapsa sobre una sincronización del mismo período que todavía va a correr o está corriendo; si ya terminó, encola una lectura fresca. period por omisión es el mes anterior en hora de Santiago y siempre viaja en result.period.

El job se cierra. Ningún job rcv_sync había pasado nunca de queued: diecinueve en producción el día que se escribió esto, todos con la sincronización terminada minutos antes. El worker ahora lo mueve a running al empezar, a succeeded con purchases, sales, created, updated, withdrawn_at_sii y partial_errors al terminar, y a failed sólo cuando la cola deja de reintentar.

En el MCP: sync_rcv. Un período, o from/to de hasta diez meses (el bucket del endpoint es diez por minuto por empresa; si se agota a mitad de rango la respuesta dice exactamente qué períodos quedaron sin encolar y cuánto esperar), y job_id para consultar uno. query_rcv y get_periodo_summary ahora anteponen la advertencia cuando el período nunca fue leído, y las instrucciones del servidor llevan la regla 7: un período que nadie leyó no es un período vacío.

2026-09-08 — El MCP hace todo lo que hace la API: 53 herramientas nuevas, folios:write entra al vocabulario, y el RCV se rellena solo doce meses al conectar

Lo que entra. Impuestos: 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, y las conexiones bancarias (list_bank_connections, link_bank_connection, sync_bank_connection, delete_bank_connection). Documentos: send_document, emit_credit_note, retry_document, get_document_events, check_cesion, cede_document, revoke_document_links. Folios y CAF, bajo el scope nuevo folios:write: request_folios, list_cafs, upload_caf, deactivate_caf. Recepción: acknowledge_received_document (el acuse de recibo, que no es aceptar). Cuenta: get_account, get_job (genérico, narra cada tipo de job), list_usage_records, get_event, get_mandato_terms, list_webhook_endpoints, list_webhook_deliveries. Empresa: get_empresa, update_empresa (la puerta para declarar la resolución que ya tienes con otro proveedor y para cambiar el canal), get/update_automation_settings, get/update_branding, contrapartes y sus contactos. Puesta en marcha: 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.

Lo que sigue fuera, y por qué. Los actos de la persona (onboarding:attest, mandato:attest: firmar, aprobar, autorizar la puesta en vivo) no tienen scope en el MCP; el minteo de llaves (keys:write) crearía una segunda autoridad detrás de un grant; las escrituras de webhooks no tienen sentido para un agente sin endpoint propio (las lecturas sí entran); DELETE /empresas no tiene caso de uso. Y una regla propia del MCP: update_automation_settings se niega a apagar require_confirmation — esa bandera existe para que sea la persona, y no el agente, el registro de la aprobación.

Los actos irreversibles que no pasan por la página de aprobación. La puerta de confirmación replaya exactamente cuatro acciones (emit_document, file_f29, emit_tipo46, reclaim_received_document). emit_credit_note no es una de ellas, así que en una empresa con require_confirmation se NIEGA (confirmation_path_missing) en vez de saltarse el control; en las demás exige confirm_emit: true. cede_document, deactivate_caf, delete_certificate, delete_sfg_credential y delete_counterparty_contact exigen un argumento de confirmación explícito y no llaman a la API sin él. Cada una lleva la anotación destructiveHint que corresponde a lo que hace, no a cómo se llama.

folios:write es un scope nuevo. Un grant anterior a hoy no lo tiene: si request_folios responde insufficient_scope nombrándolo, hay que reconectar el agente y volver a consentir. La pantalla de consentimiento lo muestra como consecuente (un timbraje gasta un recurso finito autorizado por el SII) y no lo incluye en el preset de un clic.

El RCV se rellena solo. El barrido diario leía los últimos tres meses y nada más. Ahora, la primera vez que una empresa tiene una sincronización exitosa (o parcial), el barrido encola los doce meses anteriores — una vez, derivado de la cola misma (una fila para un período más antiguo que la ventana, en cualquier estado, significa que la historia ya se pidió), respetando rcv_sync_frequency: manual, en una sola sesión de portal. Las dos empresas vivas ya tenían historia pedida a mano, así que no les toca; la próxima que se conecte la tiene sin que nadie lo pida. sync_rcv sigue siendo la puerta para lo más antiguo.

El historial emitido en el portal gratuito del SII, cuando el portal ya cerró. El import de «Documentos emitidos» (#45) sólo corría al pasar a emitting, y una empresa que llegó ahí a mano nunca lo tuvo. Ahora el barrido diario lo encola una vez para toda empresa con huella SFG y certificado en custodia — también para la que ya migró a mercado. Y cuando el Portal MIPYME ya no muestra a la empresa (es lo que pasa al graduarse al mercado, comprobado hoy en vivo con las dos empresas de la casa), el import no falla ruidosamente: se retira sin alerta y toma la otra fuente del SII, el registro de ventas del RCV, del que trae cada documento emitido con receptor, razón social y el desglose neto/IVA/exento que el portal nunca dio. Si el receptor es a su vez una empresa Facturia, su copia de intercambio del EnvioDTE firmado ya está en nuestra custodia: el import la enlaza, GET /documents/\{id\}/xml sirve los bytes reales e items trae la glosa. Para un receptor externo no hay XML ni PDF que podamos tener: /xml y /pdf responden 409 artifact_not_available, permanente y honesto, y el documento vive en el SII. La afirmación de #46 («la descarga por documento puede reenviar el DTE por correo») sigue sin poder probarse: ya no hay documento de la casa alcanzable en el portal.

Y query_rcv dice todo lo que trae la fila. Antes narraba «neto $0 IVA $0» para una exenta de $13.916.387 y escondía el RUT del receptor detrás de la razón social; un agente salió a buscar en internet un RUT que el SII ya nos había dado. Ahora cada fila lleva fecha, razón social con RUT, neto, IVA, exento y total.

2026-09-07 — Las boletas de una empresa del sistema gratuito ya salen — por nuestro propio motor, con el canal decidido por tipo

El canal es del documento, no de la empresa. Una sola función lo decide en todas partes (channelForTipo): tipos 39 y 41 → el motor propio de Facturia, cualquier otro tipo → el emission_channel de la empresa, como siempre. No hay que cambiarle el canal a nadie para emitir boletas, y hacerlo sería justo lo contrario de lo que conviene: le quitaría a sus facturas el carril gratuito.

Qué cambia en POST /v1/documents. Una boleta de una empresa sfg ya no se rechaza al parsear, y se valida con la gramática del motor propio (descuentos por línea, descuentos globales, referencias) en vez de con las restricciones del formulario del portal. El único tipo que el carril gratuito sigue rechazando con tipo_not_available_for_channel es la factura de compra (46), porque ahí el SII se niega a abrir su propio formulario.

Qué la condiciona, y cómo te enteras. Emitir una boleta necesita la habilitación de boletas del SII y el primer CAF de la 39/41. Mientras no estén, GET /v1/empresas/\{id\} no lista la 39 ni la 41 en capabilities.tipos, y POST /v1/documents responde 422 empresa_not_emitting — el código de siempre — nombrando el paso habilitacion y trayendo el pending_action con las dos aprobaciones. No responde tipo_not_available_for_channel: no es que el carril no pueda, es que el SII todavía no autoriza.

Dos pasos nuevos en el checklist del carril gratuito, y sólo si declaraste boletas. habilitacion y folios entran en SFG_ONBOARDING_STEPS con only_for_tipos: [39, 41], así que una empresa que sólo factura sigue viendo exactamente los siete de antes. Son los mismos dos pasos del carril de mercado — mismo actor, misma evidencia, mismas dos aprobaciones del titular y del representante legal en páginas nuestras—, y van DESPUÉS de emitting: tus facturas no esperan a la habilitación de boletas, salen en cuanto termina la puesta en vivo.

Un dato nuevo por empresa. boleta_emission_enabled_at (migración 155) guarda el «habilitados los documentos de boleta electrónica a contar del …» del SII más el primer CAF. Lo escribe sola la máquina de onboarding, igual que la puesta en vivo, y es lo que leen la puerta de emisión y capabilities — una función, un dato, sin que dos superficies puedan contestar cosas distintas.

2026-09-07 — La receta de Python que publicamos ya genera el cliente entero: vuelven los tipos de /account/* y los cinco esquemas que el generador borraba

Nada cambia en el cable. Mismo JSON, mismos campos, mismos nombres, mismos valores. Lo que cambia es cómo el documento DESCRIBE lo que ya mandaba: es una corrección aditiva bajo la regla de evolución de /v1 (§5.8), y un cliente que ya tengas generado sigue siendo correcto — sólo que al regenerarlo ahora gana los tipos que antes se perdían.

Las cinco respuestas de /account/* vuelven a tener tipo. El sobre declaraba data: [object, "null"] y cada operación volvía a declarar su data tipado encima, por allOf. Son dos declaraciones incompatibles de una misma propiedad: «un objeto que además es null». El generador contestaba «EnvelopeDataType0 | None can't be merged with RegisterAccountResponse201Data, response will be omitted» y se quedaba sin la respuesta del registro, que es el único lugar donde el secreto sk_test_ se devuelve alguna vez. Ahora Envelope no declara data y cada operación lo declara una sola vez.

Vuelven SendJob, StatementImportJob, RcvEntry, RcvEntryList y AutomationSettings. Dos causas. Una: allOf sobre un $ref escalar —el modo de ponerle una descripción a un Clp o a un EstadoContable— que un generador lee como «no puedo intersectar algo que no es un objeto» y responde borrando el esquema y todo lo que lo devuelve. En OpenAPI 3.1 un $ref puede llevar la descripción AL LADO, y eso es lo que hace ahora. Otra: const: document_send encima del enum de Job.type, que es la misma restricción escrita de una forma que no se fusiona; ahora es enum: [document_send]. Con eso vuelven también PATCH /v1/empresas/\{empresa_id\}/automation_settings y POST /v1/test_helpers/empresas/\{empresa_id\}/onboarding/steps, que no se generaban.

openapi-generator-cli -g python termina en los dos documentos. Abortaba entero por dos propiedades declaradas con el tipo "null" a secas (data en el error de /account/*, folio en la vista previa de un documento). Siguen valiendo siempre null —lo dice su descripción— y ahora se declaran [object, "null"] y [integer, "null"], que es lo que la herramienta sabe leer.

Y el contrato ya no puede volver a romperse así. test/contract-shapes.test.ts recorre los dos documentos publicados y falla si aparece cualquiera de las cuatro formas: una propiedad con tipo "null" a secas, un allOf sobre un esquema que no es objeto, un const que estrecha un enum heredado, o un miembro de allOf que redeclara de forma incompatible una propiedad de la base. El chequeo corre también contra un documento de mentira que trae las cuatro, para que no pueda pasar por no mirar nada.

La única salvedad que queda, dicha en la página. Los cuatro artefactos binarios (/documents/\{id\}/pdf y /xml, y sus gemelos en /received_documents) contestan application/pdf y application/xml, y openapi-python-client sólo tipa cuerpos JSON: el método se genera igual y los bytes llegan en response.content. No lo tocamos en el documento porque el Content-Type declarado es el que el servidor manda de verdad.

2026-09-07 — El catálogo publica la declaración de cumplimiento como nuestra, y la tabla de pasos deja de poder contradecirlo

cumplimiento es actor: facturia. El paso lo presentamos nosotros ante el SII con el certificado del representante legal que tenemos en custodia —el que la Res. Ex. SII N° 81 de 2005 CUARTO d) pide— después de que esa persona apruebe el acto cumplimiento_declaracion en la página alojada. stall_actor es customer, porque lo que puede detener el paso para siempre es esa aprobación: la fecha de la declaración fija el mes desde el que corre la Información Electrónica de Compras y Ventas. El help del paso lo dice con esas palabras. La aprobación, el enlace y el bloque pending_action.approval no cambian: siguen llegando al representante legal.

La tabla de pasos de De sandbox a producción se corrige y gana una columna. habilitacion decía customer («el representante legal las habilita en el SII») desde que pasó a ser nuestra; ahora dice facturia, con la misma frase que el catálogo. Y las dos tablas —mercado y SFG— publican execution, el campo que distingue un trabajo en cola (automatic) de una persona nuestra frente a un terminal (operator).

actor deja de ser «el único campo sobre el que ramificar». Tres pasos del carril de mercado son nuestros y aun así no terminan sin una persona: sii_postulacion, cumplimiento y habilitacion. Lo que dice que hay una decisión pendiente es pending_action.approval, no actor; el contrato y la descripción de POST /v1/empresas/\{empresa_id\}/onboarding/links lo dicen ahora así, y el enlace se acuña para actor: customer o stall_actor: customer.

El chequeo de los docs compara el catálogo con la tabla, fila por fila. Posición, actor y execution, en castellano y en inglés, contra ONBOARDING_STEPS, SFG_ONBOARDING_STEPS y STEP_EXECUTION. Una fila que se mueva en el código sin moverse en la página deja de compilar la documentación.

2026-09-07 — Un paso en curso dice qué vio la última consulta al SII y a qué hora volvemos a mirar; subir el certificado ya no se queda sin sonda

steps[].next_poll_at (nuevo, en todos los pasos). Instante RFC 3339 —o null— con la próxima consulta al SII para ese paso. Es un horario, no una promesa: es cuando vence la cadencia de la sonda, y el reconciliador la nota dentro de unos cinco minutos. El panel lo imprime en la fila: «Volvemos a mirar a las 16:47».

blocked_reason también en un paso in_progress. El campo decía «presente en un paso failed o action_required»; ahora un paso que está corriendo publica el resultado de la última sonda cuando esa sonda no lo resolvió. Tres códigos nuevos en el diccionario: sii_unreachable (la sesión o la página de consulta no contestaron), sii_propagation (el SII muestra el permiso otorgado y todavía no lo aplica: el desfase de un día hábil, y NO hay que volver a otorgarlo) y requirement_unmet (su tabla de usuarios todavía no le reconoce al emisor lo que necesita; details.key nombra el permiso). Los tres traen hint en español y retriable: true, ninguno bloquea a la empresa, ninguno pide nada a una persona.

Subir el certificado siempre dispara su verificación. Abrir la página alojada del paso certificate encolaba las dos sondas con la clave \{trabajo\}:\{empresa\}:\{ventana de 5 min\}; la subida, segundos después, tomaba la misma clave y no creaba nada, así que el certificado recién guardado no lo leía nadie y el paso quedaba «en curso» con una estimación de cuatro días. La subida ahora encola con una clave que nombra la credencial, y el barrido de cada cinco minutos rescata cualquier empresa con certificado en bóveda y ningún veredicto registrado —a lo más una sonda abierta por empresa, para no gastar sesiones del SII.

2026-09-07 — El intercambio y las muestras impresas los corre la cola, y las muestras las apruebas tú antes de que salgan

El intercambio lo corre la cola de punta a punta. OnboardingIntercambioExecutor baja el Set de Intercambio, arma y firma las tres respuestas (RecepcionEnvio, EnvioRecibos, ResultadoDTE) y las sube, todo en una sola sesión y un solo handshake. La invariante que hace válido el paso sigue donde estaba: las respuestas tienen que contestar el set que el SII tiene AHORA, y cada descarga acuña uno nuevo, así que subir respuestas a un set superado se rechaza. Un paso ya aprobado no se vuelve a correr. Se retiran las instrucciones que le pedían a una persona subir tres archivos a mano.

Las muestras impresas se envían solas, pero sólo después de que alguien las mire. El motor arma el kit —todos los documentos del set de pruebas y una muestra de cada tipo de la simulación—, la página alojada los lista uno por uno con su enlace para abrirlos, y el botón dice «Aprobar y enviar al SII». Recién ahí subimos los PDF y apretamos «Enviar al SII». El acto es muestras_revision, rol titular_cuenta, y queda registrado en sii_onboarding_step_approvals. Un «no todavía» se registra igual y no envía nada.

Un //OK del SII significa PRESENTADA, no aprobada. La revisión queda POR REVISAR y el SII la resuelve en su propio plazo; el paso sigue en curso hasta que responda, y eso lo lee onboarding_verdict_poll solo. Una revisión que ya está POR REVISAR no se vuelve a presentar: gastaría una ronda de revisión sobre un lote que ya está en revisión.

Dos cercas nuevas de entorno, cerradas en todo despliegue. ALLOW_LIVE_INTERCAMBIO_UPLOAD y ALLOW_LIVE_MUESTRAS_UPLOAD se leen del entorno del worker en vez de ser constantes del código. Con la cerca cerrada el paso no abre ningún socket y lo dice con esas palabras, en vez de contestar «no hay set ingresado» sobre un set que nunca tuvo permiso de bajar.

2026-09-07 — execution deja de estar escrito a mano: cada paso dice cómo corre HOY, y el plazo dice lo que cuesta una sesión de trabajo

execution se deriva de la cerca del paso. Cinco pasos del carril de mercado llegan al SII sólo con su cerca abierta: la postulación, el intercambio, las muestras impresas, la Declaración de Cumplimiento y la habilitación de boletas. Hasta ayer el catálogo tenía escrito operator para los cinco, con un comentario que decía que las cercas estaban cerradas «en todos los builds de este repositorio» — cierto cuando se escribió y falso desde que las cinco pasaron a leerse del entorno. Ahora el catálogo empareja cada paso con el NOMBRE de su variable (STEP_FENCE_ENV_VARS) y stepExecution() la lee al serializar, así que las dos puntas leen exactamente el mismo dato.

No es una constante del producto: léelo del cable. El mismo paso puede contestar operator en un despliegue y automatic en otro, y puede cambiar de un día para otro. Ramifica por el valor que te llegó; no lo guardes como un hecho de la llave del paso.

El help del paso cuenta la historia que corresponde. El catálogo trae las dos frases y el serializador elige una: con la cerca cerrada, «lo corre una persona de Facturia y te avisamos por correo y por evento cuando quede»; con la cerca abierta, «lo hace el motor solo». El cable sigue trayendo UN solo help. Se retira de todas las superficies la frase «esta parte todavía no va sola», que era una afirmación permanente sobre el producto y hoy cambia con una variable de entorno.

El plazo dice lo que cuesta una sesión de trabajo. eta_business_days de un paso con execution: operator no baja de 2 días hábiles, y estimated_ready_at suma esos plazos proyectados. Los números del catálogo se habían descompuesto pensando en una cola —0 quiere decir «el SII contesta este sobre en minutos»— y ninguno describía a una persona nuestra buscando una hora. El intercambio publicaba 0 días con nada agendado. Cuando la cerca se abre, el plazo vuelve al número base.

La página de las muestras impresas ya se puede acuñar. muestras_approval_required entra en CUSTOMER_REMEDY_REASON_CODES, que era la única pieza que faltaba: sin ella POST /v1/empresas/\{empresa_id\}/onboarding/links rechazaba el paso muestras_impresas y la persona que tiene que MIRAR el kit no tenía a dónde ir.

Dónde se configura. Las cinco variables (ALLOW_LIVE_POSTULACION_WRITES, ALLOW_LIVE_INTERCAMBIO_UPLOAD, ALLOW_LIVE_MUESTRAS_UPLOAD, ALLOW_LIVE_CUMPLIMIENTO_WRITES, ALLOW_LIVE_BOLETA_ENROLMENT) van en facturia-worker Y en facturia-api con el mismo valor: el worker las lee para actuar, la API para publicar. docs/deploy-fly.md trae la tabla y el fly secrets set de las dos apps.

2026-09-07 — Los plazos del onboarding dejan de ser una descomposición de una frase de marketing: ahora están medidos

Los números son observaciones. El SII contesta un set de pruebas en minutos (verdicto SOK a los pocos minutos de la declaración), una solicitud de revisión de boletas en 26 segundos, y una Declaración de Cumplimiento con un correo un minuto y 52 segundos después. La habilitación de boletas completa —set, folios, sobres, solicitud, V°B° y declaración— corrió de punta a punta en 33 minutos. Así que set_de_pruebas, cumplimiento y habilitacion pasan de 1 a 0 días hábiles.

Uno subió, y es el honesto. resolucion pasa de 0 a 1: es el único paso que es un acto del SII en su propio calendario, sin nada nuestro en vuelo, y nunca ha llegado el mismo día. Un 0 ahí se leía como «la resolución llega esta tarde».

Lo que queda en la suma del carril de mercado son tres esperas del SII: que registre la postulación (se lee en una consulta posterior, no en la respuesta del envío), la revisión de las muestras impresas y la resolución. Total: 3 días hábiles, y por eso las superficies dicen «alrededor de 2 a 3 días hábiles».

El carril del Sistema de Facturación Gratuito suma 0. credential es una carga en una página nuestra, usuario_autorizado es una lectura de la lista del Portal MIPYME, y puesta_en_vivo es un predicado sobre datos que ya tenemos —no una emisión de prueba acompañada, que es lo que su help prometía y el motor nunca hizo—. Quedas emitiendo el mismo día.

«Avanzar Siguiente Paso» pasa a ser un acto de la cola. El POST a pe_avance4 que saca una postulación de un paso de certificación estaba detrás de una constante escrita a mano, así que ningún trabajo automático podía hacerlo nunca: la cola leía el paso y una persona apretaba el botón en un navegador, mientras set_de_pruebas y simulacion se publicaban como automatic. La cerca es ahora la variable ALLOW_LIVE_STAGE_ADVANCE y el registro de cercas nombra los dos pasos que sostiene, así que execution, el help y el plazo de ambos se derivan de ella como los de los otros cinco. El motor consulta primero y sólo aprieta si el portal está esperando el clic y todos los sets quedaron conformes.

Qué hacer con esto. Si guardaste estimated_ready_at o eta_business_days como una constante, vuelve a leerlos: bajaron. Siguen siendo p50, no promesas, y lo que el SII se demore por encima de ellos no lo pone el catálogo.

2026-09-06 — La declaración de cumplimiento se aprueba en una página y la presenta Facturia; la cesión se abre a las empresas de mercado; los reintentos son del proceso

La Declaración de Cumplimiento es una aprobación, no un trámite en el SII (ADR-0013). steps[].pending_action.approval para cumplimiento trae el acto cumplimiento_declaracion, rol rep_legal: el representante legal lee las consecuencias —la empresa queda en producción y la Información Electrónica de Compras y Ventas corre desde ese mes— y aprueba en la página; Facturia presenta la declaración con el certificado en custodia, que es el del representante legal y el que la Res. Ex. SII 81/2005 exige. El mismo clic registra la autorización del mes (go_live_authorized_by = rep_legal_approval). Tres compuertas en código: la aprobación, titular del certificado = representante legal según el propio SII, y la cerca ALLOW_LIVE_CUMPLIMIENTO_WRITES (de entorno) con un requisito de versión de mandato para cualquier empresa que no sea de la casa. Nuevo código de error cumplimiento_approval_required.

Una empresa colocada a mano recibe las decisiones que sus pasos esperan. Con espina emitting y razón operator_grant, un paso estacionado en action_required ya no se proyecta como completado: pending_action y su bloque approval llegan a la API, al panel y al MCP, y get_pending_human_action lee pending_action antes de decir que nadie espera.

Al primer CAF de producción la empresa cambia sola de canal. Una empresa que certificaba para mercado mientras emitía por el Portal MIPYME pasa a emission_channel: mercado cuando recibe sus primeros folios; entre la resolución y ese momento las emisiones nuevas esperan en cola con el motivo channel_switch_pending.

POST /v1/documents/\{id\}/cede acepta empresas de mercado. Se retira el guard de canal y el código cesion_channel_unsupported; el 409 de un intento anterior informa la última respuesta del Registro (rtc_estado, TrackID). La cerca ALLOW_LIVE_CESION_SUBMITS se lee del entorno y el envío se autoriza por empresa (emitting, activa, producción).

Lo que sigue siendo del cliente lo dice por la razón real. Los permisos «Enviar Doctos.», el usuario autorizado del portal gratuito y la casilla de intercambio se hacen en la sesión del usuario administrador de la cuenta en el SII, autoridad que el certificado en custodia no tiene justo en los casos en que el acto falta; el texto ya no habla de una «decisión legal nuestra».

Las tareas muertas se reencolan solas y el correo lo dice. Tras una caída del SII se reintenta a la hora; tras un error nuestro, una vez por despliegue; hasta tres veces por tarea. Los avisos de operación describen lo que hace el proceso y piden a una persona sólo decisiones irreversibles.

2026-09-05 — Una llave root que por fin hereda alcance, un sandbox que no reserva RUT, y una aprobación con vuelta

Una llave root minteada desde una sesión por fin sirve para algo. (#449) Antes, POST /dashboard/api_keys \{root:true\} devolvía una root con empresas:[], y cualquier llave que se minteara con ella terminaba con el mismo alcance vacío o un 403 privilege_escalation al pedir empresas:["*"] — un callejón sin salida documentado como el camino principal para que un integrador dejara de sostener la contraseña de un humano. Ahora la root hereda el alcance real de la sesión y mintear con ella funciona.

Una empresa de sandbox no reserva nada. (#450) El RUT de una empresa de prueba ya no cuenta contra la unicidad global que sí rige para una empresa real — la misma cuenta puede tener el mismo RUT una vez en sandbox y otra vez en vivo — y DELETE /v1/empresas/\{empresa_id\} existe para las de sandbox (una viva responde 409 empresa_delete_not_allowed, nombrando la revocación del mandato). El alias de recepción <rut>@dte.facturia.cl nunca se asigna a una empresa de sandbox.

La suscripción se cobra después de la elegibilidad, nunca antes. (#475, #530) capabilities.blocked_by puede decir subscription, pero solo cuando la empresa ya podría emitir por todo lo demás; la autorización de puesta en marcha (go_live) es gratis en cada puerta, porque es una decisión del titular, no una emisión.

Un mandato revocado se lee del cable real, y deja puerta de vuelta. (#515, #465) El panel leía una versión propia de la revocación en vez del estado real de la empresa; ahora ambos coinciden, y una empresa con el mandato revocado tiene cómo volver a activarse en vez de quedar varada.

Cada acto humano tiene una puerta alojada genérica, con registro propio. Una pending_action puede traer un bloque approval — quién decide, qué consecuencias tiene, los dos botones — que llama a POST /onboarding/steps/:step/approve; queda una fila en sii_onboarding_step_approvals (migración 127) con quién decidió qué, cuándo y por cuál vía.

La habilitación de boletas la corre Facturia, no una persona clic a clic. (#452, #538, #543, #548) La secuencia completa — set, CAF, ronda, solicitud de revisión, veredicto, declaración — corre como una máquina con exactamente dos momentos humanos: el titular autoriza el inicio del reloj de 24 horas en una página alojada, y la declaración es una página para el representante legal con el formulario prellenado.

La postulación ante el SII la presenta Facturia, después de que el titular la aprueba. (#453) sii_postulacion pasa a actor: facturia, precedida siempre por una aprobación del titular en una página alojada que muestra la consecuencia de perder el facturador gratuito y le deja elegir el momento; el interruptor de producción (ALLOW_LIVE_POSTULACION_WRITES) sigue en false por defecto.

Las herramientas del MCP leen el contrato atestiguado, y tools/list muestra solo lo que el permiso alcanza. (#454-456, #550) Antes, tools/list ofrecía el catálogo completo sin importar el alcance real de la conexión; ahora un cliente ve exactamente las herramientas que sus scopes permiten usar. El consentimiento que autoriza esa conexión está en castellano, con una etiqueta y una frase por permiso, y un preset «Recomendado para emitir con tu agente» que no marca nada por defecto.

/aprobar habla castellano, dice quién lo pidió, y el agente ya no se queda adivinando. (#470, #471) La página mostraba el resumen en inglés y el nombre crudo de la tool (emit_document); ahora redacta el resumen en castellano una sola vez y muestra qué cliente OAuth lo pidió. Del otro lado, un agente puede llamar get_pending_confirmation para saber si la persona ya decidió, en vez de que la única forma de enterarse fuera repetir a ciegas la llamada original.

Recuperar la contraseña ya no requiere escribirnos. (#524) POST /account/password/reset_request manda un enlace de un solo uso, válido 60 minutos, sin confirmar ni negar si la cuenta existe; /recuperar y /restablecer en el panel completan el flujo.

2026-09-04 — CONNECT queda habilitado para boleta electrónica, certificado desde código en 33 minutos

El set de pruebas se ejecutó y se aprobó desde código. El envío del set 0257510898 llegó a EPR con las cinco boletas; el RCOF 0257510910 confirmó recepción conforme.

La solicitud de revisión del portal es un acto automático, no una espera de dos semanas. certBolElectDteInternet — el trámite que un correo de agosto había dejado esperando 22 días de silencio — resultó ser una acción del portal que el SII resuelve en segundos: la V°B° llegó en 26 s, y el set 4911005 quedó SOK.

La declaración de cumplimiento cerró la certificación. Presentada a las 20:35:54; el correo del SII confirmando la habilitación llegó a las 20:37:46 — «habilitados los documentos de boleta electrónica a contar del 01-09-2026».

Toda la sitting se corrió sin una persona en el medio. 33 minutos de punta a punta, cero correos manuales, cero clics — la misma disciplina que ya regía la factura electrónica, extendida ahora a la boleta.

2026-09-03 — Agente primero: la aprobación es una página, el registro es una llamada, y el CAF ya no promete lo que no cumple

confirm_action se retira; una aprobación humana es siempre una página, nunca la palabra del agente (ADR-0012). Un acto que antes se confirmaba con un booleano en la misma llamada ahora aparca un pending_confirmation y entrega una página alojada /aprobar/\{id\}: la persona decide ahí, y ningún tool puede reclamar por su cuenta que ella dijo que sí.

El registro es una sola llamada. POST /account/register acepta bootstrap: \{sandbox_key, empresa\} y el mismo 201 trae la cuenta, la sesión, la primera llave sk_test_ y una empresa de sandbox lista para emitir — las tres llamadas que hacían falta antes quedan en una.

puesta_en_vivo corre sobre un único predicado de elegibilidad. Antes de esto, dos superficies distintas podían preguntar «¿puede emitir en vivo?» y responder cosas distintas; ahora hay una sola función que las dos consultan.

Documentación bilingüe de punta a punta. Cada página de docs.facturia.cl tiene su gemela en /en, con paridad de bloques de código y tablas verificada en cada build — no solo la prosa alrededor.

El CAF declara su propia vigencia. (#191) La Res. Ex. SII N° 58/2017 fija 6 meses calendario desde la Fecha de Autorización; esa fecha ahora se persiste y la vigencia se deriva de ella o queda ausente — nunca inventada — y un folio vencido se rechaza en la recepción exactamente como lo haría el SII.

La cesión se arma y se registra de forma nativa. (#60) El AEC (Anexo Cesión de Créditos) se ensambla con sus tres firmas — la del cedente, la del cesionario y la nuestra — y se sube al Registro del SII (RTC) por POST /v1/documents/\{document_id\}/cede; el estado de una cesión deja de depender de una consulta que nunca decía «cedido».

Las muestras impresas hablan el protocolo del SII en HTTP crudo. (#58) La subida (pdfdteInternet) y la revisión que la sigue se resuelven en un solo handshake, sin sesión de navegador ni una persona en el medio.

2026-09-02 — El sobre de error trae qué hacer, y la puesta en marcha trae a quién decírselo

El sobre de error trae hint y retriable. Los dos salen del mismo diccionario que sirve GET /v1/error_codes, buscados por code y nunca escritos en el punto donde se lanza el error, así que la respuesta y el diccionario no se pueden contradecir. Un agente ya no necesita una segunda llamada de 75 KB para saber qué hacer con un código: lo trae la respuesta que ya tiene en la mano. Faltan solo si el código es más nuevo que tu cliente; trátalo como «lee message y doc_url», nunca como una falla. hint va en inglés a propósito — el español de cada código vive en su ficha de este sitio y en el panel.

La puesta en marcha publica el handoff completo. GET /v1/empresas/\{empresa_id\}/onboarding trae pending_action en cada paso que necesita a una persona, y izado al nivel superior para el paso que nombra blocked_on: quién exactamente tiene que actuar y por qué no se puede delegar, qué tener a mano, las instrucciones ordenadas, la página del SII cuando podemos nombrar una que no dé 404, qué pone en marcha el acto, y cómo nos vamos a enterar de que ocurrió. Ese último campo publica también su propio límite: verification.method: no_producer significa que nada en esta versión observa ese paso, y se nombra en vez de esconderse. El mismo objeto viaja dentro de error.details.pending_action cuando una escritura se rechaza por un paso pendiente.

Nuevo: POST /v1/empresas/\{empresa_id\}/onboarding/steps/\{step\}/recheck. La mitad de vuelta del handoff: la persona terminó y esto nos pide ir a mirar. Acepta una declaración y no produce ningún hecho — no recibe cuerpo, no escribe el estado de ningún paso y devuelve el estado tal como estaba antes de encolar nada. verified viene en la respuesta y siempre es false. Ninguna llamada a la API puede marcar un paso humano como completo, esta incluida.

En el MCP: get_pending_human_action y get_folio_status. El primero responde «¿qué le digo a mi humano?» con el mismo pending_action de arriba. El segundo deja ver el muro antes de chocar con él: cuántos folios quedan por tipo y qué pasa cuando se acaban. Además, cada herramienta del catálogo lleva ahora anotaciones derivadas de lo que hace el handler, no de su nombre — y dos salen al revés de lo que sugiere el nombre: get_document_file y get_f29_propuesta no son de solo lectura, porque acuñan un enlace público sin credencial a un documento tributario firmado. Un cliente que auto-aprueba lecturas estaría repartiendo enlaces de facturas sin preguntarle a nadie.

El SII nos dice en qué forma de IVA está tu empresa, y ahora lo guardamos. El nuevo iva_shape de la empresa reporta si el SII la clasificó como afecta o exenta, con qué evidencia y qué set de pruebas le corresponde. Es de solo lectura: es la respuesta del SII sobre tu empresa, no una configuración. null significa que todavía no lo establecimos, y nunca se asume afecta — hacerlo construiría once documentos que el SII jamás va a revisar.

/v1/cafs existe. El contrato declaraba GET, POST y DELETE /v1/cafs y producción respondía 404 unknown_endpoint a los tres: el código estaba mergeado y el deploy no. Desde ahora ninguna ruta llega al OpenAPI publicado sin haber sido probada contra el router real — si el contrato declarara un endpoint que la API no monta, el build de este sitio falla antes de publicarlo, y al revés también.

2026-08-24 — Un plan, y un 402 en la puerta

Un solo plan: $19.990 + IVA al mes, por empresa, documentos ilimitados. La suscripción es de la EMPRESA, no de la cuenta, así que cada acto se decide con la suscripción del RUT que nombra el request: una empresa vencida nunca detiene a otra de la misma cuenta.

Se bloquean exactamente cinco actos, y están escritos en un solo lugar: POST /v1/documents, POST /v1/documents/\{document_id\}/credit_note, POST /v1/documents/\{document_id\}/send, POST /v1/folios/requests y POST /v1/empresas/\{empresa_id\}/onboarding/go_live. Todo lo demás sigue abierto, y la lista de lo que NO se bloquea es tan explícita como la de lo que sí: cada lectura, todo el modo de prueba para siempre, la respuesta comercial a un documento recibido (el plazo de 8 días es legal y no lo vamos a acortar por una factura nuestra) y las declaraciones tributarias (mismo argumento, con más consecuencias).

El 402 trae con qué arreglarlo. error.details lleva la empresa, su RUT, su nombre, el plan de la cuenta, el estado de la suscripción, el instante de gracia si había uno corriendo, y un checkout_url que apunta a esa empresa. Un agente que recibe un 402 no tiene que improvisar dónde manda a pagar a su humano. Una suscripción que vence sigue emitiendo durante 7 días de gracia antes de que esto empiece.

Nota para quien ya programó contra la tabla de tipos de error. El 402 no es una décima familia: es el único caso en que un invalid_request_error sale con un estado distinto de 400, porque «te falta pagar» no es un request mal formado. plan_limit_reached sigue publicado y no lo emite nada — el plan no tiene cuota que agotar —, y su ficha lo dice.

2026-08-21 — dispatch.dispatch_type ahora es el TipoDespacho del SII

dispatch.dispatch_type cambia de significado — honestamente, porque nunca hizo nada. El vocabulario publicado («de quién es el vehículo») no calzaba con el elemento <TipoDespacho> del XML del SII, así que ambos canales lo rechazaban. Desde hoy el campo es opcional, solo para guías (tipo 52), y toma exactamente los tres valores del SII: receiver_account (1, por cuenta del receptor), issuer_to_customer_site (2, por cuenta del emisor a instalaciones del cliente), issuer_to_other_site (3, por cuenta del emisor a otras instalaciones). Los nombres viejos responden 400 invalid_field nombrando el reemplazo.

Canal SII (facturador gratuito): sigue rechazado. El formulario del portal no tiene ese campo — lo verificamos en la captura — así que mandarlo por ese canal responde 422 dispatch_not_available_for_channel. En un tipo que no es 52 responde 422 dispatch_not_applicable, igual que transfer_type.

2026-08-21 — La guía en monto cero ya no pasa por el canal SII

Qué pasó. Emitimos la primera guía de despacho real por el canal SII (facturador gratuito) con una línea en $0 y traslado interno: un movimiento que no genera débito fiscal y que, hasta ayer, tanto nuestra documentación como el motor consideraban un documento legítimo. El portal la rechazó, con estas palabras: «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.» El reintento con precio 1.000 se aceptó (folio 1, neto 1.000 / IVA 190 / total 1.190). El portal valida su formulario de guía con las validaciones de la factura — el formulario de guía es el de factura con otro código —, así que la regla no mira la clase de traslado.

Qué cambia. En una empresa con emission_channel=sfg, una guía (tipo 52) con cualquier línea de unit_price menor o igual a 0, o cuyo total sea 0, responde 422 sfg_guia_zero_amount nombrando la línea. Se rechaza en la puerta, antes de crear el documento y de encolar nada, y el mensaje cita al portal para que no parezca un límite nuestro.

Por canal mercado no cambia nada. El mismo documento en $0 lo aceptó el SII el mismo día por nuestro motor propio (guía t52 folio 1, traslado interno, aceptada). El XSD y el validador del SII no tienen esa regla: es del formulario del portal. Los dos canales siguen emitiendo guía; ahora está documentado en qué se diferencian también en los montos.

De paso: los borradores de empresas del canal SII ya no avisan no_folios_available. En ese canal los folios los asigna el SII al emitir y POST /documents/previews no tiene un contador que consultar, así que la advertencia salía en todos los borradores y apuntaba a una gestión que ese canal rechaza. folio_preview sigue siendo null y sigue significando lo mismo.

2026-08-19 — Cedibilidad consultable, guía de despacho en ambos canales, y enlaces públicos solo al emitir

Nuevo: consulta de cedibilidad (Ley 19.983). POST /documents/\{document_id\}/cesion encola una consulta de solo lectura al Registro del SII y el resultado queda en el nuevo objeto cesion del documento: \{cedible, sii_status_code, checked_at\} (null hasta la primera consulta). Aplica a los tipos que el servicio del SII admite (33, 34 y 43); en cualquier otro tipo responde 422 cesion_not_applicable. El endpoint pide solo documents:read — es una pregunta, no una acción — y en modo test simula la regla de los 8 días corridos contra el reloj de la empresa. Facturia no ejecuta cesiones: para ceder, el canal sigue siendo el Registro del SII (RPETC).

La guía de despacho (tipo 52) ya se emite por ambos canales. Hasta ahora el canal mercado la rechazaba al parsear (422 tipo_not_available_for_channel); ahora el motor propio la construye con el mismo contrato dispatch publicado para el canal SII: transfer_type obligatorio (los 9 tipos de traslado), carrier, requester_rut. Los traslados que son venta (tipos 1 y 9) llevan totales de factura; los demás pueden ir en monto cero. De paso, los RUT dentro de dispatch se normalizan al aceptar la petición — antes un RUT con puntos se aceptaba en la puerta y moría después de gastar el folio — y dispatch.destination se valida como objeto en vez de truncarse en silencio.

Retirado: pdf_public_url y xml_public_url en el objeto documento. Nunca funcionaron: el secreto del enlace se guarda hasheado y esos campos se construían sin él, así que respondían 404 desde siempre. Igual que con format=80mm, retiramos lo que estaba publicado y no existía en vez de fabricarle un comportamiento nuevo. El contrato real: la URL con ?k= se entrega una sola vez — en la respuesta de POST /documents/\{document_id\}/public_links, o en el objeto public_links de la respuesta de emisión cuando pediste pdf: \{"public": true\} / xml: \{"public": true\}. Guárdala al recibirla. Los webhooks document.created ya no llevan URLs con secreto adentro.

2026-08-16 — La copia cedible, elegible por defecto y compartible

pdf.cedible en la emisión ahora se guarda y se usa. Antes se leía del cuerpo de la petición y no llegaba a ningún lado. Ahora fija el valor por defecto de ESTE documento para GET /documents/\{document_id\}/pdf cuando la lectura no manda ?copy=: true lo hace cedible por defecto, false lo fija en original explícitamente. true en un tipo fuera de 33/34 es 422 cedible_not_available, rechazado antes de gastar un folio, no ignorado en silencio.

branding.cedible_by_default ahora se aplica. Es el valor por defecto de la EMPRESA cuando un documento no fija el suyo propio. La precedencia completa para GET …/pdf sin ?copy=: el ?copy= explícito de la lectura, si lo hay; si no, el pdf.cedible del documento; si no, el cedible_by_default de la empresa; si no, original. Corregido de paso: aplica a facturas tipo 33 y 34, no solo 33.

branding.default_pdf_format ahora se valida. PATCH /empresas/\{empresa_id\}/branding aceptaba cualquier valor sin chequearlo, incluido 80mm — el mismo valor retirado de ?format= en la wave anterior. Ahora cualquier valor que no sea carta es 400 invalid_field.

Enlaces públicos pueden nombrar cedible. POST /documents/\{document_id\}/public_links acepta cedible en artifacts — un artefacto real y separado, con su propio cedible_public_url, no el enlace de pdf con otro nombre. No está en el conjunto por defecto: pedirlo requiere nombrarlo explícitamente, y solo existe para facturas (422 cedible_not_available en cualquier otro tipo).

2026-08-15 — Lo que estaba publicado y no existía

Retirado: format (carta | 80mm) en el PDF. Ningún handler lo leyó nunca. ?format=80mm devolvía el mismo PDF carta que todo lo demás, así que una integración de POS conectada a él imprimía una hoja carta en un rollo térmico sin nada en la respuesta que lo dijera. No existe layout térmico y no lo estamos construyendo. Supply del parámetro — en la query o como pdf: \{ "format": … \} al emitir — ahora responde 400 invalid_field.

Retirado: copy=both. El Manual de Muestras Impresas del SII permite exactamente una página por archivo (p.12), así que las dos copias son dos archivos y una respuesta HTTP lleva uno. both nunca nombró algo que pudiera existir. copy acepta original y cedible; cualquier otro valor es 400.

copy=cedible ahora elige de verdad. Antes había un solo archivo por documento y para una factura ese archivo llevaba el recuadro de acuse de recibo y la leyenda CEDIBLE dibujados encima — así que copy=original devolvía una copia cedible y el parámetro publicado no seleccionaba nada. Ahora se rendean y guardan las dos copias al aceptarse el documento. Los documentos rendereados antes de hoy tienen un solo archivo: copy=cedible se sirve correctamente desde él y copy=original devuelve esos mismos bytes mientras se encola un re-render.

pdf_public_url y xml_public_url en las lecturas ahora son null, y eso es la corrección. Devolvían una URL construida solo con el slug, sin el secreto ?k= que /d/\{slug\} exige — es decir, todas respondían 404. El secreto se guarda hasheado, igual que una llave de API, así que no podemos reconstruirlo: la URL funcional aparece en la respuesta que la mintea (la emisión con public: true, o POST …/public_links) y en ninguna otra parte. null es un valor sobre el que puedes ramificar; una URL rota no lo es.

GET /v1/mandato_terms existe. §7.3 lleva desde el primer borrador diciendo que presentes el texto del mandato antes de postear la aceptación, y el endpoint respondía 404. Devuelve el texto, su versión, su hash SHA-256 y un status que hoy dice draft: el contrato todavía no termina su revisión legal, y decirlo es preferible a servir un texto que parezca vinculante. Sin autenticación, como el diccionario de errores.

expand en los listados deja de ignorarse. GET /v1/documents?expand=… acepta events, references y received_document, y expande la página completa con una consulta extra, no una por fila. xml y pdf_base64 quedan fuera a propósito y responden 400: incorporan un artefacto entero por fila. GET /v1/received_documents no acepta ninguna expansión, por lo mismo.

expand=received_document ya no está vacío. Un documento emitido se vincula a uno recibido cuando lo referencia por tipo_dte y folio y el RUT emisor del recibido es el RUT receptor del emitido, dentro de la misma empresa, y solo cuando ese calce es único. Si hay dos candidatos el vínculo queda en null: un antecedente equivocado es una afirmación y una ausencia no lo es.

El límite general es 300 req/min, como siempre dijo la tabla. El limitador contaba hasta 100. La documentación era la correcta.

Rotar y revocar una llave respetan el modo de la sesión. Una sesión de dashboard en modo prueba veía solo sus llaves de prueba al listar, y podía rotar o revocar una llave live por id. Las tres operaciones aplican ahora el mismo filtro.

CORS expone los headers que la documentación promete. Facturia-Request-Id, los RateLimit-*, Retry-After, Facturia-Idempotent-Replay y Facturia-Poll-After se enviaban desde siempre y el navegador los descartaba.

2026-08-14 — v1 — la superficie publicada

Emisión. Ocho tipos de DTE (33, 34, 39, 41, 46, 52, 56, 61). El POST responde 201 con status: queued y folio: null; el folio lo asigna el motor al pasar a sent, y el viaje al SII es una máquina de estados observable (document.sent, Facturia-Poll-After). POST /v1/documents/previews cotiza y valida sin emitir; preview_id emite lo aprobado y emitir dos veces la misma previsualización devuelve el documento original, no un segundo folio.

Artefactos. PDF y XML firmado por llave, o por enlace público firmado con expiración en días o en segundos. El XML se sirve en ISO-8859-1, byte a byte igual a lo que recibió el SII, por la ruta autenticada y por el enlace público.

Copia cedible para facturas 33 y 34. La Ley 19.983 entrega la tercera copia a la factura, sin distinguir afecta de exenta, y el Manual de Muestras Impresas del SII la exige para ambas. copy=cedible en cualquier otro tipo responde 422 cedible_not_available.

Recepción. Los DTE de tus proveedores, la respuesta comercial (ACD, RCD, RFP, RFT, ERM) y la ventana de 8 días corridos anclada en la marca de recepción del SII, en calendario de Santiago. Sin ancla no hay plazo: 422 commercial_response_anchor_pending en vez de una fecha inventada.

Impuestos. RCV de compras y ventas, caracterización individual y masiva, propuesta de F29 con explicación y trazabilidad por código, archivo de carga del SII y declaración como job con estados honestos. El código 048 se declara como insumo externo y se avisa cada vez que falta.

Webhooks. Firma HMAC sobre los bytes crudos, entrega al-menos-una-vez, reintentos publicados y un libro de eventos consultable y reproducible durante 12 meses.

Sandbox. Gratis para siempre y sin un solo socket hacia el SII: folios virtuales, resultados forzables y los mismos límites de tasa que producción, para que una prueba de carga signifique algo.

En esta página

2026-10-06 — GET /v1/contribuyentes/\{rut\}: la razón social, las actividades y un giro que cabe en la factura, desde la nómina pública del SII2026-09-23 — Una persona natural como receptor de boletas: el nombre llega al documento2026-09-23 — «¿Ya emites con otro proveedor?»: la resolución declarada se verifica cuando un operador lo confirma, y resolucion.verification_status dice dónde va2026-09-10 — GET /v1/folios cuenta los folios que de verdad tienes: los rangos en cola dejan de ser invisibles2026-09-09 — La libreta de direcciones: deja el domicilio de un cliente registrado una vez y deja de escribirlo en cada documento2026-09-08 — Si tu empresa ya emite con otro proveedor, deja de certificarse de nuevo: declaras la resolución y la comprobamos usándola2026-09-08 — Un período que nadie leyó no es un período vacío: el bloque sync habla del mes que pediste, POST /tax/sync fuerza de verdad, el job se cierra, y el MCP tiene sync_rcv2026-09-08 — El MCP hace todo lo que hace la API: 53 herramientas nuevas, folios:write entra al vocabulario, y el RCV se rellena solo doce meses al conectar2026-09-07 — Las boletas de una empresa del sistema gratuito ya salen — por nuestro propio motor, con el canal decidido por tipo2026-09-07 — La receta de Python que publicamos ya genera el cliente entero: vuelven los tipos de /account/* y los cinco esquemas que el generador borraba2026-09-07 — El catálogo publica la declaración de cumplimiento como nuestra, y la tabla de pasos deja de poder contradecirlo2026-09-07 — Un paso en curso dice qué vio la última consulta al SII y a qué hora volvemos a mirar; subir el certificado ya no se queda sin sonda2026-09-07 — El intercambio y las muestras impresas los corre la cola, y las muestras las apruebas tú antes de que salgan2026-09-07 — execution deja de estar escrito a mano: cada paso dice cómo corre HOY, y el plazo dice lo que cuesta una sesión de trabajo2026-09-07 — Los plazos del onboarding dejan de ser una descomposición de una frase de marketing: ahora están medidos2026-09-06 — La declaración de cumplimiento se aprueba en una página y la presenta Facturia; la cesión se abre a las empresas de mercado; los reintentos son del proceso2026-09-05 — Una llave root que por fin hereda alcance, un sandbox que no reserva RUT, y una aprobación con vuelta2026-09-04 — CONNECT queda habilitado para boleta electrónica, certificado desde código en 33 minutos2026-09-03 — Agente primero: la aprobación es una página, el registro es una llamada, y el CAF ya no promete lo que no cumple2026-09-02 — El sobre de error trae qué hacer, y la puesta en marcha trae a quién decírselo2026-08-24 — Un plan, y un 402 en la puerta2026-08-21 — dispatch.dispatch_type ahora es el TipoDespacho del SII2026-08-21 — La guía en monto cero ya no pasa por el canal SII2026-08-19 — Cedibilidad consultable, guía de despacho en ambos canales, y enlaces públicos solo al emitir2026-08-16 — La copia cedible, elegible por defecto y compartible2026-08-15 — Lo que estaba publicado y no existía2026-08-14 — v1 — la superficie publicada