Ir al contenido
SimploSimplo Docs

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.

Este sitio publica el Agent Skill simplo-invoicing en /.well-known/skills/:

Ventana de terminal
npx skills add https://docs.simplo.cl

La 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.

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 header X-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.example del 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 ([email protected]); desarrolla y prueba todo en sandbox mientras tanto. No inventes una key ni caigas en endpoints falsos.
Ventana de terminal
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”.

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:

  1. Los montos son pesos chilenos (CLP) enteros. Sin centavos, sin floats para dinero. En facturas precio es neto (la API agrega el IVA, 19%); en boletas (39/41) precio es bruto (IVA incluido).
  2. La respuesta retorna antes del veredicto del SII. Persiste dte.id, trata firmado como “en vuelo”, y resuelve el estado final con polling de GET /api/v1/billing/dte/{id} hasta que estado sea aceptado, rechazado o con_reparos. (La entrega de webhooks aún no está activa — polling es el camino soportado hoy.)
  3. Guarda dte.id y dte.folio en tu lado (ej. en la fila de la orden) inmediatamente después de emitir.
  4. No bloquees el flujo del usuario en la aceptación del SII — puede tardar. Emite, responde, reconcilia asíncronamente.
  5. La correlación con tu orden vive en TU base, no en la API. El campo metadata del request se acepta pero no se persiste hoy (se descarta en silencio — el metadata que devuelve GET /dte/{id} lo genera el servidor). Guarda dte.id y folio contra tu order_id en tu propio sistema, y nunca uses el RUT del receptor (ni otro PII) como identificador de correlación.

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.

  • Toda emisión (POST /api/v1/billing/dte) requiere el header Idempotency-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 422 con code: 'IDEMPOTENCY_BODY_MISMATCH'.

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.

  • 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 un fetch fake o usa una librería de mocking HTTP (ej. msw, nock) para simular las respuestas de api.simplo.cl.
  • Asegura con asserts: tipo_dte correcto, montos CLP enteros, RUT pasado sin tocar, dte.id/folio persistidos, el header Idempotency-Key presente, y los caminos de error 4xx (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-Type centralizados); key desde SIMPLO_API_KEY; .env.example actualizado; sin key en código ni frontend.
  • Camino de emisión implementado con montos CLP enteros netos, el tipo_dte correcto para el caso de uso (33 factura B2B / 39 boleta), y un Idempotency-Key nuevo por request.
  • dte.id y folio persistidos en el registro de dominio al emitir.
  • Veredicto asíncrono manejado con un job de polling hasta un estado terminal (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 key billing:dte).
  • Existe el camino rechazado: notificación al operador o flujo correctivo — nunca silencioso.
  • Los reintentos entre procesos llevan un Idempotency-Key estable.
  • Errores manejados por status HTTP según la tabla; body.code y body.details mostrados 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/sdk como requisito (aún no está publicado).