Para coding agents
Playbook autocontenido para la tarea "integra la facturación electrónica de Simplo en esta app" — flujo canónico de emisión vía REST, errores, idempotencia, testing y checklist.
Instrucciones para un coding agent cuya tarea es “integra la facturación electrónica de Simplo (DTEs chilenos) en esta aplicación”. Esta página es autocontenida; los docs enlazados agregan profundidad, no prerrequisitos.
Estas docs también existen en formato máquina: /llms.txt es un
índice real en formato llmstxt.org — anota cada página
del sitio y cada operación de la API con su enlace markdown. Toda página HTML
tiene su espejo en markdown puro agregando index.md a la URL (ej.
/comienza/quickstart/index.md), y la
referencia completa de la API vive en un solo archivo en
/api-reference.md.
Instala la skill
Sección titulada «Instala la skill»Este sitio publica el Agent Skill simplo-invoicing en
/.well-known/skills/:
npx skills add https://docs.simplo.clLa skill le entrega a tu agente este mismo playbook (instalación, flujo canónico de emisión, errores por clase, idempotencia, testing y checklist) más referencias de tipos de DTE, errores, multi-empresa y sandbox — con los mismos límites honestos: ninguna key ni endpoint debe inventarse, y la key solo alcanza las cinco rutas DTE de la tabla de abajo.
Vocabulario de dominio: DTE = Documento Tributario Electrónico. SII =
la autoridad tributaria chilena. RUT = identificador tributario chileno
(76543210-3). Folio = número de documento autorizado por el SII.
CAF = el archivo del SII que autoriza un rango de folios.
Configura tus credenciales
Sección titulada «Configura tus credenciales»Simplo es una API REST (https://api.simplo.cl): cualquier lenguaje que
hable HTTP + JSON sirve, sin instalar nada.
- Credencial: variable de entorno
SIMPLO_API_KEY, enviada en el headerX-Api-Key. Formatos:sk_simplo_sandbox_...= sandbox (documentos de prueba, sin validez tributaria);sk_simplo_...sin_sandbox_= producción (documentos reales). Solo backend — nunca expongas la key en un bundle de navegador, nunca la commitees. Agrégala al.env.exampledel proyecto como nombre, sin valor. - Si el usuario no tiene key: la crea él mismo en app.simplo.cl → Desarrolladores (key de sandbox, self-serve — sin pedir acceso a nadie). La emisión en producción se habilita por empresa (hola@simplo.cl); desarrolla y prueba todo en sandbox mientras tanto. No inventes una key ni caigas en endpoints falsos.
export SIMPLO_API_KEY="sk_simplo_sandbox_..."Envuelve las llamadas en un cliente delgado (módulo o función factory), no repitas headers en cada call site.
Qué puede hacer tu API key — no llames nada más
Sección titulada «Qué puede hacer tu API key — no llames nada más»Una key billing:dte (sandbox o live) alcanza exactamente estas rutas:
| Operación | ¿Por key? |
|---|---|
POST /api/v1/billing/dte (emitir; Idempotency-Key obligatorio) |
✅ |
GET /api/v1/billing/dte · GET /api/v1/billing/dte/{id} |
✅ |
GET /api/v1/billing/dte/{id}/pdf · …/xml |
✅ |
Cualquier otra ruta /api/v1/billing/* (CAF, empresa, certificado, libros, webhooks, clientes, productos) |
❌ 403 BILLING_API_KEY_ROUTE_NOT_ALLOWED — solo-sesión (app.simplo.cl) |
POST /api/v1/mcp |
❌ 401 — el MCP requiere una key agent:read u OAuth, nunca billing:dte |
Consecuencias prácticas: no hagas “preflight” de certificado/CAF/ambiente
con la key (esas rutas responden 403; en sandbox el certificado y los
folios ya están aprovisionados — emite directamente), y un
403 BILLING_API_KEY_ROUTE_NOT_ALLOWED significa “ruta fuera de la
superficie por key”, nunca “cuenta sin habilitar”.
El flujo canónico de emisión
Sección titulada «El flujo canónico de emisión»Toda integración de facturación se reduce a este flujo. Adapta los nombres, conserva la forma:
export async function emitInvoice(order: { id: string; // tu propio identificador de orden customerRut: string; // ej. '76543210-3' customerName: string; items: Array<{ name: string; quantity: number; unitPriceClp: number }>;}) { const res = await fetch('https://api.simplo.cl/api/v1/billing/dte', { method: 'POST', headers: { 'X-Api-Key': process.env.SIMPLO_API_KEY!, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), // un reintento nunca duplica la factura }, body: JSON.stringify({ tipo_dte: 33, // 33 factura (B2B) — 39 para boletas a consumidor // Para boletas (tipo 39/41) agrega ind_servicio: 3 — es OBLIGATORIO // (omitirlo es un 400 "IndServicio is required for boleta"). receptor: { rut: order.customerRut, razon_social: order.customerName, }, detalle: order.items.map((item) => ({ nombre: item.name, cantidad: item.quantity, precio: item.unitPriceClp, // CLP entero, NETO (el IVA lo agrega la API) })), // NO envíes `metadata`: hoy la API la acepta pero NO la persiste // (se descarta en silencio). La correlación va en TU base (regla 5). }), });
if (!res.ok) throw await toSimploError(res); // ver "Manejo de errores" abajo
const dte = await res.json(); // dte.estado === 'firmado': firmado y en cola. La aceptación del SII es ASÍNCRONA. return dte;}Reglas que hacen esto correcto:
- Los montos son pesos chilenos (CLP) enteros. Sin centavos, sin
floats para dinero. En facturas
precioes neto (la API agrega el IVA, 19%); en boletas (39/41)precioes bruto (IVA incluido). - La respuesta retorna antes del veredicto del SII. Persiste
dte.id, tratafirmadocomo “en vuelo”, y resuelve el estado final con polling deGET /api/v1/billing/dte/{id}hasta queestadoseaaceptado,rechazadoocon_reparos. (La entrega de webhooks aún no está activa — polling es el camino soportado hoy.) - Guarda
dte.idydte.folioen tu lado (ej. en la fila de la orden) inmediatamente después de emitir. - No bloquees el flujo del usuario en la aceptación del SII — puede tardar. Emite, responde, reconcilia asíncronamente.
- La correlación con tu orden vive en TU base, no en la API. El campo
metadatadel request se acepta pero no se persiste hoy (se descarta en silencio — elmetadataque devuelveGET /dte/{id}lo genera el servidor). Guardadte.idyfoliocontra tuorder_iden tu propio sistema, y nunca uses el RUT del receptor (ni otro PII) como identificador de correlación.
Manejo de errores — por status HTTP
Sección titulada «Manejo de errores — por status HTTP»Un error de la API es un 4xx/5xx con un body JSON
{ code, message, details?, action? } (contrato completo:
Errores):
async function toSimploError(res: Response) { const body = await res.json().catch(() => ({})); return new SimploApiError(res.status, body.code, body.message, body.details);}| Status | Qué significa | Haz esto |
|---|---|---|
| 400/422 | Input inválido (code: "VALIDATION_ERROR") |
Bug o input malo del usuario. Muestra body.details (field/code/message). Nunca reintentes tal cual. Común: INVALID_RUT — valida el dígito verificador del RUT en tu capa de formularios. |
| 401 | Key mal configurada, revocada o inválida | Falla rápido con una pista de setup; no reintentes. La API no distingue el motivo (anti-enumeración). |
| 403 | BILLING_API_KEY_ROUTE_NOT_ALLOWED: la ruta es solo-sesión (fuera de las 5 rutas DTE) |
Deja de llamar esa ruta — usa la tabla de arriba. No es una cuenta sin habilitar; no reintentes. |
| 404 | ID equivocado o recurso de otra empresa | Trátalo como bug de datos. |
| 409 | IDEMPOTENCY_IN_PROGRESS: la misma emisión ya corre |
Espera y re-consulta en vez de re-emitir. En otro caso (ej. folio duplicado), muéstralo. |
| 422 | IDEMPOTENCY_BODY_MISMATCH: misma key, body distinto |
Usa una Idempotency-Key nueva por cada emisión distinta. |
| 429 | Rate limit (60 llamadas/min por empresa en la superficie por key) | Encola/frena al llamador; respeta el header Retry-After. |
| 5xx | Problema transitorio de la API | Reintenta solo si la request es idempotente (ver abajo); loguea el error para tu cola de reintentos. |
Tabla completa: Errores.
Reglas de idempotencia
Sección titulada «Reglas de idempotencia»- Toda emisión (
POST /api/v1/billing/dte) requiere el headerIdempotency-Key(UUID) — genera uno nuevo por llamada. Un reintento en vuelo con la misma key nunca puede duplicar una factura. - Para dedup entre procesos (job reintentado por tu cola), usa tu propia
key estable como valor del header:
Idempotency-Key: order-${orderId}. La misma key + el mismo body dentro de 24h devuelve el resultado original. - Nunca reutilices una idempotency key con un body distinto — eso es un
422concode: 'IDEMPOTENCY_BODY_MISMATCH'.
Multi-empresa
Sección titulada «Multi-empresa»Una API key = una empresa. Si la app atiende varias empresas, mantén una key por empresa (variables de entorno / entradas de secretos separadas) y selecciona la key correspondiente antes de cada request. No mezcles keys de distintas empresas en el mismo request. Detalles: Multi-empresa.
Actualizaciones de estado (polling — los webhooks aún no entregan)
Sección titulada «Actualizaciones de estado (polling — los webhooks aún no entregan)»La entrega de webhooks todavía no está activa. Aunque el registro de
endpoints existe en el contrato, hoy ningún evento se entrega — no
construyas el flujo de veredicto sobre webhooks. Implementa un job de
polling: GET /api/v1/billing/dte/{id} hasta un estado terminal
(aceptado / rechazado / con_reparos), con backoff (en sandbox el ciclo
tarda segundos; en producción, lo que tarde el SII). El contrato de entregas
(Standard Webhooks) está documentado en Webhooks
para cuando se active.
Testing
Sección titulada «Testing»- Nunca llames a producción desde tests — los documentos emitidos con
una key live son documentos tributarios reales. Para tests de integración
contra la API real, usa una key de sandbox (
sk_simplo_sandbox_..., self-serve — ver Quickstart): mismo contrato, documentos sin validez tributaria, y RUTs mágicos de receptor para forzar rechazo, con_reparos o folios agotados (casos infelices). - Para tests unitarios, mockea
fetch(o el cliente HTTP que uses) en el límite de tu módulo de integración — inyecta unfetchfake o usa una librería de mocking HTTP (ej.msw,nock) para simular las respuestas deapi.simplo.cl. - Asegura con asserts:
tipo_dtecorrecto, montos CLP enteros, RUT pasado sin tocar,dte.id/foliopersistidos, el headerIdempotency-Keypresente, y los caminos de error4xx(validación, conflicto). - Asegura que la key usada en tests sea la de sandbox (
sk_simplo_sandbox_) — nunca la live.
Definition of done — checklist de integración
Sección titulada «Definition of done — checklist de integración»- Cliente HTTP delgado creado (headers
X-Api-Key/Content-Typecentralizados); key desdeSIMPLO_API_KEY;.env.exampleactualizado; sin key en código ni frontend. - Camino de emisión implementado con montos CLP enteros netos, el
tipo_dtecorrecto para el caso de uso (33 factura B2B / 39 boleta), y unIdempotency-Keynuevo por request. -
dte.idyfoliopersistidos en el registro de dominio al emitir. - Veredicto asíncrono manejado con un job de polling hasta un
estadoterminal (los webhooks aún no entregan — no dependas de ellos). - Ninguna llamada a rutas fuera de la superficie por key (nada de
CAF/empresa/certificado/webhooks con
X-Api-Key; nada de MCP con una keybilling:dte). - Existe el camino
rechazado: notificación al operador o flujo correctivo — nunca silencioso. - Los reintentos entre procesos llevan un
Idempotency-Keyestable. - Errores manejados por status HTTP según la tabla;
body.codeybody.detailsmostrados cuando aplica. - Tests:
fetch/HTTP mockeado (o sandbox para integración), nunca producción, caminos de error cubiertos. - Si es multi-empresa: la key correcta por empresa en cada request; sin mezclar credenciales.
- No se instaló
@simplohq/sdkcomo requisito (aún no está publicado).