> Versión Markdown de https://contadeo.com/desarrolladores para agentes de IA.
> Índice completo de documentos: https://contadeo.com/llms.txt
> Las rutas relativas (por ejemplo /registro o /precios) resuelven contra https://contadeo.com.

---

# API de Contadeo™ — Documentación para integradores

Emite comprobantes electrónicos autorizados por el SRI desde tu propio
sistema (ERP, punto de venta, e-commerce o software contable) con una API
REST. Tú envías el JSON; nosotros generamos el XML, lo firmamos con el
certificado del emisor, lo transmitimos al SRI con reintentos automáticos y
te devolvemos el comprobante autorizado con su RIDE (PDF) y XML.

---

## Descripción general

- **Base URL**: `https://contadeo.com/api`
- Todos los cuerpos son JSON (`Content-Type: application/json`).
- Autenticación con **API key** (`Authorization: Bearer cdo_…`) para
  integraciones, o JWT de usuario para el flujo del panel.
- Empieza gratis: [crea una cuenta](/registro) (10 comprobantes/mes sin costo)
  y prueba todo el flujo en el ambiente de **Pruebas** del SRI antes de pasar
  a Producción.

Por API se emiten los **seis comprobantes** del SRI: factura (01), liquidación
de compra (03), nota de crédito (04), nota de débito (05), guía de remisión
(06) y comprobante de retención (07) — uno por request, o hasta 100 facturas
en un lote.

Los ejemplos usan `$API` y `$TOKEN`:

```bash
API=https://contadeo.com/api
TOKEN="<accessToken>"
```

### La integración, de punta a punta

Seis pasos desde cero hasta el PDF en manos del comprador:

