K KyrFact · API
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.

i

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:

  1. Autenticación — obtenga su llave de API
  2. Inicio rápido — emita su primera FE en 90 segundos
  3. Estructura del payload — referencia campo por campo
  4. Endpoints — todas las URLs disponibles
  5. 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:

PrefijoAmbientePara
kf_live_…ProducciónComprobantes reales enviados a Hacienda
kf_test_…SandboxAmbiente 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.

i

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

CampoTipoRequeridoDescripción
typestringTipo de comprobante. Uno de FE · TE · NC · ND · FEC · FEE.
fechastring ISO-8601Fecha y hora con offset horario. Ej. "2026-04-28T10:00:00-06:00". Hacienda exige el offset.
condicionVentastringCódigo de condición de venta. "01" contado, "02" crédito, "99" otros.
condicionVentaOtrosstringCuando condicionVenta = "99"Descripción libre de la condición.
mediosPagostring[]Códigos de medio de pago. Mín. 1. "01" efectivo, "02" tarjeta, "03" cheque, "04" transferencia, "99" otros.
codMonedastringCódigo ISO-4217 de la moneda. "CRC", "USD", "EUR".
tipoCambionumberTipo de cambio aplicado. 1 cuando moneda = CRC.
omitirReceptor"true" · "false"NoSolo válido en TE y FE. "true" para tiquetes anónimos. Default: "false".
otrosstringNoTexto libre que se imprime en el PDF.
sucursalstringNoIdentificador de sucursal (3 dígitos). Default: el del tenant.
terminalstringNoIdentificador de terminal (5 dígitos). Default: "00001".
proveedorSistemastringNoNombre 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.

CampoTipoRequeridoDescripción
totalVentasnumberSuma de subtotales antes de descuentos.
totalVentasNetanumbertotalVentas − totalDescuentos.
totalComprobantenumberTotal final que el comprador paga (incluye impuestos, descuentos, otros cargos).
totalImpuestosnumberCuando hay líneas con tarifaSuma de impuestos del comprobante.
totalDescuentosnumberNoSuma de descuentos. Default: 0.
totalServGravados · totalServExentos · totalServExonerados · totalServNoSujetonumberNoSubtotales de servicios por tipo de IVA.
totalMercGravada · totalMercExenta · totalMercExonerada · totalMercNoSujetanumberNoSubtotales de mercancías por tipo de IVA.
totalGravados · totalExentos · totalExonerado · totalNoSujetonumberNoSubtotales agregados (servicios + mercancías) por tipo de IVA.
totalImpuestosAsumidosFabricanumberNoIVA cobrado en fábrica (régimen especial).
totalIVADevueltonumberNoIVA devuelto al cliente (canasta básica, etc.).
totalOtrosCargosnumberNoOtros 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.

CampoTipoDescripción
codigoActividadEmisorstringCódigo de actividad económica de Hacienda. Ej. "6201.0".
nombrestringNombre legal del emisor.
tipoIdstringTipo de identificación: "01" física, "02" jurídica, "03" dimex, "04" nite.
idstringCédula del emisor (sin guiones).
nombreComercialstringNombre comercial.
provincia · canton · distritostringCódigos de ubicación de Hacienda. Ej. "1" · "1" · "01".
senasstringOtras señas (calle, avenida, edificio).
codigoPaisTelstringCódigo de país telefónico (ej. "506").
telstringTeléfono.
emailstringCorreo electrónico del emisor.
registroFiscal8707stringRegistro fiscal opcional para regímenes especiales.
logoUrlURLURL completa de la imagen del logo (PNG o JPG).
logoAnchonumber 20-200Ancho 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.
pieFacturastringTexto al pie del PDF.
paginaWebstringURL 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).

CampoTipoDescripción
codigoActividadReceptorstringCódigo de actividad económica del comprador (requerido en FEC).
nombrestringNombre del comprador.
tipoIdstring"01" física · "02" jurídica · "03" dimex · "04" nite · "05" extranjero (FEE).
idstringCédula del comprador. Omitir cuando tipoId = "05".
identifExtranjerostringIdentificación extranjera (DNI, pasaporte). Requerida cuando tipoId = "05".
nombreComercialstringNombre comercial.
provincia · canton · distritostringCódigos de ubicación CR. Omitir cuando comprador extranjero.
senasstringOtras señas. Requerido en FEC.
senasExtranjerostringDirección extranjera (FEE).
codigoPaisTel · tel · emailstringDatos 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
      }
    }
  }
}
CampoTipoRequeridoDescripción
cantidadnumberCantidad de unidades.
unidadMedidastringUnidad de medida de Hacienda. "Sp" servicios profesionales, "Unid" unidad, "kg" kilo, "L" litro, "m" metro.
detallestringDescripción de la línea.
precioUnitarionumberPrecio por unidad antes de descuentos e impuestos.
subTotalnumbercantidad × precioUnitario.
montoTotalnumberIgual a subTotal antes de descuentos.
montoTotalLineanumberTotal de la línea con impuestos: subTotal − montoDescuento + monto IVA.
codigoCABYSstringCódigo CABYS de 13 dígitos. KyrFact valida la longitud, no el registro (Hacienda actualiza el catálogo mensualmente).
baseImponiblenumberAutoBase sobre la que se calcula el IVA. Auto-completado a subTotal − montoDescuento cuando tarifa > 0 y el tipo no es FEE.
montoDescuentonumberNoDescuento aplicado a la línea.
naturalezaDescuentostringCuando hay descuentoDescripción del descuento. Requerido cuando montoDescuento > 0.
impuestoobjetoCuando aplica IVASub-objeto con clave numérica string. Cada entrada describe un impuesto.

