En esta página
Guía para desarrolladores
Emita, consulte y notifique comprobantes electrónicos de Costa Rica desde una sola API REST. Seis tipos de comprobante — FE, TE, NC, ND, FEC, FEE — y una misma estructura de request.
Esta guía contiene todo lo que necesita para integrar KyrFact. Llame a su URL de API con cualquier cliente HTTP; nosotros nos encargamos del resto.
Por seguridad, los ejemplos en esta guía usan $KYRFACT_API_BASE como marcador para la URL de su API. Su URL real se le entrega cuando se activa su llave (junto con el plaintext de la llave). Configure $KYRFACT_API_BASE en su entorno antes de ejecutar los ejemplos.
Léalas en orden si va empezando:
- Autenticación — obtenga su llave de API
- Inicio rápido — emita su primera FE en 90 segundos
- Estructura del payload — referencia campo por campo
- Endpoints — todas las URLs disponibles
- Tipos de comprobante — escoja el correcto y copie su payload
Autenticación
Cada endpoint requiere un Bearer token en el header Authorization:
Authorization: Bearer kf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Hay dos ambientes — escoja uno por llave:
| Prefijo | Ambiente | Para |
|---|---|---|
kf_live_… | Producción | Comprobantes reales enviados a Hacienda |
kf_test_… | Sandbox | Ambiente de pruebas (api-stag de Hacienda) |
El texto plano de la llave aparece una sola vez al momento de crearla. Guárdela inmediatamente; lecturas posteriores solo muestran el prefijo.
Inicio rápido
Emita una FE de 100 CRC + 13% IVA → 113 CRC en total. Copie, pegue, configure $KYRFACT_KEY:
curl -X POST $KYRFACT_API_BASE/v1/invoices \
-H "Authorization: Bearer $KYRFACT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: mi-orden-12345" \
-d '{
"type": "FE",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC",
"tipoCambio": 1,
"emisor": {
"codigoActividadEmisor": "6201.0",
"nombre": "Acme Comercializadora S.A.",
"tipoId": "02", "id": "3101234567",
"provincia": "1", "canton": "1", "distrito": "01",
"senas": "Calle 5, Avenida 2",
"email": "facturas@acme.cr"
},
"receptor": {
"nombre": "Cliente Ejemplo",
"tipoId": "01", "id": "100100100",
"provincia": "1", "canton": "1", "distrito": "01",
"email": "cliente@example.com"
},
"totalServGravados": 100, "totalGravados": 100,
"totalVentas": 100, "totalVentasNeta": 100,
"totalImpuestos": 13, "totalComprobante": 113,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Servicio profesional",
"precioUnitario": 100, "montoTotal": 100, "subTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "8314100000200",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 13 } }
}
},
"options": { "email": { "onAccept": true } }
}'
Recibe inmediatamente:
{
"id": "6500000000000000dd000001",
"type": "FE",
"env": "live",
"status": "enviada",
"clave": "50601012600031012345670010000101000000000412345678",
"consecutivo": "00100001010000000042",
"totals": { "totalComprobante": 113, "totalImpuestos": 13, "codMoneda": "CRC" },
"mensajeHacienda": null
}
Guarde el id y la clave. El estado pasa a completa en 1-3 minutos — vea Estado y polling.
Tip: si el perfil de su empresa ya está configurado en el tenant, puede omitir el bloque emisor completo del payload — vea Datos del emisor.
Estructura del payload
Cada solicitud POST /v1/invoices envía un único objeto JSON. Esta sección documenta cada campo. Los campos requeridos generan un 400 si faltan; los opcionales tienen comportamientos por defecto seguros.
El shape es común a todos los tipos de comprobante. Para los campos específicos por tipo (NC, ND, FEC, FEE), vea las páginas individuales más abajo.
Cabecera
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | string | Sí | Tipo de comprobante. Uno de FE · TE · NC · ND · FEC · FEE. |
fecha | string ISO-8601 | Sí | Fecha y hora con offset horario. Ej. "2026-04-28T10:00:00-06:00". Hacienda exige el offset. |
condicionVenta | string | Sí | Código de condición de venta. "01" contado, "02" crédito, "99" otros. |
condicionVentaOtros | string | Cuando condicionVenta = "99" | Descripción libre de la condición. |
mediosPago | string[] | Sí | Códigos de medio de pago. Mín. 1. "01" efectivo, "02" tarjeta, "03" cheque, "04" transferencia, "99" otros. |
codMoneda | string | Sí | Código ISO-4217 de la moneda. "CRC", "USD", "EUR". |
tipoCambio | number | Sí | Tipo de cambio aplicado. 1 cuando moneda = CRC. |
omitirReceptor | "true" · "false" | No | Solo válido en TE y FE. "true" para tiquetes anónimos. Default: "false". |
otros | string | No | Texto libre que se imprime en el PDF. |
sucursal | string | No | Identificador de sucursal (3 dígitos). Default: el del tenant. |
terminal | string | No | Identificador de terminal (5 dígitos). Default: "00001". |
proveedorSistema | string | No | Nombre identificador de su software. Se embebe en el XML. |
Totales pre-calculados
v1 espera totales pre-calculados — el cliente es responsable de la aritmética antes de llamar. La validación de totales revisa los invariantes con tolerancia ±0.01 y retorna 400 con el detalle.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
totalVentas | number | Sí | Suma de subtotales antes de descuentos. |
totalVentasNeta | number | Sí | totalVentas − totalDescuentos. |
totalComprobante | number | Sí | Total final que el comprador paga (incluye impuestos, descuentos, otros cargos). |
totalImpuestos | number | Cuando hay líneas con tarifa | Suma de impuestos del comprobante. |
totalDescuentos | number | No | Suma de descuentos. Default: 0. |
totalServGravados · totalServExentos · totalServExonerados · totalServNoSujeto | number | No | Subtotales de servicios por tipo de IVA. |
totalMercGravada · totalMercExenta · totalMercExonerada · totalMercNoSujeta | number | No | Subtotales de mercancías por tipo de IVA. |
totalGravados · totalExentos · totalExonerado · totalNoSujeto | number | No | Subtotales agregados (servicios + mercancías) por tipo de IVA. |
totalImpuestosAsumidosFabrica | number | No | IVA cobrado en fábrica (régimen especial). |
totalIVADevuelto | number | No | IVA devuelto al cliente (canasta básica, etc.). |
totalOtrosCargos | number | No | Otros cargos no impositivos. |
Bloque emisor
Quien emite el comprobante. Todos los campos son opcionales en el payload — los faltantes se rellenan desde el perfil del tenant. Vea Datos del emisor para el merge.
| Campo | Tipo | Descripción |
|---|---|---|
codigoActividadEmisor | string | Código de actividad económica de Hacienda. Ej. "6201.0". |
nombre | string | Nombre legal del emisor. |
tipoId | string | Tipo de identificación: "01" física, "02" jurídica, "03" dimex, "04" nite. |
id | string | Cédula del emisor (sin guiones). |
nombreComercial | string | Nombre comercial. |
provincia · canton · distrito | string | Códigos de ubicación de Hacienda. Ej. "1" · "1" · "01". |
senas | string | Otras señas (calle, avenida, edificio). |
codigoPaisTel | string | Código de país telefónico (ej. "506"). |
tel | string | Teléfono. |
email | string | Correo electrónico del emisor. |
registroFiscal8707 | string | Registro fiscal opcional para regímenes especiales. |
logoUrl | URL | URL completa de la imagen del logo (PNG o JPG). |
logoAncho | number 20-200 | Ancho del logo en puntos. Default: 80. |
estiloFactura | "C" · "G" · "Y" · "L" | Tema de color del PDF. C celeste (default), G verde, Y amarillo, L celeste claro. |
pieFactura | string | Texto al pie del PDF. |
paginaWeb | string | URL del sitio web del emisor. |
Los siete campos requeridos por Hacienda son: codigoActividadEmisor, nombre, tipoId, id, provincia, canton, distrito. Si faltan tras el merge con el tenant, la API responde 400 con la lista exacta.
Bloque receptor
Quien compra. No se rellena desde el tenant — debe venir en el payload (o usar omitirReceptor: "true" para TE anónimos).
| Campo | Tipo | Descripción |
|---|---|---|
codigoActividadReceptor | string | Código de actividad económica del comprador (requerido en FEC). |
nombre | string | Nombre del comprador. |
tipoId | string | "01" física · "02" jurídica · "03" dimex · "04" nite · "05" extranjero (FEE). |
id | string | Cédula del comprador. Omitir cuando tipoId = "05". |
identifExtranjero | string | Identificación extranjera (DNI, pasaporte). Requerida cuando tipoId = "05". |
nombreComercial | string | Nombre comercial. |
provincia · canton · distrito | string | Códigos de ubicación CR. Omitir cuando comprador extranjero. |
senas | string | Otras señas. Requerido en FEC. |
senasExtranjero | string | Dirección extranjera (FEE). |
codigoPaisTel · tel · email | string | Datos de contacto del comprador. |
Bloque detalles (líneas)
Objeto anidado con clave numérica como string ("1", "2", …). Cada valor es una línea del comprobante.
"detalles": {
"1": {
"cantidad": 1,
"unidadMedida": "Sp",
"detalle": "Servicio profesional",
"precioUnitario": 100,
"subTotal": 100,
"montoTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "8314100000200",
"impuesto": {
"1": {
"codigo": "01", "codigoTarifa": "08",
"tarifa": 13, "monto": 13
}
}
}
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
cantidad | number | Sí | Cantidad de unidades. |
unidadMedida | string | Sí | Unidad de medida de Hacienda. "Sp" servicios profesionales, "Unid" unidad, "kg" kilo, "L" litro, "m" metro. |
detalle | string | Sí | Descripción de la línea. |
precioUnitario | number | Sí | Precio por unidad antes de descuentos e impuestos. |
subTotal | number | Sí | cantidad × precioUnitario. |
montoTotal | number | Sí | Igual a subTotal antes de descuentos. |
montoTotalLinea | number | Sí | Total de la línea con impuestos: subTotal − montoDescuento + monto IVA. |
codigoCABYS | string | Sí | Código CABYS de 13 dígitos. KyrFact valida la longitud, no el registro (Hacienda actualiza el catálogo mensualmente). |
baseImponible | number | Auto | Base sobre la que se calcula el IVA. Auto-completado a subTotal − montoDescuento cuando tarifa > 0 y el tipo no es FEE. |
montoDescuento | number | No | Descuento aplicado a la línea. |
naturalezaDescuento | string | Cuando hay descuento | Descripción del descuento. Requerido cuando montoDescuento > 0. |
impuesto | objeto | Cuando aplica IVA | Sub-objeto con clave numérica string. Cada entrada describe un impuesto. |
Sub-bloque impuesto["1"]:
| Campo | Tipo | Descripción |
|---|---|---|
codigo | string | Código del tributo. "01" IVA, "02" ISC, etc. |
codigoTarifa | string | Tarifa aplicada. "08" 13% general, "04" 4% institucional, "02" 1%, "01" exento. |
tarifa | number | Porcentaje. Ej. 13. |
monto | number | Monto del impuesto: baseImponible × tarifa / 100. |
exoneracion | objeto | Bloque de exoneración cuando codigoTarifa = "01". Vea ejemplos en la sección por tipo. |
Bloque options
Comportamiento opcional por llamada. Todos los defaults son seguros.
| Campo | Tipo | Default | Descripción |
|---|---|---|---|
persist | "full" · "minimal" | "full" | "full" guarda request, items, emisor, receptor y XMLs en MongoDB. "minimal" guarda solo lo esencial para reportes (clave, status, totales). |
email.onAccept | boolean | false | Envía PDF + XML al receptor cuando Hacienda acepta. |
email.onReject | boolean | false | Notifica al emisor cuando Hacienda rechaza el comprobante. |
email.replyTo | — | Reply-To override. Si se omite, usa emisor.email → tenant.correo. El From: queda fijo en facturas@kyrapps.com. |
Campos específicos por tipo
Algunos campos solo aplican a ciertos tipos. Vea las páginas por tipo para los ejemplos completos.
| Campo | Tipos donde aplica | Descripción |
|---|---|---|
omitirReceptor: "true" | TE; NC contra TE anónimo | Comprobante sin receptor identificado. |
informacionReferencia | NC, ND, FEC | Referencia al comprobante original. Array u objeto con tipoDoc, numero (clave 50 dígitos), fechaEmision, codigo (motivo) y razon. |
identifExtranjero · senasExtranjero | FEE | Datos del comprador residente fuera de CR (con tipoId = "05"). |
Datos del emisor desde el tenant
Cada solicitud incluye un bloque emisor que describe a su empresa. Repetir esos datos en cada factura es tedioso, así que KyrFact combina el payload con el perfil del emisor guardado en el tenant — el payload tiene prioridad por cada campo, y los campos omitidos se rellenan automáticamente desde el tenant.
Configure el perfil del emisor una sola vez vía POST o PATCH /v1/admin/tenants. Los siguientes campos del tenant se usan como valores por defecto:
| Campo del tenant | Campo en emisor | Notas |
|---|---|---|
nombre | emisor.nombre | Nombre legal |
cedula | emisor.id | |
tipoCedula | emisor.tipoId | Mapeado: fisico→01, juridico→02, dimex→03, nite→04 |
nombreComercial | emisor.nombreComercial | |
provincia / canton / distrito | mismo nombre | Códigos de ubicación de Hacienda |
senas | emisor.senas | |
telefono | emisor.tel | |
correo | emisor.email | |
codigoActividad | emisor.codigoActividadEmisor | Código de actividad económica de Hacienda |
logoName | emisor.logoUrl | Resuelto a https://facturas-storage.s3.us-east-1.amazonaws.com/logos/<logoName> |
Tres formas de llamar
1. Payload completo — todos los campos del emisor incluidos. Funciona igual que antes; los valores del payload ganan:
POST /v1/invoices
{
"type": "FE",
"emisor": {
"codigoActividadEmisor": "6201.0",
"nombre": "Acme S.A.",
"tipoId": "02", "id": "3101234567",
"provincia": "1", "canton": "1", "distrito": "01",
"senas": "Calle 5, Avenida 2",
"email": "facturas@acme.cr"
},
...
}
2. Sin emisor — todos los campos requeridos se leen del tenant:
POST /v1/invoices
{
"type": "FE",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC", "tipoCambio": 1,
"totalComprobante": 113, "totalImpuestos": 13,
"totalVentas": 100, "totalVentasNeta": 100,
"detalles": { "1": { ... } }
}
3. Override parcial — sobrescriba solo los campos que cambian. Útil cuando una empresa tiene varias actividades registradas:
POST /v1/invoices
{
"type": "FE",
"emisor": { "codigoActividadEmisor": "8314.0" },
...
}
Lo que sigue siendo requerido
- Los siete campos requeridos por Hacienda (
codigoActividadEmisor,nombre,tipoId,id,provincia,canton,distrito) deben estar definidos en algún lugar — sea en el payload o en el tenant. Los campos faltantes generan un 400 con la lista exacta. - El bloque
receptorno se rellena desde el tenant — el receptor es el comprador, no el emisor. Use los patrones estándar de FE/TE/NC para suministrarlo (oomitirReceptor: "true"para TE anónimos).
Forma del error de validación
Si ni el payload ni el tenant tienen todos los campos requeridos:
HTTP 400
{
"statusCode": 400,
"error": "Bad Request",
"message": "emisor is incomplete after merging with tenant defaults — missing [codigoActividadEmisor, provincia, canton, distrito]. Either include these fields in the request body or set them on the tenant via PATCH /v1/admin/tenants/:id.",
"missingEmisorFields": ["codigoActividadEmisor", "provincia", "canton", "distrito"]
}
La combinación se ejecuta antes de asignar el consecutivo, así una solicitud con emisor incompleto nunca quema un número de Hacienda.
Todos los endpoints
Cada URL que expone la API. Todos están limitados a su tenant por la llave de API.
Emisión y consulta de comprobantes
| Método | Ruta | Propósito |
|---|---|---|
POST | /v1/invoices | Emitir un comprobante nuevo (cualquier tipo) |
GET | /v1/invoices | Listar comprobantes con filtros (paginado) |
GET | /v1/invoices/:id | Obtener la proyección pública de un comprobante |
Documentos (PDF + XML)
| Método | Ruta | Devuelve |
|---|---|---|
GET | /v1/invoices/:id/pdf | El PDF (binario, application/pdf) |
GET | /v1/invoices/:id/xml | El XML firmado del FE/TE/NC/ND (application/xml) |
GET | /v1/invoices/:id/respuesta-xml | El MensajeHacienda firmado por Hacienda |
POST | /v1/invoices/:id/resend-email | Re-enviar el correo al cliente con los 3 documentos adjuntos |
Contadores
| Método | Ruta | Propósito |
|---|---|---|
GET | /v1/consecutivos/next?type=FE&sucursal=1 | Ver el siguiente consecutivo para (tipo, sucursal) — solo lectura, no consume |
Filtros en GET /v1/invoices
GET /v1/invoices?type=FE&estado=completa&limit=20&offset=0
&receptorCedula=100100100
&q=widget
&from=2026-01-01&to=2026-12-31
&kyrfactOnly=true
| Parámetro | Descripción |
|---|---|
type | FE / TE / NC / ND / FEC / FEE — filtra por tipo de comprobante |
estado | pendiente / enviada / completa / rechazada / cancelada |
receptorCedula | Coincidencia exacta con la cédula del receptor |
q | Búsqueda por substring en el nombre del receptor |
from / to | Rango de fechas (ISO date) sobre la fecha del documento |
kyrfactOnly | true para excluir comprobantes legados |
limit / offset | Paginación (por defecto 20 / 0) |
Reporte PDF de uso
Genera un PDF con todas las facturas que su empresa emitió en un rango de fechas, abarcando los tres tipos de almacenamiento (FE/TE/NC/ND, FEC, FEE), con totales por tarifa de IVA y desglose por tipo y estado al pie. Auténtica con su propia llave de tenant — el endpoint solo retorna sus propios datos.
Request
GET /v1/reports/usage.pdf?from=YYYY-MM-DD&to=YYYY-MM-DD[&estado=…][&type=FE,TE,…]
Authorization: Bearer kf_live_… # o kf_test_… para sandbox
El tenant se deduce automáticamente de la llave API — no se envía ningún tenantId. Solo recibirá facturas emitidas por el tenant asociado a esa llave.
Parámetros
| Parámetro | Requerido | Descripción |
|---|---|---|
from | sí | Fecha inicial inclusiva, formato YYYY-MM-DD (UTC, inicio del día). |
to | sí | Fecha final inclusiva, formato YYYY-MM-DD (UTC, fin del día). Debe ser ≥ from. |
estado | no | Filtrar por estado del documento — p.ej. completa para incluir solo facturas aceptadas por Hacienda. Valores típicos: completa, rechazada, enviada, pendiente, cancelada. |
type | no | Lista de tipos separados por coma — cualquier subconjunto de FE,TE,NC,ND,FEC,FEE. Omita el parámetro para incluir los seis. Códigos inválidos devuelven 400 listando los problemas. |
Respuesta
Sobre éxito: 200 OK con Content-Type: application/pdf y un encabezado Content-Disposition apuntando al nombre de archivo:
Content-Type: application/pdf
Content-Disposition: attachment; filename="Reporte-de-Uso-<slug>[-<tipos>]-<from>_<to>.pdf"
Cache-Control: no-store
El <slug> se construye desde tenant.nombreComercial (o nombre, o cedula como último recurso) — minúsculas, acentos eliminados, no-alfanuméricos reemplazados por -, máximo 60 caracteres. Cuando se filtra por tipo, se agrega -FE-TE (etc.) antes del rango de fechas.
Layout del PDF
- Encabezado centrado: título (con subtítulo de estado y/o tipos cuando se filtra),
nombreComercial,cedula, "Rango de Fechas" y el rango. Numeración de página arriba a la derecha. - Tabla detalle (40 filas por página):
Tipo · Consecutivo · Cliente · Fecha · Total Imp - Exo · Total Fact. Filas alternadas en blanco/azul claro como en fs-fe. - Convención de moneda: las columnas de total muestran el equivalente en CRC usando el
tipocambiode cada documento. Filas contipocambio ≠ 1llevan un sufijo*. - Pie de página en cada hoja: nota explicando el
*("monto convertido a CRC con el tipo de cambio del comprobante"). - Bloque de totales (última página, abajo a la derecha):
Total Sin Impuestos·Total Con Impuestos·Total Impuestos al 13%·al 4%·al 2%·al 1%. - Desglose adicional (última página, abajo a la izquierda): por tipo de comprobante y por estado.
Cómo se calculan los totales
El reporte sólo suma facturas cuyo evento original fue aceptado por Hacienda (documentos[0].estado === 'completa'). Las notas de crédito (NC) se restan de los totales, no se suman — su monto y su IVA contribuyen con signo negativo al Total Con Impuestos, al Total Sin Impuestos, al desglose por tarifa y a la fila NC del bloque "Por tipo". Las notas de débito (ND), FE, TE, FEC y FEE suman normal. Esto refleja el efecto fiscal real: una NC anula o reduce el IVA cobrado por una factura previa.
Sobre cancelaciones múltiples: Hacienda no impone unicidad 1:1 entre NC y factura origen — si emite varias NCs aceptadas contra el mismo numero de referencia, el reporte restará cada una. Esto puede llevar al neto por debajo de cero. El reporte es fiel al estado en Hacienda; la solución es no emitir NCs duplicadas.
Ejemplos
Reporte completo del mes (todos los tipos, todos los estados):
curl -H "Authorization: Bearer $KYRFACT_KEY" \
"$KYRFACT_API_BASE/v1/reports/usage.pdf?from=2026-04-01&to=2026-04-30" \
-o reporte.pdf
Solo FE y TE aceptadas:
curl -H "Authorization: Bearer $KYRFACT_KEY" \
"$KYRFACT_API_BASE/v1/reports/usage.pdf?from=2026-04-01&to=2026-04-30&type=FE,TE&estado=completa" \
-o fe-te-completas.pdf
Solo notas de crédito y débito:
curl -H "Authorization: Bearer $KYRFACT_KEY" \
"$KYRFACT_API_BASE/v1/reports/usage.pdf?from=2026-04-01&to=2026-04-30&type=NC,ND" \
-o notas.pdf
Errores
| Estado | Cuándo |
|---|---|
400 | from o to faltante o malformado · from > to · algún type fuera de {FE,TE,NC,ND,FEC,FEE}. |
401 | Falta el header Authorization o llave inválida. |
403 | Llave revocada o ambiente incorrecto (ej. kf_test_… contra una operación live). |
Para integraciones programáticas, prefiera consumir este endpoint directamente sobre intentar reproducir el PDF localmente — el layout puede evolucionar en versiones futuras.
Comparar tipos de comprobante
Los seis tipos comparten la misma estructura de request — las diferencias son cuáles campos son requeridos y cómo se organizan las partes.
| Tipo | Use cuando… | Receptor requerido | Referencia requerida |
|---|---|---|---|
FEFactura Electrónica |
Venta estándar a un comprador identificado (B2B o B2C con cédula) | Sí | No |
TETiquete Electrónico |
Tiquete de POS al consumidor sin identificar | Opcional | No |
NCNota de Crédito |
Anular o reducir un FE/TE previo | Sí — debe coincidir con el original | Sí — clave del FE/TE previo |
NDNota de Débito |
Cargo adicional sobre un FE/TE previo | Sí — debe coincidir con el original | Sí — clave del FE/TE previo |
FECFactura Electrónica de Compra |
Registrar compra a proveedor sin facturación electrónica | Sí — el proveedor | Sí — el documento en papel |
FEEFactura Electrónica de Exportación |
Venta de exportación a comprador extranjero | Sí | No |
Árbol de decisión rápido
- ¿Está vendiendo algo?
- Comprador con cédula CR →
FE - Cliente anónimo de mostrador →
TE - Comprador extranjero (exportación) →
FEE
- Comprador con cédula CR →
- ¿Está ajustando un comprobante anterior?
- Reduciendo o anulando →
NC - Agregando un cargo →
ND
- Reduciendo o anulando →
- ¿Está registrando una compra que hizo? →
FEC
FE — Factura Electrónica
Venta B2B / B2C estándar a un comprador identificado. El tipo de comprobante más común.
Payload
{
"type": "FE",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC",
"tipoCambio": 1,
"emisor": {
"codigoActividadEmisor": "6201.0",
"nombre": "Acme Comercializadora S.A.",
"tipoId": "02", "id": "3101234567",
"provincia": "1", "canton": "1", "distrito": "01",
"senas": "Calle 5, Avenida 2",
"email": "facturas@acme.cr"
},
"receptor": {
"nombre": "Cliente Ejemplo S.A.",
"tipoId": "02", "id": "3101987654",
"provincia": "1", "canton": "1", "distrito": "01",
"email": "cliente@example.com"
},
"totalServGravados": 100, "totalGravados": 100,
"totalVentas": 100, "totalVentasNeta": 100,
"totalImpuestos": 13, "totalComprobante": 113,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Servicio profesional",
"precioUnitario": 100, "montoTotal": 100, "subTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "8314100000200",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 13 } }
}
},
"options": { "email": { "onAccept": true } }
}
Endpoint: POST /v1/invoices
TE — Tiquete Electrónico
Tiquete de mostrador / POS al consumidor sin identificar. Misma estructura que FE, pero el bloque de receptor es opcional y típicamente mínimo.
Payload (receptor anónimo)
{
"type": "TE",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC",
"tipoCambio": 1,
"omitirReceptor": "true",
"emisor": { /* …igual que FE */ },
"totalMercGravada": 100, "totalGravados": 100,
"totalVentas": 100, "totalVentasNeta": 100,
"totalImpuestos": 13, "totalComprobante": 113,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Unid",
"detalle": "Sandwich + bebida",
"precioUnitario": 100, "montoTotal": 100, "subTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "5611901100100",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 13 } }
}
}
}
Endpoint: POST /v1/invoices
Si el cliente pide un tiquete con su cédula, ponga omitirReceptor: "false" y proporcione el bloque de receptor — misma estructura que FE.
NC — Nota de Crédito
Anula o reduce un FE/TE previo. El receptor debe ser el mismo que el del documento original.
Payload
{
"type": "NC",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC",
"tipoCambio": 1,
"emisor": { /* …igual que FE */ },
"receptor": { /* …DEBE coincidir con el receptor del FE/TE referenciado */ },
"totalServGravados": 100, "totalGravados": 100,
"totalVentas": 100, "totalVentasNeta": 100,
"totalImpuestos": 13, "totalComprobante": 113,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Anulación de servicio facturado por error",
"precioUnitario": 100, "montoTotal": 100, "subTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "8314100000200",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 13 } }
}
},
"informacionReferencia": [{
"tipoDoc": "01",
"numero": "50601012600031012345670010000101000000000412345678",
"fechaEmision": "2026-04-15T10:00:00-06:00",
"codigo": "01",
"razon": "Anulación por error de digitación"
}]
}
Endpoint: POST /v1/invoices
Códigos de informacionReferencia
| Campo | Valores comunes |
|---|---|
tipoDoc | 01 el documento referenciado es FE, 04 TE |
numero | La clave de 50 caracteres del FE/TE original |
codigo | 01 Anula documento de referencia, 02 Corrige texto, 04 Referencia a otro documento, 99 Otros |
razon | Texto libre con la justificación, máx 180 caracteres |
ND — Nota de Débito
Agrega un cargo sobre un FE/TE previo (e.g. recargo por mora, aumento de precio acordado). Misma estructura que NC.
Payload
{
"type": "ND",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC",
"tipoCambio": 1,
"emisor": { /* …igual que FE */ },
"receptor": { /* …DEBE coincidir con el receptor del FE/TE referenciado */ },
"totalServGravados": 50, "totalGravados": 50,
"totalVentas": 50, "totalVentasNeta": 50,
"totalImpuestos": 6.5, "totalComprobante": 56.5,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Cargo adicional por mora",
"precioUnitario": 50, "montoTotal": 50, "subTotal": 50,
"montoTotalLinea": 56.5,
"codigoCABYS": "8314100000200",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 6.5 } }
}
},
"informacionReferencia": [{
"tipoDoc": "01",
"numero": "50601012600031012345670010000101000000000412345678",
"fechaEmision": "2026-04-15T10:00:00-06:00",
"codigo": "04",
"razon": "Cargo adicional sobre factura original"
}]
}
Endpoint: POST /v1/invoices
FEC — Factura Electrónica de Compra
Registra una compra que usted hizo a un proveedor que no puede emitir comprobantes electrónicos (proveedores de papel, proveedores extranjeros, etc.). Usted — el comprador — es quien firma.
Use la misma convención que los demás tipos: emisor es su tenant, receptor es el proveedor. KyrFact organiza el payload final correctamente.
Payload
{
"type": "FEC",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "CRC",
"tipoCambio": 1,
"emisor": { /* …su tenant — igual que FE */ },
"receptor": {
"nombre": "Proveedor Servicios Test",
"tipoId": "02", "id": "3101234500",
"provincia": "1", "canton": "1", "distrito": "01",
"senas": "Oficina San José centro",
"email": "proveedor@example.cr"
},
"totalServGravados": 100, "totalGravados": 100,
"totalVentas": 100, "totalVentasNeta": 100,
"totalImpuestos": 13, "totalComprobante": 113,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Servicio comprado al proveedor",
"precioUnitario": 100, "montoTotal": 100, "subTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "8314100000200",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 13 } }
}
},
"informacionReferencia": [{
"tipoDoc": "08",
"numero": "PAPER-INV-12345",
"fechaEmision": "2026-04-28T10:00:00-06:00",
"codigo": "04",
"razon": "Compra a proveedor sin facturación electrónica"
}]
}
Endpoint: POST /v1/invoices
FEE — Factura Electrónica de Exportación
Venta de exportación a un comprador extranjero. La moneda suele ser USD; el receptor con frecuencia usa tipoId: "05" con un ID fiscal extranjero.
Payload (receptor residente en CR)
{
"type": "FEE",
"fecha": "2026-04-28T10:00:00-06:00",
"condicionVenta": "01",
"mediosPago": ["01"],
"codMoneda": "USD",
"tipoCambio": 510,
"emisor": { /* …su tenant — igual que FE */ },
"receptor": {
"nombre": "Foreign Buyer LLC",
"tipoId": "02", "id": "3101000000",
"provincia": "1", "canton": "1", "distrito": "01",
"senas": "Foreign address",
"email": "buyer@example.com"
},
"totalServGravados": 100, "totalGravados": 100,
"totalVentas": 100, "totalVentasNeta": 100,
"totalImpuestos": 13, "totalComprobante": 113,
"detalles": {
"1": {
"cantidad": 1, "unidadMedida": "Sp",
"detalle": "Servicio exportado",
"precioUnitario": 100, "montoTotal": 100, "subTotal": 100,
"montoTotalLinea": 113,
"codigoCABYS": "8314100000200",
"impuesto": { "1": { "codigo": "01", "codigoTarifa": "08", "tarifa": 13, "monto": 13 } }
}
}
}
Endpoint: POST /v1/invoices
Receptores realmente extranjeros (tipoId: "05" con identifExtranjero) aún no están soportados en v1. Contáctenos si lo necesita — está en el roadmap a corto plazo.
Estado y polling
Cada POST /v1/invoices devuelve inmediatamente con status: "enviada". El estado luego transiciona a uno de:
| Estado | Significado |
|---|---|
enviada | Enviado; esperando el veredicto de Hacienda. |
completa | Hacienda aceptó. PDF y XML de respuesta disponibles, correo al cliente enviado (si está configurado). |
rechazada | Hacienda rechazó. mensajeHacienda tiene la razón; el XML de respuesta tiene el detalle completo. |
Para detectar la transición, haga polling de:
GET /v1/invoices/:id
while true; do
STATUS=$(curl -s -H "Authorization: Bearer $KEY" \
$KYRFACT_API_BASE/v1/invoices/$ID | jq -r '.status')
case "$STATUS" in
completa|rechazada) break ;;
*) sleep 30 ;;
esac
done
El tiempo mediano hasta completa es ~90 segundos. Peor caso ~5 minutos. No haga polling más rápido que cada 30s.
Alternativamente, configure options.email.onAccept: true y el correo al cliente llega en el momento en que el estado pasa a completa.
Idempotencia
Una llave de idempotencia es una manera de decirnos "si ya has visto este request exacto, devuélveme la misma respuesta en lugar de hacer el trabajo de nuevo". Existe porque las redes fallan.
El problema que resuelve
Sin ella, este escenario rompe la facturación:
- Su app envía
POST /v1/invoices. - KyrFact emite el comprobante, lo firma, lo envía a Hacienda — y empieza a responder con un 200.
- El paquete de respuesta se pierde en el camino. Su código hace timeout.
- Su código reintenta → se emite un segundo comprobante. Dos consecutivos quemados, dos correos enviados, el cliente cobrado dos veces.
Cómo usarla
Escoja un string que identifique únicamente la operación (no la llamada HTTP) — típicamente su ID interno de orden o transacción — y envíelo en el header Idempotency-Key:
POST /v1/invoices
Authorization: Bearer kf_live_…
Idempotency-Key: orden-12345
Content-Type: application/json
{ ...el payload del comprobante... }
Si su código reintenta la misma operación (problema de red, crash de la app, redelivery de cola), envíe la misma llave. KyrFact la reconoce en una ventana de 24 horas y devuelve la respuesta original sin emitir un comprobante nuevo.
Cómo escoger una buena llave
| Ejemplo | Por qué | |
|---|---|---|
| Buena | orden-12345 |
Estable entre reintentos — misma orden, misma llave. |
| Buena | checkout-2026-04-28-12345 |
ID de orden con namespace de fecha. Estable entre reintentos. |
| Mala | crypto.randomUUID() |
Valor aleatorio nuevo en cada llamada → cada reintento parece un request nuevo → ocurren duplicados. |
| Mala | Date.now() |
Diferente en cada reintento. Mismo problema. |
Reglas
- Cualquier string de hasta 256 caracteres.
- Se cachea por 24 horas. Después de eso, la misma llave se trata como un request nuevo.
- Limitada por tenant — Tenant A y Tenant B pueden usar la misma llave sin interferir.
- Es opcional pero fuertemente recomendada. Sin ella, los reintentos le cuestan consecutivos reales.
También funciona en POST /v1/invoices/:id/resend-email si quiere que el botón de reenvío sea seguro contra clicks dobles.
Validación previa al consecutivo
KyrFact corre tres capas de validación antes de asignar un consecutivo o llamar a php-api / Hacienda. Si algo está mal, devolvemos un solo 400 Bad Request con todos los problemas en errors[], así usted los corrige en una sola iteración.
Forma del error
HTTP 400
{
"statusCode": 400,
"error": "Bad Request",
"message": "<motivo>",
"errors": [
"<cada problema encontrado>",
...
]
}
Orden de las capas
- Emisor merge + campos requeridos. El bloque
emisordel payload se combina con el perfil del tenant (vea Datos del emisor). Después se valida que los siete campos requeridos por Hacienda estén presentes (codigoActividadEmisor,nombre,tipoId,id,provincia,canton,distrito). - Estructura del payload. Receptor, referencias, encabezado y cada línea — vea la lista debajo.
- Matemática. Invariantes por línea y totales del documento (tolerancia ±0.01 por redondeo).
Qué se valida
Encabezado
| Campo | Regla |
|---|---|
fecha | ISO 8601 con offset (ej. 2026-04-29T10:00:00-06:00). |
condicionVenta | Código Hacienda en 01..15 o 99. Si 99, también requiere condicionVentaOtros. |
mediosPago[] | Array no vacío; cada elemento en 01..07 o 99. |
codMoneda | ISO 4217 — 3 letras mayúsculas (CRC, USD, EUR…). |
tipoCambio | Mayor que 0. |
totalComprobante | Mayor que 0 (Hacienda rechaza facturas en cero). |
Emisor
| Campo | Regla |
|---|---|
tipoId | 01 física, 02 jurídica, 03 DIMEX, 04 NITE. El emisor no puede ser extranjero (05). |
id (cédula) | Formato según tipoId: 9 dígitos para física, 10 para jurídica, 11–12 para DIMEX, 10 para NITE. |
email | Formato básico de correo (si se incluye). |
Receptor
| Campo / regla | Detalle |
|---|---|
| Bloque receptor presente | Requerido para FE / NC / ND / FEC / FEE. Solo TE permite anónimo. |
nombre | No vacío. |
tipoId | En 01..05. El 05 es para extranjeros. |
Foreign receptor (tipoId=05) | Requiere identifExtranjero. |
| Receptor residente CR | Requiere provincia, canton y distrito. |
id (cédula) | Formato según tipoId (igual que emisor). |
email | Formato básico de correo (si se incluye). |
Por tipo de comprobante
| Tipo | Reglas adicionales |
|---|---|
TE / FE | omitirReceptor: "true" permitido (anónimo). Para los demás tipos no es permitido. |
NC | Requiere informacionReferencia. numero debe ser una clave Hacienda de 50 dígitos. codigo en 01..05 o 99. |
ND | Igual que NC. |
FEC | Requiere informacionReferencia apuntando al documento físico. receptor.senas debe ser no vacío (php-api silenciosamente rechaza FEC con señas vacías y la solicitud nunca llega a Hacienda). |
Por línea
| Campo / regla | Detalle |
|---|---|
detalle | Texto descriptivo no vacío. |
unidadMedida | Código Hacienda no vacío (Sp, Unid, kg, etc.). |
codigoCABYS | 13 dígitos. |
impuesto.1.codigo + codigoTarifa | Ambos requeridos cuando hay impuesto. |
naturalezaDescuento | Requerido cuando montoDescuento > 0. |
| Soporte de exoneración | Si tarifaExoneracion > 0: requeridos tipoDocumento, numeroDocumento, nombreInstitucion, fechaEmision. |
| Invariantes matemáticos | montoTotal = cantidad × precioUnitario; subTotal = montoTotal − montoDescuento; impuesto.monto = subTotal × tarifa/100; montoTotalLinea = subTotal + impuesto.monto. |
tarifa | En {0, 0.5, 1, 2, 4, 13}. Se auto-rellena baseImponible = subTotal − montoDescuento cuando tarifa > 0 y no se envía explícitamente. |
Totales
Cada total que envía debe coincidir con la suma derivada de las líneas, dentro de ±0.01. Si lo omite (cuando es opcional) se ignora; si lo envía, se valida.
totalVentas,totalDescuentos,totalVentasNeta,totalImpuestos,totalExonerado,totalComprobante— todos contra las líneas.
Ejemplo: respuesta con varios errores
POST /v1/invoices
{
"type": "FEC",
"fecha": "yesterday",
"codMoneda": "crc",
"tipoCambio": -1,
"mediosPago": ["77"],
"receptor": { "nombre": "Proveedor X", "tipoId": "02", "id": "3101234567" },
"informacionReferencia": [{
"tipoDoc": "08",
"numero": "PAPER-INV-1",
"fechaEmision": "2026-04-29",
"codigo": "04"
}],
...
}
HTTP 400
{
"statusCode": 400,
"error": "Bad Request",
"message": "invoice payload validation failed",
"errors": [
"fecha must be ISO 8601 with offset (got \"yesterday\", expected e.g. \"2026-04-29T10:00:00-06:00\").",
"mediosPago[0] \"77\" is not a Hacienda payment-method code (valid: 01-07, 99).",
"codMoneda must be a 3-letter ISO 4217 code in uppercase (got \"crc\").",
"tipoCambio must be > 0 (got -1).",
"informacionReferencia[0].razon is required (free-text reason, max 180 chars).",
"FEC.receptor.senas is required and must be non-empty (php-api gen_xml_fec rejects FEC with empty supplier senas)."
]
}
Como las validaciones corren antes de asignar un consecutivo, una solicitud con cualquier error nunca quema un número de Hacienda.
Lo que no validamos (lo deja Hacienda)
- Registro CABYS — solo validamos formato (13 dígitos). El catálogo CABYS lo mantiene Hacienda y cambia mensualmente.
- Catálogo de actividades económicas — el código tiene que estar registrado a su cédula con Hacienda; no podemos verificar eso desde aquí.
- Listas completas de
impuesto.codigoycodigoTarifa— Hacienda ha agregado códigos en revisiones recientes; preferimos que un código nuevo válido pase y deje que Hacienda lo confirme, en vez de rechazarlo aquí. - Política de "fecha demasiado antigua" — la ventana de Hacienda puede cambiar.
Errores
Todos los errores siguen el formato:
{
"statusCode": 400,
"error": "Bad Request",
"message": "explicación legible"
}
Los 400 de validación previa al consecutivo incluyen además un campo errors[] con cada problema encontrado — vea Validación.
| Estado | Cuándo |
|---|---|
400 | Error de validación — matemática de totales, campo requerido faltante, tipo de comprobante no soportado, falta informacionReferencia en NC/ND/FEC. |
401 | Header Authorization faltante o inválido. |
403 | Llave revocada, o ambiente equivocado (e.g. kf_test_… contra una operación live). |
404 | Comprobante no encontrado, o pertenece a otro tenant. |
409 | Colisión de Idempotency-Key con un request concurrente todavía en curso. |
Errores de validación de totales
Cuando los totales no coinciden con lo que recalculamos a partir de las líneas, recibe un error estructurado señalando los campos con diferencia:
{
"statusCode": 400,
"message": "invoice totals failed validation",
"errors": [
{ "path": "totalComprobante", "expected": 113, "actual": 100, "diff": -13 },
{ "path": "detalles.1.impuesto.1.monto", "expected": 13, "actual": 12.5, "diff": -0.5 }
]
}
Corrija y reintente — ningún consecutivo se consume con payloads inválidos.
Rechazos de Hacienda
No son errores HTTP. El comprobante se emitió y se envió, pero Hacienda encontró un problema con los datos. Verá:
HTTP 200 OK
{
"id": "...",
"status": "rechazada",
"mensajeHacienda": "El código del tipo de identificación no corresponde…",
...
}
Inspeccione mensajeHacienda para ver la razón o haga GET /v1/invoices/:id/respuesta-xml para el detalle completo. La mayoría de rechazos son correcciones de una línea — corrija, reenvíe con un Idempotency-Key nuevo.