# 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`](/llms.txt) es un
índice real en formato [llmstxt.org](https://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`](/comienza/quickstart/index.md)), y la
referencia completa de la API vive en un solo archivo en
[`/api-reference.md`](/api-reference.md).

> **El SDK TypeScript está en preparación — usa REST hoy:**
> `@simplohq/sdk` **todavía no está publicado en npm** y su repositorio todavía
> no es público. **No ejecutes `npm install @simplohq/sdk`** — el paquete no
> existe en el registro y ese comando fallará. El camino que funciona hoy, y el
> que sigue este playbook, es la **API REST** directa contra el
> [contrato OpenAPI](/referencia/contrato-openapi/). Cuando el SDK se publique
> será un complemento opcional sobre el mismo contrato — ver
> [SDK TypeScript](/agentes/sdk/).

## Instala la skill

Este sitio publica el Agent Skill `simplo-invoicing` en
[`/.well-known/skills/`](/.well-known/skills/index.json):

```bash
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.

## 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 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
  ([hola@simplo.cl](mailto:hola@simplo.cl)); desarrolla y prueba todo en
  sandbox mientras tanto. No inventes una key ni caigas en endpoints falsos.

```bash
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

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

Toda integración de facturación se reduce a este flujo. Adapta los nombres,
conserva la forma:

```ts
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.

## 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](/referencia/errores/)):

```ts
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](/referencia/errores/).

## Reglas de idempotencia

- 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'`.

## 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](/comienza/multi-empresa/).

## 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](/referencia/webhooks/)
para cuando se active.

## 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](/comienza/quickstart/)): mismo contrato,
  documentos sin validez tributaria, y RUTs mágicos de receptor para forzar
  rechazo, con_reparos o folios agotados
  ([casos infelices](/comienza/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

- [ ] 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).

---

Versión HTML: https://docs.simplo.cl/agentes/para-coding-agents/ · Índice para agentes: https://docs.simplo.cl/llms.txt