Sub-bloque impuesto["1"]:

CampoTipoDescripción
codigostringCódigo del tributo. "01" IVA, "02" ISC, etc.
codigoTarifastringTarifa aplicada. "08" 13% general, "04" 4% institucional, "02" 1%, "01" exento.
tarifanumberPorcentaje. Ej. 13.
montonumberMonto del impuesto: baseImponible × tarifa / 100.
exoneracionobjetoBloque 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.

CampoTipoDefaultDescripció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.onAcceptbooleanfalseEnvía PDF + XML al receptor cuando Hacienda acepta.
email.onRejectbooleanfalseNotifica al emisor cuando Hacienda rechaza el comprobante.
email.replyToemailReply-To override. Si se omite, usa emisor.emailtenant.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.

CampoTipos donde aplicaDescripción
omitirReceptor: "true"TE; NC contra TE anónimoComprobante sin receptor identificado.
informacionReferenciaNC, ND, FECReferencia al comprobante original. Array u objeto con tipoDoc, numero (clave 50 dígitos), fechaEmision, codigo (motivo) y razon.
identifExtranjero · senasExtranjeroFEEDatos 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 tenantCampo en emisorNotas
nombreemisor.nombreNombre legal
cedulaemisor.id
tipoCedulaemisor.tipoIdMapeado: fisico01, juridico02, dimex03, nite04
nombreComercialemisor.nombreComercial
provincia / canton / distritomismo nombreCódigos de ubicación de Hacienda
senasemisor.senas
telefonoemisor.tel
correoemisor.email
codigoActividademisor.codigoActividadEmisorCódigo de actividad económica de Hacienda
logoNameemisor.logoUrlResuelto 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 receptor no se rellena desde el tenant — el receptor es el comprador, no el emisor. Use los patrones estándar de FE/TE/NC para suministrarlo (o omitirReceptor: "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"]
}
i

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étodoRutaPropósito
POST/v1/invoicesEmitir un comprobante nuevo (cualquier tipo)
GET/v1/invoicesListar comprobantes con filtros (paginado)
GET/v1/invoices/:idObtener la proyección pública de un comprobante

Documentos (PDF + XML)

MétodoRutaDevuelve
GET/v1/invoices/:id/pdfEl PDF (binario, application/pdf)
GET/v1/invoices/:id/xmlEl XML firmado del FE/TE/NC/ND (application/xml)
GET/v1/invoices/:id/respuesta-xmlEl MensajeHacienda firmado por Hacienda
POST/v1/invoices/:id/resend-emailRe-enviar el correo al cliente con los 3 documentos adjuntos

Contadores

MétodoRutaPropósito
GET/v1/consecutivos/next?type=FE&sucursal=1Ver 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ámetroDescripción
typeFE / TE / NC / ND / FEC / FEE — filtra por tipo de comprobante
estadopendiente / enviada / completa / rechazada / cancelada
receptorCedulaCoincidencia exacta con la cédula del receptor
qBúsqueda por substring en el nombre del receptor
from / toRango de fechas (ISO date) sobre la fecha del documento
kyrfactOnlytrue para excluir comprobantes legados
limit / offsetPaginació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ámetroRequeridoDescripción
fromFecha inicial inclusiva, formato YYYY-MM-DD (UTC, inicio del día).
toFecha final inclusiva, formato YYYY-MM-DD (UTC, fin del día). Debe ser ≥ from.
estadonoFiltrar por estado del documento — p.ej. completa para incluir solo facturas aceptadas por Hacienda. Valores típicos: completa, rechazada, enviada, pendiente, cancelada.
typenoLista 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 tipocambio de cada documento. Filas con tipocambio ≠ 1 llevan 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

