code218 Facturación SRI

Guía de integración

Emisión de comprobantes electrónicos autorizados por el SRI de Ecuador, por cuenta de varios contribuyentes.

El contrato exacto —rutas, campos, tipos— está en la especificación OpenAPI. Esta guía cubre el orden de los pasos y el significado de cada resultado.

https://api.facturacion.code218.org

Tres cosas antes de empezar

  1. La emisión tiene tres resultados, y dos de ellos no son errores HTTP.
  2. La cabecera Idempotency-Key es obligatoria.
  3. El ambiente de pruebas no es otra URL: es una propiedad del emisor.

Credenciales

El alta de integradores es una operación fuera de banda. Escríbenos y te entregamos una credencial junto con un emisor de prueba.

Authorization: Bearer fsri_<prefijo>_<secreto>

La credencial identifica al integrador: con una sola operas todos tus emisores. El emisor viaja en la ruta (/v1/emisores/{emisorId}/…) y el punto de emisión en el cuerpo.

El valor completo se entrega una sola vez y no se puede recuperar. Si se pierde, emitimos otro y revocamos el anterior. Puedes tener varias credenciales vigentes a la vez, que es el estado normal durante una rotación.

Los cuatro motivos de 401 —cabecera ausente, mal formada, credencial revocada, integrador desactivado— devuelven el mismo cuerpo.


Alta de un contribuyente

Tres pasos. GET /v1/emisores/{emisorId} devuelve puedeEmitir para comprobar si falta alguno.

1. Emisor

POST /v1/emisores
{
  "ruc": "1790012345001",
  "razonSocial": "ASADERO EL BUEN SABOR S.A.",
  "nombreComercial": "El Buen Sabor",
  "direccionMatriz": "Av. Amazonas 1234, Quito",
  "ambiente": "PRUEBAS",
  "obligadoContabilidad": false,
  "referenciaExterna": "negocio-0077"
}

razonSocial debe coincidir con el registro del SRI; el nombre comercial va aparte.

referenciaExterna es tu identificador para este negocio. Resuelve el emisor sin guardar nuestros UUID (GET /v1/emisores?referenciaExterna=negocio-0077) y viaja en cada webhook. Pertenece a tu espacio de nombres: dos integradores pueden usar la misma.

Un RUC es único en todo el servicio. Repetirlo devuelve 409 EMISOR_DUPLICADO.

2. Punto de emisión

Una caja o local, con su propia serie de numeración.

{
  "establecimiento": "001",
  "puntoEmision": "001",
  "descripcion": "Caja principal",
  "referenciaExterna": "caja-principal",
  "secuencialInicial": 42
}

secuencialInicial es el número del próximo comprobante, no el del último emitido. Para una caja nueva, 1. Para un local que ya facturaba con esa serie, el siguiente al último que emitió. Empezar en 1 sobre una serie en uso repite números ya autorizados; el campo no tiene valor por defecto por eso.

La serie admite 9 dígitos. Al llegar a 999999999 hay que registrar otro punto de emisión.

3. Certificado

POST /v1/emisores/{emisorId}/certificado
Content-Type: multipart/form-data

Campos archivo (el .p12) y password. El archivo se cifra en reposo y no se puede descargar; la contraseña no aparece en ninguna respuesta ni en los logs.

{
  "estado": "VALIDO",
  "titular": "JUAN PEREZ MORA",
  "expiraEn": "2027-03-14T23:59:59Z",
  "diasParaExpirar": 150,
  "cargadoEn": "2026-10-15T09:12:44-05:00"
}

estado pasa a POR_EXPIRAR a los 30 días restantes, el mismo umbral del evento certificado.por_expirar.

Un .p12 autofirmado supera esta validación y el SRI lo rechaza al transmitir. La carga comprueba que el archivo sirva para firmar, no quién lo emitió. El certificado debe ser de una entidad acreditada y del mismo RUC que emite.


Emitir

