Referencia de la API · Facturaencr
Bienvenido a la API de Facturación Electrónica de Costa Rica
Esta API le permite emitir comprobantes electrónicos ante el Ministerio de Hacienda desde cualquier sistema: un ERP, un punto de venta, un e-commerce o un script. Usted envía JSON plano; nosotros generamos el XML v4.4, lo validamos contra los esquemas oficiales, lo firmamos con XAdES-EPES, lo enviamos a Hacienda, guardamos la respuesta durante los cinco años que exige la ley y le avisamos por webhook cuando haya veredicto.
No necesita conocer el XSD, ni la especificación de firma digital, ni la API de TRIBU-CR. Necesita saber qué está vendiendo y a quién.
Qué cubre
Los ocho tipos de comprobante del reglamento vigente, más el Mensaje Receptor:
| Tipo | Comprobante | Endpoint |
|---|---|---|
01 |
Factura Electrónica | POST /documents/factura |
02 |
Nota de Débito | POST /documents/nota-debito |
03 |
Nota de Crédito | POST /documents/nota-credito |
04 |
Tiquete Electrónico | POST /documents/tiquete |
05 06 07 |
Mensaje Receptor (aceptación / parcial / rechazo) | POST /documents/mensaje-receptor |
08 |
Factura Electrónica de Compra | POST /documents/factura-compra |
09 |
Factura Electrónica de Exportación | POST /documents/factura-exportacion |
10 |
Recibo Electrónico de Pago | POST /documents/recibo-pago |
Cumplimiento normativo
La plataforma implementa la versión 4.4 de los comprobantes electrónicos, obligatoria desde el 1.º de septiembre de 2025:
- Reglamento de Comprobantes Electrónicos — Decreto Ejecutivo N.º 41820-H
- Resolución MH-DGT-RES-0027-2024 y Decreto N.º 44739-H
- Anexos y Estructuras v4.4 de la Dirección General de Tributación
- Esquemas XSD v4.4 oficiales publicados por la DGT
Los catálogos que aparecen en esta guía (impuestos, tarifas, condición de venta, medios de pago, referencias, exoneraciones, otros cargos) son los del anexo oficial, corregidos contra el comportamiento real del validador de Hacienda. Donde el XSD publicado y la emisión real no coinciden, esta guía documenta la emisión real y lo señala explícitamente.
Primeros pasos
Cuatro pasos desde cero hasta su primera factura aceptada.
Paso 1 — Cree su cuenta de integrador
Regístrese en el portal (/portal/signup). La cuenta queda activa de inmediato en modo
trial, con acceso completo a sandbox.
Paso 2 — Genere su API Key
Desde el portal, cree una llave. Recibirá dos valores:
X-API-Key: efk_EJEMPLO7NO7USAR7EN7PRODUCCION
X-API-Secret: efs_EjemploNoUsarEnProduccion-SuValorRealEsDistinto
El secret se muestra una sola vez. Guárdelo en su gestor de secretos: no hay forma de recuperarlo, solo de rotar la llave.
Verifique que funciona antes de seguir:
curl -X POST https://api.facturaencr.com/v2/efactura/auth/verify \
-H "X-API-Key: $EFACTURA_KEY" \
-H "X-API-Secret: $EFACTURA_SECRET"
{
"ok": true,
"integrator": { "id": "6a12352db0dcdddbf99c91f9", "name": "Su Empresa S.A.", "status": "trial", "defaultEnvironment": "sandbox" },
"apiKey": {
"keyId": "efk_EJEMPLO7NO7USAR7EN7PRODUCCION",
"alias": "produccion-erp",
"scopes": ["documents:write", "documents:read", "certificates:manage", "webhooks:manage", "account:read"],
"environment": "sandbox",
"expiresAt": null
}
}
Fíjese en apiKey.environment: ese campo decide si sus comprobantes tienen valor
fiscal. Vea Ambientes.
Paso 3 — Suba el certificado del comercio
Firmar y enviar son dos cosas distintas, y cada una pide sus propias credenciales: el
.p12 firma el comprobante, las credenciales de TRIBU-CR lo transmiten a Hacienda. Por eso
van los seis campos, todos obligatorios:
| Qué | Campo | Para qué sirve |
|---|---|---|
Certificado .p12 del BCCR |
p12 (archivo, máx 64 KB) |
firmar el comprobante |
PIN del .p12 |
p12Password |
abrir esa llave |
| Usuario de TRIBU-CR | haciendaUsername |
transmitir a Hacienda |
| Contraseña de TRIBU-CR | haciendaPassword |
transmitir a Hacienda |
| Etiqueta para reconocerlo | alias |
identificarlo en su listado |
| Datos del emisor | emisor |
cédula, actividad y ubicación del comercio |
Opcionales: environment (sandbox por defecto) y haciendaClientId.
El PIN del .p12 no es la contraseña de TRIBU-CR. Son dos secretos separados, se
confunden seguido, y mandar uno en lugar del otro falla sin decir cuál de los dos era.
curl -X POST https://api.facturaencr.com/v2/efactura/certificates \
-H "X-API-Key: $EFACTURA_KEY" \
-H "X-API-Secret: $EFACTURA_SECRET" \
-F "p12=@/ruta/certificado.p12" \
-F "p12Password=1234" \
-F "haciendaUsername=cpj-3-101-000000@comprobanteselectronicos.go.cr" \
-F "haciendaPassword=PasswordDelATV" \
-F "alias=Panadería Undamo (producción)" \
-F "environment=production" \
-F 'emisor={"tipoIdentificacion":"02","numeroIdentificacion":"3101678166","nombre":"UNDAMO DE ALAJUELA SOCIEDAD ANONIMA","codigoActividad":["1071.9"],"correoElectronico":"facturacion@undamo.cr","telefono":{"codigoPais":"506","numTelefono":"71725455"},"ubicacion":{"provincia":"2","canton":"01","distrito":"01","otrasSenas":"Guadalupe, contiguo a la pulpería"}}'
La llave privada se cifra en reposo y no se devuelve por ningún endpoint.
¿Todavía no tiene el certificado de sandbox? No lo espere para empezar. Su cuenta ya puede emitir en sandbox con el emisor de pruebas de la plataforma —
emisorLegalId: "EMISORPRUEBA"— sin subir nada. Vea Probar sin certificado.
Conviene que la ubicación coincida con el padrón.
provincia,cantonydistritodeberían ser los que el contribuyente tiene registrados en Tributación. Si no coinciden, Hacienda devuelve el aviso -37. No bloquea la emisión: medido sobre 426 rechazos, el -37 nunca fue el único motivo, y el 98 % de las veces salió en sandbox, que lo agrega de acompañante. Si lo ve, busque el OTRO código del mensaje: ese es el que rechaza.
Paso 4 — Emita
curl -X POST https://api.facturaencr.com/v2/efactura/documents/factura \
-H "X-API-Key: $EFACTURA_KEY" \
-H "X-API-Secret: $EFACTURA_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-2026-07-000123" \
-d '{
"emisorLegalId": "3101678166",
"condicionVenta": "01",
"medioPago": ["01"],
"receptor": {
"tipoIdentificacion": "01",
"numeroIdentificacion": "112340567",
"nombre": "Juan Pérez",
"correoElectronico": "juan@cliente.com"
},
"detalle": [
{
"cantidad": 1,
"unidadMedida": "Unid",
"codigoCabys": "2349002011500",
"detalle": "Pan pita 400g",
"precioUnitario": 1000,
"impuesto": [ { "codigo": "01", "codigoTarifa": "08", "tarifa": 13 } ]
}
]
}'
{
"documentId": "6a640c68a06e822633e9db71",
"clave": "50624072600310167816600100001010000000866142351111",
"consecutivo": "00100001010000000866",
"status": "queued",
"environment": "sandbox",
"estimatedReadyAt": "2026-07-25T01:07:58.112Z"
}
Listo. El envío a Hacienda sigue en segundo plano; el veredicto llega por webhook o
consultando GET /documents/{documentId}.
Autenticación
Todas las peticiones llevan dos cabeceras, ambas obligatorias:
X-API-Key: efk_...
X-API-Secret: efs_...
No se usa Authorization: Bearer. Si falta cualquiera de las dos, la respuesta es
401 unauthorized.
Scopes
Cada llave lleva permisos explícitos. Un endpoint invocado sin el scope necesario devuelve
403 forbidden_scope.
| Scope | Habilita |
|---|---|
documents:write |
Emitir comprobantes, reenviar, refrescar estado |
documents:read |
Consultar documentos, descargar XML/PDF, buscar en CABYS, consultar contribuyentes |
certificates:manage |
Subir, listar, probar y eliminar certificados |
webhooks:manage |
Crear, editar y probar endpoints de webhook |
account:read |
Consultar la cuenta y el consumo |
Cree llaves con el mínimo necesario: la del servidor de facturación no necesita
certificates:manage.
Aislamiento de datos
Cada llave solo ve los recursos de las cuentas a las que pertenece —la suya, y las que la hayan autorizado explícitamente (ver abajo)—. No existe forma de leer documentos, certificados ni webhooks de una cuenta que no lo haya autorizado; el filtro se aplica en todos los endpoints sin excepción.
Llave de desarrollador: una llave, varias cuentas
Si usted escribe un sistema y lo instala en varios comercios, no necesita una llave por comercio. La integración multicuenta se habilita cuenta por cuenta: escríbanos y se la activamos (no tiene costo). Al activarla recibe un código de 8 caracteres en su portal, en Desarrollador → Mis clientes. Cada comercio lo agrega desde su propio portal, en Usuarios → Desarrolladores, y elige con qué cédulas puede usted emitir.
Desde ese momento su llave —una sola— emite por todas esas cuentas.
Quién paga. El comercio, siempre. Cada comprobante debita la cartera de la cuenta dueña del emisor, avanza su tramo y aparece en su consumo. A usted no se le cobra nada por operar cuentas ajenas, y no necesita un plan de API propio para hacerlo.
Cómo se sabe de qué cuenta es cada llamada. Por la cédula del emisor, que ya viaja en toda emisión:
POST /documents/factura
{ "emisorLegalId": "3101000000", "receptor": { ... }, "detalle": [ ... ] }
Para llamadas sin cuerpo (consultas, subir un .p12) use la cabecera X-Emisor con la
cédula. Si dos de sus clientes declararan la misma cédula, la API responde 409 emisor_ambiguo en vez de adivinar: agregue X-Cuenta con el id de la cuenta correcta.
Adivinar ahí significaría emitir un documento fiscal en la cuenta equivocada.
| Situación | Respuesta |
|---|---|
| Ninguna cuenta lo ha agregado | 403 sin_clientes |
| La cuenta le quitó el acceso | 403 sin_permiso_en_cuenta |
| Esa cédula no es de ninguna de sus cuentas | 403 emisor_no_asignado |
| El comercio no le habilitó esa cédula | 403 emisor_fuera_de_alcance |
| El plan de ese comercio es ilimitado | 403 plan_ilimitado_sin_desarrollador |
| Dos clientes con la misma cédula | 409 emisor_ambiguo (mande X-Cuenta) |
| No dijo de qué cuenta es la llamada | 400 falta_cuenta |
Planes ilimitados. Si el plan del comercio es ilimitado, su llave no puede emitir en producción por esa cuenta: el plan cubre el volumen de ese negocio por una mensualidad fija, no el de una cartera de clientes. El comercio sí emite, desde su portal o con su propia llave de API. Sandbox no cambia: puede seguir armando y probando la integración con normalidad, porque las pruebas no consumen el plan.
El comercio manda. Puede quitarle el acceso cuando quiera y surte efecto en la llamada siguiente: su llave sigue viva para sus otros clientes. También decide qué pantallas de su cuenta puede ver usted en el portal.
Rotación
- Cree la llave nueva desde el portal.
- Despliéguela a sus servidores.
- Confirme con
POST /auth/verifyque respondeok: true. - Espere 24–48 h a que no queden peticiones en vuelo con la vieja.
- Revoque la vieja.
Si un secret se filtró, revoque primero y reponga después: un secret comprometido puede emitir comprobantes fiscales a nombre de sus comercios.
Formato de respuestas
Las respuestas exitosas devuelven el recurso directamente, sin envoltorio:
{
"documentId": "6a640c68a06e822633e9db71",
"clave": "50624072600310167816600100001010000000866142351111",
"status": "queued"
}
Los listados devuelven items más la paginación:
{
"items": [ { "documentId": "..." } ],
"page": 1,
"limit": 50,
"total": 1835
}
Errores
Todos los errores comparten forma:
{
"error": "validation_error",
"message": "Payload de Factura inválido",
"details": [
{
"path": "detalle.0",
"msg": "La exoneración excede el IVA de la línea...",
"message": "La exoneración excede el IVA de la línea..."
}
],
"requestId": "req_01HZY8Q4X2K7"
}
Programe contra error, nunca contra message. El texto de message está pensado
para humanos y puede cambiar sin aviso; el código es estable.
Tres detalles que conviene saber de entrada:
-
pathusa notación de puntos con índice numérico —detalle.0.impuesto.1, nodetalle[0].impuesto[1]. Viene vacío ("") cuando la regla que falló es transversal y no pertenece a un campo puntual. -
msgymessagetraen el mismo texto, duplicado por compatibilidad histórica. Leamessage. -
Hay dos convenciones de código de error. Las validaciones de esquema devuelven
validation_erroren minúscula condetails; las reglas de negocio devuelven un código propio en mayúscula y sindetails, por ejemplo:{ "error": "MEDIO_PAGO_MONTO_REQUERIDO", "message": "Con más de un medio de pago, cada medio debe incluir su monto (> 0)." }Su manejador debe contemplar ambas:
detailspuede no existir.
Incluya siempre el requestId cuando abra un ticket: con él ubicamos la petición exacta.
Emisión de comprobantes
El flujo asíncrono
Los POST /documents/* responden 202 Accepted en 1 a 3 segundos —el tiempo de
validar, generar el XML y firmarlo— y el envío a Hacienda sigue en segundo plano.
El veredicto de Hacienda tarda de 5 a 60 segundos más. Esa es la diferencia que hace útil el modelo asíncrono: usted no espera por Hacienda, espera por la firma.
202no significa «aceptado por Hacienda». Significa que recibimos su petición, la validamos, generamos el comprobante y lo firmamos. El veredicto fiscal llega después. No entregue mercadería ni cierre la venta contra un202: hágalo contrastatus: "accepted".
Estados
| Estado | Significado | ¿Qué hago? |
|---|---|---|
pending |
Registrado, aún sin procesar | Esperar |
queued |
En cola de emisión | Esperar |
signing |
Generando el XML y firmándolo | Esperar |
sent |
Entregado a Hacienda, sin veredicto | Esperar |
polling |
Consultando el veredicto a Hacienda | Esperar |
accepted |
Aceptado. Tiene valor fiscal | Entregar, cobrar, archivar |
rejected |
Rechazado. No tiene valor fiscal | Leer haciendaMessage, corregir y emitir uno nuevo |
Solo
acceptedyrejectedson finales. Todo lo demás significa «en vuelo». Escriba su máquina de estados con esa regla —final contra no final— y no con una lista cerrada: si aparece un estado nuevo, su código lo tratará como no final en vez de romperse.
Un rejected no se corrige: el consecutivo se consumió. Se emite un comprobante nuevo
con los datos corregidos.
Punto de venta: no bloquee la caja esperando a Hacienda
El error de diseño más común en un POS es emitir de forma síncrona y dejar al cajero —y a la fila— esperando el veredicto. Hacienda tarda entre 5 y 60 segundos, y si su conexión se cae, espera para siempre.
No hace falta. El 202 ya le devuelve la clave y el consecutivo, que es todo lo
que el tiquete físico necesita llevar impreso. El veredicto fiscal llega después y no
cambia esos dos valores.
Tiene tres formas de conseguir el número, de más rápida a más simple. Elija según cuánto pueda esperar la caja:
A · Numeración propia — 0 ms, sin llamar a nadie.
Si el emisor está en consecutivoMode: "integrator", el consecutivo lo lleva usted: ya
sabe cuál sigue sin preguntarnos nada. Imprime al instante y encola la emisión en su propio
sistema para enviarla cuando quiera.
// usted decide el número y lo manda en el payload
{ "consecutivoNumero": "0000004471", "branchCode": "002", "terminalCode": "00007", … }
// → consecutivo 00200007040000004471
Es la opción de menor latencia y la natural si viene de un sistema con numeración propia.
A cambio, usted responde por que no haya saltos ni repeticiones, incluso con varias
cajas a la vez. Se activa con PATCH /emisores/{legalId}/config.
B · Reservar por adelantado — una llamada, sin firmar.
POST /clave/reserve
→ { "clave": "…", "consecutivo": "…", "codigoSeguridad": "…", "expiresAt": "…" }
Saca el número del mismo contador que la emisión normal —así no se producen huecos ni
choques al mezclar reservadas con directas—, arma la clave de 50 completa y la guarda con
un TTL de 24 horas. No toca Hacienda, no firma y no cobra. Al emitir, mande esa
clave en el payload: el comprobante sale con ella y el contador no vuelve a avanzar.
Cada reserva se consume una sola vez; reusarla devuelve
409 reserved_clave_already_consumed.
Reserve de a uno, justo antes de usarlo — no en lote. El contador avanza al reservar, no al emitir. Un número reservado que no se use es un hueco en su numeración que después hay que justificar, y la reserva caduca a las 24 horas. Pedir cien claves al abrir caja y usar sesenta deja cuarenta huecos.
C · Emitir y usar lo que devuelve el 202 — lo más simple.
1. Cobrar
2. POST /documents/tiquete → 202 con clave y consecutivo
3. Imprimir el tiquete con esa clave y ese consecutivo
4. Despedir al cliente
5. …el webhook document.accepted llega después, en segundo plano
En cualquiera de las tres, lo que se imprime no cambia después: la clave y el consecutivo quedan fijos desde el principio y el veredicto de Hacienda no los toca.
Qué hacer si el webhook dice
rejected. La venta ya ocurrió y el tiquete ya se imprimió; eso no se deshace. Se corrige emitiendo de nuevo, y por eso conviene registrar eldocumentIdjunto a la venta en su base: es lo que le permite reconciliar al día siguiente qué se aceptó y qué no. Un rechazo en caja es raro —la mayoría son de configuración del emisor, no de la venta— pero su sistema tiene que tener una respuesta.
Anatomía de una emisión
Todos los comprobantes comparten la misma estructura de raíz.
{
// ── Quién firma. Exactamente UNO de los dos ──
"emisorLegalId": "3101678166", // cédula del comercio (recomendado)
// "certificateId": "6a4d7b00899e08b9a0d4011f",
// ── Condiciones comerciales ──
"condicionVenta": "01", // 01 = contado
"medioPago": ["01"], // 01 = efectivo
"currency": "CRC",
// ── A quién ──
"receptor": { "tipoIdentificacion": "01", "numeroIdentificacion": "112340567", "nombre": "Juan Pérez" },
// ── Qué ──
"detalle": [ /* líneas */ ],
// ── Opcionales ──
"otrosCargos": [],
"observaciones": "Pedido #4471"
}
Tres cosas que no se envían nunca:
- El emisor. Se resuelve del certificado. Enviarlo no tiene efecto.
-
Los totales del resumen.
TotalVentaNeta,TotalImpuesto,TotalComprobantey los subtotales por naturaleza los calcula la plataforma a partir de las líneas. Si los manda, se ignoran. Esto es deliberado: los totales son la causa número uno de rechazo por descuadre, y no hay ninguna razón para que los calcule usted. -
La clave y el consecutivo. Se generan solos, salvo que use reserva previa o el modo
integrator(vea Consecutivos).
emisorLegalId frente a certificateId
| Cuándo usarlo | |
|---|---|
emisorLegalId |
Casi siempre. Usa el certificado activo más reciente de esa cédula. Si el comercio renueva el .p12, su código no cambia. |
certificateId |
Cuando necesita fijar un certificado concreto — por ejemplo, un comercio con certificados de sandbox y producción cargados a la vez. |
Enviar los dos, o ninguno, es 400.
Cuál necesito
| Situación | Comprobante |
|---|---|
| Venta con cliente identificado | Factura (01) |
| Venta a consumidor final que no pide factura | Tiquete (04) |
| Devolver, anular o corregir a la baja | Nota de crédito (03) |
| Cobrar de más, intereses o un cargo omitido | Nota de débito (02) |
| Le compro a alguien no inscrito | Factura de compra (08) |
| Vendo fuera de Costa Rica | Factura de exportación (09) |
| Me pagan una factura a crédito con IVA diferido | Recibo de pago (10) |
| Responder a una factura que me emitieron | Mensaje receptor (05/06/07) |
El payload completo, campo por campo
Los ejemplos de cada endpoint son escenarios reales y compactos. Este es lo contrario: todo lo que se puede enviar en una factura, para que vea el mapa completo. Casi nada de esto es obligatorio.
{
// ── Quién firma — exactamente UNO de los dos ──────────────────────────────
"emisorLegalId": "3101678166", // cédula del comercio (recomendado)
"certificateId": "6a4d7b00…", // o el id de un certificado concreto
"environment": "production", // opcional: fuerza el ambiente del certificado
// ── Numeración — opcional; la plataforma la administra ────────────────────
"clave": "506…", // clave pre-reservada (POST /clave/reserve)
"branchCode": "001", // sucursal (default: la del emisor)
"terminalCode": "00001", // caja (default: la del emisor)
"consecutivoNumero": "0000000866", // sólo en consecutivoMode = 'integrator'
// ── Contingencia — opcional; sólo si la venta fue offline ─────────────────
"fechaEmision": "2026-07-20T09:00:00-06:00",
"situacion": "3", // 1 normal · 2 contingencia · 3 sin internet
// ── Actividad económica ───────────────────────────────────────────────────
"codigoActividad": "1071.9", // del emisor (default: su principal)
"codigoActividadReceptor": "960113", // alternativa a receptor.codigoActividad
// ── Condiciones comerciales ───────────────────────────────────────────────
"condicionVenta": "02",
"condicionVentaOtros": "Permuta de mercadería", // obligatorio si es 99
"plazoCredito": "30", // STRING, en días; si es 02
"medioPago": [{ "tipo": "01", "monto": 6000 }, { "tipo": "02", "monto": 4170 }], // opcional (salvo REP)
"currency": "USD",
"exchangeRate": 512.5, // obligatorio si currency ≠ CRC
// ── A quién ───────────────────────────────────────────────────────────────
"receptor": {
"tipoIdentificacion": "02",
"numeroIdentificacion": "3101456789",
"nombre": "Comercial XYZ S.A.",
"nombreComercial": "Comercial XYZ",
"codigoActividad": "960113",
"correoElectronico": "facturacion@comercialxyz.com",
"telefono": { "codigoPais": "506", "numTelefono": "22221111" },
"ubicacion": {
"provincia": "1", "canton": "01", "distrito": "01",
"barrio": "San Rafael", // v4.4: el NOMBRE, no el código
"otrasSenas": "Edificio Plaza, oficina 5"
},
"otrasSenasExtranjero": "…" // en vez de ubicacion, si es tipo 05
},
// ── Qué se vende ──────────────────────────────────────────────────────────
"detalle": [
{
"codigoCabys": "2341000000100", // 13 dígitos, obligatorio
"cantidad": 3,
"unidadMedida": "Unid",
"unidadMedidaComercial": "Caja 12u",
"detalle": "Pan tostado 200g",
"precioUnitario": 1200, // SIN IVA
"codigoComercial": [{ "tipo": "01", "codigo": "SKU-4471" }],
"tipoTransaccion": "01", // v4.4; default venta normal
"ivaCobradoFabrica": "01", // sólo casos de fábrica/mayorista
"descuento": [{
"montoDescuento": 1000,
"codigoDescuento": "02",
"codigoDescuentoOtro": "…", // si es 99
"naturalezaDescuento": "Promoción de temporada"
}],
"impuesto": [
{ "codigo": "01", "codigoTarifa": "08", "tarifa": 13,
"exoneracion": {
"tipoDocumento": "01",
"numeroDocumento": "AL-2026-00123",
"fechaEmision": "2026-01-10T10:00:00-06:00",
"nombreInstitucion": "01", // el CÓDIGO, no el nombre
"tarifaExonerada": 13, // los PUNTOS de tarifa que se exoneran
"articulo": 8, "inciso": 2 // inciso: 0 si el artículo no tiene incisos
}
},
{ "codigo": "04", // específico, ADEMÁS del IVA
"datosImpuestoEspecifico": {
"cantidadUnidadMedida": 350,
"porcentaje": 5,
"impuestoUnidad": 2,
"volumenUnidadConsumo": 350 // requerido para el código 05
}
}
]
// montoTotal, subtotal, baseImponible, impuestoNeto y montoTotalLinea
// se CALCULAN: no los envíe.
}
],
// ── Cargos, referencias y extras ──────────────────────────────────────────
"otrosCargos": [{
"tipoDocumento": "06", "detalle": "Impuesto de servicio",
"porcentaje": 10, "montoCargo": 1130,
"tipoIdentidadTercero": "02", // si tipoDocumento = 04
"numeroIdentidadTercero": "3101456789",
"nombreTercero": "Transportes Unidos S.A."
}],
"referencia": [{ // obligatoria en NC, ND, FEC y REP
"tipoDocumento": "01",
"numero": "506…", // clave de 50 si el tipo es electrónico
"fechaEmision": "2026-07-25T01:07:52-06:00",
"codigo": "01", // opcional, salvo en NC y ND
"razon": "Anulación por devolución total" // opcional
}],
"observaciones": "Pedido #4471",
"totalIVADevuelto": "auto", // sólo salud privada pagada con tarjeta
"pdfBase64": "JVBERi0x…" // su propio PDF para el correo
}
Lo que nunca se envía: el emisor (sale del certificado) y los totales del resumen
(TotalVentaNeta, TotalImpuesto, TotalComprobante y los subtotales por naturaleza), que
la plataforma calcula desde las líneas.
La única excepción es totalIVADevuelto, porque no se deriva de las líneas: depende de
cómo pagó el paciente. Ver IVA devuelto.
Un campo en blanco es un campo que no mandó
No hace falta que limpie el JSON antes de enviarlo. Si arma el payload desde su base de
datos, sus columnas vacías llegan como "" o null, y la API las trata igual que si la
clave no viniera:
{
"condicionVenta": "01",
"plazoCredito": "", // contado: no aplica → se ignora
"detalle": [{
"ivaCobradoFabrica": null, // no aplica → se ignora
"unidadMedidaComercial": "",
"precioUnitario": 1500,
"impuesto": [{ "codigo": "01", "codigoTarifa": "10", "tarifa": 0 }]
}]
}
Vale para null, para el string vacío y para el que solo tiene espacios. Un objeto que
queda sin nada adentro desaparece entero ("telefono": { "codigoPais": "", "numTelefono": "" }
no es un teléfono a medias), y los elementos vacíos salen de las listas.
El 0 no es un hueco. "tarifa": 0 y "montoDescuento": 0 son datos y se respetan tal
cual; lo mismo false. Solo se descarta lo que está en blanco.
Lo obligatorio sigue siendo obligatorio. Mandar "condicionVenta": "" es no mandarla:
la respuesta es 400 diciendo que falta, no que está vacía.
Lo que NO tiene que mandar
Estos campos son opcionales en el XSD v4.4 y la API no se los pide. Se documentan porque antes sí se exigían y puede que su integración los esté rellenando de más:
| Campo | Cuándo sí es obligatorio |
|---|---|
medioPago |
Solo en el Recibo Electrónico de Pago. En una venta a crédito no hay medio de pago todavía: omítalo en vez de inventar uno. No ponemos un valor por defecto — cómo le pagaron no nos consta. |
referencia[].numero |
Cuando el documento referenciado es un comprobante electrónico (01–04, 08–20); ahí va la clave completa de 50 y se valida el largo, que es lo que evita un rechazo -80 con el consecutivo quemado. Para un contrato, una nota de despacho, un procedimiento u "otros" (05, 06, 07, 99) es opcional y admite el número que traiga el papel. |
referencia[].codigo |
Solo en la Nota de Crédito, la Nota de Débito y la Factura de Compra. En una nota dice si se anula (01) o se corrige el monto (02), y de eso depende el saldo del comprobante original: no es un dato que podamos poner por usted. En el resto de comprobantes, si no lo envía se escribe 04 (Referencia a otro documento). |
referencia[].razon |
Nunca. Si no la envía se escribe "Referencia a otro documento". |
La dirección: entera o nada
ubicacion es opcional — un comprobante sin ella se acepta sin problema. Pero si la
envía, otrasSenas es obligatoria y de mínimo 5 caracteres: así lo exige el XSD, y un
<Ubicacion> sin ella se rechaza con -1 (cvc-minLength-valid … minLength '5' for type 'OtrasSenasUbicacionType').
Por eso la API la pide cuando manda el bloque, y el error se lo dice: si no tiene la
dirección exacta, omita ubicacion entera. Verificado emitiendo: sin ubicacion →
aceptado; con ubicacion y sin otrasSenas → rechazado; con las dos → aceptado.
Si el comprobante llega por otra vía (portal o punto de venta) con la ubicación a medias,
se emite sin la dirección —un comprobante válido sin un dato opcional le gana a uno
inválido— y recibe el aviso ubicacion_receptor_omitida en warnings.
⚠️ Ojo con estos dos: el XSD dice que son opcionales y Hacienda los exige igual. Los marca
minOccurs="0", pero un comprobante conInformacionReferenciasinCodigoo sinRazonse rechaza con -131 («El nodo denominado 'Codigo' es un campo obligatorio»). Verificado emitiendo. Por eso la API los rellena en vez de pedírselos: usted no los manda, pero salen en el XML. Si le importa qué dicen, mándelos — lo suyo nunca se pisa.
Los totales no se envían — y si los envía, se los verificamos
Los totales del resumen y los de cada línea los calcula la plataforma con la fórmula de Hacienda. Hay dos que sí puede mandar, y sirven para contrastar su cálculo con el nuestro:
| Campo de línea | Qué pasa si lo envía |
|---|---|
montoTotal |
Se compara con cantidad × precioUnitario. Si no coincide, 400 con los dos números. |
subtotal |
Se compara con cantidad × precioUnitario − descuentos. Igual. |
baseImponible, impuestoNeto, montoTotalLinea |
Se descartan y le llega un aviso totales_de_linea_calculados en warnings: los calcula el motor de impuestos (dependen de exoneraciones, IVA cobrado en fábrica e impuestos por unidad). No bloquean. |
Antes se aceptaban los cinco y se descartaban en silencio: usted creía haber fijado un número que nunca llegaba al XML.
El Recibo Electrónico de Pago es la excepción: su detalle no lleva cantidad ni precio unitario, así que ahí
subtotales un dato suyo y es obligatorio.
Pago mixto: la suma tiene que cuadrar
Con más de un medioPago, la suma de los montos debe igualar el total del comprobante.
Hacienda lo cruza con una tolerancia de ₡1 y rechaza con -493; por eso la API corta
antes de firmar, cuando la diferencia es mayor. Si saliera, perdería el consecutivo.
Con totalIVADevuelto el objetivo es el total ya rebajado por la devolución, y como la
devolución se prorratea según lo pagado con tarjeta, la cuenta es circular: fije el monto de
la tarjeta y despeje el otro medio contra el total final. El mensaje de error se lo dice.
Factura Electrónica · tipo 01
Para ventas a un receptor identificado. El receptor es obligatorio.
POST /documents/factura
Para servicios de salud privados pagados con tarjeta admite totalIVADevuelto. Ver
IVA devuelto.
Tiquete Electrónico · tipo 04
Para venta a consumidor final. El receptor es opcional: si el cliente no pide factura, omítalo por completo.
POST /documents/tiquete
El tiquete no da crédito fiscal al comprador. Si un cliente contribuyente pide factura después de haber recibido un tiquete, no se «convierte»: se emite una nota de crédito que anula el tiquete y luego una factura nueva.
Y si lo envía, la identificación del receptor también es opcional. El XSD marca
Identificacion como opcional dentro del receptor del Tiquete, así que basta el nombre:
"receptor": { "nombre": "CLIENTE DE CONTADO" } // sin cédula: válido en tiquete
La identificación va atada: o manda tipoIdentificacion y numeroIdentificacion, o
no manda ninguno. Uno solo produce un <Identificacion> a medias que Hacienda rechaza, así
que la API lo corta antes con un 400.
Lo mismo aplica a la Nota de Crédito, la Nota de Débito y la Factura de Exportación. En la Factura, la Factura de Compra y el Recibo de Pago la identificación sigue siendo obligatoria: ahí el XSD la exige.
Es el comprobante del consultorio: para servicios de salud privados pagados con tarjeta
lleva totalIVADevuelto, que le reembolsa al paciente el 4 % en el acto. Ver
IVA devuelto.
Nota de Crédito · tipo 03
Resta de un comprobante anterior: devoluciones, anulaciones, correcciones a la baja.
referencia es obligatoria.
POST /documents/nota-credito
Para una devolución parcial, incluya solo las líneas devueltas y use codigo: "02"
(corrige monto).
El receptor es opcional en NC/ND: una nota sobre un tiquete no lo lleva.
Si corrige un comprobante de salud privada, la nota lleva su propio totalIVADevuelto,
calculado sobre sus líneas y no sobre las del original. Ver
IVA devuelto.
Nota de Débito · tipo 02
Suma a un comprobante anterior: intereses, cargos omitidos, correcciones al alza. Misma estructura que la nota de crédito.
POST /documents/nota-debito
También admite totalIVADevuelto, con las mismas reglas que la nota de crédito.
Tipo de documento de referencia
Qué clase de documento está referenciando (referencia[].tipoDocumento). Catálogo de la
Nota 10 del Anexo v4.4:
| Código | Documento |
|---|---|
01 |
Factura electrónica |
02 |
Nota de débito electrónica |
03 |
Nota de crédito electrónica |
04 |
Tiquete electrónico |
05 |
Nota de despacho |
06 |
Contrato |
07 |
Procedimiento |
08 |
Comprobante emitido en contingencia |
09 |
Devolución de mercadería — solo en notas de crédito y débito |
10 |
Comprobante electrónico rechazado por el Ministerio de Hacienda |
11 |
Sustituye factura rechazada por el receptor del comprobante |
12 |
Sustituye factura de exportación |
13 |
Facturación mes vencido |
14 |
Comprobante aportado por contribuyente de Régimen Especial |
15 |
Sustituye una Factura Electrónica de Compra |
16 |
Comprobante de Proveedor No Domiciliado — solo en la Factura de Compra |
17 |
Nota de Crédito a Factura Electrónica de Compra |
18 |
Nota de Débito a Factura Electrónica de Compra |
19 |
Factura Electrónica de Exportación |
20 |
Recibo Electrónico de Pago |
99 |
Otros — requiere tipoDocRefOTRO |
Si integró antes de setiembre de 2026, ojo con el
19y el20. Hubo un período en que el anexo los publicaba pero ningún XSD los tenía, y Hacienda los rechazaba por esquema. Ya republicó los esquemas: ambos funcionan —verificado emitiendo una nota de crédito sobre una Factura de Exportación con19—. El01también se acepta y no pierde información (la clave de 50 lleva embebido el tipo del original), pero lo correcto hoy es el código específico.El
12(«Sustituye factura de exportación») no es el19: es para cuando el comprobante que emite sustituye la exportación por otra, no para referenciarla.⚠️ Una nota sobre una Factura de Exportación tiene que repetir la línea tal cual: mismo
codigoCabysy mismapartidaArancelaria. Si la línea usa un CAByS de mercancía y omite la partida, Hacienda rechaza con -503; si cambia el CAByS, con -509.El
13es para servicios que vencen el último día del mes (servicios públicos y similares, que se facturan en los primeros días del mes siguiente pero cuyo ingreso pertenece al mes anterior). En ese caso,fechaEmisionde la referencia lleva la fecha del período fiscal al que corresponde el ingreso.
Códigos de referencia
Por qué lo está referenciando (referencia[].codigo):
| Código | Motivo |
|---|---|
01 |
Anula Documento de Referencia |
02 |
Corrige monto |
04 |
Referencia a otro documento |
05 |
Sustituye comprobante provisional por contingencia |
06 |
Devolución de mercancía |
07 |
Sustituye comprobante electrónico |
08 |
Factura Endosada |
09 |
Nota de crédito financiera |
10 |
Nota de débito financiera |
11 |
Proveedor No Domiciliado |
12 |
Crédito por exoneración posterior a la facturación |
13 |
Anula documento de referencia por error material |
14 |
Corrige monto por error material |
15 |
Sustituye comprobante electrónico por error material |
16 |
Sustituye comprobante electrónico rechazado |
99 |
Otros — requiere codigoReferenciaOTRO |
El
17(pago a comprobante electrónico) es exclusivo del Recibo de Pago. En una nota de crédito o débito, Hacienda lo rechaza: su enumeración para las notas termina en16.El
13y el14cambian el período. A diferencia del01, el02y el06—que pesan en el mes de la nota—, una nota con código13o14refleja su efecto contable en el período del comprobante que modifica: una nota de setiembre que corrige un error material de agosto se declara en agosto.
Los nombres de 01 a 16 son los de la Nota 9 del Anexo y Estructuras v4.4 (pp. 72-73),
el catálogo vigente publicado por Hacienda.
No existe el código
03. Era "corrige monto" en la v4.3; en la v4.4 ese significado pasó al02.Los códigos
13a16ya tienen nombre oficial. Los publicó la Nota 9 del Anexo y Estructuras v4.4 (p. 73), en la bitácora de ajustes al 22/04/2026 —el mismo documento que renombró el12—. Hubo un período en que el validador de Hacienda los aceptaba y la tabla publicada llegaba solo hasta el12; esta doc los dejaba entonces sin descripción. Ya no hace falta: los cuatro están en la tabla de arriba.El código
17no es válido en notas de crédito ni de débito. Hacienda lo rechaza (cvc-enumeration-valid); pertenece únicamente al Recibo Electrónico de Pago.Y una advertencia práctica: Hacienda no valida el contenido de
InformacionReferencia. Puede referenciar una clave inexistente y el comprobante se acepta igual. La consistencia es responsabilidad suya; la plataforma reconcilia la factura original cuando la nota se acepta.
Factura Electrónica de Compra · tipo 08
Para comprarle a un proveedor no inscrito como contribuyente. Aquí los roles se
invierten: usted emite el comprobante de una compra suya, y el receptor del payload es
el vendedor.
POST /documents/factura-compra
Dos particularidades:
receptor.codigoActividades obligatorio (el del vendedor).-
referenciaes obligatoria, pero a diferencia de NC/ND,numerono tiene que ser una clave de 50: puede ser el número físico del documento del proveedor.
La FEC está excluida del envío automático de correo: el receptor tiene que firmarla, y mandarla antes genera confusión. El reenvío manual sigue disponible.
Factura Electrónica de Exportación · tipo 09
Para ventas fuera de Costa Rica. Receptor opcional; cada línea admite
partidaArancelaria de exactamente 12 dígitos.
POST /documents/factura-exportacion
Una exportación es exenta: use codigoTarifa: "10". Si envía receptor extranjero, su
dirección va en otrasSenasExtranjero, no en ubicacion.
Recibo Electrónico de Pago · tipo 10
Documenta el pago de una factura a crédito con IVA diferido. Su estructura es
reducida: no lleva codigoActividad, el receptor es obligatorio y las líneas no llevan
CABYS ni unidad de medida.
POST /documents/recibo-pago
condicionVenta solo admite 09 u 11 en el REP. Cualquier otro valor es 400.
En la referencia, codigo y razon son obligatorios de hecho —Hacienda rechaza con
-131 si faltan—, pero la API los deriva por usted: codigo → "17",
razon → "Pago a comprobante electrónico".
Mensaje Receptor · tipos 05, 06, 07
Su respuesta como receptor a una factura que le emitieron. Es lo que determina si puede acreditar el IVA de esa compra.
POST /documents/mensaje-receptor
mensaje |
Significado | Reglas |
|---|---|---|
05 |
Aceptación | detalleMensaje opcional |
06 |
Aceptación parcial | detalleMensaje obligatorio; exige montoTotalImpuesto y que impuestoAcreditar + gastoCosto = montoTotalImpuesto |
07 |
Rechazo | detalleMensaje obligatorio; montoTotalImpuesto prohibido |
condicionImpuesto (catálogo oficial v4.4, nota 18) define el destino del IVA soportado:
| Código | Condición |
|---|---|
01 |
Genera crédito IVA — se acredita en su totalidad |
02 |
Genera crédito parcial |
03 |
Bienes de capital |
04 |
Gasto corriente — no genera crédito |
05 |
Proporcionalidad (prorrata) |
Un Mensaje Receptor solo se envía una vez por comprobante. Un segundo intento devuelve
409 mr_already_sent.
Impuestos y cálculo de totales
Esta es la sección que decide si sus comprobantes se aceptan. Léala completa antes de emitir en producción.
La regla de oro
Toda línea debe declarar un impuesto de la familia IVA — códigos 01, 07 u 08 —
incluso si el producto es exento, no sujeto o de tarifa cero.
Un producto exento no es «una línea sin impuesto»: es una línea con IVA de tarifa exenta.
// Exento: SÍ lleva impuesto, con codigoTarifa 10
"impuesto": [ { "codigo": "01", "codigoTarifa": "10", "tarifa": 0 } ]
Cómo se comporta la API según lo que mande:
| Lo que envía | Qué pasa |
|---|---|
impuesto con IVA (01/07/08) |
Correcto |
impuesto con impuestos pero ninguno de la familia IVA |
400 antes de emitir. No se consume consecutivo. |
impuesto: [] |
Pasa el filtro y Hacienda lo rechaza con -1 (violación del XSD). Consecutivo consumido. |
Sin la clave impuesto |
Igual que el anterior: -1 y consecutivo consumido. |
Las dos últimas filas son una fuga real: la validación local solo revisa arrays que traen
algo. Si su código construye líneas dinámicamente, asegúrese de que nunca emita un
impuesto vacío, porque eso quema numeración.
Estructura del impuesto por línea
Una línea lleva dos cosas distintas:
- El IVA — siempre, sin excepción.
- Un impuesto adicional — opcional: combustible, licor, tabaco, cemento, selectivo.
El adicional no reemplaza al IVA: lo acompaña como otro elemento del array.
"impuesto": [
{ "codigo": "01", "codigoTarifa": "08", "tarifa": 13 }, // IVA
{ "codigo": "02", "tarifa": 10 } // ISC, ad-valorem
]
Catálogo de impuestos
| Código | Impuesto | Familia | Cómo se calcula |
|---|---|---|---|
01 |
Impuesto al Valor Agregado | IVA | Base imponible × tarifa |
02 |
Selectivo de Consumo (ISC) | ad-valorem | Subtotal × tarifa |
03 |
Único a los Combustibles | por unidad · asumido | ₡ por litro |
04 |
Específico de Bebidas Alcohólicas | por unidad · asumido | ₡ por mL de alcohol absoluto |
05 |
Bebidas envasadas sin alcohol y jabones | por unidad · asumido | ₡ por unidad de consumo |
06 |
Productos de Tabaco | por unidad · se cobra | ₡ por unidad |
07 |
IVA (cálculo especial) | IVA | Base imponible × tarifa |
08 |
IVA Régimen de Bienes Usados (Factor) | IVA | Requiere factorIVA |
12 |
Específico al Cemento | por unidad · asumido | ₡ por saco · tarifa obligatoria = 5 |
99 |
Otros | ad-valorem | Requiere codigoTarifaOtro |
No existen los códigos
09,10ni11. Aparecen en documentación de terceros pero no en el catálogo vigente.
Códigos de tarifa del IVA
Obligatorio para la familia IVA (01, 07, 08).
| Código | Tarifa | Nota |
|---|---|---|
01 |
0 % | Artículo 32, num. 1, RLIVA |
02 |
1 % | Reducida |
03 |
2 % | Reducida |
04 |
4 % | Reducida |
05 |
0 % | Transitorio — solo NC/ND |
06 |
4 % | Transitorio — solo NC/ND |
07 |
8 % | Transitorio — solo NC/ND |
08 |
13 % | General |
09 |
0,5 % | Reducida |
10 |
— | Exenta |
11 |
0 % | Sin derecho a crédito (no sujeta) |
Las tarifas transitorias
05,06y07solo valen en notas de crédito y débito. En una factura o tiquete son rechazo seguro de Hacienda (-505). La API las bloquea antes con400, así que no gasta consecutivo — pero conviene saber por qué: para un 8 % en una factura no hay código vigente.
Base imponible
El IVA no se calcula sobre el subtotal, sino sobre la base imponible:
BaseImponible = SubTotal
+ ISC (02)
+ Bebidas Alcohólicas (04)
+ Bebidas Envasadas (05)
+ Cemento (12)
El combustible (03) y el tabaco (06) NO engrosan la base. Equivocarse acá produce
un rechazo -454.
Cobrado frente a asumido
Este es el concepto que más sorprende y el que más rechazos causa.
Los impuestos por unidad de códigos 03, 04, 05 y 12 ya los pagó el productor o
el importador y viajan dentro del precio. El comercio no se los vuelve a cobrar al
cliente: se declaran para trazabilidad, pero no suman al total ni aparecen en el
desglose de impuestos.
El tabaco (06) es la excepción: sí se cobra y sí suma al total, aunque también sea
por unidad.
| Código | Por unidad | ¿Suma al total? |
|---|---|---|
03 combustible |
Sí | No — asumido |
04 alcohol |
Sí | No — asumido |
05 bebidas envasadas |
Sí | No — asumido |
06 tabaco |
Sí | Sí — se cobra |
12 cemento |
Sí | No — asumido |
Fórmulas de los impuestos por unidad
Se declaran en datosImpuestoEspecifico:
| Código | Fórmula |
|---|---|
03 combustible |
cantidadUnidadMedida × impuestoUnidad |
04 alcohol |
proporcion = cantidadUnidadMedida × porcentaje / 100, luego cantidad × proporcion × impuestoUnidad |
05 bebidas envasadas |
cantidad × cantidadUnidadMedida × (impuestoUnidad / volumenUnidadConsumo) |
06 tabaco |
cantidad × cantidadUnidadMedida × impuestoUnidad |
12 cemento |
cantidadUnidadMedida × impuestoUnidad |
Sobre el código 04: grava el alcohol absoluto, no el líquido. Una botella de 750 mL a
12° tiene 90 mL de alcohol puro (750 × 12 / 100). Sí se divide entre 100; no hacerlo
produce -471 o -473. Además el emisor debe tener cargado su registroFiscal8707
(Ley 8707).
Sobre el código 05: exige volumenUnidadConsumo (los mL del envase). Sin él, -459.
Ejemplos resueltos
1 · Servicio gravado al 13 %
{
"cantidad": 1,
"unidadMedida": "Sp",
"codigoCabys": "8511100000000",
"detalle": "Servicio de reclutamiento ejecutivo",
"precioUnitario": 100000,
"impuesto": [ { "codigo": "01", "codigoTarifa": "08", "tarifa": 13 } ]
}
Subtotal 100 000 → IVA 13 000 → Total línea 113 000
2 · Mercancía gravada al 13 %
{
"cantidad": 3,
"unidadMedida": "Unid",
"codigoCabys": "2341000000100",
"detalle": "Pan tostado 200g",
"precioUnitario": 1200,
"impuesto": [ { "codigo": "01", "codigoTarifa": "08", "tarifa": 13 } ]
}
Subtotal 3 600 → IVA 468 → Total línea 4 068
3 · Canasta básica al 1 %
{
"cantidad": 2,
"unidadMedida": "Unid",
"codigoCabys": "2349002011500",
"detalle": "Pan pita 400g",
"precioUnitario": 1000,
"impuesto": [ { "codigo": "01", "codigoTarifa": "02", "tarifa": 1 } ]
}
Subtotal 2 000 → IVA 20 → Total línea 2 020
La tarifa correcta de cada producto la sugiere el propio catálogo CABYS: el buscador
devuelve el campo impuesto con la tarifa asociada.
4 · Producto exento
{
"cantidad": 1,
"unidadMedida": "Unid",
"codigoCabys": "2349002020800",
"detalle": "Producto exento",
"precioUnitario": 5000,
"impuesto": [ { "codigo": "01", "codigoTarifa": "10", "tarifa": 0 } ]
}
Subtotal 5 000 → IVA 0 → Total línea 5 000. El monto se declara en TotalExento, no en
TotalGravado.
5 · Línea con descuento
{
"cantidad": 1,
"unidadMedida": "Unid",
"codigoCabys": "2341000000100",
"detalle": "Pan tostado 200g",
"precioUnitario": 10000,
"descuento": [ { "montoDescuento": 1000, "codigoDescuento": "02" } ],
"impuesto": [ { "codigo": "01", "codigoTarifa": "08", "tarifa": 13 } ]
}
El descuento baja la base: 10 000 − 1 000 = 9 000 → IVA 1 170 → Total línea 10 170.
Los códigos de descuento
01(Regalía) y03(Bonificación) exigen que el descuento sea el 100 % de la línea —el bien se entrega gratis— y el emisor asume el IVA. Un «descuento parcial con código 01» no existe: use02(Promoción) o el que corresponda.
6 · Bebida alcohólica
Cerveza de 350 mL a 5°, dos unidades, con impuesto específico de ₡2 por mL de alcohol absoluto:
{
"cantidad": 2,
"unidadMedida": "Unid",
"codigoCabys": "2431001000000",
"detalle": "Cerveza 350ml",
"precioUnitario": 1500,
"impuesto": [
{ "codigo": "01", "codigoTarifa": "08", "tarifa": 13 },
{
"codigo": "04",
"datosImpuestoEspecifico": {
"cantidadUnidadMedida": 350,
"porcentaje": 5,
"impuestoUnidad": 2
}
}
]
}
proporcion = 350 × 5 / 100 = 17,5 mL → monto = 2 × 17,5 × 2 = ₡70. Ese específico
engrosa la base del IVA pero no se le cobra al cliente.
Exoneración
TarifaExonerada no es «el porcentaje de la exoneración»: es la tarifa que se está
exonerando. Hacienda valida:
MontoExoneracion = BaseImponible × TarifaExonerada / 100
Exonerar el 100 % de un IVA del 13 % sobre ₡10 000 son 13 de tarifa y ₡1 300 de monto — no 100. Anexo v4.4, pág. 46: «Debe de indicarse los puntos de la tarifa otorgado de exoneración (…) la tarifa del 13 % se debe reflejar como 13, la del 1 % como 1, o bien la del 0.5 % como 0.5».
Diga cuánto se exonera con UNO de estos tres (al menos uno, mayor que 0):
| campo | qué es |
|---|---|
tarifaExonerada |
recomendado — los puntos de tarifa: 13, 1, 0.5 |
porcentajeExoneracion |
histórico y ambiguo (ver abajo) |
montoExoneracion |
el monto de impuesto, si ya lo tiene calculado |
montoExoneracion ya no es obligatorio: si no lo manda, lo calculamos. Si lo manda, se
respeta el suyo. Si no cuadra la igualdad de arriba: -190.
La API además rechaza con 400 cualquier exoneración que supere el IVA que la línea
genera, con un mensaje que le dice el máximo exacto:
La exoneración excede el IVA de la línea: montoExoneracion=9999 pero el IVA (13%)
sobre la base 1000.00 es 130.00 como máximo.
Una exoneración parcial parte la base: exonerar la mitad de un IVA del 13 % sobre ₡10 000 declara ₡5 000 gravados y ₡5 000 exonerados.
IVA devuelto (servicios de salud privados)
Los servicios de salud privados van con tarifa reducida del 4 % (art. 11, inc. 1, subinc. b de la Ley del IVA). Y el art. 39 del Reglamento (Decreto 41779-H) obliga a reembolsar ese mismo 4 % en el acto cuando el paciente paga con tarjeta de crédito o débito: el paciente termina pagando sólo la base.
El impuesto se factura normal en la línea y la devolución se resta en el resumen:
TotalComprobante = TotalVentaNeta + TotalImpuesto + TotalOtrosCargos − TotalIVADevuelto
Se activa con un campo en la raíz del payload:
{
"condicionVenta": "01",
"medioPago": ["02"], // 02 = tarjeta: es lo que dispara la devolución
"totalIVADevuelto": "auto", // o el monto exacto: 1600
"detalle": [{
"codigoCabys": "9310100010100", // consulta médica → 4 %
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Consulta de medicina general",
"precioUnitario": 40000,
"impuesto": [{ "codigo": "01", "codigoTarifa": "04", "tarifa": 4 }]
}]
}
Resultado: base ₡40 000 · IVA 4 % ₡1 600 · devuelto −₡1 600 · total a pagar ₡40 000.
| Valor | Qué hace |
|---|---|
"auto" |
Calcula el monto que Hacienda espera. Es lo que debe usar. |
| un número | Ese monto exacto. Sólo si necesita anular nuestro cálculo — se valida igual. |
| ausente | No hay devolución. El 4 % se cobra completo. |
Disponible en Factura, Tiquete, Nota de Crédito y Nota de Débito. Los XSD de la Factura
de Compra, la de Exportación y el Recibo Electrónico de Pago no tienen el campo:
enviarlo ahí devuelve 400.
Hacienda RECALCULA el monto
No se limita a aceptar lo que usted mande: recalcula la devolución y la compara. Rechaza
con -496 en dos variantes:
-
«El Total IVA Devuelto no debe de estar presente si no existe Medio de Pago con Tarjeta» — sin un
medioPago"02", el comprobante se cae. -
«El Total IVA Devuelto no concuerda con la suma de impuestos sobre Servicios Médicos pagados con Tarjeta (X)» — con pago mixto, Hacienda prorratea:
TotalIVADevuelto = IVA_al_4% × (Σ montos con medioPago "02") ÷ TotalVentaNeta
Con un solo medio de pago el XML no lleva TotalMedioPago, no hay nada que prorratear,
y va el 4 % completo.
⚠️ En pago mixto hay una vuelta de tuerca: el total del comprobante baja por la devolución, y la devolución depende de cuánto se pagó con tarjeta. Fije el monto de la tarjeta y despeje el resto:
IVA al 4 % = 1 600 · TotalVentaNeta = 50 000 · tarjeta = 41 300
devolución = 1 600 × 41 300 ÷ 50 000 = 1 321,60
total = 50 000 + 2 900 − 1 321,60 = 51 578,40
efectivo = 51 578,40 − 41 300 = 10 278,40
Mande "auto" y esa cuenta la hacemos nosotros; usted sólo tiene que cuadrar los montos
de medioPago contra el total resultante (si no cuadran, el 400 le dice cuál es).
Todo lo que Hacienda valida, lo cortamos antes
Un comprobante rechazado no se corrige ni se reenvía: hay que emitirlo de nuevo con
otro consecutivo, y el rechazado le queda registrado al contribuyente. Por eso estas
comprobaciones son 400 antes de gastar un número de la secuencia:
| Código | Cuándo |
|---|---|
IVA_DEVUELTO_SIN_TARJETA |
Ningún medioPago es "02". En pago mixto basta con que uno lo sea. |
IVA_DEVUELTO_MONTO_DISTINTO |
El monto no es el que Hacienda recalcula. El mensaje trae el esperado. |
IVA_DEVUELTO_EXCEDE_IVA_AL_4 |
Se devuelve más IVA del cobrado al 4 % (típico: contar las líneas al 13 %). |
IVA_DEVUELTO_SIN_LINEAS_AL_4 |
"auto" no encontró nada al 4 %: CAByS o tarifa equivocados. |
Que los montos por medio no sumen el total ya rebajado por la devolución no bloquea:
sale como aviso medio_pago_suma_distinta (ver Pago mixto).
La condición de venta es la única que queda como aviso, no como bloqueo: Hacienda
acepta la devolución en un comprobante a crédito, así que la emisión sigue y la respuesta
trae un warning. La regla ahí es sólo legal — el art. 39 devuelve en el acto del pago:
{
"documentId": "6a8c...",
"status": "queued",
"warnings": [
{
"code": "iva_devuelto_a_credito",
"message": "Se está devolviendo el IVA en un comprobante a CRÉDITO (condicionVenta 02). La devolución del art. 39 es en el acto del pago con tarjeta; en una venta a crédito no procede hasta que el pago se realice."
}
]
}
Régimen simplificado
Un emisor de régimen simplificado no traslada IVA, así que no hay IVA del 4 % que devolver y el comprobante queda mal emitido. Hacienda no valida esto: le sale aceptado.
Por defecto no lo bloqueamos — se emite exactamente lo que usted envía. Si activa
«Exención automática para Régimen Simplificado» en su cuenta, enviar totalIVADevuelto
desde un emisor simplificado devuelve 422 · iva_devuelto_no_aplica_simplificado.
Moneda extranjera
La devolución viaja en la moneda del documento, no convertida a colones. Una consulta
de US$ 80 al 4 % devuelve 3.20000 y el TipoCambio del resumen no la toca.
Notas de crédito y débito
La nota calcula su devolución sobre sus propias líneas, no sobre las del documento que corrige. Una NC que sólo reversa la consulta médica devuelve el IVA de esa consulta, aunque la factura original tuviera más líneas y una devolución mayor.
⚠️ El prefijo del CAByS no es la regla. Casi todo 931… es salud humana al 4 %, pero
hay excepciones en ambas direcciones:
-
Dentro de
931…hay códigos al 13 %: internamiento, quirófano, UCI, sala de recuperación, sala de observación, consultorio con equipo especializado, ambulancias, autopsia y certificado de defunción. -
Fuera de la salud humana también hay tarifa del 4 %: veterinaria (
835…) y tiquete aéreo (641…,642…). Ahí no hay devolución.
Lea la tarifa del catálogo CAByS código por código —
GET /catalogs/cabys la devuelve con su tarifa— en vez de deducirla del prefijo. Y
recuerde que la actividad económica del emisor debe ser de salud: división 86 de CIIU 4
(8610.0–8690.9) desde la migración del 6 de octubre de 2025.
Régimen del emisor
Cada certificado trae un emisor.regimen detectado automáticamente contra el registro de
Hacienda:
| Régimen | Qué significa |
|---|---|
tradicional |
Traslada IVA con normalidad. |
simplificado |
Por disposición de Hacienda no traslada IVA. |
Por defecto la plataforma no toca sus números. Si usted envía IVA para un emisor de régimen simplificado, el comprobante sale con ese IVA. Hacienda lo acepta —el XML v4.4 no lleva ningún campo de régimen y no cruza la cédula contra el padrón—, así que el cálculo y la responsabilidad fiscal son suyos.
Exención automática (opcional)
Si prefiere que nosotros lo resolvamos, puede activar Exención automática desde su cuenta del portal, en Perfil → Emisión. Con ella encendida, a los emisores en régimen simplificado les reescribimos cada línea a exenta:
// Lo que envía // Lo que se emite
{ "codigo": "01", <CodigoTarifaIVA>10</CodigoTarifaIVA>
"codigoTarifa": "08", <Tarifa>0.00</Tarifa>
"tarifa": 13 } <Monto>0.00000</Monto>
<TotalExento>1000.00000</TotalExento>
Y siempre se lo avisamos en warnings de la respuesta de emisión, con el número de
líneas que tocamos:
{
"documentId": "6a7f...",
"status": "queued",
"warnings": [
{
"code": "simplificado_exento_forzado",
"message": "El emisor está en Régimen Simplificado y tu cuenta tiene activada la exención automática: se reescribieron 2 líneas a EXENTA (impuesto 0).",
"lineas": 2
}
]
}
Si concilia lo que envió contra lo que devolvió la API, revise
warnings. Con la exención automática encendida, para un emisor simplificado esperetotalImpuesto: 0.
Los impuestos que no son IVA nunca se tocan: el impuesto específico a combustibles, bebidas alcohólicas, tabaco o cemento se cobra por unidad y un emisor simplificado los sigue debiendo.
Exoneración
Un emisor de régimen simplificado no puede emitir con exoneración, tenga o no
encendida la exención automática: la API responde 422 exoneracion_no_aplica_simplificado.
No traslada IVA, así que no hay impuesto que exonerar.
Consultarlo antes de tener el certificado
emisor.regimen existe una vez que el certificado está cargado. Para averiguarlo antes
—al dar de alta a un comercio, o para saber qué esperar de un receptor— hay un endpoint
directo contra el registro de Hacienda:
curl "https://api.facturaencr.com/v2/efactura/contribuyentes/3101912877/regimen" \
-H "X-API-Key: sk_live_..." -H "X-API-Secret: ..."
{
"identificacion": "3101912877",
"encontrado": true,
"contribuyente": true,
"regimen": { "codigo": 2, "clave": "simplificado", "descripcion": "Régimen simplificado",
"simplificado": true, "trasladaIva": false }
}
Sin /regimen el mismo endpoint devuelve además la inscripción, la morosidad, la
administración tributaria y las actividades económicas. Lo importante de esa respuesta:
| Campo | Para qué |
|---|---|
regimen.trasladaIva |
false = simplificado: prepare su conciliación para totalImpuesto: 0. |
contribuyente |
false = no inscrito: Hacienda le rechaza lo que emita, y como receptor no acepta Factura Electrónica → emítale un Tiquete (04). |
encontrado |
false = la identificación no está en el registro. Casi siempre es una cédula mal escrita. |
situacion.moroso / omiso |
Informativos. No impiden facturar. |
Tres advertencias sobre cómo leerlo:
-
contribuyente: falseconencontrado: trueno es lo mismo que conencontrado: false. El primero existe pero no está inscrito (el motivo textual de Hacienda viene ensituacion.mensaje); el segundo no existe. contribuyente: nullsignifica no verificable ahora, no "no". No lo trate como un no.-
Si Hacienda no responde, el endpoint devuelve 503, nunca un
false. Un falso negativo lo llevaría a emitir el documento equivocado.
La consulta no consume cuota ni se cobra: solo va acotada por el rate limit. Programe
contra regimen.clave y situacion.estado (estables), no contra codigo ni
descripcion (los define Hacienda y puede cambiarlos).
La respuesta se cachea 6 horas. Como el régimen y la inscripción sí cambian, agregue
?refresh=true cuando necesite el dato del momento. Tenga presente que el registro de
Hacienda tarda en reflejar los cambios hechos en TRIBU-CR: refresh fuerza nuestra
reconsulta, no acelera a Hacienda. Mientras el registro siga diciendo simplificado, la
plataforma sigue emitiendo exento — que es lo fiscalmente correcto hasta que el cambio
rija.
Consulta y descargas
Los endpoints y la respuesta completa están en Emisión de comprobantes, más abajo. Acá va solo lo que hay que decidir.
Qué archivar
El PDF no es el documento legal. El comprobante fiscal es el XML firmado; el PDF es una representación gráfica de cortesía que se genera al vuelo y no se almacena. Archive el XML —y la respuesta de Hacienda— por su cuenta, aunque nosotros los custodiemos cinco años.
Polling, si no puede usar webhooks
Espere entre 5 y 60 segundos y aplique backoff. No consulte en bucle cerrado:
+10 s → +20 s → +40 s → +60 s → cada 5 min hasta 30 min
Si a los 30 minutos sigue en sent, use POST /documents/{id}/refresh para forzar una
reconsulta contra Hacienda.
Más allá de emitir
Emitir el XML es el mínimo. Estas piezas son las que normalmente hay que construir aparte —y mantener— cuando se integra facturación electrónica; acá vienen resueltas.
Correo al receptor, sin montar un servidor de email
Costa Rica no obliga a enviarle el comprobante al receptor: Hacienda ya custodia el XML. Pero en la práctica todos lo esperan. Montarlo por su cuenta significa SMTP, plantillas, reputación de dominio, rebotes, reintentos y anti-spam.
El add-on de email lo hace por usted: al aceptarse el comprobante, se envía automáticamente al correo del receptor con el XML firmado, la respuesta de Hacienda y el PDF adjuntos. Se activa una vez, a nivel de cuenta, desde el portal (Facturación → Envío de comprobante por email).
Para que un correo salga se necesitan cuatro cosas: add-on activo, cuenta no suspendida, documento aceptado y correo de receptor en el XML.
La Factura de Compra (08) está excluida del envío automático: la firma el receptor, y mandarla antes genera confusión. El reenvío manual sí funciona.
Reenvío idempotente
POST /documents/{id}/reenviar-email
El cliente dice que no le llegó. Usted reenvía. El cliente vuelve a escribir. Usted reenvía otra vez. En un servicio facturado por envío, eso es dinero.
Por eso el reenvío deduplica solo: si ya se envió ese comprobante a ese correo en
los últimos 5 minutos, responde 200 con deduped: true, no reenvía y no cobra.
{ "id": "...", "status": "sent", "recipientEmail": "juan@cliente.com",
"deduped": true, "billed": false, "lastSentAt": "2026-07-25T01:10:00.000Z" }
La ventana es por par (documento, correo): enviar a otra dirección nunca se deduplica.
Si de verdad quiere reenviar dentro de la ventana —y cobrarlo—, mande force: true.
También acepta email para mandarlo a una dirección alterna sin tocar el XML.
El PDF: nuestro, o el suyo
Cada comprobante tiene un PDF de cortesía que se genera al vuelo desde el XML firmado. No se almacena, así que nunca queda desincronizado del documento legal.
Tiene dos formas de personalizarlo:
1 · Branding por emisor. Configure una vez el aspecto del PDF de cada comercio:
PUT /emisores/{legalId}/branding
{
"logo": "data:image/png;base64,iVBORw0KGgo...",
"accentColor": "#7C3AED",
"paymentInfo": "Cuenta IBAN CR05015202001026284066 (BAC, colones)\nSINPE Móvil 8888-8888",
"thankYouMessage": "¡Gracias por su compra!",
"contactPhone": "2222-1111",
"contactEmail": "facturacion@comercialxyz.com",
"contactAddress": "San José, Catedral, Edificio Plaza, oficina 5",
"website": "https://comercialxyz.com"
}
Es un merge parcial: mande solo lo que quiere cambiar; un string vacío ("") limpia el
campo. El logo se guarda en almacenamiento de objetos y se inyecta al generar el PDF —las
respuestas devuelven hasLogo, no el binario, así que listar emisores sigue siendo barato.
Esto importa si usted es un integrador con muchos comercios: cada uno recibe su propio PDF con su marca, sin que usted genere PDFs.
2 · Su propio PDF. Si ya tiene una representación gráfica que le gusta, mándela en
pdfBase64 al emitir (o al reenviar) y esa se adjunta al correo en lugar de la nuestra.
Debe empezar con %PDF- y pesar 15 MB o menos.
Saldo del comprobante ya reconciliado
Cuando una nota de crédito o débito que referencia un comprobante es aceptada, el documento original se actualiza solo:
"credito": {
"acreditado": 0, "impuestoAcreditado": 0, "debitado": 0,
"saldo": 1130, "anulado": false, "anuladoEn": null, "notas": []
}
No tiene que cruzar notas contra facturas por su cuenta: saldo y anulado ya vienen
calculados, con la lista de notas que afectaron el documento.
La respuesta del receptor, sin buzón propio
Cuando alguien acepta o rechaza un comprobante suyo, lo sabrá:
receiverMRStatus—pending,received,no_responseonot_applicable.GET /documents/{id}/receiver-mr— la respuesta estructurada.GET /documents/{id}/receiver-mr/xml— el XML firmado del receptor.- Webhook
document.receiver_mr_receiveden cuanto llega.
Eso le dice si su cliente acreditó el IVA de la factura que usted emitió.
Clave por adelantado
POST /clave/reserve
Devuelve una clave y un consecutivo antes de emitir. Sirve cuando necesita imprimir
o mostrar el número antes de tener el comprobante —tiquetes de caja, órdenes—. Después
envía esa clave en el payload y el comprobante usa la reservada.
Numeración propia, si ya la tiene
Por defecto la plataforma numera de forma atómica y usted se olvida del tema. Pero si viene
de un sistema con su propia numeración y necesita preservarla, active el modo integrator:
PATCH /emisores/{legalId}/config { "consecutivoMode": "integrator" }
A partir de ahí el contador es suyo: nosotros dejamos de administrarlo, y repetir un
número devuelve 409.
El punto de venta que imprime la clave antes de transmitir
Un POS cobra, imprime el tiquete y después transmite — a veces horas después, si se cayó el internet. Para imprimir necesita la clave en ese momento. Hay tres caminos:
| Cómo | Cuándo | Qué envía |
|---|---|---|
| La API numera | Caso normal | Nada: omita clave y claveIntegrador |
claveIntegrador |
El POS ya compuso e imprimió la clave | La clave completa de 50 |
clave |
Reservó por adelantado con POST /clave/reserve |
La clave reservada |
clave y claveIntegrador son excluyentes: mandar los dos es ambiguo, no redundante.
claveIntegrador: la clave que usted imprimió
Los dos últimos caminos ya existían en piezas (consecutivoNumero + codigoSeguridad +
branchCode + terminalCode + situacion), y nosotros re-componíamos la clave a partir
de ellas. El problema es que así la clave se compone dos veces: usted para imprimirla y
nosotros para firmarla. Si las dos versiones divergen, el papel que se llevó el cliente
dice una clave y el XML legal dice otra, y nada lo delata.
Con claveIntegrador hay una sola versión: firmamos exactamente la que usted imprimió.
{
"emisorLegalId": "3101737993",
"codigoActividad": "4711.2",
// La clave que el POS compuso e imprimió en el tiquete
"claveIntegrador": "50627082600310173799300100009040000000015342240125",
"fechaEmision": "2026-08-27T11:45:00-06:00", // la de la OPERACIÓN, con hora
"condicionVenta": "01",
"currency": "CRC",
"exchangeRate": 1,
"receptor": {
"tipoIdentificacion": "01",
"numeroIdentificacion": "112340567",
"nombre": "Juan Pérez Mora",
"correoElectronico": "juan@ejemplo.cr"
},
"detalle": [
{ "numeroLinea": 1, "codigoCabys": "0125300020500",
"cantidad": 0.5, "unidadMedida": "Kg",
"detalle": "CEBOLLA BLANCA A GRANEL", "precioUnitario": 346.55,
"impuesto": [{ "codigo": "01", "codigoTarifa": "02", "tarifa": 1 }] },
{ "numeroLinea": 2, "codigoCabys": "2316100000200",
"cantidad": 2, "unidadMedida": "Unid",
"detalle": "ARROZ BLANCO 1 kg", "precioUnitario": 1850,
"descuento": [{ "montoDescuento": 200, "codigoDescuento": "02" }],
"impuesto": [{ "codigo": "01", "codigoTarifa": "08", "tarifa": 13 }] }
],
"medioPago": [
{ "tipo": "02", "monto": 4000 },
{ "tipo": "01", "monto": 130.00775 }
],
"observaciones": "Venta offline #4471 - clave impresa en el tiquete"
}
La situacion no va en el payload: sale de la clave (dígito 42, acá 3 = sin internet).
fechaEmision sí, porque la clave sólo lleva el día y el XML necesita la hora.
Y el mismo tiquete por el otro camino, mandando las piezas en vez de la clave. Aquí la compone la API — sirve cuando su numeración es propia pero no necesita imprimir la clave antes de transmitir:
{
"emisorLegalId": "3101737993",
"codigoActividad": "4711.2",
// Las piezas, en vez de la clave entera
"branchCode": "001",
"terminalCode": "00009",
"consecutivoNumero": "0000000016",
"codigoSeguridad": "42240125",
"situacion": "3",
"fechaEmision": "2026-08-27T11:45:00-06:00",
"condicionVenta": "01",
"currency": "CRC",
"exchangeRate": 1,
"receptor": {
"tipoIdentificacion": "01",
"numeroIdentificacion": "112340567",
"nombre": "Juan Pérez Mora",
"correoElectronico": "juan@ejemplo.cr"
},
"detalle": [
{ "numeroLinea": 1, "codigoCabys": "0125300020500",
"cantidad": 0.5, "unidadMedida": "Kg",
"detalle": "CEBOLLA BLANCA A GRANEL", "precioUnitario": 346.55,
"impuesto": [{ "codigo": "01", "codigoTarifa": "02", "tarifa": 1 }] },
{ "numeroLinea": 2, "codigoCabys": "2316100000200",
"cantidad": 2, "unidadMedida": "Unid",
"detalle": "ARROZ BLANCO 1 kg", "precioUnitario": 1850,
"descuento": [{ "montoDescuento": 200, "codigoDescuento": "02" }],
"impuesto": [{ "codigo": "01", "codigoTarifa": "08", "tarifa": 13 }] }
],
"medioPago": [
{ "tipo": "02", "monto": 4000 },
{ "tipo": "01", "monto": 130.00775 }
],
"observaciones": "Venta offline #4472"
}
Los dos producen exactamente el mismo comprobante. Ambos fueron emitidos y aceptados por Hacienda, y sus totales coinciden al último decimal:
TotalVenta 3873.27500 ← 173.275 (cebolla) + 3700 (arroz)
TotalDescuentos 200.00000
TotalVentaNeta 3673.27500
TotalImpuesto 456.73275 ← 1.73275 al 1% + 455 al 13%
TotalComprobante 4130.00775 ← y la suma de los medioPago da exactamente esto
Con pago mixto, Σ
medioPago[].montodebe dar elTotalComprobanteexacto. Acá son ₡4 000 con tarjeta más ₡130,00775 en efectivo. Si no cuadra, la API responde400diciendo cuál es el total que espera.
Cómo se arma la clave:
506 + ddmmaa + cédula(12) + consecutivo(20) + situación(1) + códigoSeguridad(8)
506 270826 003101737993 00100009040000000011 3 42240125
y el consecutivo de 20 es:
branchCode(3) + terminalCode(5) + tipoDoc(2) + consecutivoNumero(10)
001 00009 04 0000000011
Tipo de documento: 01 factura · 02 nota de débito · 03 nota de crédito · 04 tiquete
· 08 factura de compra · 09 exportación · 10 recibo de pago.
⚠️ Las dos trampas que producen una clave válida pero equivocada:
La fecha va en hora de Costa Rica, no en UTC. Son los dígitos 4 a 9. Un servidor en UTC que la calcule a partir de las 18:00 hora tica genera el día siguiente.
La cédula se rellena a 12 con ceros a la izquierda. 3101737993 son 10 dígitos y en la
clave va 003101737993.
Ambas se validan antes de firmar. La API rechaza con 400 si la cédula de la clave no es
la del emisor que firma (clave_integrador_cedula_mismatch), si el día no cuadra con
fechaEmision (clave_integrador_fecha_mismatch), si el tipo de documento no corresponde
al endpoint (clave_tipo_documento_mismatch) o si el emisor no está en modo integrator
(clave_integrador_requiere_modo_integrator). Ninguno de esos gasta consecutivo.
La situación se toma de la clave (dígito 42), no del payload: es la que ya se imprimió. Si el payload dice otra cosa, se emite con la de la clave y la respuesta trae un aviso.
Custodia de cinco años
Los XML firmados y las respuestas de Hacienda se conservan el plazo legal completo. Cada
documento expone su retentionUntil. Aun así, archive sus propios XML: son la prueba
fiscal de sus operaciones.
Catálogos oficiales v4.4
Los catálogos de referencia (
referencia[].tipoDocumentoyreferencia[].codigo) no están en esta sección. Viven arriba, dentro de Emisión de comprobantes, junto a la Nota de Crédito y la Nota de Débito que son quienes los usan: busque "Tipo de documento de referencia" y "Códigos de referencia".
Condición de venta
| Código | Condición |
|---|---|
01 |
Contado |
02 |
Crédito — exige plazoCredito |
03 |
Consignación |
04 |
Apartado |
05 |
Arrendamiento con opción de compra |
06 |
Arrendamiento en función financiera |
07 |
Cobro a favor de un tercero |
08 |
Servicios prestados al Estado a crédito |
10 |
Venta a crédito en IVA hasta 90 días (art. 27, LIVA) |
12 |
Venta de mercancía no nacionalizada |
13 |
Venta de bienes usados no contribuyente |
14 |
Arrendamiento operativo |
15 |
Arrendamiento financiero |
99 |
Otros — exige condicionVentaOtros |
La condición de venta no es igual en todos los comprobantes. Cada esquema de Hacienda lleva su propia lista, y mandar un código fuera de ella se rechaza con
-1(cvc-enumeration-valid) gastando el consecutivo:
Comprobante Códigos admitidos Factura electrónica (01) 01–08,10,12–15,99Tiquete (04), Compra (08), Exportación (09) igual, pero sin el 12Notas de crédito y débito (03, 02) todos, 01–15y99Recibo Electrónico de Pago (10) solo 09y11Los códigos
09y11corresponden al pago de una factura emitida con08y con10respectivamente: por eso viven en el REP y en las notas que ajustan esas facturas, no en la factura original.
Dos campos condicionales que conviene no olvidar:
-
plazoCreditoes unstring, no un número. Va en días (v4.4 cambió la unidad; antes eran meses)."30","60","90". Enviar30sin comillas devuelve400 · "plazoCredito" must be a string. -
condicionVentaOtros(5–100 caracteres) es obligatorio concondicionVenta: "99". La API lo exige antes de emitir, así que no se gasta consecutivo.
Medios de pago
| Código | Medio |
|---|---|
01 |
Efectivo |
02 |
Tarjeta |
03 |
Cheque |
04 |
Transferencia o depósito bancario |
05 |
Recaudado por terceros |
06 |
SINPE Móvil (nuevo en v4.4) |
07 |
Plataforma digital (nuevo en v4.4) |
99 |
Otros — requiere otros |
De uno a cuatro medios. Dos formas de enviarlos:
"medioPago": ["01"] // simple
"medioPago": [ // mixto
{ "tipo": "01", "monto": 3000 },
{ "tipo": "02", "monto": 2000 }
]
Con más de un medio, cada objeto exige monto (> 0). Si falta alguno, la emisión se
rechaza con 400 y MEDIO_PAGO_MONTO_REQUERIDO: sin los montos el reparto no existe y no
hay forma de deducirlo.
Lo ideal es que la suma iguale el total del comprobante —total que incluye impuestos y otros cargos—, pero si no cuadra el comprobante se emite igual. Hacienda no valida ese dato, así que no le negamos la emisión: en su lugar la respuesta trae un aviso.
{
"id": "...",
"clave": "...",
"status": "queued",
"warnings": [
{
"code": "medio_pago_suma_distinta",
"message": "La suma de los montos por medio de pago (150.00) no coincide con el total del comprobante (1130.00). El comprobante se emitió igual: Hacienda no valida este dato. Revisalo si llevás arqueo o cartera por medio de pago.",
"sumaMedios": 150,
"totalComprobante": 1130
}
]
}
Como el total lo calcula la plataforma, conviene calcular el reparto después de conocer el total, o usar un solo medio. Si su conciliación depende del desglose por medio de pago, trate ese aviso como un error suyo aunque nosotros emitamos.
El código
08no existe en v4.4. La API lo rechaza con400.
Tipos de identificación
| Código | Tipo | Longitud | ¿Emisor? | ¿Receptor? |
|---|---|---|---|---|
01 |
Física | 9 | Sí | Sí |
02 |
Jurídica | 10 | Sí | Sí |
03 |
DIMEX | 11–12 | Sí | Sí |
04 |
NITE | 10 | Sí | Sí |
05 |
Extranjero no domiciliado | 1–20, admite letras | No | Sí |
06 |
No contribuyente | — | No | Solo en REP |
Para un receptor extranjero: tipoIdentificacion: "05", el identificador (pasaporte, tax
ID) en numeroIdentificacion, y la dirección en otrasSenasExtranjero en lugar de
ubicacion.
Hacienda valida DIMEX y NITE contra su padrón: un número con formato válido pero inexistente se rechaza con -38.
Unidades de medida
unidadMedida acepta los códigos del catálogo oficial: Unid, Sp (servicios
profesionales), kg, g, L, mL, m, cm, m2, m3, h, d, Al (alquiler),
Os (otros servicios), entre otros.
La API no valida este campo contra el catálogo — solo el largo (máximo 15 caracteres). Un typo o una mayúscula equivocada viaja tal cual al XML. Use exactamente los códigos oficiales y trátelos como sensibles a mayúsculas.
Regla práctica: Sp para servicios, Unid para mercancías que se cuentan, y la unidad
física real (kg, L, m) cuando se venda a granel.
Otros cargos
Hasta 15 por comprobante.
| Código | Tipo de cargo |
|---|---|
01 |
Contribución parafiscal |
02 |
Timbre de la Cruz Roja |
03 |
Timbre del Benemérito Cuerpo de Bomberos |
04 |
Cobro de un tercero — exige los datos del tercero |
05 |
Costos de exportación |
06 |
Impuesto de servicio 10 % |
07 |
Timbre de Colegios Profesionales |
08 |
Depósitos de garantía |
09 |
Multas o penalizaciones |
10 |
Intereses moratorios |
99 |
Otros — requiere tipoDocumentoOtros |
El caso 06 (servicio 10 %) es el más frecuente en restaurantes:
"otrosCargos": [
{ "tipoDocumento": "06", "detalle": "Impuesto de servicio", "porcentaje": 10, "montoCargo": 1130 }
]
Códigos de descuento
| Código | Naturaleza |
|---|---|
01 |
Descuento por Regalía — 100 % de la línea |
02 |
Descuento por Regalía, IVA cobrado al cliente |
03 |
Descuento por Bonificación — 100 % de la línea |
04 |
Descuento por volumen |
05 |
Descuento por Temporada (estacional) |
06 |
Descuento promocional |
07 |
Descuento Comercial |
08 |
Descuento por frecuencia |
09 |
Descuento sostenido |
99 |
Otros — requiere codigoDescuentoOtro y naturalezaDescuento |
codigoDescuento es obligatorio siempre que exista un descuento. Sin él, Hacienda rechaza
con -41.
Instituciones de exoneración
| Código | Institución |
|---|---|
01 |
Ministerio de Hacienda |
02 |
Ministerio de Relaciones Exteriores y Culto |
03 |
Ministerio de Agricultura y Ganadería |
04 |
Ministerio de Economía, Industria y Comercio |
05 |
Cruz Roja Costarricense |
06 |
Benemérito Cuerpo de Bomberos |
07 |
Asociación Obras del Espíritu Santo |
08 |
Fecrunapa |
09 |
EARTH |
10 |
INCAE |
11 |
Junta de Protección Social |
12 |
Aresep |
99 |
Otros — requiere nombreInstitucionOtros |
nombreInstitucionespera el código, no el nombre escrito.
Tipos de documento de exoneración
| Código | Tipo |
|---|---|
01 |
Compras autorizadas por la Dirección General de Tributación |
02 |
Ventas exentas a diplomáticos |
03 |
Autorizado por Ley Especial |
04 |
Exenciones DGH — Autorización Local Genérica |
05 |
Exenciones DGH — Transitorio V (ingeniería, arquitectura, topografía, obra civil) |
06 |
Servicios turísticos inscritos ante el ICT |
07 |
Transitorio XVII (recolección, clasificación y almacenamiento de reciclaje) |
08 |
Exoneración a Zona Franca |
09 |
Servicios complementarios para la exportación (artículo 11 RLIVA) |
10 |
Órgano de las corporaciones municipales |
11 |
Exenciones DGH — Autorización de Impuesto Local Concreta |
99 |
Otros — requiere tipoDocumentoOTRO |
Los tipos 02, 03, 06, 07 y 08 exigen además articulo (el número de artículo de
la ley). Sin él: -478.
Y con articulo hay que mandar inciso. Si ese artículo no tiene incisos, el valor es
0 —no se omite—: Hacienda rechaza con -479 («De no existir un inciso, se indicará un
cero en el campo»). Si envía articulo sin inciso, la API pone el cero por usted.
Los tipos 01, 05, 06, 07 y 11 el Anexo v4.4 los reserva a notas de crédito y
débito que ajusten comprobantes emitidos bajo ese régimen (notas al pie 39-44 de la nota
10.1). En una factura o tiquete la API los acepta, pero lo avisa en warnings. Para una
venta de hoy, lo normal es el 04.
Al XML viajan la TARIFA y el MONTO
TarifaExonerada son los puntos de tarifa (decimal(4,2): tope 99,99) y MontoExoneracion
es el monto de impuesto, mayor que cero. Exonerar el 100 % de un IVA del 13 % sobre
₡10 000 son 13 y ₡1 300.
El porcentajeExoneracion no existe en la v4.4 (la v4.3 sí lo tenía). Si lo envía, se
resuelve así: si el valor cabe dentro de la tarifa del impuesto son puntos de tarifa
(8 sobre un IVA del 13 % exonera 8 puntos); si la excede, se toma como la convención
vieja de «qué parte del impuesto» (100 sobre un IVA del 13 % exonera todo). Las dos
lecturas coinciden en el caso que importa. Para no depender de eso, use tarifaExonerada.
⚠️ inciso va en CERO cuando el artículo no tiene incisos, no se omite: Hacienda
rechaza con -479 («De no existir un inciso, se indicará un cero en el campo»). La API
lo rellena sola si envía articulo sin inciso.
Una exoneración parcial reparte la venta entre gravada y exonerada, línea por línea: cada línea aporta su base en la misma proporción en que se exoneró su impuesto. Exonerar la mitad del IVA de una venta de ₡10 000 declara ₡5 000 gravados y ₡5 000 exonerados; con el 100 % la línea entera va a exonerada.
⚠️ En una fórmula:
Total<Grupo>Exonerado = Σ (base de la línea × MontoExoneracion ÷ impuesto BRUTO de la línea)Antes decía
Σ (MontoExoneracion ÷ TarifaExonerada × 100), que es lo mismo sólo si se exonera la tarifa completa. Con una exoneración parcial de tarifa divergen: 8 puntos de un IVA del 13 % sobre ₡10 000 son ₡6 153,85 exonerados y ₡3 846,15 gravados, no la línea entera. Verificado emitiendo la versión equivocada: Hacienda la rechazó con -111/-106/-108 nombrando esos mismos números — la base que cada exoneración implica. El anexo lo escribe como «(Σ montos exonerados / Σ montos de impuesto) × monto total de la venta» sin aclarar el alcance de esas sumas: es la línea, no el comprobante. Sumando por grupo, dos mercancías de ₡10 000 —una al 13 % exonerada y otra al 2 % sin exonerar— salieron rechazadas con -111, -106 y -108. La diferencia solo aparece con tarifas mezcladas.
Las exentas y no sujetas conservan su propio total. TotalImpuesto también es por línea
(impuesto de las no exoneradas + ImpuestoNeto de las exoneradas).
La Factura de Exportación y el Recibo Electrónico de Pago no tienen nodo de
exoneración en el esquema v4.4: mandarlo devuelve 422 exoneracion_no_aplica_documento.
Y un emisor de régimen simplificado no traslada IVA, así que tampoco puede exonerar
(422 exoneracion_no_aplica_simplificado).
Verificar la autorización antes de emitir
GET /exoneraciones/{autorizacion} consulta el registro de exoneraciones de Hacienda
(EXONET) y devuelve tipo, institución, fecha, tarifa autorizada, vigencia y los CAByS
cubiertos. Es el mismo registro contra el que Hacienda cruza los tipos 04 y 11 al
recibir el comprobante, y por eso no se puede probar en el sandbox: ahí toda
exoneración de esa familia sale rechazada con -125, sea real o inventada.
curl -H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET" \
"https://api.facturaencr.com/api/v2/efactura/exoneraciones/AL-00460853-20?cabys=2341000000100"
Al emitir, la API hace esa consulta por su cuenta para los tipos 04 y 11 y devuelve en
warnings lo que no calce (número no registrado, fecha distinta, tarifa mayor a la
autorizada, CAByS fuera de la autorización, beneficiario distinto del receptor). Avisa,
no bloquea: la palabra final la tiene Hacienda.
También quedan expuestos los otros dos registros públicos:
GET /productores/agropecuario/{identificacion} (MAG) y
GET /productores/pesca/{identificacion} (INCOPESCA y acuicultura). Ojo: Hacienda
responde esos dos con HTTP 200 aunque la persona no esté inscrita, poniendo el
"Not Found" dentro del cuerpo; acá eso se traduce a encontrado:false.
Medicamentos y medios de transporte
Tres campos de la línea que dependen de qué se vende. Son opcionales en el esquema y se vuelven obligatorios según el CABYS; si faltan cuando corresponden, Hacienda emite una advertencia — no rechaza el comprobante.
| Campo | Cuándo | Qué lleva |
|---|---|---|
numeroVINoSerie |
vehículos, aeronaves y embarcaciones | VIN, número de serie o consecutivo de fabricación (17 caracteres). Si la línea cubre varias unidades, mandá un arreglo |
registroMedicamento |
medicamentos con registro sanitario | el número de registro del Ministerio de Salud |
formaFarmaceutica |
medicamentos con registro sanitario | el código de la nota 19 |
formaFarmaceutica es el código NUMÉRICO del catálogo «Código de forma Farmacéutica» de
Hacienda: del 01 al 229. No es una abreviatura: 04 es cápsula, 10 cápsula de
gelatina dura, 229 ungüento. Se acepta con o sin el cero a la izquierda (4 = 04).
{
"codigoCabys": "3521100000000",
"detalle": "Acetaminofén 500 mg",
"cantidad": 1, "unidadMedida": "Unid", "precioUnitario": 1500,
"registroMedicamento": "MS-2026-12345",
"formaFarmaceutica": "04",
"impuesto": [{ "codigo": "01", "codigoTarifa": "08", "tarifa": 13 }]
}
Los tres van en la Factura, el Tiquete, la Factura de Compra, la de Exportación y las notas de crédito y débito. El Recibo Electrónico de Pago no los tiene: su línea ni siquiera lleva CABYS.
Exportar mercancía con impuestos especiales
impuesto[].montoExportacion es el monto de impuesto de exportación, y solo aplica a
mercancías con impuestos especiales (licores, combustibles, cemento…). Existe únicamente
en la Factura de Exportación y en las notas que la ajusten; en los demás
comprobantes se ignora. No se calcula: es el monto que declara el exportador.
Tipo de transacción
Nuevo en v4.4. Si se omite, se asume venta normal.
| Código | Tipo |
|---|---|
01 |
Venta normal de bienes y servicios |
02 |
Mercancía de autoconsumo exento |
03 |
Mercancía de autoconsumo gravado |
04 |
Servicio de autoconsumo exento |
05 |
Servicio de autoconsumo gravado |
06 |
Cuota de afiliación |
07 |
Cuota de afiliación exenta |
08 |
Bienes de capital para el emisor |
09 |
Bienes de capital para el receptor |
10 |
Bienes de capital para emisor y receptor |
11 |
Bienes de capital de autoconsumo exento para el emisor |
12 |
Bienes de capital sin contraprestación a terceros exento para el emisor |
13 |
Sin contraprestación a terceros |
Los tipos
08,09y10determinan el crédito de IVA de su cliente. Marcar mal una venta de bienes de capital le cuesta dinero al comprador.
CABYS
Cada línea exige un código CABYS de 13 dígitos del Catálogo de Bienes y Servicios del BCCR. El CABYS hace dos cosas:
-
Clasifica la línea. El primer dígito determina si es bien o servicio: 0–4 = mercancía, 5–9 = servicio. De ahí sale si el monto suma a
TotalMercanciasGravadaso aTotalServGravados. Hacienda re-deriva esta clasificación del CABYS: si sutipoVentala contradice, gana el CABYS.Ojo con el 5: la sección es "Construcciones y servicios de construcción" y cuenta como servicio, no como mercancía. Es la que más confunde, porque incluye tanto la obra (
53…) como los servicios de construcción (54…). -
Sugiere la tarifa de IVA que le corresponde al producto.
Buscador
curl "https://api.facturaencr.com/v2/efactura/catalogs/cabys?q=pan%20pita&top=5" \
-H "X-API-Key: $EFACTURA_KEY" -H "X-API-Secret: $EFACTURA_SECRET"
{
"items": [
{ "codigo": "2349002011500", "descripcion": "Pan pita o pan árabe, sin congelar", "impuesto": 1 },
{ "codigo": "2349002020800", "descripcion": "Pan pita o pan árabe, congelado", "impuesto": 1 }
]
}
q— texto o código, mínimo 3 caracteres. Menos de 3:400 query_too_short.top— máximo de resultados, 1 a 50 (por defecto 30). El parámetro estop, nolimit:limitse ignora en silencio.impuesto— tarifa de IVA asociada al código, en porcentaje.
Buscar por código exacto devuelve cero o un resultado; sirve como validador.
Verifique sus códigos antes de facturar
La API no valida el CABYS contra el catálogo: solo comprueba que sean 13 dígitos. Un código bien formado pero inexistente pasa la validación local, consume consecutivo y lo rechaza Hacienda con -400:
-400 En la línea (1) el código indicado en el campo 'Código de Producto/Servicio' no se encuentra en el Catálogo de Bienes y Servicios CABySSi carga productos desde un maestro propio, páselo una vez por
GET /catalogs/cabys?q={codigo}y marque los que devuelvanitems: []. Es la causa más común de rechazo en una integración nueva, y no se detecta en pruebas si sus datos de prueba usan códigos válidos.
Contingencia: cuando se cae el internet
La venta ocurre el lunes con el sistema caído. Usted la transmite el miércoles. ¿Qué fecha lleva el comprobante?
La del lunes. La FechaEmision es la de la operación, no la de la transmisión, y
determina el período tributario: fecharla el miércoles mete la venta en el mes equivocado
del D-104. Y no se puede corregir después — habría que anularla con una nota de crédito y
volver a emitir.
Para eso existen dos campos:
{
"fechaEmision": "2026-07-20T09:00:00-06:00", // cuándo ocurrió la venta
"situacion": "3" // por qué se transmite tarde
}
situacion |
Significado |
|---|---|
1 |
Normal — emisión y transmisión en línea. Es el default; no lo envíe. |
2 |
Contingencia — falló su sistema |
3 |
Sin internet — falló la conectividad |
Ambos campos son opcionales y en el flujo normal no se envían: la plataforma pone la
hora del servidor y situación 1.
Las dos salvaguardas
La fecha no puede ser futura. Hacienda rechaza el comprobante, así que la API lo corta
antes con 400.
Retrofechar más de 15 minutos con situacion: "1" genera un aviso, no un error. El
comprobante se emite:
"warnings": [
{
"code": "fecha_emision_retrofechada",
"message": "fechaEmision está 612 minutos en el pasado y situacion es \"1\" (normal). El comprobante se emite igual —Hacienda acepta la retrofecha— pero quedará fechado en ese momento…",
"minutos": 612
}
]
Está verificado emitiendo: Hacienda acepta la retrofecha con situación normal — se probó con 30 minutos, 6 horas, 12 horas, 2 días, 7 días y hasta 30 días de atraso, y los aceptó todos. Por eso avisamos en vez de bloquear: un punto de venta que transmite en lote al final del día tiene fechas correctas y con horas de atraso, y bloquearlo le impedía facturar.
El aviso está porque el riesgo sigue siendo real en el otro caso: un reloj mal configurado emite con fecha equivocada, el comprobante es válido para Hacienda, cae en el período tributario que no era y ya no se corrige. Los 15 minutos de tolerancia absorben el desfase normal entre su reloj y el nuestro.
Si la venta se hizo sin internet, declárelo con situacion: "3": es lo correcto y además
no genera aviso.
Los consecutivos siguen su orden normal. La contingencia cambia la fecha y el dígito de situación de la clave, no la numeración: emita en el orden en que va transmitiendo.
Cédulas alfanuméricas
Desde la v4.4 —oficio DGL-195-2026, Decreto 44648-MJP— las cédulas jurídicas y físicas pueden contener letras una vez agotado el consecutivo numérico. La longitud no cambia; solo el conjunto de caracteres.
| Tipo | Longitud | Ejemplo válido |
|---|---|---|
01 Física |
9 | 1A234B567 |
02 Jurídica |
10 | 310123A456 |
03 DIMEX |
11–12 | 12345678901 |
04 NITE |
10 | 1234567890 |
05 Extranjero |
1–20 | X-9912345 |
Esto rompe tres suposiciones que casi todo sistema viejo tiene:
- No valide con
^\d+$. El patrón correcto es[0-9A-Za-z]con la longitud del tipo. -
No guarde la cédula como número. Un
INTo unparseIntdestruye las letras y, de paso, los ceros a la izquierda. - No normalice a mayúsculas ni minúsculas por su cuenta. Envíela tal como está inscrita.
El consecutivo (consecutivoNumero, 10 caracteres) admite el mismo juego alfanumérico por
la misma razón.
La cédula del emisor se rellena con ceros a la izquierda hasta 12 caracteres dentro de la clave de 50. El relleno respeta las letras:
310123A456→00310123A456.
Consecutivos y numeración
El consecutivo tiene 20 dígitos y lo genera la plataforma:
001 00001 01 0000000866
│ │ │ └── número secuencial (10)
│ │ └───────── tipo de comprobante (2)
│ └────────────── terminal / caja (5)
└─────────────────── sucursal (3)
Dos modos, configurables por emisor:
| Modo | Quién numera | Cuándo usarlo |
|---|---|---|
platform (por defecto) |
La plataforma, de forma atómica | Casi siempre |
integrator |
Usted, enviando consecutivoNumero |
Solo si ya tiene numeración propia que debe preservar |
En modo platform, branchCode y terminalCode son opcionales: se usan los del emisor.
Enviar consecutivoNumero en modo platform devuelve 400.
En modo integrator usted es responsable de que no haya saltos ni repeticiones, incluso
con concurrencia. Un consecutivo repetido se rechaza; uno saltado hay que justificarlo.
Reserva previa de clave
Si necesita imprimir o mostrar la clave antes de emitir:
POST /clave/reserve → { "clave": "...", "consecutivo": "..." }
Luego envíe esa clave en el payload de emisión. El comprobante usará la reservada en vez
de generar una nueva.
Idempotencia
Los POST de emisión exigen la cabecera Idempotency-Key:
Idempotency-Key: pedido-2026-07-000123
| Situación | Resultado |
|---|---|
| Misma clave, mismo body | Devuelve el resultado original. No se emite de nuevo. |
| Misma clave, body distinto | 409 idempotency_conflict |
| Misma clave, todavía procesándose | 409 idempotency_in_progress |
| Clave nueva | Emisión nueva |
La ventana es de 24 horas.
Derive la clave de su dominio, no de su reintento. Use
pedido-4471,orden-2026-07-000123: algo que identifique la venta. Si genera un UUID nuevo en cada intento, la idempotencia no lo protege de nada — que es justamente el escenario que la hace necesaria: su proceso envía la factura, se cae antes de guardar la respuesta y reintenta. Con una clave estable, el reintento devuelve la factura que ya existe. Sin ella, factura dos veces al mismo cliente.
Los 429 y los 5xx son seguros de reintentar con la misma clave: si la petición no llegó
a emitir, no consumió consecutivo.
La clave se reserva ANTES de emitir
Apenas recibimos la petición, la clave queda tomada. No se libera hasta que la emisión termina, bien o mal. Esto importa porque emitir no es instantáneo: hay que firmar, tomar el consecutivo y hablar con Hacienda.
Si durante esa ventana llega otra petición con la misma clave, recibe:
{
"error": "idempotency_in_progress",
"message": "La Idempotency-Key \"pedido-4471\" está siendo procesada en este momento.",
"retryAfterSeconds": 5
}
No es un error de su lado, y no significa que la emisión haya fallado. Significa que ya hay una en curso con esa clave. Espere unos segundos y vuelva a pedir con la misma clave: si el comprobante se emitió, va a recibir su respuesta original.
Lo que este 409 evita es el caso que rompe una integración de verdad: su proceso manda la factura, se cae antes de recibir la respuesta, y reintenta. Sin la reserva previa no había rastro de la clave y se emitía un segundo comprobante para la misma venta, con su propio consecutivo y su propio cobro.
Qué pasa cuando la emisión falla
| Resultado | Qué pasa con la clave |
|---|---|
2xx |
Queda tomada con la respuesta guardada. Reintentar devuelve esa respuesta. |
4xx / 5xx |
Se libera. Puede reintentar con la misma clave y el payload corregido. |
| El proceso se cae a mitad | Queda reservada ~2 minutos y después se libera sola. |
Que un error libere la clave es deliberado: si un 400 por un CABYS mal puesto la dejara
tomada, usted tendría que inventar una clave nueva por cada corrección, y perdería la
protección justo cuando más la necesita.
Comprobantes recuperados
Si nuestro proceso se interrumpe mientras firma su comprobante, lo terminamos solo —
con la misma clave numérica y el mismo consecutivo que ya se habían reservado. Usted
recibe un webhook document.queued con "recuperado": true y el documento sigue su curso
normal.
No hay que hacer nada de su lado, y no queda un salto en su numeración.
Probar sin certificado
Conseguir el .p12 de sandbox y las credenciales TRIBU-CR de stag toma días. Para que eso no
frene su integración, podemos habilitarle un emisor de pruebas de la plataforma: emite
contra el sandbox de Hacienda con nuestra cédula, y usted no sube ni configura nada.
Ya está habilitado en su cuenta: no hay nada que pedir ni que configurar. Aparece en
GET /certificates junto a sus emisores, marcado "shared": true, con el identificador
EMISORPRUEBA. Emita poniéndolo en emisorLegalId:
curl -X POST https://api.facturaencr.com/v2/efactura/documents/tiquete \
-H "X-API-Key: $EFACTURA_KEY" -H "X-API-Secret: $EFACTURA_SECRET" \
-H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{ "emisorLegalId": "EMISORPRUEBA", "condicionVenta": "01",
"medioPago": ["01"], "detalle": [ … ] }'
EMISORPRUEBA no es una cédula: es el identificador con el que la plataforma publica
este emisor. El backend lo traduce al certificado y firma con la identidad real, que
usted no necesita conocer. Todo lo demás del payload funciona igual que con un emisor
suyo.
Recibe un 202 con su clave y consecutivo, y a los pocos segundos el veredicto real de
Hacienda: accepted o rejected con su motivo. Es una emisión de verdad contra sandbox,
no un simulador — que es justamente el punto: lo que pasa acá es lo que va a pasar en
producción.
Lo que cambia respecto a usar su propio certificado:
| Con el emisor de pruebas | Con su certificado | |
|---|---|---|
| Ambiente | Solo sandbox | Sandbox y producción |
| Sucursal / terminal / consecutivo | Los asigna la plataforma | Los elige usted |
| Descargar XML firmado y respuesta | No | Sí |
| Sí, con la identidad del emisor tapada | Completo | |
| Datos del emisor en la ficha | "Emisor de pruebas" |
Los suyos |
codigoActividad en el payload |
Se ignora: usa la del emisor | La que usted elija |
Los tres motivos, por si le sirven:
-
La numeración es automática. Cada cuenta recibe su propia sucursal y terminal dentro del emisor compartido —a partir de
050/00999— y por eso dos integradores no pueden generar el mismo consecutivo sobre la misma cédula. Si mandabranchCode,terminalCodeoconsecutivoNumero, la respuesta es400 shared_cert_numbering_is_automatic— se rechaza en vez de ignorarse en silencio, para que no crea que se aplicó. -
El XML firmado no se descarga (
403 shared_test_emisor_no_download). Lleva una firma digital real de nuestra cédula sobre contenido que escribió usted. El estado, la respuesta de Hacienda y el PDF sí los tiene. -
La identidad del emisor va tapada. En el PDF la razón social sale como
PRUEBAy la cédula comoCert de prueba; la ubicación y la actividad económica no se imprimen. El resto del comprobante —sus líneas, sus impuestos, sus totales— es suyo y se ve completo. Si mandacodigoActividadse ignora y se usa la del emisor: las actividades inscritas son un dato de su dueño, y usted igual no puede elegir entre ellas.Una cosa que no podemos tapar: la clave de 50 dígitos lleva la cédula del emisor embebida (posiciones 9-21). Es el formato de Hacienda y la clave hay que devolvérsela — sin ella usted no puede consultar su comprobante ni rastrearlo.
Su certificado siempre manda. Si sube uno propio para la misma cédula, la API usa el suyo y el compartido deja de aparecerle — sin que cambie una línea de código, y sin las restricciones de arriba (podrá descargar el XML y elegir su numeración).
Si alguna cuenta abusa del emisor compartido, podemos desactivárselo puntualmente; el resto lo sigue teniendo.
Ambientes
Hay una sola URL base:
https://api.facturaencr.com/v2/efactura
El ambiente no se elige por subdominio ni por parámetro: lo determina el campo
environment de la API Key que usa (y, en su defecto, el del certificado).
| Sandbox | Producción | |
|---|---|---|
| Destino | Sandbox oficial de Hacienda | Hacienda producción |
| Valor fiscal | Ninguno | Sí, es una factura real |
| Certificado | .p12 de sandbox del BCCR |
.p12 de producción |
| Cobro | Progresión propia, separada | Tramos de producción |
| Numeración | Serie propia, separada | Su serie fiscal real |
Esto significa que la misma petición, con otra llave, emite de verdad. Antes de mover
algo a producción, confirme con POST /auth/verify qué llave tiene cargada su aplicación.
La numeración de sandbox y producción es independiente. Puede probar todo lo que quiera en sandbox sin mover un dígito de su consecutivo fiscal: cada ambiente lleva su propia serie por sucursal/terminal y tipo de comprobante. La primera emisión de pruebas de un emisor arranca su serie de sandbox desde el número que ya tenía en producción, así que tampoco repite una clave.
Un certificado de sandbox no puede emitir en producción ni al revés: la combinación se valida antes de firmar.
Límites
-
Rate limiting por API key. Al excederlo:
429conRetry-After. Respételo; no reintente de inmediato. -
Aplane las ráfagas. Si tiene que emitir mil comprobantes, distribúyalos en el tiempo
en lugar de dispararlos en paralelo. Hacienda responde
202sin cabeceras de rate limit, así que su límite real es de comportamiento: quien ráfaguea termina bloqueado. - Máximo 1 000 líneas por comprobante, 15 otros cargos, 5 impuestos por línea, 4 medios de pago, 10 referencias.
- PDF adjunto máximo 15 MB.
Retención y disponibilidad
Los XML firmados y las respuestas de Hacienda se conservan cinco años, como exige la
normativa. Cada documento expone su retentionUntil.
Aun así, archive sus propios XML. Son la prueba fiscal de sus operaciones y no conviene que su única copia viva en un tercero.
La clave de 50 dígitos
Identifica el comprobante ante Hacienda de forma única y permanente.
506 240726 003101678166 00100001010000000866 1 42351111
│ │ │ │ │ └── código de seguridad (8)
│ │ │ │ └──────── situación: 1 normal, 2 contingencia, 3 sin internet
│ │ │ └───────────────────── consecutivo (20)
│ │ └───────────────────────────────────── cédula del emisor, 12 con ceros a la izquierda
│ └─────────────────────────────────────────────── fecha DDMMAA
└───────────────────────────────────────────────────── código de país
Guárdela: es la llave para consultar (GET /documents/clave/{clave}), para referenciar en
notas de crédito y débito, y para responder con un Mensaje Receptor.
Códigos de error
HTTP
| Código | Significado | Acción |
|---|---|---|
200 / 201 |
OK | — |
202 |
Aceptado y encolado | Esperar veredicto |
400 |
Payload inválido o regla de negocio | Corregir y reenviar. No consumió consecutivo. |
401 |
Credenciales ausentes o inválidas | Revisar cabeceras |
403 |
Scope insuficiente, cuenta suspendida, plan sin API, o descarga de XML con el emisor de pruebas | Revisar scopes y estado |
404 |
No existe en su cuenta | Revisar el id |
409 |
Idempotency-Key reusada con otro cuerpo, o MR ya enviado |
Cambiar la clave |
413 |
PDF adjunto de más de 15 MB | Reducir el PDF |
422 |
Regla fiscal incumplible | Leer el mensaje |
429 |
Rate limit | Respetar Retry-After y reintentar con la misma clave |
500 / 503 |
Fallo nuestro | Reintentar con backoff |
Errores de Hacienda
Aparecen en haciendaMessage cuando status es rejected. Los que más se ven:
| Código | Qué pasó | Cómo se arregla |
|---|---|---|
| -1 | Violación del esquema XSD | Casi siempre una línea sin impuesto o un enum inválido |
| -37 | Provincia/cantón/distrito del emisor no coinciden con el padrón. No bloquea: el sandbox lo agrega de acompañante y nunca es el motivo real | Ignórelo y busque el OTRO código del mensaje, que es el que rechaza. Si igual quiere limpiarlo: PATCH /emisores/:legalId/config con la ubicacion exacta de TRIBU-CR |
| -38 | DIMEX o NITE inexistente | Verificar la cédula del receptor |
| -41 | Descuento sin codigoDescuento |
Añadirlo |
| -45 | Monto de impuesto por línea mal calculado | Revisar base × tarifa |
| -111 | Clasificación bien/servicio inconsistente | Hacienda la re-deriva del CABYS |
| -125 | Exoneración inválida | Revisar documento y montos |
| -131 | REP sin codigo o razon en la referencia |
La API los deriva |
| -190 | MontoExoneracion no cuadra con TarifaExonerada |
Recalcular |
| -400 | CABYS inexistente | Validar contra GET /catalogs/cabys |
| -451 | ivaCobradoFabrica: "02" sin tarifa exenta |
Usar codigoTarifa: "10" |
| -454 | Base imponible mal armada | Revisar qué impuestos engrosan la base |
| -459 | Código 05 sin volumenUnidadConsumo |
Añadirlo |
| -471 / -473 | Alcohol absoluto mal calculado | Dividir entre 100 |
| -478 | Exoneración sin articulo |
Los tipos 02, 03, 06, 07 y 08 lo exigen |
| -479 | Exoneración con articulo pero sin inciso |
El inciso es 0 si el artículo no tiene incisos, no se omite. La API lo rellena |
| -496 | IVA devuelto: no hay pago con tarjeta, o el monto no es el que Hacienda recalcula | Exige un medioPago "02". Con pago mixto Hacienda prorratea y el mensaje trae entre paréntesis el monto que espera. Mande totalIVADevuelto: "auto" |
| -505 | Tarifa transitoria en factura o tiquete | Solo valen en NC/ND |
| -508 | El receptor no coincide con el del documento referenciado | En un REP, todas las referencias deben ser de comprobantes del mismo cliente |
| -509 | CABYS de la NC/ND ausente en el documento referenciado | Use los mismos CABYS; para algo nuevo, emita una factura aparte |
| -58 | Estructura inválida del plazo de crédito | Con condicionVenta: "02", plazoCredito es obligatorio y es un string |
| -513 | Línea sin impuesto de la familia IVA | Declarar IVA aunque sea exento |
Cuando un comprobante sale rejected, GET /documents/{id} trae además un objeto
rechazo ya masticado: el resumen encabezado por el código que de verdad bloqueó, y
cada código con su explicacion, su comoArreglarlo y —cuando corresponde—
bloqueaPorSiSolo: false, que marca los acompañantes:
{
"status": "rejected",
"rechazo": {
"resumen": "-400: El código CAByS no existe en el catálogo.",
"codigos": [
{ "codigo": "-400", "mensajeHacienda": "…", "explicacion": "…", "comoArreglarlo": "Validá el código contra GET /catalogs/cabys…" },
{ "codigo": "-37", "mensajeHacienda": "…", "explicacion": "…", "bloqueaPorSiSolo": false }
],
"siguientePaso": "Un comprobante rechazado no se corrige ni se reenvía: emitilo de nuevo con los datos arreglados."
}
}
Un rechazo suele traer varios códigos y no todos son la causa. El
-37es el caso claro: medido sobre 426 rechazos de 30 días apareció en 306, pero nunca solo — el 100 % de las veces venía acompañado del código que de verdad rechazaba, y 301 de esos 306 eran de sandbox. Los mismos emisores emiten sin problema. Antes de perseguir un código, mire si aparece solo en el documento.
Un rejected no se corrige ni se reintenta: emita un comprobante nuevo con los datos
arreglados. El consecutivo rechazado queda registrado como tal.
Antes de pasar a producción
-
POST /auth/verifydevuelve la llave de producción que espera. - El certificado
.p12es el de producción y no vence pronto (notAfter). -
provincia,cantonydistritodel emisor coinciden con el padrón de Tributación (si no, Hacienda agrega el aviso -37 a sus respuestas; no bloquea, pero ensucia el diagnóstico de los rechazos reales). -
Todos los CABYS de su maestro de productos devuelven resultado en
GET /catalogs/cabys. - Ninguna línea puede salir con
impuestovacío o ausente. -
Idempotency-Keyderivada de su número de pedido, no de un UUID por reintento. - Webhook registrado, firma verificada y handler idempotente.
-
Si algún emisor es de régimen simplificado, su conciliación espera
totalImpuesto: 0. - Sabe qué hace su sistema ante un
rejected. - Guarda
documentId,claveyconsecutivode cada emisión.
Soporte
Al reportar un problema, incluya el requestId de la respuesta y, si aplica, la
clave del comprobante. Con eso ubicamos la petición exacta y su traza completa.
- Correo: soporte@facturaencr.com
- Especificación en crudo:
GET /docs/openapi.yaml·GET /docs/openapi.json - Colección Postman lista para importar:
GET /docs/postman.json - Esta guía en Markdown:
GET /docs/guia.md - Todo junto en un ZIP:
GET /docs/bundle.zip