1. **Autentícate.** Crea una API key en el panel (**Configuración → API**) y
   mándala en cada request: no expira ni necesita refresh.
   → [Autenticación](#1-autenticacion)
2. **Registra el emisor.** RUC y régimen de la empresa, su establecimiento y
   su punto de emisión. Si el RUC ya facturaba con otro sistema, sincroniza el
   secuencial de una vez.
   → [Organización](#3-organizacion-emisores-establecimientos-puntos)
3. **Sube el certificado `.p12`** de firma con su contraseña. Se cifra en
   reposo y solo se descifra en memoria en el momento de firmar.
   → [Certificados de firma](#4-certificados-de-firma-p12)
4. **Emite.** `POST /comprobantes/factura` responde **202** con el
   `comprobanteId` y la clave de acceso; firmar y transmitir ocurre en segundo
   plano. Manda `Idempotency-Key` y un reintento nunca duplica ni quema otro
   secuencial. → [Comprobantes](#6-comprobantes-emision)
5. **Espera la resolución.** Polling de `GET /comprobantes/:id` cada ~3 s hasta
   `AUTORIZADO`, `RECHAZADO` o `DEVUELTA` (el SRI suele resolver en 5-30 s), o
   registra un webhook y te avisamos sin que preguntes.
   → [Webhooks](#8-webhooks)
6. **Entrega el resultado.** `GET /comprobantes/:id/ride` y `/xml` devuelven
   URLs prefirmadas del PDF y del XML autorizado. Si mandas el correo del
   comprador, el RIDE le llega solo.
   → [Notificación por email](#9-notificacion-por-email-al-receptor)

Lo que no tienes que programar: el armado del XML de la ficha técnica vigente,
la firma XAdES-BES, los reintentos ante el SRI, la contingencia cuando el
servicio está caído y el cuadre de impuestos (se valida antes de enviar y
devuelve **400** con el error de negocio, no un rechazo del SRI tres minutos
después).

### Pruebas y Producción

Cada cuenta emite en un ambiente y se cambia con un solo campo:
`PATCH /tenants/current` con `{"ambienteActivo": 2}` (1 = Pruebas,
2 = Producción). En **Pruebas** el SRI autoriza igual, pero los comprobantes
**no tienen validez tributaria** y no gastan el cupo de tu plan (van contra
500/mes aparte). Prueba ahí el flujo entero —con tu `.p12` real— antes de
pasar a Producción.

### Esta documentación en Markdown

Cada sección de esta página se copia o se descarga en Markdown desde su
título, y el documento entero está en
[contadeo.com/desarrolladores.md](/desarrolladores.md). Pégaselo a tu
asistente de IA junto con el [spec OpenAPI](/openapi.json) y tiene el contexto
completo para escribir la integración. ¿Prefieres que el asistente facture él
mismo? Eso es el [servidor MCP](#conecta-tu-asistente-de-ia-mcp).

---

## 1. Autenticación

Hay dos formas de autenticarse. Para **integraciones de sistemas** (ERP, POS,
e-commerce) recomendamos **API keys**; el flujo JWT es el que usa el panel web.

### API keys (recomendado para integraciones)

Se crean en el panel: **Configuración → API** (rol owner o admin). La key se
muestra **una sola vez** al crearla — guárdala en tu gestor de secretos.

```bash
curl $API/comprobantes -H "authorization: Bearer cdo_tu_api_key"
```

- Se envía igual que un token: `Authorization: Bearer cdo_...`.
- **No expira ni necesita refresh** — ideal para servidores.
- Permisos limitados por diseño: una key puede tener los roles `emisor`,
  `contador`, `lector` y/o `webhooks` (default: `emisor`), pero **nunca**
  owner/admin — no puede gestionar usuarios, otras keys ni la configuración de
  la cuenta. `webhooks` es opt-in y habilita gestionar los webhooks de la
  cuenta (ver la sección Webhooks).
- **Revocación instantánea** desde el panel (las integraciones que la usen
  reciben 401 de inmediato).
- Hasta 5 keys activas por cuenta (una por sistema integrado, recomendado).

### JWT de usuario (el flujo del panel)

JWT Bearer. El **access token** dura 15 minutos; el **refresh token** 7 días.
Todas las rutas exigen `Authorization: Bearer <accessToken>` salvo
`auth/login`, `auth/refresh`, `health/*` y `tenants/register`.

| Método | Ruta | Descripción |
| --- | --- | --- |
| POST | `/auth/login` | Inicia sesión. Devuelve ambos tokens. |
| POST | `/auth/refresh` | Rota el par de tokens con un refresh token válido. |
| POST | `/auth/password` | Cambia la contraseña propia: `{actual, nueva}` (≥ 8 chars). 204. |

El cambio de contraseña **revoca los refresh tokens** emitidos antes del
cambio (401 `Sesión revocada` al intentar refrescar); hay que volver a
iniciar sesión.

```bash
curl -X POST $API/auth/login -H 'content-type: application/json' \
  -d '{"email":"owner@miempresa.ec","password":"********"}'
# → { "accessToken": "...", "refreshToken": "..." }

curl -X POST $API/auth/refresh -H 'content-type: application/json' \
  -d '{"refreshToken":"..."}'
# → { "accessToken": "...", "refreshToken": "..." }   (par nuevo)
```

- El login no elige empresa: entra a la del último acceso. Con varias
  cuentas, cámbiate con `POST /auth/cambiar-cuenta` (`{tenantId}`, tomado de
  `GET /auth/mis-cuentas`).
- Un refresh token **no** sirve como access token (401).
- Ante un `401` por expiración: llamar a `/auth/refresh` y reintentar.

### Roles

Cada usuario tiene roles por cuenta (en el JWT): `owner` > `admin` >
`emisor` > `contador` / `lector`. Cada endpoint lista sus roles permitidos.
Para integraciones máquina-a-máquina recomendamos crear un usuario dedicado
con rol `emisor`.

### Formato de errores

```jsonc
// Error genérico
{ "statusCode": 404, "message": "Emisor no encontrado" }

// Validación de negocio pre-firma (evita rechazos del SRI)
{
  "mensaje": "Comprobante inválido",
  "problemas": [
    { "codigo": "IDENTIFICACION_INVALIDA", "campo": "identificacionComprador",
      "severidad": "error", "mensaje": "Identificación inválida..." }
  ]
}
```

| HTTP | Significado |
| --- | --- |
| 400 | Cuerpo inválido o validación de negocio fallida (`problemas[]`) |
| 401 | Token ausente/inválido/expirado |
| 403 | Rol insuficiente |
| 402 | Cupo mensual del plan agotado (`upgrade: true` en el cuerpo) — ver [precios](/precios) |
| 404 | Recurso inexistente (o de otra cuenta — aislamiento multi-tenant) |
| 409 | Conflicto de unicidad (RUC/código/identificación duplicados) |
| 429 | Rate-limit del registro (5 altas por IP cada 10 min) |

### Referencia OpenAPI

Además de esta guía narrativa, la API publica su especificación
**OpenAPI 3.1**: [referencia interactiva](/referencia-api.html) (todos los
endpoints con esquemas, parámetros y errores) y el
[spec descargable](/openapi.json) para generar SDKs
(openapi-generator, orval, etc.).

### Versionado y compatibilidad

- La URL base actual es la **v1** de la API (sin prefijo de versión). Toda
  respuesta incluye el header `X-API-Version` (fecha de la versión de la
  superficie, p. ej. `2026-06`).
- Los cambios **aditivos** (campos nuevos en respuestas, endpoints nuevos,
  parámetros opcionales) pueden ocurrir en cualquier momento: tu integración
  debe tolerar campos desconocidos.
- Los cambios **incompatibles** (renombrar/eliminar campos o endpoints,
  cambiar códigos de estado) se anuncian en esta página con **mínimo 90 días
  de aviso** y el endpoint antiguo se mantiene durante la ventana de
  deprecación.

---

## 2. Onboarding y cuenta

| Método | Ruta | Roles | Descripción |
| --- | --- | --- | --- |
| POST | `/tenants/register` | público | Crea la cuenta + usuario owner (plan free). Devuelve tokens. |
| GET | `/tenants/current` | cualquiera | Cuenta del token (incluye `plan` y `planExpiraAt`). |
| GET | `/tenants/current/uso-plan` | cualquiera | El plan que rige la emisión y su consumo: `{regimen, plan, limiteComprobantes, usadosEsteMes, limiteUsuarios, usuarios, planExpiraAt, tiposPermitidos, propio}`. En una empresa patrocinada (`regimen: 'pool'`) el nivel superior es el plan del patrocinador — el mismo que aplica el servidor — y lo contratado por la empresa va en `propio`. |
| PATCH | `/tenants/current` | owner, admin | Edita `nombre`, `ambienteActivo` (1=Pruebas, 2=Producción). |

El registro exige `ruc` (con dígito verificador válido), `declaraTitularidadRuc: true`,
`aceptaPrivacidad: true` y `aceptaTerminos: true` (consentimiento LOPDP del
[Aviso de Privacidad](/privacidad)) — 400 si falta cualquiera. El `slug` lo genera el
sistema: no se pide ni se acepta del cliente.

```bash
curl -X POST $API/tenants/register -H 'content-type: application/json' \
  -d '{"nombre":"Mi Empresa","email":"yo@empresa.ec","password":"secreta123",
       "ruc":"0912345675001","declaraTitularidadRuc":true,
       "aceptaPrivacidad":true,"aceptaTerminos":true}'

# Cambiar a Producción (¡los comprobantes pasan a tener validez tributaria!)
curl -X PATCH $API/tenants/current -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d '{"ambienteActivo":2}'
```

### Equipo (usuarios de la cuenta)

| Método | Ruta | Roles | Descripción |
| --- | --- | --- | --- |
| GET | `/equipo` | todos | Miembros de la cuenta con sus roles. |
| POST | `/equipo` | owner, admin | Alta: `{email, nombre, password, roles[]}`. |
| PATCH | `/equipo/:membershipId` | owner, admin | Cambia `roles[]`. |
| DELETE | `/equipo/:membershipId` | owner | Quita la membresía. 204. |

Reglas: roles válidos `owner, admin, emisor, contador, lector`; un **admin no
puede otorgar `owner` ni gestionar a un owner**; la cuenta debe conservar
**al menos un owner activo**. El alta respeta el **límite de usuarios del
plan** — 400 al alcanzarlo.

---

## 3. Organización: emisores, establecimientos, puntos

| Método | Ruta | Roles |
| --- | --- | --- |
| GET | `/emisores` | todos |
| GET | `/emisores/:id` | todos |
| POST | `/emisores` | owner, admin |
| PATCH | `/emisores/:id` | owner, admin |
| DELETE | `/emisores/:id` | owner (409 si ya emitió comprobantes) |
| GET | `/emisores/:emisorId/establecimientos` | todos |
| POST | `/emisores/:emisorId/establecimientos` | owner, admin |
| PATCH | `/establecimientos/:id` | owner, admin |
| DELETE | `/establecimientos/:id` | owner, admin |
| GET | `/establecimientos/:estabId/puntos-emision` | todos |
| POST | `/establecimientos/:estabId/puntos-emision` | owner, admin |
| PATCH | `/puntos-emision/:id` | owner, admin |
| DELETE | `/puntos-emision/:id` | owner, admin |
| GET | `/emisores/:emisorId/secuenciales` | todos |
| PATCH | `/emisores/:emisorId/secuenciales` | owner, admin |

### Secuenciales (migración desde otro sistema)

Si el RUC ya emitió comprobantes con otro facturador, el SRI devolverá el
error 45 («secuencial registrado») hasta alcanzar el último número usado.
Contadeo se autorecupera (reemite con el siguiente secuencial), pero puedes
sincronizar el contador de una vez:

```bash
curl -X PATCH $API/emisores/$EMISOR_ID/secuenciales \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"tipoComprobante":"01","establecimiento":"001","puntoEmision":"001",
       "ambiente":2,"ultimoNumero":4582}'
# ultimoNumero = el último YA USADO; el siguiente comprobante será el 4583.
# 400 si intentas bajar de un secuencial ya emitido en Contadeo (duplicados).
```

> **Deprecado:** `PUT /emisores/:emisorId/secuenciales` hace lo mismo y se
> mantiene hasta el **2027-06-30** (responde con headers `Deprecation` y
> `Sunset`). Usa `PATCH`: el body es parcial, no un reemplazo del recurso.

```bash
curl -X POST $API/emisores -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"ruc":"0912345675001","razonSocial":"MI EMPRESA S.A.",
       "dirMatriz":"Av. Principal 123, Guayaquil",
       "obligadoContabilidad":false,"regimen":"GENERAL"}'
# regimen: GENERAL | RIMPE_EMPRENDEDOR | RIMPE_NEGOCIO_POPULAR
# El RUC se valida (dígito verificador) → 400 si es inválido.

curl -X POST $API/emisores/$EMISOR_ID/establecimientos \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"codigo":"001","direccion":"Matriz centro","nombre":"Matriz"}'
# codigo: exactamente 3 dígitos. Igual para puntos-emision.
```

## 4. Certificados de firma (.p12)

| Método | Ruta | Roles |
| --- | --- | --- |
| GET | `/certificados?emisorId=` | todos (sin material secreto) |
| POST | `/certificados` | owner, admin |

```bash
curl -X POST $API/certificados -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d "{\"emisorId\":\"$EMISOR_ID\",
       \"p12Base64\":\"$(base64 -w0 certificado.p12)\",
       \"passphrase\":\"<contraseña del .p12>\",\"alias\":\"firma 2026\",
       \"declaraTitularidadFirma\":true}"
# → { "id": "<certificadoId>", "subject": "...", "notAfter": "2029-..." }
```

El `.p12` y su contraseña se cifran con envelope AES-256-GCM; solo se
descifran en memoria al momento de firmar. El GET nunca devuelve secretos.

## 5. Catálogos: productos y clientes

| Método | Ruta | Roles |
| --- | --- | --- |
| GET / POST | `/productos?q=&limit=` | todos / owner, admin, emisor |
| PATCH / DELETE | `/productos/:id` | owner, admin, emisor / owner, admin |
| GET / POST | `/clientes?q=&limit=` | todos / owner, admin, emisor |
| PATCH / DELETE | `/clientes/:id` | owner, admin, emisor / owner, admin |
| GET | `/clientes/export.csv` | todos (descarga CSV; LOPDP: portabilidad) |
| GET | `/clientes/lookup/:identificacion?tipo=04\|05` | owner, admin, emisor, contador |
| PUT | `/clientes/por-identificacion/:tipo/:identificacion` | owner, admin, emisor |
| GET | `/sri/catalogos` | todos |

`GET /sri/catalogos` devuelve las tablas de referencia del SRI con las que
valida el backend (tarifa general de IVA **vigente**, tarifas por código,
formas de pago, tipos de identificación/comprobante, regla de consumidor
final). Consúmelas desde tu integración en vez de hardcodearlas: cuando una
tarifa cambie (como el IVA 12%→15% de 2024), tu sistema seguirá cuadrando.

Productos: `codigoPrincipal` único por cuenta (409), `precioUnitario`
numérico, `tarifaCodigo` ("4"=IVA 15%, "0"=0%, "7"=exento, "6"=no objeto).
El DELETE es lógico. Clientes: `tipoIdentificacion`+`identificacion` únicos
por cuenta (409).

En ambos listados, `?q=` busca por texto (clientes: razón social,
identificación, email; productos: nombre y códigos) y `?limit=` acota la
respuesta (máx. 200). Sin `limit` se devuelve el catálogo completo —
**envíalo siempre** desde integraciones.

### Prellenado del comprador por RUC/cédula

`GET /clientes/lookup/:identificacion?tipo=04|05` busca primero en tu
directorio de clientes (devuelve también email/teléfono) y, si no está, en
el **catastro público del SRI** — ideal para autocompletar los datos del
comprador en tu ERP antes de emitir. Para cédula (`tipo=05`) consulta el
RUC de persona natural (cédula+`001`).

```bash
curl "$API/clientes/lookup/1790016919001?tipo=04" \
  -H "authorization: Bearer $TOKEN"
# → { "fuente": "sri", "razonSocial": "CORPORACION FAVORITA C.A.",
#     "direccion": "...", "estadoSri": "ACTIVO", "regimen": "GENERAL",
#     "advertencias": ["..."] }   // advertencias: RUC suspendido/fantasma
```

| Error | Significado |
| --- | --- |
| 400 | Identificación con dígito verificador inválido o tipo no consultable |
| 404 | Sin datos (ni en tu directorio ni en el SRI) |
| 503 | Catastro del SRI no disponible — trátalo como "sin prellenado" |

### Validación de RUC (catastro del SRI)

`GET /sri/ruc/:ruc` valida cualquier RUC contra el catastro público del SRI y
devuelve **siempre 200 con un veredicto** — pensado para integraciones:
verificar a un cliente o proveedor antes de facturar, sin manejar errores.

```bash
curl "$API/sri/ruc/1790016919001" -H "authorization: Bearer $TOKEN"
# → { "ruc": "1790016919001", "valido": true, "encontrado": true,
#     "fuente": "sri",
#     "contribuyente": { "razonSocial": "CORPORACION FAVORITA C.A.",
#       "estado": "ACTIVO", "regimen": "GENERAL", "regimenSri": "GENERAL",
#       "obligadoContabilidad": true, "contribuyenteEspecial": true,
#       "agenteRetencion": true, "actividadEconomica": "VENTA AL POR..." },
#     "advertencias": [] }
```

- `valido` es la estructura y el dígito verificador; `encontrado`, que consta
  en el catastro. Una entrada malformada responde `valido: false`, nunca 400.
- **`agenteRetencion`** te avisa de que esa contraparte va a retenerte — dato
  del catastro que nadie te da antes de emitir. También llegan
  `contribuyenteEspecial`, `tipoContribuyente` y la actividad económica (la
  descripción del catastro, no un código CIIU).
- **Resiliente**: caché de 24 horas y, si el SRI se cae, respondemos con la
  última copia guardada (hasta 7 días, `fuente: "cache"`; `degradado: true` si
  además venció su frescura). Sin copia: `degradado: true` con `motivo`.
  Nunca 404 ni 503.
- `advertencias` trae las señales de riesgo: RUC suspendido, contribuyente
  fantasma o transacciones inexistentes.
- Accesible con API key (roles `emisor`, `contador` o `lector`).

### Upsert por clave natural

`PUT /clientes/por-identificacion/:tipo/:identificacion` crea o actualiza
el cliente con esa identificación ("recordar cliente"). Solo pisa los
campos que llegan con valor — un upsert sin `email` no borra el email
guardado. Ideal tras emitir: guarda los datos del comprador para
autocompletarlos la próxima vez.

```bash
curl -X PUT $API/clientes/por-identificacion/04/1790016919001 \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"razonSocial":"CORPORACION FAVORITA C.A.","email":"pagos@favorita.ec"}'
```

---

## 6. Comprobantes (emisión)

La emisión es **asíncrona**: el POST valida, reserva el secuencial atómico,
genera la clave de acceso y encola la firma/transmisión. Responde **202**:

```json
{ "comprobanteId": "uuid", "claveAcceso": "49 dígitos", "estado": "BORRADOR" }
```

**Idempotencia (recomendada siempre).** Manda el header `Idempotency-Key` con
un identificador tuyo (el ID del pedido, por ejemplo): si el request se corta y
reintentas con la misma key, recibes el MISMO comprobante (`reutilizado: true`)
en vez de crear un duplicado y quemar otro secuencial.

- **Alcance**: la clave es única por `(cuenta, emisor, tipo de comprobante)`,
  así que reutilizar `pedido-8841` en la factura y en su nota de crédito no
  colisiona.
- **Sin caducidad**: la clave vive con el comprobante. Un reintento de hace un
  mes sigue devolviendo el original — no hay ventana de 24 horas como en otros
  proveedores.
- **Misma clave con cuerpo distinto → 422**: el payload se huella (SHA-256); si
  la clave llega con otro contenido, la API rechaza en vez de devolver un
  comprobante que no corresponde.
- Máximo 200 caracteres (en el header de lote, 190; ver abajo).

```bash
curl -X POST https://contadeo.com/api/comprobantes/factura \
  -H "Authorization: Bearer cdo_..." \
  -H "Idempotency-Key: pedido-8841" \
  -d @factura.json
```

**Lote.** `POST /comprobantes/lote` acepta hasta **100 facturas** por request
(body de hasta 2 MB, solo en esta ruta) con semántica **parcial**: cada factura
se valida por separado y una inválida se reporta con su índice sin frenar a las
demás. Dos errores sí detienen el lote — **402** (cupo del plan agotado) y
**403** (sin permiso sobre el emisor) — porque afectarían igual a todas las
restantes, que quedan en `NO_INTENTADA`. Corrige y reenvía el MISMO lote: la
idempotencia hace el reintento seguro.

La respuesta resume y detalla por ítem:

```json
{ "total": 100, "encoladas": 97, "fallidas": 2, "noIntentadas": 1,
  "resultados": [{ "indice": 0, "referencia": "pedido-8841",
    "estado": "BORRADOR", "comprobanteId": "uuid",
    "claveAcceso": "49 dígitos", "reutilizado": false, "error": null }] }
```

- `estado` por ítem: `BORRADOR` (encolada), `ERROR` (falló esta),
  `NO_INTENTADA` (el lote se detuvo antes de intentarla).
- `referencia`: tu identificador por factura (máx. 120 caracteres); vuelve tal
  cual, para casar la respuesta con tus pedidos.
- Idempotencia: con el header `Idempotency-Key` del lote (máx. **190**
  caracteres) la clave de cada elemento se deriva como `header:indice`; si un
  elemento trae su propia `idempotencyKey`, esa manda. Sin header no se inventa
  ninguna.
- El lote se encola con **prioridad menor** que la emisión individual: tus
  emisiones sueltas no esperan detrás de un lote de 100.

| Método | Ruta | codDoc | Roles |
| --- | --- | --- | --- |
| POST | `/comprobantes/factura` | 01 | owner, admin, emisor |
| POST | `/comprobantes/liquidacion` | 03 | owner, admin, emisor |
| POST | `/comprobantes/nota-credito` | 04 | owner, admin, emisor |
| POST | `/comprobantes/nota-debito` | 05 | owner, admin, emisor |
| POST | `/comprobantes/retencion` | 07 | owner, admin, emisor |
| POST | `/comprobantes/guia-remision` | 06 | owner, admin, emisor |
| GET | `/comprobantes?estado=&emisorId=&limit=` | — | todos |
| GET | `/comprobantes/export.csv?estado=&emisorId=&desde=&hasta=` | — | todos (CSV, máx. 10 000 filas) |
| GET | `/comprobantes/:id` | — | todos |
| GET | `/comprobantes/:id/ride` | — | todos (URL prefirmada del PDF) |
| GET | `/comprobantes/:id/xml` | — | todos (URL prefirmada del XML autorizado) |
| POST | `/comprobantes/:id/anular` | — | owner, admin, emisor |
| POST | `/comprobantes/:id/reenviar-email` | — | owner, admin, emisor (202) |

**Anular**: solo sobre `AUTORIZADO`; marca el estado interno `ANULADO`.
Aplica las reglas de la **Resolución NAC-DGERCGC25-00000017** (vigente desde
enero 2026): anulable solo **hasta el día 7 (inclusive) del mes siguiente**
a la emisión, y **nunca** sobre comprobantes a consumidor final
(`9999999999999`) — 400 en ambos casos. **No** tramita la anulación ante el
SRI: el trámite formal se hace en el portal *SRI en línea* (el emisor
solicita, el receptor acepta). **Reenviar email**: solo `AUTORIZADO` con
RIDE; cuerpo opcional `{para}` — sin él, usa el email del campo adicional
del XML (400 si no hay).

**Campos comunes** de todo POST de emisión:

```json
{ "emisorId": "uuid", "certificadoId": "uuid",
  "establecimiento": "001", "puntoEmision": "001" }
```

La emisión aplica el **plan de la cuenta** antes de reservar el secuencial
(ver [precios](/precios)): tipo no incluido en el plan → 400 (campo
`tipoComprobante`); cupo mensual agotado → **402** con
`{mensaje, problemas[], upgrade: true}`.

La `fechaEmision` la fija el **servidor** (hora de Ecuador) — no se envía.
Excepción: la guía de remisión recibe `fechaIniTransporte`/`fechaFinTransporte`.

### Estados del comprobante

```
BORRADOR → FIRMADO → ENVIADO → AUTORIZADO ✓
                   ↘ DEVUELTA (recepción rechazó)   RECHAZADO ✗ (autorización)
        (SRI caído) ↘ CONTINGENCIA → reenvío automático → ENVIADO → ...
```

Hacer **polling** de `GET /comprobantes/:id` cada ~3 s hasta estado terminal
(`AUTORIZADO`, `RECHAZADO`, `DEVUELTA`). El SRI suele resolver en 5–30 s.

Cuando el comprobante termina en `DEVUELTA` o `RECHAZADO`, tanto
`GET /comprobantes/:id` como el listado `GET /comprobantes` incluyen
`mensajesSri`: los mensajes literales del SRI, con la forma
`[{identificador, tipo, mensaje, informacionAdicional}]`. Úsalos para mostrar
la razón al usuario (p. ej. `[45] ERROR SECUENCIAL REGISTRADO`).

### Flujo completo (ejemplo: factura)

```bash
# 1) Emitir (10.00 + 15% IVA = 11.50)
RES=$(curl -s -X POST $API/comprobantes/factura \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
  "emisorId":"'$EMISOR_ID'","certificadoId":"'$CERT_ID'",
  "establecimiento":"001","puntoEmision":"001",
  "infoFactura":{
    "tipoIdentificacionComprador":"05",
    "razonSocialComprador":"JUAN PEREZ","identificacionComprador":"1745678902",
    "obligadoContabilidad":"NO",
    "totalSinImpuestos":10,"totalDescuento":0,
    "totalConImpuestos":[{"codigo":"2","codigoPorcentaje":"4",
      "baseImponible":10,"tarifa":15,"valor":1.5}],
    "importeTotal":11.5,"pagos":[{"formaPago":"01","total":11.5}]},
  "detalles":[{"descripcion":"Producto","cantidad":1,"precioUnitario":10,
    "descuento":0,"precioTotalSinImpuesto":10,
    "impuestos":[{"codigo":"2","codigoPorcentaje":"4","tarifa":15,
      "baseImponible":10,"valor":1.5}]}]
}')
ID=$(echo $RES | jq -r .comprobanteId)

# 2) Poll hasta resolución
curl -s $API/comprobantes/$ID -H "authorization: Bearer $TOKEN"
# → { "estado": "AUTORIZADO", "claveAcceso": "...", "numeroAutorizacion": "...",
#     "mensajesSri": null }   // en DEVUELTA/RECHAZADO trae los mensajes del SRI

# 3) Descargar RIDE (PDF) y XML autorizado (URLs prefirmadas, expiran en 1 h)
curl -s $API/comprobantes/$ID/ride -H "authorization: Bearer $TOKEN"  # {"url": ...}
curl -s $API/comprobantes/$ID/xml  -H "authorization: Bearer $TOKEN"  # {"url": ...}
```

### Cuerpos por tipo (campos propios)

- **Factura (01)** — `infoFactura` (comprador, totales, `pagos[]`), `detalles[]`.
- **Liquidación de Compra (03)** — `infoLiquidacion` con **proveedor**
  (`tipoIdentificacionProveedor`, `razonSocialProveedor`,
  `identificacionProveedor`), totales y `pagos[]`; `detalles[]`.
- **Nota de Crédito (04)** — `infoNotaCredito`: comprador + documento
  modificado (`codDocModificado:"01"`, `numDocModificado:"001-001-000000001"`,
  `fechaEmisionDocSustento:"2026-06-01"`), `totalSinImpuestos`,
  `valorModificacion`, `totalConImpuestos[]` (sin `tarifa`), `motivo`;
  `detalles[]`.
- **Nota de Débito (05)** — `infoNotaDebito`: comprador + documento modificado
  + `impuestos[]` + `valorTotal`; `motivos[]` (`{razon, valor}`), sin detalles.
- **Retención ATS (07)** — `infoCompRetencion` (sujeto retenido,
  `periodoFiscal:"mm/aaaa"`, `parteRel`), `docsSustento[]` con
  `numDocSustento` de **15 dígitos sin guiones**, `impuestosDocSustento[]`,
  `retenciones[]` (`codigo` 1=Renta/2=IVA, `codigoRetencion`,
  `porcentajeRetener`, `valorRetenido`) y `pagos[]`.
- **Guía de Remisión (06)** — `infoGuiaRemision` (transportista, `placa`,
  `dirPartida`, `fechaIniTransporte`/`fechaFinTransporte`),
  `destinatarios[]` con sus `detalles[]`. Sin montos.

### Validaciones de negocio (devuelven 400 antes de llegar al SRI)

| Regla | Error SRI que previene |
| --- | --- |
| Dígito verificador de cédula/RUC | 62 |
| Cuadre de totales (líneas, impuestos, importe) | 52 |
| Fecha futura / extemporánea | 65 |
| Consumidor final (`07`) con total > $50 | 69 |
| Clave de acceso (módulo 11) | 39 |

Notas: en retención el **código de retención debe coincidir con su tarifa**
de la tabla del SRI (p. ej. 312 → 2.0%), o el SRI rechaza con error 52.
IVA 15% = `codigo:"2"`, `codigoPorcentaje:"4"`.

---

## 7. Operación y monitoreo

| Método | Ruta | Roles | Descripción |
| --- | --- | --- | --- |
| GET | `/reportes/ventas?emisorId=&desde=&hasta=` | todos | Agregados del período: conteo por estado, total $ autorizado, desglose por tipo y por mes. |
| GET | `/admin/colas` | owner, admin | Conteos por estado de las colas de emisión. |
| POST | `/admin/colas/:nombre/reintentar` | owner, admin | Reencola los fallidos. |
| GET | `/health/live` | público | Proceso vivo. |
| GET | `/health/ready` | público | Plataforma operativa. |
| GET | `/health/sri` | público | Alcanzabilidad del WS del SRI. |

## 8. Webhooks

La alternativa al polling: registra un endpoint HTTPS y te avisamos cuando un
comprobante llega a estado final.

| Método | Ruta | Roles | Descripción |
| --- | --- | --- | --- |
| GET | `/webhooks` | owner, admin, webhooks | Endpoints registrados. |
| POST | `/webhooks` | owner, admin, webhooks | Alta: `{url, eventos?, descripcion?}`. El `secret` (`whsec_…`) viaja **solo** en esta respuesta. |
| PATCH | `/webhooks/:id` | owner, admin, webhooks | Pausa o reanuda: `{activo}`. |
| DELETE | `/webhooks/:id` | owner, admin, webhooks | Baja inmediata. |
| POST | `/webhooks/:id/rotar-secreto` | owner, admin, webhooks | Nuevo secreto; el anterior deja de firmar al instante. |
| GET | `/webhooks/entregas?limit=` | owner, admin, webhooks | Bitácora de entregas (estado, intentos, código HTTP, último error). |

- **Eventos:** `comprobante.autorizado`, `comprobante.rechazado`,
  `comprobante.devuelto` y `f104.listo` (el borrador del F104 del período se
  marcó listo: llega con las cifras congeladas y el vencimiento).
- **Default del alta:** sin `eventos` te suscribes a todo el catálogo VIGENTE
  al crear el webhook (el array se materializa en ese momento). Por eso un
  webhook guardado antes de que existiera `f104.listo` no lo recibe: actívalo
  con un `PATCH` si lo quieres. Si solo te interesan los estados de
  comprobante, decláralos explícitos.
- **Gestión por API key:** requiere una key con el rol `webhooks` (opt-in al
  crearla). También se gestionan desde el panel: Configuración → Equipo →
  Webhooks.
- **Reintentos:** 8 intentos con backoff exponencial si tu servidor no
  responde 2xx. Máximo 5 endpoints por cuenta. Solo HTTPS.

**Verificación de la firma.** Cada entrega llega con
`X-Contadeo-Signature: t=<timestamp>,v1=<hex>`, donde
`hex = HMAC_SHA256(secret, "<timestamp>.<cuerpo crudo>")`. Verifica sobre el
cuerpo **crudo** (sin re-serializar) y en tiempo constante:

```js
import { createHmac, timingSafeEqual } from "node:crypto";

function verificar(secret, firma, cuerpoCrudo) {
  const { t, v1 } = Object.fromEntries(
    firma.split(",").map((p) => p.split("=")),
  );
  const esperado = createHmac("sha256", secret)
    .update(`${t}.${cuerpoCrudo}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(esperado, "hex"));
}
```

El SDK (`contadeo-sdk`) trae `verificarFirmaWebhook()` ya hecha.

### Límites de uso

| Límite | Valor |
| --- | --- |
| Global | 300 requests/min **por cuenta** (no por IP) |
| Emisión individual | 60/min |
| Lote | 10/min (hasta 100 facturas cada uno) |
| Ambiente de Pruebas | 500 comprobantes/mes (no gastan tu cupo) |

Al superarlos recibes **429**; espera y reintenta (con `Idempotency-Key` el
reintento es seguro).

## 9. Notificación por email al receptor

Si el comprobante incluye en `infoAdicional` un campo cuyo nombre contenga
`email`/`correo`, al autorizarse se envía automáticamente el RIDE (PDF) + XML
al receptor:

```json
"infoAdicional": [{ "nombre": "Email", "valor": "cliente@correo.com" }]
```

## 10. Buzón de compras (recepción por correo)

Cada cuenta tiene una dirección de buzón (`<token>@buzon.contadeo.com`, con un
token opaco y rotable). Reenvía ahí el correo con el que tu proveedor te mandó
su factura y el **XML adjunto entra solo al módulo Compras**: el emisor se
reconoce por el **RUC receptor del comprobante** (por eso un correo reenviado
por cualquiera no puede meterte compras ajenas), la clave de acceso deduplica,
y los PDF sin XML quedan guardados **para revisión** en la bandeja.

| Método | Ruta | Roles | Descripción |
| --- | --- | --- | --- |
| GET | `/buzon` | owner, admin, contador | La dirección del buzón (se crea al primer uso) y si está habilitado. |
| POST | `/buzon/rotar` | owner, admin | Token nuevo; la dirección anterior muere al instante. |
| GET | `/buzon/mensajes` | todos | La bandeja: cada correo con su saldo por adjunto (compra creada, duplicado, PDF a revisión, error con motivo). |

Quien reenvía un correo no ve ninguna respuesta: **la bandeja es la
respuesta**. El cuerpo del correo no se guarda (solo remitente, asunto y el
resultado de cada adjunto).

---

## Conecta tu asistente de IA (MCP)

¿Usas Claude u otro asistente compatible con
[MCP](https://modelcontextprotocol.io)? Con el servidor **`contadeo-mcp`**
tu asistente factura por ti: *"emite una factura de 2 horas de consultoría a
$75 para Juan Pérez, pago por transferencia, y mándale el PDF"*.

```json
{
  "mcpServers": {
    "contadeo": {
      "command": "npx",
      "args": ["-y", "contadeo-mcp"],
      "env": { "CONTADEO_API_KEY": "cdo_tu_api_key" }
    }
  }
}
```

**La matemática tributaria no la hace la IA — la hace el servidor**, con las
reglas oficiales del SRI embebidas:

- `preparar_factura` calcula líneas, IVA por tarifa y totales con redondeo
  oficial, valida el **dígito verificador** de cédulas/RUC y la regla de
  consumidor final (máx. $50) — y devuelve un resumen que el asistente debe
  confirmar contigo **antes** de emitir.
- `emitir_factura` re-valida el cuadre y rechaza payloads calculados a mano.
- Catálogos: buscar y **crear** clientes y productos conversacionalmente
  ("regístrame estos 20 clientes").
- `esperar_autorizacion` (polling al SRI), descarga de RIDE/XML, reporte de
  ventas y `consultar_reglas_sri` (tablas oficiales de tarifas, formas de
  pago e identificaciones).
- `anular_comprobante` marca como anulada una factura autorizada, y por ser
  una acción sensible el asistente la confirma contigo antes de llamarla. Es
  el registro interno: el trámite formal sigue haciéndose en el portal del
  SRI. Fuera del plazo legal o a consumidor final no se anula — se reversa
  con una nota de crédito.

Autentica con una **API key dedicada** con rol `emisor` (Configuración →
API) y prueba primero con tu cuenta en ambiente de **Pruebas** del SRI. El
paquete incluye además un *Agent Skill* para Claude con el flujo completo y
el manejo de errores del SRI.

→ **Guía completa, ejemplos y preguntas frecuentes en
[contadeo.com/mcp](/mcp).**

---

## ¿Listo para integrar?

1. [Crea tu cuenta gratis](/registro) — incluye 10 comprobantes/mes.
2. Configura tu emisor y sube tu certificado `.p12` (puedes hacerlo desde el
   panel o por API).
3. Prueba el flujo completo en el ambiente de **Pruebas** del SRI.
4. Cambia a Producción cuando estés listo (`ambienteActivo: 2`).

**SDK oficial para TypeScript/Node:** [`contadeo-sdk`](https://www.npmjs.com/package/contadeo-sdk)
(`npm install contadeo-sdk`) — emisión individual y por lote, espera de
autorización, descargas y **verificación de firma de webhooks** en tiempo
constante. Para otros lenguajes, la [spec OpenAPI](/openapi.json) trae los
payloads de emisión completamente tipados: cualquier generador produce un
cliente utilizable.

¿Dudas o volúmenes altos? Escríbenos a **soporte@contadeo.com** o desde el
panel (**Tickets**) — el plan Empresa incluye soporte prioritario para
integraciones.
