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:
- Reglamento de Comprobantes Electrónicos (Decreto Ejecutivo N.° 41820-H)
- Resolución General sobre disposiciones técnicas (MH-DGT-RES-0019-2022)
- Anexos y Estructuras v4.4 de la Dirección General de Tributación
- Esquemas XSD v4.4 oficiales publicados por la DGT
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 Acceptedno 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 exento → TotalServExentos 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
cabys_code: 13 dígitos válidos. Verifique conGET /catalogs/cabys?search={código}.- Gravado (IVA > 0%):
taxesconcodigo+codigoTarifa+tarifa+monto, másimpuesto_netoybase_imponible. - Exento: tarifa
10con monto0.00000, Otaxes: []. monto=base_imponible × tarifa / 100(5 decimales).total_line_amount=sub_total + impuesto_neto.- 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:
- Listado con filtros:
GET /voucherspermite filtrar por estado, tipo, fecha, cédula del receptor, ambiente y moneda. Soporta múltiples valores separados por coma (ejemplo:?status=accepted,rejectedo?voucher_type=01,04). - Detalle individual:
GET /vouchers/{key}retorna todos los datos del comprobante incluyendo líneas de detalle, referencias, medios de pago, totales y el estado de Hacienda. - XML firmado:
GET /vouchers/{key}/xmldescarga el XML con firma digital tal como fue enviado a Hacienda. - Respuesta de Hacienda:
GET /vouchers/{key}/xml-responsedescarga el XML de respuesta (MensajeHacienda) que confirma la aceptación o el rechazo. - PDF:
GET /vouchers/{key}/pdfdescarga la representación gráfica del comprobante, generada según la plantilla PDF activa del contribuyente, con código QR conforme a la normativa vigente.
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:
- Envío automático: activar o desactivar el envío al aceptar el comprobante.
- Momento del envío: enviar solo cuando Hacienda acepta, o también al emitir.
- Adjuntos: incluir o excluir el XML y/o el PDF del correo.
- Reply-to: dirección personalizada para que las respuestas del receptor lleguen a su correo en lugar del correo del sistema.
- BCC: hasta 4 direcciones de copia oculta (por ejemplo, su departamento de contabilidad).
- Asunto personalizado: con placeholders dinámicos como
{tipo},{consecutivo},{receptor},{total},{moneda}y{emisor}. - Mensaje personalizado: texto que aparece en el cuerpo del correo antes de los datos del comprobante.
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.
- Resumen general (
/reports/summary): totales de ventas, impuestos y cantidad de comprobantes para el período seleccionado. - Ventas por período (
/reports/sales-by-period): evolución temporal agrupada por día, semana o mes. La agrupación se detecta automáticamente según el rango de fechas, pero puede forzarla con el parámetrogroup_by. - Ventas por receptor (
/reports/sales-by-receiver): ranking de los receptores con mayor volumen de ventas. - Ventas por actividad (
/reports/sales-by-activity): desglose por actividad económica CIIU del emisor. - Resumen de IVA (
/reports/tax-summary): desglose de IVA por tarifa, útil para preparar la declaración D-104. - Comprobantes por estado (
/reports/vouchers-by-status): distribución de comprobantes según su estado (aceptados, rechazados, pendientes, etc.). - Resumen de MensajeReceptor (
/reports/receiver-messages): estado de las confirmaciones pendientes con información de plazos.
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:
- CABYS (
/catalogs/cabys): catálogo de bienes y servicios con aproximadamente 20,500 códigos. Busque por texto o por prefijo numérico del código. Cada resultado incluye el código de 13 dígitos, la descripción, la tarifa de IVA asociada y si el bien es mercancía o servicio. - Actividades económicas (
/catalogs/activities): catálogo CIIU 4 con aproximadamente 800 actividades. El código de actividad es obligatorio en el campoissuer_activity_codede todos los comprobantes. - Ubicaciones (
/catalogs/locations): división territorial de Costa Rica (provincia, cantón, distrito). Consulta jerárquica: sin parámetros retorna las 7 provincias; con?province=1retorna los cantones de San José; con?province=1&canton=01retorna los distritos.
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:
- Autocompletar los datos del receptor al digitar la cédula en su formulario de emisión.
- Validar que la cédula existe antes de emitir, para evitar rechazos de Hacienda.
- Obtener las actividades económicas del receptor para el campo
receiver_activity_codede la Factura de Compra (tipo 08).
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:
- Verifica que el UUID pertenece a un cliente vinculado a su cuenta de integrador
- Genera la clave de 50 dígitos usando la cédula del cliente (no la del integrador)
- Construye el XML v4.4 con el cliente como emisor (
EmisorType) ante Hacienda - Firma digitalmente con el certificado
.p12del cliente - Asigna el consecutivo en los contadores del cliente, usando la terminal exclusiva del integrador
- 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:
- Solicitudes de acceso recibidas de un integrador.
- Respuestas a solicitudes de acceso enviadas.
- Certificados próximos a vencer.
- Cancelaciones de solicitudes.
- Revocaciones de acceso.
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 | Sí | 1 endpoint | 1 |
| Profesional | $19/mes o $182/año | 2,000 | $0.02/comp | 40 | Sí | 3 endpoints | 5 |
| Empresa | $39/mes o $351/año | 8,000 | $0.012/comp | 80 | Sí | 5 endpoints | 1 |
| Integrador | $79/mes o $711/año | 50,000 | $0.008/comp | 150 | Sí | 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 | Sí | No |
| Cuenta contra límite mensual | Sí | No |
| Envío de email al receptor | Sí | No (solo registro interno) |
| Marca de agua en PDF | Sin marca | "SANDBOX — SIN VALOR FISCAL" |
| Token y payload | El mismo | El mismo |
| Webhooks | Sí | Sí |
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
- País: siempre
506(Costa Rica) - Fecha: día, mes y año de emisión en formato DDMMAA
- Cédula: identificación del emisor, rellenada con ceros hasta 12 dígitos
- Consecutivo: 20 dígitos compuestos por sucursal (3), terminal (5), tipo (2) y secuencia (10)
- Situación:
1Normal,2Contingencia,3Sin Internet - Seguridad: 8 dígitos aleatorios generados por el sistema
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
- Documentación: fe.almendro.cr/docs
- Soporte técnico para desarrolladores: developers@almendro.cr
- Soporte general: soporte@almendro.cr
- Ventas e información comercial: ventas@almendro.cr
- Seguridad: seguridad@almendro.cr
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¤cy_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": "..."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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).
null cuando el tipo no requiere receptor (TE sin receptor, FEE sin receptor).id_type
string
Tipo de identificación del receptor: 01–06.
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:
- Lee el comprobante original por su clave.
- Construye automáticamente el payload de la NC (mismas líneas, mismo receptor) con la referencia correspondiente.
- La emite por el mismo pipeline que
POST /vouchers(valida XSD, firma, envía a Hacienda). - Retorna la NC generada con HTTP 202.
Restricciones:
- Solo comprobantes en estado
acceptedpueden anularse. - Solo FE (
01) y TE (04) son anulables con NC. - Un comprobante ya cancelado no puede re-cancelarse.
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"
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Solo comprobantes que ya pasaron la firma digital tienen XML.
- Comprobantes en
draftaún no tienen XML → 404. - Comprobantes cuyo XML fue purgado (retención expirada) → 410 Gone.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
Mensaje:1=Aceptado,3=Rechazado.DetalleMensaje: descripción del error (si fue rechazado).MontoTotalImpuesto: monto de impuesto validado por Hacienda.
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:
- Solo comprobantes que Hacienda ya procesó (estado
acceptedorejected) tienen este XML. - Comprobantes en tránsito (
pending,sent) → 404. - Si la retención expiró → 410 Gone.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Solo comprobantes que ya pasaron la firma (estado ≠
draft). - El contribuyente debe tener al menos una plantilla PDF activa marcada como default.
- El PDF permanece disponible incluso si el XML fue purgado — los metadatos y totales nunca se eliminan.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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.
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.
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)."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Ambiente (
sandbox/production). - Estado (
is_active) y fecha de desactivación si aplica. - Vigencia del certificado (
valid_from,valid_until). - Días restantes hasta la expiración (
days_remaining) — útil para monitorear renovación. - Subject del certificado X.509 (nombre del titular).
- Número de serie del certificado.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Se valida que el archivo sea un
.p12legítimo y que la contraseña lo abra correctamente. - Se extraen los metadatos del certificado X.509 (vigencia, subject, número de serie).
- Si ya existía un certificado activo del mismo ambiente, se desactiva automáticamente (queda en el histórico).
- 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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Si el certificado desactivado era el activo de su ambiente, la emisión en ese ambiente queda bloqueada hasta que suba un nuevo certificado activo.
- Si algún integrador tenía acceso delegado a este certificado, el acceso se revoca automáticamente y se le envía una notificación.
- El conteo de accesos revocados aparece en el
messagede la respuesta.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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, 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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Crear otra plantilla nueva, o
- 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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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 endpointGET /vouchers/{key}/pdfcon?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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Obtener el logo de una plantilla PDF
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).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/pdf-templates/1/logo" \
--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/logo"
);
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/1/logo';
$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, Logo disponible):
{
"Content-Type": "image/png",
"Cache-Control": "private, max-age=3600",
"X-Content-Type-Options": "nosniff",
"body": "(contenido binario de la imagen)"
}
Example response (404, Plantilla sin logo configurado):
{
"success": false,
"data": null,
"message": "Esta plantilla no tiene logo.",
"errors": null
}
Example response (404, Archivo de logo no encontrado en el servidor):
{
"success": false,
"data": null,
"message": "Archivo de logo no encontrado.",
"errors": null
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Subir o reemplazar el logo de una plantilla PDF
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:
- Extensiones permitidas:
png,jpg,jpeg,svg. - Tamaño máximo: 200 KB.
- Para logos que se verán bien en el PDF, use preferentemente
resolución de 300 DPI y dimensiones proporcionales a
layout.logo_max_heightconfigurado.
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}` } },
)
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/pdf-templates/1/logo" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "logo_file=@/tmp/php3okg9sr9ghif59DykAr" const url = new URL(
"https://fe.almendro.cr/api/v1/public/pdf-templates/1/logo"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('logo_file', document.querySelector('input[name="logo_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/pdf-templates/1/logo';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'logo_file',
'contents' => fopen('/tmp/php3okg9sr9ghif59DykAr', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Logo subido exitosamente):
{
"success": true,
"data": {
"id": 1,
"name": "Classic",
"is_default": true,
"paper_size": "letter",
"config_json": {
"colors": {},
"fonts": {},
"layout": {},
"qr": {}
},
"has_logo": true,
"is_active": true,
"created_at": "2026-04-01T10:00:00-06:00",
"updated_at": "2026-04-16T12:00:00-06:00"
},
"message": "Logo subido correctamente.",
"errors": null
}
Example response (422, Archivo faltante):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"logo_file": [
"El archivo del logo es obligatorio."
]
}
}
Example response (422, Extensión no permitida):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"logo_file": [
"Extensión no permitida. Use: png, jpg, jpeg, svg."
]
}
}
Example response (422, Archivo excede 200 KB):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"logo_file": [
"El logo no puede superar 200 KB."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Eliminar el logo de una plantilla PDF
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.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/pdf-templates/1/logo" \
--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/logo"
);
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/1/logo';
$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, Logo eliminado):
{
"success": true,
"data": {
"id": 1,
"name": "Classic",
"is_default": true,
"paper_size": "letter",
"config_json": {
"colors": {},
"fonts": {},
"layout": {},
"qr": {}
},
"has_logo": false,
"is_active": true,
"created_at": "2026-04-01T10:00:00-06:00",
"updated_at": "2026-04-16T13:00:00-06:00"
},
"message": "Logo eliminado correctamente.",
"errors": null
}
Example response (409, Sin logo configurado):
{
"success": false,
"data": null,
"message": "Esta plantilla no tiene logo configurado.",
"errors": null
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
id_type+id_numberdeben ser únicos por contribuyente.default_activity_codedebe estar en formato HaciendaXXXX.X(cuatro dígitos, punto, un dígito). Ejemplo:6121.0.emailses un arreglo con máximo 4 correos válidos.phonedebe tener entre 4 y 20 dígitos.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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.
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.
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)."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Crear un nuevo cliente con la misma combinación
id_type+id_numberdespués de eliminar uno. La restricción de unicidad solo aplica a registros activos (no eliminados). - Los comprobantes ya emitidos con este cliente no se ven afectados — los datos del receptor se copiaron al comprobante al momento de la emisión (inmutabilidad post-emisión).
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
cabys_codees obligatorio y debe existir en el catálogo CABYS vigente (13 dígitos).internal_codees opcional, pero si se envía debe ser único por contribuyente.- Si
tax_codees01(IVA) o07(IVA cálculo especial),tax_rate_codees obligatorio. - Si
tax_codese envía,tax_ratetambién es obligatorio. - Si
unit_of_measureesOtros,commercial_unites obligatorio.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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, 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).
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:
- Si cambia
internal_code, se verifica que no esté en uso por otro item del mismo contribuyente. - Si cambia
cabys_code, se valida contra el catálogo CABYS vigente. - Si
tax_codequeda en01o07pero no haytax_rate_code, la edición se rechaza. - Si
unit_of_measurequeda enOtrospero no haycommercial_unit, la edición se rechaza.
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'."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Crear un nuevo item con el mismo
internal_codedespués de eliminar uno. La restricción de unicidad solo aplica a items activos (no eliminados). - Los comprobantes ya emitidos con este item no se ven afectados — los datos de la línea se copiaron al comprobante al momento de la emisión (inmutabilidad post-emisión).
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
healthy— activo, sin fallos recientes.degraded— activo pero con fallos consecutivos (entre 1 y 9).disabled— desactivado automáticamente tras 10 fallos consecutivos.inactive— desactivado manualmente.never_used— activo, aún no se ha disparado ningún evento hacia él.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Debe empezar con
https://— no se acepta HTTP plano. - El certificado TLS debe ser válido (no auto-firmado).
- La URL debe responder POST en menos de 15 segundos.
- Evite URLs con query string — el path es suficiente.
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)."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
url— nueva URL HTTPS del receptor.events— nueva lista de eventos a suscribir.description— descripción opcional.is_active— activar / desactivar el endpoint.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Verificar que un evento específico fue entregado.
- Diagnosticar fallos: qué HTTP code devolvió su endpoint.
- Medir latencia: campo
response_time_ms. - Auditoría: trazabilidad completa de notificaciones enviadas.
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:
success— el endpoint respondió con HTTP 2xx.failed— el endpoint respondió con 4xx/5xx, o hubo timeout/error de red.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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¤cy_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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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).
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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Sin parametros → retorna las 7 provincias
?province=1→ cantones de San Jose?province=1&canton=01→ distritos de San Jose central?q=escazu→ busqueda por nombre en cualquier nivel
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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).
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.
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).
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.
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.
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.
.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:
1= Aceptado → consecutivo tipo 052= Aceptado Parcialmente → consecutivo tipo 063= Rechazado → consecutivo tipo 07
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Plan Gratis (el cliente o el integrador pueden actualizarlo después).
- Una cuenta de usuario administrador (
owner) lista para recibir el magic link. - Un vínculo de gestión activo con este integrador.
El cliente NO queda con:
- Ningún certificado digital (debe subirlo después con
POST /my-contributors/{id}/certificates). - Ningún grant activo para que el integrador firme por él. Para
eso, debe solicitar un grant desde el grupo
Certificados de terceros.
Tras crear:
- El cliente recibe un email con el magic link.
- El cliente debe activar la cuenta en ≤ 72 horas.
- 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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
default_pdf_template_id— la plantilla PDF que este integrador usará por defecto al emitir por este cliente. Per-integrador: no afecta a otros integradores del mismo cliente.
Campos bloqueados (retornan 403):
legal_name,trade_name,emails,economic_activities,province,canton,district,neighborhood,address,phone,phone_country_code
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Desvincular al integrador del cliente
requires authentication
Rompe únicamente la relación de gestión entre usted y este cliente. El cliente conserva:
- Su cuenta y sus datos.
- Sus certificados digitales.
- Sus comprobantes emitidos.
- Los vínculos con otros integradores (si los tiene).
Efectos inmediatos de la desvinculación:
- La relación de gestión queda marcada como desvinculada.
- Todos los grants activos de este cliente hacia usted se revocan automáticamente (no podrá seguir firmando por él).
- 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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Revoque su propio grant de acceso al certificado desde
el grupo Certificados de terceros (
DELETE /my-access/{grantId}). - Deje de emitir por ese cliente.
- Desvincúlese del cliente con
DELETE /my-contributors/{id}si ya no desea gestionarlo.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Reenviar el magic link al cliente gestionado
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:
- Solo se puede reenviar si el usuario del cliente tiene
must_change_password = true(es decir, aún no estableció su contraseña definitiva). - Si el cliente ya activó su cuenta, este endpoint retorna 422.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Migración de facturador: el cliente usaba Alegra y su última FE
fue la 9992. Setee
current_sequence=9992→ AlmendroFEC continúa desde 9993. - Corrección post-contingencia: ajustar al valor real.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Migración: la terminal 00002 ya estaba comprometida → cambiar a 00003.
- Reorganización operativa de terminales.
migrate_sequences:
false(default): nueva terminal arranca limpia (secuencias en 0).true: copiacurrent_sequencede cada tipo desde la terminal anterior a la nueva.
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."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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:
- Verificar en el portal cuándo fue el último uso del token (si lleva mucho sin usarse, quizá la integración esté caída).
- Detectar si ya existe un token antes de regenerar.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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), useGET /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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
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
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.