MENU navbar-image

Introduction

API RESTful para emisión, consulta, descarga y gestión completa de comprobantes electrónicos ante el Ministerio de Hacienda de Costa Rica. Compatible al 100% con la normativa vigente del esquema XML v4.4 y la firma digital XAdES-EPES. Soporta los 7 tipos de comprobante: Factura Electrónica (01), Nota de Débito (02), Nota de Crédito (03), Tiquete Electrónico (04), Factura de Compra (08), Factura de Exportación (09) y Recibo Electrónico de Pago (10). Diseñada para e-commerce, ERP, PMS, CRM y POS.

Bienvenido a la API de Almendro Factura Electrónica

Almendro Factura Electrónica le permite integrar facturación electrónica costarricense en cualquier sistema en minutos. Emita comprobantes, consulte estados, descargue XML y PDF, gestione clientes y productos, reciba notificaciones en tiempo real y administre múltiples contribuyentes desde una sola API.

La API cumple al 100% con la normativa version 4.4, vigente del Ministerio de Hacienda:


Primeros pasos

Integrar facturación electrónica con Almendro requiere solo cuatro pasos. No necesita conocer el esquema XML ni la especificación de firma digital: Almendro se encarga de todo lo técnico.

Paso 1 — Cree su cuenta. Regístrese en fe.almendro.cr. Su cuenta queda activa inmediatamente. Recibirá un correo de verificación para confirmar su dirección de email.

Paso 2 — Obtenga su token API. Desde el portal, vaya a Configuración y genere su token API. Este token es su credencial para autenticarse en todos los endpoints. Guárdelo en un lugar seguro: por razones de seguridad, el token solo se muestra una vez al momento de generarlo. Si lo pierde, puede generar uno nuevo (el anterior se revoca automáticamente).

Paso 3 — Suba su certificado digital. Envíe su archivo .p12 de firma digital (emitido por una entidad certificadora registrada ante el BCCR) con POST /api/v1/public/certificates. Almendro valida el certificado, extrae su información y lo almacena de forma segura. La llave privada nunca se expone en ningún endpoint. Necesitará también el PIN de Hacienda que usa para acceder al sistema de comprobantes electrónicos del Ministerio.

Paso 4 — Emita su primer comprobante. Envíe los datos de la factura con POST /api/v1/public/vouchers. Almendro genera el XML conforme al esquema v4.4 de Hacienda, lo valida contra los esquemas oficiales, lo firma digitalmente con XAdES-EPES y lo envía automáticamente al Ministerio de Hacienda. Usted recibe la clave de 50 dígitos del comprobante y puede consultar el resultado con GET /api/v1/public/vouchers/{key} o recibir una notificación automática por Webhook cuando Hacienda responda.

Recomendación: antes de emitir en producción, pruebe su integración usando el ambiente sandbox. Los comprobantes de sandbox se envían al sandbox oficial de Hacienda, no tienen valor fiscal y no consumen su cuota mensual. Vea la sección "Ambiente Sandbox" más adelante.


Autenticación

Todas las peticiones a la API requieren un Bearer Token en el header Authorization:

Authorization: Bearer {YOUR_AUTH_KEY}

Obtenga su token desde el portal en Configuración. Cada contribuyente genera y administra su propio token de forma independiente.

Aislamiento de datos: cada token está vinculado a un contribuyente específico. Todas sus consultas retornan exclusivamente los comprobantes, clientes, productos y configuración de su propio contribuyente. Es imposible acceder a datos de otro contribuyente, incluso si conociera su token. Este aislamiento se aplica automáticamente en todos los endpoints sin excepción.

Gestión del token: puede consultar los metadatos de su token activo (fecha de creación, último uso) con GET /api/v1/public/tokens/current. Para generar un nuevo token y revocar el anterior, use POST /api/v1/public/tokens.


Formato de respuesta

Todas las respuestas de la API siguen un formato JSON consistente:

{
    "success": true,
    "data": {},
    "message": "Descripción legible en español",
    "errors": null
}

Cuando ocurre un error de validación (422), el campo errors detalla cada campo que falló con mensajes específicos en español:

{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "voucher_type": ["El tipo de comprobante (voucher_type) es obligatorio."],
        "line_items.0.cabys_code": ["El código CABYS no existe en el catálogo."]
    }
}

En las respuestas paginadas (listados), se incluyen los campos meta y links para facilitar la navegación entre páginas:

{
    "success": true,
    "data": [...],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 5,
        "per_page": 15,
        "total": 73,
        "from": 1,
        "to": 15
    },
    "links": {
        "first": "...?page=1",
        "last": "...?page=5",
        "prev": null,
        "next": "...?page=2"
    }
}

Emisión de comprobantes

La emisión es el corazón de la API. El proceso es asíncrono: el endpoint retorna 202 Accepted en milisegundos y el envío a Hacienda ocurre en segundo plano.

sequenceDiagram
    participant I as Su sistema
    participant A as Almendro
    participant H as Hacienda

    I->>+A: POST /vouchers
    Note right of A: Almendro genera clave, XML v4.4 y firma XAdES-EPES
    A-->>-I: 202 Accepted (clave de 50 dígitos)

    rect rgba(128, 128, 128, 0.08)
        Note over A,H: Procesamiento asíncrono (5 a 60 segundos)
        A->>+H: XML firmado
        H-->>-A: Respuesta fiscal
    end

    alt
        A-->>I: Webhook voucher.accepted
    else
        A-->>I: Webhook voucher.rejected
    end

El endpoint retorna 202 Accepted inmediatamente. Esto confirma que el comprobante fue generado, validado contra los esquemas XSD oficiales de Hacienda y firmado digitalmente. El envío a Hacienda ocurre automáticamente en segundo plano.

Importante: el código 202 Accepted no significa "aceptado por Hacienda". Es la confirmación de que Almendro recibió y procesó su solicitud correctamente. La respuesta fiscal de Hacienda se obtiene consultando el estado del comprobante o configurando Webhooks.

Atajos para emisión rápida: si tiene clientes y productos registrados en su catálogo de Almendro, puede simplificar el payload de emisión. Envíe client_id para resolver automáticamente los datos del receptor, y item_id en cada línea para resolver código CABYS, descripción, unidad de medida, precio e impuesto. Si incluye campos explícitos junto con el client_id o item_id, los valores explícitos siempre tienen prioridad.

Anulación: para anular un comprobante previamente aceptado por Hacienda, use POST /api/v1/public/vouchers/{key}/cancel. Almendro genera automáticamente una Nota de Crédito (tipo 03) que referencia al comprobante original.

Estados del comprobante:

Estado Significado
draft Generado, pendiente de firma (ocurre solo si hay un error temporal con el certificado)
pending Firmado, pendiente de envío a Hacienda
sent Enviado a Hacienda, esperando respuesta
accepted Aceptado por Hacienda — tiene validez fiscal
rejected Rechazado por Hacienda — consulte el campo hacienda_message para conocer el motivo
error Error técnico al comunicarse con Hacienda — se reintenta automáticamente
cancelled Anulado mediante Nota de Crédito

Impuestos y cálculo de totales

Almendro calcula automáticamente todos los totales del ResumenFactura a partir de las líneas de detalle. Usted solo necesita enviar los impuestos correctamente en cada línea.

Estructura del impuesto por línea

Cada línea (line_items[]) puede tener un array taxes[] con uno o más impuestos:

{
    "taxes": [
        {
            "codigo": "01",
            "codigoTarifa": "08",
            "tarifa": "13.00",
            "monto": "1300.00000"
        }
    ]
}
Campo Descripción Valores comunes
codigo Código del impuesto 01=IVA, 02=Selectivo consumo, 07=IVA cálculo especial, 08=IVA bienes usados
codigoTarifa Tarifa del IVA (obligatorio si codigo es 01 o 07) 01=0%, 02=1%, 03=2%, 04=4%, 08=13%, 10=Exenta
tarifa Porcentaje de la tarifa "13.00", "4.00", "0.00"
monto Monto del impuesto: base_imponible × tarifa / 100 "1300.00000"
exoneracion Objeto con datos de exoneración (opcional) Ver sección Exonerados
factor Factor para IVA cálculo especial (solo código 07) "0.5000"

Clasificación automática de cada línea

Almendro clasifica cada línea en dos dimensiones para calcular los 8 subtotales del ResumenFactura:

Dimensión 1 — Tipo de bien (primer dígito del código CABYS):

Primer dígito CABYS Clasificación Ejemplo
0, 1, 2, 3, 4 Mercancía 1234500000000 → mercancía
5, 6, 7, 8, 9 Servicio 7331100000000 → servicio

Dimensión 2 — Condición del IVA (determinado por el array taxes[]):

Condición Cuándo aplica
Gravado Tiene IVA (código 01 o 07) con tarifa distinta de 10 y sin exoneración
Exento Tiene IVA con tarifa 10 (exenta), O no tiene ningún impuesto IVA
Exonerado Tiene IVA con objeto exoneracion presente
No sujeto Casos especiales del Art. 9 LIVA (prácticamente nunca en facturación normal)

Estas dos dimensiones generan los 8 subtotales del XML:

                    Servicio              Mercancía
                    ────────              ─────────
Gravado         TotalServGravados     TotalMercanciasGravadas
Exento          TotalServExentos      TotalMercanciasExentas
Exonerado       TotalServExonerado    TotalMercExonerada
No sujeto       TotalServNoSujeto     TotalMercNoSujeta

Ejemplo 1 — Servicio gravado con IVA 13%

{
    "line_items": [{
        "line_number": 1,
        "cabys_code": "7331100000000",
        "detail": "Servicio de consultoría",
        "quantity": "1.000",
        "unit_of_measure": "Sp",
        "unit_price": "10000.00000",
        "total_amount": "10000.00000",
        "sub_total": "10000.00000",
        "base_imponible": "10000.00000",
        "taxes": [{"codigo": "01", "codigoTarifa": "08", "tarifa": "13.00", "monto": "1300.00000"}],
        "impuesto_neto": "1300.00000",
        "total_line_amount": "11300.00000"
    }]
}

CABYS 7 → servicio. IVA tarifa 08 → gravado. Aporta ₡10,000 a TotalServGravados.

Ejemplo 2 — Mercancía gravada con IVA 13%

{
    "line_items": [{
        "line_number": 1,
        "cabys_code": "1234500000000",
        "detail": "Producto de limpieza",
        "quantity": "2.000",
        "unit_of_measure": "Unid",
        "unit_price": "5000.00000",
        "total_amount": "10000.00000",
        "sub_total": "10000.00000",
        "base_imponible": "10000.00000",
        "taxes": [{"codigo": "01", "codigoTarifa": "08", "tarifa": "13.00", "monto": "1300.00000"}],
        "impuesto_neto": "1300.00000",
        "total_line_amount": "11300.00000"
    }]
}

CABYS 1 → mercancía. IVA tarifa 08 → gravado. Aporta ₡10,000 a TotalMercanciasGravadas.

Ejemplo 3 — Servicio exento de IVA

Opción A — Tarifa exenta (10):

{"taxes": [{"codigo": "01", "codigoTarifa": "10", "tarifa": "0.00", "monto": "0.00000"}], "impuesto_neto": "0.00000"}

Opción B — Sin taxes:

{"taxes": [], "impuesto_neto": "0.00000"}

Ambas clasifican como exentoTotalServExentos o TotalMercanciasExentas.

Ejemplo 4 — Factura mixta (servicio + mercancía gravados)

{
    "line_items": [
        {
            "line_number": 1,
            "cabys_code": "7331100000000",
            "detail": "Consultoría técnica",
            "quantity": "1.000", "unit_of_measure": "Sp",
            "unit_price": "50000.00000", "total_amount": "50000.00000",
            "sub_total": "50000.00000", "base_imponible": "50000.00000",
            "taxes": [{"codigo": "01", "codigoTarifa": "08", "tarifa": "13.00", "monto": "6500.00000"}],
            "impuesto_neto": "6500.00000", "total_line_amount": "56500.00000"
        },
        {
            "line_number": 2,
            "cabys_code": "1234500000000",
            "detail": "Material de oficina",
            "quantity": "10.000", "unit_of_measure": "Unid",
            "unit_price": "1000.00000", "total_amount": "10000.00000",
            "sub_total": "10000.00000", "base_imponible": "10000.00000",
            "taxes": [{"codigo": "01", "codigoTarifa": "08", "tarifa": "13.00", "monto": "1300.00000"}],
            "impuesto_neto": "1300.00000", "total_line_amount": "11300.00000"
        }
    ]
}

ResumenFactura: TotalServGravados=₡50,000 · TotalMercanciasGravadas=₡10,000 · TotalGravado=₡60,000 · TotalImpuesto=₡7,800 · TotalComprobante=₡67,800.

Ejemplo 5 — Payload completo funcional (servicio gravado 13%)

{
    "voucher_type": "01",
    "situation": "1",
    "issued_at": "2026-04-20T10:00:00-06:00",
    "issuer_activity_code": "6201.0",
    "sale_condition": "01",
    "currency_code": "CRC",
    "exchange_rate": "1.00000",
    "payment_methods": [{"tipo": "01"}],
    "receiver": {
        "id_type": "02",
        "id_number": "3101234567",
        "name": "Empresa Receptora S.A.",
        "emails": ["facturacion@receptor.com"]
    },
    "line_items": [{
        "line_number": 1,
        "cabys_code": "7331100000000",
        "detail": "Servicio de desarrollo de software",
        "quantity": "1.000",
        "unit_of_measure": "Sp",
        "unit_price": "100000.00000",
        "total_amount": "100000.00000",
        "sub_total": "100000.00000",
        "base_imponible": "100000.00000",
        "taxes": [{"codigo": "01", "codigoTarifa": "08", "tarifa": "13.00", "monto": "13000.00000"}],
        "impuesto_neto": "13000.00000",
        "total_line_amount": "113000.00000"
    }]
}

Ejemplo 6 — IVA reducido 4% (canasta básica)

{"taxes": [{"codigo": "01", "codigoTarifa": "04", "tarifa": "4.00", "monto": "400.00000"}], "impuesto_neto": "400.00000"}

Fórmulas de cada línea

total_amount     = unit_price × quantity
sub_total        = total_amount - descuentos (si aplica)
base_imponible   = sub_total (normalmente igual, salvo exoneraciones parciales)
monto (impuesto) = base_imponible × tarifa / 100
impuesto_neto    = monto - monto_exonerado (si aplica)
total_line_amount = sub_total + impuesto_neto

Todos los montos: exactamente 5 decimales ("10000.00000").

Errores comunes de Hacienda

Código Error Causa Solución
-111 "Monto total de mercancías gravadas no coincide" CABYS de mercancía (dígito 0-4) con IVA pero total_amount/sub_total inconsistente Verifique sub_total = unit_price × quantity - descuentos. Use 5 decimales.
-481 "Carece del monto Total mercancías No Sujetas" CABYS de mercancía sin taxes válidos o CABYS inexistente en catálogo Verifique cabys_code con GET /catalogs/cabys. Use 13 dígitos exactos.
-485 "Carece del monto Total No Sujeto" Línea sin clasificar por CABYS inválido o taxes mal formados Toda línea gravada: codigo + codigoTarifa + tarifa + monto completos.

Regla de oro

  1. cabys_code: 13 dígitos válidos. Verifique con GET /catalogs/cabys?search={código}.
  2. Gravado (IVA > 0%): taxes con codigo + codigoTarifa + tarifa + monto, más impuesto_neto y base_imponible.
  3. Exento: tarifa 10 con monto 0.00000, O taxes: [].
  4. monto = base_imponible × tarifa / 100 (5 decimales).
  5. total_line_amount = sub_total + impuesto_neto.
  6. 5 decimales siempre: "10000.00000".

Almendro calcula automáticamente los 23 campos del ResumenFactura — no envíe totales.


Consulta y descarga de comprobantes

Una vez emitido un comprobante, dispone de varios endpoints para consultarlo y descargar sus archivos:


Gestión de clientes y productos

Almendro le permite mantener un catálogo de clientes (receptores frecuentes) y productos o servicios (líneas de detalle reutilizables). Esto simplifica la emisión de comprobantes al permitir referenciar un client_id o item_id en lugar de repetir todos los datos en cada factura.

Clientes (/clients): registre la razón social, cédula, correos, dirección y teléfono de sus receptores frecuentes. Al emitir un comprobante con client_id, los datos del receptor se completan automáticamente.

Productos/Servicios (/items): registre código CABYS, descripción, unidad de medida, precio unitario e impuesto de cada producto o servicio. Al emitir con item_id en una línea, esos campos se resuelven automáticamente. Puede asignar un código interno (SKU, PLU) único por contribuyente para localizar rápidamente sus productos.

Ambos catálogos ofrecen los cinco endpoints estándar: listar, crear, consultar, actualizar y eliminar.


Plantillas PDF y personalización visual

Cada contribuyente puede personalizar cómo lucen sus comprobantes en formato PDF. El sistema de plantillas permite configurar colores, fuentes, márgenes, qué secciones mostrar u ocultar, y subir su propio logotipo.

Cinco estilos predefinidos: Predeterminado, Moderno, Clásico, Minimalista y Dividido. Cada uno ofrece un diseño distinto que se adapta a diferentes tipos de negocio.

Configuración visual: a través del campo config_json puede ajustar colores (primario, secundario, texto, acento en formato hexadecimal), familias tipográficas, tamaños de fuente para encabezado, cuerpo y pie de página, y márgenes en milímetros.

Logotipo: suba el logo de su empresa con POST /pdf-templates/{id}/logo (formatos JPG, PNG o SVG, máximo 2 MB). El logo aparecerá en la posición configurada (izquierda, centro o derecha) de todos los comprobantes generados con esa plantilla.

Restricciones normativas: el código QR debe medir al menos 2.5 cm de alto por 2.5 cm de ancho y ubicarse en la parte inferior derecha del PDF, conforme al artículo 5 de la Resolución. Estas restricciones se validan automáticamente y no pueden modificarse.

Puede tener varias plantillas y marcar una como predeterminada. La cantidad máxima de plantillas depende de su plan.


Configuración de email transaccional

Almendro envía automáticamente los comprobantes por correo electrónico al receptor, adjuntando el XML firmado y el PDF con código QR, conforme al artículo 18 del Reglamento que obliga la entrega del comprobante electrónico y su representación gráfica.

Desde PUT /email-settings puede configurar:


Webhooks — notificaciones en tiempo real

En lugar de consultar repetidamente el estado de un comprobante, puede configurar Webhooks para que Almendro le notifique automáticamente cuando ocurran eventos relevantes. Al recibir la notificación, su sistema puede actualizar sus registros, notificar al usuario final o ejecutar cualquier lógica de negocio.

Eventos disponibles:

Evento Se dispara cuando...
voucher.accepted Hacienda acepta un comprobante
voucher.rejected Hacienda rechaza un comprobante
voucher.sent Un comprobante se envía a Hacienda
receiver.confirmed Un receptor confirma o rechaza un comprobante recibido
receiver.deadline Se acerca el vencimiento del plazo de 8 días hábiles para confirmar
certificate.expiring Un certificado digital está próximo a vencer

Seguridad: cada notificación incluye una firma HMAC-SHA256 en el header para que su servidor pueda verificar que la notificación proviene de Almendro y no fue alterada en tránsito. La URL de su endpoint debe usar HTTPS.

Tolerancia a fallos: si su servidor no responde o retorna un error, Almendro reintenta con intervalos crecientes. Tras 10 fallos consecutivos, el endpoint se desactiva automáticamente. Puede reactivarlo en cualquier momento con PUT /webhooks/{id}.

Historial de entregas: consulte el registro de entregas con GET /webhooks/{id}/logs para verificar qué notificaciones se enviaron, cuáles fallaron y cuáles se reintentaron.

La cantidad de endpoints webhook disponibles depende de su plan (desde 1 en el plan Pyme hasta 15 en el plan Integrador).


Reportes y análisis financiero

La API ofrece siete endpoints de reportes diseñados para alimentar tableros de control, preparar la declaración D-104 y analizar el desempeño del negocio. Todos comparten los mismos filtros: rango de fechas, tipo de comprobante, estado, moneda, ambiente, cédula del receptor, condición de venta y medio de pago.

Valores por defecto: si no envía filtros, los reportes cubren el mes actual, solo comprobantes de producción aceptados por Hacienda, en colones costarricenses. Estos valores corresponden al período y criterios más comunes para la declaración tributaria mensual.


Catálogos oficiales

La API expone tres catálogos oficiales de Hacienda necesarios para construir comprobantes electrónicos válidos:


Consulta de contribuyentes

El endpoint GET /taxpayer/{id_number} permite verificar los datos de una cédula ante el registro de Hacienda antes de emitir un comprobante. Retorna el nombre oficial, tipo de identificación, régimen tributario, situación (activo/inactivo) y actividades económicas inscritas.

Casos de uso frecuentes:

Para cédulas físicas de 9 dígitos y planes con la funcionalidad habilitada, el endpoint enriquece la respuesta con datos del Padrón Electoral del TSE (nombre completo, vigencia de la cédula, ubicación). Si la persona no está inscrita como contribuyente ante Hacienda pero sí aparece en el Padrón TSE, el endpoint la identifica como consumidor final, indicando que puede recibir Tiquetes Electrónicos (tipo 04) pero no Facturas Electrónicas.

Los resultados se almacenan en caché durante 24 horas para respuestas rápidas en consultas posteriores.


Confirmación del Receptor (MensajeReceptor)

Conforme al artículo 15 del Reglamento, los receptores de comprobantes tienen 8 días hábiles (excluyendo fines de semana y feriados nacionales de Costa Rica) para responder a un comprobante recibido de un proveedor. Vencido el plazo, se considera aceptación tácita.

El endpoint POST /receiver/confirm le permite enviar la respuesta a Hacienda con tres opciones:

Código Tipo de respuesta Consecuencia
1 Aceptado Genera crédito fiscal para el receptor
2 Aceptado Parcialmente Genera crédito fiscal parcial
3 Rechazado No genera crédito fiscal

Al aceptar (total o parcialmente), debe indicar la condición del impuesto (crédito IVA general, crédito parcial, bienes de capital, gasto sin crédito, o proporcionalidad), el monto del impuesto acreditable y la actividad económica asociada.

La respuesta incluye información detallada del plazo: días hábiles transcurridos, días restantes, fecha límite y alertas de vencimiento próximo.


Modo Integrador — gestión de múltiples contribuyentes

Si usted es integrador (plan Integrador), puede administrar la facturación de múltiples clientes desde su propia cuenta. Esto es ideal para empresas que desarrollan software de punto de venta, e-commerce, hotelería o cualquier sistema que facture en nombre de terceros.

Cómo funciona:

Vincular un cliente nuevo: créelo con POST /my-contributors indicando su cédula, razón social, correos y actividades económicas. Almendro crea la cuenta del cliente con un usuario propietario y una contraseña temporal. El cliente recibe un email de bienvenida con instrucciones para acceder a su portal. Automáticamente recibe el plan Gratis (5 comprobantes por mes) para que pueda probar el servicio.

Vincular un cliente que ya tiene cuenta: si el cliente ya está registrado en Almendro, solicite acceso con POST /access-requests. El cliente recibe una notificación por email y en su portal, y puede aceptar (total o parcialmente, eligiendo qué ambientes autoriza) o rechazar la solicitud. Al aceptar, usted recibe acceso al certificado digital del cliente con una terminal exclusiva que evita colisión de consecutivos.

Emitir en nombre del cliente: incluya managed_contributor_id en el payload de emisión. El comprobante se registra bajo la cédula del cliente ante Hacienda (el cliente aparece como emisor, nunca el integrador), se firma con el certificado digital del cliente y se envía desde su cuenta. El comprobante cuenta contra el límite mensual de su plan de integrador, no del plan del cliente.

Administrar certificados: suba el certificado .p12 del cliente con POST /my-contributors/{id}/certificates. El certificado queda vinculado a la cuenta del cliente, no a la suya. Puede listar los certificados activos y su estado de vigencia.

Revocar acceso: el cliente puede revocar su acceso en cualquier momento desde DELETE /certificate-grants/{id}. Usted también puede renunciar voluntariamente a un acceso desde DELETE /my-access/{grantId}.

Ejemplo completo — emitir una factura en nombre de un cliente

A continuación se muestra el flujo completo desde cero. Todos los pasos usan el token del integrador — no se necesita el token del cliente en ningún momento.

Paso 1 — Buscar al cliente por cédula (opcional, para autocompletar datos)

curl -X GET "https://fe.almendro.cr/api/v1/public/taxpayer/3101234567" \
  -H "Authorization: Bearer {TOKEN_DEL_INTEGRADOR}"

Retorna nombre oficial, tipo de identificación, régimen tributario y actividades económicas del contribuyente según el registro de Hacienda. Use estos datos para prellenar el paso 2.

Paso 2 — Crear al cliente como contribuyente gestionado

curl -X POST "https://fe.almendro.cr/api/v1/public/my-contributors" \
  -H "Authorization: Bearer {TOKEN_DEL_INTEGRADOR}" \
  -H "Content-Type: application/json" \
  -d '{
    "legal_name": "Empresa del Cliente S.A.",
    "id_type": "02",
    "id_number": "3101234567",
    "email": "cliente@ejemplo.com",
    "password": "ContraseñaSegura123!"
  }'

La respuesta incluye el UUID del cliente en el campo data.id. Guarde este UUID: lo necesita para todos los pasos siguientes. El objeto managed_relationship describe el vínculo activo entre usted (integrador) y el cliente — incluye la terminal exclusiva asignada (5 dígitos) para evitar colisión de consecutivos.

{
    "success": true,
    "data": {
        "id": "019d867d-0241-7288-8ece-fd64da75616d",
        "legal_name": "Empresa del Cliente S.A.",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "id_number": "3101234567",
        "is_active": true,
        "production_enabled": false,
        "plan_id": "019d0001-0000-0000-0000-000000000001",
        "can_emit_from_portal": true,
        "managed_relationship": {
            "id": "019d870a-0001-7288-8ece-fd64da75a001",
            "assigned_terminal": "00002",
            "retention_months_override": null,
            "default_pdf_template_id": null,
            "linked_at": "2026-04-18T10:00:00-06:00"
        }
    },
    "message": "Cliente creado correctamente. Se envió email de bienvenida.",
    "errors": null
}

Paso 3 — Subir el certificado digital .p12 del cliente

El certificado debe pertenecer al cliente (emitido por una CA registrada ante el BCCR para la cédula del cliente). Almendro lo almacena cifrado y lo vincula a la cuenta del cliente, no a la del integrador.

curl -X POST "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-0241-7288-8ece-fd64da75616d/certificates" \
  -H "Authorization: Bearer {TOKEN_DEL_INTEGRADOR}" \
  -F "p12_file=@/ruta/al/certificado-cliente.p12" \
  -F "p12_password=ContraseñaDelP12" \
  -F "hacienda_pin=PinDeHaciendaDelCliente" \
  -F "environment=sandbox"

Paso 4 — Emitir un comprobante en nombre del cliente

Use POST /vouchers con su token de integrador y agregue el campo managed_contributor_id con el UUID del cliente obtenido en el paso 2. El resto del payload es idéntico a una emisión normal:

curl -X POST "https://fe.almendro.cr/api/v1/public/vouchers" \
  -H "Authorization: Bearer {TOKEN_DEL_INTEGRADOR}" \
  -H "Content-Type: application/json" \
  -d '{
    "managed_contributor_id": "019d867d-0241-7288-8ece-fd64da75616d",
    "voucher_type": "01",
    "situation": "1",
    "issued_at": "2026-04-18T10:00:00-06:00",
    "sale_condition": "01",
    "payment_methods": [{"tipo": "01"}],
    "receiver": {
        "name": "Juan Pérez Solís",
        "id_type": "01",
        "id_number": "101230456"
    },
    "line_items": [{
        "line_number": 1,
"cabys_code": "4321500000100",
        "detail": "Servicio de consultoría",
        "unit_of_measure": "Sp",
        "quantity": "1.000",
        "unit_price": "10000.00000",
        "sub_total": "10000.00000",
        "total_amount": "10000.00000",
        "taxes": [{
            "codigo": "01",
            "codigoTarifa": "08",
            "tarifa": "13.00",
            "monto": "1300.00000"
        }],
        "impuesto_neto": "1300.00000",
        "total_line_amount": "11300.00000"
    }]
  }'

¿Qué sucede internamente?

Cuando envía managed_contributor_id, Almendro ejecuta automáticamente lo siguiente:

  1. Verifica que el UUID pertenece a un cliente vinculado a su cuenta de integrador
  2. Genera la clave de 50 dígitos usando la cédula del cliente (no la del integrador)
  3. Construye el XML v4.4 con el cliente como emisor (EmisorType) ante Hacienda
  4. Firma digitalmente con el certificado .p12 del cliente
  5. Asigna el consecutivo en los contadores del cliente, usando la terminal exclusiva del integrador
  6. Descuenta el comprobante del límite mensual del plan del integrador

Hacienda siempre ve al cliente como el emisor del comprobante. El integrador nunca aparece en el XML ni en la clave de 50 dígitos.

Sin managed_contributor_id

Si omite el campo managed_contributor_id, el comprobante se emite bajo su propia cédula de integrador, como si fuera un contribuyente normal (emisión directa).

Consultar comprobantes de un cliente gestionado

Use el filtro managed_contributor_id en GET /vouchers para ver solo los comprobantes emitidos en nombre de un cliente específico:

curl -X GET "https://fe.almendro.cr/api/v1/public/vouchers?managed_contributor_id=019d867d-0241-7288-8ece-fd64da75616d" \
  -H "Authorization: Bearer {TOKEN_DEL_INTEGRADOR}"

Reportes agregados por cliente

