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 |
rejected |
Rechazado por Hacienda |
error |
Error técnico al comunicarse con Hacienda |
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 |
| 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.
Seguridad y tratamiento de sus datos
Esta es la respuesta técnica a la pregunta que más hacen las empresas antes de integrarse: quién puede ver la información que pasa por la API.
Credenciales de firma cifradas. El certificado .p12, su contraseña y el PIN de Hacienda se guardan cifrados con AES-256-GCM, con una llave dedicada que no forma parte del código de la aplicación. Estos valores no se devuelven en ninguna respuesta de la API, ni siquiera al dueño del certificado: se usan para firmar y nunca salen del servidor.
Aislamiento entre contribuyentes. Cada token pertenece a un contribuyente y toda
consulta se filtra por él automáticamente. Además, los endpoints que leen o escriben
comprobantes exigen que el token esté vinculado a un contribuyente activo: una petición sin
contribuyente asociado se rechaza con 403 antes de tocar la base de datos. No existe
ningún parámetro que permita salir de ese ámbito.
Un integrador solo alcanza los comprobantes de un cliente gestionado cuando existe una relación activa, y el alcance depende del rol de esa relación: en rol integrador ve únicamente los comprobantes que él mismo emitió por el cliente; en rol contador ve la facturación completa del cliente. Lo que el cliente emita por su cuenta o con otro proveedor no aparece en el primer caso. El permiso lo otorga el cliente sobre su propio certificado y lo puede revocar cuando quiera.
Qué ve el personal de Almendro. El panel administrativo no tiene ninguna pantalla que liste, abra o exporte comprobantes de un cliente. Lo que el equipo ve son cifras de operación de la cuenta: cantidad de comprobantes emitidos, usuarios, plan y estado de pago. Las acciones administrativas sensibles quedan registradas con autor y fecha.
Respaldos. Se genera un respaldo diario que se cifra en el servidor con criptografía asimétrica: el servidor solo tiene la llave pública, de modo que produce respaldos que él mismo no puede leer. La llave privada vive fuera del servidor.
El límite honesto. Como en cualquier plataforma en la nube, el personal técnico con acceso al servidor tiene acceso material a la base de datos. Eso no lo resuelve el software sino el contrato: firmamos acuerdo de confidencialidad y de tratamiento de datos conforme a la Ley 8968 de Costa Rica. Los respaldos se almacenan en Estados Unidos, siempre cifrados, lo que constituye una transferencia internacional declarada en dicho acuerdo.
El detalle completo, con la referencia al código que sostiene cada afirmación, está en el documento de tratamiento de datos que entregamos a solicitud.
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: support@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&external_reference=ORD-12345&environment=production¤cy_code=CRC&contributor_id=019d867d-0241-7288-8ece-fd64da75616d&scope=own&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",
"external_reference": "ORD-12345",
"environment": "production",
"currency_code": "CRC",
"contributor_id": "019d867d-0241-7288-8ece-fd64da75616d",
"scope": "own",
"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',
'external_reference' => 'ORD-12345',
'environment' => 'production',
'currency_code' => 'CRC',
'contributor_id' => '019d867d-0241-7288-8ece-fd64da75616d',
'scope' => 'own',
'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.
GET /api/v1/public/vouchers/issuers
requires authentication
Opciones disponibles para los filtros del listado, calculadas a partir de los comprobantes que realmente existen (acotados al ambiente). Evita ofrecer en los desplegables valores que no devolverían nada.
Retorna: has_own → true si la cuenta tiene comprobantes propios clients → clientes gestionados con al menos un comprobante voucher_types → tipos de comprobante presentes (01, 03, …) statuses → estados presentes (accepted, rejected, …)
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/vouchers/issuers" \
--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/issuers"
);
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/issuers';
$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));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 "Idempotency-Key: Identificador único de ESTA emisión, generado por usted (recomendado: un" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"voucher_type\": \"01\",
\"situation\": \"1\",
\"local_code\": \"564\",
\"terminal_code\": \"56425\",
\"voucher_key\": \"56425593142326822820492192294472586671521743076933\",
\"consecutive_number\": \"56425593142326822820\",
\"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\",
\"external_reference\": \"ORD-12345\",
\"xml_otros\": [
{
\"tipo\": \"texto\",
\"codigo\": \"orden\",
\"valor\": \"ORD-0001\"
}
],
\"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\": \"p\",
\"emails\": [
\"architecto\"
],
\"province\": 1,
\"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\",
\"factorCalculo\": 4326.41688,
\"monto\": \"1300.00000\",
\"monto_exportacion\": 77,
\"exoneracion\": {
\"tipoDocumento\": \"iy\",
\"numeroDocumento\": \"v\",
\"nombreInstitucion\": \"d\",
\"fechaEmision\": \"2026-08-11T21:37:46\",
\"tarifaExonerada\": 37,
\"montoExoneracion\": 9
}
}
],
\"impuesto_neto\": \"1300.00000\",
\"total_line_amount\": \"11300.00000\",
\"medicine_registration\": \"n\",
\"pharma_form_code\": \"564\",
\"commercial_codes\": [
{
\"tipo\": 4,
\"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}",
"Idempotency-Key": "Identificador único de ESTA emisión, generado por usted (recomendado: un",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"voucher_type": "01",
"situation": "1",
"local_code": "564",
"terminal_code": "56425",
"voucher_key": "56425593142326822820492192294472586671521743076933",
"consecutive_number": "56425593142326822820",
"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",
"external_reference": "ORD-12345",
"xml_otros": [
{
"tipo": "texto",
"codigo": "orden",
"valor": "ORD-0001"
}
],
"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": "p",
"emails": [
"architecto"
],
"province": 1,
"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",
"factorCalculo": 4326.41688,
"monto": "1300.00000",
"monto_exportacion": 77,
"exoneracion": {
"tipoDocumento": "iy",
"numeroDocumento": "v",
"nombreInstitucion": "d",
"fechaEmision": "2026-08-11T21:37:46",
"tarifaExonerada": 37,
"montoExoneracion": 9
}
}
],
"impuesto_neto": "1300.00000",
"total_line_amount": "11300.00000",
"medicine_registration": "n",
"pharma_form_code": "564",
"commercial_codes": [
{
"tipo": 4,
"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}',
'Idempotency-Key' => 'Identificador único de ESTA emisión, generado por usted (recomendado: un',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'voucher_type' => '01',
'situation' => '1',
'local_code' => '564',
'terminal_code' => '56425',
'voucher_key' => '56425593142326822820492192294472586671521743076933',
'consecutive_number' => '56425593142326822820',
'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',
'external_reference' => 'ORD-12345',
'xml_otros' => [
[
'tipo' => 'texto',
'codigo' => 'orden',
'valor' => 'ORD-0001',
],
],
'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' => 'p',
'emails' => [
'architecto',
],
'province' => 1,
'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',
'factorCalculo' => 4326.41688,
'monto' => '1300.00000',
'monto_exportacion' => 77,
'exoneracion' => [
'tipoDocumento' => 'iy',
'numeroDocumento' => 'v',
'nombreInstitucion' => 'd',
'fechaEmision' => '2026-08-11T21:37:46',
'tarifaExonerada' => 37,
'montoExoneracion' => 9,
],
],
],
'impuesto_neto' => '1300.00000',
'total_line_amount' => '11300.00000',
'medicine_registration' => 'n',
'pharma_form_code' => '564',
'commercial_codes' => [
[
'tipo' => 4,
'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 (429, Cupo mensual agotado (plan sin overage)):
{
"success": false,
"data": null,
"message": "Límite mensual de comprobantes alcanzado (50/50). Actualice a PyME para emitir más. Período: junio 2026. Precio: $29.99/mes ≈ ₡15,745.",
"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 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 "Idempotency-Key: Identificador único de ESTA anulación, generado por usted. Es OPCIONAL," \
--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}",
"Idempotency-Key": "Identificador único de ESTA anulación, generado por usted. Es OPCIONAL,",
"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}',
'Idempotency-Key' => 'Identificador único de ESTA anulación, generado por usted. Es OPCIONAL,',
'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.
Reenviar a Hacienda un comprobante que quedó sin enviarse
requires authentication
Reintenta la emisión de un comprobante que NO llegó a Hacienda y por lo
tanto NO consumió consecutivo allá: típicamente quedó en draft con su
XML generado pero sin firmar (falló la firma por falta de certificado).
Una vez subido el .p12, este endpoint lo firma y lo encola | reusando
el mismo comprobante y consecutivo, sin generar huecos de numeración.
También cubre el estado error (Hacienda no lo procesó por un fallo
temporal; ya está firmado y solo se reencola).
El ambiente del reenvío es el del comprobante original (un comprobante sandbox se firma con el certificado sandbox y se envía a sandbox).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/vouchers/50617062600020627065200100001010000000101173247335/resend" \
--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/50617062600020627065200100001010000000101173247335/resend"
);
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/50617062600020627065200100001010000000101173247335/resend';
$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));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.
Comprobantes / Borradores
Listar borradores del contribuyente
requires authentication
Devuelve los borradores del wizard de emisión guardados por el contribuyente, ordenados del más reciente al más antiguo (por fecha de última actualización). Un borrador es el estado parcial de un comprobante en construcción; no es un comprobante fiscal ni consume consecutivo.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/vouchers/drafts?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/drafts"
);
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/vouchers/drafts';
$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, Listado paginado):
{
"success": true,
"data": [
{
"id": "019d867d-aaaa-7288-8ece-fd64da756001",
"voucher_type": "01",
"payload": {
"client_id": null,
"line_items": [],
"currency_code": "CRC"
},
"totals_preview": {
"currency_code": "CRC",
"line_count": 0,
"total_venta": "0.00000",
"total_descuento": "0.00000",
"total_gravado": "0.00000",
"total_exento": "0.00000",
"total_venta_neta": "0.00000",
"total_impuesto": "0.00000",
"total_otros_cargos": "0.00000",
"total_comprobante": "0.00000"
},
"comments": "Esperando datos del cliente.",
"validation": null,
"created_at": "2026-04-16T10:00:00-06:00",
"updated_at": "2026-04-16T10:30: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 borradores):
{
"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 borrador
requires authentication
Guarda el estado parcial del wizard. NO consume consecutivo. Devuelve la validación en vivo (errores condicionales + preview de totales).
El payload es el cuerpo PARCIAL del comprobante (mismo contrato que
POST /vouchers, pero puede estar incompleto entre pasos del wizard).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/vouchers/drafts" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"voucher_type\": \"01\",
\"payload\": {
\"client_id\": null,
\"line_items\": [],
\"currency_code\": \"CRC\"
},
\"comments\": \"Esperando datos del cliente.\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/vouchers/drafts"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"voucher_type": "01",
"payload": {
"client_id": null,
"line_items": [],
"currency_code": "CRC"
},
"comments": "Esperando datos del cliente."
};
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/drafts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'voucher_type' => '01',
'payload' => [
'client_id' => null,
'line_items' => [],
'currency_code' => 'CRC',
],
'comments' => 'Esperando datos del cliente.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Borrador guardado):
{
"success": true,
"data": {
"id": "019d867d-aaaa-7288-8ece-fd64da756001",
"voucher_type": "01",
"payload": {
"client_id": null,
"line_items": [],
"currency_code": "CRC"
},
"totals_preview": {
"currency_code": "CRC",
"line_count": 0,
"total_venta": "0.00000",
"total_descuento": "0.00000",
"total_gravado": "0.00000",
"total_exento": "0.00000",
"total_venta_neta": "0.00000",
"total_impuesto": "0.00000",
"total_otros_cargos": "0.00000",
"total_comprobante": "0.00000"
},
"comments": "Esperando datos del cliente.",
"validation": {
"valid": false,
"errors": {
"line_items": [
"Agregá al menos una línea de detalle."
],
"sale_condition": [
"Indicá la condición de venta."
]
},
"totals": {
"currency_code": "CRC",
"line_count": 0,
"total_comprobante": "0.00000"
}
},
"created_at": "2026-04-16T10:00:00-06:00",
"updated_at": "2026-04-16T10:00:00-06:00"
},
"message": "Borrador guardado.",
"errors": null
}
Example response (422, Payload ausente o inválido):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"payload": [
"El cuerpo del borrador (payload) 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.
Consultar un borrador (con validación en vivo)
requires authentication
Devuelve el detalle del borrador indicado, recalculando en el momento la
validación en vivo (errores condicionales + preview de totales) sin
persistirla. Solo se retornan borradores que pertenecen a su
contribuyente. Un id que no sea un UUID válido retorna 404.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/vouchers/drafts/019d867d-aaaa-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/vouchers/drafts/019d867d-aaaa-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/vouchers/drafts/019d867d-aaaa-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, Borrador encontrado):
{
"success": true,
"data": {
"id": "019d867d-aaaa-7288-8ece-fd64da756001",
"voucher_type": "01",
"payload": {
"client_id": null,
"line_items": [],
"currency_code": "CRC"
},
"totals_preview": {
"currency_code": "CRC",
"line_count": 0,
"total_comprobante": "0.00000"
},
"comments": "Esperando datos del cliente.",
"validation": {
"valid": false,
"errors": {
"line_items": [
"Agregá al menos una línea de detalle."
]
},
"totals": {
"currency_code": "CRC",
"line_count": 0,
"total_comprobante": "0.00000"
}
},
"created_at": "2026-04-16T10:00:00-06:00",
"updated_at": "2026-04-16T10: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 borrador. Úselo en las rutas /{id}, /{id}/issue y DELETE /{id}.
voucher_type
string
Tipo de comprobante en construcción (01, 02, 03, 04, 08, 09, 10). null mientras el usuario no elige.
payload
object
Cuerpo PARCIAL del comprobante (mismo contrato que POST /vouchers, posiblemente incompleto).
totals_preview
object
Preview de totales persistido en el último store/update. null si nunca se calculó.
comments
string
Notas internas del borrador. No viajan al XML. Puede ser null.
validation
object
Validación en vivo (dry-run): valid (bool), errors (mapa campo→mensajes condicionales) y totals (preview recalculado). null si no se adjuntó.
created_at
string
Fecha de creación del borrador (ISO 8601).
updated_at
string
Fecha de última actualización del borrador (ISO 8601).
Guardar un paso del wizard (actualización del borrador)
requires authentication
Reemplaza el estado del borrador con lo enviado (voucher_type,
payload y comments) y vuelve a calcular la validación en vivo
(errores condicionales + preview de totales), persistiendo el preview.
NO consume consecutivo ni firma. Un id que no sea un UUID válido
retorna 404.
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/vouchers/drafts/019d867d-aaaa-7288-8ece-fd64da756001" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"voucher_type\": \"01\",
\"payload\": {
\"client_id\": \"019d867d-0001-7288-8ece-fd64da756001\",
\"sale_condition\": \"01\",
\"line_items\": [
{
\"quantity\": 1,
\"unit_price\": 1000
}
]
},
\"comments\": \"Listo para emitir.\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/vouchers/drafts/019d867d-aaaa-7288-8ece-fd64da756001"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"voucher_type": "01",
"payload": {
"client_id": "019d867d-0001-7288-8ece-fd64da756001",
"sale_condition": "01",
"line_items": [
{
"quantity": 1,
"unit_price": 1000
}
]
},
"comments": "Listo para emitir."
};
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/vouchers/drafts/019d867d-aaaa-7288-8ece-fd64da756001';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'voucher_type' => '01',
'payload' => [
'client_id' => '019d867d-0001-7288-8ece-fd64da756001',
'sale_condition' => '01',
'line_items' => [
[
'quantity' => 1,
'unit_price' => 1000,
],
],
],
'comments' => 'Listo para emitir.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Borrador actualizado):
{
"success": true,
"data": {
"id": "019d867d-aaaa-7288-8ece-fd64da756001",
"voucher_type": "01",
"payload": {
"client_id": "019d867d-0001-7288-8ece-fd64da756001",
"sale_condition": "01",
"line_items": [
{
"quantity": 1,
"unit_price": 1000
}
]
},
"totals_preview": {
"currency_code": "CRC",
"line_count": 1,
"total_venta": "1000.00000",
"total_descuento": "0.00000",
"total_gravado": "1000.00000",
"total_exento": "0.00000",
"total_venta_neta": "1000.00000",
"total_impuesto": "130.00000",
"total_otros_cargos": "0.00000",
"total_comprobante": "1130.00000"
},
"comments": "Listo para emitir.",
"validation": {
"valid": true,
"errors": [],
"totals": {
"currency_code": "CRC",
"line_count": 1,
"total_comprobante": "1130.00000"
}
},
"created_at": "2026-04-16T10:00:00-06:00",
"updated_at": "2026-04-16T11:00:00-06:00"
},
"message": "Borrador actualizado.",
"errors": null
}
Example response (404, No encontrado):
{
"success": false,
"data": null,
"message": "Recurso no encontrado.",
"errors": null
}
Example response (422, Payload ausente o inválido):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"payload": [
"El cuerpo del borrador (payload) 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.
Emitir el comprobante a partir del borrador
requires authentication
Valida en seco (reglas condicionales); si pasa, reproduce el pipeline real de POST /vouchers (consume consecutivo, firma, valida XSD, encola el envío a Hacienda) y ELIMINA el borrador al tener éxito.
No recibe cuerpo: emite a partir del payload ya guardado en el
borrador. La respuesta es liviana (no serializa el comprobante
completo); el detalle se consulta en GET /vouchers/{key}. Un id que
no sea un UUID válido retorna 404.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/vouchers/drafts/019d867d-aaaa-7288-8ece-fd64da756001/issue" \
--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/drafts/019d867d-aaaa-7288-8ece-fd64da756001/issue"
);
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/drafts/019d867d-aaaa-7288-8ece-fd64da756001/issue';
$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, Comprobante emitido):
{
"success": true,
"data": {
"id": "019d867d-bbbb-7288-8ece-fd64da756999",
"voucher_key": "50624062600310112345600100001010000000001100000001",
"voucher_type": "01",
"consecutive_number": "00100001010000000001",
"status": "pending"
},
"message": "Comprobante emitido.",
"errors": null
}
Example response (404, No encontrado):
{
"success": false,
"data": null,
"message": "Recurso no encontrado.",
"errors": null
}
Example response (422, Borrador incompleto):
{
"success": false,
"data": null,
"message": "El borrador tiene datos incompletos para emitir.",
"errors": {
"line_items": [
"Agregá al menos una línea de detalle."
],
"sale_condition": [
"Indicá la condición de venta."
]
}
}
Example response (422, Falla del esquema oficial (XSD)):
{
"success": false,
"data": null,
"message": "El comprobante no cumple el esquema oficial (XSD).",
"errors": {
"xsd": [
"El elemento 'CodigoMoneda' no es válido."
]
}
}
Example response (422, Sucursal/terminal inexistente):
{
"success": false,
"data": null,
"message": "La sucursal/terminal indicada no existe.",
"errors": {
"terminal_code": [
"Créela primero en Sucursales y terminales."
]
}
}
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.
Descartar un borrador
requires authentication
Elimina el borrador indicado. No afecta a comprobantes ya emitidos (el
borrador no es un comprobante fiscal). Un id que no sea un UUID válido
retorna 404.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/vouchers/drafts/019d867d-aaaa-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/vouchers/drafts/019d867d-aaaa-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/vouchers/drafts/019d867d-aaaa-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, Borrador descartado):
{
"success": true,
"data": null,
"message": "Borrador descartado.",
"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.
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_this_month": 340,
"emails_limit": 2250,
"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_this_month": 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_this_month
integer
Correos transaccionales enviados en el mes en curso.
emails_limit
integer
Límite MENSUAL de correos del plan. null = sin tope.
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": "3101999999",
"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_monthly_emails": 2250,
"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_monthly_emails
integer
Máximo de correos por mes.
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\": \"3101999999\",
\"id_type\": 2,
\"hacienda_confirmed\": true,
\"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": "3101999999",
"id_type": 2,
"hacienda_confirmed": true,
"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' => '3101999999',
'id_type' => 2,
'hacienda_confirmed' => true,
'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": "3101999999",
"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.
Usuarios de la Cuenta
Listar usuarios del contribuyente
requires authentication
Devuelve todos los usuarios (cuentas de login) asociados al contribuyente activo: los activos, los que están pendientes de establecer su contraseña y los desactivados. Los usuarios cuyo acceso fue revocado no aparecen.
Cada usuario es una cuenta de login (nombre + correo + rol), sin cédula | la cédula vive en el contribuyente, no en el usuario.
Solo el owner o un admin del contribuyente pueden consultar esta lista. Otros roles reciben HTTP 403.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/users" \
--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/users"
);
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/users';
$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 usuarios):
{
"success": true,
"data": [
{
"id": "019d867d-0001-7288-8ece-fd64da756001",
"name": "Ana Propietaria",
"email": "ana@empresa.cr",
"role": "owner",
"is_active": true,
"is_owner": true,
"status": "active",
"created_at": "2026-04-01T10:00:00-06:00"
},
{
"id": "019d867d-0002-7288-8ece-fd64da756002",
"name": "María Pérez",
"email": "maria@empresa.cr",
"role": "cashier",
"is_active": true,
"is_owner": false,
"status": "pending",
"created_at": "2026-04-10T14:30:00-06:00"
}
],
"message": "",
"errors": null
}
Example response (403, Sin permisos):
{
"success": false,
"data": null,
"message": "No tenés permisos para gestionar usuarios.",
"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.
Invitar un usuario
requires authentication
Crea una cuenta de login (sin cédula) y la asocia al contribuyente
activo. El usuario recibe un correo con un enlace para establecer
su contraseña; mientras tanto queda en estado pending.
Reglas:
- El
emaildebe ser único en toda la plataforma. Si ya existe un usuario con ese correo, responde HTTP 422. - El
roleno puede serowner; solo se admitenadmin,accountant,cashier,vieweroapi_user.
Solo el owner o un admin pueden invitar usuarios. Otros roles reciben HTTP 403.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/users" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"María Pérez\",
\"email\": \"maria@empresa.cr\",
\"role\": \"cashier\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/users"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "María Pérez",
"email": "maria@empresa.cr",
"role": "cashier"
};
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/users';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'María Pérez',
'email' => 'maria@empresa.cr',
'role' => 'cashier',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Usuario invitado):
{
"success": true,
"data": {
"id": "019d867d-0002-7288-8ece-fd64da756002",
"name": "María Pérez",
"email": "maria@empresa.cr",
"role": "cashier",
"is_active": true,
"is_owner": false,
"status": "pending",
"created_at": "2026-04-16T10:00:00-06:00"
},
"message": "Usuario invitado. Recibirá un correo para establecer su contraseña.",
"errors": null
}
Example response (403, Sin permisos):
{
"success": false,
"data": null,
"message": "No tenés permisos para gestionar usuarios.",
"errors": null
}
Example response (422, Correo duplicado):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"email": [
"Ya existe un usuario con ese correo."
]
}
}
Example response (422, Rol inválido):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"role": [
"El rol seleccionado no es 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.
Editar un usuario
requires authentication
Permite cambiar el rol y/o el estado (activar / desactivar) de un usuario del contribuyente. Admite actualización parcial | envíe solo los campos que desea modificar.
Acción sensible: cambiar el estado
is_activerequiere enviar el campopasswordcon la contraseña del usuario que opera. Si falta o es incorrecta, responde HTTP 401.
Al activar un usuario se respeta el max_users del plan: si ya se
alcanzó el límite de usuarios activos, responde HTTP 422.
El propietario de la cuenta no puede modificarse (HTTP 403), y
el rol owner no puede asignarse desde aquí. Solo el owner o un admin
pueden editar usuarios.
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/users/019d867d-0002-7288-8ece-fd64da756002" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"role\": \"viewer\",
\"is_active\": true,
\"password\": \"MiClaveSegura123\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/users/019d867d-0002-7288-8ece-fd64da756002"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"role": "viewer",
"is_active": true,
"password": "MiClaveSegura123"
};
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/users/019d867d-0002-7288-8ece-fd64da756002';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'role' => 'viewer',
'is_active' => true,
'password' => 'MiClaveSegura123',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Usuario actualizado):
{
"success": true,
"data": {
"id": "019d867d-0002-7288-8ece-fd64da756002",
"name": "María Pérez",
"email": "maria@empresa.cr",
"role": "viewer",
"is_active": true,
"is_owner": false,
"status": "active",
"created_at": "2026-04-10T14:30:00-06:00"
},
"message": "Usuario actualizado.",
"errors": null
}
Example response (401, Contraseña incorrecta):
{
"success": false,
"data": null,
"message": "Contraseña incorrecta.",
"errors": null
}
Example response (403, Sin permisos):
{
"success": false,
"data": null,
"message": "No tenés permisos para gestionar usuarios.",
"errors": null
}
Example response (403, Es el propietario):
{
"success": false,
"data": null,
"message": "No se puede modificar al propietario de la cuenta.",
"errors": null
}
Example response (404, Usuario no encontrado):
{
"success": false,
"data": null,
"message": "Usuario no encontrado en este contribuyente.",
"errors": null
}
Example response (422, Límite del plan alcanzado):
{
"success": false,
"data": null,
"message": "Alcanzaste el límite de usuarios activos de tu plan (3).",
"errors": {
"is_active": [
"Desactivá otro usuario o cambiá de plan."
]
}
}
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.
Revocar el acceso de un usuario
requires authentication
Desvincula al usuario del contribuyente activo, liberando su asiento del plan. La cuenta de login global del usuario no se elimina: sigue viva para que pueda asociarse a otro contribuyente o facturar por su cuenta. Simplemente deja de pertenecer a esta cuenta y desaparece del listado.
Acción sensible: requiere enviar el campo
passwordcon la contraseña del usuario que opera. Si falta o es incorrecta, responde HTTP 401.
No puede revocar su propio acceso ni el del propietario de la cuenta (ambos casos responden HTTP 403). Solo el owner o un admin pueden revocar usuarios.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/users/019d867d-0002-7288-8ece-fd64da756002" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"password\": \"MiClaveSegura123\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/users/019d867d-0002-7288-8ece-fd64da756002"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"password": "MiClaveSegura123"
};
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/users/019d867d-0002-7288-8ece-fd64da756002';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'password' => 'MiClaveSegura123',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Acceso revocado):
{
"success": true,
"data": null,
"message": "Usuario desvinculado de esta cuenta.",
"errors": null
}
Example response (401, Contraseña incorrecta):
{
"success": false,
"data": null,
"message": "Contraseña incorrecta.",
"errors": null
}
Example response (403, Sin permisos):
{
"success": false,
"data": null,
"message": "No tenés permisos para gestionar usuarios.",
"errors": null
}
Example response (403, No puede revocarse a sí mismo):
{
"success": false,
"data": null,
"message": "No podés revocar tu propio acceso.",
"errors": null
}
Example response (403, Es el propietario):
{
"success": false,
"data": null,
"message": "No se puede revocar al propietario de la cuenta.",
"errors": null
}
Example response (404, Usuario no encontrado):
{
"success": false,
"data": null,
"message": "Usuario no encontrado en este 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.
Cambio de Contribuyente
Listar contribuyentes accesibles
requires authentication
Devuelve los contribuyentes a los que el usuario autenticado tiene
acceso operativo (pivote user_contributors activo y aceptado), por
ejemplo como dueño de su propia empresa o como contador de varios
clientes. Solo se incluyen los contribuyentes que estén activos.
El campo is_current indica cuál de ellos es el contribuyente activo
de la sesión (el que lee el middleware multi-tenant). Use el endpoint
POST /switch-contributor para cambiarlo.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/my-access-contributors" \
--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-contributors"
);
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-contributors';
$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, Contribuyentes accesibles):
{
"success": true,
"data": [
{
"contributor_id": "019d867d-0001-7288-8ece-fd64da756001",
"legal_name": "EMPRESA EJEMPLO S.A.",
"id_number": "3101123456",
"role": "owner",
"is_current": true
},
{
"contributor_id": "019d867d-0002-7288-8ece-fd64da756002",
"legal_name": "CLIENTE CONTABLE S.A.",
"id_number": "3101987654",
"role": "accountant",
"is_current": false
}
],
"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
contributor_id
string
UUID del contribuyente. Úselo como contributor_id en POST /switch-contributor.
legal_name
string
Nombre legal o razón social del contribuyente.
id_number
string
Número de identificación (cédula) del contribuyente.
role
string
Rol del usuario sobre ese contribuyente (por ejemplo owner o accountant).
is_current
boolean
true si es el contribuyente activo de la sesión actual.
Cambiar el contribuyente activo
requires authentication
Cambia el contribuyente activo de la sesión del usuario autenticado.
El usuario debe tener acceso operativo (pivote user_contributors
activo y aceptado) al contribuyente destino, y este debe estar
activo. El mismo token sigue siendo válido tras el cambio.
Si el usuario no tiene acceso al contribuyente indicado, responde HTTP 403. Si el contribuyente no existe o está inactivo, responde HTTP 404.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/switch-contributor" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contributor_id\": \"019d867d-0002-7288-8ece-fd64da756002\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/switch-contributor"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contributor_id": "019d867d-0002-7288-8ece-fd64da756002"
};
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/switch-contributor';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contributor_id' => '019d867d-0002-7288-8ece-fd64da756002',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Contribuyente cambiado):
{
"success": true,
"data": {
"contributor_id": "019d867d-0002-7288-8ece-fd64da756002",
"legal_name": "CLIENTE CONTABLE S.A.",
"role": "accountant"
},
"message": "Contribuyente activo cambiado.",
"errors": null
}
Example response (403, Sin acceso al contribuyente):
{
"success": false,
"data": null,
"message": "No tenés acceso a ese contribuyente.",
"errors": null
}
Example response (404, Contribuyente inexistente o inactivo):
{
"success": false,
"data": null,
"message": "El contribuyente no existe o está inactivo.",
"errors": null
}
Example response (422, Validación fallida):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"contributor_id": [
"El campo contributor id 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.
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 de certificados):
{
"success": true,
"data": [],
"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.
Consultar disponibilidad de subida de certificado por ambiente
requires authentication
Indica para cada ambiente (sandbox, production) si está
disponible para subir un certificado nuevo o si ya hay uno
activo que debe desactivarse primero.
Útil para mostrar el formulario de subida con el selector de ambiente correctamente deshabilitado, evitando que el usuario intente subir un .p12 que sería rechazado con HTTP 422.
Reglas:
available = true→ no hay certificado activo en ese ambiente, se puede subir uno nuevo.available = false→ ya hay certificado activo. Para subir uno nuevo en este ambiente debe desactivar el actual conDELETE /api/v1/public/certificates/{id}.
Cuando available = false, el campo active_certificate incluye
los datos del certificado vigente para que el frontend pueda
mostrar al usuario qué cert está bloqueando el upload.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/certificates/availability" \
--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/availability"
);
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/availability';
$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, Sandbox disponible, producción ocupada):
{
"success": true,
"data": {
"sandbox": {
"available": true,
"active_certificate": null
},
"production": {
"available": false,
"active_certificate": {
"id": "019d867d-1111-7288-8ece-fd64da756001",
"certificate_serial": "019DD6D90F43",
"certificate_subject": "CN=ALBERT MENDEZ, serialNumber=CPF-02-0627-0652",
"valid_from": "2025-12-15T10:30:00-06:00",
"valid_until": "2027-12-15T10:30:00-06:00",
"days_remaining": 596,
"is_expired": false,
"created_at": "2026-04-15T08:00:00-06:00"
}
}
},
"message": "",
"errors": null
}
Example response (200, Ambos ambientes disponibles):
{
"success": true,
"data": {
"sandbox": {
"available": true,
"active_certificate": null
},
"production": {
"available": true,
"active_certificate": null
}
},
"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
sandbox
object
available
boolean
true si el ambiente sandbox está disponible para subir un cert nuevo, false si ya hay uno activo.
active_certificate
object
Datos del certificado activo en sandbox. null si no hay activo.
production
object
available
boolean
Igual al de sandbox pero para producción.
active_certificate
object
Datos del certificado activo en producción. null si no hay activo.
Subir y activar un certificado digital `.p12`
requires authentication
Sube un archivo .p12 (PKCS#12) con su contraseña y la contraseña
del IdP 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.
Validaciones que se ejecutan ANTES de guardar:
- Disponibilidad del ambiente: solo puede subir cert si NO hay
uno activo en ese ambiente. Si ya hay uno, debe desactivarlo
primero con
DELETE /certificates/{id}o usar el endpointGET /certificates/availabilitypara chequear estado. - PIN del .p12: el archivo debe abrirse con la contraseña
provista en
p12_password. - Llave privada RSA: el .p12 debe contener una llave RSA válida (Hacienda CR exige RSA-SHA256 para XAdES-EPES).
- Subject del certificado: el cert debe pertenecer al contribuyente que lo está subiendo (anti-fraude | REGLAMENTO art. 16).
- Vigencia del certificado: el X.509 no puede estar vencido ni ser prematuro.
- Credenciales IdP de Hacienda: el
hacienda_pindebe autenticar correctamente contra el IdP del ambiente solicitado (POST grant_type=password). Si falla, el cert NO se guarda | sería inutilizable sin token OAuth2 válido.
Si CUALQUIERA de las 6 validaciones falla, se retorna HTTP 422 (o 503 si el IdP de Hacienda no responde) y NADA se guarda en DB.
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=PIN-del-archivo-p12" \
-F "hacienda_pin=Contraseña-del-IdP-de-Hacienda" \
-F "environment=sandbox"
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=architecto"\
--form "hacienda_pin=architecto"\
--form "environment=architecto"\
--form "p12_file=@/tmp/phpbon97l1emqj1dgYHhsD" 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', 'architecto');
body.append('hacienda_pin', 'architecto');
body.append('environment', 'architecto');
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' => 'architecto'
],
[
'name' => 'hacienda_pin',
'contents' => 'architecto'
],
[
'name' => 'environment',
'contents' => 'architecto'
],
[
'name' => 'p12_file',
'contents' => fopen('/tmp/phpbon97l1emqj1dgYHhsD', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Certificado registrado exitosamente):
{
"success": true,
"data": {
"id": "019d867d-1111-7288-8ece-fd64da756001",
"environment": "sandbox",
"is_active": true,
"is_expired": false,
"days_remaining": 596,
"valid_from": "2025-12-15T10:30:00-06:00",
"valid_until": "2027-12-15T10:30:00-06:00",
"certificate_subject": "CN=ALBERT MENDEZ, serialNumber=CPF-02-0627-0652",
"certificate_serial": "019DD6C62577",
"created_at": "2026-04-29T08:00:00-06:00"
},
"message": "Certificado registrado y activado correctamente.",
"errors": null
}
Example response (422, PIN del archivo .p12 incorrecto (5001)):
{
"success": false,
"data": null,
"message": "El PIN del archivo .p12 es incorrecto, el archivo está corrupto, o no es un PKCS#12 válido emitido por el BCCR. ...",
"errors": {
"p12_password": [
"El PIN del archivo .p12 es incorrecto, ..."
]
}
}
Example response (422, Cert no pertenece al contribuyente (5002)):
{
"success": false,
"data": null,
"message": "El certificado .p12 NO pertenece a este contribuyente. ...",
"errors": {
"p12_file": [
"El certificado .p12 NO pertenece a este contribuyente. ..."
]
}
}
Example response (422, Credenciales IdP rechazadas (5003)):
{
"success": false,
"data": null,
"message": "El IdP de Hacienda rechazó las credenciales con HTTP 401. ...",
"errors": {
"hacienda_pin": [
"El IdP de Hacienda rechazó las credenciales con HTTP 401. ..."
]
}
}
Example response (422, Ya hay cert activo en el ambiente (5004)):
{
"success": false,
"data": null,
"message": "Ya existe un certificado activo en el ambiente 'sandbox'. ...",
"errors": {
"environment": [
"Ya existe un certificado activo en el ambiente 'sandbox'. ..."
]
}
}
Example response (422, El .p12 pertenece al otro ambiente (5005)):
{
"success": false,
"data": null,
"message": "El certificado .p12 pertenece al ambiente de sandbox (pruebas) pero usted lo está subiendo como producción. ...",
"errors": {
"p12_file": [
"El certificado .p12 pertenece al ambiente de sandbox (pruebas) pero usted lo está subiendo como producción. ..."
]
}
}
Example response (422, Perfil sin identificación registrada (5006)):
{
"success": false,
"data": null,
"message": "No es posible validar el certificado porque el contribuyente todavía no tiene registrada su identificación (tipo y número de cédula) en el perfil. ...",
"errors": {
"p12_file": [
"No es posible validar el certificado porque el contribuyente todavía no tiene registrada su identificación ..."
]
}
}
Example response (422, Certificado vencido (2002)):
{
"success": false,
"data": null,
"message": "El certificado del contribuyente '...' no está vigente. ...",
"errors": {
"p12_file": [
"El certificado del contribuyente '...' no está vigente. ..."
]
}
}
Example response (503, IdP de Hacienda no responde temporalmente):
{
"success": false,
"data": null,
"message": "El IdP de Hacienda no responde temporalmente. Intente nuevamente en unos minutos.",
"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.
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 certificado nuevo (debe desactivar el vigente primero | el sistema no permite dos activos en el mismo ambiente).
- Migración de un contribuyente a otra plataforma.
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_header": 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_header": 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_header\": 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_header": 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_header' => 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_header": 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_header
integer
Tamaño del encabezado/título principal en pt (8-24).
size_footer
integer
Tamaño del pie de página en pt (5-10).
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\",
\"muted\": \"#2815Df\"
},
\"fonts\": {
\"family\": \"Helvetica\",
\"size_header\": 14,
\"size_body\": 9,
\"size_footer\": 7
},
\"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\": true,
\"show_payment_methods\": false,
\"show_references\": false,
\"show_other_charges\": false
},
\"qr\": {
\"size\": 100,
\"position\": \"bottom-right\"
},
\"margins\": {
\"top\": 22,
\"right\": 24,
\"bottom\": 18,
\"left\": 8
},
\"style\": \"bold\",
\"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",
"muted": "#2815Df"
},
"fonts": {
"family": "Helvetica",
"size_header": 14,
"size_body": 9,
"size_footer": 7
},
"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": true,
"show_payment_methods": false,
"show_references": false,
"show_other_charges": false
},
"qr": {
"size": 100,
"position": "bottom-right"
},
"margins": {
"top": 22,
"right": 24,
"bottom": 18,
"left": 8
},
"style": "bold",
"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',
'muted' => '#2815Df',
],
'fonts' => [
'family' => 'Helvetica',
'size_header' => 14,
'size_body' => 9,
'size_footer' => 7,
],
'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' => true,
'show_payment_methods' => false,
'show_references' => false,
'show_other_charges' => false,
],
'qr' => [
'size' => 100,
'position' => 'bottom-right',
],
'margins' => [
'top' => 22,
'right' => 24,
'bottom' => 18,
'left' => 8,
],
'style' => 'bold',
'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_header": 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/phpfs70aiqiehda2jhlLNz" 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/phpfs70aiqiehda2jhlLNz', '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&for_contributor_id=0b3f1c2a-0000-4000-8000-000000000000&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",
"for_contributor_id": "0b3f1c2a-0000-4000-8000-000000000000",
"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',
'for_contributor_id' => '0b3f1c2a-0000-4000-8000-000000000000',
'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_factor\": 4326.41688,
\"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_factor": 4326.41688,
"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_factor' => 4326.41688,
'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.
Secuencias
Listar secuencias del contribuyente
requires authentication
Devuelve todas las secuencias de consecutivos (sucursal + terminal + tipo de comprobante) registradas por el contribuyente autenticado, ordenadas por sucursal, terminal y tipo.
Cada secuencia representa un contador independiente del consecutivo de
20 dígitos definido en la normativa (pág. 65 Anexos): los 3 dígitos de
sucursal (local_code), 5 de terminal (terminal_code), 2 del tipo
(voucher_type) y 10 del consecutivo correlativo.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/sequences" \
--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/sequences"
);
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/sequences';
$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 secuencias):
{
"success": true,
"data": [
{
"id": "019d867d-0001-7288-8ece-fd64da756001",
"local_code": "001",
"terminal_code": "00001",
"voucher_type": "01",
"voucher_type_label": "Factura Electrónica",
"voucher_type_abbr": "FE",
"current_sequence": 42,
"next_sequence": 43,
"last_consecutive": "00100001010000000042",
"next_consecutive": "00100001010000000043",
"last_issued_at": "2026-04-10T14:30:00-06:00",
"last_issued_key": "506041026...00000042",
"is_active": true,
"is_near_rollover": false,
"display_key": "001-00001-01"
}
],
"message": "Secuencias 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 de la secuencia. Úselo como {id} en PUT /sequences/{id}.
local_code
string
Código de sucursal (3 dígitos). 001 = casa matriz.
terminal_code
string
Código de terminal (5 dígitos). 00001 = terminal directa.
voucher_type
string
Código del tipo de comprobante (01-10).
voucher_type_label
string
Nombre legible del tipo en español.
voucher_type_abbr
string
Abreviatura del tipo (FE, ND, NC, TE, FEC, FEE, REP).
current_sequence
integer
Último consecutivo emitido (0 = ninguno).
next_sequence
integer
Próximo consecutivo que se usará (current_sequence + 1).
last_consecutive
string
Consecutivo de 20 dígitos del último comprobante emitido. null si ninguno.
next_consecutive
string
Consecutivo de 20 dígitos que tendrá el próximo comprobante (preview).
last_issued_at
string
Fecha del último comprobante emitido (ISO 8601). null si ninguno.
last_issued_key
string
Clave de 50 dígitos del último comprobante emitido (auditoría). null si ninguno.
is_active
boolean
true si la terminal está habilitada para este tipo.
is_near_rollover
boolean
true si la secuencia está cerca del límite 9,999,999,999.
display_key
string
Representación legible sucursal-terminal-tipo (ej. 001-00001-01). No es el formato XML.
Crear una secuencia (sucursal/terminal)
requires authentication
Registra una nueva secuencia de consecutivos para una combinación de sucursal + terminal + tipo de comprobante del contribuyente autenticado. Útil para dar de alta una nueva caja, sucursal o un tipo de comprobante que aún no tiene secuencia.
Reglas de validación:
local_code+terminal_code+voucher_typedeben ser una combinación única. Duplicar responde HTTP 422.local_codeson 3 dígitos y no puede ser000.terminal_codeson 5 dígitos y no puede ser00000.voucher_typedebe ser uno de los 7 tipos de emisión directa.- Crear una terminal nueva consume cupo del plan
(
max_branch_terminals); si lo supera responde HTTP 422.
La secuencia se crea con is_active=true.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/sequences" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"local_code\": \"001\",
\"terminal_code\": \"00001\",
\"voucher_type\": \"01\",
\"current_sequence\": 9992
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/sequences"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"local_code": "001",
"terminal_code": "00001",
"voucher_type": "01",
"current_sequence": 9992
};
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/sequences';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'local_code' => '001',
'terminal_code' => '00001',
'voucher_type' => '01',
'current_sequence' => 9992,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Secuencia creada):
{
"success": true,
"data": {
"id": "019d867d-0002-7288-8ece-fd64da756002",
"local_code": "001",
"terminal_code": "00001",
"voucher_type": "01",
"voucher_type_label": "Factura Electrónica",
"voucher_type_abbr": "FE",
"current_sequence": 9992,
"next_sequence": 9993,
"last_consecutive": "00100001010000009992",
"next_consecutive": "00100001010000009993",
"last_issued_at": null,
"last_issued_key": null,
"is_active": true,
"is_near_rollover": false,
"display_key": "001-00001-01"
},
"message": "Secuencia creada.",
"errors": null
}
Example response (422, Secuencia duplicada):
{
"success": false,
"data": null,
"message": "Ya existe una secuencia para esa sucursal, terminal y tipo.",
"errors": {
"voucher_type": [
"Secuencia duplicada."
]
}
}
Example response (422, Límite del plan alcanzado):
{
"success": false,
"data": null,
"message": "Ha alcanzado el límite de terminales de su plan.",
"errors": {
"terminal_code": [
"Ha alcanzado el límite de terminales de su plan."
]
}
}
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 una secuencia existente
requires authentication
Admite actualización parcial del current_sequence y/o del estado
is_active de una secuencia del contribuyente autenticado. Solo se
pueden ajustar secuencias propias; una secuencia inexistente o de otro
contribuyente responde HTTP 404.
Importante: bajar
current_sequencepor debajo del último consecutivo ya emitido se rechaza con 422: el próximo comprobante repetiría un número existente y Hacienda lo rechazaría. Sí se permite ajustarlo por encima de ese piso, que es el escenario legítimo de migración desde otro facturador.Cambiar el consecutivo exige confirmación de contraseña y queda registrado en la bitácora de auditoría con el valor anterior, el nuevo, el motivo, el usuario y la IP (Resolución MH-DGT-RES-0027-2024, art. 6).
El motivo (reason) es obligatorio en toda actualización.
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/sequences/019d867d-0001-7288-8ece-fd64da756001" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"current_sequence\": 9992,
\"is_active\": true,
\"reason\": \"Migración desde Alegra, último consecutivo tipo 01 fue 9992.\",
\"password\": \"MiC0ntr4señ4\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/sequences/019d867d-0001-7288-8ece-fd64da756001"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"current_sequence": 9992,
"is_active": true,
"reason": "Migración desde Alegra, último consecutivo tipo 01 fue 9992.",
"password": "MiC0ntr4señ4"
};
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/sequences/019d867d-0001-7288-8ece-fd64da756001';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'current_sequence' => 9992,
'is_active' => true,
'reason' => 'Migración desde Alegra, último consecutivo tipo 01 fue 9992.',
'password' => 'MiC0ntr4señ4',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Secuencia actualizada):
{
"success": true,
"data": {
"id": "019d867d-0001-7288-8ece-fd64da756001",
"local_code": "001",
"terminal_code": "00001",
"voucher_type": "01",
"voucher_type_label": "Factura Electrónica",
"voucher_type_abbr": "FE",
"current_sequence": 9992,
"next_sequence": 9993,
"last_consecutive": "00100001010000009992",
"next_consecutive": "00100001010000009993",
"last_issued_at": "2026-04-10T14:30:00-06:00",
"last_issued_key": "506041026...00009992",
"is_active": true,
"is_near_rollover": false,
"display_key": "001-00001-01"
},
"message": "Secuencia actualizada.",
"errors": null
}
Example response (404, Secuencia no encontrada):
{
"success": false,
"data": null,
"message": "Secuencia 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.
Sucursales y cajas
Listar sucursales y cajas propias
requires authentication
Devuelve las sucursales (branches) directas del contribuyente autenticado,
cada una con sus cajas (terminals). La sucursal directa usa slot 00 en el
terminal_code (= 00 + caja de 3 díg.); la primera caja es 00001.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/branches" \
--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/branches"
);
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/branches';
$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, Sucursales del contribuyente):
{
"success": true,
"data": [
{
"id": "019ee0-...",
"local_code": "001",
"name": "Casa Matriz",
"is_active": true,
"is_direct": true,
"terminals": [
{
"id": "019ee1-...",
"terminal_code": "00001",
"slot": "00",
"box": "001",
"name": "Caja 1",
"is_active": true
}
],
"created_at": "2026-06-19T15:00:00-06:00"
}
],
"message": "Sucursales 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 de la sucursal.
local_code
string
Código de sucursal Hacienda (001-999).
name
string
Nombre de la sucursal.
is_direct
boolean
true = sucursal propia/directa; false = creada por un integrador.
terminals
object[]
Cajas de la sucursal.
terminal_code
string
Terminal de 5 díg. (posición 04-08 del NumeroConsecutivo).
slot
string
Slot de 2 díg. (00 = directo).
box
string
Caja de 3 díg. dentro del slot.
Crear una sucursal
requires authentication
Crea una sucursal directa del contribuyente y, automáticamente, su primera
caja (toda sucursal nace con ≥1 caja). Consume cupo del POZO del plan
(max_branches). La primera caja directa es 00001.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/branches" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"local_code\": \"002\",
\"name\": \"Sucursal Heredia\",
\"box_name\": \"Caja Principal\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/branches"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"local_code": "002",
"name": "Sucursal Heredia",
"box_name": "Caja Principal"
};
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/branches';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'local_code' => '002',
'name' => 'Sucursal Heredia',
'box_name' => 'Caja Principal',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Sucursal creada (con su primera caja)):
{
"success": true,
"data": {
"id": "019ee0-...",
"local_code": "002",
"name": "Sucursal Heredia",
"is_direct": true,
"terminals": [
{
"terminal_code": "00002",
"name": "Caja Principal",
"is_active": true
}
]
},
"message": "Sucursal creada.",
"errors": null
}
Example response (422, Sucursal duplicada):
{
"success": false,
"data": null,
"message": "Esa sucursal ya existe.",
"errors": {
"local_code": [
"Ya tenés una sucursal con ese código."
]
}
}
Example response (422, Pozo del plan agotado):
{
"success": false,
"data": null,
"message": "Alcanzó el pozo de 2 sucursal(es) de su plan. Actualice su plan para agregar más.",
"errors": {
"local_code": [
"Alcanzó el pozo de 2 sucursal(es) de su plan. Actualice su plan para agregar más."
]
}
}
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 una sucursal (nombre y/o código)
requires authentication
Cambia el nombre de la sucursal y/o su local_code (posiciones 01-03 del
NumeroConsecutivo). Cambiar el código exige confirmar la contraseña y
migra las secuencias existentes al código nuevo (nunca las reinicia), de
modo que la numeración continúa donde iba. Pensado para cuentas que
migran desde otro sistema de facturación y necesitan conservar su
numeración de sucursal (p. ej. casa matriz 002 en vez de 001).
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Casa Matriz San José\",
\"local_code\": \"002\",
\"reason\": \"Numeración heredada del sistema anterior\",
\"password\": \"MiC0ntr4señ4\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Casa Matriz San José",
"local_code": "002",
"reason": "Numeración heredada del sistema anterior",
"password": "MiC0ntr4señ4"
};
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Casa Matriz San José',
'local_code' => '002',
'reason' => 'Numeración heredada del sistema anterior',
'password' => 'MiC0ntr4señ4',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Sucursal actualizada):
{
"success": true,
"data": {
"id": "019ee0-...",
"local_code": "002",
"name": "Casa Matriz San José"
},
"message": "Sucursal actualizada.",
"errors": null
}
Example response (404, Sucursal inexistente):
{
"success": false,
"data": null,
"message": "La sucursal no existe.",
"errors": null
}
Example response (422, Código duplicado):
{
"success": false,
"data": null,
"message": "Ya tenés una sucursal con ese código.",
"errors": {
"local_code": [
"Ya tenés una sucursal con ese código."
]
}
}
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.
Agregar una caja a una sucursal
requires authentication
Crea una caja (terminal) en la sucursal directa indicada. El terminal_code
se asigna automáticamente: slot 00 + la siguiente caja libre (los códigos
borrados no se reutilizan). Respeta el tope de cajas por sucursal del plan.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Caja 2\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Caja 2"
};
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Caja 2',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Caja creada):
{
"success": true,
"data": {
"id": "019ee0-...",
"local_code": "001",
"terminals": [
{
"terminal_code": "00001",
"name": "Caja 1"
},
{
"terminal_code": "00002",
"name": "Caja 2"
}
]
},
"message": "Caja creada.",
"errors": null
}
Example response (404, Sucursal inexistente):
{
"success": false,
"data": null,
"message": "La sucursal no existe.",
"errors": null
}
Example response (422, Tope de cajas alcanzado):
{
"success": false,
"data": null,
"message": "Alcanzó el tope de 3 caja(s) por sucursal de su plan.",
"errors": {
"name": [
"Actualice su plan para agregar más cajas."
]
}
}
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 una caja (nombre y/o código)
requires authentication
Cambia el nombre de la caja y/o su terminal_code (posiciones 04-08 del
NumeroConsecutivo). Cambiar el código exige confirmar la contraseña y
migra las secuencias existentes al código nuevo (nunca las reinicia).
Pensado para cuentas que migran desde otro sistema y necesitan conservar
su numeración de caja (p. ej. 00003 en vez de 00001). No se permite un
código ya reservado por la terminal de un integrador que gestione (o
haya gestionado) esta cuenta.
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"POS Entrada\",
\"terminal_code\": \"00003\",
\"delegated_issuance\": true,
\"reason\": \"Numeración heredada del sistema anterior\",
\"password\": \"MiC0ntr4señ4\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "POS Entrada",
"terminal_code": "00003",
"delegated_issuance": true,
"reason": "Numeración heredada del sistema anterior",
"password": "MiC0ntr4señ4"
};
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'POS Entrada',
'terminal_code' => '00003',
'delegated_issuance' => true,
'reason' => 'Numeración heredada del sistema anterior',
'password' => 'MiC0ntr4señ4',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Caja actualizada):
{
"success": true,
"data": {
"id": "019ee0-...",
"terminals": [
{
"terminal_code": "00003",
"name": "POS Entrada"
}
]
},
"message": "Caja actualizada.",
"errors": null
}
Example response (404, Caja inexistente):
{
"success": false,
"data": null,
"message": "La caja no existe.",
"errors": null
}
Example response (422, Código en uso):
{
"success": false,
"data": null,
"message": "Ya tenés una caja con ese código.",
"errors": {
"terminal_code": [
"Ya tenés una caja con ese código."
]
}
}
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 caja
requires authentication
Borra (soft delete) una caja de la sucursal. No se puede eliminar la última caja activa de la sucursal (toda sucursal debe conservar ≥1 caja). El código de la caja borrada no se reutiliza.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff" \
--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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff"
);
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff';
$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, Caja eliminada):
{
"success": true,
"data": null,
"message": "Caja eliminada.",
"errors": null
}
Example response (404, Caja inexistente):
{
"success": false,
"data": null,
"message": "La caja no existe.",
"errors": null
}
Example response (409, Es la última caja de la sucursal):
{
"success": false,
"data": null,
"message": "Debés mantener al menos una caja en la sucursal.",
"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 una sucursal
requires authentication
Borra (soft delete) la sucursal junto con sus cajas. No se puede eliminar la última sucursal del contribuyente (siempre debe quedar ≥1).
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee" \
--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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee"
);
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee';
$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, Sucursal eliminada):
{
"success": true,
"data": null,
"message": "Sucursal eliminada.",
"errors": null
}
Example response (404, Sucursal inexistente):
{
"success": false,
"data": null,
"message": "La sucursal no existe.",
"errors": null
}
Example response (409, Es la última sucursal):
{
"success": false,
"data": null,
"message": "Debés mantener al menos una sucursal.",
"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/Almendro",
"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\\/Almendro\",
\"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\/Almendro",
"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/Almendro',
'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/Almendro",
"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/Almendro",
"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.
Enviar un evento de prueba
requires authentication
Envía SÍNCRONAMENTE un payload webhook.test al endpoint, firmado con la
misma HMAC-SHA256 y los mismos headers X-Almendro-* que las entregas
reales. Sirve para verificar que el receptor está accesible, valida la
firma y responde 2xx, sin tener que emitir un comprobante real.
La entrega queda registrada en el historial (/webhooks/{id}/logs) pero
NO afecta los contadores de salud del endpoint (no auto-desactiva ni
resetea fallos reales).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a/test" \
--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/test"
);
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/webhooks/9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a/test';
$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, delivered):
{
"success": true,
"data": {
"success": true,
"response_status": 200,
"response_time_ms": 142,
"error_message": null,
"delivery_id": "…"
},
"message": "Evento de prueba entregado correctamente.",
"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 150 (IVA mensual) y 101/102/103 (utilidades).
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.
Ventas por período
Ventas agrupadas por periodo.
requires authentication
Retorna series temporales de ventas agrupadas por dia, semana o mes.
La agrupacion se controla con el parametro group_by (day, week, month)
y se auto-detecta segun el rango de fechas si no se especifica.
Util para graficos de tendencia en el dashboard y para verificar datos antes de la declaracion 150 mensual.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/sales-by-period?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=4&status[]=draft¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=4&payment_method[]=6&group_by=month&limit=10" \
--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/sales-by-period"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "4",
"status[0]": "draft",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "4",
"payment_method[0]": "6",
"group_by": "month",
"limit": "10",
};
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/sales-by-period';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '4',
'status[0]' => 'draft',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '4',
'payment_method[0]' => '6',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"data": {
"period": {
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"currency_code": "CRC",
"environment": "production"
},
"group_by": "week",
"data": [
{
"period_start": "2026-03-02",
"period_label": "Sem. 02 mar 2026",
"count": 48,
"total_venta": "3120500.00000",
"total_descuentos": "45000.00000",
"total_impuesto": "399815.00000",
"total_comprobante": "3475315.00000"
},
{
"period_start": "2026-03-09",
"period_label": "Sem. 09 mar 2026",
"count": 52,
"total_venta": "3410820.50000",
"total_descuentos": "38000.00000",
"total_impuesto": "438846.67000",
"total_comprobante": "3811667.17000"
},
{
"period_start": "2026-03-16",
"period_label": "Sem. 16 mar 2026",
"count": 46,
"total_venta": "2719000.00000",
"total_descuentos": "42000.00000",
"total_impuesto": "298880.00000",
"total_comprobante": "2975880.00000"
}
],
"grand_total": {
"count": 146,
"total_venta": "9250320.50000",
"total_impuesto": "1137541.67000",
"total_comprobante": "10262862.17000"
}
},
"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.
Ventas por receptor
Ranking de ventas por receptor.
requires authentication
Retorna los receptores (clientes) con mayor monto facturado en el
periodo, ordenados de mayor a menor. El parametro limit controla
cuantos registros retornar (default 10, maximo 100).
Util para identificar clientes principales y para conciliacion fiscal por cedula de receptor.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/sales-by-receiver?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=9&status[]=pending¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=11&payment_method[]=2&group_by=month&limit=10" \
--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/sales-by-receiver"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "9",
"status[0]": "pending",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "11",
"payment_method[0]": "2",
"group_by": "month",
"limit": "10",
};
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/sales-by-receiver';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '9',
'status[0]' => 'pending',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '11',
'payment_method[0]' => '2',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"data": {
"period": {
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"currency_code": "CRC",
"environment": "production"
},
"limit": 10,
"data": [
{
"receiver_id_type": "02",
"receiver_id_number": "3101123456",
"receiver_name": "Comercializadora La Catedral S.A.",
"count": 24,
"total_venta": "2980000.00000",
"total_impuesto": "387400.00000",
"total_comprobante": "3367400.00000",
"percentage": 32.81
},
{
"receiver_id_type": "02",
"receiver_id_number": "3101998877",
"receiver_name": "Inversiones del Valle S.R.L.",
"count": 18,
"total_venta": "1740500.50000",
"total_impuesto": "226265.07000",
"total_comprobante": "1966765.57000",
"percentage": 19.16
},
{
"receiver_id_type": "01",
"receiver_id_number": "108880777",
"receiver_name": "María Fernanda Rojas Alvarado",
"count": 11,
"total_venta": "845000.00000",
"total_impuesto": "109850.00000",
"total_comprobante": "954850.00000",
"percentage": 9.3
}
],
"others": {
"count": 93,
"total_comprobante": "3973846.60000",
"percentage": 38.73
},
"grand_total": {
"count": 146,
"total_comprobante": "10262862.17000",
"unique_receivers": 47
}
},
"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.
Ventas por actividad
Ventas agrupadas por actividad economica.
requires authentication
Retorna montos y conteos agrupados por codigo de actividad economica CIIU del emisor. Esencial para la declaracion 150 que requiere reportar IVA desglosado por actividad economica.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/sales-by-activity?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=8&status[]=sent¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=10&payment_method[]=6&group_by=month&limit=10" \
--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/sales-by-activity"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "8",
"status[0]": "sent",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "10",
"payment_method[0]": "6",
"group_by": "month",
"limit": "10",
};
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/sales-by-activity';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '8',
'status[0]' => 'sent',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '10',
'payment_method[0]' => '6',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"data": {
"period": {
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"currency_code": "CRC",
"environment": "production"
},
"data": [
{
"activity_code": "620100",
"activity_description": "Actividades de programación informática",
"count": 86,
"total_venta": "6120000.00000",
"total_impuesto": "795600.00000",
"total_comprobante": "6915600.00000",
"percentage": 67.38
},
{
"activity_code": "702000",
"activity_description": "Actividades de consultoría de gestión",
"count": 41,
"total_venta": "2380320.50000",
"total_impuesto": "309441.67000",
"total_comprobante": "2689762.17000",
"percentage": 26.21
},
{
"activity_code": "631100",
"activity_description": "Procesamiento de datos y hospedaje",
"count": 19,
"total_venta": "750000.00000",
"total_impuesto": "97500.00000",
"total_comprobante": "657500.00000",
"percentage": 6.41
}
],
"grand_total": {
"count": 146,
"total_comprobante": "10262862.17000",
"unique_activities": 3
}
},
"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.
IVA | Desglose por tarifa
Desglose de IVA por tarifa | datos para la declaracion 150.
requires authentication
Retorna el impuesto agrupado por codigo de tarifa IVA (0%, 1%, 2%, 4%, 13%, exento, etc.). La declaracion 150 ante Hacienda requiere reportar el IVA desglosado exactamente por estas tarifas.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/tax-summary?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=2&status[]=draft¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=4&payment_method[]=99&group_by=month&limit=10" \
--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/tax-summary"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "2",
"status[0]": "draft",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "4",
"payment_method[0]": "99",
"group_by": "month",
"limit": "10",
};
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/tax-summary';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '2',
'status[0]' => 'draft',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '4',
'payment_method[0]' => '99',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"data": {
"period": {
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"currency_code": "CRC",
"environment": "production"
},
"by_tax_code": [
{
"tax_code": "01",
"tax_code_label": "IVA",
"total_monto": "1118041.67000",
"voucher_count": 138,
"breakdown_by_rate": [
{
"rate_code": "08",
"rate_label": "13% General",
"rate_percentage": 13,
"total_monto": "1095541.67000",
"voucher_count": 132
},
{
"rate_code": "04",
"rate_label": "4% Bienes y servicios especiales",
"rate_percentage": 4,
"total_monto": "22500.00000",
"voucher_count": 6
}
]
},
{
"tax_code": "07",
"tax_code_label": "Impuesto específico",
"total_monto": "19500.00000",
"voucher_count": 8,
"breakdown_by_rate": [
{
"rate_code": "11",
"rate_label": "Tarifa especial",
"rate_percentage": 0,
"total_monto": "19500.00000",
"voucher_count": 8
}
]
}
],
"totals": {
"total_impuesto": "1137541.67000",
"total_iva_devuelto": "0.00000",
"voucher_count": 146
}
},
"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.
MensajeReceptor
Resumen de confirmaciones de receptor (MensajeReceptor).
requires authentication
Retorna el conteo de mensajes de receptor agrupados por estado: confirmados, pendientes y vencidos. Incluye alertas de plazo segun el art. 15 del Reglamento (8 dias habiles para confirmar).
Este endpoint solo acepta los filtros date_from, date_to y
environment. Los demas filtros (voucher_type, currency_code, etc.)
no aplican a mensajes de receptor.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/receiver-messages?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=2&status[]=accepted¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=10&payment_method[]=4&group_by=month&limit=10" \
--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/receiver-messages"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "2",
"status[0]": "accepted",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "10",
"payment_method[0]": "4",
"group_by": "month",
"limit": "10",
};
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/receiver-messages';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '2',
'status[0]' => 'accepted',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '10',
'payment_method[0]' => '4',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"data": {
"period": {
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"environment": "production"
},
"summary": {
"total": 64,
"by_status": {
"accepted": 58,
"sent": 3,
"draft": 2,
"error": 1
},
"by_message": {
"accepted_total": 55,
"accepted_partial": 3,
"rejected": 2
}
},
"deadline_alerts": {
"overdue": 2,
"due_soon": 5,
"on_time": 57,
"deadline_business_days": 8,
"alert_threshold_days": 2
},
"by_issuer": [
{
"issuer_id_number": "3101456789",
"count": 31,
"total_invoice": "4120000.00000",
"total_tax": "535600.00000",
"accepted": 29,
"rejected": 1
},
{
"issuer_id_number": "3101778899",
"count": 22,
"total_invoice": "1890500.00000",
"total_tax": "245765.00000",
"accepted": 21,
"rejected": 0
},
{
"issuer_id_number": "108450321",
"count": 11,
"total_invoice": "612000.00000",
"total_tax": "79560.00000",
"accepted": 8,
"rejected": 1
}
]
},
"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.
Estado Hacienda
Comprobantes agrupados por estado Hacienda.
requires authentication
Retorna conteo y montos agrupados por el estado de confirmacion de Hacienda (aceptado, rechazado, enviado, error, pendiente). Incluye metricas como tasa de aceptacion, tasa de rechazo y conteo de comprobantes pendientes.
A diferencia de otros reportes, este endpoint no filtra por estado (ya que el proposito es mostrar TODOS los estados agrupados).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/vouchers-by-status?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=9&status[]=error¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=14&payment_method[]=99&group_by=month&limit=10" \
--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/vouchers-by-status"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "9",
"status[0]": "error",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "14",
"payment_method[0]": "99",
"group_by": "month",
"limit": "10",
};
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/vouchers-by-status';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '9',
'status[0]' => 'error',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '14',
'payment_method[0]' => '99',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"success": true,
"data": {
"period": {
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"currency_code": "CRC",
"environment": "production"
},
"data": [
{
"status": "accepted",
"status_label": "Aceptado",
"count": 138,
"total_venta": "8920320.50000",
"total_impuesto": "1118041.67000",
"total_comprobante": "9912362.17000",
"percentage_count": 94.52,
"percentage_amount": 96.58
},
{
"status": "rejected",
"status_label": "Rechazado",
"count": 4,
"total_venta": "210000.00000",
"total_impuesto": "16000.00000",
"total_comprobante": "275500.00000",
"percentage_count": 2.74,
"percentage_amount": 2.68
},
{
"status": "pending",
"status_label": "Pendiente",
"count": 3,
"total_venta": "108000.00000",
"total_impuesto": "3500.00000",
"total_comprobante": "62500.00000",
"percentage_count": 2.05,
"percentage_amount": 0.61
},
{
"status": "error",
"status_label": "Error",
"count": 1,
"total_venta": "12000.00000",
"total_impuesto": "0.00000",
"total_comprobante": "12500.00000",
"percentage_count": 0.68,
"percentage_amount": 0.12
}
],
"grand_total": {
"count": 146,
"total_comprobante": "10262862.17000"
},
"metrics": {
"acceptance_rate": 94.52,
"rejection_rate": 2.74,
"pending_count": 3,
"error_count": 1,
"avg_processing_time_hours": 1.4
}
},
"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.
IVA | Auxiliar D-150
Auxiliar del IVA (D-150): ventas y compras por tarifa.
requires authentication
Produce los dos auxiliares de la declaracion autoliquidativa 150 de TRIBU-CR: ventas generales por tarifa (base + impuesto), ventas sin IVA, compras por tarifa (importe + impuesto soportado), y el detalle por cliente y por proveedor. Solo comprobantes ACEPTADOS; las notas de credito restan.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/iva-auxiliary?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=1&status[]=accepted¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=10&payment_method[]=6&group_by=month&limit=10" \
--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/iva-auxiliary"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "1",
"status[0]": "accepted",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "10",
"payment_method[0]": "6",
"group_by": "month",
"limit": "10",
};
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/iva-auxiliary';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '1',
'status[0]' => 'accepted',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '10',
'payment_method[0]' => '6',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Exporta el auxiliar del IVA (D-150) en CSV o PDF.
requires authentication
format=csv§ion=sales|purchases descarga el auxiliar pedido en CSV; format=pdf descarga el documento completo (ventas + compras) listo para imprimir o adjuntar al expediente del periodo.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/iva-auxiliary/export?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=9&status[]=rejected¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=9&payment_method[]=6&group_by=month&limit=10" \
--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/iva-auxiliary/export"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "9",
"status[0]": "rejected",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "9",
"payment_method[0]": "6",
"group_by": "month",
"limit": "10",
};
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/iva-auxiliary/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '9',
'status[0]' => 'rejected',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '9',
'payment_method[0]' => '6',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Export
Exporta un reporte del portal en CSV o PDF.
requires authentication
Reportes: sales-by-period, sales-by-receiver, sales-by-activity. format=csv descarga la tabla; format=pdf el documento membretado.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/sales-by-period|sales-by-receiver|sales-by-activity/export?date_from=2026-08-01&date_to=2026-08-11&voucher_type[]=3&status[]=sent¤cy_code=CRC&environment=production&receiver_id_number=3101234567&sale_condition[]=11&payment_method[]=4&group_by=month&limit=10" \
--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/sales-by-period|sales-by-receiver|sales-by-activity/export"
);
const params = {
"date_from": "2026-08-01",
"date_to": "2026-08-11",
"voucher_type[0]": "3",
"status[0]": "sent",
"currency_code": "CRC",
"environment": "production",
"receiver_id_number": "3101234567",
"sale_condition[0]": "11",
"payment_method[0]": "4",
"group_by": "month",
"limit": "10",
};
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/sales-by-period|sales-by-receiver|sales-by-activity/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'date_from' => '2026-08-01',
'date_to' => '2026-08-11',
'voucher_type[0]' => '3',
'status[0]' => 'sent',
'currency_code' => 'CRC',
'environment' => 'production',
'receiver_id_number' => '3101234567',
'sale_condition[0]' => '11',
'payment_method[0]' => '4',
'group_by' => 'month',
'limit' => '10',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Filtros
Facets para los filtros de reportes.
requires authentication
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/reports/filters" \
--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/filters"
);
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/filters';
$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));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.
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.
Catálogo de monedas activas (ISO 4217) para el selector de moneda en la emisión de comprobantes.
requires authentication
Las más usadas en Costa Rica (CRC, USD, EUR) se devuelven primero; el
resto en orden alfabético por código. bccr_rate trae el tipo de cambio
de referencia del BCCR cuando aplica (USD), para prellenar el campo de
tipo de cambio en la UI; si es null, el usuario lo ingresa a mano.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/catalogs/currencies" \
--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/currencies"
);
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/currencies';
$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));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 ISO 4217 (3 letras). Example: USD
name
string
Nombre de la moneda. Example: Dólar Americano
requires_bccr_rate
boolean
Si exige tipo de cambio (≠ CRC).
bccr_rate
string|null
Tipo de cambio de referencia BCCR. Example: 530.50000
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 Almendro. 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 Almendro.
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 "Idempotency-Key: Identificador único de ESTA confirmación, generado por usted. Es" \
--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}",
"Idempotency-Key": "Identificador único de ESTA confirmación, generado por usted. Es",
"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}',
'Idempotency-Key' => 'Identificador único de ESTA confirmación, generado por usted. Es',
'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": "3101999999",
"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 Almendro. 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).
Buzón de Recibidos
Listar comprobantes del buzón de recibidos
requires authentication
Devuelve, paginados, los comprobantes que el contribuyente ha
recibido como receptor: tanto los emitidos por otros usuarios de
Almendro como los cargados manualmente (XML de otro facturador). Es un
listado de solo lectura | la aceptación o rechazo se hace por el
flujo de POST /receiver/confirm (MensajeReceptor).
Cada fila expone solo la información pública del documento (Art. 16): emisor, clave, tipo, fecha, moneda y totales, junto al plazo de respuesta (8 días hábiles), el estado de respuesta del receptor y el origen del comprobante en el buzón.
La búsqueda por texto (search) filtra por clave, nombre del emisor o
número de identificación del emisor.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/inbox?search=3101123456&per_page=20&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/inbox"
);
const params = {
"search": "3101123456",
"per_page": "20",
"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/inbox';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'search' => '3101123456',
'per_page' => '20',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Listado paginado):
{
"success": true,
"data": [
{
"voucher_key": "50601012600310112345600100001010000000001199999999",
"consecutive_number": "00100001010000000001",
"voucher_type": "01",
"voucher_type_label": "Factura Electrónica",
"issued_at": "2026-06-01T10:00:00-06:00",
"currency_code": "CRC",
"emisor": {
"name": "PROVEEDOR EJEMPLO S.A.",
"id_number": "3101123456"
},
"alias": "Hosting junio",
"notes": "Pagado con tarjeta corporativa",
"xsd_valid": null,
"totals": {
"total_impuesto": "1300.00000",
"total_comprobante": "11300.00000"
},
"deadline": {
"deadline_at": "2026-06-11",
"remaining_business_days": 5,
"overdue": false
},
"status": "accepted",
"environment": "prod",
"source": "almendro",
"source_label": "Emitido en Almendro",
"response_status": "pending",
"response_status_label": "Pendiente",
"is_automatic": false,
"confirmation": {
"confirmed": false,
"message": null,
"message_label": null,
"status": null
},
"line_items": null
}
],
"message": "Comprobantes recibidos (buzón).",
"errors": null,
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 20,
"total": 45,
"from": 1,
"to": 20
},
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
}
}
Example response (200, Buzón vacío):
{
"success": true,
"data": [],
"message": "Comprobantes recibidos (buzón).",
"errors": null,
"meta": {
"current_page": 1,
"last_page": 1,
"per_page": 20,
"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.
Cargar un comprobante externo al buzón
requires authentication
Sube el XML firmado de un comprobante (generado por cualquier
facturador, dentro o fuera de Almendro) al buzón del receptor. El
sistema parsea el XML, valida que el receptor del documento sea el
contribuyente autenticado (la cédula del receptor debe coincidir con la
del certificado .p12) y consulta su estado en Hacienda.
Si el comprobante ya estaba en el buzón, la carga refresca su estado en vez de duplicarlo.
Envíe el XML de una de estas dos formas (al menos una es obligatoria):
xml_file: el archivo.xml(multipart/form-data).xml: el contenido del XML como string (JSON o form).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/inbox/upload" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"xml\": \"<FacturaElectronica>...<\\/FacturaElectronica>\",
\"source\": \"api_upload\",
\"target_contributor_id\": \"a4855dc5-0acb-33c3-b921-f4291f719ca0\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/inbox/upload"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"xml": "<FacturaElectronica>...<\/FacturaElectronica>",
"source": "api_upload",
"target_contributor_id": "a4855dc5-0acb-33c3-b921-f4291f719ca0"
};
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/inbox/upload';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'xml' => '<FacturaElectronica>...</FacturaElectronica>',
'source' => 'api_upload',
'target_contributor_id' => 'a4855dc5-0acb-33c3-b921-f4291f719ca0',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Comprobante cargado):
{
"success": true,
"data": {
"voucher_key": "50601012600310112345600100001010000000001199999999",
"consecutive_number": "00100001010000000001",
"voucher_type": "01",
"voucher_type_label": "Factura Electrónica",
"issued_at": "2026-06-01T10:00:00-06:00",
"currency_code": "CRC",
"emisor": {
"name": "PROVEEDOR EJEMPLO S.A.",
"id_number": "3101123456"
},
"alias": null,
"notes": null,
"xsd_valid": true,
"totals": {
"total_impuesto": "1300.00000",
"total_comprobante": "11300.00000"
},
"deadline": {
"deadline_at": "2026-06-11",
"remaining_business_days": 5,
"overdue": false
},
"status": "accepted",
"environment": "prod",
"source": "api_upload",
"source_label": "Cargado por API",
"response_status": "pending",
"response_status_label": "Pendiente",
"is_automatic": false,
"confirmation": {
"confirmed": false,
"message": null,
"message_label": null,
"status": null
},
"line_items": null
},
"message": "Comprobante cargado en el buzón.",
"errors": null
}
Example response (422, Falta el XML):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"xml": [
"Debe enviar el comprobante en el campo \"xml\" (string) o \"xml_file\" (archivo)."
]
}
}
Example response (422, No está a nombre del contribuyente):
{
"success": false,
"data": null,
"message": "El comprobante no está a nombre de su contribuyente.",
"errors": {
"xml": [
"El comprobante no está a nombre de su contribuyente."
]
}
}
Example response (422, XML inválido o no respondible):
{
"success": false,
"data": null,
"message": "No se pudo procesar el XML del comprobante.",
"errors": {
"xml": [
"No se pudo procesar el XML del 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.
Cargar comprobantes externos en lote
requires authentication
Carga masiva al buzón: procesa cada comprobante de forma independiente, de modo que un XML inválido o ajeno no aborta el lote | el resultado se reporta por ítem.
Envíe el lote de una o ambas de estas formas (al menos una es obligatoria):
files[]: varios archivos.xml(multipart/form-data), máximo 50.zip: un archivo.zipcuyas entradas.xmlse procesan una a una.
El total de comprobantes procesados se limita a 50 (archivos sueltos + entradas del ZIP). Siempre responde HTTP 200 con el detalle por ítem y un resumen, aunque algunos o todos hayan fallado.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/inbox/upload-batch" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"source\": \"api_upload\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/inbox/upload-batch"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"source": "api_upload"
};
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/inbox/upload-batch';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'source' => 'api_upload',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Lote procesado (con un fallo)):
{
"success": true,
"data": {
"results": [
{
"name": "factura-001.xml",
"status": "ok",
"voucher_key": "50601012600310112345600100001010000000001199999999",
"message": null
},
{
"name": "factura-002.xml",
"status": "error",
"voucher_key": null,
"message": "El comprobante no está a nombre de su contribuyente."
}
],
"summary": {
"total": 2,
"ok": 1,
"failed": 1
}
},
"message": "Lote procesado: 1 de 2 comprobante(s) cargado(s).",
"errors": null
}
Example response (422, Lote vacío):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"files": [
"Debe enviar al menos un archivo en \"files[]\" o un \"zip\" con comprobantes."
]
}
}
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
results
object[]
Resultado individual de cada comprobante del lote.
name
string
Nombre del archivo XML procesado.
status
string
Resultado del ítem: ok (cargado) o error (rechazado).
voucher_key
string
Clave del comprobante cargado. null si el ítem falló.
message
string
Mensaje de error del ítem. null si el ítem se cargó correctamente.
summary
object
Resumen agregado del lote.
total
integer
Total de comprobantes procesados.
ok
integer
Cantidad cargada correctamente.
failed
integer
Cantidad que falló.
Validar una clave contra Hacienda
requires authentication
Consulta directamente en Hacienda el estado de una clave (aceptado / rechazado), incluso de comprobantes emitidos por otros facturadores y que no estén en su buzón. Sirve para validar la factura de un proveedor antes de aceptarla.
La consulta usa el certificado activo del contribuyente para el ambiente en curso. Si no hay certificado activo para ese ambiente, responde HTTP 422. Si Hacienda no responde, responde HTTP 502.
El campo in_inbox indica si esa clave ya figura como recibida por el
contribuyente (su cédula es la del receptor del comprobante).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/inbox/lookup/50601012600310112345600100001010000000001199999999" \
--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/inbox/lookup/50601012600310112345600100001010000000001199999999"
);
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/inbox/lookup/50601012600310112345600100001010000000001199999999';
$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, Clave encontrada en Hacienda):
{
"success": true,
"data": {
"clave": "50601012600310112345600100001010000000001199999999",
"found": true,
"ind_estado": "aceptado",
"detail": "Comprobante aceptado por Hacienda.",
"in_inbox": false
},
"message": "Estado consultado en Hacienda.",
"errors": null
}
Example response (200, Clave no registrada en Hacienda):
{
"success": true,
"data": {
"clave": "50601012600310112345600100001010000000001199999999",
"found": false,
"ind_estado": null,
"detail": null,
"in_inbox": false
},
"message": "Hacienda no tiene registro de esta clave.",
"errors": null
}
Example response (422, Sin certificado activo):
{
"success": false,
"data": null,
"message": "No hay un certificado activo para consultar a Hacienda en este ambiente.",
"errors": {
"certificate": [
"Falta el certificado de sandbox."
]
}
}
Example response (502, Hacienda no disponible):
{
"success": false,
"data": null,
"message": "No se pudo consultar a Hacienda: Connection timed out.",
"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
clave
string
La clave consultada (eco del parámetro de URL).
found
boolean
true si Hacienda tiene registro de la clave.
ind_estado
string
Indicador de estado devuelto por Hacienda (ej. aceptado, rechazado). null si no se encontró.
detail
string
Detalle textual del estado devuelto por Hacienda. null si no se encontró.
in_inbox
boolean
true si esa clave ya figura como comprobante recibido por el contribuyente.
Consultar un comprobante del buzón por clave
requires authentication
Devuelve el detalle de un comprobante recibido, identificado por su
clave (50 dígitos). A diferencia del listado, el detalle incluye
además las líneas del comprobante (line_items): provienen del
comprobante interno cuando el emisor es de Almendro, o del snapshot
parseado del XML cuando es un comprobante externo.
Solo se retornan comprobantes que están en el buzón del contribuyente autenticado. Si la clave no está en su buzón, responde HTTP 404.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/inbox/50601012600310112345600100001010000000001199999999" \
--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/inbox/50601012600310112345600100001010000000001199999999"
);
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/inbox/50601012600310112345600100001010000000001199999999';
$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": "50601012600310112345600100001010000000001199999999",
"consecutive_number": "00100001010000000001",
"voucher_type": "01",
"voucher_type_label": "Factura Electrónica",
"issued_at": "2026-06-01T10:00:00-06:00",
"currency_code": "CRC",
"emisor": {
"name": "PROVEEDOR EJEMPLO S.A.",
"id_number": "3101123456"
},
"alias": "Hosting junio",
"notes": "Pagado con tarjeta corporativa",
"xsd_valid": null,
"totals": {
"total_impuesto": "1300.00000",
"total_comprobante": "11300.00000"
},
"deadline": {
"deadline_at": "2026-06-11",
"remaining_business_days": 5,
"overdue": false
},
"status": "accepted",
"environment": "prod",
"source": "almendro",
"source_label": "Emitido en Almendro",
"response_status": "pending",
"response_status_label": "Pendiente",
"is_automatic": false,
"confirmation": {
"confirmed": false,
"message": null,
"message_label": null,
"status": null
},
"line_items": [
{
"line_number": 1,
"detail": "Servicio de hosting mensual",
"quantity": "1.00000",
"unit_price": "10000.00000",
"total_line_amount": "10000.00000"
}
]
},
"message": "Detalle del comprobante recibido.",
"errors": null
}
Example response (404, No está en el buzón):
{
"success": false,
"data": null,
"message": "El comprobante no está en su buzó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.
Response
Response Fields
data
object
voucher_key
string
Clave de 50 dígitos del comprobante. Identificador único del documento ante Hacienda.
consecutive_number
string
Consecutivo del comprobante. null si es externo y no se pudo resolver el comprobante interno.
voucher_type
string
Tipo de comprobante: 01=Factura, 02=Nota Débito, 03=Nota Crédito, 04=Tiquete, 08=FEC, etc.
voucher_type_label
string
Nombre legible del tipo de comprobante en español. null si el tipo no es reconocido.
issued_at
string
Fecha de emisión del documento (ISO 8601).
currency_code
string
Código de moneda del comprobante (ej. CRC, USD).
receiver_activity_code
string
CodigoActividadReceptor declarado por el emisor (formato XXXX.X o 6 dígitos). Es la actividad que debe usarse como activity_code al responder con MensajeReceptor. null si el comprobante no lo trae: en ese caso el receptor elige su actividad.
emisor
object
Datos públicos del emisor (dueño del documento).
name
string
Nombre o razón social del emisor.
id_number
string
Número de identificación del emisor.
alias
string
Alias privado que el receptor le puso al comprobante. null si no tiene. No fiscal.
notes
string
Observaciones internas privadas del receptor. null si no tiene. No fiscal.
xsd_valid
boolean
Resultado de validar el XML cargado contra el XSD v4.4. null=no validado (internos), true=válido, false=no válido.
totals
object
Totales del comprobante.
total_impuesto
string
Total de impuestos del comprobante.
total_comprobante
string
Total general del comprobante.
deadline
object
Plazo de respuesta del receptor (8 días hábiles, Art. 15 del Reglamento).
deadline_at
string
Fecha límite para responder (Y-m-d). null si no se pudo calcular.
remaining_business_days
integer
Días hábiles restantes para responder. null si no aplica.
overdue
boolean
true si ya venció el plazo de respuesta. null si no aplica.
status
string
Estado fiscal del comprobante: el del comprobante interno (puede ser cancelled) o el ind-estado de Hacienda para los externos.
environment
string
Ambiente del comprobante: sandbox o prod.
source
string
Origen del comprobante en el buzón: almendro, gui_upload, api_upload, email_inbound (recibido por correo en facturas@almendro.cr).
source_label
string
Nombre legible del origen del comprobante.
response_status
string
Estado de respuesta del receptor: pending, accepted, partial, rejected, tacit.
response_status_label
string
Nombre legible del estado de respuesta.
is_automatic
boolean
true si la aceptación fue tácita por vencimiento del plazo (sin tratamiento manual).
confirmation
object
Datos del MensajeReceptor de este receptor, si respondió.
confirmed
boolean
true si el receptor ya resolvió (aceptó o rechazó) el comprobante.
message
string
Código del mensaje de respuesta del receptor. null si no ha respondido.
message_label
string
Nombre legible del mensaje de respuesta. null si no ha respondido.
status
string
Estado del envío del MensajeReceptor a Hacienda. null si no ha respondido.
line_items
object[]
Líneas de detalle del comprobante. Solo presente en el detalle (no en el listado). null si no hay líneas disponibles.
null si no hay líneas disponibles.line_number
integer
Número de línea.
detail
string
Descripción de la línea.
quantity
string
Cantidad de la línea.
unit_price
string
Precio unitario de la línea.
total_line_amount
string
Importe total de la línea.
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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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,
\"identifier_color\": \"#fEEeDb\"
}"
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,
"identifier_color": "#fEEeDb"
};
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,
'identifier_color' => '#fEEeDb',
],
]
);
$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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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/phpqot0fgk5vbs7bodOCQ1" 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/phpqot0fgk5vbs7bodOCQ1', '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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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.),
Almendro 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=&terminal_code=01001" \
--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",
"terminal_code": "01001",
};
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',
'terminal_code' => '01001',
],
]
);
$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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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 Almendro 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\",
\"terminal_code\": \"01001\",
\"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",
"terminal_code": "01001",
"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',
'terminal_code' => '01001',
'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→ Almendro 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.
Listar sucursales y cajas de un cliente gestionado
requires authentication
Devuelve las sucursales (branches) + cajas (terminals) que ESTE integrador
creó para el cliente. Las cajas usan el slot del integrador en el
terminal_code (= [slot 2 díg.][caja 3 díg.]).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches" \
--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/branches"
);
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/branches';
$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, Sucursales del cliente):
{
"success": true,
"data": [
{
"id": "019ee0-...",
"local_code": "001",
"name": "Casa Matriz",
"is_direct": false,
"terminals": [
{
"terminal_code": "01001",
"slot": "01",
"box": "001",
"name": "Caja 1"
}
]
}
],
"message": "Sucursales del cliente gestionado.",
"errors": null
}
Example response (404, Cliente no gestionado):
{
"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.
Crear una sucursal para un cliente gestionado
requires authentication
Crea una sucursal del cliente gestionada por el integrador y su primera caja
(en el slot del integrador). Consume cupo del POZO del plan del INTEGRADOR
(max_branches), repartido entre todos sus clientes.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"local_code\": \"001\",
\"name\": \"Casa Matriz\",
\"box_name\": \"Caja 1\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"local_code": "001",
"name": "Casa Matriz",
"box_name": "Caja 1"
};
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/branches';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'local_code' => '001',
'name' => 'Casa Matriz',
'box_name' => 'Caja 1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Sucursal creada (con su primera caja en el slot)):
{
"success": true,
"data": {
"id": "019ee0-...",
"local_code": "001",
"name": "Casa Matriz",
"is_direct": false,
"terminals": [
{
"terminal_code": "01001",
"slot": "01",
"name": "Caja 1"
}
]
},
"message": "Sucursal creada.",
"errors": null
}
Example response (422, Pozo del integrador agotado):
{
"success": false,
"data": null,
"message": "Alcanzó el pozo de 50 sucursal(es) de su plan. Actualice su plan para agregar más.",
"errors": {
"local_code": [
"..."
]
}
}
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.
Renombrar una sucursal de un cliente gestionado
requires authentication
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Sucursal Heredia\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Sucursal Heredia"
};
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Sucursal Heredia',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Sucursal renombrada):
{
"success": true,
"data": {
"id": "019ee0-...",
"name": "Sucursal Heredia"
},
"message": "Sucursal actualizada.",
"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.
Agregar una caja a una sucursal de un cliente gestionado
requires authentication
Crea una caja en la sucursal del cliente, en el slot del integrador
(terminal_code = [slot][caja]). Respeta el tope de cajas por sucursal del
plan del integrador.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Caja 2\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Caja 2"
};
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Caja 2',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Caja creada):
{
"success": true,
"data": {
"id": "019ee0-...",
"terminals": [
{
"terminal_code": "01002",
"name": "Caja 2"
}
]
},
"message": "Caja creada.",
"errors": null
}
Example response (422, Tope de cajas alcanzado):
{
"success": false,
"data": null,
"message": "Alcanzó el tope de 20 caja(s) por sucursal de su plan.",
"errors": {
"name": [
"..."
]
}
}
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.
Renombrar una caja de un cliente gestionado
requires authentication
Example request:
curl --request PUT \
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"POS 2\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "POS 2"
};
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'POS 2',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Caja renombrada):
{
"success": true,
"data": {
"id": "019ee0-...",
"terminals": [
{
"terminal_code": "01001",
"name": "POS 2"
}
]
},
"message": "Caja actualizada.",
"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 una caja de un cliente gestionado
requires authentication
No se puede eliminar la última caja activa de la sucursal.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff" \
--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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff"
);
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee/terminals/019ee1bb-cccc-dddd-eeee-ffffffffffff';
$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, Caja eliminada):
{
"success": true,
"data": null,
"message": "Caja eliminada.",
"errors": null
}
Example response (409, Es la última caja de la sucursal):
{
"success": false,
"data": null,
"message": "Debés mantener al menos una caja en la sucursal.",
"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 una sucursal de un cliente gestionado
requires authentication
Borra la sucursal junto con sus cajas. No se puede eliminar la última sucursal que el integrador tiene para ese cliente.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-c001-7288-8ece-fd64da756c01/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee" \
--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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee"
);
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/branches/019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee';
$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, Sucursal eliminada):
{
"success": true,
"data": null,
"message": "Sucursal eliminada.",
"errors": null
}
Example response (409, Es la última sucursal del cliente):
{
"success": false,
"data": null,
"message": "Debés mantener al menos una sucursal 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.
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.
Cambiar el ROL de un gestor sobre el cliente autenticado.
requires authentication
La información del cliente es del cliente: él decide qué ve cada gestor. · accountant (contador) → ve la facturación completa + recibidos. · integrator → solo ve las facturas que él mismo emitió.
Al pasar a integrator se retira el puente GUI (switch-contributor):
los usuarios del gestor dejan de poder operar al cliente por la interfaz.
Example request:
curl --request PATCH \
"https://fe.almendro.cr/api/v1/public/my-integrators/e507fb2b94-c9e62d073743f8140a658fd5f/role" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"role\": \"architecto\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/my-integrators/e507fb2b94-c9e62d073743f8140a658fd5f/role"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"role": "architecto"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/my-integrators/e507fb2b94-c9e62d073743f8140a658fd5f/role';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'role' => 'architecto',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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": "Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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.
Suscripción y Facturación
Consultar el estado de la suscripción
requires authentication
Devuelve el estado completo de billing del contribuyente autenticado: el plan vigente, los datos de la suscripción (ciclo y período pagado), el consumo de comprobantes del mes en curso, el catálogo de planes disponibles para cambiar y, si existe, la baja de plan programada.
El consumo del mes (usage) se calcula sobre los comprobantes de
producción emitidos por el contribuyente y se compara contra el límite
mensual del plan (vouchers_limit puede ser null = ilimitado).
El bloque scheduled_downgrade solo está presente (no null) cuando
el contribuyente programó una baja de plan que se aplicará al fin del
período pagado (ver POST /subscription/downgrade).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/subscription" \
--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/subscription"
);
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/subscription';
$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, Estado de la suscripción):
{
"success": true,
"data": {
"plan": {
"code": "pyme",
"name": "Pyme",
"is_free": false,
"monthly_price_usd": "29.00",
"annual_price_usd": "290.00",
"monthly_voucher_limit": 1000,
"tier": 3,
"max_clients": 300,
"max_items": 1000,
"max_users": 5,
"max_managed_contributors": 0,
"max_branches": 3,
"max_terminals_per_branch": 5,
"has_api": true
},
"subscription": {
"status": "active",
"billing_cycle": "monthly",
"current_period_start": "2026-06-01T00:00:00-06:00",
"current_period_end": "2026-07-01T00:00:00-06:00",
"days_remaining": 8,
"is_past_due": false,
"is_recurring": true
},
"usage": {
"vouchers_used": 142,
"vouchers_limit": 1000
},
"available_plans": [
{
"code": "free",
"name": "Gratis",
"is_free": true,
"monthly_price_usd": "0.00",
"annual_price_usd": "0.00",
"monthly_voucher_limit": 10,
"tier": 1,
"max_clients": 5,
"max_items": 10,
"max_users": 1,
"max_managed_contributors": 0,
"max_branches": 1,
"max_terminals_per_branch": 1,
"has_api": false
}
],
"scheduled_downgrade": null
},
"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
plan
object
Plan vigente del contribuyente. null si no tiene plan asignado.
null si no tiene plan asignado.code
string
Código del plan (ej. free, pyme). Úselo como plan_code en checkout/subscribe/downgrade.
name
string
Nombre legible del plan.
is_free
boolean
true si es el plan gratuito (no requiere pago).
monthly_price_usd
string
Precio mensual en USD.
annual_price_usd
string
Precio anual en USD.
monthly_voucher_limit
integer
Límite mensual de comprobantes. null = ilimitado.
tier
integer
Rango del plan (mayor = plan superior). Sirve para distinguir upgrade de downgrade.
max_clients
integer
Máximo de clientes. null = ilimitado.
max_items
integer
Máximo de productos/servicios. null = ilimitado.
max_users
integer
Máximo de usuarios. null = ilimitado.
max_managed_contributors
integer
Máximo de contribuyentes gestionados (planes de contador). null = ilimitado.
max_branches
integer
Máximo de sucursales. null = ilimitado.
max_terminals_per_branch
integer
Máximo de terminales por sucursal. null = ilimitado.
has_api
boolean
true si el plan incluye acceso a la API pública.
subscription
object
Datos de la suscripción activa. null si no hay suscripción.
null si no hay suscripción.status
string
Estado de la suscripción (ej. active, past_due).
billing_cycle
string
Ciclo de cobro: monthly o annual.
current_period_start
string
Inicio del período pagado actual (ISO 8601).
current_period_end
string
Fin del período pagado actual (ISO 8601).
days_remaining
integer
Días restantes del período pagado.
is_past_due
boolean
true si la suscripción está vencida o en mora.
is_recurring
boolean
true si hay una suscripción recurrente activa en Tilopay (habilita "Cancelar suscripción").
usage
object
Consumo de comprobantes del mes en curso.
vouchers_used
integer
Comprobantes de producción emitidos este mes.
vouchers_limit
integer
Límite mensual del plan. null = ilimitado.
available_plans
object[]
Catálogo de planes activos disponibles para cambiar (misma forma que data.plan).
scheduled_downgrade
object
Baja de plan programada al fin del período. null si no hay ninguna.
null si no hay ninguna.plan
string
Código del plan destino de la baja programada.
plan_name
string
Nombre del plan destino.
at
string
Fecha en que se aplicará la baja (ISO 8601).
Listar el historial de pagos
requires authentication
Devuelve hasta los 100 pagos más recientes del contribuyente (mensualidades y cargos de suscripción), ordenados del más nuevo al más antiguo. Cada pago incluye, si existe, la factura electrónica emitida por Almendro al cobrar.
El campo data es un arreglo plano de pagos (sin paginación). El
bloque invoice es null mientras el comprobante de la mensualidad
aún no se ha emitido o vinculado.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/subscription/payments" \
--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/subscription/payments"
);
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/subscription/payments';
$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, Historial de pagos):
{
"success": true,
"data": [
{
"id": "019d867d-0009-7288-8ece-fd64da756009",
"date": "2026-06-01T08:30:00-06:00",
"type": "subscription",
"status": "paid",
"amount_usd": "29.00",
"currency": "USD",
"amount_local": "15080.00",
"description": "Suscripción Pyme (monthly)",
"period_start": "2026-06-01",
"period_end": "2026-07-01",
"gateway": "tilopay",
"invoice": {
"voucher_key": "50601062600310112345600100001010000000001100000001",
"status": "accepted",
"hacienda_status": "aceptado"
}
},
{
"id": "019d867d-0008-7288-8ece-fd64da756008",
"date": "2026-05-01T08:30:00-06:00",
"type": "subscription",
"status": "paid",
"amount_usd": "29.00",
"currency": "USD",
"amount_local": "15010.00",
"description": "Suscripción Pyme (monthly)",
"period_start": "2026-05-01",
"period_end": "2026-06-01",
"gateway": "tilopay",
"invoice": null
}
],
"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[]
Lista de pagos (máximo 100, más recientes primero).
id
string
UUID del pago.
date
string
Fecha del pago (paid_at o created_at, ISO 8601).
type
string
Tipo de pago (ej. subscription).
status
string
Estado del pago (ej. paid, pending, failed).
amount_usd
string
Monto cobrado en USD.
currency
string
Moneda del cobro.
amount_local
string
Monto en moneda local (colones) si aplica. Puede ser null.
description
string
Descripción legible del cargo.
period_start
string
Inicio del período cubierto (fecha). Puede ser null.
period_end
string
Fin del período cubierto (fecha). Puede ser null.
gateway
string
Pasarela usada para el cobro (ej. tilopay).
invoice
object
Factura electrónica del cobro. null si aún no se ha emitido o vinculado.
null si aún no se ha emitido o vinculado.voucher_key
string
Clave de 50 dígitos del comprobante.
status
string
Estado interno del comprobante.
hacienda_status
string
Estado del comprobante en Hacienda.
Cancelar una baja de plan programada
requires authentication
Revierte una baja de plan programada (downgrade o cancelación de suscripción aún no ejecutada) y conserva el plan actual sin cambios ni cobros nuevos. Solo tiene efecto mientras la baja no se haya aplicado.
Por seguridad exige la contraseña del usuario autenticado. Requiere que exista una baja programada; si no, responde 404.
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/subscription/scheduled-downgrade" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"password\": \"mi-contrasena-secreta\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/scheduled-downgrade"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"password": "mi-contrasena-secreta"
};
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/subscription/scheduled-downgrade';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'password' => 'mi-contrasena-secreta',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Baja cancelada):
{
"success": true,
"data": null,
"message": "Baja de plan cancelada. Mantenés tu plan actual.",
"errors": null
}
Example response (404, Sin baja programada):
{
"success": false,
"data": null,
"message": "No hay una baja de plan programada.",
"errors": null
}
Example response (422, Contraseña incorrecta):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"password": [
"La contraseña es incorrecta."
]
}
}
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.
Iniciar el pago único de un plan (checkout)
requires authentication
Inicia el checkout de Tilopay para contratar un plan de pago por un
único período (mensual o anual). Crea una sesión de pago en Tilopay,
registra un pago PENDIENTE y devuelve la redirect_url a la que su
backend debe redirigir al cliente para completar el pago.
La activación del plan no ocurre en esta respuesta: sucede en el retorno firmado de Tilopay una vez que el pago se confirma.
Pagar no exige contraseña (el cliente entrega dinero, no hay riesgo). El plan gratis no puede contratarse por esta vía.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/checkout" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"plan_code\": \"pyme\",
\"billing_cycle\": \"monthly\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/checkout"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"plan_code": "pyme",
"billing_cycle": "monthly"
};
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/subscription/checkout';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'plan_code' => 'pyme',
'billing_cycle' => 'monthly',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Sesión de pago creada):
{
"success": true,
"data": {
"redirect_url": "https://app.tilopay.com/checkout/abc123",
"order": "TPAY-019d867d-0001"
},
"message": "Redirigiendo a la pasarela de pago.",
"errors": null
}
Example response (422, Plan no disponible):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"plan_code": [
"El plan solicitado no existe o no está disponible."
]
}
}
Example response (422, Plan gratuito):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"plan_code": [
"El plan gratuito no requiere pago."
]
}
}
Example response (422, Sin precio para el ciclo):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"billing_cycle": [
"El plan no tiene precio para el ciclo seleccionado."
]
}
}
Example response (502, Fallo de la pasarela):
{
"success": false,
"data": null,
"message": "No se pudo iniciar el pago con Tilopay. Intente de nuevo en unos minutos.",
"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.
Iniciar la suscripción recurrente de un plan
requires authentication
Inicia una suscripción recurrente (cobro automático periódico) de un
plan de pago en Tilopay. Genera la URL de registro de la suscripción con
el correo del contribuyente pre-cargado y devuelve la redirect_url a la
que su backend debe redirigir al cliente.
El plan se activa cuando llega el webhook de suscripción/cobro de Tilopay, no en esta respuesta. Si el contribuyente ya estaba registrado, se devuelve la URL de renovación.
Requiere que exista un plan de pasarela configurado para la combinación plan + ciclo, y que la cuenta tenga al menos un correo registrado. El plan gratis no admite suscripción.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/subscribe" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"plan_code\": \"pyme\",
\"billing_cycle\": \"monthly\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/subscribe"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"plan_code": "pyme",
"billing_cycle": "monthly"
};
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/subscription/subscribe';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'plan_code' => 'pyme',
'billing_cycle' => 'monthly',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Registro de suscripción creado):
{
"success": true,
"data": {
"redirect_url": "https://app.tilopay.com/subscribe/register/xyz789"
},
"message": "Redirigiendo al registro de la suscripción.",
"errors": null
}
Example response (409, Suscripción recurrente no disponible para el plan):
{
"success": false,
"data": null,
"message": "La suscripción recurrente para este plan aún no está disponible. Contacte a soporte.",
"errors": null
}
Example response (422, Plan no disponible o gratuito):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"plan_code": [
"El plan solicitado no existe, no está disponible o es gratuito."
]
}
}
Example response (422, Sin correo registrado):
{
"success": false,
"data": null,
"message": "Su cuenta no tiene un correo registrado para la suscripción.",
"errors": null
}
Example response (502, Fallo de la pasarela):
{
"success": false,
"data": null,
"message": "No se pudo iniciar la suscripción con Tilopay. Intente más tarde.",
"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 la suscripción recurrente
requires authentication
Cancela el cobro automático de la suscripción recurrente en Tilopay y programa la baja al plan gratis al fin del período pagado. El contribuyente conserva su plan actual hasta entonces y no hay reembolso.
Por tratarse de un cambio de plan (no un pago), exige la contraseña del usuario autenticado y un motivo de al menos 100 caracteres (retención y analítica). Requiere una suscripción recurrente activa en Tilopay.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/cancel-recurring" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"password\": \"mi-contrasena-secreta\",
\"reason\": \"Cancelo la suscripcion porque por ahora estoy facturando muy poco y prefiero volver al plan gratuito mientras crece mi negocio.\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/cancel-recurring"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"password": "mi-contrasena-secreta",
"reason": "Cancelo la suscripcion porque por ahora estoy facturando muy poco y prefiero volver al plan gratuito mientras crece mi negocio."
};
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/subscription/cancel-recurring';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'password' => 'mi-contrasena-secreta',
'reason' => 'Cancelo la suscripcion porque por ahora estoy facturando muy poco y prefiero volver al plan gratuito mientras crece mi negocio.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Suscripción cancelada):
{
"success": true,
"data": null,
"message": "Suscripción cancelada. Mantenés el plan hasta el fin del período pagado.",
"errors": null
}
Example response (404, Sin suscripción recurrente activa):
{
"success": false,
"data": null,
"message": "No hay una suscripción recurrente activa para cancelar.",
"errors": null
}
Example response (422, Contraseña incorrecta):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"password": [
"La contraseña es incorrecta."
]
}
}
Example response (422, Motivo demasiado corto):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"reason": [
"El motivo debe tener al menos 100 caracteres."
]
}
}
Example response (502, Fallo de la pasarela):
{
"success": false,
"data": null,
"message": "No se pudo cancelar la suscripción con Tilopay. Intente más tarde.",
"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.
Programar una baja de plan (downgrade)
requires authentication
Programa una baja a un plan inferior que se aplicará al fin del período pagado. Si existe una suscripción recurrente en Tilopay, se cancela el cobro automático para que no haya cargos futuros del plan superior. El contribuyente conserva su plan actual hasta que venza el período pagado y entonces pasa al plan destino. No hay reembolso.
Por tratarse de un cambio de plan (no un pago), exige la contraseña del usuario y un motivo de al menos 100 caracteres.
El plan destino debe ser de menor rango (tier/display_order) que
el actual y no puede ser el plan que ya tiene activo. Para subir de
plan use la suscripción (POST /subscription/subscribe).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/downgrade" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"plan_code\": \"emprendedor\",
\"password\": \"mi-contrasena-secreta\",
\"reason\": \"Bajo de plan porque mi volumen de facturacion disminuyo bastante este trimestre y el plan inferior cubre de sobra mis necesidades actuales.\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/downgrade"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"plan_code": "emprendedor",
"password": "mi-contrasena-secreta",
"reason": "Bajo de plan porque mi volumen de facturacion disminuyo bastante este trimestre y el plan inferior cubre de sobra mis necesidades actuales."
};
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/subscription/downgrade';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'plan_code' => 'emprendedor',
'password' => 'mi-contrasena-secreta',
'reason' => 'Bajo de plan porque mi volumen de facturacion disminuyo bastante este trimestre y el plan inferior cubre de sobra mis necesidades actuales.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Baja programada):
{
"success": true,
"data": null,
"message": "Baja de plan programada. Mantenés tu plan actual hasta el fin del período pagado.",
"errors": null
}
Example response (422, Contraseña incorrecta):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"password": [
"La contraseña es incorrecta."
]
}
}
Example response (422, Motivo demasiado corto):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"reason": [
"El motivo debe tener al menos 100 caracteres."
]
}
}
Example response (422, Plan no disponible):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"plan_code": [
"El plan solicitado no existe o no está disponible."
]
}
}
Example response (422, Ya tiene este plan):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"plan_code": [
"Ya tenés este plan activo."
]
}
}
Example response (422, No es una baja real):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"plan_code": [
"Para subir de plan, usá la suscripción (cobro automático)."
]
}
}
Example response (502, Fallo de la pasarela):
{
"success": false,
"data": null,
"message": "No se pudo procesar la baja con Tilopay. Intente más tarde.",
"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.
Soporte
Listar conversaciones de soporte
requires authentication
Devuelve todas las conversaciones de soporte abiertas por el
contribuyente autenticado, ordenadas con las más recientes primero
(por última actividad). Cada conversación incluye el conteo de
mensajes, pero no el hilo completo | para ver los mensajes use
GET /support/conversations/{id}.
Las conversaciones son tipo chat in-app entre el cliente y el equipo de soporte, dentro de la plataforma (sin correos). El alcance está limitado al contribuyente del token (multi-tenant).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/support/conversations" \
--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/support/conversations"
);
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/support/conversations';
$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 conversaciones):
{
"success": true,
"data": [
{
"id": "019d867d-0001-7288-8ece-fd64da756001",
"subject": "No me llega el correo al cliente",
"status": "open",
"priority": "normal",
"unread_for_client": false,
"messages_count": 3,
"last_message_at": "2026-06-23T10:00:00-06:00",
"created_at": "2026-06-22T09:00:00-06:00"
}
],
"message": "Conversaciones de soporte.",
"errors": null
}
Example response (200, Sin conversaciones):
{
"success": true,
"data": [],
"message": "Conversaciones de soporte.",
"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.
Abrir una conversación de soporte
requires authentication
Crea una nueva conversación de soporte con su primer mensaje. La
conversación nace en estado open y con la prioridad correspondiente
al plan del contribuyente (priority si tiene soporte prioritario,
normal en caso contrario).
Al crearla se envía un correo de acuse al cliente, pero el hilo sigue siendo in-app.
Límite: el cliente solo puede enviar un mensaje de soporte por hora. Si el último mensaje del cliente (en cualquiera de sus conversaciones) tiene menos de una hora y el equipo aún no lo respondió, la API responde HTTP 429 con el tiempo restante.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/support/conversations" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"subject\": \"No me llega el correo al cliente\",
\"message\": \"Hola, emití una factura pero el cliente dice que no le llegó el correo.\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/support/conversations"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"subject": "No me llega el correo al cliente",
"message": "Hola, emití una factura pero el cliente dice que no le llegó el correo."
};
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/support/conversations';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'subject' => 'No me llega el correo al cliente',
'message' => 'Hola, emití una factura pero el cliente dice que no le llegó el correo.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Conversación creada):
{
"success": true,
"data": {
"id": "019d867d-0001-7288-8ece-fd64da756001",
"subject": "No me llega el correo al cliente",
"status": "open",
"priority": "normal",
"unread_for_client": false,
"messages_count": 1,
"last_message_at": "2026-06-23T10:00:00-06:00",
"created_at": "2026-06-23T10:00:00-06:00",
"client_can_write": false,
"cooldown_seconds": 3600,
"messages": [
{
"id": "019d867d-0002-7288-8ece-fd64da756002",
"sender_type": "client",
"body": "Hola, emití una factura pero el cliente dice que no le llegó el correo.",
"created_at": "2026-06-23T10:00:00-06:00"
}
]
},
"message": "Conversación creada.",
"errors": null
}
Example response (422, Mensaje requerido):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"message": [
"El campo message es obligatorio."
]
}
}
Example response (429, Límite de un mensaje por hora):
{
"success": false,
"data": {
"cooldown_seconds": 2400
},
"message": "Solo puedes enviar un mensaje de soporte cada hora. Podrás escribir de nuevo en 40 min, o cuando el equipo responda.",
"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 una conversación de soporte
requires authentication
Devuelve el detalle de una conversación con su hilo completo de mensajes ordenados cronológicamente. Solo se retornan conversaciones que pertenecen al contribuyente autenticado.
Al consultar el detalle, la conversación se marca como leída por el
cliente (unread_for_client pasa a false).
La respuesta incluye además client_can_write y cooldown_seconds,
que reflejan el límite de un mensaje por hora para que el frontend
habilite o deshabilite el cuadro de respuesta.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/support/conversations/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/support/conversations/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/support/conversations/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, Detalle con hilo de mensajes):
{
"success": true,
"data": {
"id": "019d867d-0001-7288-8ece-fd64da756001",
"subject": "No me llega el correo al cliente",
"status": "open",
"priority": "normal",
"unread_for_client": false,
"messages_count": 2,
"last_message_at": "2026-06-23T11:00:00-06:00",
"created_at": "2026-06-23T10:00:00-06:00",
"client_can_write": true,
"cooldown_seconds": 0,
"messages": [
{
"id": "019d867d-0002-7288-8ece-fd64da756002",
"sender_type": "client",
"body": "Hola, emití una factura pero el cliente dice que no le llegó el correo.",
"created_at": "2026-06-23T10:00:00-06:00"
},
{
"id": "019d867d-0003-7288-8ece-fd64da756003",
"sender_type": "admin",
"body": "Hola, ya revisamos y el correo salió correctamente. Pídale al cliente revisar spam.",
"created_at": "2026-06-23T11:00:00-06:00"
}
]
},
"message": "Detalle de la conversación.",
"errors": null
}
Example response (404, No encontrada):
{
"success": false,
"data": null,
"message": "Conversació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.
Responder en una conversación de soporte
requires authentication
Añade un nuevo mensaje del cliente al hilo de una conversación
existente. Si la conversación estaba resuelta o cerrada, se reabre
automáticamente (status vuelve a open) y se notifica al equipo.
Devuelve la conversación con el hilo completo de mensajes actualizado.
Límite: aplica el mismo límite de un mensaje por hora que al abrir una conversación. Si el último mensaje del cliente tiene menos de una hora y el equipo aún no respondió, la API responde HTTP 429 con el tiempo restante.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/support/conversations/019d867d-0001-7288-8ece-fd64da756001/reply" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"message\": \"Listo, el cliente lo encontró en spam. Muchas gracias.\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/support/conversations/019d867d-0001-7288-8ece-fd64da756001/reply"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"message": "Listo, el cliente lo encontró en spam. Muchas gracias."
};
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/support/conversations/019d867d-0001-7288-8ece-fd64da756001/reply';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'message' => 'Listo, el cliente lo encontró en spam. Muchas gracias.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Mensaje enviado):
{
"success": true,
"data": {
"id": "019d867d-0001-7288-8ece-fd64da756001",
"subject": "No me llega el correo al cliente",
"status": "open",
"priority": "normal",
"unread_for_client": false,
"messages_count": 3,
"last_message_at": "2026-06-23T11:30:00-06:00",
"created_at": "2026-06-23T10:00:00-06:00",
"client_can_write": false,
"cooldown_seconds": 3600,
"messages": [
{
"id": "019d867d-0004-7288-8ece-fd64da756004",
"sender_type": "client",
"body": "Listo, el cliente lo encontró en spam. Muchas gracias.",
"created_at": "2026-06-23T11:30:00-06:00"
}
]
},
"message": "Mensaje enviado.",
"errors": null
}
Example response (404, No encontrada):
{
"success": false,
"data": null,
"message": "Conversación no encontrada.",
"errors": null
}
Example response (422, Mensaje requerido):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"message": [
"El campo message es obligatorio."
]
}
}
Example response (429, Límite de un mensaje por hora):
{
"success": false,
"data": {
"cooldown_seconds": 1800
},
"message": "Solo puedes enviar un mensaje de soporte cada hora. Podrás escribir de nuevo en 30 min, o cuando el equipo responda.",
"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.
Soporte Prioritario
Consultar el estado del soporte prioritario
requires authentication
Devuelve el tier de soporte actual del contribuyente, si tiene una suscripción recurrente activa, los precios vigentes de cada tier y la disponibilidad de cada uno para contratación en línea.
Los precios provienen de la configuración del sistema y están expresados en dólares estadounidenses (USD). La disponibilidad depende de que exista un plan de pasarela (Tilopay) configurado para el addon correspondiente con ciclo mensual.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/support-tier" \
--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/support-tier"
);
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/support-tier';
$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, Estado del soporte):
{
"success": true,
"data": {
"current_tier": "standard",
"has_subscription": false,
"premium_usd": 500,
"premium_247_usd": 2400,
"premium_available": true,
"premium_247_available": true
},
"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
current_tier
string
Tier de soporte actual del contribuyente: standard, premium o premium_247.
has_subscription
boolean
true si el contribuyente tiene una suscripción de soporte recurrente activa.
premium_usd
number
Precio mensual del tier premium en dólares (USD).
premium_247_usd
number
Precio mensual del tier premium 24/7 en dólares (USD).
premium_available
boolean
true si el tier premium se puede contratar en línea actualmente.
premium_247_available
boolean
true si el tier premium 24/7 se puede contratar en línea actualmente.
Contratar soporte prioritario
requires authentication
Inicia la suscripción recurrente del tier elegido y devuelve una URL de redirección a la pasarela Tilopay, donde el contribuyente registra su medio de pago para el cobro mensual.
El tier solo se activa de forma efectiva cuando la pasarela confirma el registro mediante sus webhooks recurrentes | esta llamada solo inicia el proceso.
Requisitos:
- El tier solicitado (
premiumopremium_247) debe estar disponible para contratación en línea (veapremium_availableypremium_247_availableenGET /support-tier). Si no hay plan de pasarela configurado, responde HTTP 409. - El contribuyente debe tener al menos un correo en su perfil. Si no lo tiene, responde HTTP 422.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/support-tier/subscribe" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"tier\": \"premium\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/support-tier/subscribe"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"tier": "premium"
};
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/support-tier/subscribe';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'tier' => 'premium',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Redirección a la pasarela):
{
"success": true,
"data": {
"redirect_url": "https://app.tilopay.com/recurrent/register/abc123"
},
"message": "Redirigiendo a la pasarela de pago.",
"errors": null
}
Example response (409, Tier no disponible para contratar en línea):
{
"success": false,
"data": null,
"message": "El soporte prioritario aún no está disponible para contratar en línea. Escríbenos y lo activamos.",
"errors": null
}
Example response (422, Sin correo en el perfil):
{
"success": false,
"data": null,
"message": "Necesitas un correo en tu perfil para contratar el soporte.",
"errors": null
}
Example response (422, Tier inválido):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"tier": [
"El tier seleccionado no es válido."
]
}
}
Example response (502, Error al iniciar la suscripción en la pasarela):
{
"success": false,
"data": null,
"message": "No se pudo iniciar la suscripción. Intenta de nuevo en unos minutos.",
"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 soporte prioritario
requires authentication
Cancela la suscripción recurrente de soporte: elimina el suscriptor en
la pasarela Tilopay y devuelve al contribuyente al tier standard,
limpiando su identificador de suscripción.
Si el contribuyente no tiene una suscripción de soporte activa, responde HTTP 404.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/support-tier/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/support-tier/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/support-tier/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, Soporte cancelado):
{
"success": true,
"data": null,
"message": "Soporte prioritario cancelado.",
"errors": null
}
Example response (404, Sin suscripción activa):
{
"success": false,
"data": null,
"message": "No tienes una suscripción de soporte activa para cancelar.",
"errors": null
}
Example response (502, Error al cancelar en la pasarela):
{
"success": false,
"data": null,
"message": "No se pudo cancelar la suscripción. Intenta de nuevo.",
"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.
Onboarding
Estado del onboarding del contribuyente
requires authentication
Devuelve un resumen del onboarding del contribuyente autenticado: sus créditos disponibles (gratuito anual + comprados), si puede autoagendar en este momento, el precio vigente de la hora y la próxima cita futura agendada (si existe).
El campo can_schedule es true solo cuando hay al menos un crédito
disponible y hay agentes activos en la agenda. Úselo para habilitar
o deshabilitar el botón de agendar en el portal.
El bloque appointment es null cuando no hay una cita de onboarding o
pagada futura con estado agendado; de lo contrario trae los datos de la
próxima sesión.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/onboarding" \
--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/onboarding"
);
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/onboarding';
$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, Con crédito y cita agendada):
{
"success": true,
"data": {
"credits": {
"free": 1,
"purchased": 0,
"total": 1
},
"can_schedule": true,
"price_usd": 100,
"appointment": {
"id": "019d867d-0001-7288-8ece-fd64da756001",
"starts_at": "2026-07-01T09:00:00-06:00",
"ends_at": "2026-07-01T09:50:00-06:00",
"agent_name": "Ana Soporte",
"meeting_url": "https://meet.google.com/abc-defg-hij",
"status": "scheduled"
}
},
"message": "",
"errors": null
}
Example response (200, Sin créditos ni cita):
{
"success": true,
"data": {
"credits": {
"free": 0,
"purchased": 0,
"total": 0
},
"can_schedule": false,
"price_usd": 100,
"appointment": null
},
"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
credits
object
Resumen de créditos de onboarding disponibles.
free
integer
Créditos gratuitos anuales disponibles.
purchased
integer
Créditos comprados disponibles.
total
integer
Suma de créditos disponibles (free + purchased).
can_schedule
boolean
true si tiene al menos un crédito y hay agentes activos en la agenda.
price_usd
number
Precio vigente de la hora de onboarding en USD.
appointment
object
Próxima cita futura agendada, o null si no hay ninguna.
null si no hay ninguna.id
string
UUID de la cita.
starts_at
string
Inicio de la cita (ISO 8601).
ends_at
string
Fin de la cita (ISO 8601).
agent_name
string
Nombre del agente asignado. Puede ser null.
meeting_url
string
Enlace de la reunión (Google Meet). Puede ser null.
status
string
Estado de la cita (por ejemplo scheduled).
Huecos disponibles para autoagendar
requires authentication
Devuelve los horarios libres en los que el contribuyente puede agendar su sesión de onboarding, calculados a partir de las agendas de los agentes activos. La ventana de búsqueda va desde ahora hasta 120 días hacia adelante y respeta la antelación mínima configurada.
Por privacidad, la respuesta no incluye el agente de cada hueco;
solo el inicio y fin del horario. El agente se asigna internamente al
confirmar la cita en POST /onboarding/appointments.
Use el valor starts_at de un hueco tal cual al agendar.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/onboarding/slots" \
--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/onboarding/slots"
);
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/onboarding/slots';
$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, Huecos disponibles):
{
"success": true,
"data": {
"slots": [
{
"starts_at": "2026-07-01T09:00:00-06:00",
"ends_at": "2026-07-01T09:50:00-06:00"
},
{
"starts_at": "2026-07-01T10:00:00-06:00",
"ends_at": "2026-07-01T10:50:00-06:00"
}
]
},
"message": "",
"errors": null
}
Example response (200, Sin huecos disponibles):
{
"success": true,
"data": {
"slots": []
},
"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
slots
object[]
Lista de horarios libres disponibles para agendar.
starts_at
string
Inicio del hueco (ISO 8601). Envíelo como starts_at al agendar.
ends_at
string
Fin del hueco (ISO 8601).
Agendar la sesión de onboarding
requires authentication
Reserva un hueco para la sesión de onboarding y consume 1 crédito
disponible (se usa primero el crédito gratuito anual; si no hay, se usa
uno comprado). El starts_at enviado debe coincidir exactamente con un
hueco devuelto por GET /onboarding/slots.
Antes de crear la cita, el sistema reconfirma la disponibilidad del horario contra la agenda actual. Si el hueco ya fue tomado o el agente tiene un conflicto, responde HTTP 422 y debe elegir otro horario. Si no tiene créditos disponibles, también responde 422.
Al agendar correctamente se crea el evento (y enlace de Google Meet, si la integración está activa) y se envía un correo de confirmación al contribuyente cuando tiene un email registrado.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/onboarding/appointments" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"starts_at\": \"2026-07-01T09:00:00-06:00\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/onboarding/appointments"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"starts_at": "2026-07-01T09:00:00-06:00"
};
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/onboarding/appointments';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'starts_at' => '2026-07-01T09:00:00-06:00',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Cita agendada):
{
"success": true,
"data": {
"id": "019d867d-0001-7288-8ece-fd64da756001",
"starts_at": "2026-07-01T09:00:00-06:00",
"ends_at": "2026-07-01T09:50:00-06:00",
"agent_name": "Ana Soporte",
"meeting_url": "https://meet.google.com/abc-defg-hij",
"status": "scheduled"
},
"message": "Cita de onboarding agendada.",
"errors": null
}
Example response (422, Sin créditos disponibles):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"credit": [
"No tienes créditos de onboarding disponibles."
]
}
}
Example response (422, Horario ya no disponible):
{
"success": false,
"data": null,
"message": "Los datos proporcionados no son válidos.",
"errors": {
"starts_at": [
"Ese horario ya no está disponible. Elige otro."
]
}
}
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 de la cita creada.
starts_at
string
Inicio de la cita (ISO 8601).
ends_at
string
Fin de la cita (ISO 8601).
agent_name
string
Nombre del agente asignado. Puede ser null.
meeting_url
string
Enlace de la reunión (Google Meet). Puede ser null si la integración no está activa.
status
string
Estado de la cita (por ejemplo scheduled).
Comprar una hora de onboarding
requires authentication
Inicia el checkout de Tilopay para comprar 1 hora de onboarding a la tarifa vigente (USD). Disponible para todos los planes. Devuelve la URL de redirección a la pasarela de pago y el número de orden.
Importante: el crédito no se otorga en este endpoint. Solo se inicia el cobro y se registra un pago en estado pendiente. El crédito
purchasedse crea cuando el pago se confirma mediante el retorno firmado de Tilopay (procesado por el webhook de retorno).
Si la pasarela falla al iniciar el checkout, responde HTTP 502.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/onboarding/purchase" \
--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/onboarding/purchase"
);
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/onboarding/purchase';
$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, Checkout iniciado):
{
"success": true,
"data": {
"redirect_url": "https://app.tilopay.com/checkout/abc123",
"order": "TPAY-0001"
},
"message": "Redirigiendo a la pasarela de pago.",
"errors": null
}
Example response (502, Pasarela no disponible):
{
"success": false,
"data": null,
"message": "No se pudo iniciar el pago con Tilopay. Intente de nuevo en unos minutos.",
"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
redirect_url
string
URL de la pasarela de Tilopay a la que debe redirigir al usuario.
order
string
Identificador de la orden/pago en la pasarela.
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 Almendro 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.
Otros
POST api/v1/public/item-shares
requires authentication
GET api/v1/public/item-shares/sent
requires authentication
GET api/v1/public/item-shares/received
requires authentication
GET api/v1/public/item-shares/{id}
requires authentication
GET api/v1/public/item-shares/{id}/accept-preview
requires authentication
POST api/v1/public/item-shares/{id}/accept
requires authentication
POST api/v1/public/item-shares/{id}/reject
requires authentication
POST api/v1/public/item-shares/{id}/cancel
requires authentication
Lista los contribuyentes cuyo buzón puede ver/operar el autenticado: él mismo más los clientes sobre los que tiene un grant de certificado activo. Alimenta el selector de contribuyente del buzón y de la carga.
requires authentication
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/inbox/accessible-contributors" \
--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/inbox/accessible-contributors"
);
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/inbox/accessible-contributors';
$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));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.
Estado de la recepción automática de facturas por correo (F2).
requires authentication
enabled: si los XML dirigidos a su cédula que llegan a
facturas@almendro.cr se cargan solos a su buzón. quarantined: cuántos
comprobantes esperan en cuarentena (72 horas) por tener la recepción
apagada; se cargan si la reactiva a tiempo.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/inbox/email-reception" \
--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/inbox/email-reception"
);
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/inbox/email-reception';
$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));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
enabled
boolean
Recepción automática encendida.
quarantined
integer
Comprobantes vigentes en cuarentena.
Enciende o apaga la recepción automática de facturas por correo (F2).
requires authentication
Al APAGARLA, los comprobantes que lleguen dirigidos a su cédula quedan en cuarentena 72 horas en lugar de cargarse (no consumen su pozo). Al ENCENDERLA, lo que esté en cuarentena vigente se carga automáticamente.
Example request:
curl --request PATCH \
"https://fe.almendro.cr/api/v1/public/inbox/email-reception" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"enabled\": true
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/inbox/email-reception"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"enabled": true
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/inbox/email-reception';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'enabled' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
PATCH api/v1/public/inbox/{key}/annotation
requires authentication
Example request:
curl --request PATCH \
"https://fe.almendro.cr/api/v1/public/inbox/56425593142326822820492192294472586671521743076933/annotation" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"alias\": \"b\",
\"notes\": \"n\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/inbox/56425593142326822820492192294472586671521743076933/annotation"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"alias": "b",
"notes": "n"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/inbox/56425593142326822820492192294472586671521743076933/annotation';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'alias' => 'b',
'notes' => 'n',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
"No mostrar más" | posponer un recordatorio de puesta a punto por 6 meses.
requires authentication
Solo disponible para integradores que gestionan al menos un contribuyente con certificado de producción vigente (facturan por sus clientes y no necesitan su propio .p12/perfil). Para el resto responde 422.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/reminders/architecto/snooze" \
--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/reminders/architecto/snooze"
);
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/reminders/architecto/snooze';
$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));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.
Descarga el PDF de la factura (FE) asociada a un pago.
requires authentication
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/subscription/payments/e507fb2b94-c9e62d073743f8140a658fd5f/pdf" \
--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/subscription/payments/e507fb2b94-c9e62d073743f8140a658fd5f/pdf"
);
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/subscription/payments/e507fb2b94-c9e62d073743f8140a658fd5f/pdf';
$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));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.
Descarga el XML firmado de la factura (FE) asociada a un pago.
requires authentication
La FE la emite Almendro HACIA el contribuyente, así que el voucher es del emisor; se autoriza por el PAGO (que sí es del contribuyente).
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/subscription/payments/e507fb2b94-c9e62d073743f8140a658fd5f/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/subscription/payments/e507fb2b94-c9e62d073743f8140a658fd5f/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/subscription/payments/e507fb2b94-c9e62d073743f8140a658fd5f/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));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.
POST api/v1/public/discount-codes/validate
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/discount-codes/validate" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"code\": \"b\",
\"plan_code\": \"architecto\",
\"billing_cycle\": \"architecto\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/discount-codes/validate"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"code": "b",
"plan_code": "architecto",
"billing_cycle": "architecto"
};
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/discount-codes/validate';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'code' => 'b',
'plan_code' => 'architecto',
'billing_cycle' => 'architecto',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Combinaciones plan/ciclo que tienen una promoción activa para este contribuyente. El checkout muestra el campo de descuento solo ahí.
requires authentication
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/discount-codes/availability" \
--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/discount-codes/availability"
);
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/discount-codes/availability';
$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));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.
Suscribirse / subir de plan COBRANDO la tarjeta tokenizada (server-to-server).
requires authentication
Requiere tarjeta guardada (si no, el front manda primero a "agregar tarjeta").
Si el contribuyente ya está en un plan de pago vigente del mismo ciclo, cobra
solo la diferencia prorrateada por lo que resta del período y MANTIENE la
fecha de aniversario; si viene de Gratis/sin plan, cobra el precio completo y
arranca el período desde hoy. Las renovaciones siguen con subscription:charge-due.
Solo SUBIDAS: para bajar de plan se usa la baja programada (downgrade).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/subscribe-tokenized" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"plan_code\": \"architecto\",
\"billing_cycle\": \"architecto\",
\"discount_code\": \"n\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/subscribe-tokenized"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"plan_code": "architecto",
"billing_cycle": "architecto",
"discount_code": "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/subscription/subscribe-tokenized';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'plan_code' => 'architecto',
'billing_cycle' => 'architecto',
'discount_code' => 'n',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Sesión para el SDK V2 (alta de tarjeta embebida).
requires authentication
Devuelve el token del SDK (loginSdk) y los parámetros para inicializar
Tilopay.InitTokenize() en el frontend. El token de la tarjeta vuelve del
lado cliente y se guarda con saveCard(). El email es el del contribuyente:
el cobro posterior se hace con ese mismo par email + token.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/card-session" \
--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/subscription/card-session"
);
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/subscription/card-session';
$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));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.
Guarda el token de la tarjeta tokenizada por el SDK contra el contribuyente.
requires authentication
El email almacenado es el mismo que usó cardSession() (Tilopay rutea el cobro por email + token).
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/save-card" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"token\": \"b\",
\"last4\": \"ngzm\",
\"brand\": \"iyvdljnikhwaykcm\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/save-card"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"token": "b",
"last4": "ngzm",
"brand": "iyvdljnikhwaykcm"
};
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/subscription/save-card';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'token' => 'b',
'last4' => 'ngzm',
'brand' => 'iyvdljnikhwaykcm',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Sesión de PAGO con el SDK (cobro único que guarda la tarjeta, `Init({subscription:1})`).
requires authentication
Se usa cuando el contribuyente NO tiene tarjeta guardada: el primer pago cobra y guarda la tarjeta en un solo cargo (modelo Uber). Crea un Payment pendiente con la intención (suscribirse o retención) y devuelve los params del SDK.
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/sdk-pay-session" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"purpose\": \"architecto\",
\"plan_code\": \"architecto\",
\"billing_cycle\": \"architecto\",
\"discount_code\": \"n\",
\"beneficiary_contributor_id\": \"6b72fe4a-5b40-307c-bc24-f79acf9a1bb9\",
\"tier_code\": \"miyvdl\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/sdk-pay-session"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"purpose": "architecto",
"plan_code": "architecto",
"billing_cycle": "architecto",
"discount_code": "n",
"beneficiary_contributor_id": "6b72fe4a-5b40-307c-bc24-f79acf9a1bb9",
"tier_code": "miyvdl"
};
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/subscription/sdk-pay-session';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'purpose' => 'architecto',
'plan_code' => 'architecto',
'billing_cycle' => 'architecto',
'discount_code' => 'n',
'beneficiary_contributor_id' => '6b72fe4a-5b40-307c-bc24-f79acf9a1bb9',
'tier_code' => 'miyvdl',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Confirma un pago hecho con el SDK: verifica con `consult`, guarda la tarjeta y ejecuta la compra (activar plan o crear la extensión de retención). No cobra de nuevo (el SDK ya cobró).
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/subscription/sdk-pay-confirm" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"order\": \"b\",
\"token\": \"n\",
\"last4\": \"gzmi\",
\"brand\": \"yvdljnikhwaykcmy\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/subscription/sdk-pay-confirm"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"order": "b",
"token": "n",
"last4": "gzmi",
"brand": "yvdljnikhwaykcmy"
};
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/subscription/sdk-pay-confirm';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'order' => 'b',
'token' => 'n',
'last4' => 'gzmi',
'brand' => 'yvdljnikhwaykcmy',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Elimina la tarjeta guardada. Intenta el `card-remove` en Tilopay (si el comercio tuviera el permiso) y, pase o no, suelta el token de nuestro lado: sin token no se cobra más.
requires authentication
Example request:
curl --request DELETE \
"https://fe.almendro.cr/api/v1/public/subscription/card" \
--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/subscription/card"
);
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/subscription/card';
$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));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.
Compra la hora de onboarding COBRANDO la tarjeta guardada (sin salir del portal). Mismo patrón que la retención tokenizada: chargeToken + crédito inmediato + FE. Sin tarjeta guardada responde 409 (el front abre el SDK).
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/onboarding/purchase-tokenized" \
--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/onboarding/purchase-tokenized"
);
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/onboarding/purchase-tokenized';
$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));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.
Inicia el checkout de la retención para el contribuyente autenticado.
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/retention-extension/purchase" \
--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/retention-extension/purchase"
);
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/retention-extension/purchase';
$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));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 contribuyente cobra su propia retención con la tarjeta guardada.
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/retention-extension/charge-tokenized" \
--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/retention-extension/charge-tokenized"
);
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/retention-extension/charge-tokenized';
$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));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.
Desactiva la auto-renovación: la retención sigue ACTIVA hasta el fin de su período y luego no se renueva (charge-due la omite).
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/retention-extension/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/retention-extension/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/retention-extension/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));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.
Reactiva la auto-renovación de una retención cancelada pero aún vigente.
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/retention-extension/resume" \
--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/retention-extension/resume"
);
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/retention-extension/resume';
$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));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.
POST api/v1/public/api-access/purchase
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/api-access/purchase" \
--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/api-access/purchase"
);
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/api-access/purchase';
$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));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.
POST api/v1/public/api-access/charge-tokenized
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/api-access/charge-tokenized" \
--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/api-access/charge-tokenized"
);
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/api-access/charge-tokenized';
$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));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.
Desactiva la auto-renovación: el acceso sigue ACTIVO hasta el fin del período pagado y luego no se renueva (charge-due lo omite).
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/api-access/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/api-access/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/api-access/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));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.
Reactiva la auto-renovación de un add-on cancelado pero aún vigente.
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/api-access/resume" \
--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/api-access/resume"
);
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/api-access/resume';
$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));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.
GET /distributor/info
requires authentication
Estado del programa para la sección de Mi Plan: inscripción, niveles a la venta con el precio aplicable (reposición incluida si califica), y el resumen de pozos propios si ya es distribuidor.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/distributor/info" \
--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/distributor/info"
);
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/distributor/info';
$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));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.
POST api/v1/public/distributor/purchase
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/distributor/purchase" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"tier_code\": \"bngzmi\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/distributor/purchase"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"tier_code": "bngzmi"
};
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/distributor/purchase';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'tier_code' => 'bngzmi',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
POST api/v1/public/distributor/charge-tokenized
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/distributor/charge-tokenized" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"tier_code\": \"bngzmi\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/distributor/charge-tokenized"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"tier_code": "bngzmi"
};
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/distributor/charge-tokenized';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'tier_code' => 'bngzmi',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
GET api/v1/public/managed-sales
requires authentication
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/managed-sales" \
--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/managed-sales"
);
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/managed-sales';
$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));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.
Registro PRIVADO de la venta del gestor (lo que él cobra a su cliente): monto, moneda y modo de pago. El admin nunca lo ve.
requires authentication
Example request:
curl --request PATCH \
"https://fe.almendro.cr/api/v1/public/managed-sales/564" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"sale_amount\": 1,
\"sale_currency\": \"USD\",
\"sale_payment_method\": \"architecto\"
}"
const url = new URL(
"https://fe.almendro.cr/api/v1/public/managed-sales/564"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"sale_amount": 1,
"sale_currency": "USD",
"sale_payment_method": "architecto"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://fe.almendro.cr/api/v1/public/managed-sales/564';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'sale_amount' => 1,
'sale_currency' => 'USD',
'sale_payment_method' => 'architecto',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));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.
Inicia el checkout de la retención que un gestor paga para un cliente gestionado. El gestor autenticado paga; el cliente es el beneficiario.
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/my-contributors/e507fb2b94-c9e62d073743f8140a658fd5f/retention-extension" \
--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/e507fb2b94-c9e62d073743f8140a658fd5f/retention-extension"
);
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/e507fb2b94-c9e62d073743f8140a658fd5f/retention-extension';
$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));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.
Un gestor cobra la retención de un cliente gestionado con SU tarjeta guardada.
requires authentication
Example request:
curl --request POST \
"https://fe.almendro.cr/api/v1/public/my-contributors/e507fb2b94-c9e62d073743f8140a658fd5f/retention-extension/charge-tokenized" \
--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/e507fb2b94-c9e62d073743f8140a658fd5f/retention-extension/charge-tokenized"
);
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/e507fb2b94-c9e62d073743f8140a658fd5f/retention-extension/charge-tokenized';
$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));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 del contribuyente
Entitlement del plan
requires authentication
Devuelve el plan vigente del contribuyente autenticado con TODOS sus
límites resueltos (override por contribuyente > valor del plan;
null = sin tope). Pensado para que integraciones (POS, canal
e-commerce) lo cacheen y apliquen los límites localmente.
Example request:
curl --request GET \
--get "https://fe.almendro.cr/api/v1/public/entitlement" \
--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/entitlement"
);
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/entitlement';
$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, Entitlement resuelto):
{
"success": true,
"data": {
"plan": {
"code": "professional",
"name": "Profesional",
"billing_mode": "annual_or_monthly"
},
"features": [
"api_access",
"receiver_module",
"webhooks"
],
"limits": {
"monthly_voucher_limit": 500,
"max_users": 5,
"max_clients": 1500,
"max_items": 3000,
"max_branches": 5,
"max_terminals_per_branch": 5,
"max_pos_items": 15000,
"max_pos_cashiers_per_terminal": 10,
"max_channel_pages": 15
}
},
"message": "Entitlement del plan (límites resueltos con overrides).",
"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
limits
object
Cada límite YA resuelto: override del contribuyente sobre el valor del plan; null = sin tope.