POST /v1/emisores/{emisorId}/comprobantes
Idempotency-Key: pedido-4871
{
  "puntoEmisionId": "b1d60f38-9c45-4a2e-8f71-3e5c8a0b2947",
  "comprador": {
    "tipoIdentificacion": "CEDULA",
    "identificacion": "1712345678",
    "razonSocial": "JUAN PEREZ MORA",
    "email": "juan@ejemplo.com",
    "direccion": "Av. Universitaria 123"
  },
  "items": [
    {
      "codigo": "ALM-001",
      "descripcion": "Almuerzo completo",
      "cantidad": 2,
      "precioUnitario": 5.00,
      "tarifaIva": "GENERAL"
    }
  ],
  "formaPago": "SIN_UTILIZACION_SISTEMA_FINANCIERO"
}

Los tres resultados

El cuerpo tiene una sola forma. El código HTTP y el campo estado indican en qué acabó.

Código estado Acción
201 AUTORIZADO Ninguna. Hay RIDE y XML descargables
202 EN_CONTINGENCIA Ninguna. No reenviar
200 RECHAZADO Corregir los datos y emitir otro comprobante

Un rechazo del SRI no es un error de la petición: se procesó correctamente y el resultado fue que no se autorizó. Reintentarlo emite el mismo comprobante rechazado; el número ya se consumió, y uno nuevo lleva número nuevo. motivoRechazo trae el texto del SRI sin traducir.

El código 43 del SRI, «clave de acceso registrada», no es un rechazo: significa que un envío anterior sí llegó. El servicio consulta la autorización en lugar de darlo por rechazado.

Catálogos

Los valores son los del SRI. Los completos están en la especificación; los habituales:

Campo Valores
tarifaIva GENERAL, CERO, EXENTO, NO_OBJETO_DE_IMPUESTO
formaPago SIN_UTILIZACION_SISTEMA_FINANCIERO (efectivo), TARJETA_CREDITO, TARJETA_DEBITO, DINERO_ELECTRONICO, TARJETA_PREPAGO
tipoIdentificacion CEDULA, RUC, PASAPORTE, CONSUMIDOR_FINAL

Idempotencia

Idempotency-Key es obligatoria y lleva el id del pedido en tu plataforma. Sin ella, la emisión responde 400 CLAVE_IDEMPOTENCIA_AUSENTE.

Protege del caso en que un POST se va en timeout mientras el SRI tarda: la petición original sigue emitiendo, y un reintento sin clave produciría una segunda factura autorizada del mismo pedido.

Situación Respuesta
Misma clave, mismo cuerpo El comprobante de la primera emisión
Misma clave, cuerpo distinto 409 IDEMPOTENCIA_CUERPO_DISTINTO
Misma clave, emisión en vuelo 409 EMISION_EN_CURSO. Reintentar más tarde

La clave debe ser la misma en todos los reintentos del mismo pedido. Se usa tal cual, sin normalizar; máximo 200 caracteres. Una clave que no llegó a emitir no se consume.


Contingencia

202 EN_CONTINGENCIA significa que el SRI no respondió.

  • Se reintenta cada 20 minutos durante 24 horas.
  • Termina en AUTORIZADO o RECHAZADO.
  • El reintento va contra el ambiente en el que empezó, aunque el emisor haya pasado a producción mientras tanto.

El estado se conoce consultando el comprobante —siempre 200— o por webhook cuando se resuelve. Pedir el RIDE o el XML mientras tanto devuelve 409 COMPROBANTE_NO_AUTORIZADO_AUN.


Consultar y descargar

Para qué Endpoint
Estado por identificador GET /v1/emisores/{emisorId}/comprobantes/{comprobanteId}
Estado por clave de acceso GET …/comprobantes/por-clave-acceso/{claveAcceso}
RIDE en PDF GET …/comprobantes/{comprobanteId}/ride
XML autorizado GET …/comprobantes/{comprobanteId}/xml

La clave de acceso es el identificador que circula fuera del sistema: va impresa en el RIDE y es por la que preguntan el contador y el SRI.

El XML autorizado lo emite el SRI y no se puede reconstruir; es el documento que el contribuyente conserva por obligación legal. El RIDE es su representación imprimible.


Conciliación

GET /v1/emisores/{emisorId}/comprobantes?desde=2026-03-01T00:00:00-05:00&hasta=2026-04-01T00:00:00-05:00