Los endpoints de reportes (/reports/*) agregan automáticamente los datos de todos sus clientes gestionados. Para filtrar los datos de un solo cliente, use el parámetro ?managed_contributor_id={UUID} en cualquier endpoint de reportes.


Control de acceso — solicitudes y grants

El sistema de control de acceso regula la relación entre integradores y clientes, garantizando que el cliente siempre tiene soberanía sobre sus certificados digitales conforme al artículo 16 del Reglamento.

Solicitudes de acceso (/access-requests): el integrador solicita, el cliente decide. Una solicitud puede estar en estado pendiente, aceptada (total o parcialmente), rechazada o cancelada. El integrador puede ver sus solicitudes enviadas; el cliente, las recibidas.

Grants de certificados (/certificate-grants): cuando el cliente acepta una solicitud, se crean grants que autorizan al integrador a firmar con el certificado del cliente en los ambientes aprobados. Cada grant tiene una terminal exclusiva asignada para evitar duplicación de consecutivos.

El cliente puede consultar qué integradores tienen acceso a sus certificados con GET /certificate-grants, y revocar cualquier acceso en cualquier momento.


Notificaciones

El sistema de notificaciones mantiene informado al usuario sobre eventos importantes que requieren su atención:

Use GET /notifications para obtener la lista paginada de notificaciones con un conteo de no leídas para actualizar badges en su interfaz. Las notificaciones no leídas aparecen primero. Puede marcar una notificación como leída con POST /notifications/{id}/read o marcar todas de una vez con POST /notifications/read-all.


Planes y límites

El volumen de comprobantes, la cantidad de peticiones por minuto y las funcionalidades disponibles dependen del plan contratado:

Plan Precio Comprobantes/mes Excedente Peticiones/min Sandbox Webhooks Clientes gestionados
Gratis $0 5 Bloquea 5 No No 1
Emprendedor $36/año 100 $0.05/comp 10 No No 1
Pyme $96/año 500 $0.03/comp 20 1 endpoint 1
Profesional $19/mes o $182/año 2,000 $0.02/comp 40 3 endpoints 5
Empresa $39/mes o $351/año 8,000 $0.012/comp 80 5 endpoints 1
Integrador $79/mes o $711/año 50,000 $0.008/comp 150 15 endpoints 100

Límite de peticiones por minuto: cuando se excede, la API retorna 429 Too Many Requests con el header Retry-After indicando cuántos segundos esperar. Las operaciones de lectura (GET) están exentas del límite; solo las operaciones transaccionales (POST, PUT, DELETE) cuentan contra la cuota.

Límite mensual de comprobantes: solo cuentan comprobantes de producción aceptados por Hacienda. Los comprobantes de sandbox no consumen cuota. El plan Gratis bloquea la emisión al alcanzar el límite. Desde el plan Emprendedor, se permite emitir por encima del límite con un cargo automático por comprobante adicional.

Otros límites por plan: cada plan define también la cantidad máxima de clientes en catálogo, productos en catálogo, plantillas PDF, tokens API, emails diarios, sucursales y días de anticipación para alertas de vencimiento de certificado. Consulte los detalles completos en fe.almendro.cr.


Ambiente Sandbox

Almendro ofrece un ambiente sandbox que replica los endpoints de emisión contra el sandbox oficial del Ministerio de Hacienda. Los comprobantes emitidos en sandbox no tienen valor fiscal y no cuentan contra su límite mensual.

Para usar el sandbox, reemplace /api/v1/public/ por /api/v1/public/sandbox/ en la URL. El token, los headers y el payload son exactamente iguales:

Producción:  https://fe.almendro.cr/api/v1/public/vouchers
Sandbox:     https://fe.almendro.cr/api/v1/public/sandbox/vouchers
Aspecto Producción Sandbox
Destino API oficial de Hacienda API sandbox de Hacienda
Valor fiscal No
Cuenta contra límite mensual No
Envío de email al receptor No (solo registro interno)
Marca de agua en PDF Sin marca "SANDBOX — SIN VALOR FISCAL"
Token y payload El mismo El mismo
Webhooks

Endpoints sandbox disponibles: emisión de los 7 tipos de comprobante, consulta, descarga XML/PDF, anulación, confirmación del receptor y consulta de contribuyentes. Los demás endpoints (clientes, productos, webhooks, reportes, plantillas, certificados, configuración) no necesitan URL sandbox porque operan sobre los mismos datos en ambos ambientes.

El acceso al sandbox requiere plan Pyme o superior.


Retención y disponibilidad de XML

Almendro retiene el XML firmado y la respuesta de Hacienda de cada comprobante durante 3 meses en todos los planes. Transcurrido ese período, el contenido XML se elimina automáticamente, pero los metadatos del comprobante (totales, estado, receptor, líneas de detalle) y la generación de PDF permanecen disponibles de forma indefinida.

Cuando el XML ya no está disponible, los endpoints GET /vouchers/{key}/xml y GET /vouchers/{key}/xml-response retornan 410 Gone con la fecha en que se eliminó el archivo.

Retención extendida: puede ampliar la retención de XML hasta 5 años por $12/año (disponible desde el plan Emprendedor). Al vencer la retención extendida, se otorga un período de gracia de 7 días para que descargue sus documentos antes de la eliminación definitiva.


Clave de 50 dígitos

Cada comprobante se identifica con una Clave numérica de exactamente 50 dígitos, definida por los Anexos y Estructuras v4.4 de la DGT. Esta clave se usa en todos los endpoints que reciben el parámetro {key}:

[3 país][6 fecha DDMMAA][12 cédula emisor][20 consecutivo][1 situación][8 seguridad]

Ejemplo: 50613032600206270652001000010100000000011 12345678


Tipos de comprobante soportados

Código Tipo Descripción Receptor
01 Factura Electrónica Venta de bienes o servicios con receptor identificado Obligatorio
02 Nota de Débito Corrección que incrementa el monto de un comprobante previo Obligatorio
03 Nota de Crédito Corrección que disminuye el monto, o anulación total Obligatorio
04 Tiquete Electrónico Venta al consumidor final sin receptor obligatorio Opcional
08 Factura de Compra Compra a proveedores no inscritos en el régimen Obligatorio
09 Factura de Exportación Venta de bienes o servicios fuera del territorio nacional Opcional
10 Recibo Electrónico de Pago Constancia de recepción de pago en ventas a crédito fiscal o al Estado Obligatorio

Las Notas de Débito (02), Notas de Crédito (03), Facturas de Compra (08) y Recibos de Pago (10) requieren al menos una referencia al comprobante original en el campo references.


Códigos de estado HTTP

Código Significado
200 Consulta, actualización o eliminación exitosa
201 Recurso creado correctamente (clientes, productos, webhooks, plantillas, certificados)
202 Comprobante recibido, validado, firmado y en proceso de envío a Hacienda
401 Token de autenticación inválido, expirado o ausente
403 Su plan no permite esta operación o no tiene permisos suficientes
404 Recurso no encontrado o no pertenece a su contribuyente
410 El XML de este comprobante fue eliminado por vencimiento de retención. Los metadatos y el PDF siguen disponibles
422 Error de validación. Revise el campo errors en la respuesta para detalles por campo
429 Límite de peticiones por minuto excedido. Espere los segundos indicados en el header Retry-After
500 Error interno del servidor
502 La API del Ministerio de Hacienda no está disponible temporalmente
504 Tiempo de espera agotado al contactar la API del Ministerio de Hacienda

Soporte y contacto

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {YOUR_AUTH_KEY}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

Obtenga su token desde el portal en Configuración → Token API. Cada token está vinculado a un contribuyente específico y hereda los límites de su plan. El aislamiento de datos se aplica automáticamente — un token solo puede acceder a los comprobantes y datos de su propio contribuyente.

Comprobantes Electrónicos

Listar comprobantes con filtros y paginación

requires authentication

Devuelve todos los comprobantes emitidos por el contribuyente, con paginación y filtros por estado, tipo, rango de fechas, receptor, ambiente y moneda. Los filtros status y voucher_type aceptan múltiples valores separados por coma.

El listado retorna campos resumidos (sin XML ni totales detallados) para máxima eficiencia. Para el detalle completo de un comprobante, use GET /vouchers/{key}.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/vouchers?status=accepted%2Crejected&voucher_type=01%2C04&date_from=2026-04-01&date_to=2026-04-30&receiver_id_number=3101234567&environment=production&currency_code=CRC&contributor_id=019d867d-c001-7288-8ece-fd64da756c01&sort_by=issued_at&sort_dir=desc&per_page=15&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers"
);

const params = {
    "status": "accepted,rejected",
    "voucher_type": "01,04",
    "date_from": "2026-04-01",
    "date_to": "2026-04-30",
    "receiver_id_number": "3101234567",
    "environment": "production",
    "currency_code": "CRC",
    "contributor_id": "019d867d-c001-7288-8ece-fd64da756c01",
    "sort_by": "issued_at",
    "sort_dir": "desc",
    "per_page": "15",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'status' => 'accepted,rejected',
            'voucher_type' => '01,04',
            'date_from' => '2026-04-01',
            'date_to' => '2026-04-30',
            'receiver_id_number' => '3101234567',
            'environment' => 'production',
            'currency_code' => 'CRC',
            'contributor_id' => '019d867d-c001-7288-8ece-fd64da756c01',
            'sort_by' => 'issued_at',
            'sort_dir' => 'desc',
            'per_page' => '15',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado paginado):


{
    "success": true,
    "data": [
        {
            "voucher_key": "50620032600206270652001000010100000000011XXXXXXXX",
            "voucher_type": "01",
            "voucher_type_label": "Factura Electrónica",
            "consecutive_number": "00100001010000000001",
            "issued_at": "2026-03-20T10:30:00-06:00",
            "situation": "1",
            "sale_condition": "01",
            "sale_condition_label": "Contado",
            "receiver": {
                "id_type": "02",
                "id_number": "3102345678",
                "name": "Empresa S.A."
            },
            "currency_code": "CRC",
            "total_comprobante": "56500.00000",
            "status": "accepted",
            "hacienda": {
                "status": "aceptado",
                "sent_at": "2026-03-20T10:31:00-06:00",
                "processed_at": "2026-03-20T10:32:00-06:00"
            },
            "environment": "sandbox",
            "is_xml_available": true,
            "line_items_count": 3,
            "created_at": "2026-03-20T10:30:00-06:00",
            "updated_at": "2026-03-20T10:32:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 5,
        "per_page": 15,
        "total": 72,
        "from": 1,
        "to": 15
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": "..."
    }
}
 

Request      

GET api/v1/public/vouchers

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

Estado(s) separados por coma. draft, pending, sent, accepted, rejected, error, cancelled. Example: accepted,rejected

voucher_type   string  optional    

Tipo(s) separados por coma. 01=FE, 02=ND, 03=NC, 04=TE, 08=FEC, 09=FEE, 10=REP. Example: 01,04

date_from   string  optional    

Fecha de emisión desde (YYYY-MM-DD). Example: 2026-04-01

date_to   string  optional    

Fecha de emisión hasta (YYYY-MM-DD). Example: 2026-04-30

receiver_id_number   string  optional    

Cédula del receptor (9-12 dígitos). Example: 3101234567

environment   string  optional    

Ambiente: sandbox o production. Example: production

currency_code   string  optional    

Código ISO 4217. Example: CRC

contributor_id   string  optional    

UUID del cliente gestionado cuyos comprobantes se consultan. Solo para plan Integrador con relación managed activa. Sin este filtro, retorna comprobantes propios. validation.uuid. Example: 019d867d-c001-7288-8ece-fd64da756c01

sort_by   string  optional    

Campo de ordenamiento. Default: issued_at. Example: issued_at

sort_dir   string  optional    

Dirección: asc o desc. Default: desc. Example: desc

per_page   integer  optional    

Resultados por página (1-100). Default: 15. Example: 15

page   integer  optional    

Número de página. Example: 1

Emitir un comprobante electrónico

requires authentication

Genera, valida contra el XSD oficial v4.4, firma digitalmente con XAdES-EPES y envía automáticamente a Hacienda cualquiera de los 7 tipos de comprobante soportados.

La respuesta es HTTP 202 Accepted — el envío a Hacienda es asíncrono. Para obtener el resultado final (aceptado/rechazado), suscríbase a los eventos voucher.accepted y voucher.rejected vía webhooks (ver grupo Webhooks), o consulte periódicamente GET /vouchers/{key}.

HTTP 202 no significa "aceptado por Hacienda". Solo confirma que el comprobante fue generado, firmado y encolado exitosamente. El estado inicial en la respuesta es pending.

Para ejemplos completos de payload por cada tipo de comprobante, revise la sección "Ejemplos completos de payload por tipo" de la guía de integración al inicio de este grupo.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/vouchers" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"voucher_type\": \"01\",
    \"situation\": \"1\",
    \"issued_at\": \"2026-04-20T10:30:00-06:00\",
    \"issuer_activity_code\": \"6201.0\",
    \"receiver_activity_code\": \"4711.0\",
    \"sale_condition\": \"01\",
    \"sale_condition_other\": \"Acuerdo especial de intercambio\",
    \"credit_term\": \"30\",
    \"currency_code\": \"CRC\",
    \"exchange_rate\": \"1.00000\",
    \"client_id\": \"019d1234-0000-0000-0000-000000000001\",
    \"managed_contributor_id\": \"019d867d-0241-7288-8ece-fd64da75616d\",
    \"receiver\": {
        \"name\": \"Empresa Ejemplo S.A.\",
        \"id_type\": \"02\",
        \"id_number\": \"3101000001\",
        \"commercial_name\": \"b\",
        \"emails\": [
            \"architecto\"
        ],
        \"province\": 2,
        \"canton\": \"56\",
        \"district\": \"56\",
        \"address\": \"i\",
        \"phone_country_code\": 8,
        \"phone\": \"564255931\"
    },
    \"line_items\": [
        {
            \"item_id\": \"019d1234-0000-0000-0000-000000000002\",
            \"line_number\": 1,
            \"cabys_code\": \"5311100000000\",
            \"arancelary_partition\": \"564255931423\",
            \"quantity\": \"1.000\",
            \"unit_of_measure\": \"Unid\",
            \"detail\": \"Servicio de consultoría\",
            \"unit_price\": \"10000.00000\",
            \"total_amount\": \"10000.00000\",
            \"sub_total\": \"10000.00000\",
            \"base_imponible\": \"10000.00000\",
            \"discounts\": [
                {
                    \"monto\": 39,
                    \"codigo\": 6
                }
            ],
            \"taxes\": [
                {
                    \"codigo\": \"01\",
                    \"codigoTarifa\": \"08\",
                    \"tarifa\": \"13.00\",
                    \"monto\": \"1300.00000\",
                    \"monto_exportacion\": 50,
                    \"exoneracion\": {
                        \"tipoDocumento\": \"kc\",
                        \"numeroDocumento\": \"m\",
                        \"nombreInstitucion\": \"y\",
                        \"fechaEmision\": \"2026-04-29T10:12:07\",
                        \"tarifaExonerada\": 72,
                        \"montoExoneracion\": 61
                    }
                }
            ],
            \"impuesto_neto\": \"1300.00000\",
            \"total_line_amount\": \"11300.00000\",
            \"medicine_registration\": \"p\",
            \"pharma_form_code\": \"564\",
            \"commercial_codes\": [
                {
                    \"tipo\": 2,
                    \"codigo\": \"yvdljnikhwaykcmy\"
                }
            ],
            \"vin_numbers\": [
                \"u\"
            ]
        }
    ],
    \"references\": [
        {
            \"doc_type\": \"01\",
            \"number\": \"50613032600206270652001000010100000000001XXXXXXXX\",
            \"issued_at\": \"2026-03-01T10:00:00-06:00\",
            \"code\": \"01\",
            \"reason\": \"Anulación por error en monto\"
        }
    ],
    \"payment_methods\": [
        {
            \"tipo\": \"01\"
        }
    ],
    \"other_charges\": [
        {
            \"tipo\": \"06\",
            \"description\": \"Quidem nostrum qui commodi incidunt iure odit.\",
            \"percentage\": 10,
            \"amount\": 5000
        }
    ]
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "voucher_type": "01",
    "situation": "1",
    "issued_at": "2026-04-20T10:30:00-06:00",
    "issuer_activity_code": "6201.0",
    "receiver_activity_code": "4711.0",
    "sale_condition": "01",
    "sale_condition_other": "Acuerdo especial de intercambio",
    "credit_term": "30",
    "currency_code": "CRC",
    "exchange_rate": "1.00000",
    "client_id": "019d1234-0000-0000-0000-000000000001",
    "managed_contributor_id": "019d867d-0241-7288-8ece-fd64da75616d",
    "receiver": {
        "name": "Empresa Ejemplo S.A.",
        "id_type": "02",
        "id_number": "3101000001",
        "commercial_name": "b",
        "emails": [
            "architecto"
        ],
        "province": 2,
        "canton": "56",
        "district": "56",
        "address": "i",
        "phone_country_code": 8,
        "phone": "564255931"
    },
    "line_items": [
        {
            "item_id": "019d1234-0000-0000-0000-000000000002",
            "line_number": 1,
            "cabys_code": "5311100000000",
            "arancelary_partition": "564255931423",
            "quantity": "1.000",
            "unit_of_measure": "Unid",
            "detail": "Servicio de consultoría",
            "unit_price": "10000.00000",
            "total_amount": "10000.00000",
            "sub_total": "10000.00000",
            "base_imponible": "10000.00000",
            "discounts": [
                {
                    "monto": 39,
                    "codigo": 6
                }
            ],
            "taxes": [
                {
                    "codigo": "01",
                    "codigoTarifa": "08",
                    "tarifa": "13.00",
                    "monto": "1300.00000",
                    "monto_exportacion": 50,
                    "exoneracion": {
                        "tipoDocumento": "kc",
                        "numeroDocumento": "m",
                        "nombreInstitucion": "y",
                        "fechaEmision": "2026-04-29T10:12:07",
                        "tarifaExonerada": 72,
                        "montoExoneracion": 61
                    }
                }
            ],
            "impuesto_neto": "1300.00000",
            "total_line_amount": "11300.00000",
            "medicine_registration": "p",
            "pharma_form_code": "564",
            "commercial_codes": [
                {
                    "tipo": 2,
                    "codigo": "yvdljnikhwaykcmy"
                }
            ],
            "vin_numbers": [
                "u"
            ]
        }
    ],
    "references": [
        {
            "doc_type": "01",
            "number": "50613032600206270652001000010100000000001XXXXXXXX",
            "issued_at": "2026-03-01T10:00:00-06:00",
            "code": "01",
            "reason": "Anulación por error en monto"
        }
    ],
    "payment_methods": [
        {
            "tipo": "01"
        }
    ],
    "other_charges": [
        {
            "tipo": "06",
            "description": "Quidem nostrum qui commodi incidunt iure odit.",
            "percentage": 10,
            "amount": 5000
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'voucher_type' => '01',
            'situation' => '1',
            'issued_at' => '2026-04-20T10:30:00-06:00',
            'issuer_activity_code' => '6201.0',
            'receiver_activity_code' => '4711.0',
            'sale_condition' => '01',
            'sale_condition_other' => 'Acuerdo especial de intercambio',
            'credit_term' => '30',
            'currency_code' => 'CRC',
            'exchange_rate' => '1.00000',
            'client_id' => '019d1234-0000-0000-0000-000000000001',
            'managed_contributor_id' => '019d867d-0241-7288-8ece-fd64da75616d',
            'receiver' => [
                'name' => 'Empresa Ejemplo S.A.',
                'id_type' => '02',
                'id_number' => '3101000001',
                'commercial_name' => 'b',
                'emails' => [
                    'architecto',
                ],
                'province' => 2,
                'canton' => '56',
                'district' => '56',
                'address' => 'i',
                'phone_country_code' => 8,
                'phone' => '564255931',
            ],
            'line_items' => [
                [
                    'item_id' => '019d1234-0000-0000-0000-000000000002',
                    'line_number' => 1,
                    'cabys_code' => '5311100000000',
                    'arancelary_partition' => '564255931423',
                    'quantity' => '1.000',
                    'unit_of_measure' => 'Unid',
                    'detail' => 'Servicio de consultoría',
                    'unit_price' => '10000.00000',
                    'total_amount' => '10000.00000',
                    'sub_total' => '10000.00000',
                    'base_imponible' => '10000.00000',
                    'discounts' => [
                        [
                            'monto' => 39,
                            'codigo' => 6,
                        ],
                    ],
                    'taxes' => [
                        [
                            'codigo' => '01',
                            'codigoTarifa' => '08',
                            'tarifa' => '13.00',
                            'monto' => '1300.00000',
                            'monto_exportacion' => 50,
                            'exoneracion' => [
                                'tipoDocumento' => 'kc',
                                'numeroDocumento' => 'm',
                                'nombreInstitucion' => 'y',
                                'fechaEmision' => '2026-04-29T10:12:07',
                                'tarifaExonerada' => 72,
                                'montoExoneracion' => 61,
                            ],
                        ],
                    ],
                    'impuesto_neto' => '1300.00000',
                    'total_line_amount' => '11300.00000',
                    'medicine_registration' => 'p',
                    'pharma_form_code' => '564',
                    'commercial_codes' => [
                        [
                            'tipo' => 2,
                            'codigo' => 'yvdljnikhwaykcmy',
                        ],
                    ],
                    'vin_numbers' => [
                        'u',
                    ],
                ],
            ],
            'references' => [
                [
                    'doc_type' => '01',
                    'number' => '50613032600206270652001000010100000000001XXXXXXXX',
                    'issued_at' => '2026-03-01T10:00:00-06:00',
                    'code' => '01',
                    'reason' => 'Anulación por error en monto',
                ],
            ],
            'payment_methods' => [
                [
                    'tipo' => '01',
                ],
            ],
            'other_charges' => [
                [
                    'tipo' => '06',
                    'description' => 'Quidem nostrum qui commodi incidunt iure odit.',
                    'percentage' => 10,
                    'amount' => 5000.0,
                ],
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (202, Comprobante emitido correctamente):


{
    "success": true,
    "data": {
        "voucher_key": "50620042600206270652001000010100000000011XXXXXXXX",
        "voucher_type": "01",
        "voucher_type_label": "Factura Electrónica",
        "consecutive_number": "00100001010000000001",
        "issued_at": "2026-04-20T10:30:00-06:00",
        "situation": "1",
        "situation_label": "Normal",
        "sale_condition": "01",
        "sale_condition_label": "Contado",
        "sale_condition_other": null,
        "credit_term": null,
        "receiver": {
            "id_type": "02",
            "id_number": "3101000001",
            "name": "EMPRESA EJEMPLO S.A.",
            "commercial_name": null,
            "emails": [
                "facturacion@ejemplo.cr"
            ]
        },
        "currency_code": "CRC",
        "exchange_rate": "1.00000",
        "totals": {
            "serv_gravados": "10000.00000",
            "serv_exentos": "0.00000",
            "serv_exonerado": "0.00000",
            "serv_no_sujeto": "0.00000",
            "merc_gravadas": "0.00000",
            "merc_exentas": "0.00000",
            "merc_exonerada": "0.00000",
            "merc_no_sujeta": "0.00000",
            "total_gravado": "10000.00000",
            "total_exento": "0.00000",
            "total_exonerado": "0.00000",
            "total_no_sujeto": "0.00000",
            "total_venta": "10000.00000",
            "total_descuentos": "0.00000",
            "total_venta_neta": "10000.00000",
            "total_impuesto": "1300.00000",
            "total_imp_asum_emisor_fab": "0.00000",
            "total_iva_devuelto": "0.00000",
            "total_otros_cargos": "0.00000",
            "total_comprobante": "11300.00000"
        },
        "payment_methods": [
            {
                "tipo": "01"
            }
        ],
        "status": "pending",
        "hacienda": {
            "status": null,
            "message": null,
            "sent_at": null,
            "processed_at": null
        },
        "environment": "production",
        "is_xml_available": true,
        "line_items_count": 1,
        "references_count": 0,
        "created_at": "2026-04-20T10:30:00-06:00",
        "updated_at": "2026-04-20T10:30:00-06:00"
    },
    "message": "Comprobante [50620042600206270652001000010100000000011XXXXXXXX] generado y encolado para envío a Hacienda.",
    "errors": null
}
 

Example response (422, Error de validación del payload):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "voucher_type": [
            "El campo voucher_type es obligatorio."
        ]
    }
}
 

Example response (422, Error de validación XSD (no consume consecutivo)):


{
    "success": false,
    "data": null,
    "message": "Validación XSD fallida para Factura Electrónica (01) — 1 error. Primer error: [45:12] Element 'CodigoCABYS': [facet 'length'] The value has a length of '12'; this differs from the allowed length of '13'.",
    "errors": {
        "voucher_type": [
            "01"
        ],
        "xsd": [
            {
                "nivel": "error",
                "codigo": 1824,
                "linea": 45,
                "columna": 12,
                "mensaje": "Element 'CodigoCABYS': [facet 'length'] The value has a length of '12'; this differs from the allowed length of '13'."
            }
        ]
    }
}
 

Example response (422, Error de firma digital (consume consecutivo)):


{
    "success": false,
    "data": null,
    "message": "No se pudo firmar el comprobante. El certificado digital está vencido. Renueve su certificado con el BCCR y vuelva a subirlo.",
    "errors": {
        "signature": [
            "..."
        ],
        "code": [
            "2002"
        ]
    }
}
 

Example response (422, Cupo mensual agotado):


{
    "success": false,
    "data": null,
    "message": "Ha alcanzado el límite mensual de comprobantes de su plan.",
    "errors": {
        "plan": [
            "Actualice a un plan superior o active overage para continuar emitiendo."
        ]
    }
}
 

Request      

POST api/v1/public/vouchers

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

voucher_type   string     

Tipo de comprobante. 01=FE, 02=ND, 03=NC, 04=TE, 08=FEC, 09=FEE, 10=REP. Example: 01

situation   string     

Situación del comprobante. 1=Normal, 2=Contingencia, 3=Sin internet. Example: 1

issued_at   string     

Fecha y hora de emisión ISO 8601 con offset de Costa Rica (-06:00). No puede ser futura. Example: 2026-04-20T10:30:00-06:00

issuer_activity_code   string  optional    

Código CIIU del emisor en formato Hacienda (XXXX.X). Example: 6201.0

receiver_activity_code   string  optional    

Código CIIU del receptor (XXXX.X). Solo para FEC (tipo 08). Example: 4711.0

sale_condition   string     

Condición de venta. 01=Contado, 02=Crédito, etc. Example: 01

sale_condition_other   string  optional    

Descripción si sale_condition=99. Obligatorio para 99 (5-100 chars). Example: Acuerdo especial de intercambio

credit_term   string  optional    

Plazo del crédito en días. Obligatorio si sale_condition es 02, 08 o 10. Example: 30

currency_code   string  optional    

Código ISO 4217. Default: CRC. Example: CRC

exchange_rate   string  optional    

Tipo de cambio respecto al CRC (5 decimales). Requerido si currency_code != CRC. Example: 1.00000

observations   string  optional    

Observaciones opcionales (máx 250 chars). validation.max.

client_id   string  optional    

UUID de un cliente del catálogo (GET /clients). Sobrescribe los datos del receptor. Example: 019d1234-0000-0000-0000-000000000001

managed_contributor_id   string  optional    

UUID del cliente gestionado. Solo para integradores. Si se envía, el comprobante se emite bajo la cédula de ese cliente. Example: 019d867d-0241-7288-8ece-fd64da75616d

receiver   object  optional    

Datos del receptor. Requerido para FE/ND/NC/FEC. Opcional si se envía client_id.

name   string     

Nombre o razón social (3–100 chars). Example: Empresa Ejemplo S.A.

id_type   string     

Tipo de identificación. 01=Física, 02=Jurídica, 03=DIMEX, 04=NITE, 05=Extranjero. Example: 02

id_number   string     

Número de identificación. Example: 3101000001

commercial_name   string  optional    

validation.max. Example: b

emails   string[]  optional    

Correos del receptor para envío automático del comprobante.

province   integer  optional    

validation.between. Example: 2

canton   string  optional    

Must match the regex /^\d{2}$/. Example: 56

district   string  optional    

Must match the regex /^\d{2}$/. Example: 56

address   string  optional    

validation.min validation.max. Example: i

phone_country_code   integer  optional    

validation.min validation.max. Example: 8

phone   string  optional    

Must match the regex /^\d{4,20}$/. Example: 564255931

line_items   object[]     

Líneas de detalle (máximo 1000).

item_id   string  optional    

UUID de un producto del catálogo (GET /items). Resuelve CABYS, descripción, unidad, precio e impuesto automáticamente. Los campos explícitos tienen prioridad. Example: 019d1234-0000-0000-0000-000000000002

line_number   integer     

Número de línea (1-based). Example: 1

cabys_code   string     

Código CABYS de 13 dígitos. Example: 5311100000000

arancelary_partition   string  optional    

Must match the regex /^\d{12}$/. Example: 564255931423

quantity   string     

Cantidad con 3 decimales. Example: 1.000

unit_of_measure   string     

Unidad de medida (Unid, kg, Sp, etc.). Example: Unid

detail   string     

Descripción (3–200 chars). Example: Servicio de consultoría

unit_price   string     

Precio unitario (5 decimales). Example: 10000.00000

total_amount   string     

Monto total antes de descuentos. Example: 10000.00000

sub_total   string     

Subtotal de la línea. Example: 10000.00000

base_imponible   string  optional    

Base imponible para el impuesto. Example: 10000.00000

discounts   object[]  optional    

validation.max.

monto   number     

validation.min. Example: 39

codigo   string     

Example: 6

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 99
taxes   object[]  optional    

Lista de impuestos de la línea.

codigo   string     

Código impuesto. 01=IVA. Example: 01

codigoTarifa   string     

Tarifa IVA. 08=13%, 10=Exenta. Example: 08

tarifa   string     

Porcentaje de tarifa. Example: 13.00

monto   string     

Monto del impuesto. Example: 1300.00000

monto_exportacion   number  optional    

validation.min. Example: 50

exoneracion   object  optional    
tipoDocumento   string  optional    

This field is required when line_items..taxes..exoneracion is present. validation.max. Example: kc

numeroDocumento   string  optional    

This field is required when line_items..taxes..exoneracion is present. validation.max. Example: m

nombreInstitucion   string  optional    

This field is required when line_items..taxes..exoneracion is present. validation.max. Example: y

fechaEmision   string  optional    

This field is required when line_items..taxes..exoneracion is present. validation.date. Example: 2026-04-29T10:12:07

tarifaExonerada   number  optional    

This field is required when line_items..taxes..exoneracion is present. validation.min. Example: 72

montoExoneracion   number  optional    

This field is required when line_items..taxes..exoneracion is present. validation.min. Example: 61

impuesto_neto   string  optional    

Monto neto del impuesto (obligatorio si hay taxes). Example: 1300.00000

total_line_amount   string     

Total de la línea incluyendo impuesto. Example: 11300.00000

medicine_registration   string  optional    

validation.max. Example: p

pharma_form_code   string  optional    

Must match the regex /^\d{3}$/. Example: 564

commercial_codes   object[]  optional    

validation.max.

tipo   string     

Example: 2

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 99
codigo   string     

validation.max. Example: yvdljnikhwaykcmy

vin_numbers   string[]  optional    

validation.max.

references   object[]  optional    

Referencias a otros comprobantes. Obligatorio para ND/NC y REP.

doc_type   string     

Tipo de documento referenciado. 01=FE, 03=NC, etc. Example: 01

number   string     

Clave de 50 dígitos del comprobante referenciado. Example: 50613032600206270652001000010100000000001XXXXXXXX

issued_at   string     

Fecha de emisión del comprobante referenciado. Example: 2026-03-01T10:00:00-06:00

code   string     

Código de referencia. 01=Anula, 04=Referencia, 06=Devolución, 10=ND financiera. Example: 01

reason   string     

Razón de la referencia (máx 180 chars). Example: Anulación por error en monto

payment_methods   object[]     

Lista de medios de pago.

tipo   string     

Código medio pago. 01=Efectivo, 02=Tarjeta, 03=Cheque, 04=Transferencia. Example: 01

monto   number  optional    

Monto pagado con este medio. Obligatorio cuando hay 2+ medios de pago. validation.min.

other_charges   object[]     
tipo   string     

Tipo de cargo. 01=Contribución parafiscal, 04=Cobro terceros, 06=Garantía. Example: 06

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 99
description   string  optional    

validation.max. Example: Quidem nostrum qui commodi incidunt iure odit.

percentage   number  optional    

validation.min validation.max. Example: 10

amount   number     

Monto del cargo (Decimal 18,5). validation.min. Example: 5000

Listar comprobantes de contingencia pendientes

requires authentication

Devuelve los comprobantes emitidos en situación de contingencia (caída del sistema) o sin internet, que aún no fueron aceptados ni rechazados por Hacienda.

La normativa establece un plazo máximo de 2 días hábiles para remitir los comprobantes de contingencia. Este endpoint le muestra cuántos están pendientes y cuántos ya superaron el plazo, para ayudarle a cumplir con el monitoreo requerido antes de incurrir en infracciones tributarias.

Estados incluidos: draft, pending, sent, error.

Los comprobantes ya resueltos (accepted, rejected, cancelled) no aparecen aquí.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/vouchers/pending-contingency?situation=3&expired_only=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers/pending-contingency"
);

const params = {
    "situation": "3",
    "expired_only": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers/pending-contingency';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'situation' => '3',
            'expired_only' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Con comprobantes pendientes):


{
    "success": true,
    "data": {
        "items": [
            {
                "voucher_key": "50617042026...",
                "voucher_type": "01",
                "voucher_type_label": "Factura Electrónica",
                "consecutive_number": "00100001010000000001",
                "status": "error",
                "situation": "3",
                "situation_label": "Sin Internet",
                "issued_at": "2026-04-10T14:30:00-06:00",
                "deadline": "2026-04-14",
                "elapsed_business_days": 3,
                "is_expired": true,
                "receiver": {
                    "id_type": "02",
                    "id_number": "3101000001",
                    "name": "Empresa S.A."
                },
                "currency_code": "CRC",
                "total_comprobante": "56500.00000"
            }
        ],
        "total": 5,
        "expired_count": 2
    },
    "message": "5 comprobante(s) de contingencia sin resolver. 2 superan el plazo de 2 días hábiles.",
    "errors": null
}
 

Example response (200, Sin pendientes):


{
    "success": true,
    "data": {
        "items": [],
        "total": 0,
        "expired_count": 0
    },
    "message": "Sin comprobantes de contingencia pendientes.",
    "errors": null
}
 

Request      

GET api/v1/public/vouchers/pending-contingency

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

situation   string  optional    

Filtrar por situación. 2=Contingencia, 3=Sin internet. Sin valor muestra ambas. Example: 3

expired_only   boolean  optional    

Mostrar solo los que superaron el plazo de 2 días hábiles. Default: false. Example: true

Consultar un comprobante por clave

requires authentication

Devuelve el detalle completo de un comprobante identificado por su clave de 50 dígitos: datos del emisor y receptor, líneas, totales, estado actual y respuesta de Hacienda (si ya fue procesado).

La clave debe tener exactamente 50 dígitos numéricos — formatos inválidos retornan 404 sin procesar la consulta.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Comprobante encontrado):


{
    "success": true,
    "data": {
        "voucher_key": "50619032600310199999900100001010000000001112345678",
        "voucher_type": "01",
        "voucher_type_label": "Factura Electrónica",
        "consecutive_number": "00100001010000000001",
        "issued_at": "2026-04-13T10:00:00-06:00",
        "situation": "1",
        "situation_label": "Normal",
        "sale_condition": "01",
        "sale_condition_label": "Contado",
        "sale_condition_other": null,
        "credit_term": null,
        "receiver": {
            "id_type": "02",
            "id_number": "3101000001",
            "name": "EMPRESA EJEMPLO S.A.",
            "commercial_name": "Ejemplo Comercial",
            "emails": [
                "facturacion@ejemplo.cr"
            ]
        },
        "currency_code": "CRC",
        "exchange_rate": "1.00000",
        "totals": {
            "serv_gravados": "10000.00000",
            "serv_exentos": "0.00000",
            "serv_exonerado": "0.00000",
            "serv_no_sujeto": "0.00000",
            "merc_gravadas": "0.00000",
            "merc_exentas": "0.00000",
            "merc_exonerada": "0.00000",
            "merc_no_sujeta": "0.00000",
            "total_gravado": "10000.00000",
            "total_exento": "0.00000",
            "total_exonerado": "0.00000",
            "total_no_sujeto": "0.00000",
            "total_venta": "10000.00000",
            "total_descuentos": "0.00000",
            "total_venta_neta": "10000.00000",
            "total_impuesto": "1300.00000",
            "total_imp_asum_emisor_fab": "0.00000",
            "total_iva_devuelto": "0.00000",
            "total_otros_cargos": "0.00000",
            "total_comprobante": "11300.00000"
        },
        "payment_methods": [
            {
                "tipo": "01"
            }
        ],
        "status": "accepted",
        "hacienda": {
            "status": "aceptado",
            "message": null,
            "sent_at": "2026-04-13T10:01:00-06:00",
            "processed_at": "2026-04-13T10:02:00-06:00"
        },
        "environment": "production",
        "is_xml_available": true,
        "line_items_count": 1,
        "references_count": 0,
        "created_at": "2026-04-13T10:00:00-06:00",
        "updated_at": "2026-04-13T10:02:00-06:00"
    },
    "message": "Comprobante obtenido correctamente.",
    "errors": null
}
 

Example response (404, Comprobante no encontrado):


{
    "success": false,
    "data": null,
    "message": "El recurso solicitado no existe.",
    "errors": null
}
 

Request      

GET api/v1/public/vouchers/{key}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

key   string     

Clave de 50 dígitos del comprobante. Example: 50619032600310199999900100001010000000001112345678

Response

Response Fields

data   object     
voucher_key   string     

Clave numérica de 50 dígitos. Identificador único del comprobante ante Hacienda. Estructura: [3 país][6 fecha DDMMYY][12 cédula][20 consecutivo][1 situación][8 seguridad].

voucher_type   string     

Código del tipo de comprobante: 01=FE, 02=ND, 03=NC, 04=TE, 08=FEC, 09=FEE, 10=REP.

voucher_type_label   string     

Nombre legible del tipo de comprobante en español.

consecutive_number   string     

Número consecutivo interno de 20 dígitos asignado al emitir.

issued_at   string     

Fecha y hora de emisión en formato ISO 8601 con offset CR (-06:00).

situation   string     

Situación del comprobante: 1=Normal, 2=Contingencia, 3=Sin internet.

situation_label   string     

Nombre legible de la situación en español.

sale_condition   string     

Código de condición de venta: 01=Contado, 02=Crédito, 08=Crédito al Estado, 10=Crédito IVA 90d, 99=Otros.

sale_condition_label   string     

Nombre legible de la condición de venta.

sale_condition_other   string     

Descripción personalizada cuando sale_condition=99. Null en cualquier otro caso.

credit_term   string     

Plazo del crédito en días. Presente cuando sale_condition es 02, 08 o 10.

receiver   object     

Datos del receptor del comprobante. null cuando el tipo no requiere receptor (TE sin receptor, FEE sin receptor).

id_type   string     

Tipo de identificación del receptor: 0106.

id_number   string     

Número de identificación del receptor.

name   string     

Nombre o razón social del receptor.

commercial_name   string     

Nombre comercial del receptor. Puede ser null.

emails   string[]     

Correos electrónicos del receptor usados para el envío transaccional.

currency_code   string     

Código de moneda ISO 4217 (CRC, USD, EUR, etc.).

exchange_rate   string     

Tipo de cambio respecto al CRC. 1.00000 si la moneda es CRC.

totals   object     

Totales del ResumenFactura desglosados (Decimal 18,5).

serv_gravados   string     

Total servicios gravados con IVA.

serv_exentos   string     

Total servicios exentos.

serv_exonerado   string     

Total servicios exonerados.

serv_no_sujeto   string     

Total servicios no sujetos a IVA.

merc_gravadas   string     

Total mercancías gravadas con IVA.

merc_exentas   string     

Total mercancías exentas.

merc_exonerada   string     

Total mercancías exoneradas.

merc_no_sujeta   string     

Total mercancías no sujetas a IVA.

total_gravado   string     

Suma de servicios + mercancías gravados.

total_exento   string     

Suma de servicios + mercancías exentos.

total_exonerado   string     

Suma de servicios + mercancías exonerados.

total_no_sujeto   string     

Suma de servicios + mercancías no sujetos.

total_venta   string     

TotalVenta = suma de MontoTotalLinea de todas las líneas.

total_descuentos   string     

Suma de todos los descuentos aplicados.

total_venta_neta   string     

TotalVenta − TotalDescuentos.

total_impuesto   string     

Suma de todos los impuestos (IVA, selectivo, etc.).

total_imp_asum_emisor_fab   string     

Impuesto asumido por el emisor/fabricante. Normalmente 0.00000.

total_iva_devuelto   string     

IVA devuelto (aplicable en devoluciones). Normalmente 0.00000.

total_otros_cargos   string     

Suma de otros cargos adicionales al comprobante.

total_comprobante   string     

Monto final: TotalVentaNeta + TotalImpuesto − TotalIVADevuelto + TotalOtrosCargos.

payment_methods   object[]     

Medios de pago del comprobante. Cada objeto tiene tipo: 01=Efectivo, 02=Tarjeta, 03=Cheque, 04=Transferencia, 99=Otros.

status   string     

Estado actual del comprobante: draft, pending, sent, accepted, rejected, error, cancelled.

hacienda   object     

Información de la respuesta de Hacienda (MensajeHacienda).

status   string     

Estado según Hacienda: aceptado, rechazado, o null si aún no procesado.

message   string     

Detalle del mensaje de Hacienda. Contiene la descripción del error si fue rechazado. null si fue aceptado.

sent_at   string     

Fecha/hora en que se envió a Hacienda (ISO 8601). null si no enviado aún.

processed_at   string     

Fecha/hora en que Hacienda procesó el comprobante (ISO 8601). null si pendiente.

environment   string     

Ambiente del comprobante: production (valor fiscal) o sandbox (pruebas, sin valor fiscal).

is_xml_available   boolean     

true si los XML (firmado + respuesta) están disponibles para descarga. false si la retención expiró y fueron purgados.

line_items_count   integer     

Cantidad de líneas de detalle (LineaDetalle) del comprobante.

references_count   integer     

Cantidad de referencias a otros comprobantes (InformacionReferencia).

created_at   string     

Fecha de creación del registro (ISO 8601).

updated_at   string     

Fecha de última actualización del registro (ISO 8601).

Anular un comprobante (emite una Nota de Crédito)

requires authentication

En Costa Rica, la anulación de un comprobante electrónico NO es una operación directa. El mecanismo normativo es emitir una Nota de Crédito que referencia al comprobante original con el código 01 (Anula documento).

Este endpoint automatiza ese proceso:

  1. Lee el comprobante original por su clave.
  2. Construye automáticamente el payload de la NC (mismas líneas, mismo receptor) con la referencia correspondiente.
  3. La emite por el mismo pipeline que POST /vouchers (valida XSD, firma, envía a Hacienda).
  4. Retorna la NC generada con HTTP 202.

Restricciones:

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/cancel" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/cancel"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/cancel';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (202, NC de anulación generada):


{
    "success": true,
    "data": {
        "voucher_key": "50621042600310199999900100001010000000002212345679",
        "voucher_type": "03",
        "voucher_type_label": "Nota de Crédito",
        "consecutive_number": "00100001030000000001",
        "status": "pending",
        "environment": "production",
        "created_at": "2026-04-21T11:00:00-06:00"
    },
    "message": "NC de anulación generada. Encolada para envío a Hacienda.",
    "errors": null
}
 

Example response (409, El estado no permite anulación):


{
    "success": false,
    "data": null,
    "message": "Solo comprobantes aceptados por Hacienda pueden anularse.",
    "errors": null
}
 

Example response (422, Error de validación XSD de la NC):


{
    "success": false,
    "data": null,
    "message": "Validación XSD fallida para Nota de Crédito (03) — ...",
    "errors": {
        "voucher_type": [
            "03"
        ],
        "xsd": [
            {
                "linea": 45,
                "columna": 12,
                "mensaje": "..."
            }
        ]
    }
}
 

Example response (422, Error de firma de la NC):


{
    "success": false,
    "data": null,
    "message": "No se pudo firmar el comprobante. El certificado digital está vencido.",
    "errors": {
        "signature": [
            "..."
        ],
        "code": [
            "2002"
        ]
    }
}
 

Request      

POST api/v1/public/vouchers/{key}/cancel

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

key   string     

Clave de 50 dígitos del comprobante a anular. Example: 50619032600310199999900100001010000000001112345678

Descargar el XML firmado del comprobante

requires authentication

Devuelve el XML completo con firma digital XAdES-EPES, tal como fue enviado a la API de recepción de Hacienda. Este XML es el documento fiscal con validez legal.

Obligación de archivo: la normativa exige conservar este XML por 5 años. Descárguelo y archívelo de su lado dentro del período de retención de la plataforma (3 meses por defecto, hasta 5 años con el add-on de retención extendida).

Disponibilidad:

Después de la retención, los metadatos del comprobante y el PDF siguen disponibles — solo el XML deja de estar accesible.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/xml" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/xml"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/xml';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, XML disponible):


[archivo binario application/xml]
 

Example response (404, XML aún no generado):


{
    "success": false,
    "data": null,
    "message": "El XML firmado aún no está disponible. El comprobante está en estado 'draft'.",
    "errors": null
}
 

Example response (410, XML purgado — retención expirada):


{
    "success": false,
    "data": null,
    "message": "XML purgado. Retención expirada el 2026-06-15.",
    "errors": null
}
 

Request      

GET api/v1/public/vouchers/{key}/xml

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

key   string     

Clave de 50 dígitos. Example: 50619032600310199999900100001010000000001112345678

Descargar el XML de respuesta de Hacienda (MensajeHacienda)

requires authentication

Devuelve el MensajeHacienda XML firmado por Hacienda que representa el resultado oficial del procesamiento del comprobante. Incluye:

Este XML es evidencia oficial de la validación ante Hacienda. Su obligación legal de archivo es de 5 años, igual que el XML firmado.

Disponibilidad:

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/xml-response" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/xml-response"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/xml-response';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, XML respuesta disponible):


[archivo binario application/xml]
 

Example response (404, Respuesta aún no disponible):


{
    "success": false,
    "data": null,
    "message": "La respuesta de Hacienda aún no está disponible. El comprobante está en estado 'sent'.",
    "errors": null
}
 

Example response (410, XML purgado — retención expirada):


{
    "success": false,
    "data": null,
    "message": "XML purgado. Retención expirada el 2026-06-15.",
    "errors": null
}
 

Request      

GET api/v1/public/vouchers/{key}/xml-response

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

key   string     

Clave de 50 dígitos. Example: 50619032600310199999900100001010000000001112345678

Descargar la representación gráfica (PDF) del comprobante

requires authentication

Genera y devuelve el PDF del comprobante usando la plantilla por defecto del contribuyente (o una específica si se indica vía template_id). El PDF incluye obligatoriamente un código QR con la clave de 50 dígitos en la esquina inferior derecha, con tamaño mínimo de 2.5 cm conforme a la normativa.

Disponibilidad:

Para gestionar plantillas PDF personalizadas, vea el grupo Plantillas PDF de esta documentación.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/pdf?template_id=16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/pdf"
);

const params = {
    "template_id": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/vouchers/50619032600310199999900100001010000000001112345678/pdf';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'template_id' => '16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, PDF generado):


[archivo binario application/pdf]
 

Example response (404, Comprobante en draft):


{
    "success": false,
    "data": null,
    "message": "El PDF no está disponible. El comprobante está en estado 'draft'.",
    "errors": null
}
 

Example response (404, Sin plantilla PDF default activa):


{
    "success": false,
    "data": null,
    "message": "El contribuyente no tiene plantilla PDF default activa.",
    "errors": null
}
 

Request      

GET api/v1/public/vouchers/{key}/pdf

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

key   string     

Clave de 50 dígitos. Example: 50619032600310199999900100001010000000001112345678

Query Parameters

template_id   integer  optional    

ID de una plantilla específica. Si no se envía, usa la plantilla default del contribuyente. Example: 16

Perfil

Estadísticas de uso mensual.

requires authentication

Retorna los conteos de comprobantes emitidos y emails enviados durante el período mensual en curso, junto con los límites de su plan.

Solo cuenta comprobantes de producción aceptados por Hacienda. Los comprobantes emitidos en sandbox no consumen el límite del plan.

Si es integrador, el conteo agrega automáticamente los comprobantes propios y los de todos sus clientes gestionados, permitiendo monitorear el consumo total del plan en una sola consulta.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/profile/usage" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/profile/usage"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/profile/usage';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Contribuyente normal):


{
    "success": true,
    "data": {
        "vouchers_this_month": 87,
        "vouchers_limit": 500,
        "vouchers_remaining": 413,
        "vouchers_percentage": 17.4,
        "emails_today": 12,
        "emails_limit": 75,
        "period_start": "2026-04-01",
        "period_end": "2026-04-30"
    },
    "message": "Estadísticas de uso mensual.",
    "errors": null
}
 

Example response (200, Integrador (consumo agregado con todos sus clientes)):


{
    "success": true,
    "data": {
        "vouchers_this_month": 12450,
        "vouchers_limit": 50000,
        "vouchers_remaining": 37550,
        "vouchers_percentage": 24.9,
        "emails_today": 0,
        "emails_limit": 5000,
        "period_start": "2026-04-01",
        "period_end": "2026-04-30"
    },
    "message": "Estadísticas de uso mensual.",
    "errors": null
}
 

Request      

GET api/v1/public/profile/usage

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Response

Response Fields

data   object     
vouchers_this_month   integer     

Comprobantes de producción aceptados emitidos en el mes actual. Si es integrador, suma propios + todos los clientes gestionados.

vouchers_limit   integer     

Límite mensual del plan contratado.

vouchers_remaining   integer     

Comprobantes restantes antes de alcanzar el límite: max(0, limit - this_month).

vouchers_percentage   number     

Porcentaje de uso del cupo mensual (0-100, 2 decimales).

emails_today   integer     

Emails transaccionales enviados hoy. 0 si no hay tracking implementado aún.

emails_limit   integer     

Límite diario de emails del plan.

period_start   string     

Primer día del mes actual (YYYY-MM-DD). Zona horaria: America/Costa_Rica.

period_end   string     

Último día del mes actual (YYYY-MM-DD).

Consultar perfil completo del contribuyente autenticado.

requires authentication

Retorna todos los datos del contribuyente incluyendo el plan activo con sus limites y features, las actividades economicas registradas, la ubicacion y la configuracion de contacto.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/profile" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/profile"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/profile';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Perfil completo):


{
    "success": true,
    "data": {
        "id": "019d867d-0241-7288-8ece-fd64da75616d",
        "legal_name": "Empresa Ejemplo S.A.",
        "trade_name": "Ejemplo Shop",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "id_number": "3101000000",
        "emails": [
            "facturacion@empresa.cr"
        ],
        "economic_activities": [
            "6201.0"
        ],
        "location": {
            "province": 1,
            "canton": 1,
            "district": 1
        },
        "neighborhood": "San Pedro",
        "address": "200 metros norte del parque central",
        "phone": {
            "country_code": 506,
            "number": "22001234"
        },
        "is_active": true,
        "production_enabled": false,
        "hacienda_environment": "sandbox",
        "plan_id": "019d0001-0000-0000-0000-000000000001",
        "is_managed": false,
        "is_integrator": false,
        "plan": {
            "id": "019d0001-0000-0000-0000-000000000001",
            "code": "pyme",
            "name": "Pyme",
            "description": "Automatice su facturación con hasta 500 comprobantes/mes.",
            "monthly_price_usd": null,
            "annual_price_usd": 96,
            "billing_mode": "annual_only",
            "annual_discount_percent": 0,
            "vouchers_per_month": 500,
            "overage_price_per_voucher": 0.03,
            "max_users": 3,
            "max_api_tokens": 2,
            "max_clients": 300,
            "max_items": 750,
            "max_pdf_templates": 1,
            "max_daily_emails": 75,
            "max_webhook_endpoints": 1,
            "max_branch_terminals": 2,
            "max_managed_contributors": 1,
            "api_rate_limit_per_minute": 20,
            "retention_months": 3,
            "extended_retention_eligible": true,
            "pdf_branding": "platform",
            "api_write_enabled": true,
            "api_readonly_enabled": false,
            "sandbox_access": true,
            "webhooks_enabled": false,
            "multi_contributor": false,
            "receiver_module": true,
            "bulk_emission": false,
            "sla_support": false
        },
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-13T14:30:00-06:00"
    },
    "message": "Perfil del contribuyente.",
    "errors": null
}
 

Request      

GET api/v1/public/profile

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Response

Response Fields

data   object     
id   string     

UUID del contribuyente.

legal_name   string     

Razón social o nombre legal. Mapea a EmisorType/Nombre del XSD (min 5, max 100).

trade_name   string     

Nombre comercial. Mapea a EmisorType/NombreComercial del XSD. null si no se configuró.

id_type   string     

Tipo de identificación: 01=Física, 02=Jurídica, 03=DIMEX, 04=NITE. Inmutable.

id_type_label   string     

Nombre legible del tipo de identificación.

id_number   string     

Número de identificación del contribuyente.

emails   string[]     

Correos electrónicos del emisor (máximo 4). El primero mapea a CorreoElectronico del XSD.

economic_activities   string[]     

Códigos CIIU en formato Hacienda XXXX.X. Mapean a CodigoActividadEmisor del XSD.

location   object     

Ubicación del emisor (UbicacionType del XSD). null si no tiene ubicación configurada.

province   integer     

Código de provincia (1-7).

canton   integer     

Código de cantón.

district   integer     

Código de distrito.

neighborhood   string     

Barrio del emisor. null si no se configuró.

address   string     

Señas exactas del emisor. Mapea a OtrasSenas del XSD (max 250). null si no se configuró.

phone   object     

Teléfono del emisor (TelefonoType del XSD). null si no tiene teléfono configurado.

country_code   integer     

Código de país (1-3 dígitos). Default: 506.

number   string     

Número de teléfono.

is_active   boolean     

true si la cuenta está activa. Solo un admin de plataforma puede suspenderla.

production_enabled   boolean     

true si la cuenta está autorizada por DGT (BCCR + cumplimiento normativo) para emitir comprobantes con valor fiscal real ante Hacienda. Fuente de verdad del estado fiscal del contribuyente.

hacienda_environment   string     

@deprecated Derivado de production_enabled. Para nuevas integraciones use production_enabled directamente. Valores: sandbox (production_enabled=false, modo pruebas) o production (production_enabled=true, autorizado para emisión real). El ambiente del voucher individual se determina por la URL del request (/sandbox/ vs producción), NO por este campo.

plan_id   string     

UUID del plan contratado.

is_managed   boolean     

true si el contribuyente es gestionado por al menos un integrador.

is_integrator   boolean     

true si el contribuyente tiene plan Integrador y gestiona otros clientes.

plan   object     

Detalle completo del plan contratado con todos los límites y features.

code   string     

Código del plan: free, starter, pyme, professional, business, integrator.

name   string     

Nombre comercial del plan.

vouchers_per_month   integer     

Límite mensual de comprobantes.

max_clients   integer     

Máximo de clientes en el catálogo.

max_items   integer     

Máximo de items en el catálogo.

max_webhook_endpoints   integer     

Máximo de webhook endpoints.

max_pdf_templates   integer     

Máximo de plantillas PDF.

max_daily_emails   integer     

Máximo de emails por día.

api_rate_limit_per_minute   integer     

Requests por minuto permitidos.

sandbox_access   boolean     

true si el plan incluye acceso a la API sandbox.

pdf_branding   string     

Nivel de branding: platform, minimal o white_label.

created_at   string     

Fecha de creación de la cuenta (ISO 8601).

updated_at   string     

Fecha de última actualización (ISO 8601).

Actualizar datos editables del perfil del contribuyente.

requires authentication

Soporta actualizacion parcial: envie unicamente los campos que desea modificar. Los campos no incluidos en el payload conservan su valor actual.

Campos editables: id_number, legal_name, trade_name, emails, economic_activities, phone, phone_country_code, province, canton, district, neighborhood, address.

Campos inmutables (no se pueden cambiar desde este endpoint): id_type (tipo de identificacion), el plan activo, production_enabled (autorización DGT — gestionada por super_admin tras verificar firma BCCR y cumplimiento normativo) y el estado activo/inactivo de la cuenta.

La configuracion de email transaccional se gestiona desde su propio endpoint: PUT /email-settings.

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/profile" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"id_number\": \"3101000000\",
    \"legal_name\": \"Empresa Ejemplo S.A.\",
    \"trade_name\": \"Ejemplo Tienda\",
    \"emails\": [
        \"facturacion@empresa.cr\"
    ],
    \"economic_activities\": [
        \"6201.0\"
    ],
    \"phone\": \"22001234\",
    \"phone_country_code\": 506,
    \"province\": 1,
    \"canton\": 1,
    \"district\": 1,
    \"neighborhood\": \"San Pedro\",
    \"address\": \"200 metros norte del parque central\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/profile"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "id_number": "3101000000",
    "legal_name": "Empresa Ejemplo S.A.",
    "trade_name": "Ejemplo Tienda",
    "emails": [
        "facturacion@empresa.cr"
    ],
    "economic_activities": [
        "6201.0"
    ],
    "phone": "22001234",
    "phone_country_code": 506,
    "province": 1,
    "canton": 1,
    "district": 1,
    "neighborhood": "San Pedro",
    "address": "200 metros norte del parque central"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/profile';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'id_number' => '3101000000',
            'legal_name' => 'Empresa Ejemplo S.A.',
            'trade_name' => 'Ejemplo Tienda',
            'emails' => [
                'facturacion@empresa.cr',
            ],
            'economic_activities' => [
                '6201.0',
            ],
            'phone' => '22001234',
            'phone_country_code' => 506,
            'province' => 1,
            'canton' => 1,
            'district' => 1,
            'neighborhood' => 'San Pedro',
            'address' => '200 metros norte del parque central',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Perfil actualizado):


{
    "success": true,
    "data": {
        "id": "019d867d-0241-7288-8ece-fd64da75616d",
        "legal_name": "Empresa Ejemplo S.A. Actualizada",
        "trade_name": "Ejemplo Shop Nuevo",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "id_number": "3101000000",
        "emails": [
            "facturacion@empresa.cr",
            "contabilidad@empresa.cr"
        ],
        "economic_activities": [
            "6201.0",
            "4711.0"
        ],
        "location": {
            "province": 1,
            "canton": 1,
            "district": 1
        },
        "neighborhood": "San Pedro",
        "address": "200 metros norte del parque central",
        "phone": {
            "country_code": 506,
            "number": "22001234"
        },
        "is_active": true,
        "production_enabled": false,
        "hacienda_environment": "sandbox",
        "plan_id": "019d0001-0000-0000-0000-000000000001",
        "is_managed": false,
        "is_integrator": false,
        "plan": {
            "id": "019d0001-0000-0000-0000-000000000001",
            "code": "pyme",
            "name": "Pyme",
            "vouchers_per_month": 500,
            "max_clients": 300,
            "sandbox_access": true
        },
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-16T09:15:00-06:00"
    },
    "message": "Perfil actualizado correctamente.",
    "errors": null
}
 

Example response (422, Error de validación):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "emails.0": [
            "El campo emails.0 debe ser un correo electrónico válido."
        ],
        "economic_activities.0": [
            "El formato del código de actividad económica debe ser XXXX.X (ejemplo: 6201.0)."
        ]
    }
}
 

Request      

PUT api/v1/public/profile

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

id_number   string  optional    

Numero de identificacion. Solo digitos, 5-12 caracteres. Cambiar este valor afecta la clave de 50 digitos en comprobantes futuros. Must match the regex /^\d{5,12}$/. Example: 3101000000

legal_name   string  optional    

Razon social o nombre completo. Min 5, max 100 caracteres. validation.min validation.max. Example: Empresa Ejemplo S.A.

trade_name   string  optional    

Nombre comercial. Opcional, min 3, max 80 caracteres. validation.min validation.max. Example: Ejemplo Tienda

emails   string[]  optional    

Correo electrónico individual. Max 160 chars. validation.email validation.max.

economic_activities   string[]     

Código CIIU individual en formato XXXX.X. Must match the regex /^\d{4}.\d$/.

phone   string  optional    

Número de teléfono sin código de país. Solo dígitos, 4-20 chars. Must match the regex /^\d{4,20}$/. Example: 22001234

phone_country_code   integer  optional    

Código de país telefónico (1-999). Default 506 (Costa Rica). validation.between. Example: 506

province   integer  optional    

Código de provincia (1-7). Si se envía, canton y district son obligatorios. validation.between. Example: 1

canton   integer  optional    

Código de cantón (1-99). Obligatorio si province presente. validation.between. Example: 1

district   integer  optional    

Código de distrito (1-99). Obligatorio si province presente. validation.between. Example: 1

neighborhood   string  optional    

Barrio. Opcional, min 5, max 50 chars. validation.min validation.max. Example: San Pedro

address   string  optional    

Senas exactas de la direccion. Min 5, max 250 caracteres. validation.min validation.max. Example: 200 metros norte del parque central

Certificados Digitales

Listar certificados digitales del contribuyente

requires authentication

Devuelve el historial completo de certificados digitales registrados por el contribuyente: el certificado actualmente activo y todos los que fueron desactivados previamente (conservados para auditoría).

Los datos sensibles del certificado (contenido del archivo .p12, contraseña y PIN de Hacienda) nunca se incluyen en la respuesta, incluso si usted mismo los cargó.

Cada registro incluye:

Ordenamiento: el más reciente primero.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/certificates" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/certificates"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/certificates';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado con activo e histórico):


{
    "success": true,
    "data": [
        {
            "id": "019d867d-1111-7288-8ece-fd64da756001",
            "environment": "sandbox",
            "is_active": true,
            "is_expired": false,
            "days_remaining": 120,
            "valid_from": "2024-01-01T00:00:00-06:00",
            "valid_until": "2026-08-15T23:59:59-06:00",
            "certificate_subject": "CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001",
            "certificate_serial": "0A1B2C3D4E5F",
            "created_at": "2026-04-09T10:00:00-06:00"
        },
        {
            "id": "019d867d-2222-7288-8ece-fd64da756002",
            "environment": "sandbox",
            "is_active": false,
            "is_expired": true,
            "days_remaining": 0,
            "valid_from": "2022-01-01T00:00:00-06:00",
            "valid_until": "2024-01-01T00:00:00-06:00",
            "certificate_subject": "CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001",
            "certificate_serial": "AA11BB22CC33",
            "created_at": "2024-01-10T08:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null
}
 

Example response (200, Sin certificados cargados):


{
    "success": true,
    "data": [],
    "message": "",
    "errors": null
}
 

Example response (401, No autenticado):


{
    "success": false,
    "data": null,
    "message": "Unauthenticated.",
    "errors": null
}
 

Request      

GET api/v1/public/certificates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Subir y activar un certificado digital `.p12`

requires authentication

Sube un archivo .p12 (PKCS#12) con su contraseña y el PIN de Hacienda, y lo activa como certificado para firmar comprobantes en el ambiente indicado.

Este request debe enviarse como multipart/form-data (no JSON), porque incluye un archivo binario.

Proceso interno al subir:

  1. Se valida que el archivo sea un .p12 legítimo y que la contraseña lo abra correctamente.
  2. Se extraen los metadatos del certificado X.509 (vigencia, subject, número de serie).
  3. Si ya existía un certificado activo del mismo ambiente, se desactiva automáticamente (queda en el histórico).
  4. El nuevo certificado queda activo y listo para firmar.

Ejemplo con curl:

curl -X POST https://api.almendro.cr/api/v1/public/certificates \
  -H "Authorization: Bearer {su_token}" \
  -F "p12_file=@/ruta/a/certificado.p12" \
  -F "p12_password=contrasena-del-p12" \
  -F "hacienda_pin=1234" \
  -F "environment=sandbox"

Ejemplo con Node.js (axios + form-data):

const FormData = require('form-data')
const fs = require('fs')
const axios = require('axios')

const form = new FormData()
form.append('p12_file', fs.createReadStream('certificado.p12'))
form.append('p12_password', 'contrasena-del-p12')
form.append('hacienda_pin', '1234')
form.append('environment', 'sandbox')

const response = await axios.post(
  'https://api.almendro.cr/api/v1/public/certificates',
  form,
  { headers: { ...form.getHeaders(), Authorization: `Bearer ${token}` } },
)
Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/certificates" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "p12_password=MiContrasenaDelP12"\
    --form "hacienda_pin=1234"\
    --form "environment=sandbox"\
    --form "p12_file=@/tmp/phpgrm2i4vq3o9baKpptFj" 
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/certificates"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('p12_password', 'MiContrasenaDelP12');
body.append('hacienda_pin', '1234');
body.append('environment', 'sandbox');
body.append('p12_file', document.querySelector('input[name="p12_file"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/certificates';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'multipart/form-data',
            'Accept' => 'application/json',
        ],
        'multipart' => [
            [
                'name' => 'p12_password',
                'contents' => 'MiContrasenaDelP12'
            ],
            [
                'name' => 'hacienda_pin',
                'contents' => '1234'
            ],
            [
                'name' => 'environment',
                'contents' => 'sandbox'
            ],
            [
                'name' => 'p12_file',
                'contents' => fopen('/tmp/phpgrm2i4vq3o9baKpptFj', 'r')
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Certificado registrado y activado):


{
    "success": true,
    "data": {
        "id": "019d867d-3333-7288-8ece-fd64da756003",
        "environment": "sandbox",
        "is_active": true,
        "is_expired": false,
        "days_remaining": 730,
        "valid_from": "2026-01-01T00:00:00-06:00",
        "valid_until": "2028-01-01T00:00:00-06:00",
        "certificate_subject": "CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001",
        "certificate_serial": "0A1B2C3D4E5F",
        "created_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Certificado registrado y activado correctamente.",
    "errors": null
}
 

Example response (422, Contraseña incorrecta del .p12):


{
    "success": false,
    "data": null,
    "message": "No se pudo leer el certificado .p12. Verifique que la contraseña sea correcta y que el archivo sea un certificado digital válido.",
    "errors": {
        "p12_file": [
            "No se pudo leer el certificado .p12. Verifique que la contraseña sea correcta y que el archivo sea un certificado digital válido."
        ]
    }
}
 

Example response (422, Archivo no es un .p12 válido):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "p12_file": [
            "El archivo debe tener extensión .p12 o .pfx."
        ]
    }
}
 

Example response (422, Ambiente inválido):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "environment": [
            "El ambiente seleccionado no es válido. Use sandbox o production."
        ]
    }
}
 

Example response (422, Falta el PIN de Hacienda):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "hacienda_pin": [
            "El PIN de Hacienda es obligatorio."
        ]
    }
}
 

Request      

POST api/v1/public/certificates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters

p12_file   file     

Archivo de certificado digital en formato PKCS#12 (extensión .p12 o .pfx). Tamaño máximo: 2 MB. Example: /tmp/phpgrm2i4vq3o9baKpptFj

p12_password   string     

Contraseña que protege el archivo .p12 (la que usted definió al generarlo). Example: MiContrasenaDelP12

hacienda_pin   string     

PIN asignado por Hacienda al registrarse como emisor en ATV (normalmente 4 dígitos). Example: 1234

environment   string     

Ambiente al que se asigna este certificado. Valores: sandbox o production. Example: sandbox

Response

Response Fields

data   object     
id   string     

UUID del certificado. Úselo en DELETE /certificates/{id} para desactivarlo.

environment   string     

Ambiente al que pertenece: sandbox o production. Cada ambiente tiene su propio certificado activo independiente.

is_active   boolean     

true si este certificado es el actualmente usado para firmar en su ambiente. Solo puede haber 1 activo por contribuyente + ambiente.

is_expired   boolean     

true si el certificado X.509 ya venció (valid_until < now()). Independiente de is_active — un certificado activo puede estar vencido si no fue renovado a tiempo. null si no se pudo extraer la vigencia.

days_remaining   integer     

Días restantes hasta el vencimiento del certificado. 0 si ya venció. null si no se pudo extraer la vigencia. Útil para alertas de renovación.

valid_from   string     

Fecha de inicio de vigencia del certificado X.509 (ISO 8601). Extraída de notBefore del X.509 al subir el .p12. null si no se pudo parsear.

valid_until   string     

Fecha de fin de vigencia del certificado X.509 (ISO 8601). Extraída de notAfter del X.509. Los certificados del BCCR tienen vigencia típica de 2 años. null si no se pudo parsear.

certificate_subject   string     

Distinguished Name (DN) del titular del certificado. Ejemplo: CN=EMPRESA S.A., serialNumber=3101000001. null si no se pudo extraer.

certificate_serial   string     

Número de serie del certificado X.509 en hexadecimal. Útil para auditoría e identificación del certificado ante la entidad certificadora. null si no se pudo extraer.

created_at   string     

Fecha en que se subió este certificado a la plataforma (ISO 8601).

Desactivar un certificado digital

requires authentication

Marca el certificado como inactivo. El registro no se elimina: permanece en el histórico para auditoría de todas las firmas realizadas durante su vigencia.

Consecuencias inmediatas:

Solo puede desactivar certificados propios. Si el UUID no corresponde a su contribuyente, retorna 404.

Casos típicos para desactivar:

  • El certificado fue comprometido (pérdida, acceso no autorizado).
  • Va a subir un nuevo certificado pero quiere dejar el ambiente sin firma temporalmente (caso raro).
  • Migración de un contribuyente a otra plataforma.

No necesita desactivar el certificado anterior antes de subir uno nuevo — el POST /certificates ya lo hace automáticamente al registrar el nuevo.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/certificates/019d867d-1111-7288-8ece-fd64da756001" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/certificates/019d867d-1111-7288-8ece-fd64da756001"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/certificates/019d867d-1111-7288-8ece-fd64da756001';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Certificado desactivado (sin accesos asociados)):


{
    "success": true,
    "data": null,
    "message": "Certificado desactivado correctamente.",
    "errors": null
}
 

Example response (200, Certificado desactivado con auto-revocación de accesos):


{
    "success": true,
    "data": null,
    "message": "Certificado desactivado correctamente. Se revocaron 2 accesos de integradores asociados.",
    "errors": null
}
 

Example response (404, Certificado no encontrado o no pertenece al contribuyente):


{
    "success": false,
    "data": null,
    "message": "Certificado no encontrado.",
    "errors": null
}
 

Request      

DELETE api/v1/public/certificates/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del certificado a desactivar. Example: 019d867d-1111-7288-8ece-fd64da756001

Plantillas PDF

Listar plantillas PDF del contribuyente

requires authentication

Devuelve todas las plantillas PDF activas del contribuyente, ordenadas con la plantilla por defecto primero (is_default=true) y luego por nombre alfabéticamente.

Este endpoint no pagina — el número de plantillas por contribuyente es pequeño (límite por plan entre 1 y 15), por lo que siempre se devuelve la lista completa.

Cada plantilla incluye su configuración completa (config_json) con colores, fuentes, layout y parámetros del QR, además de un flag has_logo que indica si tiene un logo cargado.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/pdf-templates" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/pdf-templates"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/pdf-templates';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado de plantillas (default primero)):


{
    "success": true,
    "data": [
        {
            "id": 1,
            "name": "Classic",
            "is_default": true,
            "paper_size": "letter",
            "config_json": {
                "colors": {
                    "primary": "#2d3748",
                    "secondary": "#4a5568",
                    "accent": "#3182ce"
                },
                "fonts": {
                    "family": "Helvetica",
                    "size_title": 14,
                    "size_body": 9
                },
                "layout": {
                    "show_logo": true,
                    "logo_position": "left",
                    "logo_max_height": 60
                },
                "qr": {
                    "enabled": true,
                    "size": 100,
                    "position": "bottom-right"
                }
            },
            "has_logo": true,
            "is_active": true,
            "created_at": "2026-04-01T10:00:00-06:00",
            "updated_at": "2026-04-10T14:30:00-06:00"
        },
        {
            "id": 2,
            "name": "Minimal",
            "is_default": false,
            "paper_size": "A4",
            "config_json": {
                "colors": {
                    "primary": "#000000",
                    "secondary": "#666666",
                    "accent": "#000000"
                },
                "fonts": {
                    "family": "Helvetica",
                    "size_title": 12,
                    "size_body": 8
                },
                "layout": {
                    "show_logo": false,
                    "logo_position": "left",
                    "logo_max_height": 60
                },
                "qr": {
                    "enabled": true,
                    "size": 80,
                    "position": "bottom-right"
                }
            },
            "has_logo": false,
            "is_active": true,
            "created_at": "2026-04-05T08:00:00-06:00",
            "updated_at": "2026-04-05T08:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null
}
 

Example response (401, No autenticado):


{
    "success": false,
    "data": null,
    "message": "Unauthenticated.",
    "errors": null
}
 

Request      

GET api/v1/public/pdf-templates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Crear una nueva plantilla PDF

requires authentication

Registra una plantilla personalizada en el catálogo del contribuyente. Si envía is_default: true, la plantilla default anterior se desmarca automáticamente.

Si no envía config_json, se aplica una configuración por defecto que cumple con todos los requisitos normativos (QR mínimo 2.5 cm en la esquina inferior derecha).

Si envía config_json, puede enviar un objeto parcial — las claves no enviadas usan los valores del template default.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/pdf-templates" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Mi Plantilla Corporativa\",
    \"is_default\": false,
    \"paper_size\": \"letter\",
    \"config_json\": {
        \"colors\": {
            \"primary\": \"#0a66c2\",
            \"secondary\": \"#333333\",
            \"accent\": \"#0a66c2\"
        },
        \"fonts\": {
            \"family\": \"Helvetica\",
            \"size_title\": 14,
            \"size_body\": 9
        },
        \"layout\": {
            \"show_logo\": true,
            \"logo_position\": \"left\",
            \"logo_max_height\": 60
        },
        \"qr\": {
            \"enabled\": true,
            \"size\": 100,
            \"position\": \"bottom-right\"
        },
        \"margins\": {
            \"top\": 15,
            \"right\": 15,
            \"bottom\": 15,
            \"left\": 15
        },
        \"style\": \"classic\",
        \"footer_text\": \"Documento generado electrónicamente | Almendro Factura Electrónica\"
    }
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/pdf-templates"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Mi Plantilla Corporativa",
    "is_default": false,
    "paper_size": "letter",
    "config_json": {
        "colors": {
            "primary": "#0a66c2",
            "secondary": "#333333",
            "accent": "#0a66c2"
        },
        "fonts": {
            "family": "Helvetica",
            "size_title": 14,
            "size_body": 9
        },
        "layout": {
            "show_logo": true,
            "logo_position": "left",
            "logo_max_height": 60
        },
        "qr": {
            "enabled": true,
            "size": 100,
            "position": "bottom-right"
        },
        "margins": {
            "top": 15,
            "right": 15,
            "bottom": 15,
            "left": 15
        },
        "style": "classic",
        "footer_text": "Documento generado electrónicamente | Almendro Factura Electrónica"
    }
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/pdf-templates';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Mi Plantilla Corporativa',
            'is_default' => false,
            'paper_size' => 'letter',
            'config_json' => [
                'colors' => [
                    'primary' => '#0a66c2',
                    'secondary' => '#333333',
                    'accent' => '#0a66c2',
                ],
                'fonts' => [
                    'family' => 'Helvetica',
                    'size_title' => 14,
                    'size_body' => 9,
                ],
                'layout' => [
                    'show_logo' => true,
                    'logo_position' => 'left',
                    'logo_max_height' => 60,
                ],
                'qr' => [
                    'enabled' => true,
                    'size' => 100,
                    'position' => 'bottom-right',
                ],
                'margins' => [
                    'top' => 15,
                    'right' => 15,
                    'bottom' => 15,
                    'left' => 15,
                ],
                'style' => 'classic',
                'footer_text' => 'Documento generado electrónicamente | Almendro Factura Electrónica',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Plantilla creada):


{
    "success": true,
    "data": {
        "id": 3,
        "name": "Mi Plantilla Corporativa",
        "is_default": false,
        "paper_size": "letter",
        "config_json": {
            "colors": {
                "primary": "#0a66c2",
                "secondary": "#333333",
                "accent": "#0a66c2"
            },
            "fonts": {
                "family": "Helvetica",
                "size_title": 14,
                "size_body": 9
            },
            "layout": {
                "show_logo": true,
                "logo_position": "left",
                "logo_max_height": 60
            },
            "qr": {
                "enabled": true,
                "size": 100,
                "position": "bottom-right"
            }
        },
        "has_logo": false,
        "is_active": true,
        "created_at": "2026-04-16T10:00:00-06:00",
        "updated_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Plantilla creada correctamente.",
    "errors": null
}
 

Example response (422, Nombre duplicado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "name": [
            "Ya existe una plantilla con este nombre para el contribuyente."
        ]
    }
}
 

Example response (422, Color hexadecimal inválido):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "config_json.colors.primary": [
            "El color debe ser un hexadecimal válido (ej. #1a365d)."
        ]
    }
}
 

Example response (422, Tamaño del QR por debajo del mínimo normativo):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "config_json.qr.size": [
            "El tamaño mínimo del QR es 70 pt (≈ 2.5 cm) por normativa."
        ]
    }
}
 

Example response (422, Límite del plan alcanzado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "plan": [
            "Ha alcanzado el límite de plantillas PDF de su plan. Actualice a un plan superior."
        ]
    }
}
 

Request      

POST api/v1/public/pdf-templates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Nombre identificable de la plantilla (3-100 caracteres). Único por contribuyente. Example: Mi Plantilla Corporativa

is_default   boolean  optional    

Si será la plantilla por defecto. Si true, desmarca la anterior automáticamente. Default: false. Example: false

paper_size   string  optional    

Tamaño del papel. Valores: letter o A4. Default: letter. Example: letter

config_json   object  optional    

Configuración visual. Acepta un objeto parcial; las claves no enviadas usan valores por defecto. Ver estructura en la Guía del grupo.

colors   object  optional    
primary   string  optional    

Color hexadecimal principal. Example: #0a66c2

secondary   string  optional    

Color hexadecimal secundario. Example: #333333

text   string  optional    

Must match the regex /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/. Example: #1a202c

accent   string  optional    

Color hexadecimal de acentos. Example: #0a66c2

fonts   object  optional    
family   string  optional    

Familia tipográfica. Valores: Helvetica, Times-Roman, Courier. Example: Helvetica

size_header   integer  optional    

validation.min validation.max. Example: 13

size_body   integer  optional    

Tamaño del cuerpo (6-14). Example: 9

size_footer   integer  optional    

validation.min validation.max. Example: 6

size_title   integer  optional    

Tamaño del título (10-20). Example: 14

layout   object  optional    
show_logo   boolean  optional    

Si se muestra el logo. Example: true

logo_position   string  optional    

Posición del logo. Valores: left, center, right. Example: left

logo_max_height   integer  optional    

Alto máximo del logo en pt (20-100). Example: 60

show_commercial_name   boolean  optional    

Example: true

show_address   boolean  optional    

Example: true

show_phone   boolean  optional    

Example: true

show_email   boolean  optional    

Example: true

show_observations   boolean  optional    

Example: true

show_payment_methods   boolean  optional    

Example: true

show_references   boolean  optional    

Example: true

show_other_charges   boolean  optional    

Example: true

qr   object  optional    
size   integer  optional    

Tamaño del QR en pt (70-150, mínimo 70 ≈ 2.5 cm). Example: 100

position   string  optional    

Posición del QR. Valor recomendado por normativa: bottom-right. Example: bottom-right

enabled   boolean  optional    

Incluir QR (normativa obliga true). Example: true

margins   object  optional    
top   integer  optional    

validation.min validation.max. Example: 15

right   integer  optional    

validation.min validation.max. Example: 15

bottom   integer  optional    

validation.min validation.max. Example: 15

left   integer  optional    

validation.min validation.max. Example: 15

style   string  optional    

Example: classic

Must be one of:
  • classic
  • modern
  • minimal
  • bold
  • split
footer_text   string  optional    

validation.max. Example: Documento generado electrónicamente | Almendro Factura Electrónica

Response

Response Fields

data   object     
id   integer     

ID numérico de la plantilla. Úselo en PUT, DELETE, /preview y /logo.

name   string     

Nombre identificable de la plantilla (3-100 chars). Único por contribuyente.

is_default   boolean     

true si es la plantilla usada por defecto al generar PDFs. Solo una plantilla puede ser default por contribuyente.

paper_size   string     

Tamaño del papel: letter (Carta US 8.5×11") o A4 (210×297mm).

config_json   object     

Configuración visual completa de la plantilla con 4 bloques: colors, fonts, layout, qr.

colors   object     

Paleta de colores hexadecimales.

primary   string     

Color principal (encabezados, títulos). Ejemplo: #2d3748.

secondary   string     

Color secundario (texto, bordes). Ejemplo: #4a5568.

accent   string     

Color de acentos (líneas, badges). Ejemplo: #3182ce.

fonts   object     

Configuración tipográfica.

family   string     

Familia tipográfica: Helvetica, Times-Roman o Courier.

size_title   integer     

Tamaño del título principal en pt (10-20).

size_body   integer     

Tamaño del texto general en pt (6-14).

layout   object     

Configuración de layout y logo.

show_logo   boolean     

Si se muestra el logo del contribuyente en el encabezado.

logo_position   string     

Posición del logo: left, center o right.

logo_max_height   integer     

Alto máximo del logo en pt (20-100).

qr   object     

Configuración del código QR (obligatorio por normativa).

enabled   boolean     

Si se incluye el QR. Siempre true por normativa — el sistema ignora false.

size   integer     

Tamaño del QR en pt. Mínimo 70 (≈2.5 cm por normativa). Máximo 150.

position   string     

Posición del QR: bottom-right (exigido por normativa) o bottom-left.

has_logo   boolean     

true si la plantilla tiene un archivo de logo cargado. Use POST /pdf-templates/{id}/logo para subirlo.

is_active   boolean     

true si la plantilla está activa y puede usarse para generar PDFs.

created_at   string     

Fecha de creación de la plantilla (ISO 8601).

updated_at   string     

Fecha de última actualización (ISO 8601).

Editar una plantilla PDF existente

requires authentication

Admite actualización parcial — envíe únicamente los campos que desea modificar. Los campos no enviados conservan su valor actual.

El campo config_json se fusiona con la configuración existente (merge profundo): las claves que envíe se actualizan y las que no envíe se mantienen intactas. Esto permite cambiar, por ejemplo, solo los colores sin afectar fuentes, layout o QR.

Si envía is_default: true y la plantilla no era la default, la default anterior se desmarca automáticamente.

Importante: editar una plantilla no afecta a los PDFs ya generados con ella. Los PDFs se congelan con la configuración vigente al momento de la emisión (inmutabilidad post-emisión).

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/pdf-templates/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Profesional Actualizada\",
    \"is_default\": true,
    \"is_active\": true,
    \"paper_size\": \"letter\",
    \"config_json\": {
        \"colors\": {
            \"primary\": \"#1a365d\",
            \"secondary\": \"#4a5568\",
            \"text\": \"#2815Df\",
            \"accent\": \"#3182ce\"
        },
        \"fonts\": {
            \"family\": \"Helvetica\",
            \"size_header\": 2,
            \"size_body\": 9,
            \"size_footer\": 3,
            \"size_title\": 14
        },
        \"layout\": {
            \"show_logo\": true,
            \"logo_position\": \"left\",
            \"logo_max_height\": 60,
            \"show_commercial_name\": true,
            \"show_address\": true,
            \"show_phone\": true,
            \"show_email\": false,
            \"show_observations\": false,
            \"show_payment_methods\": true,
            \"show_references\": false,
            \"show_other_charges\": false
        },
        \"qr\": {
            \"size\": 100,
            \"position\": \"bottom-right\"
        },
        \"margins\": {
            \"top\": 22,
            \"right\": 24,
            \"bottom\": 18,
            \"left\": 8
        },
        \"style\": \"modern\",
        \"footer_text\": \"m\"
    }
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/pdf-templates/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Profesional Actualizada",
    "is_default": true,
    "is_active": true,
    "paper_size": "letter",
    "config_json": {
        "colors": {
            "primary": "#1a365d",
            "secondary": "#4a5568",
            "text": "#2815Df",
            "accent": "#3182ce"
        },
        "fonts": {
            "family": "Helvetica",
            "size_header": 2,
            "size_body": 9,
            "size_footer": 3,
            "size_title": 14
        },
        "layout": {
            "show_logo": true,
            "logo_position": "left",
            "logo_max_height": 60,
            "show_commercial_name": true,
            "show_address": true,
            "show_phone": true,
            "show_email": false,
            "show_observations": false,
            "show_payment_methods": true,
            "show_references": false,
            "show_other_charges": false
        },
        "qr": {
            "size": 100,
            "position": "bottom-right"
        },
        "margins": {
            "top": 22,
            "right": 24,
            "bottom": 18,
            "left": 8
        },
        "style": "modern",
        "footer_text": "m"
    }
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/pdf-templates/1';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Profesional Actualizada',
            'is_default' => true,
            'is_active' => true,
            'paper_size' => 'letter',
            'config_json' => [
                'colors' => [
                    'primary' => '#1a365d',
                    'secondary' => '#4a5568',
                    'text' => '#2815Df',
                    'accent' => '#3182ce',
                ],
                'fonts' => [
                    'family' => 'Helvetica',
                    'size_header' => 2,
                    'size_body' => 9,
                    'size_footer' => 3,
                    'size_title' => 14,
                ],
                'layout' => [
                    'show_logo' => true,
                    'logo_position' => 'left',
                    'logo_max_height' => 60,
                    'show_commercial_name' => true,
                    'show_address' => true,
                    'show_phone' => true,
                    'show_email' => false,
                    'show_observations' => false,
                    'show_payment_methods' => true,
                    'show_references' => false,
                    'show_other_charges' => false,
                ],
                'qr' => [
                    'size' => 100,
                    'position' => 'bottom-right',
                ],
                'margins' => [
                    'top' => 22,
                    'right' => 24,
                    'bottom' => 18,
                    'left' => 8,
                ],
                'style' => 'modern',
                'footer_text' => 'm',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Plantilla actualizada):


{
    "success": true,
    "data": {
        "id": 1,
        "name": "Profesional Actualizada",
        "is_default": true,
        "paper_size": "letter",
        "config_json": {
            "colors": {
                "primary": "#1a365d",
                "secondary": "#4a5568",
                "accent": "#3182ce"
            },
            "fonts": {
                "family": "Helvetica",
                "size_title": 14,
                "size_body": 9
            },
            "layout": {
                "show_logo": true,
                "logo_position": "left",
                "logo_max_height": 60
            },
            "qr": {
                "enabled": true,
                "size": 100,
                "position": "bottom-right"
            }
        },
        "has_logo": true,
        "is_active": true,
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-16T11:00:00-06:00"
    },
    "message": "Plantilla actualizada correctamente.",
    "errors": null
}
 

Example response (404, Plantilla no encontrada):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Example response (422, Nombre duplicado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "name": [
            "Ya existe una plantilla con este nombre para el contribuyente."
        ]
    }
}
 

Request      

PUT api/v1/public/pdf-templates/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la plantilla. Example: 1

Body Parameters

name   string  optional    

Nombre de la plantilla (3-100 caracteres, único por contribuyente). Example: Profesional Actualizada

is_default   boolean  optional    

Si será la plantilla default. Si true, desmarca la anterior. Example: true

is_active   boolean  optional    

Estado activo/inactivo de la plantilla. Example: true

paper_size   string  optional    

Tamaño del papel. Valores: letter, A4. Example: letter

config_json   object  optional    

Cambios parciales a la configuración visual (merge profundo con la actual).

colors   object  optional    
primary   string  optional    

Color hexadecimal principal. Example: #1a365d

secondary   string  optional    

Color hexadecimal secundario. Example: #4a5568

text   string  optional    

Must match the regex /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/. Example: #2815Df

accent   string  optional    

Color hexadecimal de acentos. Example: #3182ce

fonts   object  optional    
family   string  optional    

Familia tipográfica. Example: Helvetica

size_header   integer  optional    

validation.min validation.max. Example: 2

size_body   integer  optional    

Tamaño del cuerpo (6-14). Example: 9

size_footer   integer  optional    

validation.min validation.max. Example: 3

size_title   integer  optional    

Tamaño del título (10-20). Example: 14

layout   object  optional    
show_logo   boolean  optional    

Si se muestra el logo. Example: true

logo_position   string  optional    

Posición del logo (left, center, right). Example: left

logo_max_height   integer  optional    

Alto máximo del logo en pt (20-100). Example: 60

show_commercial_name   boolean  optional    

Example: true

show_address   boolean  optional    

Example: true

show_phone   boolean  optional    

Example: true

show_email   boolean  optional    

Example: false

show_observations   boolean  optional    

Example: false

show_payment_methods   boolean  optional    

Example: true

show_references   boolean  optional    

Example: false

show_other_charges   boolean  optional    

Example: false

qr   object  optional    
size   integer  optional    

Tamaño del QR en pt (70-150). Example: 100

position   string  optional    

Posición del QR (bottom-right recomendado). Example: bottom-right

margins   object  optional    
top   integer  optional    

validation.min validation.max. Example: 22

right   integer  optional    

validation.min validation.max. Example: 24

bottom   integer  optional    

validation.min validation.max. Example: 18

left   integer  optional    

validation.min validation.max. Example: 8

style   string  optional    

Example: modern

Must be one of:
  • classic
  • modern
  • minimal
  • bold
  • split
footer_text   string  optional    

validation.max. Example: m

Eliminar una plantilla PDF

requires authentication

Realiza un soft delete de la plantilla. Los PDFs ya generados con esta plantilla no se ven afectados — la configuración se aplica al momento de generar cada PDF y no se modifica retroactivamente.

Protección: no se puede eliminar una plantilla si es la única plantilla activa del contribuyente. En ese caso el sistema responde HTTP 409 y debe:

  1. Crear otra plantilla nueva, o
  2. Reactivar otra plantilla existente,

antes de eliminar la actual.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/pdf-templates/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/pdf-templates/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/pdf-templates/2';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Plantilla eliminada):


{
    "success": true,
    "data": null,
    "message": "Plantilla eliminada correctamente.",
    "errors": null
}
 

Example response (404, Plantilla no encontrada):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Example response (409, No se puede eliminar la única plantilla activa):


{
    "success": false,
    "data": null,
    "message": "No se puede eliminar la única plantilla activa del contribuyente. Cree otra plantilla o marque otra como default antes de eliminar.",
    "errors": null
}
 

Request      

DELETE api/v1/public/pdf-templates/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la plantilla. Example: 2

Generar un preview del PDF con datos reales

requires authentication

Renderiza un PDF de ejemplo usando la plantilla indicada y el comprobante más reciente emitido por el contribuyente. Permite verificar la apariencia de la plantilla antes de asignarla como default.

La respuesta es un archivo PDF (Content-Type: application/pdf) inline. Puede guardarlo localmente con -o archivo.pdf en curl o mostrarlo directamente en un iframe del navegador.

Si el contribuyente aún no ha emitido ningún comprobante, retorna HTTP 404 con un mensaje indicando que debe emitir al menos uno primero.

Nota: el comprobante usado para el preview es el más reciente (ORDER BY issued_at DESC LIMIT 1). Si quiere probar la plantilla con un comprobante específico, emita uno nuevo y use este endpoint después — o bien use el endpoint GET /vouchers/{key}/pdf con ?pdf_template_id={id}.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/pdf-templates/1/preview" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/pdf-templates/1/preview"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/pdf-templates/1/preview';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, PDF generado exitosamente):


{
    "Content-Type": "application/pdf",
    "Content-Disposition": "inline; filename=\"preview_Classic.pdf\"",
    "Cache-Control": "no-store",
    "body": "%PDF-1.4 ... (binario) ... %%EOF"
}
 

Example response (404, Sin comprobantes para preview):


{
    "success": false,
    "data": null,
    "message": "No hay comprobantes emitidos para generar un preview. Emita al menos un comprobante antes de usar preview.",
    "errors": null
}
 

Example response (404, Plantilla no encontrada):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Request      

POST api/v1/public/pdf-templates/{id}/preview

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la plantilla. Example: 1

requires authentication

Devuelve el archivo de imagen del logo como respuesta binaria inline, con el Content-Type correcto según el formato (PNG, JPG/JPEG, SVG).

Si la plantilla no tiene logo configurado, retorna HTTP 404.

El archivo se sirve con cache privado de 1 hora y cabeceras de seguridad (X-Content-Type-Options: nosniff).

requires authentication

Sube un archivo de imagen que se utilizará como logo en el encabezado del PDF generado con esta plantilla. Si la plantilla ya tenía un logo, se reemplaza automáticamente (el archivo anterior se elimina del servidor).

Este request debe enviarse como multipart/form-data (no JSON), porque incluye un archivo binario.

Restricciones del archivo:

Ejemplo con curl:

curl -X POST https://api.almendro.cr/api/v1/public/pdf-templates/1/logo \
  -H "Authorization: Bearer {su_token}" \
  -F "logo_file=@/ruta/a/logo.png"

Ejemplo con Node.js (axios + form-data):

const FormData = require('form-data')
const fs = require('fs')
const axios = require('axios')

const form = new FormData()
form.append('logo_file', fs.createReadStream('logo.png'))

await axios.post(
  'https://api.almendro.cr/api/v1/public/pdf-templates/1/logo',
  form,
  { headers: { ...form.getHeaders(), Authorization: `Bearer ${token}` } },
)

requires authentication

Elimina el archivo de logo asociado a la plantilla. Los PDFs que ya fueron generados con este logo no se ven afectados.

Si la plantilla no tiene logo configurado, retorna HTTP 409.

Configuración Email

Consultar la configuración de email del contribuyente.

requires authentication

Retorna la configuración de envío automático de comprobantes por correo electrónico. Si el contribuyente nunca ha configurado estos valores, se retornan los valores por defecto que cumplen con la obligación de entrega del art. 18 del Reglamento: envío automático activado, XML firmado y PDF adjuntos.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/email-settings" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/email-settings"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/email-settings';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Valores por defecto (nunca configurado)):


{
    "success": true,
    "data": {
        "auto_send": true,
        "send_on_accepted_only": true,
        "attach_xml": true,
        "attach_pdf": true,
        "reply_to": null,
        "bcc": [],
        "custom_subject": null,
        "custom_message": null
    },
    "message": "Configuración de email del contribuyente.",
    "errors": null
}
 

Example response (200, Configuración personalizada):


{
    "success": true,
    "data": {
        "auto_send": true,
        "send_on_accepted_only": true,
        "attach_xml": true,
        "attach_pdf": true,
        "reply_to": "facturas@miempresa.cr",
        "bcc": [
            "contabilidad@miempresa.cr"
        ],
        "custom_subject": "{emisor} — {tipo} {consecutivo}",
        "custom_message": "Adjunto su comprobante electrónico."
    },
    "message": "Configuración de email del contribuyente.",
    "errors": null
}
 

Request      

GET api/v1/public/email-settings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Response

Response Fields

data   object     
auto_send   boolean     

true si el sistema envía automáticamente el comprobante por correo al receptor al emitir. Default true por obligación del art. 18 del Reglamento.

send_on_accepted_only   boolean     

true si el email se envía solo después de que Hacienda acepte el comprobante (práctica estándar CR). false para enviar inmediatamente tras la emisión.

attach_xml   boolean     

true si se adjunta el XML firmado del comprobante al correo. Art. 18 exige entregar "el comprobante electrónico" (= XML).

attach_pdf   boolean     

true si se adjunta la representación gráfica PDF al correo. Resolución art. 5 exige "representación gráfica".

reply_to   string     

Correo al que llegarán las respuestas del receptor. null si no se configuró (usa el default del sistema).

bcc   string[]     

Lista de correos en copia oculta (máximo 4). Útil para enviar copia al contador o sistema interno. Array vacío si no se configuró.

custom_subject   string     

Asunto personalizado del correo. Soporta placeholders: {tipo}, {consecutivo}, {clave}, {receptor}, {total}, {moneda}, {emisor}. null usa el asunto por defecto del sistema.

custom_message   string     

Mensaje personalizado que aparece antes de los datos del comprobante en el cuerpo del correo. null usa el mensaje por defecto.

Actualizar la configuración de email del contribuyente.

requires authentication

Soporta actualización parcial: envíe únicamente los campos que desea modificar. Los campos omitidos conservan su valor actual (o el valor por defecto si nunca fueron configurados).

Advertencia normativa: si desactiva attach_xml o attach_pdf, el contribuyente asume la responsabilidad de entregar los documentos al receptor por otro medio (descarga desde API, portal web, impresión), conforme al art. 18 del Reglamento de Comprobantes Electrónicos.

Placeholders disponibles para custom_subject: {tipo}, {consecutivo}, {clave}, {receptor}, {total}, {moneda}, {emisor}

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/email-settings" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"auto_send\": true,
    \"send_on_accepted_only\": true,
    \"attach_xml\": true,
    \"attach_pdf\": true,
    \"reply_to\": \"facturas@miempresa.cr\",
    \"bcc\": [
        \"b\"
    ],
    \"custom_subject\": \"{emisor} — {tipo} {consecutivo}\",
    \"custom_message\": \"Adjunto encontrará su comprobante electrónico.\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/email-settings"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "auto_send": true,
    "send_on_accepted_only": true,
    "attach_xml": true,
    "attach_pdf": true,
    "reply_to": "facturas@miempresa.cr",
    "bcc": [
        "b"
    ],
    "custom_subject": "{emisor} — {tipo} {consecutivo}",
    "custom_message": "Adjunto encontrará su comprobante electrónico."
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/email-settings';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'auto_send' => true,
            'send_on_accepted_only' => true,
            'attach_xml' => true,
            'attach_pdf' => true,
            'reply_to' => 'facturas@miempresa.cr',
            'bcc' => [
                'b',
            ],
            'custom_subject' => '{emisor} — {tipo} {consecutivo}',
            'custom_message' => 'Adjunto encontrará su comprobante electrónico.',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Configuración actualizada):


{
    "success": true,
    "data": {
        "auto_send": true,
        "send_on_accepted_only": true,
        "attach_xml": true,
        "attach_pdf": true,
        "reply_to": "facturas@miempresa.cr",
        "bcc": [
            "contabilidad@miempresa.cr"
        ],
        "custom_subject": "{emisor} — {tipo} {consecutivo}",
        "custom_message": "Adjunto su comprobante electrónico."
    },
    "message": "Configuración de email actualizada correctamente.",
    "errors": null
}
 

Example response (422, Error de validación):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "reply_to": [
            "El campo reply_to debe ser un correo electrónico válido."
        ],
        "bcc.0": [
            "El campo bcc.0 debe ser un correo electrónico válido."
        ]
    }
}
 

Request      

PUT api/v1/public/email-settings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

auto_send   boolean  optional    

Envío automático al aceptar comprobante. Default true (art. 18 Reglamento obliga entrega). Example: true

send_on_accepted_only   boolean  optional    

Enviar solo cuando Hacienda acepta (true) o también al emitir (false). Default true. Example: true

attach_xml   boolean  optional    

Adjuntar XML firmado (XAdES-EPES). Default true (art. 18: "comprobante electrónico"). Example: true

attach_pdf   boolean  optional    

Adjuntar PDF con QR. Default true (art. 18 + Resolución art. 5: "representación gráfica"). Example: true

reply_to   string  optional    

Dirección reply-to personalizada. Las respuestas del receptor llegarán a este correo. validation.email validation.max. Example: facturas@miempresa.cr

bcc   string[]  optional    

validation.email validation.max.

custom_subject   string  optional    

Asunto personalizado. Placeholders: {tipo}, {consecutivo}, {clave}, {receptor}, {total}, {moneda}, {emisor}. validation.max. Example: {emisor} — {tipo} {consecutivo}

custom_message   string  optional    

Mensaje personalizado en el cuerpo del email. Se muestra antes de los datos del comprobante. validation.max. Example: Adjunto encontrará su comprobante electrónico.

Clientes

Listar clientes del contribuyente

requires authentication

Devuelve los clientes (receptores reutilizables) registrados por el contribuyente, con soporte para búsqueda por texto, filtro por tipo de identificación y filtro por estado.

Por defecto, el listado incluye solo clientes activos ordenados alfabéticamente por nombre. Para ver los inactivos use is_active=false, y para ver todos use is_active=all.

La búsqueda por texto (q) es case-insensitive y busca en: nombre legal, nombre comercial y número de cédula.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/clients?q=Empresa&id_type=02&is_active=true&per_page=15&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/clients"
);

const params = {
    "q": "Empresa",
    "id_type": "02",
    "is_active": "true",
    "per_page": "15",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/clients';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'q' => 'Empresa',
            'id_type' => '02',
            'is_active' => 'true',
            'per_page' => '15',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado paginado):


{
    "success": true,
    "data": [
        {
            "id": "019d867d-0001-7288-8ece-fd64da756001",
            "id_type": "02",
            "id_type_label": "Cédula Jurídica",
            "id_number": "3101123456",
            "name": "EMPRESA EJEMPLO S.A.",
            "commercial_name": "Ejemplo Shop",
            "location": {
                "province": 1,
                "canton": "01",
                "district": "01"
            },
            "neighborhood": "San Pedro",
            "address": "200 metros norte del parque central",
            "phone": {
                "country_code": 506,
                "number": "22001234"
            },
            "emails": [
                "facturacion@ejemplo.cr"
            ],
            "default_activity_code": "6121.0",
            "notes": "Cliente preferencial",
            "is_active": true,
            "has_location": true,
            "has_email": true,
            "created_at": "2026-04-01T10:00:00-06:00",
            "updated_at": "2026-04-10T14:30:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 4,
        "per_page": 15,
        "total": 50,
        "from": 1,
        "to": 15
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": "..."
    }
}
 

Example response (200, Sin resultados):


{
    "success": true,
    "data": [],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 0,
        "from": null,
        "to": null
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/clients

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

q   string  optional    

Búsqueda por nombre, nombre comercial o número de cédula (mínimo 2 caracteres). Example: Empresa

id_type   string  optional    

Filtrar por tipo de identificación. Valores: 01, 02, 03, 04, 05, 06. Example: 02

is_active   string  optional    

Filtrar por estado. true = solo activos (default), false = solo inactivos, all = todos. Example: true

per_page   integer  optional    

Resultados por página (1-100). Default: 15. Example: 15

page   integer  optional    

Número de página. Example: 1

Crear un nuevo cliente

requires authentication

Registra un cliente (receptor reutilizable) en el catálogo del contribuyente. Los datos corresponden a los campos del receptor definidos en la normativa de comprobantes electrónicos.

Reglas de validación:

Los campos de ubicación (province, canton, district) son todos opcionales individualmente, pero si envía uno debería enviar los tres para que la ubicación sea coherente.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/clients" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"id_type\": \"02\",
    \"id_number\": \"3101123456\",
    \"name\": \"EMPRESA EJEMPLO S.A.\",
    \"commercial_name\": \"Ejemplo Shop\",
    \"province\": 1,
    \"canton\": 1,
    \"district\": 1,
    \"neighborhood\": \"San Pedro\",
    \"address\": \"200 metros norte del parque central\",
    \"phone_country_code\": 506,
    \"phone\": \"22001234\",
    \"emails\": [
        \"facturacion@ejemplo.cr\"
    ],
    \"default_activity_code\": \"6121.0\",
    \"notes\": \"Cliente preferencial\",
    \"is_active\": true
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/clients"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "id_type": "02",
    "id_number": "3101123456",
    "name": "EMPRESA EJEMPLO S.A.",
    "commercial_name": "Ejemplo Shop",
    "province": 1,
    "canton": 1,
    "district": 1,
    "neighborhood": "San Pedro",
    "address": "200 metros norte del parque central",
    "phone_country_code": 506,
    "phone": "22001234",
    "emails": [
        "facturacion@ejemplo.cr"
    ],
    "default_activity_code": "6121.0",
    "notes": "Cliente preferencial",
    "is_active": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/clients';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'id_type' => '02',
            'id_number' => '3101123456',
            'name' => 'EMPRESA EJEMPLO S.A.',
            'commercial_name' => 'Ejemplo Shop',
            'province' => 1,
            'canton' => 1,
            'district' => 1,
            'neighborhood' => 'San Pedro',
            'address' => '200 metros norte del parque central',
            'phone_country_code' => 506,
            'phone' => '22001234',
            'emails' => [
                'facturacion@ejemplo.cr',
            ],
            'default_activity_code' => '6121.0',
            'notes' => 'Cliente preferencial',
            'is_active' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Cliente creado):


{
    "success": true,
    "data": {
        "id": "019d867d-0002-7288-8ece-fd64da756002",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "id_number": "3101123456",
        "name": "EMPRESA EJEMPLO S.A.",
        "commercial_name": "Ejemplo Shop",
        "location": {
            "province": 1,
            "canton": "01",
            "district": "01"
        },
        "neighborhood": "San Pedro",
        "address": "200 metros norte del parque central",
        "phone": {
            "country_code": 506,
            "number": "22001234"
        },
        "emails": [
            "facturacion@ejemplo.cr"
        ],
        "default_activity_code": "6121.0",
        "notes": "Cliente preferencial",
        "is_active": true,
        "has_location": true,
        "has_email": true,
        "created_at": "2026-04-16T10:00:00-06:00",
        "updated_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Cliente creado correctamente.",
    "errors": null
}
 

Example response (422, Identificación duplicada):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "id_number": [
            "Ya existe un cliente con este tipo y número de identificación."
        ]
    }
}
 

Example response (422, Formato de actividad económica inválido):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "default_activity_code": [
            "Debe tener el formato XXXX.X (ejemplo: 6121.0)."
        ]
    }
}
 

Example response (422, Tipo de identificación inválido):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "id_type": [
            "El tipo seleccionado no es válido."
        ]
    }
}
 

Example response (422, Límite del plan alcanzado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "plan": [
            "Ha alcanzado el límite de clientes de su plan. Actualice a un plan superior."
        ]
    }
}
 

Request      

POST api/v1/public/clients

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

id_type   string     

Tipo de identificación. Valores permitidos: 01 (Física), 02 (Jurídica), 03 (DIMEX), 04 (NITE), 05 (Extranjero), 06 (No Contribuyente). Example: 02

id_number   string     

Número de identificación (9-20 dígitos, solo números). Example: 3101123456

name   string     

Nombre legal o razón social (3-100 caracteres). Example: EMPRESA EJEMPLO S.A.

commercial_name   string  optional    

Nombre comercial (3-80 caracteres). Example: Ejemplo Shop

province   integer  optional    

Código de provincia (1-7). Example: 1

canton   integer  optional    

Código de cantón (1-99). Example: 1

district   integer  optional    

Código de distrito (1-99). Example: 1

neighborhood   string  optional    

Barrio (5-50 caracteres). Example: San Pedro

address   string  optional    

Señas exactas (5-300 caracteres). Example: 200 metros norte del parque central

phone_country_code   integer  optional    

Código de país del teléfono. Default: 506. Example: 506

phone   string  optional    

Número de teléfono (4-20 dígitos, solo números). Example: 22001234

emails   string[]  optional    

Correos del cliente (máximo 4).

default_activity_code   string  optional    

Código CIIU por defecto en formato Hacienda XXXX.X. Usado como CodigoActividadReceptor al emitir FEC (tipo 08). Example: 6121.0

notes   string  optional    

Notas internas (máximo 1000 caracteres). No aparecen en el comprobante. Example: Cliente preferencial

is_active   boolean  optional    

Si el cliente está activo (puede usarse al emitir). Default: true. Example: true

Consultar un cliente por UUID

requires authentication

Devuelve el detalle completo del cliente indicado. Solo se retornan clientes que pertenecen a su contribuyente. Los clientes eliminados (soft delete) retornan 404 — no se pueden consultar.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Cliente encontrado):


{
    "success": true,
    "data": {
        "id": "019d867d-0001-7288-8ece-fd64da756001",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "id_number": "3101123456",
        "name": "EMPRESA EJEMPLO S.A.",
        "commercial_name": "Ejemplo Shop",
        "location": {
            "province": 1,
            "canton": "01",
            "district": "01"
        },
        "neighborhood": "San Pedro",
        "address": "200 metros norte del parque central",
        "phone": {
            "country_code": 506,
            "number": "22001234"
        },
        "emails": [
            "facturacion@ejemplo.cr"
        ],
        "default_activity_code": "6121.0",
        "notes": "Cliente preferencial",
        "is_active": true,
        "has_location": true,
        "has_email": true,
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-10T14:30:00-06:00"
    },
    "message": "",
    "errors": null
}
 

Example response (404, No encontrado):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Request      

GET api/v1/public/clients/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente. Example: 019d867d-0001-7288-8ece-fd64da756001

Response

Response Fields

data   object     
id   string     

UUID del cliente. Úselo como client_id en POST /vouchers para resolver automáticamente los datos del receptor.

id_type   string     

Tipo de identificación del receptor: 01=Física, 02=Jurídica, 03=DIMEX, 04=NITE, 05=Extranjero, 06=No Contribuyente.

id_type_label   string     

Nombre legible del tipo de identificación en español.

id_number   string     

Número de identificación (9-20 dígitos). Mapea a IdentificacionType/Numero del XSD.

name   string     

Nombre legal o razón social (3-100 chars). Mapea a ReceptorType/Nombre del XSD.

commercial_name   string     

Nombre comercial del receptor. Mapea a NombreComercial del XSD. Puede ser null.

location   object     

Ubicación del receptor (UbicacionType del XSD). null si no tiene ubicación costarricense configurada.

province   integer     

Código de provincia (1-7). Mapea a Provincia del XSD.

canton   string     

Código de cantón (2 dígitos). Mapea a Canton del XSD.

district   string     

Código de distrito (2 dígitos). Mapea a Distrito del XSD.

neighborhood   string     

Barrio (5-50 chars). Campo opcional dentro de la ubicación.

address   string     

Señas exactas. Mapea a OtrasSenas (max 250) si tiene ubicación CR, o OtrasSenasExtranjero (max 300) si no.

phone   object     

Teléfono del receptor (TelefonoType del XSD). null si no tiene teléfono configurado.

country_code   integer     

Código de país del teléfono (1-3 dígitos). Default: 506 (Costa Rica).

number   string     

Número de teléfono (4-20 dígitos).

emails   string[]     

Correos electrónicos del receptor (máximo 4). El primero mapea a CorreoElectronico del XSD. Los demás se usan como destinatarios adicionales del email transaccional.

default_activity_code   string     

Código CIIU en formato Hacienda XXXX.X. Usado como CodigoActividadReceptor al emitir FEC (tipo 08). null si no aplica.

notes   string     

Notas internas libres del integrador. No aparecen en el comprobante electrónico.

is_active   boolean     

true si el cliente está activo y puede usarse al emitir. false si fue desactivado manualmente.

has_location   boolean     

true si tiene provincia, cantón y distrito configurados. Útil para saber si el receptor tendrá nodo Ubicacion en el XML.

has_email   boolean     

true si tiene al menos un correo. Útil para saber si se enviará email transaccional automático al emitir.

created_at   string     

Fecha de creación del registro (ISO 8601).

updated_at   string     

Fecha de última actualización del registro (ISO 8601).

Editar un cliente existente

requires authentication

Admite actualización parcial — envíe únicamente los campos que desea modificar. Los campos no enviados conservan su valor actual.

Importante: editar un cliente no afecta a los comprobantes ya emitidos con él. Los datos del receptor se copian al comprobante en el momento de la emisión y no cambian retroactivamente (inmutabilidad post-emisión por normativa fiscal).

Si cambia el id_type o id_number, el sistema verifica que la nueva combinación no esté en uso por otro cliente del mismo contribuyente. Si ya existe, responde HTTP 422.

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"id_type\": \"02\",
    \"id_number\": \"3101123456\",
    \"name\": \"Nombre Actualizado S.A.\",
    \"commercial_name\": \"Nuevo Nombre Comercial\",
    \"province\": 1,
    \"canton\": 1,
    \"district\": 1,
    \"neighborhood\": \"Escalante\",
    \"address\": \"Frente al parque\",
    \"phone_country_code\": 506,
    \"phone\": \"22009999\",
    \"emails\": [
        \"facturacion@ejemplo.cr\",
        \"contabilidad@ejemplo.cr\"
    ],
    \"default_activity_code\": \"6121.0\",
    \"notes\": \"Cliente preferencial — actualizado\",
    \"is_active\": true
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "id_type": "02",
    "id_number": "3101123456",
    "name": "Nombre Actualizado S.A.",
    "commercial_name": "Nuevo Nombre Comercial",
    "province": 1,
    "canton": 1,
    "district": 1,
    "neighborhood": "Escalante",
    "address": "Frente al parque",
    "phone_country_code": 506,
    "phone": "22009999",
    "emails": [
        "facturacion@ejemplo.cr",
        "contabilidad@ejemplo.cr"
    ],
    "default_activity_code": "6121.0",
    "notes": "Cliente preferencial — actualizado",
    "is_active": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'id_type' => '02',
            'id_number' => '3101123456',
            'name' => 'Nombre Actualizado S.A.',
            'commercial_name' => 'Nuevo Nombre Comercial',
            'province' => 1,
            'canton' => 1,
            'district' => 1,
            'neighborhood' => 'Escalante',
            'address' => 'Frente al parque',
            'phone_country_code' => 506,
            'phone' => '22009999',
            'emails' => [
                'facturacion@ejemplo.cr',
                'contabilidad@ejemplo.cr',
            ],
            'default_activity_code' => '6121.0',
            'notes' => 'Cliente preferencial — actualizado',
            'is_active' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Cliente actualizado):


{
    "success": true,
    "data": {
        "id": "019d867d-0001-7288-8ece-fd64da756001",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "id_number": "3101123456",
        "name": "Nombre Actualizado S.A.",
        "commercial_name": "Nuevo Nombre Comercial",
        "location": {
            "province": 1,
            "canton": "01",
            "district": "01"
        },
        "neighborhood": "San Pedro",
        "address": "200 metros norte del parque central",
        "phone": {
            "country_code": 506,
            "number": "22001234"
        },
        "emails": [
            "facturacion@ejemplo.cr",
            "contabilidad@ejemplo.cr"
        ],
        "default_activity_code": "6121.0",
        "notes": "Cliente preferencial — actualizado",
        "is_active": true,
        "has_location": true,
        "has_email": true,
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-16T11:00:00-06:00"
    },
    "message": "Cliente actualizado correctamente.",
    "errors": null
}
 

Example response (404, Cliente no encontrado):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Example response (422, Identificación duplicada):


{
    "success": false,
    "data": null,
    "message": "Ya existe un cliente con este tipo y número de identificación.",
    "errors": {
        "id_number": [
            "Duplicado para este contribuyente."
        ]
    }
}
 

Example response (422, Formato de actividad económica inválido):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "default_activity_code": [
            "Debe tener el formato XXXX.X (ejemplo: 6121.0)."
        ]
    }
}
 

Request      

PUT api/v1/public/clients/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente. Example: 019d867d-0001-7288-8ece-fd64da756001

Body Parameters

id_type   string  optional    

Tipo de identificación. Example: 02

id_number   string  optional    

Número de identificación (9-20 dígitos). Example: 3101123456

name   string  optional    

Nombre legal o razón social (3-100 chars). Example: Nombre Actualizado S.A.

commercial_name   string  optional    

Nombre comercial (3-80 chars). Example: Nuevo Nombre Comercial

province   integer  optional    

Código de provincia (1-7). Example: 1

canton   integer  optional    

Código de cantón (1-99). Example: 1

district   integer  optional    

Código de distrito (1-99). Example: 1

neighborhood   string  optional    

Barrio (5-50 chars). Example: Escalante

address   string  optional    

Señas (5-300 chars). Example: Frente al parque

phone_country_code   integer  optional    

Código de país del teléfono. Example: 506

phone   string  optional    

Número de teléfono (4-20 dígitos). Example: 22009999

emails   string[]  optional    

Correos (máx 4).

default_activity_code   string  optional    

Código CIIU formato XXXX.X. Example: 6121.0

notes   string  optional    

Notas internas (máx 1000 chars). Example: Cliente preferencial — actualizado

is_active   boolean  optional    

Estado del cliente. Example: true

Eliminar un cliente

requires authentication

Realiza un soft delete del cliente. El registro queda marcado como eliminado pero no se borra físicamente — por eso puede:

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/clients/019d867d-0001-7288-8ece-fd64da756001';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Cliente eliminado):


{
    "success": true,
    "data": null,
    "message": "Cliente eliminado correctamente.",
    "errors": null
}
 

Example response (404, No encontrado):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Request      

DELETE api/v1/public/clients/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente. Example: 019d867d-0001-7288-8ece-fd64da756001

Items / Productos

Listar productos y servicios del contribuyente

requires authentication

Devuelve los items (productos/servicios reutilizables) registrados por el contribuyente, con soporte para búsqueda por texto, filtro por código CABYS exacto y filtro por estado.

Por defecto, el listado incluye solo items activos ordenados alfabéticamente por descripción. Para ver los inactivos use is_active=false.

La búsqueda por texto (q) es case-insensitive y busca en: descripción, código interno (SKU) y código CABYS.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/items?q=consultor%C3%ADa&cabys_code=4233201000000&is_active=true&per_page=15&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/items"
);

const params = {
    "q": "consultoría",
    "cabys_code": "4233201000000",
    "is_active": "true",
    "per_page": "15",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/items';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'q' => 'consultoría',
            'cabys_code' => '4233201000000',
            'is_active' => 'true',
            'per_page' => '15',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado paginado):


{
    "success": true,
    "data": [
        {
            "id": "019d867d-0010-7288-8ece-fd64da756010",
            "cabys_code": "4233201000000",
            "internal_code": "SKU-001",
            "description": "Servicio de consultoría profesional",
            "unit_of_measure": "Sp",
            "unit_of_measure_label": "Servicios Profesionales",
            "commercial_unit": null,
            "unit_price": "50000.00000",
            "tax": {
                "code": "01",
                "rate_code": "08",
                "rate": "13.00",
                "label": "IVA 13%"
            },
            "is_mercancia": false,
            "is_servicio": true,
            "is_active": true,
            "created_at": "2026-04-01T10:00:00-06:00",
            "updated_at": "2026-04-10T14:30:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 2,
        "per_page": 15,
        "total": 30,
        "from": 1,
        "to": 15
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": "..."
    }
}
 

Example response (200, Sin resultados):


{
    "success": true,
    "data": [],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 0,
        "from": null,
        "to": null
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

q   string  optional    

Búsqueda por descripción, código interno o código CABYS (mínimo 2 caracteres). Example: consultoría

cabys_code   string  optional    

Filtrar por código CABYS exacto (13 dígitos). Example: 4233201000000

is_active   string  optional    

Filtrar por estado. true = solo activos (default), false = solo inactivos. Example: true

per_page   integer  optional    

Resultados por página (1-100). Default: 15. Example: 15

page   integer  optional    

Número de página. Example: 1

Crear un nuevo producto o servicio

requires authentication

Registra un item reutilizable en el catálogo del contribuyente. Los datos corresponden a los campos de una línea de detalle del comprobante.

Reglas de validación:

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"cabys_code\": \"4233201000000\",
    \"internal_code\": \"SKU-001\",
    \"description\": \"Servicio de consultoría profesional\",
    \"unit_of_measure\": \"Sp\",
    \"commercial_unit\": \"caja x 12\",
    \"unit_price\": 50000,
    \"tax_code\": \"01\",
    \"tax_rate_code\": \"08\",
    \"tax_rate\": 13,
    \"is_active\": true
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "cabys_code": "4233201000000",
    "internal_code": "SKU-001",
    "description": "Servicio de consultoría profesional",
    "unit_of_measure": "Sp",
    "commercial_unit": "caja x 12",
    "unit_price": 50000,
    "tax_code": "01",
    "tax_rate_code": "08",
    "tax_rate": 13,
    "is_active": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/items';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'cabys_code' => '4233201000000',
            'internal_code' => 'SKU-001',
            'description' => 'Servicio de consultoría profesional',
            'unit_of_measure' => 'Sp',
            'commercial_unit' => 'caja x 12',
            'unit_price' => 50000.0,
            'tax_code' => '01',
            'tax_rate_code' => '08',
            'tax_rate' => 13.0,
            'is_active' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Item creado):


{
    "success": true,
    "data": {
        "id": "019d867d-0011-7288-8ece-fd64da756011",
        "cabys_code": "4233201000000",
        "internal_code": "SKU-001",
        "description": "Servicio de consultoría profesional",
        "unit_of_measure": "Sp",
        "unit_of_measure_label": "Servicios Profesionales",
        "commercial_unit": null,
        "unit_price": "50000.00000",
        "tax": {
            "code": "01",
            "rate_code": "08",
            "rate": "13.00",
            "label": "IVA 13%"
        },
        "is_mercancia": false,
        "is_servicio": true,
        "is_active": true,
        "created_at": "2026-04-16T10:00:00-06:00",
        "updated_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Item creado correctamente.",
    "errors": null
}
 

Example response (422, Código interno duplicado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "internal_code": [
            "Ya existe un item con este código interno."
        ]
    }
}
 

Example response (422, Código CABYS no existe en el catálogo):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "cabys_code": [
            "El código CABYS no existe en el catálogo vigente."
        ]
    }
}
 

Example response (422, Falta tax_rate_code para IVA):


{
    "success": false,
    "data": null,
    "message": "tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07).",
    "errors": {
        "tax_rate_code": [
            "tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07)."
        ]
    }
}
 

Example response (422, Unit_of_measure 'Otros' sin commercial_unit):


{
    "success": false,
    "data": null,
    "message": "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'.",
    "errors": {
        "commercial_unit": [
            "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'."
        ]
    }
}
 

Example response (422, Límite del plan alcanzado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "plan": [
            "Ha alcanzado el límite de items de su plan. Actualice a un plan superior."
        ]
    }
}
 

Request      

POST api/v1/public/items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

cabys_code   string     

Código CABYS (13 dígitos exactos). Debe existir en el catálogo vigente. Example: 4233201000000

internal_code   string  optional    

Código interno / SKU del producto (máximo 20 caracteres). Único por contribuyente. Example: SKU-001

description   string     

Descripción del producto o servicio (3-200 caracteres). Example: Servicio de consultoría profesional

unit_of_measure   string     

Unidad de medida. Valores frecuentes: Sp (Servicios Profesionales), Unid (Unidades), kg, l, m, , h, Al, Otros. Si usa Otros, debe enviar commercial_unit. Example: Sp

commercial_unit   string  optional    

Unidad de medida comercial, obligatoria solo cuando unit_of_measure es Otros (máximo 20 caracteres). Example: caja x 12

unit_price   number     

Precio unitario (mayor a 0, hasta 5 decimales). Example: 50000

tax_code   string  optional    

Código del impuesto. Valores: 01 (IVA), 02 (ISC), 03 (Único combustibles), 04 (Específico bebidas alcohólicas), 05 (Específico bebidas envasadas), 06 (Específico tabaco), 07 (IVA cálculo especial), 08 (IVA bienes usados), 12 (Específico cemento), 99 (Otros). Example: 01

tax_rate_code   string  optional    

Código de tarifa de IVA (obligatorio si tax_code es 01 o 07). Valores: 01 (0% exento), 02 (1%), 03 (2%), 04 (4%), 05 (0.5%), 06 (4%), 07 (8%), 08 (13%), 09 (14%), 10 (0% exenta), 11 (0% no sujeta). Example: 08

tax_rate   number  optional    

Tarifa efectiva en porcentaje (0-100, hasta 2 decimales). Example: 13

is_active   boolean  optional    

Si el item está activo. Default: true. Example: true

Consultar un item por UUID

requires authentication

Devuelve el detalle completo del item indicado. Solo se retornan items que pertenecen a su contribuyente. Los items eliminados (soft delete) retornan 404 — no se pueden consultar.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Item encontrado):


{
    "success": true,
    "data": {
        "id": "019d867d-0010-7288-8ece-fd64da756010",
        "cabys_code": "4233201000000",
        "internal_code": "SKU-001",
        "description": "Servicio de consultoría profesional",
        "unit_of_measure": "Sp",
        "unit_of_measure_label": "Servicios Profesionales",
        "commercial_unit": null,
        "unit_price": "50000.00000",
        "tax": {
            "code": "01",
            "rate_code": "08",
            "rate": "13.00",
            "label": "IVA 13%"
        },
        "is_mercancia": false,
        "is_servicio": true,
        "is_active": true,
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-10T14:30:00-06:00"
    },
    "message": "",
    "errors": null
}
 

Example response (404, No encontrado):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Request      

GET api/v1/public/items/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del item. Example: 019d867d-0010-7288-8ece-fd64da756010

Response

Response Fields

data   object     
id   string     

UUID del item. Úselo como item_id en line_items[] de POST /vouchers para resolver automáticamente los datos de la línea.

cabys_code   string     

Código CABYS de 13 dígitos exactos. Mapea a CodigoCABYS del XSD en cada LineaDetalle.

internal_code   string     

Código interno / SKU del producto (máx 20 chars). Si está presente, se usa como CodigoComercial tipo 04 en el XML. null si no se configuró.

description   string     

Descripción del producto o servicio (3-200 chars). Mapea al campo Detalle de la LineaDetalle del XSD.

unit_of_measure   string     

Código de unidad de medida de Hacienda: Sp, Unid, kg, l, m, , h, Al, Otros, etc. Mapea a UnidadMedida del XSD.

unit_of_measure_label   string     

Nombre legible de la unidad de medida en español.

commercial_unit   string     

Unidad de medida comercial personalizada. Solo presente cuando unit_of_measure es Otros (ej. "caja x 12", "paquete"). null en caso contrario.

unit_price   string     

Precio unitario con 5 decimales (Decimal 18,5). Mapea a PrecioUnitario del XSD.

tax   object     

Configuración simplificada de impuesto del item. null si el item no tiene impuesto configurado (exento sin código).

code   string     

Código del impuesto: 01=IVA, 02=ISC, 07=IVA cálculo especial, etc. Mapea a CodigoImpuesto del XSD.

rate_code   string     

Código de tarifa IVA: 01=0%, 02=1%, 03=2%, 04=4%, 08=13%, 10=Exenta. null si code no es 01 ni 07. Mapea a CodigoTarifaIVA del XSD.

rate   string     

Tarifa efectiva en porcentaje con 2 decimales (ej. 13.00). Mapea a Tarifa del XSD.

label   string     

Etiqueta legible del impuesto (ej. "IVA 13%", "IVA Exento", "ISC").

is_mercancia   boolean     

true si el código CABYS corresponde a mercancía. Afecta los totales merc_gravadas/merc_exentas del ResumenFactura.

is_servicio   boolean     

true si el código CABYS corresponde a servicio. Afecta los totales serv_gravados/serv_exentos del ResumenFactura.

is_active   boolean     

true si el item está activo y puede usarse al emitir. false si fue desactivado manualmente.

created_at   string     

Fecha de creación del registro (ISO 8601).

updated_at   string     

Fecha de última actualización del registro (ISO 8601).

Editar un item existente

requires authentication

Admite actualización parcial — envíe únicamente los campos que desea modificar. Los campos no enviados conservan su valor actual.

Importante: editar un item no afecta a los comprobantes ya emitidos con él. Los datos de la línea se copian al comprobante en el momento de la emisión y no cambian retroactivamente (inmutabilidad post-emisión por normativa fiscal).

Validaciones cruzadas que se aplican al editar:

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"cabys_code\": \"4233201000000\",
    \"internal_code\": \"SKU-001-V2\",
    \"description\": \"Servicio de consultoría profesional actualizado\",
    \"unit_of_measure\": \"Sp\",
    \"commercial_unit\": \"caja x 12\",
    \"unit_price\": 55000,
    \"tax_code\": \"01\",
    \"tax_rate_code\": \"08\",
    \"tax_rate\": 13,
    \"is_active\": true
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "cabys_code": "4233201000000",
    "internal_code": "SKU-001-V2",
    "description": "Servicio de consultoría profesional actualizado",
    "unit_of_measure": "Sp",
    "commercial_unit": "caja x 12",
    "unit_price": 55000,
    "tax_code": "01",
    "tax_rate_code": "08",
    "tax_rate": 13,
    "is_active": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'cabys_code' => '4233201000000',
            'internal_code' => 'SKU-001-V2',
            'description' => 'Servicio de consultoría profesional actualizado',
            'unit_of_measure' => 'Sp',
            'commercial_unit' => 'caja x 12',
            'unit_price' => 55000.0,
            'tax_code' => '01',
            'tax_rate_code' => '08',
            'tax_rate' => 13.0,
            'is_active' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Item actualizado):


{
    "success": true,
    "data": {
        "id": "019d867d-0010-7288-8ece-fd64da756010",
        "cabys_code": "4233201000000",
        "internal_code": "SKU-001-V2",
        "description": "Servicio de consultoría profesional actualizado",
        "unit_of_measure": "Sp",
        "unit_of_measure_label": "Servicios Profesionales",
        "commercial_unit": null,
        "unit_price": "55000.00000",
        "tax": {
            "code": "01",
            "rate_code": "08",
            "rate": "13.00",
            "label": "IVA 13%"
        },
        "is_mercancia": false,
        "is_servicio": true,
        "is_active": true,
        "created_at": "2026-04-01T10:00:00-06:00",
        "updated_at": "2026-04-16T11:00:00-06:00"
    },
    "message": "Item actualizado correctamente.",
    "errors": null
}
 

Example response (404, Item no encontrado):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Example response (422, Código interno duplicado):


{
    "success": false,
    "data": null,
    "message": "Ya existe un item con el código interno [SKU-001-V2].",
    "errors": {
        "internal_code": [
            "Duplicado para este contribuyente."
        ]
    }
}
 

Example response (422, Código CABYS no existe):


{
    "success": false,
    "data": null,
    "message": "El código CABYS [9999999999999] no existe en el catálogo vigente.",
    "errors": {
        "cabys_code": [
            "Código CABYS no encontrado."
        ]
    }
}
 

Example response (422, Falta tax_rate_code para IVA):


{
    "success": false,
    "data": null,
    "message": "tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07).",
    "errors": {
        "tax_rate_code": [
            "tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07)."
        ]
    }
}
 

Example response (422, Falta tax_rate cuando hay tax_code):


{
    "success": false,
    "data": null,
    "message": "tax_rate es obligatorio cuando se especifica tax_code.",
    "errors": {
        "tax_rate": [
            "Envíe tax_rate junto con tax_code."
        ]
    }
}
 

Example response (422, unit_of_measure 'Otros' sin commercial_unit):


{
    "success": false,
    "data": null,
    "message": "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'.",
    "errors": {
        "commercial_unit": [
            "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'."
        ]
    }
}
 

Request      

PUT api/v1/public/items/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del item. Example: 019d867d-0010-7288-8ece-fd64da756010

Body Parameters

cabys_code   string  optional    

Código CABYS (13 dígitos). Debe existir en el catálogo vigente. Example: 4233201000000

internal_code   string  optional    

Código interno / SKU (máx 20 chars). Único por contribuyente. Example: SKU-001-V2

description   string  optional    

Descripción (3-200 chars). Example: Servicio de consultoría profesional actualizado

unit_of_measure   string  optional    

Unidad de medida (ver lista en Guía). Example: Sp

commercial_unit   string  optional    

Unidad comercial (obligatoria si unit_of_measure es Otros). Example: caja x 12

unit_price   number  optional    

Precio unitario (> 0, hasta 5 decimales). Example: 55000

tax_code   string  optional    

Código de impuesto (ver lista en Guía). Example: 01

tax_rate_code   string  optional    

Código de tarifa IVA (obligatorio si tax_code es 01 o 07). Example: 08

tax_rate   number  optional    

Tarifa efectiva en % (0-100, 2 decimales). Example: 13

is_active   boolean  optional    

Estado del item. Example: true

Eliminar un item

requires authentication

Realiza un soft delete del item. El registro queda marcado como eliminado pero no se borra físicamente — por eso puede:

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/items/019d867d-0010-7288-8ece-fd64da756010';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Item eliminado):


{
    "success": true,
    "data": null,
    "message": "Item eliminado correctamente.",
    "errors": null
}
 

Example response (404, No encontrado):


{
    "success": false,
    "data": null,
    "message": "Recurso no encontrado.",
    "errors": null
}
 

Request      

DELETE api/v1/public/items/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del item. Example: 019d867d-0010-7288-8ece-fd64da756010

Webhooks

Listar webhook endpoints registrados

requires authentication

Devuelve todos los endpoints de su cuenta (activos e inactivos), ordenados del más reciente al más antiguo. Cada endpoint incluye su estado de salud actual y los timestamps del último intento de entrega.

El campo health_status puede tomar los valores:

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/webhooks?is_active=1&per_page=15&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/webhooks"
);

const params = {
    "is_active": "1",
    "per_page": "15",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/webhooks';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'is_active' => '1',
            'per_page' => '15',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, success):


{
    "success": true,
    "data": [
        {
            "id": "9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a",
            "url": "https://api.miempresa.cr/webhooks/almendrofec",
            "secret": "whsec_****************************ab3f",
            "events": [
                "voucher.accepted",
                "voucher.rejected"
            ],
            "events_detail": [
                {
                    "value": "voucher.accepted",
                    "label": "Comprobante aceptado por Hacienda",
                    "group": "voucher"
                },
                {
                    "value": "voucher.rejected",
                    "label": "Comprobante rechazado por Hacienda",
                    "group": "voucher"
                }
            ],
            "description": "Webhook principal e-commerce",
            "is_active": true,
            "health_status": "healthy",
            "consecutive_failures": 0,
            "last_triggered_at": "2026-03-22T10:30:00-06:00",
            "last_success_at": "2026-03-22T10:30:00-06:00",
            "last_failure_at": null,
            "last_failure_reason": null,
            "created_at": "2026-03-20T08:00:00-06:00",
            "updated_at": "2026-03-22T10:30:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "total": 3,
        "per_page": 15,
        "last_page": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/webhooks

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

is_active   boolean  optional    

Filtrar por estado. Sin filtro: muestra todos. Example: true

per_page   integer  optional    

Resultados por página (1-100). Example: 15

page   integer  optional    

Número de página. Example: 1

Crear un webhook endpoint

requires authentication

Registra una URL HTTPS que recibirá notificaciones para los eventos que usted seleccione. El sistema genera automáticamente un secret único de 64 caracteres que deberá usar para verificar la firma de cada entrega.

Importante: el secret completo se devuelve una única vez en esta respuesta, en el campo secret. Almacénelo de forma segura en ese momento — en consultas posteriores (GET /webhooks/{id}) solo verá una versión enmascarada (whsec_****...ab3f). Para obtener un nuevo secret deberá eliminar el endpoint y crear uno nuevo.

Consejos para la URL:

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/webhooks" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"url\": \"https:\\/\\/api.miempresa.cr\\/webhooks\\/almendrofec\",
    \"events\": [
        \"voucher.accepted\",
        \"voucher.rejected\"
    ],
    \"description\": \"Webhook principal e-commerce\",
    \"is_active\": true
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/webhooks"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "url": "https:\/\/api.miempresa.cr\/webhooks\/almendrofec",
    "events": [
        "voucher.accepted",
        "voucher.rejected"
    ],
    "description": "Webhook principal e-commerce",
    "is_active": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/webhooks';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'url' => 'https://api.miempresa.cr/webhooks/almendrofec',
            'events' => [
                'voucher.accepted',
                'voucher.rejected',
            ],
            'description' => 'Webhook principal e-commerce',
            'is_active' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, created):


{
    "success": true,
    "data": {
        "id": "9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a",
        "url": "https://api.miempresa.cr/webhooks/almendrofec",
        "secret": "a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f6ab3f",
        "events": [
            "voucher.accepted",
            "voucher.rejected"
        ],
        "events_detail": [
            {
                "value": "voucher.accepted",
                "label": "Comprobante aceptado por Hacienda",
                "group": "voucher"
            },
            {
                "value": "voucher.rejected",
                "label": "Comprobante rechazado por Hacienda",
                "group": "voucher"
            }
        ],
        "description": "Webhook principal e-commerce",
        "is_active": true,
        "health_status": "never_used",
        "consecutive_failures": 0,
        "last_triggered_at": null,
        "last_success_at": null,
        "last_failure_at": null,
        "last_failure_reason": null,
        "created_at": "2026-03-22T10:30:00-06:00",
        "updated_at": "2026-03-22T10:30:00-06:00"
    },
    "message": "Webhook endpoint creado. Almacene el secret — no se mostrará de nuevo.",
    "errors": null
}
 

Example response (422, validation_error):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "url": [
            "La URL debe usar HTTPS."
        ],
        "events.0": [
            "El evento seleccionado no es válido."
        ]
    }
}
 

Example response (422, plan_restriction):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "plan": [
            "Su plan actual no incluye webhooks. Actualice su plan para usar esta funcionalidad."
        ]
    }
}
 

Example response (422, limit_reached):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "plan": [
            "Ha alcanzado el límite de webhooks de su plan (3 endpoints)."
        ]
    }
}
 

Request      

POST api/v1/public/webhooks

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

url   string     

URL HTTPS del endpoint receptor. Example: https://api.miempresa.cr/webhooks/almendrofec

events   string[]     

Lista de eventos a suscribir. Valores permitidos: voucher.sent, voucher.accepted, voucher.rejected, receiver.confirmed, receiver.deadline, certificate.expiring.

description   string  optional    

Descripción opcional para identificar el endpoint (máx 255). Example: Webhook principal e-commerce

is_active   boolean  optional    

Estado del endpoint. Solo disponible en PUT para reactivar endpoints desactivados. Al reactivar (true), se resetean los fallos consecutivos. Example: true

Response

Response Fields

data   object     
id   string     

UUID del webhook endpoint. Úselo en PUT, DELETE y GET /webhooks/{id}/logs.

url   string     

URL HTTPS registrada que recibirá los POST de cada evento.

secret   string     

Secret HMAC-SHA256 para verificar la firma de cada entrega. En POST (creación) se devuelve el texto plano completo (64 hex chars) — única vez. En GET/PUT/DELETE se devuelve enmascarado: whsec_****...últimos4.

events   string[]     

Códigos de los eventos suscritos. Valores posibles: voucher.sent, voucher.accepted, voucher.rejected, receiver.confirmed, receiver.deadline, certificate.expiring.

events_detail   object[]     

Eventos con detalle legible para UI. Cada objeto tiene value (código), label (descripción en español) y group (categoría: voucher, receiver, certificate).

description   string     

Descripción libre del endpoint para identificarlo en listados. Puede ser null.

is_active   boolean     

true si el endpoint está activo y recibirá entregas. false si fue desactivado manualmente o automáticamente tras 10 fallos consecutivos.

health_status   string     

Estado de salud computado: healthy (sin fallos), degraded (1-9 fallos consecutivos), disabled (≥10 fallos, auto-desactivado), inactive (desactivado manualmente), never_used (activo, nunca disparado).

consecutive_failures   integer     

Cantidad de fallos consecutivos sin éxito intermedio. Se resetea a 0 con cada entrega exitosa (HTTP 2xx). Al llegar a 10, el endpoint se auto-desactiva.

last_triggered_at   string     

Fecha/hora del último intento de entrega (ISO 8601). null si nunca se disparó.

last_success_at   string     

Fecha/hora de la última entrega exitosa — HTTP 2xx (ISO 8601). null si nunca hubo éxito.

last_failure_at   string     

Fecha/hora del último fallo de entrega (ISO 8601). null si nunca falló.

last_failure_reason   string     

Razón del último fallo para debugging rápido. Ejemplos: HTTP 503 Service Unavailable, Connection timeout 15s. null si nunca falló.

created_at   string     

Fecha de creación del endpoint (ISO 8601).

updated_at   string     

Fecha de última actualización del endpoint (ISO 8601).

Consultar un webhook endpoint

requires authentication

Devuelve el detalle completo de un endpoint: su URL, eventos suscritos, estado de salud actual, fallos consecutivos acumulados y timestamps de la última entrega (exitosa y fallida).

El campo secret se devuelve siempre enmascarado en este endpoint (whsec_****...ab3f), revelando solo los últimos 4 caracteres para permitir identificarlo visualmente. Si extravió el secret original, deberá eliminar el endpoint y crear uno nuevo.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, found):


{
    "success": true,
    "data": {
        "id": "9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a",
        "url": "https://api.miempresa.cr/webhooks/almendrofec",
        "secret": "whsec_****************************ab3f",
        "events": [
            "voucher.accepted",
            "voucher.rejected"
        ],
        "events_detail": [
            {
                "value": "voucher.accepted",
                "label": "Comprobante aceptado por Hacienda",
                "group": "voucher"
            }
        ],
        "description": "Webhook principal e-commerce",
        "is_active": true,
        "health_status": "healthy",
        "consecutive_failures": 0,
        "last_triggered_at": "2026-03-22T10:30:00-06:00",
        "last_success_at": "2026-03-22T10:30:00-06:00",
        "last_failure_at": null,
        "last_failure_reason": null,
        "created_at": "2026-03-20T08:00:00-06:00",
        "updated_at": "2026-03-22T10:30:00-06:00"
    },
    "message": "",
    "errors": null
}
 

Example response (404, not_found):


{
    "success": false,
    "data": null,
    "message": "El recurso solicitado no existe.",
    "errors": null
}
 

Request      

GET api/v1/public/webhooks/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del webhook endpoint. Example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a

Editar un webhook endpoint

requires authentication

Admite actualización parcial — envíe solo los campos que desee modificar. Los campos no enviados conservan su valor actual.

Reactivación de endpoints desactivados automáticamente: si un endpoint fue desactivado por acumular 10 fallos consecutivos, puede reactivarlo enviando is_active = true. Esto reinicia el contador de fallos a 0 y vuelve a incluir el endpoint en futuras entregas. Asegúrese antes de corregir el problema que causó los fallos.

Campos editables:

El secret no es editable. Si necesita rotarlo (por ejemplo, tras una sospecha de compromiso), elimine el endpoint actual y cree uno nuevo — recibirá un secret nuevo en la respuesta.

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"url\": \"https:\\/\\/api.miempresa.cr\\/webhooks\\/v2\",
    \"events\": [
        \"voucher.accepted\",
        \"voucher.rejected\",
        \"voucher.sent\"
    ],
    \"description\": \"Webhook e-commerce v2\",
    \"is_active\": true
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "url": "https:\/\/api.miempresa.cr\/webhooks\/v2",
    "events": [
        "voucher.accepted",
        "voucher.rejected",
        "voucher.sent"
    ],
    "description": "Webhook e-commerce v2",
    "is_active": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'url' => 'https://api.miempresa.cr/webhooks/v2',
            'events' => [
                'voucher.accepted',
                'voucher.rejected',
                'voucher.sent',
            ],
            'description' => 'Webhook e-commerce v2',
            'is_active' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, updated):


{
    "success": true,
    "data": {
        "id": "9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a",
        "url": "https://api.miempresa.cr/webhooks/v2",
        "secret": "whsec_****************************ab3f",
        "events": [
            "voucher.accepted",
            "voucher.rejected",
            "voucher.sent"
        ],
        "description": "Webhook e-commerce v2",
        "is_active": true,
        "health_status": "never_used",
        "consecutive_failures": 0,
        "last_triggered_at": "2026-03-22T10:30:00-06:00",
        "last_success_at": "2026-03-22T10:30:00-06:00",
        "last_failure_at": null,
        "last_failure_reason": null,
        "created_at": "2026-03-20T08:00:00-06:00",
        "updated_at": "2026-03-25T15:45:00-06:00"
    },
    "message": "Webhook endpoint actualizado correctamente.",
    "errors": null
}
 

Example response (404, not_found):


{
    "success": false,
    "data": null,
    "message": "El recurso solicitado no existe.",
    "errors": null
}
 

Request      

PUT api/v1/public/webhooks/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del webhook endpoint. Example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a

Body Parameters

url   string  optional    

URL HTTPS del endpoint receptor. Example: https://api.miempresa.cr/webhooks/v2

events   string[]  optional    

Lista de eventos a suscribir.

description   string  optional    

Descripción opcional (máx 255). Example: Webhook e-commerce v2

is_active   boolean  optional    

Activar o desactivar el endpoint. Example: true

Eliminar un webhook endpoint

requires authentication

Elimina el endpoint y deja de recibir notificaciones de todos sus eventos. Los logs históricos de entregas se conservan durante el período de retención para consulta y auditoría — eliminar el endpoint no borra sus logs.

Tras la eliminación, puede crear un endpoint nuevo con la misma URL si lo desea. Recibirá un secret nuevo en la respuesta de creación.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, deleted):


{
    "success": true,
    "data": null,
    "message": "Webhook endpoint eliminado correctamente.",
    "errors": null
}
 

Example response (404, not_found):


{
    "success": false,
    "data": null,
    "message": "El recurso solicitado no existe.",
    "errors": null
}
 

Request      

DELETE api/v1/public/webhooks/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del webhook endpoint. Example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a

Consultar el historial de entregas de un endpoint

requires authentication

Devuelve el registro de cada intento de entrega hacia el endpoint: evento disparado, ID de entrega (útil para deduplicar), resultado (éxito o fallo), código HTTP devuelto por su servidor y tiempo de respuesta en milisegundos.

Los logs se conservan durante 3 meses. Útil para:

Los campos voluminosos (payload enviado, response_body recibido) se excluyen del listado por razones de volumen. Si necesita esa información para una entrega específica, contacte a soporte indicando el delivery_id.

Interpretando el campo status:

Cuando una entrega tiene reintentos, cada intento queda registrado como una fila separada con el mismo delivery_id pero distinto valor de attempt (1, 2, 3 o 4).

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a/logs?event=voucher.accepted&status=failed&per_page=25&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a/logs"
);

const params = {
    "event": "voucher.accepted",
    "status": "failed",
    "per_page": "25",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a/logs';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'event' => 'voucher.accepted',
            'status' => 'failed',
            'per_page' => '25',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, success):


{
    "success": true,
    "data": [
        {
            "id": 12345,
            "event": "voucher.accepted",
            "event_label": "Comprobante aceptado por Hacienda",
            "delivery_id": "550e8400-e29b-41d4-a716-446655440000",
            "attempt": 1,
            "status": "success",
            "response_status": 200,
            "response_time_ms": 145,
            "response_time_formatted": "145ms",
            "error_message": null,
            "created_at": "2026-03-22T10:30:05-06:00"
        },
        {
            "id": 12344,
            "event": "voucher.rejected",
            "event_label": "Comprobante rechazado por Hacienda",
            "delivery_id": "660f9511-f30c-52e5-b827-557766551111",
            "attempt": 2,
            "status": "failed",
            "response_status": 503,
            "response_time_ms": 15000,
            "response_time_formatted": "15.00s",
            "error_message": "HTTP 503",
            "created_at": "2026-03-22T10:28:30-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "total": 87,
        "per_page": 25,
        "last_page": 4
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": "..."
    }
}
 

Example response (404, endpoint_not_found):


{
    "success": false,
    "data": null,
    "message": "El recurso solicitado no existe.",
    "errors": null
}
 

Request      

GET api/v1/public/webhooks/{id}/logs

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del webhook endpoint. Example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a

Query Parameters

event   string  optional    

Filtrar por tipo de evento. Example: voucher.accepted

status   string  optional    

Filtrar por resultado: success o failed. Example: failed

per_page   integer  optional    

Resultados por página (1-100). Example: 25

page   integer  optional    

Número de página. Example: 1

Reportes

Dashboard

Resumen del dashboard.

requires authentication

Retorna conteos por estado Hacienda, montos agregados (venta, descuentos, impuestos, total comprobante) y desglose por tipo de comprobante para el periodo solicitado.

Los datos son base para la preparacion de la declaracion D-104 (IVA mensual) y D-101 (renta anual).


Modo Integrador

Cuando el token pertenece a un integrador, este endpoint agrega automaticamente los comprobantes de todos sus clientes gestionados. La respuesta incluye adicionalmente el campo by_contributor con el desglose por cliente, ordenado de mayor a menor uso.

Para filtrar solo un cliente especifico, use ?managed_contributor_id={uuid}.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/reports/summary?date_from=2026-04-01&date_to=2026-04-30&voucher_type=01%2C04&status=accepted%2Crejected&currency_code=CRC&environment=production&receiver_id_number=3101000001&sale_condition=01%2C02&payment_method=01%2C02&group_by=month&limit=10&managed_contributor_id=019d867d-0241-7288-8ece-fd64da75616d" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/reports/summary"
);

const params = {
    "date_from": "2026-04-01",
    "date_to": "2026-04-30",
    "voucher_type": "01,04",
    "status": "accepted,rejected",
    "currency_code": "CRC",
    "environment": "production",
    "receiver_id_number": "3101000001",
    "sale_condition": "01,02",
    "payment_method": "01,02",
    "group_by": "month",
    "limit": "10",
    "managed_contributor_id": "019d867d-0241-7288-8ece-fd64da75616d",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/reports/summary';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'date_from' => '2026-04-01',
            'date_to' => '2026-04-30',
            'voucher_type' => '01,04',
            'status' => 'accepted,rejected',
            'currency_code' => 'CRC',
            'environment' => 'production',
            'receiver_id_number' => '3101000001',
            'sale_condition' => '01,02',
            'payment_method' => '01,02',
            'group_by' => 'month',
            'limit' => '10',
            'managed_contributor_id' => '019d867d-0241-7288-8ece-fd64da75616d',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Contribuyente normal):


{
    "success": true,
    "data": {
        "period": {
            "date_from": "2026-04-01",
            "date_to": "2026-04-30",
            "currency_code": "CRC",
            "environment": "production"
        },
        "counts": {
            "total": 150,
            "accepted": 140,
            "rejected": 5,
            "sent": 3,
            "error": 2,
            "cancelled": 0,
            "draft": 0,
            "pending": 0,
            "acceptance_rate": 96.55
        },
        "totals": {
            "total_venta": "15000000.00000",
            "total_descuentos": "500000.00000",
            "total_venta_neta": "14500000.00000",
            "total_impuesto": "1885000.00000",
            "total_otros_cargos": "0.00000",
            "total_comprobante": "16385000.00000",
            "average_per_voucher": "117035.71429"
        },
        "by_type": [
            {
                "voucher_type": "01",
                "voucher_type_label": "Factura Electrónica",
                "count": 100,
                "total_venta": "12000000.00000",
                "total_impuesto": "1560000.00000",
                "total_comprobante": "13560000.00000"
            }
        ]
    },
    "message": "",
    "errors": null
}
 

Example response (200, Integrador (incluye desglose por cliente)):


{
    "success": true,
    "data": {
        "period": {
            "date_from": "2026-04-01",
            "date_to": "2026-04-30",
            "currency_code": "CRC",
            "environment": "production"
        },
        "counts": {
            "total": 8500,
            "accepted": 8200,
            "rejected": 180,
            "sent": 70,
            "error": 50,
            "cancelled": 0,
            "draft": 0,
            "pending": 0,
            "acceptance_rate": 97.85
        },
        "totals": {
            "total_venta": "850000000.00000",
            "total_descuentos": "0.00000",
            "total_venta_neta": "850000000.00000",
            "total_impuesto": "110500000.00000",
            "total_otros_cargos": "0.00000",
            "total_comprobante": "960500000.00000",
            "average_per_voucher": "117134.14634"
        },
        "by_type": [
            {
                "voucher_type": "01",
                "voucher_type_label": "Factura Electrónica",
                "count": 7200,
                "total_venta": "720000000.00000",
                "total_impuesto": "93600000.00000",
                "total_comprobante": "813600000.00000"
            }
        ],
        "by_contributor": [
            {
                "contributor_id": "019d867d-a001-7288-8ece-fd64da756a01",
                "legal_name": "Hotel Las Palmas S.A.",
                "count": 5200,
                "total_comprobante": "621000000.00000",
                "total_impuesto": "80730000.00000"
            },
            {
                "contributor_id": "019d867d-a002-7288-8ece-fd64da756a02",
                "legal_name": "Restaurante El Mango S.A.",
                "count": 2800,
                "total_comprobante": "280000000.00000",
                "total_impuesto": "21000000.00000"
            },
            {
                "contributor_id": "019d867d-a003-7288-8ece-fd64da756a03",
                "legal_name": "SistemasPOS de Costa Rica S.A.",
                "count": 500,
                "total_comprobante": "59500000.00000",
                "total_impuesto": "8770000.00000"
            }
        ]
    },
    "message": "",
    "errors": null
}
 

Request      

GET api/v1/public/reports/summary

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

date_from   string  optional    

Inicio del periodo (YYYY-MM-DD). Default: primer dia del mes actual. Example: 2026-04-01

date_to   string  optional    

Fin del periodo (YYYY-MM-DD). Default: hoy. Example: 2026-04-30

voucher_type   string  optional    

CSV de tipos. 01,02,03,04,08,09,10. Example: 01,04

status   string  optional    

CSV de estados. Default: accepted. Example: accepted,rejected

currency_code   string  optional    

ISO 4217. Default: CRC. Example: CRC

environment   string  optional    

production o sandbox. Default: production. Example: production

receiver_id_number   string  optional    

Cedula del receptor para filtrar. Example: 3101000001

sale_condition   string  optional    

CSV de condiciones de venta. Example: 01,02

payment_method   string  optional    

CSV de medios de pago. Example: 01,02

group_by   string  optional    

Agrupación temporal (solo sales-by-period). Valores: day, week, month. Auto-detectado según rango. Example: month

Must be one of:
  • day
  • week
  • month
limit   integer  optional    

Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. validation.min validation.max. Example: 10

managed_contributor_id   string  optional    

UUID de un cliente gestionado. Solo para integradores. Filtra el resumen para mostrar unicamente los comprobantes de ese cliente. Sin este parametro se agregan todos. Example: 019d867d-0241-7288-8ece-fd64da75616d

Response

Response Fields

data   object     
period   object     

Parámetros del periodo consultado.

date_from   string     

Inicio del rango (YYYY-MM-DD). Default: primer día del mes actual.

date_to   string     

Fin del rango (YYYY-MM-DD). Default: hoy.

currency_code   string     

Moneda ISO 4217 del reporte. Default: CRC.

environment   string     

Ambiente consultado: production o sandbox.

counts   object     

Conteos de comprobantes por estado en el periodo.

total   integer     

Total de comprobantes en el rango (todos los estados).

accepted   integer     

Comprobantes aceptados por Hacienda (Mensaje=1).

rejected   integer     

Comprobantes rechazados por Hacienda (Mensaje=3).

sent   integer     

Comprobantes enviados, esperando respuesta de Hacienda.

error   integer     

Comprobantes con error temporal (se reintentan automáticamente).

cancelled   integer     

Comprobantes anulados mediante Nota de Crédito.

draft   integer     

Comprobantes en borrador (no firmados aún).

pending   integer     

Comprobantes firmados, encolados para envío.

acceptance_rate   number     

Porcentaje de aceptación: accepted / (accepted + rejected) * 100. 0 si no hay comprobantes resueltos.

totals   object     

Montos agregados del ResumenFactura (Decimal 18,5). Solo incluye comprobantes en los estados filtrados (default: accepted).

total_venta   string     

Suma de TotalVenta de todos los comprobantes.

total_descuentos   string     

Suma de TotalDescuentos.

total_venta_neta   string     

TotalVenta − TotalDescuentos.

total_impuesto   string     

Suma de todos los impuestos (IVA, selectivo, etc.).

total_otros_cargos   string     

Suma de otros cargos adicionales.

total_comprobante   string     

Monto final agregado de todos los comprobantes.

average_per_voucher   string     

Promedio por comprobante: total_comprobante / count.

by_type   object[]     

Desglose por tipo de comprobante. Cada objeto agrupa un tipo (01-10).

voucher_type   string     

Código del tipo: 01=FE, 02=ND, 03=NC, 04=TE, 08=FEC, 09=FEE, 10=REP.

voucher_type_label   string     

Nombre legible del tipo en español.

count   integer     

Cantidad de comprobantes de este tipo.

total_venta   string     

TotalVenta agregado para este tipo.

total_impuesto   string     

TotalImpuesto agregado para este tipo.

total_comprobante   string     

TotalComprobante agregado para este tipo.

by_contributor   object[]     

Desglose por cliente gestionado. Solo presente en modo integrador. Ordenado de mayor a menor uso.

contributor_id   string     

UUID del cliente gestionado.

legal_name   string     

Razón social del cliente.

count   integer     

Comprobantes emitidos por este cliente.

total_comprobante   string     

TotalComprobante agregado del cliente.

total_impuesto   string     

TotalImpuesto agregado del cliente.

Catálogos

Buscar en el catalogo CABYS (Bienes y Servicios).

requires authentication

Busqueda paginada en el catalogo oficial de bienes y servicios de Hacienda (~20,501 codigos). El codigo CABYS es obligatorio en todos los comprobantes electronicos (tipos 01-09).

Si q contiene solo digitos, busca por prefijo de codigo. Si contiene texto, busca por descripcion. Sin q, retorna todos los codigos activos ordenados por codigo.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/catalogs/cabys?q=arroz&per_page=10&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/catalogs/cabys"
);

const params = {
    "q": "arroz",
    "per_page": "10",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/catalogs/cabys';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'q' => 'arroz',
            'per_page' => '10',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Busqueda por texto):


{
    "success": true,
    "data": [
        {
            "code": "0111101000100",
            "description": "Arroz con cáscara (arroz paddy)",
            "tax_rate": "01",
            "tax_code": "08",
            "is_merchandise": true,
            "is_medicine": false
        },
        {
            "code": "0111101000200",
            "description": "Arroz descascarillado (arroz cargo o arroz pardo)",
            "tax_rate": "01",
            "tax_code": "08",
            "is_merchandise": true,
            "is_medicine": false
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "has_more": true
    },
    "links": {
        "prev": null,
        "next": "/api/v1/public/catalogs/cabys?q=arroz&page=2"
    }
}
 

Request      

GET api/v1/public/catalogs/cabys

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

q   string  optional    

Texto libre o prefijo numerico del codigo CABYS. Minimo 2 caracteres. Example: arroz

per_page   integer  optional    

Resultados por pagina (1-100). Default: 25. Example: 10

page   integer  optional    

Pagina actual. Example: 1

Response

Response Fields

data   object     
code   string     

Código CABYS de 13 dígitos exactos. Va directo al campo CodigoCABYS de cada LineaDetalle del XML.

description   string     

Descripción oficial del bien o servicio. Útil para autocompletar en la UI del integrador.

tax_rate   string     

Tasa de IVA aplicable: 01=1%, 02=2%, 04=4%, 08=13% (general), 10=Exenta. Mapea a CodigoTarifaIVA del XSD.

tax_code   string     

Código de impuesto para el campo CodigoImpuesto del XSD. Normalmente 01 (IVA).

is_merchandise   boolean     

true si el código corresponde a mercancía (afecta los totales merc_gravadas/merc_exentas del ResumenFactura). false si es servicio (afecta serv_gravados/serv_exentos).

is_medicine   boolean     

true si el código requiere el campo FormaFarmaceutica obligatorio en la LineaDetalle. Solo aplica a productos farmacéuticos registrados.

meta   object     
current_page   integer     

Página actual.

per_page   integer     

Resultados por página.

has_more   boolean     

true si hay más páginas disponibles. CABYS usa simplePaginate (sin COUNT total) por rendimiento con 20K+ registros.

Buscar actividad economica (CIIU).

requires authentication

Busqueda paginada en el catalogo de actividades economicas CIIU 4 (~800 codigos). El codigo de actividad es obligatorio en el campo CodigoActividadEmisor de todos los comprobantes electronicos.

Si q contiene solo digitos, busca por prefijo de codigo CIIU. Si contiene texto, busca por descripcion de la actividad.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/catalogs/activities?q=desarrollo+software&per_page=10&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/catalogs/activities"
);

const params = {
    "q": "desarrollo software",
    "per_page": "10",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/catalogs/activities';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'q' => 'desarrollo software',
            'per_page' => '10',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Busqueda por texto):


{
    "success": true,
    "data": [
        {
            "code": "6201.0",
            "description": "Actividades de programación informática"
        },
        {
            "code": "6202.0",
            "description": "Actividades de consultoría informática y gestión de instalaciones informáticas"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 25,
        "total": 2
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/catalogs/activities

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

q   string  optional    

Texto libre o prefijo numerico. Minimo 2 caracteres. Example: desarrollo software

per_page   integer  optional    

Resultados por pagina (1-100). Default: 25. Example: 10

page   integer  optional    

Pagina actual. Example: 1

Consultar ubicaciones (provincia, canton, distrito).

requires authentication

Consulta jerarquica de la division territorial de Costa Rica, necesaria para construir el nodo Ubicacion del emisor y receptor en el XML del comprobante.

Comportamiento jerarquico:

No usa paginacion: el resultado maximo es de aproximadamente 16 registros (distritos de un canton).

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/catalogs/locations?province=1&canton=01&q=escazu" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/catalogs/locations"
);

const params = {
    "province": "1",
    "canton": "01",
    "q": "escazu",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/catalogs/locations';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'province' => '1',
            'canton' => '01',
            'q' => 'escazu',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, 7 provincias (sin parametros)):


{
    "success": true,
    "data": [
        {
            "level": "province",
            "province_code": 1,
            "canton_code": null,
            "district_code": null,
            "name": "San José"
        },
        {
            "level": "province",
            "province_code": 2,
            "canton_code": null,
            "district_code": null,
            "name": "Alajuela"
        },
        {
            "level": "province",
            "province_code": 3,
            "canton_code": null,
            "district_code": null,
            "name": "Cartago"
        },
        {
            "level": "province",
            "province_code": 4,
            "canton_code": null,
            "district_code": null,
            "name": "Heredia"
        },
        {
            "level": "province",
            "province_code": 5,
            "canton_code": null,
            "district_code": null,
            "name": "Guanacaste"
        },
        {
            "level": "province",
            "province_code": 6,
            "canton_code": null,
            "district_code": null,
            "name": "Puntarenas"
        },
        {
            "level": "province",
            "province_code": 7,
            "canton_code": null,
            "district_code": null,
            "name": "Limón"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "count": 7
    }
}
 

Example response (200, Cantones de una provincia):


{
    "success": true,
    "data": [
        {
            "level": "canton",
            "province_code": 1,
            "canton_code": 1,
            "district_code": null,
            "name": "San José"
        },
        {
            "level": "canton",
            "province_code": 1,
            "canton_code": 2,
            "district_code": null,
            "name": "Escazú"
        },
        {
            "level": "canton",
            "province_code": 1,
            "canton_code": 3,
            "district_code": null,
            "name": "Desamparados"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "count": 20
    }
}
 

Request      

GET api/v1/public/catalogs/locations

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

province   string  optional    

Codigo de provincia (1-7) para obtener sus cantones. Example: 1

canton   string  optional    

Codigo de canton (01-20) para obtener sus distritos. Requiere province. Example: 01

q   string  optional    

Busqueda por nombre de ubicacion. Minimo 2 caracteres. Example: escazu

Consulta de Contribuyentes

Consultar identificacion por numero.

requires authentication

Busca primero en cache (24 horas), luego en el API publico de Hacienda. Para cedulas fisicas de 9 digitos no encontradas en Hacienda, y planes que incluyen consulta de padron, intenta el Padron Electoral TSE como fuente alternativa de nombre verificado.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/taxpayer/3101234567" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/taxpayer/3101234567"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/taxpayer/3101234567';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Caso A — Contribuyente en Hacienda con TSE (plan business|integrator)):


{
    "success": true,
    "data": {
        "id_number": "101230456",
        "id_type": "01",
        "id_type_label": "Cédula Física",
        "name": "JUAN PÉREZ SOLÍS",
        "hacienda_registered": true,
        "regime": {
            "code": "02",
            "description": "Simplificado"
        },
        "situation": {
            "status": "Activo",
            "morphs_fijo": true
        },
        "activities": [
            {
                "code": "620100",
                "status": "A",
                "description": "Actividades de consultoría informática"
            }
        ],
        "cached": false,
        "cached_at": "2026-04-08T10:30:00-06:00",
        "tse": {
            "found": true,
            "nombre_completo": "JUAN PÉREZ SOLÍS",
            "nombre": "JUAN",
            "apellido1": "PÉREZ",
            "apellido2": "SOLÍS",
            "cedula_vence": "2028-05-14",
            "cedula_vigente": true,
            "provincia": "San José",
            "canton": "Escazú",
            "distrito_codigo": "003",
            "padron_version": "2026-04-01",
            "nota": "Datos del Padrón Electoral TSE (Código Electoral Art. 105).",
            "cached": false
        }
    },
    "message": "Contribuyente encontrado.",
    "errors": null
}
 

Example response (200, Caso B — Consumidor final: no en Hacienda pero sí en padrón TSE (plan business|integrator)):


{
    "success": true,
    "data": {
        "id_number": "101230456",
        "id_type": "01",
        "id_type_label": "Cédula Física",
        "name": "JUAN PÉREZ SOLÍS",
        "hacienda_registered": false,
        "regime": null,
        "situation": null,
        "activities": [],
        "cached": false,
        "cached_at": "2026-04-08T10:30:00-06:00",
        "tse": {
            "found": true,
            "nombre_completo": "JUAN PÉREZ SOLÍS",
            "nombre": "JUAN",
            "apellido1": "PÉREZ",
            "apellido2": "SOLÍS",
            "cedula_vence": "2028-05-14",
            "cedula_vigente": true,
            "provincia": "San José",
            "canton": "Escazú",
            "distrito_codigo": "003",
            "padron_version": "2026-04-01",
            "nota": "Datos del Padrón Electoral TSE (Código Electoral Art. 105).",
            "cached": false
        }
    },
    "message": "Identificación verificada en Padrón Electoral TSE. No inscrita como contribuyente ante Hacienda.",
    "errors": null
}
 

Example response (200, Cédula jurídica o plan sin consulta de padrón (solo Hacienda)):


{
    "success": true,
    "data": {
        "id_number": "3101234567",
        "id_type": "02",
        "id_type_label": "Cédula Jurídica",
        "name": "EMPRESA EJEMPLO S.A.",
        "hacienda_registered": true,
        "regime": {
            "code": "01",
            "description": "Tradicional"
        },
        "situation": {
            "status": "Activo",
            "morphs_fijo": true
        },
        "activities": [
            {
                "code": "620100",
                "status": "A",
                "description": "Actividades de consultoría informática"
            }
        ],
        "cached": false,
        "cached_at": "2026-04-08T10:30:00-06:00"
    },
    "message": "Contribuyente encontrado.",
    "errors": null
}
 

Example response (404, Caso C — No encontrado en Hacienda ni en padrón TSE):


{
    "success": false,
    "data": null,
    "message": "No se encontró la identificación '999999999' en el registro de Hacienda ni en el Padrón Electoral TSE.",
    "errors": null
}
 

Example response (502, Hacienda no disponible):


{
    "success": false,
    "data": null,
    "message": "El API público de Hacienda retornó un error interno. Intente nuevamente más tarde.",
    "errors": null
}
 

Example response (504, Timeout Hacienda):


{
    "success": false,
    "data": null,
    "message": "El API público de Hacienda no respondió dentro del tiempo límite. Intente nuevamente.",
    "errors": null
}
 

Request      

GET api/v1/public/taxpayer/{id_number}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id_number   string     

Número de identificación (9-12 dígitos). Example: 3101234567

Response

Response Fields

data   object     
id_number   string     

Número de identificación consultado (9-12 dígitos).

id_type   string     

Tipo de identificación: 01=Física, 02=Jurídica, 03=DIMEX, 04=NITE, 05=Extranjero, 06=No Contribuyente.

id_type_label   string     

Nombre legible del tipo de identificación.

name   string     

Nombre o razón social. En Caso A proviene del RUT de Hacienda. En Caso B proviene del Padrón Electoral TSE.

hacienda_registered   boolean     

true si la identificación está inscrita en el RUT de Hacienda (Caso A). false si el nombre proviene del TSE y no es contribuyente (Caso B).

regime   object     

Régimen tributario del contribuyente. null en Caso B (no inscrito en Hacienda).

code   string     

Código del régimen (ej. 01=Tradicional, 02=Simplificado).

description   string     

Descripción del régimen.

situation   object     

Situación del contribuyente ante Hacienda. null en Caso B.

status   string     

Estado ante Hacienda: Activo, Inactivo, Moroso, etc.

morphs_fijo   boolean     

Indicador de morphs fijo del contribuyente.

activities   object[]     

Actividades económicas CIIU inscritas ante Hacienda. Array vacío en Caso B. Usar solo las que tengan status=A (activas).

code   string     

Código CIIU de 6 dígitos. Mapea a CodigoActividadReceptor al emitir FEC (tipo 08).

status   string     

Estado de la actividad: A=Activa, I=Inactiva.

description   string     

Descripción de la actividad económica.

cached   boolean     

true si los datos provienen de la cache Redis (TTL 24h para Hacienda, 1h para TSE-only).

cached_at   string     

Fecha/hora en que se almacenaron en cache (ISO 8601). null si no están cacheados.

tse   object     

Datos del Padrón Electoral TSE. Solo presente cuando: (a) plan business/integrator + cédula física de 9 dígitos + encontrada en el padrón, o (b) Caso B (TSE es la fuente principal). Ausente (clave omitida del JSON, no null) cuando el plan no incluye TSE, la cédula no es física, o no se encontró en el padrón.

found   boolean     

true si la cédula fue encontrada en el padrón.

nombre_completo   string     

Nombre completo en mayúsculas tal como el TSE lo almacena.

nombre   string     

Primer nombre del titular.

apellido1   string     

Primer apellido.

apellido2   string     

Segundo apellido.

cedula_vence   string     

Fecha de vencimiento de la cédula (YYYY-MM-DD). No es fecha de nacimiento.

cedula_vigente   boolean     

true si la cédula no ha expirado. Cédula vencida no invalida el comprobante — Hacienda no verifica vigencia del receptor.

provincia   string     

Nombre de la provincia de inscripción electoral (no necesariamente domicilio actual).

canton   string     

Nombre del cantón de inscripción electoral.

distrito_codigo   string     

Código TSE del distrito (3 dígitos). Distinto al sistema de 2 dígitos de Hacienda.

padron_version   string     

Fecha de corte del padrón fuente (YYYY-MM-DD). El TSE actualiza mensualmente.

nota   string     

Nota legal sobre la fuente de los datos.

cached   boolean     

true si los datos TSE provienen de cache Redis (TTL 30 días).

platform   object     

Información de existencia en la plataforma AlmendroFEC. Solo presente cuando el plan del contributor permite gestionar clientes (max_managed_contributors > 1). Ausente para planes que no gestionan clientes.

exists   boolean     

true si la cédula ya tiene cuenta activa en AlmendroFEC.

is_managed   boolean     

true si la cuenta está gestionada por algún integrador. El integrador sabe si será "el primero" o si comparte gestión.

available_certificates   object     

Ambientes con certificado .p12 activo.

sandbox   boolean     

true si tiene certificado activo de sandbox.

production   boolean     

true si tiene certificado activo de producción.

Confirmación del Receptor

Confirmar o rechazar comprobante recibido.

requires authentication

Genera y envia un MensajeReceptor a Hacienda para aceptar (total o parcial) o rechazar un comprobante electronico recibido de un proveedor.

Valores del campo message:

Plazo: 8 dias habiles desde la emision del documento (art. 15 Reglamento). Vencido el plazo se considera aceptacion tacita y no se puede enviar MensajeReceptor.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/receiver/confirm" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"voucher_key\": \"50619032600310199999900100001010000000001112345678\",
    \"issuer_id_number\": \"3101999999\",
    \"doc_issued_at\": \"2026-03-18T10:00:00-06:00\",
    \"message\": \"1\",
    \"message_detail\": \"Comprobante aceptado conforme.\",
    \"total_tax\": 6500,
    \"activity_code\": \"6201.0\",
    \"tax_condition\": \"01\",
    \"tax_creditable_amount\": 6500,
    \"expense_applicable_amount\": 50000,
    \"total_invoice\": 56500
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/receiver/confirm"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "voucher_key": "50619032600310199999900100001010000000001112345678",
    "issuer_id_number": "3101999999",
    "doc_issued_at": "2026-03-18T10:00:00-06:00",
    "message": "1",
    "message_detail": "Comprobante aceptado conforme.",
    "total_tax": 6500,
    "activity_code": "6201.0",
    "tax_condition": "01",
    "tax_creditable_amount": 6500,
    "expense_applicable_amount": 50000,
    "total_invoice": 56500
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/receiver/confirm';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'voucher_key' => '50619032600310199999900100001010000000001112345678',
            'issuer_id_number' => '3101999999',
            'doc_issued_at' => '2026-03-18T10:00:00-06:00',
            'message' => '1',
            'message_detail' => 'Comprobante aceptado conforme.',
            'total_tax' => 6500.0,
            'activity_code' => '6201.0',
            'tax_condition' => '01',
            'tax_creditable_amount' => 6500.0,
            'expense_applicable_amount' => 50000.0,
            'total_invoice' => 56500.0,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Aceptación enviada exitosamente):


{
    "success": true,
    "data": {
        "id": "019d867d-0030-7288-8ece-fd64da756030",
        "voucher_key": "50619032600310199999900100001010000000001112345678",
        "voucher_id": "019d867d-0031-7288-8ece-fd64da756031",
        "issuer_id_number": "3101999999",
        "doc_issued_at": "2026-03-26T08:00:00-06:00",
        "message": "1",
        "message_label": "Aceptado",
        "message_detail": null,
        "total_tax": "130000.00000",
        "activity_code": "620100",
        "tax_condition": "01",
        "tax_condition_label": "Genera crédito IVA",
        "tax_creditable_amount": "130000.00000",
        "expense_applicable_amount": null,
        "total_invoice": "1130000.00000",
        "receiver_id_number": "3101000000",
        "receiver_consecutive": "00100001050000000001",
        "status": "draft",
        "has_xml": true,
        "is_signed": true,
        "environment": "production",
        "deadline": {
            "business_days_elapsed": 2,
            "business_days_remaining": 6,
            "deadline_date": "2026-04-07",
            "is_within_deadline": true,
            "is_near_deadline": false,
            "is_overdue": false
        },
        "hacienda": {
            "status": null,
            "message": null,
            "sent_at": null,
            "processed_at": null
        },
        "issued_by": "019d867d-0032-7288-8ece-fd64da756032",
        "created_at": "2026-04-16T10:00:00-06:00",
        "updated_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Aceptado del comprobante [506...001] generada y validada correctamente. Pendiente de envío a Hacienda.",
    "errors": null
}
 

Example response (422, Plazo de 8 días hábiles vencido):


{
    "success": false,
    "data": null,
    "message": "El plazo de 8 días hábiles para confirmar/rechazar el comprobante ha vencido.",
    "errors": {
        "voucher_key": [
            "El plazo de 8 días hábiles ha vencido."
        ]
    }
}
 

Example response (422, Ya existe confirmación para este comprobante):


{
    "success": false,
    "data": null,
    "message": "Ya existe una confirmación/rechazo para este comprobante.",
    "errors": {
        "voucher_key": [
            "Ya existe una confirmación/rechazo para este comprobante."
        ]
    }
}
 

Request      

POST api/v1/public/receiver/confirm

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

voucher_key   string     

Clave del comprobante recibido (50 dígitos exactos). Estructura: [3 país][8 fecha][12 cédula][10 consecutivo][1 situación][8 seguridad][8 verificador]. Must match the regex /^\d{50}$/. Example: 50619032600310199999900100001010000000001112345678

issuer_id_number   string     

Número de cédula del emisor del comprobante (máx 20 chars). validation.max. Example: 3101999999

doc_issued_at   string     

Fecha de emisión del comprobante recibido (xs:dateTime). No puede ser futura. validation.date validation.before_or_equal. Example: 2026-03-18T10:00:00-06:00

message   string     

Tipo de confirmación. 1=Aceptado, 2=Aceptado Parcialmente, 3=Rechazado. Example: 1

Must be one of:
  • 1
  • 2
  • 3
message_detail   string     

Detalle del mensaje (1-160 chars). Motivo de aceptación o rechazo. validation.min validation.max. Example: Comprobante aceptado conforme.

total_tax   number  optional    

Monto total de impuesto del comprobante (Decimal 18,5). Condicional: obligatorio si el comprobante tiene impuestos. validation.min. Example: 6500

activity_code   string  optional    

Código CIIU del receptor (6 dígitos). Obligatorio si tax_condition05. Prohibido si tax_condition = 05. Must match the regex /^(\d{4}.\d|\d{6})$/. Example: 6201.0

tax_condition   string  optional    

Condición de impuesto del receptor. 01=Crédito IVA general, 02=Crédito parcial, 03=Bienes de capital, 04=Gasto sin crédito, 05=Proporcionalidad. Example: 01

Must be one of:
  • 1
  • 2
  • 3
  • 4
  • 5
tax_creditable_amount   number  optional    

Monto total de impuesto acreditable (Decimal 18,5). Obligatorio si tax_condition05. validation.min. Example: 6500

expense_applicable_amount   number  optional    

Monto total de gasto aplicable (Decimal 18,5). Obligatorio si tax_condition05. validation.min. Example: 50000

total_invoice   number     

Monto total del comprobante (Decimal 18,5). Siempre obligatorio. validation.min. Example: 56500

voucher_id   integer  optional    

ID interno del comprobante en el sistema (opcional). Referencia para tracking. The id of an existing record in the vouchers table.

Response

Response Fields

data   object     
id   string     

UUID del MensajeReceptor generado.

voucher_key   string     

Clave de 50 dígitos del comprobante al que se responde.

voucher_id   string     

UUID interno del voucher en AlmendroFEC. null si el comprobante no fue emitido por este sistema.

issuer_id_number   string     

Cédula del emisor del comprobante original (NumeroCedulaEmisor del MensajeReceptor XSD).

doc_issued_at   string     

Fecha de emisión del documento original (ISO 8601). Usada para calcular el plazo de 8 días hábiles.

message   string     

Código del mensaje: 1=Aceptado, 2=Aceptado Parcialmente, 3=Rechazado.

message_label   string     

Nombre legible del tipo de mensaje en español.

message_detail   string     

Detalle o razón del rechazo/aceptación parcial. null si fue aceptación total.

total_tax   string     

MontoTotalImpuesto del comprobante (Decimal 18,5). Obligatorio cuando message es 1 o 2.

activity_code   string     

Código de actividad económica CIIU (6 dígitos). Obligatorio cuando message es 1 o 2.

tax_condition   string     

Código de condición de impuesto: 01=Crédito IVA general, 02=Crédito parcial, 03=Bienes de capital, 04=Gasto sin crédito, 05=Proporcionalidad.

tax_condition_label   string     

Nombre legible de la condición de impuesto.

tax_creditable_amount   string     

MontoTotalImpuestoAcreditar (Decimal 18,5). Monto de IVA que el receptor puede acreditar. null si no aplica.

expense_applicable_amount   string     

MontoTotalDeGastoAplicable (Decimal 18,5). Monto del gasto aplicable. null si no aplica.

total_invoice   string     

TotalFactura del comprobante original (Decimal 18,5).

receiver_id_number   string     

Cédula del receptor (quien emite este MensajeReceptor).

receiver_consecutive   string     

Consecutivo de 20 dígitos del receptor (tipo 05/06/07 según el mensaje).

status   string     

Estado del envío: draft (generado, pendiente de envío), sent, accepted, rejected, error.

has_xml   boolean     

true si el XML del MensajeReceptor fue generado.

is_signed   boolean     

true si el XML fue firmado con XAdES-EPES.

environment   string     

Ambiente: production o sandbox.

deadline   object     

Información del plazo de 8 días hábiles (art. 15 Reglamento). Calculado considerando feriados de CR.

business_days_elapsed   integer     

Días hábiles transcurridos desde la emisión del comprobante.

business_days_remaining   integer     

Días hábiles restantes para responder. 0 si venció.

deadline_date   string     

Fecha límite para responder (YYYY-MM-DD).

is_within_deadline   boolean     

true si aún está dentro del plazo.

is_near_deadline   boolean     

true si quedan 2 días hábiles o menos.

is_overdue   boolean     

true si el plazo venció.

hacienda   object     

Respuesta de Hacienda al MensajeReceptor.

status   string     

Estado: aceptado, rechazado, o null si no procesado aún.

message   string     

Detalle del mensaje de Hacienda. null si pendiente.

sent_at   string     

Fecha/hora de envío a Hacienda (ISO 8601). null si no enviado.

processed_at   string     

Fecha/hora de procesamiento por Hacienda (ISO 8601). null si pendiente.

issued_by   string     

UUID del usuario que generó el MensajeReceptor.

created_at   string     

Fecha de creación del registro (ISO 8601).

updated_at   string     

Fecha de última actualización (ISO 8601).

Clientes del Integrador

Listar clientes gestionados por el integrador

requires authentication

Devuelve los contribuyentes-cliente que el integrador autenticado gestiona actualmente, con búsqueda por texto y paginación. Ordenado por fecha de creación descendente (los más recientes primero).

Solo los clientes con vínculo activo se incluyen. Si un cliente fue desvinculado, no aparece en este listado aunque su cuenta siga existiendo.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-contributors?search=cliente&per_page=15&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors"
);

const params = {
    "search": "cliente",
    "per_page": "15",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'search' => 'cliente',
            'per_page' => '15',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado paginado de clientes gestionados (cliente exclusivo)):


{
    "success": true,
    "data": [
        {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "legal_name": "CLIENTE DEL INTEGRADOR S.A.",
            "trade_name": "Cliente Shop",
            "id_type": "02",
            "id_type_label": "Cédula Jurídica",
            "id_number": "3101123456",
            "primary_email": "juan@cliente.cr",
            "is_active": true,
            "can_emit_from_portal": true,
            "plan_code": "free",
            "plan_name": "Gratis",
            "integrators_count": 1,
            "created_at": "2026-04-10T09:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1,
        "from": 1,
        "to": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Example response (200, Cliente compartido con otros integradores (N:N)):


{
    "success": true,
    "data": [
        {
            "id": "019d867d-c001-7288-8ece-fd64da756c02",
            "legal_name": "CLIENTE COMPARTIDO S.A.",
            "trade_name": null,
            "id_type": "02",
            "id_type_label": "Cédula Jurídica",
            "id_number": "3101999999",
            "primary_email": "contacto@compartido.cr",
            "is_active": true,
            "can_emit_from_portal": true,
            "plan_code": "pyme",
            "plan_name": "Pyme",
            "integrators_count": 3,
            "created_at": "2026-03-15T12:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1,
        "from": 1,
        "to": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Example response (200, Sin clientes gestionados):


{
    "success": true,
    "data": [],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 0,
        "from": null,
        "to": null
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Request      

GET api/v1/public/my-contributors

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

search   string  optional    

Búsqueda por razón social o número de identificación (mínimo 2 caracteres). Example: cliente

per_page   integer  optional    

Resultados por página (1-100). Default: 15. Example: 15

page   integer  optional    

Número de página. Example: 1

Response

Response Fields

data   object     
id   string     

UUID del contribuyente-cliente.

legal_name   string     

Razón social del cliente.

trade_name   string     

Nombre comercial. null si no tiene.

id_type   string     

Tipo de identificación: 01-04.

id_type_label   string     

Nombre legible del tipo.

id_number   string     

Número de identificación.

primary_email   string     

Correo principal del cliente.

is_active   boolean     

Estado de la cuenta del cliente.

can_emit_from_portal   boolean     

true si el plan del cliente permite emitir desde el portal web (determinado por el plan del cliente, no del integrador).

plan_code   string     

Código del plan del cliente: free, starter, pyme, etc.

plan_name   string     

Nombre comercial del plan del cliente.

integrators_count   integer     

Cantidad de integradores activos que gestionan a este cliente. 1 = exclusivo, >1 = compartido (badge "Compartido" en UI). Solo se expone el conteo, no la identidad de otros integradores.

created_at   string     

Fecha de creación de la cuenta del cliente (ISO 8601).

Crear un cliente gestionado

requires authentication

Crea un nuevo contribuyente-cliente en el sistema y establece un vínculo de gestión con el integrador autenticado. El cliente recibe un magic link en el correo indicado para activar su cuenta.

El cliente queda con:

El cliente NO queda con:

Tras crear:

  1. El cliente recibe un email con el magic link.
  2. El cliente debe activar la cuenta en ≤ 72 horas.
  3. Si quiere firmar por él, solicite un grant con POST /access-requests.
Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/my-contributors" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"legal_name\": \"CLIENTE DEL INTEGRADOR S.A.\",
    \"trade_name\": \"Cliente Shop\",
    \"id_type\": \"02\",
    \"id_number\": \"3101123456\",
    \"email\": \"contacto@cliente.cr\",
    \"phone\": \"22001234\",
    \"economic_activities\": [
        \"6201.0\"
    ],
    \"province\": 1,
    \"canton\": 1,
    \"district\": 1,
    \"neighborhood\": \"San Pedro\",
    \"address\": \"200m norte del parque central\",
    \"owner_name\": \"Juan Cliente\",
    \"owner_email\": \"juan@cliente.cr\",
    \"phone_country_code\": 506,
    \"emails\": [
        \"contacto@cliente.cr\"
    ]
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "legal_name": "CLIENTE DEL INTEGRADOR S.A.",
    "trade_name": "Cliente Shop",
    "id_type": "02",
    "id_number": "3101123456",
    "email": "contacto@cliente.cr",
    "phone": "22001234",
    "economic_activities": [
        "6201.0"
    ],
    "province": 1,
    "canton": 1,
    "district": 1,
    "neighborhood": "San Pedro",
    "address": "200m norte del parque central",
    "owner_name": "Juan Cliente",
    "owner_email": "juan@cliente.cr",
    "phone_country_code": 506,
    "emails": [
        "contacto@cliente.cr"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'legal_name' => 'CLIENTE DEL INTEGRADOR S.A.',
            'trade_name' => 'Cliente Shop',
            'id_type' => '02',
            'id_number' => '3101123456',
            'email' => 'contacto@cliente.cr',
            'phone' => '22001234',
            'economic_activities' => [
                '6201.0',
            ],
            'province' => 1,
            'canton' => 1,
            'district' => 1,
            'neighborhood' => 'San Pedro',
            'address' => '200m norte del parque central',
            'owner_name' => 'Juan Cliente',
            'owner_email' => 'juan@cliente.cr',
            'phone_country_code' => 506,
            'emails' => [
                'contacto@cliente.cr',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Cliente creado y magic link enviado):


{
    "success": true,
    "data": {
        "id": "019d867d-c001-7288-8ece-fd64da756c01",
        "id_type": "02",
        "id_number": "3101123456",
        "legal_name": "CLIENTE DEL INTEGRADOR S.A.",
        "commercial_name": "Cliente Shop",
        "emails": [
            "juan@cliente.cr"
        ],
        "economic_activities": [
            "6201.0"
        ],
        "province": 1,
        "canton": 1,
        "district": 1,
        "neighborhood": "San Pedro",
        "address": "200m norte del parque central",
        "phone": "22001234",
        "phone_country_code": 506,
        "is_active": true,
        "plan": {
            "code": "free",
            "name": "Gratis"
        },
        "has_active_grant": false,
        "created_at": "2026-04-16T10:00:00-06:00",
        "updated_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Cliente creado correctamente. Se envió email de bienvenida.",
    "errors": null
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (422, Límite del plan alcanzado):


{
    "success": false,
    "data": null,
    "message": "Ha alcanzado el máximo de clientes gestionados de su plan.",
    "errors": {
        "managed_contributors": [
            "Ha alcanzado el máximo de clientes gestionados de su plan."
        ]
    }
}
 

Example response (422, Correo del propietario ya registrado):


{
    "success": false,
    "data": null,
    "message": "Los datos proporcionados no son válidos.",
    "errors": {
        "owner_email": [
            "Este correo ya está registrado en el sistema."
        ]
    }
}
 

Request      

POST api/v1/public/my-contributors

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

legal_name   string     

Razón social o nombre legal del cliente (3-150 chars). Example: CLIENTE DEL INTEGRADOR S.A.

trade_name   string  optional    

Nombre comercial del cliente (3-80 chars). Example: Cliente Shop

id_type   string     

Tipo de identificación del cliente. Valores: 01 (Física), 02 (Jurídica), 03 (DIMEX), 04 (NITE). Example: 02

id_number   string     

Número de identificación del cliente. Example: 3101123456

email   string     

Correo del cliente. Se usa para la cuenta en el portal y envío de facturación. Único en el sistema. validation.email validation.max. Example: contacto@cliente.cr

phone   string  optional    

Teléfono de contacto (4-20 dígitos). Example: 22001234

economic_activities   string[]  optional    

Códigos CIIU en formato Hacienda XXXX.X.

province   integer  optional    

Código de provincia (1-7). Example: 1

canton   integer  optional    

Código de cantón (1-99). Example: 1

district   integer  optional    

Código de distrito (1-99). Example: 1

neighborhood   string  optional    

Barrio (5-50 chars). Example: San Pedro

address   string  optional    

Señas exactas (5-300 chars). Example: 200m norte del parque central

owner_name   string     

Nombre completo del usuario administrador del cliente. Example: Juan Cliente

owner_email   string     

Correo del administrador del cliente (recibe el magic link). Example: juan@cliente.cr

phone_country_code   integer  optional    

Código de país del teléfono. Default: 506. Example: 506

emails   string[]  optional    

Correos de contacto adicionales del cliente (máx 4).

Response

Response Fields

data   object     
id   string     

UUID del contribuyente-cliente creado.

id_type   string     

Tipo de identificación: 01=Física, 02=Jurídica, 03=DIMEX, 04=NITE.

id_number   string     

Número de identificación del cliente.

legal_name   string     

Razón social o nombre legal del cliente.

commercial_name   string     

Nombre comercial. null si no se envió.

emails   string[]     

Correos del cliente.

economic_activities   string[]     

Códigos CIIU en formato XXXX.X.

province   integer     

Código de provincia (1-7). null si no se envió.

canton   integer     

Código de cantón. null si no se envió.

district   integer     

Código de distrito. null si no se envió.

neighborhood   string     

Barrio. null si no se envió.

address   string     

Señas exactas. null si no se envió.

phone   string     

Teléfono de contacto. null si no se envió.

phone_country_code   integer     

Código de país del teléfono. Default: 506.

is_active   boolean     

true — el cliente se crea activo.

plan   object     

Plan asignado al cliente (siempre Gratis al crear).

code   string     

Código del plan: free.

name   string     

Nombre del plan: Gratis.

has_active_grant   boolean     

false al crear — el integrador debe solicitar un grant por separado vía POST /access-requests.

created_at   string     

Fecha de creación (ISO 8601).

updated_at   string     

Fecha de última actualización (ISO 8601).

Consultar un cliente gestionado

requires authentication

Devuelve el detalle completo del cliente indicado. Solo se retornan clientes con vínculo activo con el integrador autenticado. Si el UUID no corresponde a un cliente gestionado actualmente por usted, retorna HTTP 404.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Cliente encontrado):


{
    "success": true,
    "data": {
        "id": "019d867d-c001-7288-8ece-fd64da756c01",
        "id_type": "02",
        "id_number": "3101123456",
        "legal_name": "CLIENTE DEL INTEGRADOR S.A.",
        "commercial_name": "Cliente Shop",
        "emails": [
            "juan@cliente.cr"
        ],
        "economic_activities": [
            "6201.0"
        ],
        "province": 1,
        "canton": 1,
        "district": 1,
        "neighborhood": "San Pedro",
        "address": "200m norte del parque central",
        "phone": "22001234",
        "phone_country_code": 506,
        "is_active": true,
        "plan": {
            "code": "free",
            "name": "Gratis"
        },
        "has_active_grant": true,
        "active_grant_environment": "sandbox",
        "created_at": "2026-04-10T09:00:00-06:00",
        "updated_at": "2026-04-10T09:00:00-06:00"
    },
    "message": "",
    "errors": null
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (404, Cliente no encontrado o desvinculado):


{
    "success": false,
    "data": null,
    "message": "Cliente no encontrado.",
    "errors": null
}
 

Request      

GET api/v1/public/my-contributors/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

Actualizar relación con el cliente gestionado

requires authentication

Permite modificar únicamente campos de la relación de gestión entre el integrador y el cliente. Los datos propios del cliente (razón social, dirección, teléfono, etc.) no pueden ser modificados por el integrador — solo el cliente puede editarlos desde su propio portal o API (Art. 16 Reglamento).

Campo editable:

Campos bloqueados (retornan 403):

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"default_pdf_template_id\": 3
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "default_pdf_template_id": 3
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'default_pdf_template_id' => 3,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Relación actualizada):



 

Example response (403, Intento de modificar datos del cliente):


{
    "success": false,
    "data": null,
    "message": "El integrador no puede modificar los datos del cliente. Solo el cliente puede editar su información desde su propio portal o API. Campos bloqueados: legal_name, trade_name.",
    "errors": null
}
 

Request      

PUT api/v1/public/my-contributors/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente. Example: 019d867d-c001-7288-8ece-fd64da756c01

Body Parameters

default_pdf_template_id   integer  optional    

ID de la plantilla PDF. Example: 3

Desvincular al integrador del cliente

requires authentication

Rompe únicamente la relación de gestión entre usted y este cliente. El cliente conserva:

Efectos inmediatos de la desvinculación:

  1. La relación de gestión queda marcada como desvinculada.
  2. Todos los grants activos de este cliente hacia usted se revocan automáticamente (no podrá seguir firmando por él).
  3. Este cliente desaparecerá del listado GET /my-contributors.

No se puede usar este endpoint para desvincularse a sí mismo (es decir, pasar su propio UUID de integrador) — retorna HTTP 403.

Usos típicos: finalización de contrato con el cliente, transferencia del cliente a otro integrador, limpieza de clientes inactivos.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Cliente desvinculado):


{
    "success": true,
    "data": null,
    "message": "Cliente desvinculado correctamente. Se revocaron los accesos asociados.",
    "errors": null
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (403, Intento de auto-desvinculación):


{
    "success": false,
    "data": null,
    "message": "No puede desvincularse a sí mismo desde este endpoint.",
    "errors": null
}
 

Example response (404, Cliente no encontrado o ya desvinculado):


{
    "success": false,
    "data": null,
    "message": "Cliente no encontrado.",
    "errors": null
}
 

Request      

DELETE api/v1/public/my-contributors/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente a desvincular. Example: 019d867d-c001-7288-8ece-fd64da756c01

Listar los certificados digitales de un cliente gestionado

requires authentication

Devuelve el historial completo de certificados del cliente (activo + desactivados), ordenados por fecha de creación descendente. Los datos sensibles (contenido del .p12, contraseñas, PIN de Hacienda) nunca se incluyen en la respuesta.

La estructura es idéntica a GET /api/v1/public/certificates (grupo Certificados Digitales), pero opera sobre el cliente indicado en la URL.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Listado de certificados del cliente):


{
    "success": true,
    "data": [
        {
            "id": "019d867d-1111-7288-8ece-fd64da756001",
            "environment": "sandbox",
            "is_active": true,
            "is_expired": false,
            "days_remaining": 120,
            "valid_from": "2024-01-01T00:00:00-06:00",
            "valid_until": "2026-08-15T23:59:59-06:00",
            "certificate_subject": "CN=CLIENTE DEL INTEGRADOR S.A., serialNumber=3101123456",
            "certificate_serial": "0A1B2C3D4E5F",
            "created_at": "2026-04-09T10:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null
}
 

Example response (200, Cliente sin certificados):


{
    "success": true,
    "data": [],
    "message": "",
    "errors": null
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (404, Cliente no encontrado o desvinculado):


{
    "success": false,
    "data": null,
    "message": "Cliente no encontrado o no pertenece a este integrador.",
    "errors": null
}
 

Request      

GET api/v1/public/my-contributors/{id}/certificates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

Eliminar un certificado del cliente (siempre denegado)

requires authentication

Este endpoint siempre retorna HTTP 403 y no realiza ningún cambio. El integrador no puede eliminar certificados del cliente — el certificado es propiedad del contribuyente emisor y solo él puede desactivarlo desde su propio portal.

Si quiere dejar de usar el certificado del cliente:

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates/019d867d-1111-7288-8ece-fd64da756001" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates/019d867d-1111-7288-8ece-fd64da756001"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates/019d867d-1111-7288-8ece-fd64da756001';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (403, Operación siempre denegada):


{
    "success": false,
    "data": null,
    "message": "El integrador no puede eliminar certificados del cliente. El certificado es propiedad del contribuyente emisor. Si desea dejar de usar este certificado, revoque su acceso desde DELETE /api/v1/public/my-access/{grantId}.",
    "errors": null
}
 

Request      

DELETE api/v1/public/my-contributors/{id}/certificates/{certId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

certId   string     

UUID del certificado (no relevante, siempre 403). Example: 019d867d-1111-7288-8ece-fd64da756001

Subir un certificado digital para el cliente

requires authentication

Carga el archivo .p12 del cliente desde la cuenta del integrador. El certificado queda vinculado al cliente (no al integrador): el integrador administra el .p12 en nombre del cliente pero el certificado pertenece al cliente.

Este endpoint es equivalente a POST /api/v1/public/certificates del grupo Certificados Digitales, pero opera sobre el cliente gestionado indicado en la URL. Consulte la guía de ese grupo para entender el formato del .p12, el PIN de Hacienda, etc.

Request: multipart/form-data (no JSON).

Ejemplo con curl:

curl -X POST "https://api.almendro.cr/api/v1/public/my-contributors/{id}/certificates" \
  -H "Authorization: Bearer {su_token_api}" \
  -F "p12_file=@/ruta/cert-del-cliente.p12" \
  -F "p12_password=contrasena-del-p12" \
  -F "hacienda_pin=1234" \
  -F "environment=sandbox"

Recuerde: cargar el certificado del cliente no le da automáticamente permiso de firmar por él. Necesita además un grant activo (ver grupo Certificados de terceros).

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "p12_password=ContrasenaDelP12"\
    --form "hacienda_pin=1234"\
    --form "environment=sandbox"\
    --form "p12_file=@/tmp/phpqkb9gjdo42jpb07SXK0" 
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('p12_password', 'ContrasenaDelP12');
body.append('hacienda_pin', '1234');
body.append('environment', 'sandbox');
body.append('p12_file', document.querySelector('input[name="p12_file"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/certificates';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'multipart/form-data',
            'Accept' => 'application/json',
        ],
        'multipart' => [
            [
                'name' => 'p12_password',
                'contents' => 'ContrasenaDelP12'
            ],
            [
                'name' => 'hacienda_pin',
                'contents' => '1234'
            ],
            [
                'name' => 'environment',
                'contents' => 'sandbox'
            ],
            [
                'name' => 'p12_file',
                'contents' => fopen('/tmp/phpqkb9gjdo42jpb07SXK0', 'r')
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Certificado del cliente registrado):


{
    "success": true,
    "data": {
        "id": "019d867d-3333-7288-8ece-fd64da756c03",
        "environment": "sandbox",
        "is_active": true,
        "is_expired": false,
        "days_remaining": 730,
        "valid_from": "2026-01-01T00:00:00-06:00",
        "valid_until": "2028-01-01T00:00:00-06:00",
        "certificate_subject": "CN=CLIENTE DEL INTEGRADOR S.A., serialNumber=3101123456",
        "certificate_serial": "0A1B2C3D4E5F",
        "created_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Certificado del cliente registrado y activado correctamente.",
    "errors": null
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (404, Cliente no encontrado o desvinculado):


{
    "success": false,
    "data": null,
    "message": "Cliente no encontrado.",
    "errors": null
}
 

Example response (422, Contraseña del .p12 incorrecta):


{
    "success": false,
    "data": null,
    "message": "No se pudo leer el certificado .p12. Verifique que la contraseña sea correcta y que el archivo sea un certificado digital válido.",
    "errors": {
        "p12_file": [
            "No se pudo leer el certificado .p12. Verifique que la contraseña sea correcta y que el archivo sea un certificado digital válido."
        ]
    }
}
 

Request      

POST api/v1/public/my-contributors/{id}/certificates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

Body Parameters

p12_file   file     

Archivo .p12 o .pfx de firma digital emitida por BCCR al cliente. Máximo 5 MB. Example: /tmp/phpqkb9gjdo42jpb07SXK0

p12_password   string     

Contraseña que protege el archivo .p12 (la que el cliente definió al generarlo). Example: ContrasenaDelP12

hacienda_pin   string     

PIN de Hacienda del cliente (asignado por ATV al registrarse como emisor). Example: 1234

environment   string     

Ambiente al que se asigna el certificado. Valores: sandbox o production. Example: sandbox

requires authentication

Genera un nuevo token de magic link y envía un correo de bienvenida al usuario propietario del cliente. Útil cuando el magic link original expiró (TTL 72 horas) y el cliente aún no activó su cuenta.

Restricciones:

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/resend-magic-link" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/resend-magic-link"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/resend-magic-link';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Magic link reenviado):


{
    "success": true,
    "data": null,
    "message": "Magic link reenviado al correo del cliente.",
    "errors": null
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (404, Cliente no encontrado):


{
    "success": false,
    "data": null,
    "message": "Cliente no encontrado.",
    "errors": null
}
 

Example response (422, Cliente ya activó su cuenta):


{
    "success": false,
    "data": null,
    "message": "El cliente ya estableció su contraseña. No se puede reenviar el magic link.",
    "errors": null
}
 

Listar las secuencias de consecutivos del cliente

requires authentication

Devuelve todas las secuencias (voucher_sequences) del cliente gestionado filtradas por la terminal asignada a este integrador. Cada secuencia corresponde a un tipo de comprobante (01–10).

¿Para qué sirve?

Al migrar un cliente desde otro facturador (Alegra, Gosocket, etc.), AlmendroFEC no tiene historial de los consecutivos previos. Si se emite desde la secuencia 1, Hacienda rechaza por "consecutivo duplicado". Este endpoint permite verificar en qué número va cada tipo antes de emitir, y el endpoint PUT permite ajustarlo.

Tipos de comprobante (01–10):

Código Tipo Abrev.
01 Factura Electrónica FE
02 Nota de Débito Electrónica ND
03 Nota de Crédito Electrónica NC
04 Tiquete Electrónico TE
05 Confirmación Aceptación Total CA
06 Confirmación Aceptación Parcial CP
07 Confirmación Rechazo CR
08 Factura Electrónica de Compra FEC
09 Factura Electrónica de Exportación FEE
10 Recibo Electrónico de Pago REP

Las secuencias se crean automáticamente al emitir el primer comprobante de cada tipo. Si nunca se ha emitido una FEC (tipo 08), no existirá secuencia para ella — eso es normal.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences?include_all_terminals=" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences"
);

const params = {
    "include_all_terminals": "0",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'include_all_terminals' => '0',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Secuencias de la terminal del integrador):


{
    "success": true,
    "data": [
        {
            "id": 42,
            "local_code": "001",
            "terminal_code": "00002",
            "voucher_type": "01",
            "voucher_type_label": "Factura Electrónica",
            "voucher_type_abbr": "FE",
            "current_sequence": 9992,
            "next_sequence": 9993,
            "last_consecutive": "00100002010000009992",
            "next_consecutive": "00100002010000009993",
            "last_issued_at": "2026-04-20T14:30:00-06:00",
            "last_issued_key": null,
            "is_active": true,
            "is_near_rollover": false,
            "display_key": "001-00002-01"
        }
    ],
    "message": "",
    "errors": null,
    "terminal": "00002"
}
 

Example response (200, Sin secuencias (cliente recién integrado)):


{
    "success": true,
    "data": [],
    "message": "No hay secuencias creadas aún para la terminal 00002.",
    "errors": null,
    "terminal": "00002"
}
 

Example response (403, Plan no es Integrador):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Example response (404, Cliente no encontrado):


{
    "success": false,
    "data": null,
    "message": "Cliente no encontrado.",
    "errors": null
}
 

Request      

GET api/v1/public/my-contributors/{id}/sequences

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

Query Parameters

include_all_terminals   boolean  optional    

Si true, incluye secuencias de TODAS las terminales del cliente (diagnóstico). Default: false. Example: false

Inicializar consecutivo para un tipo de comprobante

requires authentication

Crea una secuencia con un valor inicial para un tipo de comprobante que aún no tiene secuencia en la terminal del integrador. Crítico para migración desde otro facturador.

Caso de uso: el cliente usaba Alegra y su última FE fue la 8475. Si AlmendroFEC emite desde 1, Hacienda rechaza por "consecutivo duplicado". Con este endpoint, setee current_sequence=8475 para el tipo 01 → el próximo comprobante usará 8476.

Importante: si la secuencia ya existe (porque ya se emitió al menos un comprobante de ese tipo), use PUT /sequences/{seqId} para ajustarla.

Seguridad: Requiere confirmación de contraseña.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"voucher_type\": \"01\",
    \"current_sequence\": 8475,
    \"reason\": \"Migración desde Alegra — último consecutivo tipo 01 fue 8475.\",
    \"password\": \"mi_contraseña\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "voucher_type": "01",
    "current_sequence": 8475,
    "reason": "Migración desde Alegra — último consecutivo tipo 01 fue 8475.",
    "password": "mi_contraseña"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'voucher_type' => '01',
            'current_sequence' => 8475,
            'reason' => 'Migración desde Alegra — último consecutivo tipo 01 fue 8475.',
            'password' => 'mi_contraseña',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Secuencia inicializada):


{
    "success": true,
    "data": {
        "id": 100,
        "local_code": "001",
        "terminal_code": "00002",
        "voucher_type": "01",
        "voucher_type_label": "Factura Electrónica",
        "voucher_type_abbr": "FE",
        "current_sequence": 8475,
        "next_sequence": 8476,
        "last_consecutive": "00100002010000008475",
        "next_consecutive": "00100002010000008476",
        "last_issued_at": null,
        "last_issued_key": null,
        "is_active": true,
        "is_near_rollover": false,
        "display_key": "001-00002-01"
    },
    "message": "Secuencia tipo 01 (FE) inicializada en 8475. Próximo comprobante usará 8476.",
    "errors": null
}
 

Example response (422, Secuencia ya existe):


{
    "success": false,
    "data": null,
    "message": "Ya existe una secuencia para el tipo 01 en la terminal 00002. Use PUT para ajustarla.",
    "errors": {
        "voucher_type": [
            "Ya existe. Use PUT /sequences/{id} para ajustar."
        ]
    }
}
 

Example response (422, Contraseña incorrecta):


{
    "success": false,
    "data": null,
    "message": "La contraseña es incorrecta.",
    "errors": {
        "password": [
            "La contraseña proporcionada no coincide con la del usuario autenticado."
        ]
    }
}
 

Request      

POST api/v1/public/my-contributors/{id}/sequences

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

Body Parameters

voucher_type   string     

Tipo de comprobante: 01 (FE), 02 (ND), 03 (NC), 04 (TE), 08 (FEC), 09 (FEE), 10 (REP). Example: 01

current_sequence   integer     

Último consecutivo emitido en el facturador anterior. Próximo = este + 1. Rango: 0–9,999,999,999. Example: 8475

reason   string     

Motivo (mín. 10 chars, bitácora Art. 6). Example: Migración desde Alegra — último consecutivo tipo 01 fue 8475.

password   string     

Contraseña actual del usuario. Example: mi_contraseña

Ajustar el consecutivo de una secuencia del cliente

requires authentication

Cambia manualmente el current_sequence de una secuencia. El próximo comprobante emitido usará current_sequence + 1.

Cuándo usar:

Seguridad: Requiere confirmación de contraseña.

⚠️ Setear un valor menor al actual puede generar consecutivos duplicados que Hacienda rechazará. Se permite pero se registra advertencia en la bitácora.

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences/42" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"current_sequence\": 9992,
    \"reason\": \"Migración desde Alegra — último consecutivo tipo 01 fue 9992.\",
    \"password\": \"mi_contraseña\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences/42"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "current_sequence": 9992,
    "reason": "Migración desde Alegra — último consecutivo tipo 01 fue 9992.",
    "password": "mi_contraseña"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/sequences/42';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'current_sequence' => 9992,
            'reason' => 'Migración desde Alegra — último consecutivo tipo 01 fue 9992.',
            'password' => 'mi_contraseña',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Secuencia ajustada):


{
    "success": true,
    "data": {
        "id": 42,
        "local_code": "001",
        "terminal_code": "00002",
        "voucher_type": "01",
        "voucher_type_label": "Factura Electrónica",
        "voucher_type_abbr": "FE",
        "current_sequence": 9992,
        "next_sequence": 9993,
        "last_consecutive": "00100002010000009992",
        "next_consecutive": "00100002010000009993",
        "last_issued_at": null,
        "last_issued_key": null,
        "is_active": true,
        "is_near_rollover": false,
        "display_key": "001-00002-01"
    },
    "message": "Consecutivo ajustado: tipo 01 (FE) actualizado de 0 a 9992. Próximo comprobante usará 9993.",
    "errors": null
}
 

Example response (403, Secuencia de otra terminal):


{
    "success": false,
    "data": null,
    "message": "Solo puede ajustar secuencias de su propia terminal (00002).",
    "errors": null
}
 

Example response (404, Secuencia no encontrada):


{
    "success": false,
    "data": null,
    "message": "Secuencia no encontrada para este cliente.",
    "errors": null
}
 

Example response (422, Contraseña incorrecta):


{
    "success": false,
    "data": null,
    "message": "La contraseña es incorrecta.",
    "errors": {
        "password": [
            "La contraseña proporcionada no coincide con la del usuario autenticado."
        ]
    }
}
 

Request      

PUT api/v1/public/my-contributors/{id}/sequences/{seqId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

seqId   integer     

ID de la secuencia (del listado). Example: 42

Body Parameters

current_sequence   integer     

Último consecutivo emitido. Próximo = este + 1. Rango: 0–9,999,999,999. Example: 9992

reason   string     

Motivo del ajuste (mín. 10 chars, bitácora art. 6). Example: Migración desde Alegra — último consecutivo tipo 01 fue 9992.

password   string     

Contraseña actual del usuario (confirmación de seguridad). Example: mi_contraseña

Cambiar la terminal asignada al integrador para este cliente

requires authentication

Modifica la assigned_terminal en la relación de gestión. La terminal ocupa la posición 04-08 del NumeroConsecutivo (20 dígitos).

Cuándo usar:

migrate_sequences:

Seguridad: Requiere confirmación de contraseña.

Las secuencias de la terminal anterior NO se eliminan — los vouchers ya emitidos las referencian.

Example request:
curl --request PUT \
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/terminal" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"assigned_terminal\": \"00003\",
    \"local_code\": \"001\",
    \"migrate_sequences\": true,
    \"reason\": \"Terminal 00002 comprometida con otro facturador.\",
    \"password\": \"mi_contraseña\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/terminal"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "assigned_terminal": "00003",
    "local_code": "001",
    "migrate_sequences": true,
    "reason": "Terminal 00002 comprometida con otro facturador.",
    "password": "mi_contraseña"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/terminal';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'assigned_terminal' => '00003',
            'local_code' => '001',
            'migrate_sequences' => true,
            'reason' => 'Terminal 00002 comprometida con otro facturador.',
            'password' => 'mi_contraseña',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Terminal cambiada con migración):


{
    "success": true,
    "data": {
        "previous_terminal": "00002",
        "new_terminal": "00003",
        "sequences_migrated": 3,
        "sequences": [
            {
                "id": 100,
                "local_code": "001",
                "terminal_code": "00003",
                "voucher_type": "01",
                "voucher_type_label": "Factura Electrónica",
                "voucher_type_abbr": "FE",
                "current_sequence": 9992,
                "next_sequence": 9993,
                "last_consecutive": "00100003010000009992",
                "next_consecutive": "00100003010000009993",
                "last_issued_at": null,
                "last_issued_key": null,
                "is_active": true,
                "is_near_rollover": false,
                "display_key": "001-00003-01"
            }
        ]
    },
    "message": "Terminal cambiada de 00002 a 00003. Se migraron 3 secuencias.",
    "errors": null
}
 

Example response (200, Terminal cambiada sin migración):


{
    "success": true,
    "data": {
        "previous_terminal": "00002",
        "new_terminal": "00003",
        "sequences_migrated": 0,
        "sequences": []
    },
    "message": "Terminal cambiada de 00002 a 00003. Las secuencias se crearán al emitir.",
    "errors": null
}
 

Example response (422, Terminal colisiona con otro integrador):


{
    "success": false,
    "data": null,
    "message": "La terminal 00003 ya está asignada a otro integrador de este cliente.",
    "errors": {
        "assigned_terminal": [
            "La terminal 00003 ya está asignada a otro integrador de este cliente."
        ]
    }
}
 

Example response (422, Contraseña incorrecta):


{
    "success": false,
    "data": null,
    "message": "La contraseña es incorrecta.",
    "errors": {
        "password": [
            "La contraseña proporcionada no coincide con la del usuario autenticado."
        ]
    }
}
 

Request      

PUT api/v1/public/my-contributors/{id}/terminal

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

UUID del cliente gestionado. Example: 019d867d-c001-7288-8ece-fd64da756c01

Body Parameters

assigned_terminal   string     

Nueva terminal de 5 dígitos (00002–99999). Example: 00003

local_code   string  optional    

Código de sucursal de 3 dígitos numéricos (posición 01-03 del NumeroConsecutivo, pág. 65 Anexos). Rango 001–999. 001 = casa matriz. Opcional — si no se envía, se mantiene el actual. Example: 001

migrate_sequences   boolean  optional    

Copiar secuencias de la terminal anterior. Default: false. Example: true

reason   string     

Motivo del cambio (mín. 10 chars, bitácora art. 6). Example: Terminal 00002 comprometida con otro facturador.

password   string     

Contraseña actual del usuario (confirmación de seguridad). Example: mi_contraseña

Mis Integradores

Listar los integradores que actualmente gestionan al contribuyente autenticado.

requires authentication

Devuelve las relaciones managed ACTIVAS (unlinked_at IS NULL) donde el tenant autenticado es el client_contributor_id. Cada entrada incluye el integrador, terminal asignada, grants activos por ambiente, retention_months_override y plantilla PDF default.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-integrators?per_page=15&page=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-integrators"
);

const params = {
    "per_page": "15",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-integrators';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '15',
            'page' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Integradores activos):


{
    "success": true,
    "data": [
        {
            "relationship_id": "019d870a-0001-7288-8ece-fd64da75a001",
            "integrator": {
                "id": "019d865a-1234-7000-a000-abcdef123456",
                "legal_name": "Sistemas Integrados S.A.",
                "id_number": "3101999999"
            },
            "assigned_terminal": "00002",
            "linked_at": "2026-04-10T09:00:00-06:00",
            "grants": [
                {
                    "environment": "sandbox",
                    "is_active": true
                }
            ]
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1
    }
}
 

Example response (200, Sin integradores):


{
    "success": true,
    "data": [],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 0
    }
}
 

Request      

GET api/v1/public/my-integrators

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Resultados por página (1-100, default 15). Example: 15

page   integer  optional    

Número de página. Example: 1

Consultar detalle de un integrador específico que gestiona al cliente.

requires authentication

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-integrators/9c1b4e5a-..." \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-integrators/9c1b4e5a-..."
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-integrators/9c1b4e5a-...';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Detalle del integrador):


{
    "success": true,
    "data": {
        "relationship_id": "019d870a-0001-7288-8ece-fd64da75a001",
        "integrator": {
            "id": "019d865a-1234-7000-a000-abcdef123456",
            "legal_name": "Sistemas Integrados S.A.",
            "id_number": "3101999999",
            "id_type": "02",
            "emails": [
                "soporte@integrador.cr"
            ]
        },
        "assigned_terminal": "00002",
        "retention_months_override": null,
        "default_pdf_template": null,
        "linked_at": "2026-04-10T09:00:00-06:00",
        "grants": [
            {
                "id": "019d870b-0001-7288-8ece-fd64da75b001",
                "environment": "sandbox",
                "is_active": true,
                "certificate": {
                    "environment": "sandbox",
                    "is_active": true,
                    "valid_until": "2028-01-01T00:00:00-06:00"
                }
            }
        ]
    },
    "message": "",
    "errors": null
}
 

Example response (404, Integrador no encontrado):


{
    "success": false,
    "data": null,
    "message": "Integrador no encontrado.",
    "errors": null
}
 

Request      

GET api/v1/public/my-integrators/{integratorContributorId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

integratorContributorId   string     

UUID del integrador. Example: 9c1b4e5a-...

Liberarse de un integrador específico.

requires authentication

El cliente rompe el vínculo managed con un integrador concreto. Efectos (ver UnlinkManagedRelationshipAction):

· ManagedRelationship.unlinked_at = now() con metadata de auditoría. · Cascade revoke de TODOS los grants activos del integrador sobre los .p12 del cliente (vía RevokeGrantAction, escenario GrantAutoRevoked → notifica al integrador). · Otros integradores del cliente NO se afectan (modelo N:N). · El contributor del cliente permanece intacto — NO se soft-deletea.

Irreversible: si ambas partes quieren retomar, el integrador debe iniciar una nueva IntegratorAccessRequest desde cero. La nueva relación recibirá una terminal distinta.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/my-integrators/9c1b4e5a-..." \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reason\": \"Cambio de proveedor tecnológico.\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-integrators/9c1b4e5a-..."
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason": "Cambio de proveedor tecnológico."
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-integrators/9c1b4e5a-...';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'reason' => 'Cambio de proveedor tecnológico.',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Integrador desvinculado):


{
    "success": true,
    "data": null,
    "message": "Se liberó del integrador. Los accesos asociados fueron revocados.",
    "errors": null
}
 

Example response (404, Integrador no encontrado):


{
    "success": false,
    "data": null,
    "message": "Integrador no encontrado.",
    "errors": null
}
 

Request      

DELETE api/v1/public/my-integrators/{integratorContributorId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

integratorContributorId   string     

UUID del integrador del que se libera. Example: 9c1b4e5a-...

Body Parameters

reason   string  optional    

Motivo opcional (máx 500 chars). Example: Cambio de proveedor tecnológico.

Solicitudes de Acceso

Crear una solicitud de acceso hacia un cliente.

requires authentication

El integrador envia la cedula del cliente y los ambientes a los que desea acceder. Se crea una solicitud en estado pending y se notifica al cliente por email y notificacion en el portal para que acepte o rechace.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/access-requests" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id_number\": \"3101000050\",
    \"environments\": [
        \"sandbox\"
    ],
    \"message\": \"Hola, somos su proveedor tecnológico y gustaría gestionar su facturación.\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/access-requests"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id_number": "3101000050",
    "environments": [
        "sandbox"
    ],
    "message": "Hola, somos su proveedor tecnológico y gustaría gestionar su facturación."
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/access-requests';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'client_id_number' => '3101000050',
            'environments' => [
                'sandbox',
            ],
            'message' => 'Hola, somos su proveedor tecnológico y gustaría gestionar su facturación.',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Solicitud creada):


{
    "success": true,
    "data": {
        "id": 1,
        "status": "pending",
        "status_label": "Pendiente",
        "status_color": "warning",
        "is_pending": true,
        "is_final": false,
        "resulted_in_grants": false,
        "was_fully_accepted": false,
        "can_be_responded": true,
        "can_be_cancelled": true,
        "requested_environments": [
            "sandbox",
            "production"
        ],
        "accepted_environments": null,
        "message": "Solicitamos acceso para integrar su facturación con nuestro POS.",
        "rejection_reason": null,
        "client": {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "legal_name": "Hotel Las Palmas S.A.",
            "commercial_name": "Las Palmas",
            "id_type": "02",
            "id_number": "3101456789",
            "display_name": "Las Palmas"
        },
        "integrator": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "commercial_name": "SistemasPOS",
            "id_type": "02",
            "id_number": "3101999888",
            "display_name": "SistemasPOS"
        },
        "requested_at": "2026-04-16T10:00:00-06:00",
        "responded_at": null,
        "created_at": "2026-04-16T10:00:00-06:00"
    },
    "message": "Solicitud de acceso creada. El cliente recibirá un email y notificación en su portal.",
    "errors": null
}
 

Example response (422, Error de validación):


{
    "success": false,
    "data": null,
    "message": "Ya existe una solicitud pendiente para este cliente.",
    "errors": null
}
 

Request      

POST api/v1/public/access-requests

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id_number   string     

Cédula del cliente al que se solicita acceso. Debe corresponder a un Contributor existente en la plataforma. Must match the regex /^[0-9A-Z-]+$/i. validation.min validation.max. Example: 3101000050

environments   string[]     

Cada ambiente debe ser "sandbox" o "production".

Must be one of:
  • sandbox
  • production
message   string  optional    

Mensaje opcional al cliente (máx 500 caracteres). Se muestra en el email + dashboard al revisar la solicitud. validation.max. Example: Hola, somos su proveedor tecnológico y gustaría gestionar su facturación.

Response

Response Fields

data   object     
id   integer     

ID numérico de la solicitud de acceso.

status   string     

Estado actual: pending, accepted, partially_accepted, rejected, cancelled.

status_label   string     

Nombre legible del estado en español.

status_color   string     

Color sugerido para la UI: warning, success, danger, secondary.

is_pending   boolean     

true si la solicitud aún espera respuesta del cliente.

is_final   boolean     

true si la solicitud ya fue resuelta (aceptada, rechazada o cancelada).

resulted_in_grants   boolean     

true si la aceptación generó al menos un acceso (CertificateAccessGrant).

was_fully_accepted   boolean     

true si todos los ambientes solicitados fueron aceptados. false si fue parcial o rechazada.

can_be_responded   boolean     

true si el cliente aún puede aceptar/rechazar (estado pendiente).

can_be_cancelled   boolean     

true si el integrador puede cancelar (estado pendiente).

requested_environments   string[]     

Ambientes solicitados por el integrador: ["sandbox"], ["production"] o ["sandbox", "production"].

accepted_environments   string[]     

Ambientes aceptados por el cliente. null si pendiente o rechazada. Puede ser subconjunto de requested_environments (aceptación parcial).

message   string     

Mensaje opcional del integrador al cliente justificando la solicitud. null si no se envió.

rejection_reason   string     

Razón del rechazo proporcionada por el cliente. null si no aplica.

client   object     

Datos del cliente (receptor de la solicitud).

id   string     

UUID del contribuyente cliente.

legal_name   string     

Razón social del cliente.

commercial_name   string     

Nombre comercial. null si no tiene.

id_type   string     

Tipo de identificación: 01-06.

id_number   string     

Número de identificación.

display_name   string     

Nombre para mostrar (commercial_name o legal_name).

integrator   object     

Datos del integrador (emisor de la solicitud). Misma estructura que client.

requested_at   string     

Fecha/hora en que se creó la solicitud (ISO 8601).

responded_at   string     

Fecha/hora en que el cliente respondió (ISO 8601). null si pendiente.

created_at   string     

Fecha de creación del registro (ISO 8601).

grants_count   integer     

Cantidad de accesos (CertificateAccessGrant) generados al aceptar. Solo presente en solicitudes aceptadas.

Listar solicitudes de acceso enviadas por el integrador.

requires authentication

Retorna el historial completo de solicitudes enviadas por el integrador autenticado, incluyendo todos los estados (pendientes, aceptadas, rechazadas, canceladas). Paginacion de 15 por pagina.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/access-requests/sent?per_page=16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/access-requests/sent"
);

const params = {
    "per_page": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/access-requests/sent';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Solicitudes enviadas por el integrador):


{
    "success": true,
    "data": [
        {
            "id": 1,
            "status": "accepted",
            "status_label": "Aceptada",
            "status_color": "success",
            "is_pending": false,
            "is_final": true,
            "resulted_in_grants": true,
            "was_fully_accepted": true,
            "can_be_responded": false,
            "can_be_cancelled": false,
            "requested_environments": [
                "sandbox",
                "production"
            ],
            "accepted_environments": [
                "sandbox",
                "production"
            ],
            "message": "Solicitamos acceso para integrar su facturación.",
            "rejection_reason": null,
            "client": {
                "id": "019d867d-c001-7288-8ece-fd64da756c01",
                "legal_name": "Hotel Las Palmas S.A.",
                "display_name": "Las Palmas",
                "id_type": "02",
                "id_number": "3101456789",
                "commercial_name": "Las Palmas"
            },
            "integrator": {
                "id": "019d867d-0241-7288-8ece-fd64da75616d",
                "legal_name": "SistemasPOS de Costa Rica S.A.",
                "display_name": "SistemasPOS",
                "id_type": "02",
                "id_number": "3101999888",
                "commercial_name": "SistemasPOS"
            },
            "requested_at": "2026-04-10T10:00:00-06:00",
            "responded_at": "2026-04-11T08:00:00-06:00",
            "created_at": "2026-04-10T10:00:00-06:00",
            "grants_count": 2
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/access-requests/sent

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

Filtrar por estado: pending, accepted, partially_accepted, rejected, cancelled.

per_page   integer  optional    

Resultados por pagina (1-50). Default: 15. Example: 16

Listar solicitudes de acceso recibidas por el cliente.

requires authentication

El cliente ve que integradores le han solicitado acceso a sus certificados digitales. Las solicitudes pendientes aparecen primero.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/access-requests/received?per_page=16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/access-requests/received"
);

const params = {
    "per_page": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/access-requests/received';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Solicitudes recibidas por el cliente):


{
    "success": true,
    "data": [
        {
            "id": 2,
            "status": "pending",
            "status_label": "Pendiente",
            "status_color": "warning",
            "is_pending": true,
            "is_final": false,
            "resulted_in_grants": false,
            "was_fully_accepted": false,
            "can_be_responded": true,
            "can_be_cancelled": false,
            "requested_environments": [
                "sandbox"
            ],
            "accepted_environments": null,
            "message": "Necesitamos acceso para pruebas de integración.",
            "rejection_reason": null,
            "client": {
                "id": "019d867d-c001-7288-8ece-fd64da756c01",
                "legal_name": "Hotel Las Palmas S.A.",
                "display_name": "Las Palmas",
                "id_type": "02",
                "id_number": "3101456789",
                "commercial_name": "Las Palmas"
            },
            "integrator": {
                "id": "019d867d-0241-7288-8ece-fd64da75616d",
                "legal_name": "SistemasPOS de Costa Rica S.A.",
                "display_name": "SistemasPOS",
                "id_type": "02",
                "id_number": "3101999888",
                "commercial_name": "SistemasPOS"
            },
            "requested_at": "2026-04-15T14:00:00-06:00",
            "responded_at": null,
            "created_at": "2026-04-15T14:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/access-requests/received

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

Filtrar por estado.

per_page   integer  optional    

Resultados por pagina (1-50). Default: 15. Example: 16

Aceptar una solicitud de acceso (total o parcialmente).

requires authentication

El cliente elige que ambientes autoriza. Se crean los accesos correspondientes con terminal asignada. El integrador recibe notificacion por email y en su portal.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/access-requests/16/accept" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"rejection_reason\": \"Ya tenemos un proveedor asignado para esta tarea.\",
    \"accepted_environments\": [
        \"sandbox\"
    ]
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/access-requests/16/accept"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "rejection_reason": "Ya tenemos un proveedor asignado para esta tarea.",
    "accepted_environments": [
        "sandbox"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/access-requests/16/accept';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'rejection_reason' => 'Ya tenemos un proveedor asignado para esta tarea.',
            'accepted_environments' => [
                'sandbox',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Solicitud aceptada completamente):


{
    "success": true,
    "data": {
        "id": 1,
        "status": "accepted",
        "status_label": "Aceptada",
        "status_color": "success",
        "is_pending": false,
        "is_final": true,
        "resulted_in_grants": true,
        "was_fully_accepted": true,
        "can_be_responded": false,
        "can_be_cancelled": false,
        "requested_environments": [
            "sandbox",
            "production"
        ],
        "accepted_environments": [
            "sandbox",
            "production"
        ],
        "message": "Solicitamos acceso para integrar su facturación.",
        "rejection_reason": null,
        "client": {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "display_name": "Las Palmas",
            "legal_name": "Hotel Las Palmas S.A.",
            "commercial_name": "Las Palmas",
            "id_type": "02",
            "id_number": "3101456789"
        },
        "integrator": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "display_name": "SistemasPOS",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "commercial_name": "SistemasPOS",
            "id_type": "02",
            "id_number": "3101999888"
        },
        "requested_at": "2026-04-10T10:00:00-06:00",
        "responded_at": "2026-04-16T11:00:00-06:00",
        "created_at": "2026-04-10T10:00:00-06:00",
        "grants_count": 2
    },
    "message": "Solicitud aceptada correctamente. El integrador recibirá notificación y email.",
    "errors": null
}
 

Example response (422, Error de validación):


{
    "success": false,
    "data": null,
    "message": "Solo se pueden responder solicitudes en estado pendiente.",
    "errors": null
}
 

Request      

POST api/v1/public/access-requests/{id}/accept

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la solicitud a aceptar. Example: 16

Body Parameters

rejection_reason   string  optional    

OPCIONAL en /reject. Motivo del rechazo (texto libre, máx 500 chars). IGNORADO en /accept. validation.max. Example: Ya tenemos un proveedor asignado para esta tarea.

accepted_environments   string[]  optional    

Cada ambiente debe ser "sandbox" o "production".

Rechazar una solicitud de acceso.

requires authentication

No se crean accesos. El integrador recibe notificacion con el motivo opcional del rechazo.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/access-requests/16/reject" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"rejection_reason\": \"Ya tenemos un proveedor asignado para esta tarea.\",
    \"accepted_environments\": [
        \"sandbox\"
    ]
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/access-requests/16/reject"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "rejection_reason": "Ya tenemos un proveedor asignado para esta tarea.",
    "accepted_environments": [
        "sandbox"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/access-requests/16/reject';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'rejection_reason' => 'Ya tenemos un proveedor asignado para esta tarea.',
            'accepted_environments' => [
                'sandbox',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Solicitud rechazada):


{
    "success": true,
    "data": {
        "id": 3,
        "status": "rejected",
        "status_label": "Rechazada",
        "status_color": "danger",
        "is_pending": false,
        "is_final": true,
        "resulted_in_grants": false,
        "was_fully_accepted": false,
        "can_be_responded": false,
        "can_be_cancelled": false,
        "requested_environments": [
            "sandbox",
            "production"
        ],
        "accepted_environments": null,
        "message": "Solicitamos acceso para integrar su facturación.",
        "rejection_reason": "Ya contamos con otro proveedor de integración.",
        "client": {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "display_name": "Las Palmas",
            "legal_name": "Hotel Las Palmas S.A.",
            "commercial_name": "Las Palmas",
            "id_type": "02",
            "id_number": "3101456789"
        },
        "integrator": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "display_name": "SistemasPOS",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "commercial_name": "SistemasPOS",
            "id_type": "02",
            "id_number": "3101999888"
        },
        "requested_at": "2026-04-10T10:00:00-06:00",
        "responded_at": "2026-04-16T12:00:00-06:00",
        "created_at": "2026-04-10T10:00:00-06:00"
    },
    "message": "Solicitud rechazada. El integrador recibirá notificación.",
    "errors": null
}
 

Request      

POST api/v1/public/access-requests/{id}/reject

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la solicitud a rechazar. Example: 16

Body Parameters

rejection_reason   string  optional    

OPCIONAL en /reject. Motivo del rechazo (texto libre, máx 500 chars). IGNORADO en /accept. validation.max. Example: Ya tenemos un proveedor asignado para esta tarea.

accepted_environments   string[]  optional    

Cada ambiente debe ser "sandbox" o "production".

Cancelar una solicitud de acceso antes de que el cliente responda.

requires authentication

Solo se puede cancelar si la solicitud esta en estado pending. El cliente recibe una notificacion informativa.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/access-requests/16/cancel" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/access-requests/16/cancel"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/access-requests/16/cancel';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Solicitud cancelada):


{
    "success": true,
    "data": {
        "id": 4,
        "status": "cancelled",
        "status_label": "Cancelada",
        "status_color": "secondary",
        "is_pending": false,
        "is_final": true,
        "resulted_in_grants": false,
        "was_fully_accepted": false,
        "can_be_responded": false,
        "can_be_cancelled": false,
        "requested_environments": [
            "sandbox"
        ],
        "accepted_environments": null,
        "message": null,
        "rejection_reason": null,
        "client": {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "display_name": "Las Palmas",
            "legal_name": "Hotel Las Palmas S.A.",
            "commercial_name": "Las Palmas",
            "id_type": "02",
            "id_number": "3101456789"
        },
        "integrator": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "display_name": "SistemasPOS",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "commercial_name": "SistemasPOS",
            "id_type": "02",
            "id_number": "3101999888"
        },
        "requested_at": "2026-04-14T09:00:00-06:00",
        "responded_at": "2026-04-16T13:00:00-06:00",
        "created_at": "2026-04-14T09:00:00-06:00"
    },
    "message": "Solicitud cancelada correctamente.",
    "errors": null
}
 

Example response (404, No encontrada):


{
    "success": false,
    "data": null,
    "message": "Solicitud no encontrada.",
    "errors": null
}
 

Example response (422, Estado no permite cancelación):


{
    "success": false,
    "data": null,
    "message": "La solicitud no puede cancelarse en su estado actual (accepted). Solo se pueden cancelar solicitudes pendientes.",
    "errors": null
}
 

Request      

POST api/v1/public/access-requests/{id}/cancel

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la solicitud a cancelar. Example: 16

Grants de Certificados

Listar los accesos otorgados sobre los certificados del cliente.

requires authentication

El cliente ve que integradores tienen permiso de firmar con sus certificados digitales y en que ambiente. Incluye accesos activos y revocados (historial completo). Los activos se muestran primero.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/certificate-grants?active=&per_page=16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/certificate-grants"
);

const params = {
    "active": "0",
    "per_page": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/certificate-grants';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'active' => '0',
            'per_page' => '16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Accesos del cliente):


{
    "success": true,
    "data": [
        {
            "id": 1,
            "assigned_terminal": "00002",
            "is_active": true,
            "is_revoked": false,
            "certificate": {
                "id": "019d867d-d001-7288-8ece-fd64da756d01",
                "environment": "sandbox",
                "is_active": true,
                "is_expired": false,
                "days_remaining": 1440,
                "valid_from": "2026-01-01T00:00:00-06:00",
                "valid_until": "2030-01-01T00:00:00-06:00"
            },
            "client": {
                "id": "019d867d-c001-7288-8ece-fd64da756c01",
                "legal_name": "Hotel Las Palmas S.A.",
                "commercial_name": "Las Palmas",
                "id_type": "02",
                "id_number": "3101456789",
                "display_name": "Las Palmas"
            },
            "integrator": {
                "id": "019d867d-0241-7288-8ece-fd64da75616d",
                "legal_name": "SistemasPOS de Costa Rica S.A.",
                "commercial_name": "SistemasPOS",
                "id_type": "02",
                "id_number": "3101999888",
                "display_name": "SistemasPOS"
            },
            "access_request_id": 1,
            "granted_at": "2026-04-11T08:00:00-06:00",
            "revoked_at": null,
            "revoke_reason": null,
            "revoked_by": null,
            "created_at": "2026-04-11T08:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/certificate-grants

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

active   boolean  optional    

Filtrar: true = solo activos, false = solo revocados. Omitir para ver todos. Example: false

environment   string  optional    

Filtrar por ambiente: sandbox o production.

per_page   integer  optional    

Resultados por pagina (1-50). Default: 15. Example: 16

Response

Response Fields

data   object     
id   integer     

ID numérico del grant de acceso.

assigned_terminal   string     

Terminal de 5 dígitos asignada al integrador para el consecutivo del cliente (posiciones 11-15 del NumeroConsecutivo del XSD). Garantiza que múltiples integradores no colisionen en la numeración.

is_active   boolean     

true si el acceso está vigente y el integrador puede firmar con este certificado.

is_revoked   boolean     

true si el acceso fue revocado (por el cliente, el integrador o automáticamente al desactivar el certificado).

certificate   object     

Datos del certificado al que se otorgó acceso.

id   string     

UUID del certificado.

environment   string     

Ambiente: sandbox o production.

is_active   boolean     

true si el certificado está activo para firmar.

is_expired   boolean     

true si el certificado X.509 venció.

days_remaining   integer     

Días restantes de vigencia. 0 si venció.

valid_from   string     

Inicio de vigencia del X.509 (ISO 8601).

valid_until   string     

Fin de vigencia del X.509 (ISO 8601).

client   object     

Datos del cliente (dueño del certificado). Campos: id, legal_name, commercial_name, id_type, id_number, display_name.

integrator   object     

Datos del integrador (receptor del acceso). Misma estructura que client.

access_request_id   integer     

ID de la solicitud de acceso que originó este grant.

granted_at   string     

Fecha/hora en que se otorgó el acceso (ISO 8601).

revoked_at   string     

Fecha/hora de revocación (ISO 8601). null si activo.

revoke_reason   string     

Motivo de la revocación. null si activo o sin motivo.

revoked_by   object     

Contribuyente que revocó el acceso. null si activo. Campos: id, legal_name, display_name.

created_at   string     

Fecha de creación del registro (ISO 8601).

Revocar el acceso de un integrador a un certificado.

requires authentication

El integrador pierde la capacidad de firmar comprobantes con este certificado. Los comprobantes previamente emitidos siguen siendo validos. El integrador recibe notificacion de la revocacion.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/certificate-grants/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reason\": \"architecto\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/certificate-grants/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason": "architecto"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/certificate-grants/16';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'reason' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Acceso revocado):


{
    "success": true,
    "data": {
        "id": 1,
        "assigned_terminal": "00002",
        "is_active": false,
        "is_revoked": true,
        "certificate": {
            "id": "019d867d-d001-7288-8ece-fd64da756d01",
            "environment": "sandbox",
            "is_active": true,
            "is_expired": false,
            "days_remaining": 1440,
            "valid_from": "2026-01-01T00:00:00-06:00",
            "valid_until": "2030-01-01T00:00:00-06:00"
        },
        "client": {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "legal_name": "Hotel Las Palmas S.A.",
            "commercial_name": "Las Palmas",
            "id_type": "02",
            "id_number": "3101456789",
            "display_name": "Las Palmas"
        },
        "integrator": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "commercial_name": "SistemasPOS",
            "id_type": "02",
            "id_number": "3101999888",
            "display_name": "SistemasPOS"
        },
        "access_request_id": 1,
        "granted_at": "2026-04-11T08:00:00-06:00",
        "revoked_at": "2026-04-16T14:00:00-06:00",
        "revoke_reason": "Cambio de proveedor de integración.",
        "revoked_by": {
            "id": "019d867d-c001-7288-8ece-fd64da756c01",
            "legal_name": "Hotel Las Palmas S.A.",
            "display_name": "Las Palmas"
        },
        "created_at": "2026-04-11T08:00:00-06:00"
    },
    "message": "Acceso revocado correctamente. El integrador ya no puede emitir con este certificado.",
    "errors": null
}
 

Example response (422, Grant ya revocado):


{
    "success": false,
    "data": null,
    "message": "Este acceso ya fue revocado anteriormente.",
    "errors": null
}
 

Request      

DELETE api/v1/public/certificate-grants/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID del grant a revocar. Example: 16

Body Parameters

reason   string  optional    

Motivo opcional de la revocacion (max 500 caracteres). Example: architecto

Listar los accesos recibidos por el integrador autenticado.

requires authentication

El integrador ve sobre que certificados de que clientes tiene acceso activo (o revocado, según filtro). Cada grant incluye el cert, el cliente, la terminal asignada y el ambiente.

Opcionalmente filtrable por client_id para obtener los grants del integrador sobre un cliente específico — útil en el perfil del managed client para mostrar la sección "Mi acceso".

Requiere plan INTEGRATOR. Retorna 403 para cualquier otro plan. Espeja la semántica de PublicManagedContributorController.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/my-access?active=&per_page=16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-access"
);

const params = {
    "active": "0",
    "per_page": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-access';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'active' => '0',
            'per_page' => '16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Accesos del integrador):


{
    "success": true,
    "data": [
        {
            "id": 1,
            "assigned_terminal": "00002",
            "is_active": true,
            "is_revoked": false,
            "certificate": {
                "id": "019d867d-d001-7288-8ece-fd64da756d01",
                "environment": "sandbox",
                "is_active": true,
                "is_expired": false,
                "days_remaining": 1440,
                "valid_from": "2026-01-01T00:00:00-06:00",
                "valid_until": "2030-01-01T00:00:00-06:00"
            },
            "client": {
                "id": "019d867d-c001-7288-8ece-fd64da756c01",
                "legal_name": "Hotel Las Palmas S.A.",
                "commercial_name": "Las Palmas",
                "id_type": "02",
                "id_number": "3101456789",
                "display_name": "Las Palmas"
            },
            "integrator": {
                "id": "019d867d-0241-7288-8ece-fd64da75616d",
                "legal_name": "SistemasPOS de Costa Rica S.A.",
                "commercial_name": "SistemasPOS",
                "id_type": "02",
                "id_number": "3101999888",
                "display_name": "SistemasPOS"
            },
            "access_request_id": 1,
            "granted_at": "2026-04-11T08:00:00-06:00",
            "revoked_at": null,
            "revoke_reason": null,
            "revoked_by": null,
            "created_at": "2026-04-11T08:00:00-06:00"
        }
    ],
    "message": "",
    "errors": null,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 1
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Example response (403, Plan no permite gestionar clientes):


{
    "success": false,
    "data": null,
    "message": "Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.",
    "errors": null
}
 

Request      

GET api/v1/public/my-access

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

client_id   string  optional    

UUID del cliente managed para filtrar grants hacia ese cliente.

active   boolean  optional    

Filtrar: true = solo activos, false = solo revocados. Omitir para ver todos. Example: false

environment   string  optional    

Filtrar por ambiente: sandbox o production.

per_page   integer  optional    

Resultados por pagina (1-50). Default: 15. Example: 16

El integrador renuncia voluntariamente a un acceso que recibio.

requires authentication

Util cuando el integrador deja de gestionar al cliente y quiere limpiar su listado de accesos activos. El cliente es notificado de la renuncia.

Example request:
curl --request DELETE \
    "https://fe.almendro.cr/api/v1/public/my-access/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reason\": \"architecto\"
}"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/my-access/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason": "architecto"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-access/16';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'reason' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Renuncia exitosa):


{
    "success": true,
    "data": {
        "id": 3,
        "assigned_terminal": "00003",
        "is_active": false,
        "is_revoked": true,
        "certificate": {
            "id": "019d867d-d003-7288-8ece-fd64da756d03",
            "environment": "production",
            "is_active": true,
            "is_expired": false,
            "days_remaining": 1200,
            "valid_from": "2025-06-01T00:00:00-06:00",
            "valid_until": "2029-08-01T00:00:00-06:00"
        },
        "client": {
            "id": "019d867d-c002-7288-8ece-fd64da756c02",
            "legal_name": "Restaurante El Mango S.A.",
            "commercial_name": "El Mango",
            "id_type": "02",
            "id_number": "3101789012",
            "display_name": "El Mango"
        },
        "integrator": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "commercial_name": "SistemasPOS",
            "id_type": "02",
            "id_number": "3101999888",
            "display_name": "SistemasPOS"
        },
        "access_request_id": 2,
        "granted_at": "2026-03-15T10:00:00-06:00",
        "revoked_at": "2026-04-16T15:00:00-06:00",
        "revoke_reason": "Fin de contrato de servicio.",
        "revoked_by": {
            "id": "019d867d-0241-7288-8ece-fd64da75616d",
            "legal_name": "SistemasPOS de Costa Rica S.A.",
            "display_name": "SistemasPOS"
        },
        "created_at": "2026-03-15T10:00:00-06:00"
    },
    "message": "Ha renunciado al acceso correctamente. El cliente será notificado.",
    "errors": null
}
 

Request      

DELETE api/v1/public/my-access/{grantId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

grantId   integer     

ID del grant al que renuncia. Example: 16

Body Parameters

reason   string  optional    

Motivo opcional de la renuncia (max 500 caracteres). Example: architecto

Notificaciones

Listar notificaciones del usuario autenticado.

requires authentication

Retorna las notificaciones personales del usuario y las notificaciones generales (broadcasts) del contribuyente. Las no leidas aparecen primero, luego por fecha descendente. No incluye notificaciones expiradas.

La respuesta incluye unread_count fuera del array data para actualizar el badge de notificaciones sin una consulta adicional.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/notifications?unread_only=&per_page=16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/notifications"
);

const params = {
    "unread_only": "0",
    "per_page": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/notifications';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'unread_only' => '0',
            'per_page' => '16',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Lista con notificaciones):


{
    "success": true,
    "data": [
        {
            "id": 42,
            "type": "access_request_received",
            "type_label": "Solicitud de acceso recibida",
            "type_color": "info",
            "type_icon": "key",
            "type_category": "access_control",
            "title": "Nueva solicitud de acceso",
            "message": "SistemasPOS de Costa Rica S.A. solicita acceso a sus certificados digitales.",
            "action_url": "/portal/access-requests/1",
            "action_label": "Revisar solicitud",
            "has_action": true,
            "metadata": {
                "access_request_id": 1,
                "integrator_name": "SistemasPOS"
            },
            "is_read": false,
            "is_unread": true,
            "read_at": null,
            "requires_action": true,
            "is_broadcast": false,
            "is_expired": false,
            "created_at": "2026-04-16T10:00:00-06:00",
            "expires_at": null
        }
    ],
    "message": "",
    "errors": null,
    "unread_count": 3,
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "per_page": 15,
        "total": 5
    },
    "links": {
        "first": "...",
        "last": "...",
        "prev": null,
        "next": null
    }
}
 

Request      

GET api/v1/public/notifications

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

unread_only   boolean  optional    

Solo retornar notificaciones no leidas. Default: false. Example: false

type   string  optional    

Filtrar por tipo de notificacion (ej: access_request_received).

per_page   integer  optional    

Resultados por pagina (1-50). Default: 15. Example: 16

Response

Response Fields

data   object     
id   integer     

ID numérico de la notificación.

type   string     

Código del tipo de notificación (ej. access_request_received, access_request_accepted, grant_revoked, certificate_expiring).

type_label   string     

Nombre legible del tipo en español.

type_color   string     

Color sugerido para la UI: info, success, warning, danger, secondary.

type_icon   string     

Nombre del ícono sugerido para la UI (ej. key, check, alert-triangle).

type_category   string     

Categoría funcional: access_control, voucher, certificate, system.

title   string     

Título corto de la notificación.

message   string     

Mensaje descriptivo completo.

action_url   string     

Ruta del portal donde el usuario puede actuar (ej. /portal/access-requests/1). null si no requiere acción.

action_label   string     

Texto del botón de acción (ej. "Revisar solicitud"). null si no hay acción.

has_action   boolean     

true si la notificación tiene un enlace de acción.

metadata   object     

Datos adicionales específicos del tipo de notificación (IDs, nombres, etc.). Estructura varía por tipo.

is_read   boolean     

true si el usuario ya leyó la notificación.

is_unread   boolean     

Inverso de is_read. Útil para filtros en la UI.

read_at   string     

Fecha/hora en que se marcó como leída (ISO 8601). null si no leída.

requires_action   boolean     

true si la notificación espera una acción del usuario (ej. aceptar/rechazar solicitud).

is_broadcast   boolean     

true si es una notificación general del tenant (visible para todos los usuarios). false si es personal.

is_expired   boolean     

true si la notificación expiró y ya no es relevante.

created_at   string     

Fecha de creación (ISO 8601).

expires_at   string     

Fecha de expiración (ISO 8601). null si no expira.

unread_count   integer     

Total de notificaciones no leídas del usuario. Incluido fuera del array data para actualizar el badge del header sin consulta adicional.

Marcar todas las notificaciones no leidas como leidas.

requires authentication

Operacion en lote que marca todas las notificaciones pendientes del usuario como leidas. Retorna la cantidad de notificaciones que fueron marcadas (0 si ya estaban todas leidas).

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/notifications/read-all" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/notifications/read-all"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/notifications/read-all';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Notificaciones marcadas):


{
    "success": true,
    "data": {
        "marked_count": 5,
        "unread_count": 0
    },
    "message": "5 notificaciones marcadas como leídas.",
    "errors": null
}
 

Example response (200, Sin notificaciones pendientes):


{
    "success": true,
    "data": {
        "marked_count": 0,
        "unread_count": 0
    },
    "message": "No hay notificaciones pendientes.",
    "errors": null
}
 

Request      

POST api/v1/public/notifications/read-all

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Marcar una notificacion como leida.

requires authentication

Operacion idempotente: si la notificacion ya estaba leida, retorna 200 con el mismo estado sin error. La respuesta incluye la notificacion actualizada y el nuevo unread_count.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/notifications/16/read" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/notifications/16/read"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/notifications/16/read';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Notificación marcada como leída):


{
    "success": true,
    "data": {
        "notification": {
            "id": 42,
            "type": "access_request_received",
            "type_label": "Solicitud de acceso recibida",
            "type_color": "info",
            "type_icon": "key",
            "type_category": "access_control",
            "title": "Nueva solicitud de acceso",
            "message": "SistemasPOS solicita acceso a sus certificados.",
            "action_url": "/portal/access-requests/1",
            "action_label": "Revisar solicitud",
            "has_action": true,
            "metadata": {
                "access_request_id": 1
            },
            "is_read": true,
            "is_unread": false,
            "read_at": "2026-04-16T11:00:00-06:00",
            "requires_action": true,
            "is_broadcast": false,
            "is_expired": false,
            "created_at": "2026-04-16T10:00:00-06:00",
            "expires_at": null
        },
        "unread_count": 2
    },
    "message": "Notificación marcada como leída.",
    "errors": null
}
 

Example response (404, No encontrada):


{
    "success": false,
    "data": null,
    "message": "Notificación no encontrada.",
    "errors": null
}
 

Request      

POST api/v1/public/notifications/{id}/read

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID de la notificacion a marcar. Example: 16

Token API

Consultar metadatos del token API de integración

requires authentication

Devuelve información del token API actualmente activo del contribuyente (fecha de creación, último uso, permisos) sin exponer el valor del token. Si aún no ha generado un token API, retorna data: null.

Útil para:

Recuerde: este endpoint solo muestra metadatos. El valor del token solo se devuelve una vez, en la respuesta de POST /public/tokens.

Example request:
curl --request GET \
    --get "https://fe.almendro.cr/api/v1/public/tokens/current" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/tokens/current"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/tokens/current';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Token API existente):


{
    "success": true,
    "data": {
        "name": "api-integration",
        "created_at": "2026-04-01T10:00:00-06:00",
        "last_used_at": "2026-04-16T14:30:00-06:00",
        "abilities": [
            "*"
        ],
        "token_hint": "••••••••••••••••••••••••••••••••"
    },
    "message": "",
    "errors": null
}
 

Example response (200, Sin token API generado aún):


{
    "success": true,
    "data": null,
    "message": "No tiene un token API generado aún.",
    "errors": null
}
 

Example response (401, No autenticado):


{
    "success": false,
    "data": null,
    "message": "Unauthenticated.",
    "errors": null
}
 

Request      

GET api/v1/public/tokens/current

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Generar o regenerar el token API Bearer

requires authentication

Crea un nuevo token API permanente para uso desde sistemas externos (POS, ERP, e-commerce, CRM). Si el contribuyente ya tenía un token API, el anterior se revoca automáticamente al crear el nuevo.

Importante: el valor del token se devuelve en texto plano una única vez en esta respuesta, en el campo bearer_token. Guárdelo inmediatamente — no hay forma de recuperarlo después. Para saber si ya existe un token (sin exponerlo), use GET /public/tokens/current.

Aislamiento de sesiones: regenerar el token API no afecta a su sesión actual del portal. Puede regenerar el token sin cerrar la ventana del navegador.

Impacto en sistemas productivos: al regenerar, el token anterior deja de funcionar inmediatamente. Todas sus integraciones que usaban el token viejo empezarán a recibir HTTP 401 hasta que actualice la configuración con el nuevo token.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/tokens" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/tokens"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/tokens';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201, Token generado exitosamente):


{
    "success": true,
    "data": {
        "bearer_token": "3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
        "created_at": "2026-04-16T15:00:00-06:00"
    },
    "message": "Token generado. Guárdelo ahora — no se mostrará nuevamente.",
    "errors": null
}
 

Example response (401, No autenticado):


{
    "success": false,
    "data": null,
    "message": "Unauthenticated.",
    "errors": null
}
 

Request      

POST api/v1/public/tokens

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Response

Response Fields

data   object     
bearer_token   string     

Token API de integración en texto plano. Se muestra una única vez. Guárdelo de forma segura — no hay forma de recuperarlo. Use este token en el header Authorization: Bearer {token} para todas las operaciones automatizadas (POS, ERP, e-commerce).

created_at   string     

Fecha de creación del token (ISO 8601).

Solicitud de Upgrade

Solicitar upgrade al plan Integrador.

requires authentication

Envia un correo al usuario con las instrucciones de pago y notifica al equipo de AlmendroFEC para procesar la solicitud. No requiere cuerpo en el request — la identidad del contribuyente se obtiene del token de autenticacion.

Si el contribuyente ya tiene el plan Integrador activo, retorna 422.

Este endpoint tiene rate limit de 1 solicitud por hora para evitar envios duplicados.

Example request:
curl --request POST \
    "https://fe.almendro.cr/api/v1/public/upgrade-request" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://fe.almendro.cr/api/v1/public/upgrade-request"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/upgrade-request';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200, Solicitud enviada exitosamente):


{
    "success": true,
    "data": {
        "email_sent_to": "usuario@empresa.cr"
    },
    "message": "Solicitud enviada. Revisá tu correo para las instrucciones de pago.",
    "errors": null
}
 

Example response (404, Sin contribuyente asociado):


{
    "success": false,
    "data": null,
    "message": "No se encontró un contribuyente asociado a esta cuenta.",
    "errors": null
}
 

Example response (422, Ya tiene plan Integrador):


{
    "success": false,
    "data": null,
    "message": "Tu cuenta ya tiene el plan Integrador activo.",
    "errors": null
}
 

Request      

POST api/v1/public/upgrade-request

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json