EstadoCuándo
400from o to faltante o malformado · from > to · algún type fuera de {FE,TE,NC,ND,FEC,FEE}.
401Falta el header Authorization o llave inválida.
403Llave revocada o ambiente incorrecto (ej. kf_test_… contra una operación live).
i

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.

TipoUse cuando…Receptor requeridoReferencia requerida
FE
Factura Electrónica
Venta estándar a un comprador identificado (B2B o B2C con cédula) No
TE
Tiquete Electrónico
Tiquete de POS al consumidor sin identificar Opcional No
NC
Nota de Crédito
Anular o reducir un FE/TE previo Sí — debe coincidir con el original Sí — clave del FE/TE previo
ND
Nota de Débito
Cargo adicional sobre un FE/TE previo Sí — debe coincidir con el original Sí — clave del FE/TE previo
FEC
Factura Electrónica de Compra
Registrar compra a proveedor sin facturación electrónica Sí — el proveedor Sí — el documento en papel
FEE
Factura Electrónica de Exportación
Venta de exportación a comprador extranjero 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
  • ¿Está ajustando un comprobante anterior?
    • Reduciendo o anulando → NC
    • Agregando un cargo → ND
  • ¿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

i

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

CampoValores comunes
tipoDoc01 el documento referenciado es FE, 04 TE
numeroLa clave de 50 caracteres del FE/TE original
codigo01 Anula documento de referencia, 02 Corrige texto, 04 Referencia a otro documento, 99 Otros
razonTexto 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:

EstadoSignificado
enviadaEnviado; esperando el veredicto de Hacienda.
completaHacienda aceptó. PDF y XML de respuesta disponibles, correo al cliente enviado (si está configurado).
rechazadaHacienda 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
i

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:

  1. Su app envía POST /v1/invoices.
  2. KyrFact emite el comprobante, lo firma, lo envía a Hacienda — y empieza a responder con un 200.
  3. El paquete de respuesta se pierde en el camino. Su código hace timeout.
  4. 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

EjemploPor 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.
i

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

  1. Emisor merge + campos requeridos. El bloque emisor del 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).
  2. Estructura del payload. Receptor, referencias, encabezado y cada línea — vea la lista debajo.
  3. Matemática. Invariantes por línea y totales del documento (tolerancia ±0.01 por redondeo).

Qué se valida

Encabezado

CampoRegla
fechaISO 8601 con offset (ej. 2026-04-29T10:00:00-06:00).
condicionVentaCó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.
codMonedaISO 4217 — 3 letras mayúsculas (CRC, USD, EUR…).
tipoCambioMayor que 0.
totalComprobanteMayor que 0 (Hacienda rechaza facturas en cero).

Emisor

CampoRegla
tipoId01 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.
emailFormato básico de correo (si se incluye).

Receptor

Campo / reglaDetalle
Bloque receptor presenteRequerido para FE / NC / ND / FEC / FEE. Solo TE permite anónimo.
nombreNo vacío.
tipoIdEn 01..05. El 05 es para extranjeros.
Foreign receptor (tipoId=05)Requiere identifExtranjero.
Receptor residente CRRequiere provincia, canton y distrito.
id (cédula)Formato según tipoId (igual que emisor).
emailFormato básico de correo (si se incluye).

Por tipo de comprobante

TipoReglas adicionales
TE / FEomitirReceptor: "true" permitido (anónimo). Para los demás tipos no es permitido.
NCRequiere informacionReferencia. numero debe ser una clave Hacienda de 50 dígitos. codigo en 01..05 o 99.
NDIgual que NC.
FECRequiere 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 / reglaDetalle
detalleTexto descriptivo no vacío.
unidadMedidaCódigo Hacienda no vacío (Sp, Unid, kg, etc.).
codigoCABYS13 dígitos.
impuesto.1.codigo + codigoTarifaAmbos requeridos cuando hay impuesto.
naturalezaDescuentoRequerido cuando montoDescuento > 0.
Soporte de exoneraciónSi tarifaExoneracion > 0: requeridos tipoDocumento, numeroDocumento, nombreInstitucion, fechaEmision.
Invariantes matemáticosmontoTotal = cantidad × precioUnitario; subTotal = montoTotal − montoDescuento; impuesto.monto = subTotal × tarifa/100; montoTotalLinea = subTotal + impuesto.monto.
tarifaEn {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.codigo y codigoTarifa — 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.

EstadoCuándo
400Error de validación — matemática de totales, campo requerido faltante, tipo de comprobante no soportado, falta informacionReferencia en NC/ND/FEC.
401Header Authorization faltante o inválido.
403Llave revocada, o ambiente equivocado (e.g. kf_test_… contra una operación live).
404Comprobante no encontrado, o pertenece a otro tenant.
409Colisió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.