Pagina por cursor: se envía el siguienteCursor de la respuesta anterior. Las fechas llevan zona obligatoria.

Cada línea trae creadoEn además de fechaAutorizacion. Las dos hacen falta: un comprobante que estuvo en contingencia se autoriza más tarde, y su autorización puede caer fuera del rango por el que apareció. El listado no incluye motivoRechazo.

Un rango invertido o más ancho que el tope devuelve 400 RANGO_FECHAS_INVALIDO.

Huecos en la numeración

GET /v1/emisores/{emisorId}/puntos-emision/{puntoEmisionId}/secuenciales?estado=FALLIDO

Un envío que no llegó a término consume su número y no lo reutiliza. Como no produjo comprobante, esta consulta no se deriva de la anterior.


Ambientes

El ambiente —certificación o producción del SRI— es una propiedad del emisor. No hay una URL de pruebas: se integra contra el mismo servidor con un emisor de prueba.

ambiente se copia al comprobante al emitir y es inmutable, así que los comprobantes de prueba quedan marcados de forma permanente. Viaja en cada respuesta y en cada línea del listado.

POST /v1/emisores/{emisorId}/ambiente
{ "ambiente": "PRODUCCION" }

El paso a producción no tiene vuelta atrás: volver a pruebas devuelve 409 AMBIENTE_RETROCESO_NO_PERMITIDO.


Límites de uso

Hay cupos por integrador, por emisor y de emisión. Excederlos devuelve 429 LIMITE_EXCEDIDO con la cabecera Retry-After en segundos.

Esperar ese tiempo y repetir la petición con la misma clave de idempotencia la completa. Un 429 no consume número de la serie.


Webhooks

Evento Cuándo
comprobante.autorizado El SRI autorizó
comprobante.rechazado El SRI rechazó
comprobante.en_contingencia El SRI no respondió
certificado.por_expirar Quedan 30, 15 o 5 días
certificado.expirado El certificado venció

Cada entrega es un POST a la URL que nos indiques, con tres cabeceras:

Cabecera Contenido
X-Code218-Event-Id Identificador del evento, estable entre reintentos
X-Code218-Timestamp Momento de la firma
X-Code218-Signature HMAC-SHA256 sobre timestamp + "." + cuerpo

El receptor debe validar la firma y rechazar los timestamps de más de 5 minutos.

Reintentos a 1 min, 5 min, 30 min, 2 h y 6 h; se abandona a las 24 horas. Cualquier 2xx cuenta como entregado.

El orden de llegada no está garantizado: resuelve por ocurrioEn y descarta duplicados por X-Code218-Event-Id. El .p12 y su contraseña no viajan en el payload.


Errores

Formato application/problem+json (RFC 9457), con un campo codigo además de los estándar:

{
  "codigo": "CLAVE_IDEMPOTENCIA_AUSENTE",
  "type": "https://facturacion.code218.org/errores/clave-idempotencia-ausente",
  "title": "Falta la cabecera Idempotency-Key",
  "status": 400,
  "detail": "Falta la cabecera Idempotency-Key",
  "instance": "/v1/emisores/9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33/comprobantes"
}

codigo es estable y es el campo contra el que se programa. detail está redactado para personas y puede cambiar.

El campo type resuelve a la página de ese error. Catálogo completo.


Ejemplo completo

Identificadores y RUC ficticios; la forma de las respuestas es la real.

export API=https://api.facturacion.code218.org
export CREDENCIAL="fsri_a1b2c3d4_tu-secreto"

1. Emisor

curl -sS -X POST "$API/v1/emisores" \
  -H "Authorization: Bearer $CREDENCIAL" \
  -H "Content-Type: application/json" \
  -d '{
    "ruc": "1790012345001",
    "razonSocial": "ASADERO EL BUEN SABOR S.A.",
    "direccionMatriz": "Av. Amazonas 1234, Quito",
    "ambiente": "PRUEBAS",
    "obligadoContabilidad": false,
    "referenciaExterna": "negocio-0077"
  }'
{
  "emisorId": "9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33",
  "ruc": "1790012345001",
  "razonSocial": "ASADERO EL BUEN SABOR S.A.",
  "ambiente": "PRUEBAS",
  "referenciaExterna": "negocio-0077",
  "certificado": { "estado": "NO_CARGADO" },
  "puntosEmision": [],
  "puedeEmitir": false,
  "creadoEn": "2026-10-15T09:10:02-05:00"
}

