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
Idempotency-Key es obligatoria.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.
Tres pasos. GET /v1/emisores/{emisorId} devuelve puedeEmitir para comprobar si falta
alguno.
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.
Una caja o local, con su propia serie de numeración.
{
"establecimiento": "001",
"puntoEmision": "001",
"descripcion": "Caja principal",
"referenciaExterna": "caja-principal",
"secuencialInicial": 42
}
secuencialIniciales 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 en1sobre 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.
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
.p12autofirmado 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.
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"
}
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.
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 |
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.
202 EN_CONTINGENCIA significa que el SRI no respondió.
AUTORIZADO o RECHAZADO.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.
| 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.
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.
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.
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.
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.
| 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.
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.
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" }'
Idempotency-Key lleva el id del pedido y no cambia entre reintentos.200 RECHAZADO no se reintenta: se corrige y se emite otro comprobante.202 EN_CONTINGENCIA no se reenvía.429 espera lo que indica Retry-After.secuencialInicial de cada caja es el próximo número, no el último emitido.ocurrioEn.