2. Punto de emisión

curl -sS -X POST "$API/v1/emisores/9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33/puntos-emision" \
  -H "Authorization: Bearer $CREDENCIAL" \
  -H "Content-Type: application/json" \
  -d '{
    "establecimiento": "001",
    "puntoEmision": "001",
    "descripcion": "Caja principal",
    "referenciaExterna": "caja-principal",
    "secuencialInicial": 42
  }'
{
  "puntoEmisionId": "b1d60f38-9c45-4a2e-8f71-3e5c8a0b2947",
  "codigo": "001-001",
  "proximoSecuencial": 42,
  "activo": true
}

3. Certificado

curl -sS -X POST "$API/v1/emisores/9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33/certificado" \
  -H "Authorization: Bearer $CREDENCIAL" \
  -F "archivo=@certificado.p12" \
  -F "password=la-clave-del-p12"

4. Emitir

curl -sS -X POST "$API/v1/emisores/9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33/comprobantes" \
  -H "Authorization: Bearer $CREDENCIAL" \
  -H "Idempotency-Key: pedido-4871" \
  -H "Content-Type: application/json" \
  -d '{
    "puntoEmisionId": "b1d60f38-9c45-4a2e-8f71-3e5c8a0b2947",
    "comprador": {
      "tipoIdentificacion": "CEDULA",
      "identificacion": "1712345678",
      "razonSocial": "JUAN PEREZ MORA",
      "email": "juan@ejemplo.com",
      "direccion": "Av. Universitaria 123"
    },
    "items": [
      { "codigo": "ALM-001", "descripcion": "Almuerzo completo",
        "cantidad": 2, "precioUnitario": 5.00, "tarifaIva": "GENERAL" }
    ],
    "formaPago": "SIN_UTILIZACION_SISTEMA_FINANCIERO"
  }'

201 Created:

{
  "comprobanteId": "3c7e9a52-18d4-4f60-b8e3-7d2a5c91f046",
  "estado": "AUTORIZADO",
  "ambiente": "PRUEBAS",
  "claveAcceso": "1510202601179001234500120010010000000421234567810",
  "numeroComprobante": "001-001-000000042",
  "numeroAutorizacion": "1510202601179001234500120010010000000421234567810",
  "fechaAutorizacion": "2026-10-15T14:32:05-05:00",
  "motivoRechazo": null,
  "primerIntento": "2026-10-15T14:32:01-05:00"
}

El numeroAutorizacion coincide con la clave de acceso.

5. Descargar los documentos

BASE="$API/v1/emisores/9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33/comprobantes/3c7e9a52-18d4-4f60-b8e3-7d2a5c91f046"

curl -sS -o factura.pdf "$BASE/ride" -H "Authorization: Bearer $CREDENCIAL"
curl -sS -o factura.xml "$BASE/xml"  -H "Authorization: Bearer $CREDENCIAL"

6. Pasar a producción

curl -sS -X POST "$API/v1/emisores/9f8c2d14-5b3a-4e77-9c21-0a6f4e8b1d33/ambiente" \
  -H "Authorization: Bearer $CREDENCIAL" \
  -H "Content-Type: application/json" \
  -d '{ "ambiente": "PRODUCCION" }'

Antes de producción

  • El certificado es de una entidad acreditada y del RUC que emite.
  • Idempotency-Key lleva el id del pedido y no cambia entre reintentos.
  • Un 200 RECHAZADO no se reintenta: se corrige y se emite otro comprobante.
  • Un 202 EN_CONTINGENCIA no se reenvía.
  • El 429 espera lo que indica Retry-After.
  • El secuencialInicial de cada caja es el próximo número, no el último emitido.
  • Los webhooks validan la firma, descartan duplicados y ordenan por ocurrioEn.
  • El XML autorizado se conserva.