openapi: 3.0.3
info:
title: 'Almendro Factura Electrónica | API REST v1.0.0'
description: "## Bienvenido a la API de Almendro Factura Electrónica\n\nAlmendro Factura Electrónica le permite integrar facturación electrónica costarricense en\ncualquier sistema en minutos. Emita comprobantes, consulte estados, descargue XML y PDF,\ngestione clientes y productos, reciba notificaciones en tiempo real y administre múltiples\ncontribuyentes desde una sola API.\n\nLa API cumple al 100% con la normativa version 4.4, vigente del Ministerio de Hacienda:\n\n- Reglamento de Comprobantes Electrónicos (Decreto Ejecutivo N.° 41820-H)\n- Resolución General sobre disposiciones técnicas (MH-DGT-RES-0019-2022)\n- Anexos y Estructuras v4.4 de la Dirección General de Tributación\n- Esquemas XSD v4.4 oficiales publicados por la DGT\n\n---\n\n### Primeros pasos\n\nIntegrar facturación electrónica con Almendro requiere solo cuatro pasos. No necesita\nconocer el esquema XML ni la especificación de firma digital: Almendro se encarga de todo\nlo técnico.\n\n**Paso 1 — Cree su cuenta.** Regístrese en [fe.almendro.cr](https://fe.almendro.cr).\nSu cuenta queda activa inmediatamente. Recibirá un correo de verificación para confirmar\nsu dirección de email.\n\n**Paso 2 — Obtenga su token API.** Desde el portal, vaya a **Configuración** y genere\nsu token API. Este token es su credencial para autenticarse en todos los endpoints.\nGuárdelo en un lugar seguro: por razones de seguridad, el token solo se muestra una vez\nal momento de generarlo. Si lo pierde, puede generar uno nuevo (el anterior se revoca\nautomáticamente).\n\n**Paso 3 — Suba su certificado digital.** Envíe su archivo `.p12` de firma digital\n(emitido por una entidad certificadora registrada ante el BCCR) con\n`POST /api/v1/public/certificates`. Almendro valida el certificado, extrae su información\ny lo almacena de forma segura. La llave privada nunca se expone en ningún endpoint.\nNecesitará también el PIN de Hacienda que usa para acceder al sistema de comprobantes\nelectrónicos del Ministerio.\n\n**Paso 4 — Emita su primer comprobante.** Envíe los datos de la factura con\n`POST /api/v1/public/vouchers`. Almendro genera el XML conforme al esquema v4.4 de\nHacienda, lo valida contra los esquemas oficiales, lo firma digitalmente con XAdES-EPES\ny lo envía automáticamente al Ministerio de Hacienda. Usted recibe la clave de 50 dígitos\ndel comprobante y puede consultar el resultado con `GET /api/v1/public/vouchers/{key}`\no recibir una notificación automática por Webhook cuando Hacienda responda.\n\n> **Recomendación:** antes de emitir en producción, pruebe su integración usando el\n> ambiente sandbox. Los comprobantes de sandbox se envían al sandbox oficial de Hacienda,\n> no tienen valor fiscal y no consumen su cuota mensual. Vea la sección \"Ambiente Sandbox\"\n> más adelante.\n\n---\n\n### Autenticación\n\nTodas las peticiones a la API requieren un **Bearer Token** en el header `Authorization`:\n\n```\nAuthorization: Bearer {YOUR_AUTH_KEY}\n```\n\nObtenga su token desde el portal en **Configuración**. Cada contribuyente genera y\nadministra su propio token de forma independiente.\n\n**Aislamiento de datos:** cada token está vinculado a un contribuyente específico. Todas\nsus consultas retornan exclusivamente los comprobantes, clientes, productos y configuración\nde su propio contribuyente. Es imposible acceder a datos de otro contribuyente, incluso\nsi conociera su token. Este aislamiento se aplica automáticamente en todos los endpoints\nsin excepción.\n\n**Gestión del token:** puede consultar los metadatos de su token activo (fecha de creación,\núltimo uso) con `GET /api/v1/public/tokens/current`. Para generar un nuevo token y revocar\nel anterior, use `POST /api/v1/public/tokens`.\n\n---\n\n### Formato de respuesta\n\nTodas las respuestas de la API siguen un formato JSON consistente:\n\n```json\n{\n \"success\": true,\n \"data\": {},\n \"message\": \"Descripción legible en español\",\n \"errors\": null\n}\n```\n\nCuando ocurre un error de validación (`422`), el campo `errors` detalla cada campo que\nfalló con mensajes específicos en español:\n\n```json\n{\n \"success\": false,\n \"data\": null,\n \"message\": \"Los datos proporcionados no son válidos.\",\n \"errors\": {\n \"voucher_type\": [\"El tipo de comprobante (voucher_type) es obligatorio.\"],\n \"line_items.0.cabys_code\": [\"El código CABYS no existe en el catálogo.\"]\n }\n}\n```\n\nEn las respuestas paginadas (listados), se incluyen los campos `meta` y `links` para\nfacilitar la navegación entre páginas:\n\n```json\n{\n \"success\": true,\n \"data\": [...],\n \"message\": \"\",\n \"errors\": null,\n \"meta\": {\n \"current_page\": 1,\n \"last_page\": 5,\n \"per_page\": 15,\n \"total\": 73,\n \"from\": 1,\n \"to\": 15\n },\n \"links\": {\n \"first\": \"...?page=1\",\n \"last\": \"...?page=5\",\n \"prev\": null,\n \"next\": \"...?page=2\"\n }\n}\n```\n\n---\n\n### Emisión de comprobantes\n\nLa emisión es el corazón de la API. El proceso es asíncrono: el endpoint retorna\n`202 Accepted` en milisegundos y el envío a Hacienda ocurre en segundo plano.\n\n```mermaid\nsequenceDiagram\n participant I as Su sistema\n participant A as Almendro\n participant H as Hacienda\n\n I->>+A: POST /vouchers\n Note right of A: Almendro genera clave, XML v4.4 y firma XAdES-EPES\n A-->>-I: 202 Accepted (clave de 50 dígitos)\n\n rect rgba(128, 128, 128, 0.08)\n Note over A,H: Procesamiento asíncrono (5 a 60 segundos)\n A->>+H: XML firmado\n H-->>-A: Respuesta fiscal\n end\n\n alt\n A-->>I: Webhook voucher.accepted\n else\n A-->>I: Webhook voucher.rejected\n end\n```\n\nEl endpoint retorna **`202 Accepted`** inmediatamente. Esto confirma que el comprobante\nfue generado, validado contra los esquemas XSD oficiales de Hacienda y firmado\ndigitalmente. El envío a Hacienda ocurre automáticamente en segundo plano.\n\n> **Importante:** el código `202 Accepted` no significa \"aceptado por Hacienda\". Es la\n> confirmación de que Almendro recibió y procesó su solicitud correctamente. La respuesta\n> fiscal de Hacienda se obtiene consultando el estado del comprobante o configurando Webhooks.\n\n**Atajos para emisión rápida:** si tiene clientes y productos registrados en su catálogo de\nAlmendro, puede simplificar el payload de emisión. Envíe `client_id` para resolver\nautomáticamente los datos del receptor, y `item_id` en cada línea para resolver código CABYS,\ndescripción, unidad de medida, precio e impuesto. Si incluye campos explícitos junto con\nel `client_id` o `item_id`, los valores explícitos siempre tienen prioridad.\n\n**Anulación:** para anular un comprobante previamente aceptado por Hacienda, use\n`POST /api/v1/public/vouchers/{key}/cancel`. Almendro genera automáticamente una Nota de\nCrédito (tipo 03) que referencia al comprobante original.\n\n**Estados del comprobante:**\n\n| Estado | Significado |\n|--------|------------|\n| `draft` | Generado, pendiente de firma (ocurre solo si hay un error temporal con el certificado) |\n| `pending` | Firmado, pendiente de envío a Hacienda |\n| `sent` | Enviado a Hacienda, esperando respuesta |\n| `accepted` | Aceptado por Hacienda — tiene validez fiscal |\n| `rejected` | Rechazado por Hacienda — consulte el campo `hacienda_message` para conocer el motivo |\n| `error` | Error técnico al comunicarse con Hacienda — se reintenta automáticamente |\n| `cancelled` | Anulado mediante Nota de Crédito |\n\n---\n\n### Impuestos y cálculo de totales\n\nAlmendro calcula automáticamente todos los totales del ResumenFactura a partir de las\nlíneas de detalle. Usted solo necesita enviar los impuestos correctamente en cada línea.\n\n#### Estructura del impuesto por línea\n\nCada línea (`line_items[]`) puede tener un array `taxes[]` con uno o más impuestos:\n\n```json\n{\n \"taxes\": [\n {\n \"codigo\": \"01\",\n \"codigoTarifa\": \"08\",\n \"tarifa\": \"13.00\",\n \"monto\": \"1300.00000\"\n }\n ]\n}\n```\n\n| Campo | Descripción | Valores comunes |\n|-------|-------------|----------------|\n| `codigo` | Código del impuesto | `01`=IVA, `02`=Selectivo consumo, `07`=IVA cálculo especial, `08`=IVA bienes usados |\n| `codigoTarifa` | Tarifa del IVA (obligatorio si `codigo` es `01` o `07`) | `01`=0%, `02`=1%, `03`=2%, `04`=4%, `08`=13%, `10`=Exenta |\n| `tarifa` | Porcentaje de la tarifa | `\"13.00\"`, `\"4.00\"`, `\"0.00\"` |\n| `monto` | Monto del impuesto: `base_imponible × tarifa / 100` | `\"1300.00000\"` |\n| `exoneracion` | Objeto con datos de exoneración (opcional) | Ver sección Exonerados |\n| `factor` | Factor para IVA cálculo especial (solo código `07`) | `\"0.5000\"` |\n\n#### Clasificación automática de cada línea\n\nAlmendro clasifica cada línea en **dos dimensiones** para calcular los 8 subtotales del\nResumenFactura:\n\n**Dimensión 1 — Tipo de bien** (primer dígito del código CABYS):\n\n| Primer dígito CABYS | Clasificación | Ejemplo |\n|---------------------|--------------|---------|\n| `0`, `1`, `2`, `3`, `4` | Mercancía | `1234500000000` → mercancía |\n| `5`, `6`, `7`, `8`, `9` | Servicio | `7331100000000` → servicio |\n\n**Dimensión 2 — Condición del IVA** (determinado por el array `taxes[]`):\n\n| Condición | Cuándo aplica |\n|-----------|--------------|\n| **Gravado** | Tiene IVA (código `01` o `07`) con tarifa distinta de `10` y sin exoneración |\n| **Exento** | Tiene IVA con tarifa `10` (exenta), O no tiene ningún impuesto IVA |\n| **Exonerado** | Tiene IVA con objeto `exoneracion` presente |\n| **No sujeto** | Casos especiales del Art. 9 LIVA (prácticamente nunca en facturación normal) |\n\nEstas dos dimensiones generan los 8 subtotales del XML:\n\n```\n Servicio Mercancía\n ──────── ─────────\nGravado TotalServGravados TotalMercanciasGravadas\nExento TotalServExentos TotalMercanciasExentas\nExonerado TotalServExonerado TotalMercExonerada\nNo sujeto TotalServNoSujeto TotalMercNoSujeta\n```\n\n#### Ejemplo 1 — Servicio gravado con IVA 13%\n\n```json\n{\n \"line_items\": [{\n \"line_number\": 1,\n \"cabys_code\": \"7331100000000\",\n \"detail\": \"Servicio de consultoría\",\n \"quantity\": \"1.000\",\n \"unit_of_measure\": \"Sp\",\n \"unit_price\": \"10000.00000\",\n \"total_amount\": \"10000.00000\",\n \"sub_total\": \"10000.00000\",\n \"base_imponible\": \"10000.00000\",\n \"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"08\", \"tarifa\": \"13.00\", \"monto\": \"1300.00000\"}],\n \"impuesto_neto\": \"1300.00000\",\n \"total_line_amount\": \"11300.00000\"\n }]\n}\n```\n\nCABYS `7` → servicio. IVA tarifa `08` → gravado. Aporta ₡10,000 a `TotalServGravados`.\n\n#### Ejemplo 2 — Mercancía gravada con IVA 13%\n\n```json\n{\n \"line_items\": [{\n \"line_number\": 1,\n \"cabys_code\": \"1234500000000\",\n \"detail\": \"Producto de limpieza\",\n \"quantity\": \"2.000\",\n \"unit_of_measure\": \"Unid\",\n \"unit_price\": \"5000.00000\",\n \"total_amount\": \"10000.00000\",\n \"sub_total\": \"10000.00000\",\n \"base_imponible\": \"10000.00000\",\n \"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"08\", \"tarifa\": \"13.00\", \"monto\": \"1300.00000\"}],\n \"impuesto_neto\": \"1300.00000\",\n \"total_line_amount\": \"11300.00000\"\n }]\n}\n```\n\nCABYS `1` → mercancía. IVA tarifa `08` → gravado. Aporta ₡10,000 a `TotalMercanciasGravadas`.\n\n#### Ejemplo 3 — Servicio exento de IVA\n\n**Opción A — Tarifa exenta (`10`):**\n```json\n{\"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"10\", \"tarifa\": \"0.00\", \"monto\": \"0.00000\"}], \"impuesto_neto\": \"0.00000\"}\n```\n\n**Opción B — Sin taxes:**\n```json\n{\"taxes\": [], \"impuesto_neto\": \"0.00000\"}\n```\n\nAmbas clasifican como **exento** → `TotalServExentos` o `TotalMercanciasExentas`.\n\n#### Ejemplo 4 — Factura mixta (servicio + mercancía gravados)\n\n```json\n{\n \"line_items\": [\n {\n \"line_number\": 1,\n \"cabys_code\": \"7331100000000\",\n \"detail\": \"Consultoría técnica\",\n \"quantity\": \"1.000\", \"unit_of_measure\": \"Sp\",\n \"unit_price\": \"50000.00000\", \"total_amount\": \"50000.00000\",\n \"sub_total\": \"50000.00000\", \"base_imponible\": \"50000.00000\",\n \"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"08\", \"tarifa\": \"13.00\", \"monto\": \"6500.00000\"}],\n \"impuesto_neto\": \"6500.00000\", \"total_line_amount\": \"56500.00000\"\n },\n {\n \"line_number\": 2,\n \"cabys_code\": \"1234500000000\",\n \"detail\": \"Material de oficina\",\n \"quantity\": \"10.000\", \"unit_of_measure\": \"Unid\",\n \"unit_price\": \"1000.00000\", \"total_amount\": \"10000.00000\",\n \"sub_total\": \"10000.00000\", \"base_imponible\": \"10000.00000\",\n \"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"08\", \"tarifa\": \"13.00\", \"monto\": \"1300.00000\"}],\n \"impuesto_neto\": \"1300.00000\", \"total_line_amount\": \"11300.00000\"\n }\n ]\n}\n```\n\nResumenFactura: `TotalServGravados`=₡50,000 · `TotalMercanciasGravadas`=₡10,000 · `TotalGravado`=₡60,000 · `TotalImpuesto`=₡7,800 · `TotalComprobante`=₡67,800.\n\n#### Ejemplo 5 — Payload completo funcional (servicio gravado 13%)\n\n```json\n{\n \"voucher_type\": \"01\",\n \"situation\": \"1\",\n \"issued_at\": \"2026-04-20T10:00:00-06:00\",\n \"issuer_activity_code\": \"6201.0\",\n \"sale_condition\": \"01\",\n \"currency_code\": \"CRC\",\n \"exchange_rate\": \"1.00000\",\n \"payment_methods\": [{\"tipo\": \"01\"}],\n \"receiver\": {\n \"id_type\": \"02\",\n \"id_number\": \"3101234567\",\n \"name\": \"Empresa Receptora S.A.\",\n \"emails\": [\"facturacion@receptor.com\"]\n },\n \"line_items\": [{\n \"line_number\": 1,\n \"cabys_code\": \"7331100000000\",\n \"detail\": \"Servicio de desarrollo de software\",\n \"quantity\": \"1.000\",\n \"unit_of_measure\": \"Sp\",\n \"unit_price\": \"100000.00000\",\n \"total_amount\": \"100000.00000\",\n \"sub_total\": \"100000.00000\",\n \"base_imponible\": \"100000.00000\",\n \"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"08\", \"tarifa\": \"13.00\", \"monto\": \"13000.00000\"}],\n \"impuesto_neto\": \"13000.00000\",\n \"total_line_amount\": \"113000.00000\"\n }]\n}\n```\n\n#### Ejemplo 6 — IVA reducido 4% (canasta básica)\n\n```json\n{\"taxes\": [{\"codigo\": \"01\", \"codigoTarifa\": \"04\", \"tarifa\": \"4.00\", \"monto\": \"400.00000\"}], \"impuesto_neto\": \"400.00000\"}\n```\n\n#### Fórmulas de cada línea\n\n```\ntotal_amount = unit_price × quantity\nsub_total = total_amount - descuentos (si aplica)\nbase_imponible = sub_total (normalmente igual, salvo exoneraciones parciales)\nmonto (impuesto) = base_imponible × tarifa / 100\nimpuesto_neto = monto - monto_exonerado (si aplica)\ntotal_line_amount = sub_total + impuesto_neto\n```\n\nTodos los montos: exactamente **5 decimales** (`\"10000.00000\"`).\n\n#### Errores comunes de Hacienda\n\n| Código | Error | Causa | Solución |\n|--------|-------|-------|----------|\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. |\n| -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. |\n| -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. |\n\n#### Regla de oro\n\n1. **`cabys_code`**: 13 dígitos válidos. Verifique con `GET /catalogs/cabys?search={código}`.\n2. **Gravado** (IVA > 0%): `taxes` con `codigo` + `codigoTarifa` + `tarifa` + `monto`, más `impuesto_neto` y `base_imponible`.\n3. **Exento**: tarifa `10` con monto `0.00000`, O `taxes: []`.\n4. **`monto`** = `base_imponible × tarifa / 100` (5 decimales).\n5. **`total_line_amount`** = `sub_total + impuesto_neto`.\n6. **5 decimales** siempre: `\"10000.00000\"`.\n\nAlmendro calcula automáticamente los 23 campos del ResumenFactura — no envíe totales.\n\n---\n\n### Consulta y descarga de comprobantes\n\nUna vez emitido un comprobante, dispone de varios endpoints para consultarlo y descargar\nsus archivos:\n\n- **Listado con filtros:** `GET /vouchers` permite filtrar por estado, tipo, fecha, cédula\n del receptor, ambiente y moneda. Soporta múltiples valores separados por coma\n (ejemplo: `?status=accepted,rejected` o `?voucher_type=01,04`).\n- **Detalle individual:** `GET /vouchers/{key}` retorna todos los datos del comprobante\n incluyendo líneas de detalle, referencias, medios de pago, totales y el estado de Hacienda.\n- **XML firmado:** `GET /vouchers/{key}/xml` descarga el XML con firma digital tal como fue\n enviado a Hacienda.\n- **Respuesta de Hacienda:** `GET /vouchers/{key}/xml-response` descarga el XML de respuesta\n (MensajeHacienda) que confirma la aceptación o el rechazo.\n- **PDF:** `GET /vouchers/{key}/pdf` descarga la representación gráfica del comprobante,\n generada según la plantilla PDF activa del contribuyente, con código QR conforme a la\n normativa vigente.\n\n---\n\n### Gestión de clientes y productos\n\nAlmendro le permite mantener un catálogo de clientes (receptores frecuentes) y productos\no servicios (líneas de detalle reutilizables). Esto simplifica la emisión de comprobantes\nal permitir referenciar un `client_id` o `item_id` en lugar de repetir todos los datos\nen cada factura.\n\n**Clientes** (`/clients`): registre la razón social, cédula, correos, dirección y teléfono\nde sus receptores frecuentes. Al emitir un comprobante con `client_id`, los datos del\nreceptor se completan automáticamente.\n\n**Productos/Servicios** (`/items`): registre código CABYS, descripción, unidad de medida,\nprecio unitario e impuesto de cada producto o servicio. Al emitir con `item_id` en una\nlínea, esos campos se resuelven automáticamente. Puede asignar un código interno (SKU, PLU)\núnico por contribuyente para localizar rápidamente sus productos.\n\nAmbos catálogos ofrecen los cinco endpoints estándar: listar, crear, consultar, actualizar\ny eliminar.\n\n---\n\n### Plantillas PDF y personalización visual\n\nCada contribuyente puede personalizar cómo lucen sus comprobantes en formato PDF. El sistema\nde plantillas permite configurar colores, fuentes, márgenes, qué secciones mostrar u ocultar,\ny subir su propio logotipo.\n\n**Cinco estilos predefinidos:** Predeterminado, Moderno, Clásico, Minimalista y Dividido.\nCada uno ofrece un diseño distinto que se adapta a diferentes tipos de negocio.\n\n**Configuración visual:** a través del campo `config_json` puede ajustar colores\n(primario, secundario, texto, acento en formato hexadecimal), familias tipográficas,\ntamaños de fuente para encabezado, cuerpo y pie de página, y márgenes en milímetros.\n\n**Logotipo:** suba el logo de su empresa con `POST /pdf-templates/{id}/logo` (formatos\nJPG, PNG o SVG, máximo 2 MB). El logo aparecerá en la posición configurada (izquierda,\ncentro o derecha) de todos los comprobantes generados con esa plantilla.\n\n**Restricciones normativas:** el código QR debe medir al menos 2.5 cm de alto por 2.5 cm\nde ancho y ubicarse en la parte inferior derecha del PDF, conforme al artículo 5 de la\nResolución. Estas restricciones se validan automáticamente y no pueden modificarse.\n\nPuede tener varias plantillas y marcar una como predeterminada. La cantidad máxima de\nplantillas depende de su plan.\n\n---\n\n### Configuración de email transaccional\n\nAlmendro envía automáticamente los comprobantes por correo electrónico al receptor,\nadjuntando el XML firmado y el PDF con código QR, conforme al artículo 18 del Reglamento\nque obliga la entrega del comprobante electrónico y su representación gráfica.\n\nDesde `PUT /email-settings` puede configurar:\n\n- **Envío automático:** activar o desactivar el envío al aceptar el comprobante.\n- **Momento del envío:** enviar solo cuando Hacienda acepta, o también al emitir.\n- **Adjuntos:** incluir o excluir el XML y/o el PDF del correo.\n- **Reply-to:** dirección personalizada para que las respuestas del receptor lleguen\n a su correo en lugar del correo del sistema.\n- **BCC:** hasta 4 direcciones de copia oculta (por ejemplo, su departamento de\n contabilidad).\n- **Asunto personalizado:** con placeholders dinámicos como `{tipo}`, `{consecutivo}`,\n `{receptor}`, `{total}`, `{moneda}` y `{emisor}`.\n- **Mensaje personalizado:** texto que aparece en el cuerpo del correo antes de los\n datos del comprobante.\n\n---\n\n### Webhooks — notificaciones en tiempo real\n\nEn lugar de consultar repetidamente el estado de un comprobante, puede configurar\nWebhooks para que Almendro le notifique automáticamente cuando ocurran eventos\nrelevantes. Al recibir la notificación, su sistema puede actualizar sus registros,\nnotificar al usuario final o ejecutar cualquier lógica de negocio.\n\n**Eventos disponibles:**\n\n| Evento | Se dispara cuando... |\n|--------|---------------------|\n| `voucher.accepted` | Hacienda acepta un comprobante |\n| `voucher.rejected` | Hacienda rechaza un comprobante |\n| `voucher.sent` | Un comprobante se envía a Hacienda |\n| `receiver.confirmed` | Un receptor confirma o rechaza un comprobante recibido |\n| `receiver.deadline` | Se acerca el vencimiento del plazo de 8 días hábiles para confirmar |\n| `certificate.expiring` | Un certificado digital está próximo a vencer |\n\n**Seguridad:** cada notificación incluye una firma HMAC-SHA256 en el header para que su\nservidor pueda verificar que la notificación proviene de Almendro y no fue alterada en\ntránsito. La URL de su endpoint debe usar HTTPS.\n\n**Tolerancia a fallos:** si su servidor no responde o retorna un error, Almendro reintenta\ncon intervalos crecientes. Tras 10 fallos consecutivos, el endpoint se desactiva\nautomáticamente. Puede reactivarlo en cualquier momento con `PUT /webhooks/{id}`.\n\n**Historial de entregas:** consulte el registro de entregas con\n`GET /webhooks/{id}/logs` para verificar qué notificaciones se enviaron, cuáles\nfallaron y cuáles se reintentaron.\n\nLa cantidad de endpoints webhook disponibles depende de su plan (desde 1 en el plan\nPyme hasta 15 en el plan Integrador).\n\n---\n\n### Reportes y análisis financiero\n\nLa API ofrece siete endpoints de reportes diseñados para alimentar tableros de control,\npreparar la declaración D-104 y analizar el desempeño del negocio. Todos comparten los\nmismos filtros: rango de fechas, tipo de comprobante, estado, moneda, ambiente, cédula\ndel receptor, condición de venta y medio de pago.\n\n- **Resumen general** (`/reports/summary`): totales de ventas, impuestos y cantidad de\n comprobantes para el período seleccionado.\n- **Ventas por período** (`/reports/sales-by-period`): evolución temporal agrupada por\n día, semana o mes. La agrupación se detecta automáticamente según el rango de fechas,\n pero puede forzarla con el parámetro `group_by`.\n- **Ventas por receptor** (`/reports/sales-by-receiver`): ranking de los receptores con\n mayor volumen de ventas.\n- **Ventas por actividad** (`/reports/sales-by-activity`): desglose por actividad\n económica CIIU del emisor.\n- **Resumen de IVA** (`/reports/tax-summary`): desglose de IVA por tarifa, útil para\n preparar la declaración D-104.\n- **Comprobantes por estado** (`/reports/vouchers-by-status`): distribución de\n comprobantes según su estado (aceptados, rechazados, pendientes, etc.).\n- **Resumen de MensajeReceptor** (`/reports/receiver-messages`): estado de las\n confirmaciones pendientes con información de plazos.\n\n**Valores por defecto:** si no envía filtros, los reportes cubren el mes actual, solo\ncomprobantes de producción aceptados por Hacienda, en colones costarricenses. Estos\nvalores corresponden al período y criterios más comunes para la declaración tributaria\nmensual.\n\n---\n\n### Catálogos oficiales\n\nLa API expone tres catálogos oficiales de Hacienda necesarios para construir comprobantes\nelectrónicos válidos:\n\n- **CABYS** (`/catalogs/cabys`): catálogo de bienes y servicios con aproximadamente\n 20,500 códigos. Busque por texto o por prefijo numérico del código. Cada resultado\n incluye el código de 13 dígitos, la descripción, la tarifa de IVA asociada y si el\n bien es mercancía o servicio.\n- **Actividades económicas** (`/catalogs/activities`): catálogo CIIU 4 con\n aproximadamente 800 actividades. El código de actividad es obligatorio en el campo\n `issuer_activity_code` de todos los comprobantes.\n- **Ubicaciones** (`/catalogs/locations`): división territorial de Costa Rica\n (provincia, cantón, distrito). Consulta jerárquica: sin parámetros retorna las\n 7 provincias; con `?province=1` retorna los cantones de San José; con\n `?province=1&canton=01` retorna los distritos.\n\n---\n\n### Consulta de contribuyentes\n\nEl endpoint `GET /taxpayer/{id_number}` permite verificar los datos de una cédula ante\nel registro de Hacienda antes de emitir un comprobante. Retorna el nombre oficial, tipo\nde identificación, régimen tributario, situación (activo/inactivo) y actividades\neconómicas inscritas.\n\n**Casos de uso frecuentes:**\n\n- Autocompletar los datos del receptor al digitar la cédula en su formulario de emisión.\n- Validar que la cédula existe antes de emitir, para evitar rechazos de Hacienda.\n- Obtener las actividades económicas del receptor para el campo `receiver_activity_code`\n de la Factura de Compra (tipo 08).\n\nPara cédulas físicas de 9 dígitos y planes con la funcionalidad habilitada, el endpoint\nenriquece la respuesta con datos del Padrón Electoral del TSE (nombre completo, vigencia\nde la cédula, ubicación). Si la persona no está inscrita como contribuyente ante Hacienda\npero sí aparece en el Padrón TSE, el endpoint la identifica como consumidor final,\nindicando que puede recibir Tiquetes Electrónicos (tipo 04) pero no Facturas Electrónicas.\n\nLos resultados se almacenan en caché durante 24 horas para respuestas rápidas en\nconsultas posteriores.\n\n---\n\n### Confirmación del Receptor (MensajeReceptor)\n\nConforme al artículo 15 del Reglamento, los receptores de comprobantes tienen **8 días\nhábiles** (excluyendo fines de semana y feriados nacionales de Costa Rica) para responder\na un comprobante recibido de un proveedor. Vencido el plazo, se considera aceptación tácita.\n\nEl endpoint `POST /receiver/confirm` le permite enviar la respuesta a Hacienda con tres\nopciones:\n\n| Código | Tipo de respuesta | Consecuencia |\n|--------|------------------|-------------|\n| `1` | Aceptado | Genera crédito fiscal para el receptor |\n| `2` | Aceptado Parcialmente | Genera crédito fiscal parcial |\n| `3` | Rechazado | No genera crédito fiscal |\n\nAl aceptar (total o parcialmente), debe indicar la condición del impuesto (crédito IVA\ngeneral, crédito parcial, bienes de capital, gasto sin crédito, o proporcionalidad),\nel monto del impuesto acreditable y la actividad económica asociada.\n\nLa respuesta incluye información detallada del plazo: días hábiles transcurridos, días\nrestantes, fecha límite y alertas de vencimiento próximo.\n\n---\n\n### Modo Integrador — gestión de múltiples contribuyentes\n\nSi usted es integrador (plan Integrador), puede administrar la facturación de múltiples\nclientes desde su propia cuenta. Esto es ideal para empresas que desarrollan software\nde punto de venta, e-commerce, hotelería o cualquier sistema que facture en nombre de\nterceros.\n\n**Cómo funciona:**\n\n**Vincular un cliente nuevo:** créelo con `POST /my-contributors` indicando su cédula,\nrazón social, correos y actividades económicas. Almendro crea la cuenta del cliente con\nun usuario propietario y una contraseña temporal. El cliente recibe un email de bienvenida\ncon instrucciones para acceder a su portal. Automáticamente recibe el plan Gratis\n(5 comprobantes por mes) para que pueda probar el servicio.\n\n**Vincular un cliente que ya tiene cuenta:** si el cliente ya está registrado en Almendro,\nsolicite acceso con `POST /access-requests`. El cliente recibe una notificación por email\ny en su portal, y puede aceptar (total o parcialmente, eligiendo qué ambientes autoriza)\no rechazar la solicitud. Al aceptar, usted recibe acceso al certificado digital del\ncliente con una terminal exclusiva que evita colisión de consecutivos.\n\n**Emitir en nombre del cliente:** incluya `managed_contributor_id` en el payload de\nemisión. El comprobante se registra bajo la cédula del cliente ante Hacienda (el cliente\naparece como emisor, nunca el integrador), se firma con el certificado digital del\ncliente y se envía desde su cuenta. El comprobante cuenta contra el límite mensual de\nsu plan de integrador, no del plan del cliente.\n\n**Administrar certificados:** suba el certificado `.p12` del cliente con\n`POST /my-contributors/{id}/certificates`. El certificado queda vinculado a la cuenta del\ncliente, no a la suya. Puede listar los certificados activos y su estado de vigencia.\n\n**Revocar acceso:** el cliente puede revocar su acceso en cualquier momento desde\n`DELETE /certificate-grants/{id}`. Usted también puede renunciar voluntariamente\na un acceso desde `DELETE /my-access/{grantId}`.\n\n#### Ejemplo completo — emitir una factura en nombre de un cliente\n\nA continuación se muestra el flujo completo desde cero. Todos los pasos usan el **token\ndel integrador** — no se necesita el token del cliente en ningún momento.\n\n**Paso 1 — Buscar al cliente por cédula (opcional, para autocompletar datos)**\n\n```bash\ncurl -X GET \"https://fe.almendro.cr/api/v1/public/taxpayer/3101234567\" \\\n -H \"Authorization: Bearer {TOKEN_DEL_INTEGRADOR}\"\n```\n\nRetorna nombre oficial, tipo de identificación, régimen tributario y actividades económicas\ndel contribuyente según el registro de Hacienda. Use estos datos para prellenar el paso 2.\n\n**Paso 2 — Crear al cliente como contribuyente gestionado**\n\n```bash\ncurl -X POST \"https://fe.almendro.cr/api/v1/public/my-contributors\" \\\n -H \"Authorization: Bearer {TOKEN_DEL_INTEGRADOR}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"legal_name\": \"Empresa del Cliente S.A.\",\n \"id_type\": \"02\",\n \"id_number\": \"3101234567\",\n \"email\": \"cliente@ejemplo.com\",\n \"password\": \"ContraseñaSegura123!\"\n }'\n```\n\nLa respuesta incluye el **UUID del cliente** en el campo `data.id`. Guarde este UUID:\nlo necesita para todos los pasos siguientes. El objeto `managed_relationship` describe\nel vínculo activo entre usted (integrador) y el cliente — incluye la terminal exclusiva\nasignada (5 dígitos) para evitar colisión de consecutivos.\n\n```json\n{\n \"success\": true,\n \"data\": {\n \"id\": \"019d867d-0241-7288-8ece-fd64da75616d\",\n \"legal_name\": \"Empresa del Cliente S.A.\",\n \"id_type\": \"02\",\n \"id_type_label\": \"Cédula Jurídica\",\n \"id_number\": \"3101234567\",\n \"is_active\": true,\n \"production_enabled\": false,\n \"plan_id\": \"019d0001-0000-0000-0000-000000000001\",\n \"can_emit_from_portal\": true,\n \"managed_relationship\": {\n \"id\": \"019d870a-0001-7288-8ece-fd64da75a001\",\n \"assigned_terminal\": \"00002\",\n \"retention_months_override\": null,\n \"default_pdf_template_id\": null,\n \"linked_at\": \"2026-04-18T10:00:00-06:00\"\n }\n },\n \"message\": \"Cliente creado correctamente. Se envió email de bienvenida.\",\n \"errors\": null\n}\n```\n\n**Paso 3 — Subir el certificado digital .p12 del cliente**\n\nEl certificado debe pertenecer al cliente (emitido por una CA registrada ante el BCCR\npara la cédula del cliente). Almendro lo almacena cifrado y lo vincula a la cuenta del\ncliente, no a la del integrador.\n\n```bash\ncurl -X POST \"https://fe.almendro.cr/api/v1/public/my-contributors/019d867d-0241-7288-8ece-fd64da75616d/certificates\" \\\n -H \"Authorization: Bearer {TOKEN_DEL_INTEGRADOR}\" \\\n -F \"p12_file=@/ruta/al/certificado-cliente.p12\" \\\n -F \"p12_password=ContraseñaDelP12\" \\\n -F \"hacienda_pin=PinDeHaciendaDelCliente\" \\\n -F \"environment=sandbox\"\n```\n\n**Paso 4 — Emitir un comprobante en nombre del cliente**\n\nUse `POST /vouchers` con su token de integrador y agregue el campo `managed_contributor_id`\ncon el UUID del cliente obtenido en el paso 2. El resto del payload es idéntico a una\nemisión normal:\n\n```bash\ncurl -X POST \"https://fe.almendro.cr/api/v1/public/vouchers\" \\\n -H \"Authorization: Bearer {TOKEN_DEL_INTEGRADOR}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"managed_contributor_id\": \"019d867d-0241-7288-8ece-fd64da75616d\",\n \"voucher_type\": \"01\",\n \"situation\": \"1\",\n \"issued_at\": \"2026-04-18T10:00:00-06:00\",\n \"sale_condition\": \"01\",\n \"payment_methods\": [{\"tipo\": \"01\"}],\n \"receiver\": {\n \"name\": \"Juan Pérez Solís\",\n \"id_type\": \"01\",\n \"id_number\": \"101230456\"\n },\n \"line_items\": [{\n \"line_number\": 1,\n\"cabys_code\": \"4321500000100\",\n \"detail\": \"Servicio de consultoría\",\n \"unit_of_measure\": \"Sp\",\n \"quantity\": \"1.000\",\n \"unit_price\": \"10000.00000\",\n \"sub_total\": \"10000.00000\",\n \"total_amount\": \"10000.00000\",\n \"taxes\": [{\n \"codigo\": \"01\",\n \"codigoTarifa\": \"08\",\n \"tarifa\": \"13.00\",\n \"monto\": \"1300.00000\"\n }],\n \"impuesto_neto\": \"1300.00000\",\n \"total_line_amount\": \"11300.00000\"\n }]\n }'\n```\n\n#### ¿Qué sucede internamente?\n\nCuando envía `managed_contributor_id`, Almendro ejecuta automáticamente lo siguiente:\n\n1. **Verifica** que el UUID pertenece a un cliente vinculado a su cuenta de integrador\n2. **Genera la clave de 50 dígitos** usando la cédula del **cliente** (no la del integrador)\n3. **Construye el XML v4.4** con el **cliente como emisor** (`EmisorType`) ante Hacienda\n4. **Firma digitalmente** con el certificado `.p12` del **cliente**\n5. **Asigna el consecutivo** en los contadores del **cliente**, usando la terminal exclusiva del integrador\n6. **Descuenta** el comprobante del límite mensual del **plan del integrador**\n\nHacienda siempre ve al cliente como el emisor del comprobante. El integrador nunca\naparece en el XML ni en la clave de 50 dígitos.\n\n#### Sin `managed_contributor_id`\n\nSi omite el campo `managed_contributor_id`, el comprobante se emite bajo su propia cédula\nde integrador, como si fuera un contribuyente normal (emisión directa).\n\n#### Consultar comprobantes de un cliente gestionado\n\nUse el filtro `managed_contributor_id` en `GET /vouchers` para ver solo los comprobantes\nemitidos en nombre de un cliente específico:\n\n```bash\ncurl -X GET \"https://fe.almendro.cr/api/v1/public/vouchers?managed_contributor_id=019d867d-0241-7288-8ece-fd64da75616d\" \\\n -H \"Authorization: Bearer {TOKEN_DEL_INTEGRADOR}\"\n```\n\n#### Reportes agregados por cliente\n\nLos endpoints de reportes (`/reports/*`) agregan automáticamente los datos de todos sus\nclientes gestionados. Para filtrar los datos de un solo cliente, use el parámetro\n`?managed_contributor_id={UUID}` en cualquier endpoint de reportes.\n\n---\n\n### Control de acceso — solicitudes y grants\n\nEl sistema de control de acceso regula la relación entre integradores y clientes,\ngarantizando que el cliente siempre tiene soberanía sobre sus certificados digitales\nconforme al artículo 16 del Reglamento.\n\n**Solicitudes de acceso** (`/access-requests`): el integrador solicita, el cliente decide.\nUna solicitud puede estar en estado pendiente, aceptada (total o parcialmente), rechazada\no cancelada. El integrador puede ver sus solicitudes enviadas; el cliente, las recibidas.\n\n**Grants de certificados** (`/certificate-grants`): cuando el cliente acepta una solicitud,\nse crean grants que autorizan al integrador a firmar con el certificado del cliente en los\nambientes aprobados. Cada grant tiene una terminal exclusiva asignada para evitar\nduplicación de consecutivos.\n\nEl cliente puede consultar qué integradores tienen acceso a sus certificados con\n`GET /certificate-grants`, y revocar cualquier acceso en cualquier momento.\n\n---\n\n### Notificaciones\n\nEl sistema de notificaciones mantiene informado al usuario sobre eventos importantes\nque requieren su atención:\n\n- Solicitudes de acceso recibidas de un integrador.\n- Respuestas a solicitudes de acceso enviadas.\n- Certificados próximos a vencer.\n- Cancelaciones de solicitudes.\n- Revocaciones de acceso.\n\nUse `GET /notifications` para obtener la lista paginada de notificaciones con un conteo\nde no leídas para actualizar badges en su interfaz. Las notificaciones no leídas aparecen\nprimero. Puede marcar una notificación como leída con `POST /notifications/{id}/read`\no marcar todas de una vez con `POST /notifications/read-all`.\n\n---\n\n### Planes y límites\n\nEl volumen de comprobantes, la cantidad de peticiones por minuto y las funcionalidades\ndisponibles dependen del plan contratado:\n\n| Plan | Precio | Comprobantes/mes | Excedente | Peticiones/min | Sandbox | Webhooks | Clientes gestionados |\n|------|--------|-----------------|-----------|---------------|---------|----------|---------------------|\n| **Gratis** | $0 | 5 | Bloquea | 5 | No | No | 1 |\n| **Emprendedor** | $36/año | 100 | $0.05/comp | 10 | No | No | 1 |\n| **Pyme** | $96/año | 500 | $0.03/comp | 20 | Sí | 1 endpoint | 1 |\n| **Profesional** | $19/mes o $182/año | 2,000 | $0.02/comp | 40 | Sí | 3 endpoints | 5 |\n| **Empresa** | $39/mes o $351/año | 8,000 | $0.012/comp | 80 | Sí | 5 endpoints | 1 |\n| **Integrador** | $79/mes o $711/año | 50,000 | $0.008/comp | 150 | Sí | 15 endpoints | 100 |\n\n**Límite de peticiones por minuto:** cuando se excede, la API retorna `429 Too Many Requests`\ncon el header `Retry-After` indicando cuántos segundos esperar. Las operaciones de lectura\n(GET) están exentas del límite; solo las operaciones transaccionales (POST, PUT, DELETE)\ncuentan contra la cuota.\n\n**Límite mensual de comprobantes:** solo cuentan comprobantes de producción aceptados por\nHacienda. Los comprobantes de sandbox no consumen cuota. El plan Gratis bloquea la emisión\nal alcanzar el límite. Desde el plan Emprendedor, se permite emitir por encima del límite\ncon un cargo automático por comprobante adicional.\n\n**Otros límites por plan:** cada plan define también la cantidad máxima de clientes en\ncatálogo, productos en catálogo, plantillas PDF, tokens API, emails diarios, sucursales\ny días de anticipación para alertas de vencimiento de certificado. Consulte los detalles\ncompletos en [fe.almendro.cr](https://fe.almendro.cr).\n\n---\n\n### Ambiente Sandbox\n\nAlmendro ofrece un ambiente sandbox que replica los endpoints de emisión contra el sandbox\noficial del Ministerio de Hacienda. Los comprobantes emitidos en sandbox no tienen valor\nfiscal y no cuentan contra su límite mensual.\n\nPara usar el sandbox, reemplace `/api/v1/public/` por `/api/v1/public/sandbox/` en la URL.\nEl token, los headers y el payload son exactamente iguales:\n\n```\nProducción: https://fe.almendro.cr/api/v1/public/vouchers\nSandbox: https://fe.almendro.cr/api/v1/public/sandbox/vouchers\n```\n\n| Aspecto | Producción | Sandbox |\n|---------|------------|---------|\n| Destino | API oficial de Hacienda | API sandbox de Hacienda |\n| Valor fiscal | Sí | No |\n| Cuenta contra límite mensual | Sí | No |\n| Envío de email al receptor | Sí | No (solo registro interno) |\n| Marca de agua en PDF | Sin marca | \"SANDBOX — SIN VALOR FISCAL\" |\n| Token y payload | El mismo | El mismo |\n| Webhooks | Sí | Sí |\n\n**Endpoints sandbox disponibles:** emisión de los 7 tipos de comprobante, consulta, descarga\nXML/PDF, anulación, confirmación del receptor y consulta de contribuyentes. Los demás\nendpoints (clientes, productos, webhooks, reportes, plantillas, certificados, configuración)\nno necesitan URL sandbox porque operan sobre los mismos datos en ambos ambientes.\n\n> El acceso al sandbox requiere plan **Pyme o superior**.\n\n---\n\n### Retención y disponibilidad de XML\n\nAlmendro retiene el XML firmado y la respuesta de Hacienda de cada comprobante durante\n3 meses en todos los planes. Transcurrido ese período, el contenido XML se elimina\nautomáticamente, pero los metadatos del comprobante (totales, estado, receptor, líneas de\ndetalle) y la generación de PDF permanecen disponibles de forma indefinida.\n\nCuando el XML ya no está disponible, los endpoints `GET /vouchers/{key}/xml` y\n`GET /vouchers/{key}/xml-response` retornan `410 Gone` con la fecha en que se eliminó\nel archivo.\n\n**Retención extendida:** puede ampliar la retención de XML hasta 5 años por $12/año\n(disponible desde el plan Emprendedor). Al vencer la retención extendida, se otorga un\nperíodo de gracia de 7 días para que descargue sus documentos antes de la eliminación\ndefinitiva.\n\n---\n\n### Clave de 50 dígitos\n\nCada comprobante se identifica con una Clave numérica de exactamente 50 dígitos, definida\npor los Anexos y Estructuras v4.4 de la DGT. Esta clave se usa en todos los endpoints que\nreciben el parámetro `{key}`:\n\n```\n[3 país][6 fecha DDMMAA][12 cédula emisor][20 consecutivo][1 situación][8 seguridad]\n```\n\nEjemplo: `50613032600206270652001000010100000000011 12345678`\n\n- **País:** siempre `506` (Costa Rica)\n- **Fecha:** día, mes y año de emisión en formato DDMMAA\n- **Cédula:** identificación del emisor, rellenada con ceros hasta 12 dígitos\n- **Consecutivo:** 20 dígitos compuestos por sucursal (3), terminal (5), tipo (2) y secuencia (10)\n- **Situación:** `1` Normal, `2` Contingencia, `3` Sin Internet\n- **Seguridad:** 8 dígitos aleatorios generados por el sistema\n\n---\n\n### Tipos de comprobante soportados\n\n| Código | Tipo | Descripción | Receptor |\n|--------|------|-------------|----------|\n| `01` | Factura Electrónica | Venta de bienes o servicios con receptor identificado | Obligatorio |\n| `02` | Nota de Débito | Corrección que incrementa el monto de un comprobante previo | Obligatorio |\n| `03` | Nota de Crédito | Corrección que disminuye el monto, o anulación total | Obligatorio |\n| `04` | Tiquete Electrónico | Venta al consumidor final sin receptor obligatorio | Opcional |\n| `08` | Factura de Compra | Compra a proveedores no inscritos en el régimen | Obligatorio |\n| `09` | Factura de Exportación | Venta de bienes o servicios fuera del territorio nacional | Opcional |\n| `10` | Recibo Electrónico de Pago | Constancia de recepción de pago en ventas a crédito fiscal o al Estado | Obligatorio |\n\nLas Notas de Débito (02), Notas de Crédito (03), Facturas de Compra (08) y Recibos de Pago (10)\nrequieren al menos una referencia al comprobante original en el campo `references`.\n\n---\n\n### Códigos de estado HTTP\n\n| Código | Significado |\n|--------|------------|\n| `200` | Consulta, actualización o eliminación exitosa |\n| `201` | Recurso creado correctamente (clientes, productos, webhooks, plantillas, certificados) |\n| `202` | Comprobante recibido, validado, firmado y en proceso de envío a Hacienda |\n| `401` | Token de autenticación inválido, expirado o ausente |\n| `403` | Su plan no permite esta operación o no tiene permisos suficientes |\n| `404` | Recurso no encontrado o no pertenece a su contribuyente |\n| `410` | El XML de este comprobante fue eliminado por vencimiento de retención. Los metadatos y el PDF siguen disponibles |\n| `422` | Error de validación. Revise el campo `errors` en la respuesta para detalles por campo |\n| `429` | Límite de peticiones por minuto excedido. Espere los segundos indicados en el header `Retry-After` |\n| `500` | Error interno del servidor |\n| `502` | La API del Ministerio de Hacienda no está disponible temporalmente |\n| `504` | Tiempo de espera agotado al contactar la API del Ministerio de Hacienda |\n\n---\n\n### Soporte y contacto\n\n- **Documentación:** [fe.almendro.cr/docs](https://fe.almendro.cr/docs)\n- **Soporte técnico para desarrolladores:** developers@almendro.cr\n- **Soporte general:** soporte@almendro.cr\n- **Ventas e información comercial:** ventas@almendro.cr\n- **Seguridad:** seguridad@almendro.cr"
version: 1.0.0
contact:
name: 'Almendro Factura Electrónica — Soporte Developers'
email: developers@almendro.cr
url: 'https://fe.almendro.cr/docs'
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — los comprobantes tienen valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — solo emisión, comprobantes, receptor y contribuyentes (sin valor fiscal)'
tags:
-
name: 'Comprobantes Electrónicos'
description: 'Emita, consulte, descargue XML/PDF y anule comprobantes electrónicos v4.4 ante el Ministerio de Hacienda.'
-
name: Perfil
description: 'Consulte y actualice los datos del contribuyente autenticado: razón social, dirección y uso del plan.'
-
name: 'Certificados Digitales'
description: 'Gestione los certificados .p12 de firma digital emitidos por el BCCR. Suba, liste y desactive por ambiente.'
-
name: 'Plantillas PDF'
description: 'Personalice la representación gráfica de sus comprobantes: colores, fuentes, logotipo y estilos predefinidos.'
-
name: 'Configuración Email'
description: 'Configure el envío automático de comprobantes por correo: adjuntos, reply-to, BCC y asunto personalizado.'
-
name: Clientes
description: 'Catálogo de receptores frecuentes. Registre cédula y razón social para simplificar la emisión con client_id.'
-
name: 'Items / Productos'
description: 'Catálogo de productos y servicios reutilizables. Registre CABYS, precio e impuesto para emitir con item_id.'
-
name: Webhooks
description: 'Reciba notificaciones en tiempo real cuando Hacienda responda: voucher.accepted, voucher.rejected y más.'
-
name: Reportes
description: 'Datos para tableros de control y declaración D-104: ventas por período, receptor, actividad y resumen de IVA.'
-
name: Catálogos
description: 'Catálogos oficiales de Hacienda: CABYS (20,500 códigos), actividades económicas CIIU y ubicaciones de Costa Rica.'
-
name: 'Consulta de Contribuyentes'
description: 'Verifique cédulas ante Hacienda antes de emitir. Retorna nombre, régimen, actividades y estado.'
-
name: 'Confirmación del Receptor'
description: 'Envíe el MensajeReceptor a Hacienda: aceptar, aceptar parcialmente o rechazar comprobantes recibidos.'
-
name: 'Clientes del Integrador'
description: 'Gestione contribuyentes-cliente desde su cuenta de integrador: cree cuentas, suba certificados y administre secuencias.'
-
name: 'Mis Integradores'
description: 'Vista del cliente: vea qué integradores lo gestionan y libérese de cualquiera revocando sus accesos.'
-
name: 'Solicitudes de Acceso'
description: 'Flujo de autorización integrador↔cliente: solicite, acepte, rechace o cancele accesos a certificados digitales.'
-
name: 'Grants de Certificados'
description: 'Consulte y revoque los grants que autorizan a integradores a firmar con su certificado digital.'
-
name: 'Mi Acceso'
description: 'Vista del integrador: consulte los grants que clientes le otorgaron y renuncie voluntariamente a cualquier acceso.'
-
name: Notificaciones
description: 'Bandeja de notificaciones: solicitudes de acceso, certificados por vencer, grants revocados.'
-
name: 'Token API'
description: 'Consulte metadatos de su token activo y regenere un nuevo Bearer token (el anterior se revoca automáticamente).'
-
name: 'Solicitud de Upgrade'
description: 'Solicite actualización de plan. El equipo de Almendro lo contacta para completar el proceso.'
components:
securitySchemes:
default:
type: http
scheme: bearer
description: '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.'
security:
-
default: []
paths:
/vouchers:
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Listar comprobantes con filtros y paginación'
operationId: listarComprobantesConFiltrosYPaginacin
description: "Devuelve todos los comprobantes emitidos por el contribuyente,\ncon paginación y filtros por estado, tipo, rango de fechas, receptor,\nambiente y moneda. Los filtros `status` y `voucher_type` aceptan\nmúltiples valores separados por coma.\n\nEl listado retorna campos resumidos (sin XML ni totales detallados)\npara máxima eficiencia. Para el detalle completo de un comprobante,\nuse `GET /vouchers/{key}`."
parameters:
-
in: query
name: status
description: 'Estado(s) separados por coma. `draft`, `pending`, `sent`, `accepted`, `rejected`, `error`, `cancelled`.'
example: 'accepted,rejected'
required: false
schema:
type: string
description: 'Estado(s) separados por coma. `draft`, `pending`, `sent`, `accepted`, `rejected`, `error`, `cancelled`.'
example: 'accepted,rejected'
-
in: query
name: voucher_type
description: 'Tipo(s) separados por coma. `01`=FE, `02`=ND, `03`=NC, `04`=TE, `08`=FEC, `09`=FEE, `10`=REP.'
example: '01,04'
required: false
schema:
type: string
description: 'Tipo(s) separados por coma. `01`=FE, `02`=ND, `03`=NC, `04`=TE, `08`=FEC, `09`=FEE, `10`=REP.'
example: '01,04'
-
in: query
name: date_from
description: 'Fecha de emisión desde (YYYY-MM-DD).'
example: '2026-04-01'
required: false
schema:
type: string
description: 'Fecha de emisión desde (YYYY-MM-DD).'
example: '2026-04-01'
-
in: query
name: date_to
description: 'Fecha de emisión hasta (YYYY-MM-DD).'
example: '2026-04-30'
required: false
schema:
type: string
description: 'Fecha de emisión hasta (YYYY-MM-DD).'
example: '2026-04-30'
-
in: query
name: receiver_id_number
description: 'Cédula del receptor (9-12 dígitos).'
example: '3101234567'
required: false
schema:
type: string
description: 'Cédula del receptor (9-12 dígitos).'
example: '3101234567'
-
in: query
name: environment
description: 'Ambiente: `sandbox` o `production`.'
example: production
required: false
schema:
type: string
description: 'Ambiente: `sandbox` o `production`.'
example: production
-
in: query
name: currency_code
description: 'Código ISO 4217.'
example: CRC
required: false
schema:
type: string
description: 'Código ISO 4217.'
example: CRC
-
in: query
name: contributor_id
description: 'UUID del cliente gestionado cuyos comprobantes se consultan. Solo para plan Integrador con relación managed activa. Sin este filtro, retorna comprobantes propios. validation.uuid.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: false
schema:
type: string
description: 'UUID del cliente gestionado cuyos comprobantes se consultan. Solo para plan Integrador con relación managed activa. Sin este filtro, retorna comprobantes propios. validation.uuid.'
example: 019d867d-c001-7288-8ece-fd64da756c01
-
in: query
name: sort_by
description: 'Campo de ordenamiento. Default: `issued_at`.'
example: issued_at
required: false
schema:
type: string
description: 'Campo de ordenamiento. Default: `issued_at`.'
example: issued_at
-
in: query
name: sort_dir
description: 'Dirección: `asc` o `desc`. Default: `desc`.'
example: desc
required: false
schema:
type: string
description: 'Dirección: `asc` o `desc`. Default: `desc`.'
example: desc
-
in: query
name: per_page
description: 'Resultados por página (1-100). Default: 15.'
example: 15
required: false
schema:
type: integer
description: 'Resultados por página (1-100). Default: 15.'
example: 15
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: 'Listado paginado'
content:
application/json:
schema:
type: object
example:
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: ...
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
voucher_key:
type: string
example: 50620032600206270652001000010100000000011XXXXXXXX
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
consecutive_number:
type: string
example: '00100001010000000001'
issued_at:
type: string
example: '2026-03-20T10:30:00-06:00'
situation:
type: string
example: '1'
sale_condition:
type: string
example: '01'
sale_condition_label:
type: string
example: Contado
receiver:
type: object
properties:
id_type:
type: string
example: '02'
id_number:
type: string
example: '3102345678'
name:
type: string
example: 'Empresa S.A.'
currency_code:
type: string
example: CRC
total_comprobante:
type: string
example: '56500.00000'
status:
type: string
example: accepted
hacienda:
type: object
properties:
status:
type: string
example: aceptado
sent_at:
type: string
example: '2026-03-20T10:31:00-06:00'
processed_at:
type: string
example: '2026-03-20T10:32:00-06:00'
environment:
type: string
example: sandbox
is_xml_available:
type: boolean
example: true
line_items_count:
type: integer
example: 3
created_at:
type: string
example: '2026-03-20T10:30:00-06:00'
updated_at:
type: string
example: '2026-03-20T10:32:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 5
per_page:
type: integer
example: 15
total:
type: integer
example: 72
from:
type: integer
example: 1
to:
type: integer
example: 15
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: ...
tags:
- 'Comprobantes Electrónicos'
post:
summary: 'Emitir un comprobante electrónico'
operationId: emitirUnComprobanteElectrnico
description: "Genera, valida contra el XSD oficial v4.4, firma digitalmente con\nXAdES-EPES y envía automáticamente a Hacienda cualquiera de los 7\ntipos de comprobante soportados.\n\nLa respuesta es **HTTP 202 Accepted** — el envío a Hacienda es\nasíncrono. Para obtener el resultado final (aceptado/rechazado),\nsuscríbase a los eventos `voucher.accepted` y `voucher.rejected` vía\nwebhooks (ver grupo **Webhooks**), o consulte periódicamente\n`GET /vouchers/{key}`.\n\n> **HTTP 202 no significa \"aceptado por Hacienda\".** Solo confirma\n> que el comprobante fue generado, firmado y encolado exitosamente.\n> El estado inicial en la respuesta es `pending`.\n\nPara ejemplos completos de payload por cada tipo de comprobante,\nrevise la sección **\"Ejemplos completos de payload por tipo\"** de\nla guía de integración al inicio de este grupo."
parameters: []
responses:
202:
description: 'Comprobante emitido correctamente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
voucher_key:
type: string
example: 50620042600206270652001000010100000000011XXXXXXXX
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
consecutive_number:
type: string
example: '00100001010000000001'
issued_at:
type: string
example: '2026-04-20T10:30:00-06:00'
situation:
type: string
example: '1'
situation_label:
type: string
example: Normal
sale_condition:
type: string
example: '01'
sale_condition_label:
type: string
example: Contado
sale_condition_other:
type: string
example: null
nullable: true
credit_term:
type: string
example: null
nullable: true
receiver:
type: object
properties:
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101000001'
name:
type: string
example: 'EMPRESA EJEMPLO S.A.'
commercial_name:
type: string
example: null
nullable: true
emails:
type: array
example:
- facturacion@ejemplo.cr
items:
type: string
currency_code:
type: string
example: CRC
exchange_rate:
type: string
example: '1.00000'
totals:
type: object
properties:
serv_gravados:
type: string
example: '10000.00000'
serv_exentos:
type: string
example: '0.00000'
serv_exonerado:
type: string
example: '0.00000'
serv_no_sujeto:
type: string
example: '0.00000'
merc_gravadas:
type: string
example: '0.00000'
merc_exentas:
type: string
example: '0.00000'
merc_exonerada:
type: string
example: '0.00000'
merc_no_sujeta:
type: string
example: '0.00000'
total_gravado:
type: string
example: '10000.00000'
total_exento:
type: string
example: '0.00000'
total_exonerado:
type: string
example: '0.00000'
total_no_sujeto:
type: string
example: '0.00000'
total_venta:
type: string
example: '10000.00000'
total_descuentos:
type: string
example: '0.00000'
total_venta_neta:
type: string
example: '10000.00000'
total_impuesto:
type: string
example: '1300.00000'
total_imp_asum_emisor_fab:
type: string
example: '0.00000'
total_iva_devuelto:
type: string
example: '0.00000'
total_otros_cargos:
type: string
example: '0.00000'
total_comprobante:
type: string
example: '11300.00000'
payment_methods:
type: array
example:
-
tipo: '01'
items:
type: object
properties:
tipo:
type: string
example: '01'
status:
type: string
example: pending
hacienda:
type: object
properties:
status:
type: string
example: null
nullable: true
message:
type: string
example: null
nullable: true
sent_at:
type: string
example: null
nullable: true
processed_at:
type: string
example: null
nullable: true
environment:
type: string
example: production
is_xml_available:
type: boolean
example: true
line_items_count:
type: integer
example: 1
references_count:
type: integer
example: 0
created_at:
type: string
example: '2026-04-20T10:30:00-06:00'
updated_at:
type: string
example: '2026-04-20T10:30:00-06:00'
message:
type: string
example: 'Comprobante [50620042600206270652001000010100000000011XXXXXXXX] generado y encolado para envío a Hacienda.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Error de validación del payload'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
voucher_type:
- 'El campo voucher_type es obligatorio.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
voucher_type:
type: array
example:
- 'El campo voucher_type es obligatorio.'
items:
type: string
-
description: 'Error de validación XSD (no consume consecutivo)'
type: object
example:
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'."
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "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:
type: object
properties:
voucher_type:
type: array
example:
- '01'
items:
type: string
xsd:
type: array
example:
-
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'."
items:
type: object
properties:
nivel:
type: string
example: error
codigo:
type: integer
example: 1824
linea:
type: integer
example: 45
columna:
type: integer
example: 12
mensaje:
type: string
example: "Element 'CodigoCABYS': [facet 'length'] The value has a length of '12'; this differs from the allowed length of '13'."
-
description: 'Error de firma digital (consume consecutivo)'
type: object
example:
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'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No se pudo firmar el comprobante. El certificado digital está vencido. Renueve su certificado con el BCCR y vuelva a subirlo.'
errors:
type: object
properties:
signature:
type: array
example:
- ...
items:
type: string
code:
type: array
example:
- '2002'
items:
type: string
-
description: 'Cupo mensual agotado'
type: object
example:
success: false
data: null
message: 'Ha alcanzado el límite mensual de comprobantes de su plan.'
errors:
plan:
- 'Actualice a un plan superior o active overage para continuar emitiendo.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ha alcanzado el límite mensual de comprobantes de su plan.'
errors:
type: object
properties:
plan:
type: array
example:
- 'Actualice a un plan superior o active overage para continuar emitiendo.'
items:
type: string
tags:
- 'Comprobantes Electrónicos'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
voucher_type:
type: string
description: "Tipo de comprobante.\n `01`=FE, `02`=ND, `03`=NC, `04`=TE, `08`=FEC, `09`=FEE, `10`=REP."
example: '01'
situation:
type: string
description: 'Situación del comprobante. `1`=Normal, `2`=Contingencia, `3`=Sin internet.'
example: '1'
issued_at:
type: string
description: "Fecha y hora de emisión ISO 8601 con offset de Costa Rica (`-06:00`).\n No puede ser futura."
example: '2026-04-20T10:30:00-06:00'
issuer_activity_code:
type: string
description: 'Código CIIU del emisor en formato Hacienda (`XXXX.X`).'
example: '6201.0'
nullable: true
receiver_activity_code:
type: string
description: 'Código CIIU del receptor (`XXXX.X`). Solo para FEC (tipo 08).'
example: '4711.0'
nullable: true
sale_condition:
type: string
description: 'Condición de venta. `01`=Contado, `02`=Crédito, etc.'
example: '01'
sale_condition_other:
type: string
description: 'Descripción si `sale_condition=99`. Obligatorio para `99` (5-100 chars).'
example: 'Acuerdo especial de intercambio'
nullable: true
credit_term:
type: string
description: 'Plazo del crédito en días. Obligatorio si `sale_condition` es `02`, `08` o `10`.'
example: '30'
nullable: true
currency_code:
type: string
description: 'Código ISO 4217. Default: `CRC`.'
example: CRC
exchange_rate:
type: string
description: 'Tipo de cambio respecto al CRC (5 decimales). Requerido si `currency_code != CRC`.'
example: '1.00000'
observations:
type: string
description: 'Observaciones opcionales (máx 250 chars). validation.max.'
example: null
nullable: true
client_id:
type: string
description: 'UUID de un cliente del catálogo (`GET /clients`). Sobrescribe los datos del receptor.'
example: 019d1234-0000-0000-0000-000000000001
nullable: true
managed_contributor_id:
type: string
description: "UUID del cliente gestionado. Solo para integradores.\n Si se envía, el comprobante se emite bajo la cédula de ese cliente."
example: 019d867d-0241-7288-8ece-fd64da75616d
nullable: true
receiver:
type: object
description: 'Datos del receptor. Requerido para FE/ND/NC/FEC. Opcional si se envía `client_id`.'
example: []
properties:
name:
type: string
description: 'Nombre o razón social (3–100 chars).'
example: 'Empresa Ejemplo S.A.'
id_type:
type: string
description: 'Tipo de identificación. `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE, `05`=Extranjero.'
example: '02'
id_number:
type: string
description: 'Número de identificación.'
example: '3101000001'
commercial_name:
type: string
description: validation.max.
example: b
nullable: true
emails:
type: array
description: 'Correos del receptor para envío automático del comprobante.'
example:
- architecto
items:
type: string
province:
type: integer
description: validation.between.
example: 2
nullable: true
canton:
type: string
description: 'Must match the regex /^\d{2}$/.'
example: '56'
nullable: true
district:
type: string
description: 'Must match the regex /^\d{2}$/.'
example: '56'
nullable: true
address:
type: string
description: 'validation.min validation.max.'
example: i
nullable: true
phone_country_code:
type: integer
description: 'validation.min validation.max.'
example: 8
nullable: true
phone:
type: string
description: 'Must match the regex /^\d{4,20}$/.'
example: '564255931'
nullable: true
required:
- name
- id_type
- id_number
nullable: true
line_items:
type: array
description: 'Líneas de detalle (máximo 1000).'
example:
- []
items:
type: object
properties:
item_id:
type: string
description: 'UUID de un producto del catálogo (`GET /items`). Resuelve CABYS, descripción, unidad, precio e impuesto automáticamente. Los campos explícitos tienen prioridad.'
example: 019d1234-0000-0000-0000-000000000002
nullable: true
line_number:
type: integer
description: 'Número de línea (1-based).'
example: 1
cabys_code:
type: string
description: 'Código CABYS de 13 dígitos.'
example: '5311100000000'
arancelary_partition:
type: string
description: 'Must match the regex /^\d{12}$/.'
example: '564255931423'
nullable: true
quantity:
type: string
description: 'Cantidad con 3 decimales.'
example: '1.000'
unit_of_measure:
type: string
description: 'Unidad de medida (`Unid`, `kg`, `Sp`, etc.).'
example: Unid
detail:
type: string
description: 'Descripción (3–200 chars).'
example: 'Servicio de consultoría'
unit_price:
type: string
description: 'Precio unitario (5 decimales).'
example: '10000.00000'
total_amount:
type: string
description: 'Monto total antes de descuentos.'
example: '10000.00000'
sub_total:
type: string
description: 'Subtotal de la línea.'
example: '10000.00000'
base_imponible:
type: string
description: 'Base imponible para el impuesto.'
example: '10000.00000'
nullable: true
discounts:
type: array
description: validation.max.
example: null
items:
type: object
nullable: true
properties:
monto:
type: number
description: validation.min.
example: 39
codigo:
type: string
description: ''
example: 6
enum:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 99
required:
- monto
- codigo
taxes:
type: array
description: 'Lista de impuestos de la línea.'
example:
- []
items:
type: object
nullable: true
properties:
codigo:
type: string
description: 'Código impuesto. `01`=IVA.'
example: '01'
codigoTarifa:
type: string
description: 'Tarifa IVA. `08`=13%, `10`=Exenta.'
example: '08'
nullable: true
tarifa:
type: string
description: 'Porcentaje de tarifa.'
example: '13.00'
nullable: true
monto:
type: string
description: 'Monto del impuesto.'
example: '1300.00000'
monto_exportacion:
type: number
description: validation.min.
example: 50
nullable: true
exoneracion:
type: object
description: ''
example: null
properties:
tipoDocumento:
type: string
description: 'This field is required when line_items.*.taxes.*.exoneracion is present. validation.max.'
example: kc
numeroDocumento:
type: string
description: 'This field is required when line_items.*.taxes.*.exoneracion is present. validation.max.'
example: m
nombreInstitucion:
type: string
description: 'This field is required when line_items.*.taxes.*.exoneracion is present. validation.max.'
example: 'y'
fechaEmision:
type: string
description: 'This field is required when line_items.*.taxes.*.exoneracion is present. validation.date.'
example: '2026-04-29T10:12:07'
tarifaExonerada:
type: number
description: 'This field is required when line_items.*.taxes.*.exoneracion is present. validation.min.'
example: 72
montoExoneracion:
type: number
description: 'This field is required when line_items.*.taxes.*.exoneracion is present. validation.min.'
example: 61
nullable: true
required:
- codigo
- codigoTarifa
- tarifa
- monto
impuesto_neto:
type: string
description: 'Monto neto del impuesto (obligatorio si hay taxes).'
example: '1300.00000'
nullable: true
total_line_amount:
type: string
description: 'Total de la línea incluyendo impuesto.'
example: '11300.00000'
medicine_registration:
type: string
description: validation.max.
example: p
nullable: true
pharma_form_code:
type: string
description: 'Must match the regex /^\d{3}$/.'
example: '564'
nullable: true
commercial_codes:
type: array
description: validation.max.
example: null
items:
type: object
nullable: true
properties:
tipo:
type: string
description: ''
example: 2
enum:
- 1
- 2
- 3
- 4
- 99
codigo:
type: string
description: validation.max.
example: yvdljnikhwaykcmy
required:
- tipo
- codigo
vin_numbers:
type: array
description: validation.max.
example:
- u
items:
type: string
required:
- line_number
- cabys_code
- quantity
- unit_of_measure
- detail
- unit_price
- total_amount
- sub_total
- total_line_amount
references:
type: array
description: 'Referencias a otros comprobantes. Obligatorio para ND/NC y REP.'
example:
- []
items:
type: object
properties:
doc_type:
type: string
description: 'Tipo de documento referenciado. `01`=FE, `03`=NC, etc.'
example: '01'
number:
type: string
description: 'Clave de 50 dígitos del comprobante referenciado.'
example: 50613032600206270652001000010100000000001XXXXXXXX
issued_at:
type: string
description: 'Fecha de emisión del comprobante referenciado.'
example: '2026-03-01T10:00:00-06:00'
code:
type: string
description: 'Código de referencia. `01`=Anula, `04`=Referencia, `06`=Devolución, `10`=ND financiera.'
example: '01'
reason:
type: string
description: 'Razón de la referencia (máx 180 chars).'
example: 'Anulación por error en monto'
nullable: true
required:
- doc_type
- number
- issued_at
- code
- reason
payment_methods:
type: array
description: 'Lista de medios de pago.'
example:
- []
items:
type: object
properties:
tipo:
type: string
description: 'Código medio pago. `01`=Efectivo, `02`=Tarjeta, `03`=Cheque, `04`=Transferencia.'
example: '01'
monto:
type: number
description: 'Monto pagado con este medio. Obligatorio cuando hay 2+ medios de pago. validation.min.'
example: null
nullable: true
required:
- tipo
other_charges:
type: array
description: ''
example:
- []
items:
type: object
properties:
tipo:
type: string
description: 'Tipo de cargo. `01`=Contribución parafiscal, `04`=Cobro terceros, `06`=Garantía.'
example: '06'
enum:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 99
description:
type: string
description: validation.max.
example: 'Quidem nostrum qui commodi incidunt iure odit.'
nullable: true
percentage:
type: number
description: 'validation.min validation.max.'
example: 10
nullable: true
amount:
type: number
description: 'Monto del cargo (Decimal 18,5). validation.min.'
example: 5000.0
required:
- tipo
- amount
required:
- voucher_type
- situation
- issued_at
- sale_condition
- line_items
- payment_methods
- other_charges
examples:
factura_electronica_fe:
summary: '01 — Factura Electrónica (FE)'
description: 'Factura estándar con 3 líneas de servicio gravadas al 13% IVA. Pago por transferencia en USD.'
value:
voucher_type: '01'
situation: '1'
issuer_activity_code: '6201.0'
sale_condition: '01'
currency_code: USD
exchange_rate: 1.00000
payment_methods:
- tipo: '04'
receiver:
id_type: '02'
id_number: '3101704304'
name: Hotel Las Palmas S.A.
emails:
- recepcion@hotellaspalmas.cr
line_items:
-
line_number: 1
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Integración API facturación electrónica
unit_price: 450.00
total_amount: 450.00
sub_total: 450.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 58.50
impuesto_neto: 58.50
total_line_amount: 508.50
-
line_number: 2
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Desarrollo chatbot con IA
unit_price: 2000.00
total_amount: 2000.00
sub_total: 2000.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 260.00
impuesto_neto: 260.00
total_line_amount: 2260.00
-
line_number: 3
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Implementación CRM a la medida
unit_price: 8000.00
total_amount: 8000.00
sub_total: 8000.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 1040.00
impuesto_neto: 1040.00
total_line_amount: 9040.00
nota_debito_nd:
summary: '02 — Nota de Débito (ND)'
description: 'Nota de débito que referencia una factura original. Requiere references obligatorio.'
value:
voucher_type: '02'
situation: '1'
issuer_activity_code: '6201.0'
sale_condition: '01'
currency_code: CRC
exchange_rate: 1.00000
payment_methods:
- tipo: '04'
receiver:
id_type: '02'
id_number: '3101704304'
name: Hotel Las Palmas S.A.
emails:
- recepcion@hotellaspalmas.cr
line_items:
-
line_number: 1
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Cargo adicional por soporte extendido
unit_price: 25000.00
total_amount: 25000.00
sub_total: 25000.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 3250.00
impuesto_neto: 3250.00
total_line_amount: 28250.00
references:
-
doc_type: '01'
number: '50623042600310100001000100001010000000042112345678'
issued_at: '2026-04-20T10:00:00-06:00'
code: '04'
reason: Cargo adicional al servicio de la factura original
nota_credito_nc:
summary: '03 — Nota de Crédito (NC)'
description: 'Nota de crédito que anula una factura. Requiere references con code=01 (Anula).'
value:
voucher_type: '03'
situation: '1'
issuer_activity_code: '6201.0'
sale_condition: '01'
currency_code: CRC
exchange_rate: 1.00000
payment_methods:
- tipo: '04'
receiver:
id_type: '02'
id_number: '3101704304'
name: Hotel Las Palmas S.A.
emails:
- recepcion@hotellaspalmas.cr
line_items:
-
line_number: 1
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Anulación de servicio facturado
unit_price: 50000.00
total_amount: 50000.00
sub_total: 50000.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 6500.00
impuesto_neto: 6500.00
total_line_amount: 56500.00
references:
-
doc_type: '01'
number: '50623042600310100001000100001010000000042112345678'
issued_at: '2026-04-20T10:00:00-06:00'
code: '01'
reason: Anulación completa de factura por error en datos
tiquete_electronico_te:
summary: '04 — Tiquete Electrónico (TE)'
description: 'Tiquete para consumidor final. Receptor opcional. Sin receiver_activity_code.'
value:
voucher_type: '04'
situation: '1'
issuer_activity_code: '4711.0'
sale_condition: '01'
currency_code: CRC
exchange_rate: 1.00000
payment_methods:
- tipo: '01'
line_items:
-
line_number: 1
cabys_code: '2410100000000'
quantity: 2
unit_of_measure: Unid
detail: Producto de consumo general
unit_price: 5000.00
total_amount: 10000.00
sub_total: 10000.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 1300.00
impuesto_neto: 1300.00
total_line_amount: 11300.00
factura_compra_fec:
summary: '08 — Factura de Compra (FEC)'
description: 'Factura de compra. Requiere receiver_activity_code y references obligatorio.'
value:
voucher_type: '08'
situation: '1'
issuer_activity_code: '6201.0'
receiver_activity_code: '4711.0'
sale_condition: '01'
currency_code: CRC
exchange_rate: 1.00000
payment_methods:
- tipo: '04'
receiver:
id_type: '01'
id_number: '112340567'
name: Juan Pérez Solano
emails:
- juan@ejemplo.cr
line_items:
-
line_number: 1
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Servicio de diseño gráfico freelance
unit_price: 150000.00
total_amount: 150000.00
sub_total: 150000.00
taxes:
- codigo: '01'
codigoTarifa: '08'
tarifa: 13
monto: 19500.00
impuesto_neto: 19500.00
total_line_amount: 169500.00
references:
-
doc_type: '99'
number: CONT-2026-0042
issued_at: '2026-04-01T08:00:00-06:00'
code: '04'
reason: Contrato de servicios profesionales abril 2026
factura_exportacion_fee:
summary: '09 — Factura de Exportación (FEE)'
description: 'Factura de exportación. payment_methods opcional. Tarifa 01 prohibida — usar 10 (Exenta).'
value:
voucher_type: '09'
situation: '1'
issuer_activity_code: '6201.0'
sale_condition: '01'
currency_code: USD
exchange_rate: 1.00000
receiver:
id_type: '05'
id_number: 'EXT-99887766'
name: Global Tech Solutions Inc.
emails:
- billing@globaltech.com
line_items:
-
line_number: 1
cabys_code: '8531100000100'
quantity: 1
unit_of_measure: Sp
detail: Desarrollo de software a la medida — exportación
unit_price: 5000.00
total_amount: 5000.00
sub_total: 5000.00
taxes:
- codigo: '01'
codigoTarifa: '10'
tarifa: 0
monto: 0
impuesto_neto: 0
total_line_amount: 5000.00
recibo_pago_rep:
summary: '10 — Recibo Electrónico de Pago (REP)'
description: 'REP para documentar pago recibido. sale_condition solo 09/11. Sin CABYS. references obligatorio.'
value:
voucher_type: '10'
situation: '1'
sale_condition: '09'
currency_code: CRC
exchange_rate: 1.00000
payment_methods:
- tipo: '04'
receiver:
id_type: '02'
id_number: '3101704304'
name: Hotel Las Palmas S.A.
emails:
- recepcion@hotellaspalmas.cr
line_items:
-
line_number: 1
detail: Pago de factura FE-001-00001-01-0000000042
total_amount: 56500.00
sub_total: 56500.00
total_line_amount: 56500.00
references:
-
doc_type: '01'
number: '50623042600310100001000100001010000000042112345678'
issued_at: '2026-04-20T10:00:00-06:00'
code: '04'
reason: Pago total de factura electrónica
/vouchers/pending-contingency:
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Listar comprobantes de contingencia pendientes'
operationId: listarComprobantesDeContingenciaPendientes
description: "Devuelve los comprobantes emitidos en **situación de contingencia**\n(caída del sistema) o **sin internet**, que aún no fueron aceptados\nni rechazados por Hacienda.\n\nLa normativa establece un plazo máximo de **2 días hábiles** para\nremitir los comprobantes de contingencia. Este endpoint le muestra\ncuántos están pendientes y cuántos ya superaron el plazo, para\nayudarle a cumplir con el monitoreo requerido antes de incurrir en\ninfracciones tributarias.\n\n**Estados incluidos:** `draft`, `pending`, `sent`, `error`.\n\nLos comprobantes ya resueltos (`accepted`, `rejected`, `cancelled`)\nno aparecen aquí."
parameters:
-
in: query
name: situation
description: 'Filtrar por situación. `2`=Contingencia, `3`=Sin internet. Sin valor muestra ambas.'
example: '3'
required: false
schema:
type: string
description: 'Filtrar por situación. `2`=Contingencia, `3`=Sin internet. Sin valor muestra ambas.'
example: '3'
-
in: query
name: expired_only
description: 'Mostrar solo los que superaron el plazo de 2 días hábiles. Default: `false`.'
example: true
required: false
schema:
type: boolean
description: 'Mostrar solo los que superaron el plazo de 2 días hábiles. Default: `false`.'
example: true
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Con comprobantes pendientes'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
items:
type: array
example:
-
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'
items:
type: object
properties:
voucher_key:
type: string
example: 50617042026...
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
consecutive_number:
type: string
example: '00100001010000000001'
status:
type: string
example: error
situation:
type: string
example: '3'
situation_label:
type: string
example: 'Sin Internet'
issued_at:
type: string
example: '2026-04-10T14:30:00-06:00'
deadline:
type: string
example: '2026-04-14'
elapsed_business_days:
type: integer
example: 3
is_expired:
type: boolean
example: true
receiver:
type: object
properties:
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101000001'
name:
type: string
example: 'Empresa S.A.'
currency_code:
type: string
example: CRC
total_comprobante:
type: string
example: '56500.00000'
total:
type: integer
example: 5
expired_count:
type: integer
example: 2
message:
type: string
example: '5 comprobante(s) de contingencia sin resolver. 2 superan el plazo de 2 días hábiles.'
errors:
type: string
example: null
nullable: true
-
description: 'Sin pendientes'
type: object
example:
success: true
data:
items: []
total: 0
expired_count: 0
message: 'Sin comprobantes de contingencia pendientes.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
items:
type: array
example: []
total:
type: integer
example: 0
expired_count:
type: integer
example: 0
message:
type: string
example: 'Sin comprobantes de contingencia pendientes.'
errors:
type: string
example: null
nullable: true
tags:
- 'Comprobantes Electrónicos'
'/vouchers/{key}':
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Consultar un comprobante por clave'
operationId: consultarUnComprobantePorClave
description: "Devuelve el detalle completo de un comprobante identificado por su\nclave de 50 dígitos: datos del emisor y receptor, líneas, totales,\nestado actual y respuesta de Hacienda (si ya fue procesado).\n\nLa clave debe tener exactamente 50 dígitos numéricos — formatos\ninválidos retornan 404 sin procesar la consulta."
parameters: []
responses:
200:
description: 'Comprobante encontrado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
voucher_key:
type: string
example: '50619032600310199999900100001010000000001112345678'
description: '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:
type: string
example: '01'
description: 'Código del tipo de comprobante: `01`=FE, `02`=ND, `03`=NC, `04`=TE, `08`=FEC, `09`=FEE, `10`=REP.'
voucher_type_label:
type: string
example: 'Factura Electrónica'
description: 'Nombre legible del tipo de comprobante en español.'
consecutive_number:
type: string
example: '00100001010000000001'
description: 'Número consecutivo interno de 20 dígitos asignado al emitir.'
issued_at:
type: string
example: '2026-04-13T10:00:00-06:00'
description: 'Fecha y hora de emisión en formato ISO 8601 con offset CR (`-06:00`).'
situation:
type: string
example: '1'
description: 'Situación del comprobante: `1`=Normal, `2`=Contingencia, `3`=Sin internet.'
situation_label:
type: string
example: Normal
description: 'Nombre legible de la situación en español.'
sale_condition:
type: string
example: '01'
description: '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:
type: string
example: Contado
description: 'Nombre legible de la condición de venta.'
sale_condition_other:
type: string
example: null
description: 'Descripción personalizada cuando `sale_condition=99`. Null en cualquier otro caso.'
credit_term:
type: string
example: null
description: 'Plazo del crédito en días. Presente cuando `sale_condition` es `02`, `08` o `10`.'
receiver:
type: object
properties:
id_type:
type: string
example: '02'
description: 'Tipo de identificación del receptor: `01`–`06`.'
id_number:
type: string
example: '3101000001'
description: 'Número de identificación del receptor.'
name:
type: string
example: 'EMPRESA EJEMPLO S.A.'
description: 'Nombre o razón social del receptor.'
commercial_name:
type: string
example: 'Ejemplo Comercial'
description: 'Nombre comercial del receptor. Puede ser `null`.'
emails:
type: array
example:
- facturacion@ejemplo.cr
description: 'Correos electrónicos del receptor usados para el envío transaccional.'
items:
type: string
description: 'Datos del receptor del comprobante. `null` cuando el tipo no requiere receptor (TE sin receptor, FEE sin receptor).'
currency_code:
type: string
example: CRC
description: 'Código de moneda ISO 4217 (`CRC`, `USD`, `EUR`, etc.).'
exchange_rate:
type: string
example: '1.00000'
description: 'Tipo de cambio respecto al CRC. `1.00000` si la moneda es CRC.'
totals:
type: object
properties:
serv_gravados:
type: string
example: '10000.00000'
description: 'Total servicios gravados con IVA.'
serv_exentos:
type: string
example: '0.00000'
description: 'Total servicios exentos.'
serv_exonerado:
type: string
example: '0.00000'
description: 'Total servicios exonerados.'
serv_no_sujeto:
type: string
example: '0.00000'
description: 'Total servicios no sujetos a IVA.'
merc_gravadas:
type: string
example: '0.00000'
description: 'Total mercancías gravadas con IVA.'
merc_exentas:
type: string
example: '0.00000'
description: 'Total mercancías exentas.'
merc_exonerada:
type: string
example: '0.00000'
description: 'Total mercancías exoneradas.'
merc_no_sujeta:
type: string
example: '0.00000'
description: 'Total mercancías no sujetas a IVA.'
total_gravado:
type: string
example: '10000.00000'
description: 'Suma de servicios + mercancías gravados.'
total_exento:
type: string
example: '0.00000'
description: 'Suma de servicios + mercancías exentos.'
total_exonerado:
type: string
example: '0.00000'
description: 'Suma de servicios + mercancías exonerados.'
total_no_sujeto:
type: string
example: '0.00000'
description: 'Suma de servicios + mercancías no sujetos.'
total_venta:
type: string
example: '10000.00000'
description: 'TotalVenta = suma de MontoTotalLinea de todas las líneas.'
total_descuentos:
type: string
example: '0.00000'
description: 'Suma de todos los descuentos aplicados.'
total_venta_neta:
type: string
example: '10000.00000'
description: 'TotalVenta − TotalDescuentos.'
total_impuesto:
type: string
example: '1300.00000'
description: 'Suma de todos los impuestos (IVA, selectivo, etc.).'
total_imp_asum_emisor_fab:
type: string
example: '0.00000'
description: 'Impuesto asumido por el emisor/fabricante. Normalmente `0.00000`.'
total_iva_devuelto:
type: string
example: '0.00000'
description: 'IVA devuelto (aplicable en devoluciones). Normalmente `0.00000`.'
total_otros_cargos:
type: string
example: '0.00000'
description: 'Suma de otros cargos adicionales al comprobante.'
total_comprobante:
type: string
example: '11300.00000'
description: 'Monto final: TotalVentaNeta + TotalImpuesto − TotalIVADevuelto + TotalOtrosCargos.'
description: 'Totales del ResumenFactura desglosados (Decimal 18,5).'
payment_methods:
type: array
example:
-
tipo: '01'
description: 'Medios de pago del comprobante. Cada objeto tiene `tipo`: `01`=Efectivo, `02`=Tarjeta, `03`=Cheque, `04`=Transferencia, `99`=Otros.'
items:
type: object
properties:
tipo:
type: string
example: '01'
status:
type: string
example: accepted
description: 'Estado actual del comprobante: `draft`, `pending`, `sent`, `accepted`, `rejected`, `error`, `cancelled`.'
hacienda:
type: object
properties:
status:
type: string
example: aceptado
description: 'Estado según Hacienda: `aceptado`, `rechazado`, o `null` si aún no procesado.'
message:
type: string
example: null
description: 'Detalle del mensaje de Hacienda. Contiene la descripción del error si fue rechazado. `null` si fue aceptado.'
sent_at:
type: string
example: '2026-04-13T10:01:00-06:00'
description: 'Fecha/hora en que se envió a Hacienda (ISO 8601). `null` si no enviado aún.'
processed_at:
type: string
example: '2026-04-13T10:02:00-06:00'
description: 'Fecha/hora en que Hacienda procesó el comprobante (ISO 8601). `null` si pendiente.'
description: 'Información de la respuesta de Hacienda (MensajeHacienda).'
environment:
type: string
example: production
description: 'Ambiente del comprobante: `production` (valor fiscal) o `sandbox` (pruebas, sin valor fiscal).'
is_xml_available:
type: boolean
example: true
description: '`true` si los XML (firmado + respuesta) están disponibles para descarga. `false` si la retención expiró y fueron purgados.'
line_items_count:
type: integer
example: 1
description: 'Cantidad de líneas de detalle (LineaDetalle) del comprobante.'
references_count:
type: integer
example: 0
description: 'Cantidad de referencias a otros comprobantes (InformacionReferencia).'
created_at:
type: string
example: '2026-04-13T10:00:00-06:00'
description: 'Fecha de creación del registro (ISO 8601).'
updated_at:
type: string
example: '2026-04-13T10:02:00-06:00'
description: 'Fecha de última actualización del registro (ISO 8601).'
message:
type: string
example: 'Comprobante obtenido correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'Comprobante no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El recurso solicitado no existe.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El recurso solicitado no existe.'
errors:
type: string
example: null
nullable: true
tags:
- 'Comprobantes Electrónicos'
parameters:
-
in: path
name: key
description: 'Clave de 50 dígitos del comprobante.'
example: '50619032600310199999900100001010000000001112345678'
required: true
schema:
type: string
'/vouchers/{key}/cancel':
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
post:
summary: 'Anular un comprobante (emite una Nota de Crédito)'
operationId: anularUnComprobanteemiteUnaNotaDeCrdito
description: "En Costa Rica, la anulación de un comprobante electrónico NO es una\noperación directa. El mecanismo normativo es emitir una **Nota de\nCrédito** que referencia al comprobante original con el código\n`01` (Anula documento).\n\nEste endpoint automatiza ese proceso:\n\n1. Lee el comprobante original por su clave.\n2. Construye automáticamente el payload de la NC (mismas líneas,\n mismo receptor) con la referencia correspondiente.\n3. La emite por el mismo pipeline que `POST /vouchers` (valida XSD,\n firma, envía a Hacienda).\n4. Retorna la NC generada con **HTTP 202**.\n\n**Restricciones:**\n\n- Solo comprobantes en estado `accepted` pueden anularse.\n- Solo FE (`01`) y TE (`04`) son anulables con NC.\n- Un comprobante ya cancelado no puede re-cancelarse."
parameters: []
responses:
202:
description: 'NC de anulación generada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
voucher_key:
type: string
example: '50621042600310199999900100001010000000002212345679'
voucher_type:
type: string
example: '03'
voucher_type_label:
type: string
example: 'Nota de Crédito'
consecutive_number:
type: string
example: '00100001030000000001'
status:
type: string
example: pending
environment:
type: string
example: production
created_at:
type: string
example: '2026-04-21T11:00:00-06:00'
message:
type: string
example: 'NC de anulación generada. Encolada para envío a Hacienda.'
errors:
type: string
example: null
nullable: true
409:
description: 'El estado no permite anulación'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Solo comprobantes aceptados por Hacienda pueden anularse.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Solo comprobantes aceptados por Hacienda pueden anularse.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Error de validación XSD de la NC'
type: object
example:
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: ...
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Validación XSD fallida para Nota de Crédito (03) — ...'
errors:
type: object
properties:
voucher_type:
type: array
example:
- '03'
items:
type: string
xsd:
type: array
example:
-
linea: 45
columna: 12
mensaje: ...
items:
type: object
properties:
linea:
type: integer
example: 45
columna:
type: integer
example: 12
mensaje:
type: string
example: ...
-
description: 'Error de firma de la NC'
type: object
example:
success: false
data: null
message: 'No se pudo firmar el comprobante. El certificado digital está vencido.'
errors:
signature:
- ...
code:
- '2002'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No se pudo firmar el comprobante. El certificado digital está vencido.'
errors:
type: object
properties:
signature:
type: array
example:
- ...
items:
type: string
code:
type: array
example:
- '2002'
items:
type: string
tags:
- 'Comprobantes Electrónicos'
parameters:
-
in: path
name: key
description: 'Clave de 50 dígitos del comprobante a anular.'
example: '50619032600310199999900100001010000000001112345678'
required: true
schema:
type: string
'/vouchers/{key}/xml':
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Descargar el XML firmado del comprobante'
operationId: descargarElXMLFirmadoDelComprobante
description: "Devuelve el XML completo con firma digital XAdES-EPES, tal como\nfue enviado a la API de recepción de Hacienda. Este XML es el\n**documento fiscal con validez legal**.\n\n> **Obligación de archivo:** la normativa exige conservar este XML\n> por **5 años**. Descárguelo y archívelo de su lado dentro del\n> período de retención de la plataforma (3 meses por defecto, hasta\n> 5 años con el add-on de retención extendida).\n\n**Disponibilidad:**\n\n- Solo comprobantes que ya pasaron la firma digital tienen XML.\n- Comprobantes en `draft` aún no tienen XML → **404**.\n- Comprobantes cuyo XML fue purgado (retención expirada) → **410 Gone**.\n\nDespués de la retención, los metadatos del comprobante y el PDF\nsiguen disponibles — solo el XML deja de estar accesible."
parameters: []
responses:
200:
description: 'XML disponible'
content:
text/plain:
schema:
type: string
example: '[archivo binario application/xml]'
404:
description: 'XML aún no generado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: "El XML firmado aún no está disponible. El comprobante está en estado 'draft'."
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "El XML firmado aún no está disponible. El comprobante está en estado 'draft'."
errors:
type: string
example: null
nullable: true
410:
description: 'XML purgado — retención expirada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'XML purgado. Retención expirada el 2026-06-15.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'XML purgado. Retención expirada el 2026-06-15.'
errors:
type: string
example: null
nullable: true
tags:
- 'Comprobantes Electrónicos'
parameters:
-
in: path
name: key
description: 'Clave de 50 dígitos.'
example: '50619032600310199999900100001010000000001112345678'
required: true
schema:
type: string
'/vouchers/{key}/xml-response':
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Descargar el XML de respuesta de Hacienda (MensajeHacienda)'
operationId: descargarElXMLDeRespuestaDeHaciendaMensajeHacienda
description: "Devuelve el **MensajeHacienda XML** firmado por Hacienda que\nrepresenta el resultado oficial del procesamiento del comprobante.\nIncluye:\n\n- `Mensaje`: `1`=Aceptado, `3`=Rechazado.\n- `DetalleMensaje`: descripción del error (si fue rechazado).\n- `MontoTotalImpuesto`: monto de impuesto validado por Hacienda.\n\nEste XML es evidencia oficial de la validación ante Hacienda. Su\nobligación legal de archivo es de **5 años**, igual que el XML firmado.\n\n**Disponibilidad:**\n\n- Solo comprobantes que Hacienda ya procesó (estado `accepted` o\n `rejected`) tienen este XML.\n- Comprobantes en tránsito (`pending`, `sent`) → **404**.\n- Si la retención expiró → **410 Gone**."
parameters: []
responses:
200:
description: 'XML respuesta disponible'
content:
text/plain:
schema:
type: string
example: '[archivo binario application/xml]'
404:
description: 'Respuesta aún no disponible'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: "La respuesta de Hacienda aún no está disponible. El comprobante está en estado 'sent'."
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "La respuesta de Hacienda aún no está disponible. El comprobante está en estado 'sent'."
errors:
type: string
example: null
nullable: true
410:
description: 'XML purgado — retención expirada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'XML purgado. Retención expirada el 2026-06-15.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'XML purgado. Retención expirada el 2026-06-15.'
errors:
type: string
example: null
nullable: true
tags:
- 'Comprobantes Electrónicos'
parameters:
-
in: path
name: key
description: 'Clave de 50 dígitos.'
example: '50619032600310199999900100001010000000001112345678'
required: true
schema:
type: string
'/vouchers/{key}/pdf':
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Descargar la representación gráfica (PDF) del comprobante'
operationId: descargarLaRepresentacinGrficaPDFDelComprobante
description: "Genera y devuelve el PDF del comprobante usando la plantilla por\ndefecto del contribuyente (o una específica si se indica vía\n`template_id`). El PDF incluye obligatoriamente un **código QR con\nla clave de 50 dígitos** en la esquina inferior derecha, con\ntamaño mínimo de 2.5 cm conforme a la normativa.\n\n**Disponibilidad:**\n\n- Solo comprobantes que ya pasaron la firma (estado ≠ `draft`).\n- El contribuyente debe tener al menos una plantilla PDF activa\n marcada como default.\n- El PDF **permanece disponible** incluso si el XML fue purgado —\n los metadatos y totales nunca se eliminan.\n\nPara gestionar plantillas PDF personalizadas, vea el grupo\n**Plantillas PDF** de esta documentación."
parameters:
-
in: query
name: template_id
description: 'ID de una plantilla específica. Si no se envía, usa la plantilla default del contribuyente.'
example: 16
required: false
schema:
type: integer
description: 'ID de una plantilla específica. Si no se envía, usa la plantilla default del contribuyente.'
example: 16
responses:
200:
description: 'PDF generado'
content:
text/plain:
schema:
type: string
example: '[archivo binario application/pdf]'
404:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Comprobante en draft'
type: object
example:
success: false
data: null
message: "El PDF no está disponible. El comprobante está en estado 'draft'."
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "El PDF no está disponible. El comprobante está en estado 'draft'."
errors:
type: string
example: null
nullable: true
-
description: 'Sin plantilla PDF default activa'
type: object
example:
success: false
data: null
message: 'El contribuyente no tiene plantilla PDF default activa.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El contribuyente no tiene plantilla PDF default activa.'
errors:
type: string
example: null
nullable: true
tags:
- 'Comprobantes Electrónicos'
parameters:
-
in: path
name: key
description: 'Clave de 50 dígitos.'
example: '50619032600310199999900100001010000000001112345678'
required: true
schema:
type: string
/profile/usage:
get:
summary: 'Estadísticas de uso mensual.'
operationId: estadsticasDeUsoMensual
description: "Retorna los conteos de comprobantes emitidos y emails enviados\ndurante el período mensual en curso, junto con los límites de su plan.\n\nSolo cuenta comprobantes de producción aceptados por Hacienda.\nLos comprobantes emitidos en sandbox no consumen el límite del plan.\n\nSi es integrador, el conteo agrega automáticamente los comprobantes\npropios y los de todos sus clientes gestionados, permitiendo\nmonitorear el consumo total del plan en una sola consulta."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Contribuyente normal'
type: object
example:
success: true
data:
vouchers_this_month: 87
vouchers_limit: 500
vouchers_remaining: 413
vouchers_percentage: 17.4
emails_today: 12
emails_limit: 75
period_start: '2026-04-01'
period_end: '2026-04-30'
message: 'Estadísticas de uso mensual.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
vouchers_this_month:
type: integer
example: 87
description: 'Comprobantes de producción aceptados emitidos en el mes actual. Si es integrador, suma propios + todos los clientes gestionados.'
vouchers_limit:
type: integer
example: 500
description: 'Límite mensual del plan contratado.'
vouchers_remaining:
type: integer
example: 413
description: 'Comprobantes restantes antes de alcanzar el límite: `max(0, limit - this_month)`.'
vouchers_percentage:
type: number
example: 17.4
description: 'Porcentaje de uso del cupo mensual (0-100, 2 decimales).'
emails_today:
type: integer
example: 12
description: 'Emails transaccionales enviados hoy. `0` si no hay tracking implementado aún.'
emails_limit:
type: integer
example: 75
description: 'Límite diario de emails del plan.'
period_start:
type: string
example: '2026-04-01'
description: 'Primer día del mes actual (YYYY-MM-DD). Zona horaria: America/Costa_Rica.'
period_end:
type: string
example: '2026-04-30'
description: 'Último día del mes actual (YYYY-MM-DD).'
message:
type: string
example: 'Estadísticas de uso mensual.'
errors:
type: string
example: null
nullable: true
-
description: 'Integrador (consumo agregado con todos sus clientes)'
type: object
example:
success: true
data:
vouchers_this_month: 12450
vouchers_limit: 50000
vouchers_remaining: 37550
vouchers_percentage: 24.9
emails_today: 0
emails_limit: 5000
period_start: '2026-04-01'
period_end: '2026-04-30'
message: 'Estadísticas de uso mensual.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
vouchers_this_month:
type: integer
example: 12450
description: 'Comprobantes de producción aceptados emitidos en el mes actual. Si es integrador, suma propios + todos los clientes gestionados.'
vouchers_limit:
type: integer
example: 50000
description: 'Límite mensual del plan contratado.'
vouchers_remaining:
type: integer
example: 37550
description: 'Comprobantes restantes antes de alcanzar el límite: `max(0, limit - this_month)`.'
vouchers_percentage:
type: number
example: 24.9
description: 'Porcentaje de uso del cupo mensual (0-100, 2 decimales).'
emails_today:
type: integer
example: 0
description: 'Emails transaccionales enviados hoy. `0` si no hay tracking implementado aún.'
emails_limit:
type: integer
example: 5000
description: 'Límite diario de emails del plan.'
period_start:
type: string
example: '2026-04-01'
description: 'Primer día del mes actual (YYYY-MM-DD). Zona horaria: America/Costa_Rica.'
period_end:
type: string
example: '2026-04-30'
description: 'Último día del mes actual (YYYY-MM-DD).'
message:
type: string
example: 'Estadísticas de uso mensual.'
errors:
type: string
example: null
nullable: true
tags:
- Perfil
/profile:
get:
summary: 'Consultar perfil completo del contribuyente autenticado.'
operationId: consultarPerfilCompletoDelContribuyenteAutenticado
description: "Retorna todos los datos del contribuyente incluyendo el plan activo\ncon sus limites y features, las actividades economicas registradas,\nla ubicacion y la configuracion de contacto."
parameters: []
responses:
200:
description: 'Perfil completo'
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 019d867d-0241-7288-8ece-fd64da75616d
legal_name: 'Empresa Ejemplo S.A.'
trade_name: 'Ejemplo Shop'
id_type: '02'
id_type_label: 'Cédula Jurídica'
id_number: '3101000000'
emails:
- facturacion@empresa.cr
economic_activities:
- '6201.0'
location:
province: 1
canton: 1
district: 1
neighborhood: 'San Pedro'
address: '200 metros norte del parque central'
phone:
country_code: 506
number: '22001234'
is_active: true
production_enabled: false
hacienda_environment: sandbox
plan_id: 019d0001-0000-0000-0000-000000000001
is_managed: false
is_integrator: false
plan:
id: 019d0001-0000-0000-0000-000000000001
code: pyme
name: Pyme
description: 'Automatice su facturación con hasta 500 comprobantes/mes.'
monthly_price_usd: null
annual_price_usd: 96.0
billing_mode: annual_only
annual_discount_percent: 0
vouchers_per_month: 500
overage_price_per_voucher: 0.03
max_users: 3
max_api_tokens: 2
max_clients: 300
max_items: 750
max_pdf_templates: 1
max_daily_emails: 75
max_webhook_endpoints: 1
max_branch_terminals: 2
max_managed_contributors: 1
api_rate_limit_per_minute: 20
retention_months: 3
extended_retention_eligible: true
pdf_branding: platform
api_write_enabled: true
api_readonly_enabled: false
sandbox_access: true
webhooks_enabled: false
multi_contributor: false
receiver_module: true
bulk_emission: false
sla_support: false
created_at: '2026-04-01T10:00:00-06:00'
updated_at: '2026-04-13T14:30:00-06:00'
message: 'Perfil del contribuyente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
description: 'UUID del contribuyente.'
legal_name:
type: string
example: 'Empresa Ejemplo S.A.'
description: 'Razón social o nombre legal. Mapea a `EmisorType/Nombre` del XSD (min 5, max 100).'
trade_name:
type: string
example: 'Ejemplo Shop'
description: 'Nombre comercial. Mapea a `EmisorType/NombreComercial` del XSD. `null` si no se configuró.'
id_type:
type: string
example: '02'
description: 'Tipo de identificación: `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE. Inmutable.'
id_type_label:
type: string
example: 'Cédula Jurídica'
description: 'Nombre legible del tipo de identificación.'
id_number:
type: string
example: '3101000000'
description: 'Número de identificación del contribuyente.'
emails:
type: array
example:
- facturacion@empresa.cr
description: 'Correos electrónicos del emisor (máximo 4). El primero mapea a `CorreoElectronico` del XSD.'
items:
type: string
economic_activities:
type: array
example:
- '6201.0'
description: 'Códigos CIIU en formato Hacienda `XXXX.X`. Mapean a `CodigoActividadEmisor` del XSD.'
items:
type: string
location:
type: object
properties:
province:
type: integer
example: 1
description: 'Código de provincia (1-7).'
canton:
type: integer
example: 1
description: 'Código de cantón.'
district:
type: integer
example: 1
description: 'Código de distrito.'
description: 'Ubicación del emisor (UbicacionType del XSD). `null` si no tiene ubicación configurada.'
neighborhood:
type: string
example: 'San Pedro'
description: 'Barrio del emisor. `null` si no se configuró.'
address:
type: string
example: '200 metros norte del parque central'
description: 'Señas exactas del emisor. Mapea a `OtrasSenas` del XSD (max 250). `null` si no se configuró.'
phone:
type: object
properties:
country_code:
type: integer
example: 506
description: 'Código de país (1-3 dígitos). Default: 506.'
number:
type: string
example: '22001234'
description: 'Número de teléfono.'
description: 'Teléfono del emisor (TelefonoType del XSD). `null` si no tiene teléfono configurado.'
is_active:
type: boolean
example: true
description: '`true` si la cuenta está activa. Solo un admin de plataforma puede suspenderla.'
production_enabled:
type: boolean
example: false
description: '`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:
type: string
example: sandbox
description: '**@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:
type: string
example: 019d0001-0000-0000-0000-000000000001
description: 'UUID del plan contratado.'
is_managed:
type: boolean
example: false
description: '`true` si el contribuyente es gestionado por al menos un integrador.'
is_integrator:
type: boolean
example: false
description: '`true` si el contribuyente tiene plan Integrador y gestiona otros clientes.'
plan:
type: object
properties:
id:
type: string
example: 019d0001-0000-0000-0000-000000000001
code:
type: string
example: pyme
description: 'Código del plan: `free`, `starter`, `pyme`, `professional`, `business`, `integrator`.'
name:
type: string
example: Pyme
description: 'Nombre comercial del plan.'
description:
type: string
example: 'Automatice su facturación con hasta 500 comprobantes/mes.'
monthly_price_usd:
type: string
example: null
nullable: true
annual_price_usd:
type: number
example: 96.0
billing_mode:
type: string
example: annual_only
annual_discount_percent:
type: integer
example: 0
vouchers_per_month:
type: integer
example: 500
description: 'Límite mensual de comprobantes.'
overage_price_per_voucher:
type: number
example: 0.03
max_users:
type: integer
example: 3
max_api_tokens:
type: integer
example: 2
max_clients:
type: integer
example: 300
description: 'Máximo de clientes en el catálogo.'
max_items:
type: integer
example: 750
description: 'Máximo de items en el catálogo.'
max_pdf_templates:
type: integer
example: 1
description: 'Máximo de plantillas PDF.'
max_daily_emails:
type: integer
example: 75
description: 'Máximo de emails por día.'
max_webhook_endpoints:
type: integer
example: 1
description: 'Máximo de webhook endpoints.'
max_branch_terminals:
type: integer
example: 2
max_managed_contributors:
type: integer
example: 1
api_rate_limit_per_minute:
type: integer
example: 20
description: 'Requests por minuto permitidos.'
retention_months:
type: integer
example: 3
extended_retention_eligible:
type: boolean
example: true
pdf_branding:
type: string
example: platform
description: 'Nivel de branding: `platform`, `minimal` o `white_label`.'
api_write_enabled:
type: boolean
example: true
api_readonly_enabled:
type: boolean
example: false
sandbox_access:
type: boolean
example: true
description: '`true` si el plan incluye acceso a la API sandbox.'
webhooks_enabled:
type: boolean
example: false
multi_contributor:
type: boolean
example: false
receiver_module:
type: boolean
example: true
bulk_emission:
type: boolean
example: false
sla_support:
type: boolean
example: false
description: 'Detalle completo del plan contratado con todos los límites y features.'
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
description: 'Fecha de creación de la cuenta (ISO 8601).'
updated_at:
type: string
example: '2026-04-13T14:30:00-06:00'
description: 'Fecha de última actualización (ISO 8601).'
message:
type: string
example: 'Perfil del contribuyente.'
errors:
type: string
example: null
nullable: true
tags:
- Perfil
put:
summary: 'Actualizar datos editables del perfil del contribuyente.'
operationId: actualizarDatosEditablesDelPerfilDelContribuyente
description: "Soporta actualizacion parcial: envie unicamente los campos que desea\nmodificar. Los campos no incluidos en el payload conservan su valor actual.\n\n**Campos editables:** `id_number`, `legal_name`, `trade_name`, `emails`,\n`economic_activities`, `phone`, `phone_country_code`, `province`, `canton`,\n`district`, `neighborhood`, `address`.\n\n**Campos inmutables (no se pueden cambiar desde este endpoint):**\n`id_type` (tipo de identificacion), el plan activo, `production_enabled`\n(autorización DGT — gestionada por super_admin tras verificar firma BCCR\ny cumplimiento normativo) y el estado activo/inactivo de la cuenta.\n\nLa configuracion de email transaccional se gestiona desde su propio\nendpoint: `PUT /email-settings`."
parameters: []
responses:
200:
description: 'Perfil actualizado'
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 019d867d-0241-7288-8ece-fd64da75616d
legal_name: 'Empresa Ejemplo S.A. Actualizada'
trade_name: 'Ejemplo Shop Nuevo'
id_type: '02'
id_type_label: 'Cédula Jurídica'
id_number: '3101000000'
emails:
- facturacion@empresa.cr
- contabilidad@empresa.cr
economic_activities:
- '6201.0'
- '4711.0'
location:
province: 1
canton: 1
district: 1
neighborhood: 'San Pedro'
address: '200 metros norte del parque central'
phone:
country_code: 506
number: '22001234'
is_active: true
production_enabled: false
hacienda_environment: sandbox
plan_id: 019d0001-0000-0000-0000-000000000001
is_managed: false
is_integrator: false
plan:
id: 019d0001-0000-0000-0000-000000000001
code: pyme
name: Pyme
vouchers_per_month: 500
max_clients: 300
sandbox_access: true
created_at: '2026-04-01T10:00:00-06:00'
updated_at: '2026-04-16T09:15:00-06:00'
message: 'Perfil actualizado correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'Empresa Ejemplo S.A. Actualizada'
trade_name:
type: string
example: 'Ejemplo Shop Nuevo'
id_type:
type: string
example: '02'
id_type_label:
type: string
example: 'Cédula Jurídica'
id_number:
type: string
example: '3101000000'
emails:
type: array
example:
- facturacion@empresa.cr
- contabilidad@empresa.cr
items:
type: string
economic_activities:
type: array
example:
- '6201.0'
- '4711.0'
items:
type: string
location:
type: object
properties:
province:
type: integer
example: 1
canton:
type: integer
example: 1
district:
type: integer
example: 1
neighborhood:
type: string
example: 'San Pedro'
address:
type: string
example: '200 metros norte del parque central'
phone:
type: object
properties:
country_code:
type: integer
example: 506
number:
type: string
example: '22001234'
is_active:
type: boolean
example: true
production_enabled:
type: boolean
example: false
hacienda_environment:
type: string
example: sandbox
plan_id:
type: string
example: 019d0001-0000-0000-0000-000000000001
is_managed:
type: boolean
example: false
is_integrator:
type: boolean
example: false
plan:
type: object
properties:
id:
type: string
example: 019d0001-0000-0000-0000-000000000001
code:
type: string
example: pyme
name:
type: string
example: Pyme
vouchers_per_month:
type: integer
example: 500
max_clients:
type: integer
example: 300
sandbox_access:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T09:15:00-06:00'
message:
type: string
example: 'Perfil actualizado correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: 'Error de validación'
content:
application/json:
schema:
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
emails.0:
type: array
example:
- 'El campo emails.0 debe ser un correo electrónico válido.'
items:
type: string
economic_activities.0:
type: array
example:
- 'El formato del código de actividad económica debe ser XXXX.X (ejemplo: 6201.0).'
items:
type: string
tags:
- Perfil
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id_number:
type: string
description: 'Numero de identificacion. Solo digitos, 5-12 caracteres. Cambiar este valor afecta la clave de 50 digitos en comprobantes futuros. Must match the regex /^\d{5,12}$/.'
example: '3101000000'
legal_name:
type: string
description: 'Razon social o nombre completo. Min 5, max 100 caracteres. validation.min validation.max.'
example: 'Empresa Ejemplo S.A.'
trade_name:
type: string
description: 'Nombre comercial. Opcional, min 3, max 80 caracteres. validation.min validation.max.'
example: 'Ejemplo Tienda'
nullable: true
emails:
type: array
description: 'Correo electrónico individual. Max 160 chars. validation.email validation.max.'
example:
- facturacion@empresa.cr
items:
type: string
economic_activities:
type: array
description: 'Código CIIU individual en formato XXXX.X. Must match the regex /^\d{4}\.\d$/.'
example:
- '6201.0'
items:
type: string
phone:
type: string
description: 'Número de teléfono sin código de país. Solo dígitos, 4-20 chars. Must match the regex /^\d{4,20}$/.'
example: '22001234'
nullable: true
phone_country_code:
type: integer
description: 'Código de país telefónico (1-999). Default 506 (Costa Rica). validation.between.'
example: 506
nullable: true
province:
type: integer
description: 'Código de provincia (1-7). Si se envía, canton y district son obligatorios. validation.between.'
example: 1
nullable: true
canton:
type: integer
description: 'Código de cantón (1-99). Obligatorio si province presente. validation.between.'
example: 1
nullable: true
district:
type: integer
description: 'Código de distrito (1-99). Obligatorio si province presente. validation.between.'
example: 1
nullable: true
neighborhood:
type: string
description: 'Barrio. Opcional, min 5, max 50 chars. validation.min validation.max.'
example: 'San Pedro'
nullable: true
address:
type: string
description: 'Senas exactas de la direccion. Min 5, max 250 caracteres. validation.min validation.max.'
example: '200 metros norte del parque central'
nullable: true
required:
- economic_activities
/certificates:
get:
summary: 'Listar certificados digitales del contribuyente'
operationId: listarCertificadosDigitalesDelContribuyente
description: "Devuelve el historial completo de certificados digitales registrados\npor el contribuyente: el certificado actualmente activo y todos los\nque fueron desactivados previamente (conservados para auditoría).\n\nLos datos sensibles del certificado (contenido del archivo `.p12`,\ncontraseña y PIN de Hacienda) **nunca se incluyen** en la respuesta,\nincluso si usted mismo los cargó.\n\nCada registro incluye:\n\n- Ambiente (`sandbox` / `production`).\n- Estado (`is_active`) y fecha de desactivación si aplica.\n- Vigencia del certificado (`valid_from`, `valid_until`).\n- **Días restantes** hasta la expiración (`days_remaining`) — útil\n para monitorear renovación.\n- Subject del certificado X.509 (nombre del titular).\n- Número de serie del certificado.\n\nOrdenamiento: el más reciente primero."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Listado con activo e histórico'
type: object
example:
success: true
data:
-
id: 019d867d-1111-7288-8ece-fd64da756001
environment: sandbox
is_active: true
is_expired: false
days_remaining: 120
valid_from: '2024-01-01T00:00:00-06:00'
valid_until: '2026-08-15T23:59:59-06:00'
certificate_subject: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
certificate_serial: 0A1B2C3D4E5F
created_at: '2026-04-09T10:00:00-06:00'
-
id: 019d867d-2222-7288-8ece-fd64da756002
environment: sandbox
is_active: false
is_expired: true
days_remaining: 0
valid_from: '2022-01-01T00:00:00-06:00'
valid_until: '2024-01-01T00:00:00-06:00'
certificate_subject: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
certificate_serial: AA11BB22CC33
created_at: '2024-01-10T08:00:00-06:00'
message: ''
errors: null
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
id: 019d867d-1111-7288-8ece-fd64da756001
environment: sandbox
is_active: true
is_expired: false
days_remaining: 120
valid_from: '2024-01-01T00:00:00-06:00'
valid_until: '2026-08-15T23:59:59-06:00'
certificate_subject: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
certificate_serial: 0A1B2C3D4E5F
created_at: '2026-04-09T10:00:00-06:00'
-
id: 019d867d-2222-7288-8ece-fd64da756002
environment: sandbox
is_active: false
is_expired: true
days_remaining: 0
valid_from: '2022-01-01T00:00:00-06:00'
valid_until: '2024-01-01T00:00:00-06:00'
certificate_subject: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
certificate_serial: AA11BB22CC33
created_at: '2024-01-10T08:00:00-06:00'
items:
type: object
properties:
id:
type: string
example: 019d867d-1111-7288-8ece-fd64da756001
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 120
valid_from:
type: string
example: '2024-01-01T00:00:00-06:00'
valid_until:
type: string
example: '2026-08-15T23:59:59-06:00'
certificate_subject:
type: string
example: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
certificate_serial:
type: string
example: 0A1B2C3D4E5F
created_at:
type: string
example: '2026-04-09T10:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
-
description: 'Sin certificados cargados'
type: object
example:
success: true
data: []
message: ''
errors: null
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
401:
description: 'No autenticado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: Unauthenticated.
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: Unauthenticated.
errors:
type: string
example: null
nullable: true
tags:
- 'Certificados Digitales'
post:
summary: 'Subir y activar un certificado digital `.p12`'
operationId: subirYActivarUnCertificadoDigitalp12
description: "Sube un archivo `.p12` (PKCS#12) con su contraseña y el PIN de\nHacienda, y lo activa como certificado para firmar comprobantes\nen el ambiente indicado.\n\nEste request **debe enviarse como `multipart/form-data`** (no JSON),\nporque incluye un archivo binario.\n\n**Proceso interno al subir:**\n\n1. Se valida que el archivo sea un `.p12` legítimo y que la\n contraseña lo abra correctamente.\n2. Se extraen los metadatos del certificado X.509 (vigencia,\n subject, número de serie).\n3. Si ya existía un certificado activo del mismo ambiente, se\n **desactiva automáticamente** (queda en el histórico).\n4. El nuevo certificado queda activo y listo para firmar.\n\n**Ejemplo con `curl`:**\n\n```bash\ncurl -X POST https://api.almendro.cr/api/v1/public/certificates \\\n -H \"Authorization: Bearer {su_token}\" \\\n -F \"p12_file=@/ruta/a/certificado.p12\" \\\n -F \"p12_password=contrasena-del-p12\" \\\n -F \"hacienda_pin=1234\" \\\n -F \"environment=sandbox\"\n```\n\n**Ejemplo con Node.js (axios + form-data):**\n\n```javascript\nconst FormData = require('form-data')\nconst fs = require('fs')\nconst axios = require('axios')\n\nconst form = new FormData()\nform.append('p12_file', fs.createReadStream('certificado.p12'))\nform.append('p12_password', 'contrasena-del-p12')\nform.append('hacienda_pin', '1234')\nform.append('environment', 'sandbox')\n\nconst response = await axios.post(\n 'https://api.almendro.cr/api/v1/public/certificates',\n form,\n { headers: { ...form.getHeaders(), Authorization: `Bearer ${token}` } },\n)\n```"
parameters: []
responses:
201:
description: 'Certificado registrado y activado'
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 019d867d-3333-7288-8ece-fd64da756003
environment: sandbox
is_active: true
is_expired: false
days_remaining: 730
valid_from: '2026-01-01T00:00:00-06:00'
valid_until: '2028-01-01T00:00:00-06:00'
certificate_subject: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
certificate_serial: 0A1B2C3D4E5F
created_at: '2026-04-16T10:00:00-06:00'
message: 'Certificado registrado y activado correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-3333-7288-8ece-fd64da756003
description: 'UUID del certificado. Úselo en `DELETE /certificates/{id}` para desactivarlo.'
environment:
type: string
example: sandbox
description: 'Ambiente al que pertenece: `sandbox` o `production`. Cada ambiente tiene su propio certificado activo independiente.'
is_active:
type: boolean
example: true
description: '`true` si este certificado es el actualmente usado para firmar en su ambiente. Solo puede haber 1 activo por contribuyente + ambiente.'
is_expired:
type: boolean
example: false
description: '`true` si el certificado X.509 ya venció (`valid_until < now()`). Independiente de `is_active` — un certificado activo puede estar vencido si no fue renovado a tiempo. `null` si no se pudo extraer la vigencia.'
days_remaining:
type: integer
example: 730
description: 'Días restantes hasta el vencimiento del certificado. `0` si ya venció. `null` si no se pudo extraer la vigencia. Útil para alertas de renovación.'
valid_from:
type: string
example: '2026-01-01T00:00:00-06:00'
description: 'Fecha de inicio de vigencia del certificado X.509 (ISO 8601). Extraída de `notBefore` del X.509 al subir el `.p12`. `null` si no se pudo parsear.'
valid_until:
type: string
example: '2028-01-01T00:00:00-06:00'
description: 'Fecha de fin de vigencia del certificado X.509 (ISO 8601). Extraída de `notAfter` del X.509. Los certificados del BCCR tienen vigencia típica de 2 años. `null` si no se pudo parsear.'
certificate_subject:
type: string
example: 'CN=EMPRESA EJEMPLO S.A., serialNumber=3101000001'
description: 'Distinguished Name (DN) del titular del certificado. Ejemplo: `CN=EMPRESA S.A., serialNumber=3101000001`. `null` si no se pudo extraer.'
certificate_serial:
type: string
example: 0A1B2C3D4E5F
description: 'Número de serie del certificado X.509 en hexadecimal. Útil para auditoría e identificación del certificado ante la entidad certificadora. `null` si no se pudo extraer.'
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha en que se subió este certificado a la plataforma (ISO 8601).'
message:
type: string
example: 'Certificado registrado y activado correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Contraseña incorrecta del .p12'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: '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:
type: object
properties:
p12_file:
type: array
example:
- 'No se pudo leer el certificado .p12. Verifique que la contraseña sea correcta y que el archivo sea un certificado digital válido.'
items:
type: string
-
description: 'Archivo no es un .p12 válido'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
p12_file:
- 'El archivo debe tener extensión .p12 o .pfx.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
p12_file:
type: array
example:
- 'El archivo debe tener extensión .p12 o .pfx.'
items:
type: string
-
description: 'Ambiente inválido'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
environment:
- 'El ambiente seleccionado no es válido. Use sandbox o production.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
environment:
type: array
example:
- 'El ambiente seleccionado no es válido. Use sandbox o production.'
items:
type: string
-
description: 'Falta el PIN de Hacienda'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
hacienda_pin:
- 'El PIN de Hacienda es obligatorio.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
hacienda_pin:
type: array
example:
- 'El PIN de Hacienda es obligatorio.'
items:
type: string
tags:
- 'Certificados Digitales'
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
p12_file:
type: string
format: binary
description: 'Archivo de certificado digital en formato PKCS#12 (extensión `.p12` o `.pfx`). Tamaño máximo: 2 MB.'
p12_password:
type: string
description: 'Contraseña que protege el archivo `.p12` (la que usted definió al generarlo).'
example: MiContrasenaDelP12
hacienda_pin:
type: string
description: 'PIN asignado por Hacienda al registrarse como emisor en ATV (normalmente 4 dígitos).'
example: '1234'
environment:
type: string
description: 'Ambiente al que se asigna este certificado. Valores: `sandbox` o `production`.'
example: sandbox
required:
- p12_file
- p12_password
- hacienda_pin
- environment
'/certificates/{id}':
delete:
summary: 'Desactivar un certificado digital'
operationId: desactivarUnCertificadoDigital
description: "Marca el certificado como **inactivo**. El registro **no se elimina**:\npermanece en el histórico para auditoría de todas las firmas\nrealizadas durante su vigencia.\n\n**Consecuencias inmediatas:**\n\n- Si el certificado desactivado era el activo de su ambiente, la\n emisión en ese ambiente **queda bloqueada** hasta que suba un\n nuevo certificado activo.\n- Si algún integrador tenía acceso delegado a este certificado, el\n acceso se **revoca automáticamente** y se le envía una notificación.\n- El conteo de accesos revocados aparece en el `message` de la\n respuesta.\n\nSolo puede desactivar certificados propios. Si el UUID no\ncorresponde a su contribuyente, retorna **404**.\n\n> **Casos típicos para desactivar:**\n> - El certificado fue comprometido (pérdida, acceso no autorizado).\n> - Va a subir un nuevo certificado pero quiere dejar el ambiente\n> sin firma temporalmente (caso raro).\n> - Migración de un contribuyente a otra plataforma.\n\nNo necesita desactivar el certificado anterior antes de subir uno\nnuevo — el `POST /certificates` ya lo hace automáticamente al\nregistrar el nuevo."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Certificado desactivado (sin accesos asociados)'
type: object
example:
success: true
data: null
message: 'Certificado desactivado correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Certificado desactivado correctamente.'
errors:
type: string
example: null
nullable: true
-
description: 'Certificado desactivado con auto-revocación de accesos'
type: object
example:
success: true
data: null
message: 'Certificado desactivado correctamente. Se revocaron 2 accesos de integradores asociados.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Certificado desactivado correctamente. Se revocaron 2 accesos de integradores asociados.'
errors:
type: string
example: null
nullable: true
404:
description: 'Certificado no encontrado o no pertenece al contribuyente'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Certificado no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Certificado no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Certificados Digitales'
parameters:
-
in: path
name: id
description: 'UUID del certificado a desactivar.'
example: 019d867d-1111-7288-8ece-fd64da756001
required: true
schema:
type: string
/pdf-templates:
get:
summary: 'Listar plantillas PDF del contribuyente'
operationId: listarPlantillasPDFDelContribuyente
description: "Devuelve todas las plantillas PDF activas del contribuyente,\nordenadas con la plantilla **por defecto primero** (`is_default=true`)\ny luego por nombre alfabéticamente.\n\nEste endpoint **no pagina** — el número de plantillas por\ncontribuyente es pequeño (límite por plan entre 1 y 15), por lo\nque siempre se devuelve la lista completa.\n\nCada plantilla incluye su configuración completa (`config_json`)\ncon colores, fuentes, layout y parámetros del QR, además de un flag\n`has_logo` que indica si tiene un logo cargado."
parameters: []
responses:
200:
description: 'Listado de plantillas (default primero)'
content:
application/json:
schema:
type: object
example:
success: true
data:
-
id: 1
name: Classic
is_default: true
paper_size: letter
config_json:
colors:
primary: '#2d3748'
secondary: '#4a5568'
accent: '#3182ce'
fonts:
family: Helvetica
size_title: 14
size_body: 9
layout:
show_logo: true
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 100
position: bottom-right
has_logo: true
is_active: true
created_at: '2026-04-01T10:00:00-06:00'
updated_at: '2026-04-10T14:30:00-06:00'
-
id: 2
name: Minimal
is_default: false
paper_size: A4
config_json:
colors:
primary: '#000000'
secondary: '#666666'
accent: '#000000'
fonts:
family: Helvetica
size_title: 12
size_body: 8
layout:
show_logo: false
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 80
position: bottom-right
has_logo: false
is_active: true
created_at: '2026-04-05T08:00:00-06:00'
updated_at: '2026-04-05T08:00:00-06:00'
message: ''
errors: null
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
id: 1
name: Classic
is_default: true
paper_size: letter
config_json:
colors:
primary: '#2d3748'
secondary: '#4a5568'
accent: '#3182ce'
fonts:
family: Helvetica
size_title: 14
size_body: 9
layout:
show_logo: true
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 100
position: bottom-right
has_logo: true
is_active: true
created_at: '2026-04-01T10:00:00-06:00'
updated_at: '2026-04-10T14:30:00-06:00'
-
id: 2
name: Minimal
is_default: false
paper_size: A4
config_json:
colors:
primary: '#000000'
secondary: '#666666'
accent: '#000000'
fonts:
family: Helvetica
size_title: 12
size_body: 8
layout:
show_logo: false
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 80
position: bottom-right
has_logo: false
is_active: true
created_at: '2026-04-05T08:00:00-06:00'
updated_at: '2026-04-05T08:00:00-06:00'
items:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: Classic
is_default:
type: boolean
example: true
paper_size:
type: string
example: letter
config_json:
type: object
properties:
colors:
type: object
properties:
primary:
type: string
example: '#2d3748'
secondary:
type: string
example: '#4a5568'
accent:
type: string
example: '#3182ce'
fonts:
type: object
properties:
family:
type: string
example: Helvetica
size_title:
type: integer
example: 14
size_body:
type: integer
example: 9
layout:
type: object
properties:
show_logo:
type: boolean
example: true
logo_position:
type: string
example: left
logo_max_height:
type: integer
example: 60
qr:
type: object
properties:
enabled:
type: boolean
example: true
size:
type: integer
example: 100
position:
type: string
example: bottom-right
has_logo:
type: boolean
example: true
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-10T14:30:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
401:
description: 'No autenticado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: Unauthenticated.
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: Unauthenticated.
errors:
type: string
example: null
nullable: true
tags:
- 'Plantillas PDF'
post:
summary: 'Crear una nueva plantilla PDF'
operationId: crearUnaNuevaPlantillaPDF
description: "Registra una plantilla personalizada en el catálogo del contribuyente.\nSi envía `is_default: true`, la plantilla default anterior se\ndesmarca automáticamente.\n\nSi **no envía** `config_json`, se aplica una configuración por\ndefecto que cumple con todos los requisitos normativos (QR mínimo\n2.5 cm en la esquina inferior derecha).\n\nSi **envía** `config_json`, puede enviar un objeto parcial — las\nclaves no enviadas usan los valores del template default."
parameters: []
responses:
201:
description: 'Plantilla creada'
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 3
name: 'Mi Plantilla Corporativa'
is_default: false
paper_size: letter
config_json:
colors:
primary: '#0a66c2'
secondary: '#333333'
accent: '#0a66c2'
fonts:
family: Helvetica
size_title: 14
size_body: 9
layout:
show_logo: true
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 100
position: bottom-right
has_logo: false
is_active: true
created_at: '2026-04-16T10:00:00-06:00'
updated_at: '2026-04-16T10:00:00-06:00'
message: 'Plantilla creada correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 3
description: 'ID numérico de la plantilla. Úselo en `PUT`, `DELETE`, `/preview` y `/logo`.'
name:
type: string
example: 'Mi Plantilla Corporativa'
description: 'Nombre identificable de la plantilla (3-100 chars). Único por contribuyente.'
is_default:
type: boolean
example: false
description: '`true` si es la plantilla usada por defecto al generar PDFs. Solo una plantilla puede ser default por contribuyente.'
paper_size:
type: string
example: letter
description: 'Tamaño del papel: `letter` (Carta US 8.5×11") o `A4` (210×297mm).'
config_json:
type: object
properties:
colors:
type: object
properties:
primary:
type: string
example: '#0a66c2'
description: 'Color principal (encabezados, títulos). Ejemplo: `#2d3748`.'
secondary:
type: string
example: '#333333'
description: 'Color secundario (texto, bordes). Ejemplo: `#4a5568`.'
accent:
type: string
example: '#0a66c2'
description: 'Color de acentos (líneas, badges). Ejemplo: `#3182ce`.'
description: 'Paleta de colores hexadecimales.'
fonts:
type: object
properties:
family:
type: string
example: Helvetica
description: 'Familia tipográfica: `Helvetica`, `Times-Roman` o `Courier`.'
size_title:
type: integer
example: 14
description: 'Tamaño del título principal en pt (10-20).'
size_body:
type: integer
example: 9
description: 'Tamaño del texto general en pt (6-14).'
description: 'Configuración tipográfica.'
layout:
type: object
properties:
show_logo:
type: boolean
example: true
description: 'Si se muestra el logo del contribuyente en el encabezado.'
logo_position:
type: string
example: left
description: 'Posición del logo: `left`, `center` o `right`.'
logo_max_height:
type: integer
example: 60
description: 'Alto máximo del logo en pt (20-100).'
description: 'Configuración de layout y logo.'
qr:
type: object
properties:
enabled:
type: boolean
example: true
description: 'Si se incluye el QR. Siempre `true` por normativa — el sistema ignora `false`.'
size:
type: integer
example: 100
description: 'Tamaño del QR en pt. Mínimo 70 (≈2.5 cm por normativa). Máximo 150.'
position:
type: string
example: bottom-right
description: 'Posición del QR: `bottom-right` (exigido por normativa) o `bottom-left`.'
description: 'Configuración del código QR (obligatorio por normativa).'
description: 'Configuración visual completa de la plantilla con 4 bloques: `colors`, `fonts`, `layout`, `qr`.'
has_logo:
type: boolean
example: false
description: '`true` si la plantilla tiene un archivo de logo cargado. Use `POST /pdf-templates/{id}/logo` para subirlo.'
is_active:
type: boolean
example: true
description: '`true` si la plantilla está activa y puede usarse para generar PDFs.'
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de creación de la plantilla (ISO 8601).'
updated_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de última actualización (ISO 8601).'
message:
type: string
example: 'Plantilla creada correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Nombre duplicado'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
name:
- 'Ya existe una plantilla con este nombre para el contribuyente.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
name:
type: array
example:
- 'Ya existe una plantilla con este nombre para el contribuyente.'
items:
type: string
-
description: 'Color hexadecimal inválido'
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
config_json.colors.primary:
type: array
example:
- 'El color debe ser un hexadecimal válido (ej. #1a365d).'
items:
type: string
-
description: 'Tamaño del QR por debajo del mínimo normativo'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
config_json.qr.size:
type: array
example:
- 'El tamaño mínimo del QR es 70 pt (≈ 2.5 cm) por normativa.'
items:
type: string
-
description: 'Límite del plan alcanzado'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
plan:
type: array
example:
- 'Ha alcanzado el límite de plantillas PDF de su plan. Actualice a un plan superior.'
items:
type: string
tags:
- 'Plantillas PDF'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Nombre identificable de la plantilla (3-100 caracteres). Único por contribuyente.'
example: 'Mi Plantilla Corporativa'
is_default:
type: boolean
description: 'Si será la plantilla por defecto. Si `true`, desmarca la anterior automáticamente. Default: `false`.'
example: false
paper_size:
type: string
description: 'Tamaño del papel. Valores: `letter` o `A4`. Default: `letter`.'
example: letter
config_json:
type: object
description: 'Configuración visual. Acepta un objeto parcial; las claves no enviadas usan valores por defecto. Ver estructura en la Guía del grupo.'
example:
colors:
primary: '#0a66c2'
secondary: '#333333'
accent: '#0a66c2'
fonts:
family: Helvetica
size_title: 14
size_body: 9
layout:
show_logo: true
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 100
position: bottom-right
properties:
colors:
type: object
description: ''
example:
primary: '#1a365d'
secondary: '#e2e8f0'
text: '#1a202c'
accent: '#3182ce'
properties:
primary:
type: string
description: 'Color hexadecimal principal.'
example: '#0a66c2'
secondary:
type: string
description: 'Color hexadecimal secundario.'
example: '#333333'
text:
type: string
description: 'Must match the regex /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.'
example: '#1a202c'
accent:
type: string
description: 'Color hexadecimal de acentos.'
example: '#0a66c2'
fonts:
type: object
description: ''
example:
family: 'DejaVu Sans'
size_header: 13
size_body: 8
size_footer: 6
properties:
family:
type: string
description: 'Familia tipográfica. Valores: `Helvetica`, `Times-Roman`, `Courier`.'
example: Helvetica
size_header:
type: integer
description: 'validation.min validation.max.'
example: 13
size_body:
type: integer
description: 'Tamaño del cuerpo (6-14).'
example: 9
size_footer:
type: integer
description: 'validation.min validation.max.'
example: 6
size_title:
type: integer
description: 'Tamaño del título (10-20).'
example: 14
layout:
type: object
description: ''
example:
show_logo: true
logo_position: left
logo_max_height: 60
show_commercial_name: true
show_address: true
show_phone: true
show_email: true
show_observations: true
show_payment_methods: true
show_references: true
show_other_charges: true
properties:
show_logo:
type: boolean
description: 'Si se muestra el logo.'
example: true
logo_position:
type: string
description: 'Posición del logo. Valores: `left`, `center`, `right`.'
example: left
logo_max_height:
type: integer
description: 'Alto máximo del logo en pt (20-100).'
example: 60
show_commercial_name:
type: boolean
description: ''
example: true
show_address:
type: boolean
description: ''
example: true
show_phone:
type: boolean
description: ''
example: true
show_email:
type: boolean
description: ''
example: true
show_observations:
type: boolean
description: ''
example: true
show_payment_methods:
type: boolean
description: ''
example: true
show_references:
type: boolean
description: ''
example: true
show_other_charges:
type: boolean
description: ''
example: true
qr:
type: object
description: ''
example:
size: 100
position: bottom-right
properties:
size:
type: integer
description: 'Tamaño del QR en pt (70-150, mínimo 70 ≈ 2.5 cm).'
example: 100
position:
type: string
description: 'Posición del QR. Valor recomendado por normativa: `bottom-right`.'
example: bottom-right
enabled:
type: boolean
description: 'Incluir QR (normativa obliga `true`).'
example: true
margins:
type: object
description: ''
example:
top: 15
right: 15
bottom: 15
left: 15
properties:
top:
type: integer
description: 'validation.min validation.max.'
example: 15
right:
type: integer
description: 'validation.min validation.max.'
example: 15
bottom:
type: integer
description: 'validation.min validation.max.'
example: 15
left:
type: integer
description: 'validation.min validation.max.'
example: 15
style:
type: string
description: ''
example: classic
enum:
- classic
- modern
- minimal
- bold
- split
footer_text:
type: string
description: validation.max.
example: 'Documento generado electrónicamente | Almendro Factura Electrónica'
nullable: true
required:
- name
'/pdf-templates/{id}':
put:
summary: 'Editar una plantilla PDF existente'
operationId: editarUnaPlantillaPDFExistente
description: "Admite **actualización parcial** — envíe únicamente los campos que\ndesea modificar. Los campos no enviados conservan su valor actual.\n\nEl campo `config_json` se **fusiona** con la configuración\nexistente (merge profundo): las claves que envíe se actualizan y\nlas que no envíe se mantienen intactas. Esto permite cambiar, por\nejemplo, solo los colores sin afectar fuentes, layout o QR.\n\nSi envía `is_default: true` y la plantilla no era la default, la\ndefault anterior se desmarca automáticamente.\n\n> **Importante:** editar una plantilla **no afecta** a los PDFs\n> ya generados con ella. Los PDFs se congelan con la configuración\n> vigente al momento de la emisión (inmutabilidad post-emisión)."
parameters: []
responses:
200:
description: 'Plantilla actualizada'
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 1
name: 'Profesional Actualizada'
is_default: true
paper_size: letter
config_json:
colors:
primary: '#1a365d'
secondary: '#4a5568'
accent: '#3182ce'
fonts:
family: Helvetica
size_title: 14
size_body: 9
layout:
show_logo: true
logo_position: left
logo_max_height: 60
qr:
enabled: true
size: 100
position: bottom-right
has_logo: true
is_active: true
created_at: '2026-04-01T10:00:00-06:00'
updated_at: '2026-04-16T11:00:00-06:00'
message: 'Plantilla actualizada correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: 'Profesional Actualizada'
is_default:
type: boolean
example: true
paper_size:
type: string
example: letter
config_json:
type: object
properties:
colors:
type: object
properties:
primary:
type: string
example: '#1a365d'
secondary:
type: string
example: '#4a5568'
accent:
type: string
example: '#3182ce'
fonts:
type: object
properties:
family:
type: string
example: Helvetica
size_title:
type: integer
example: 14
size_body:
type: integer
example: 9
layout:
type: object
properties:
show_logo:
type: boolean
example: true
logo_position:
type: string
example: left
logo_max_height:
type: integer
example: 60
qr:
type: object
properties:
enabled:
type: boolean
example: true
size:
type: integer
example: 100
position:
type: string
example: bottom-right
has_logo:
type: boolean
example: true
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T11:00:00-06:00'
message:
type: string
example: 'Plantilla actualizada correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'Plantilla no encontrada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
422:
description: 'Nombre duplicado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
name:
- 'Ya existe una plantilla con este nombre para el contribuyente.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
name:
type: array
example:
- 'Ya existe una plantilla con este nombre para el contribuyente.'
items:
type: string
tags:
- 'Plantillas PDF'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Nombre de la plantilla (3-100 caracteres, único por contribuyente).'
example: 'Profesional Actualizada'
is_default:
type: boolean
description: 'Si será la plantilla default. Si `true`, desmarca la anterior.'
example: true
is_active:
type: boolean
description: 'Estado activo/inactivo de la plantilla.'
example: true
paper_size:
type: string
description: 'Tamaño del papel. Valores: `letter`, `A4`.'
example: letter
config_json:
type: object
description: 'Cambios parciales a la configuración visual (merge profundo con la actual).'
example:
colors:
primary: '#1a365d'
properties:
colors:
type: object
description: ''
example:
primary: '#2d3748'
properties:
primary:
type: string
description: 'Color hexadecimal principal.'
example: '#1a365d'
secondary:
type: string
description: 'Color hexadecimal secundario.'
example: '#4a5568'
text:
type: string
description: 'Must match the regex /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.'
example: '#2815Df'
accent:
type: string
description: 'Color hexadecimal de acentos.'
example: '#3182ce'
fonts:
type: object
description: ''
example: null
properties:
family:
type: string
description: 'Familia tipográfica.'
example: Helvetica
size_header:
type: integer
description: 'validation.min validation.max.'
example: 2
size_body:
type: integer
description: 'Tamaño del cuerpo (6-14).'
example: 9
size_footer:
type: integer
description: 'validation.min validation.max.'
example: 3
size_title:
type: integer
description: 'Tamaño del título (10-20).'
example: 14
layout:
type: object
description: ''
example: null
properties:
show_logo:
type: boolean
description: 'Si se muestra el logo.'
example: true
logo_position:
type: string
description: 'Posición del logo (`left`, `center`, `right`).'
example: left
logo_max_height:
type: integer
description: 'Alto máximo del logo en pt (20-100).'
example: 60
show_commercial_name:
type: boolean
description: ''
example: true
show_address:
type: boolean
description: ''
example: true
show_phone:
type: boolean
description: ''
example: true
show_email:
type: boolean
description: ''
example: false
show_observations:
type: boolean
description: ''
example: false
show_payment_methods:
type: boolean
description: ''
example: true
show_references:
type: boolean
description: ''
example: false
show_other_charges:
type: boolean
description: ''
example: false
qr:
type: object
description: ''
example: null
properties:
size:
type: integer
description: 'Tamaño del QR en pt (70-150).'
example: 100
position:
type: string
description: 'Posición del QR (`bottom-right` recomendado).'
example: bottom-right
margins:
type: object
description: ''
example: null
properties:
top:
type: integer
description: 'validation.min validation.max.'
example: 22
right:
type: integer
description: 'validation.min validation.max.'
example: 24
bottom:
type: integer
description: 'validation.min validation.max.'
example: 18
left:
type: integer
description: 'validation.min validation.max.'
example: 8
style:
type: string
description: ''
example: modern
enum:
- classic
- modern
- minimal
- bold
- split
footer_text:
type: string
description: validation.max.
example: m
nullable: true
delete:
summary: 'Eliminar una plantilla PDF'
operationId: eliminarUnaPlantillaPDF
description: "Realiza un **soft delete** de la plantilla. Los PDFs ya generados\ncon esta plantilla **no se ven afectados** — la configuración se\naplica al momento de generar cada PDF y no se modifica\nretroactivamente.\n\n**Protección:** no se puede eliminar una plantilla si es la\n**única plantilla activa** del contribuyente. En ese caso el\nsistema responde HTTP 409 y debe:\n\n1. Crear otra plantilla nueva, o\n2. Reactivar otra plantilla existente,\n\nantes de eliminar la actual."
parameters: []
responses:
200:
description: 'Plantilla eliminada'
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Plantilla eliminada correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Plantilla eliminada correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'Plantilla no encontrada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
409:
description: 'No se puede eliminar la única plantilla activa'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No se puede eliminar la única plantilla activa del contribuyente. Cree otra plantilla o marque otra como default antes de eliminar.'
errors:
type: string
example: null
nullable: true
tags:
- 'Plantillas PDF'
parameters:
-
in: path
name: id
description: 'ID de la plantilla.'
example: 1
required: true
schema:
type: integer
'/pdf-templates/{id}/preview':
post:
summary: 'Generar un preview del PDF con datos reales'
operationId: generarUnPreviewDelPDFConDatosReales
description: "Renderiza un PDF de ejemplo usando la plantilla indicada y el\n**comprobante más reciente** emitido por el contribuyente. Permite\nverificar la apariencia de la plantilla antes de asignarla como\ndefault.\n\nLa respuesta es un archivo PDF (`Content-Type: application/pdf`)\ninline. Puede guardarlo localmente con `-o archivo.pdf` en `curl`\no mostrarlo directamente en un iframe del navegador.\n\nSi el contribuyente aún **no ha emitido ningún comprobante**,\nretorna HTTP 404 con un mensaje indicando que debe emitir al\nmenos uno primero.\n\n> **Nota:** el comprobante usado para el preview es el más\n> reciente (`ORDER BY issued_at DESC LIMIT 1`). Si quiere probar\n> la plantilla con un comprobante específico, emita uno nuevo y\n> use este endpoint después — o bien use el endpoint\n> `GET /vouchers/{key}/pdf` con `?pdf_template_id={id}`."
parameters: []
responses:
200:
description: 'PDF generado exitosamente'
content:
application/json:
schema:
type: object
example:
Content-Type: application/pdf
Content-Disposition: 'inline; filename="preview_Classic.pdf"'
Cache-Control: no-store
body: '%PDF-1.4 ... (binario) ... %%EOF'
properties:
Content-Type:
type: string
example: application/pdf
Content-Disposition:
type: string
example: 'inline; filename="preview_Classic.pdf"'
Cache-Control:
type: string
example: no-store
body:
type: string
example: '%PDF-1.4 ... (binario) ... %%EOF'
404:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Sin comprobantes para preview'
type: object
example:
success: false
data: null
message: 'No hay comprobantes emitidos para generar un preview. Emita al menos un comprobante antes de usar preview.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No hay comprobantes emitidos para generar un preview. Emita al menos un comprobante antes de usar preview.'
errors:
type: string
example: null
nullable: true
-
description: 'Plantilla no encontrada'
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Plantillas PDF'
parameters:
-
in: path
name: id
description: 'ID de la plantilla.'
example: 1
required: true
schema:
type: integer
'/pdf-templates/{id}/logo':
get:
summary: 'Obtener el logo de una plantilla PDF'
operationId: obtenerElLogoDeUnaPlantillaPDF
description: "Devuelve el archivo de imagen del logo como respuesta binaria\ninline, con el `Content-Type` correcto según el formato (PNG,\nJPG/JPEG, SVG).\n\nSi la plantilla **no tiene logo configurado**, retorna HTTP 404.\n\nEl archivo se sirve con cache privado de 1 hora y cabeceras de\nseguridad (`X-Content-Type-Options: nosniff`)."
parameters: []
responses:
200:
description: 'Logo disponible'
content:
application/json:
schema:
type: object
example:
Content-Type: image/png
Cache-Control: 'private, max-age=3600'
X-Content-Type-Options: nosniff
body: '(contenido binario de la imagen)'
properties:
Content-Type:
type: string
example: image/png
Cache-Control:
type: string
example: 'private, max-age=3600'
X-Content-Type-Options:
type: string
example: nosniff
body:
type: string
example: '(contenido binario de la imagen)'
404:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Plantilla sin logo configurado'
type: object
example:
success: false
data: null
message: 'Esta plantilla no tiene logo.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Esta plantilla no tiene logo.'
errors:
type: string
example: null
nullable: true
-
description: 'Archivo de logo no encontrado en el servidor'
type: object
example:
success: false
data: null
message: 'Archivo de logo no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Archivo de logo no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Plantillas PDF'
post:
summary: 'Subir o reemplazar el logo de una plantilla PDF'
operationId: subirOReemplazarElLogoDeUnaPlantillaPDF
description: "Sube un archivo de imagen que se utilizará como logo en el\nencabezado del PDF generado con esta plantilla. Si la plantilla ya\ntenía un logo, se **reemplaza automáticamente** (el archivo\nanterior se elimina del servidor).\n\nEste request **debe enviarse como `multipart/form-data`** (no\nJSON), porque incluye un archivo binario.\n\n**Restricciones del archivo:**\n\n- Extensiones permitidas: `png`, `jpg`, `jpeg`, `svg`.\n- Tamaño máximo: **200 KB**.\n- Para logos que se verán bien en el PDF, use preferentemente\n resolución de **300 DPI** y dimensiones proporcionales a\n `layout.logo_max_height` configurado.\n\n**Ejemplo con `curl`:**\n\n```bash\ncurl -X POST https://api.almendro.cr/api/v1/public/pdf-templates/1/logo \\\n -H \"Authorization: Bearer {su_token}\" \\\n -F \"logo_file=@/ruta/a/logo.png\"\n```\n\n**Ejemplo con Node.js (axios + form-data):**\n\n```javascript\nconst FormData = require('form-data')\nconst fs = require('fs')\nconst axios = require('axios')\n\nconst form = new FormData()\nform.append('logo_file', fs.createReadStream('logo.png'))\n\nawait axios.post(\n 'https://api.almendro.cr/api/v1/public/pdf-templates/1/logo',\n form,\n { headers: { ...form.getHeaders(), Authorization: `Bearer ${token}` } },\n)\n```"
parameters: []
responses:
200:
description: 'Logo subido exitosamente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: Classic
is_default:
type: boolean
example: true
paper_size:
type: string
example: letter
config_json:
type: object
properties:
colors:
type: object
properties: { }
fonts:
type: object
properties: { }
layout:
type: object
properties: { }
qr:
type: object
properties: { }
has_logo:
type: boolean
example: true
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T12:00:00-06:00'
message:
type: string
example: 'Logo subido correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Archivo faltante'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
logo_file:
- 'El archivo del logo es obligatorio.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
logo_file:
type: array
example:
- 'El archivo del logo es obligatorio.'
items:
type: string
-
description: 'Extensión no permitida'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
logo_file:
- 'Extensión no permitida. Use: png, jpg, jpeg, svg.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
logo_file:
type: array
example:
- 'Extensión no permitida. Use: png, jpg, jpeg, svg.'
items:
type: string
-
description: 'Archivo excede 200 KB'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
logo_file:
- 'El logo no puede superar 200 KB.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
logo_file:
type: array
example:
- 'El logo no puede superar 200 KB.'
items:
type: string
tags:
- 'Plantillas PDF'
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
logo_file:
type: string
format: binary
description: 'Archivo de imagen del logo. Extensiones: `png`, `jpg`, `jpeg`, `svg`. Máximo 200 KB.'
required:
- logo_file
delete:
summary: 'Eliminar el logo de una plantilla PDF'
operationId: eliminarElLogoDeUnaPlantillaPDF
description: "Elimina el archivo de logo asociado a la plantilla. Los PDFs que\nya fueron generados con este logo **no se ven afectados**.\n\nSi la plantilla **no tiene logo** configurado, retorna HTTP 409."
parameters: []
responses:
200:
description: 'Logo eliminado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: Classic
is_default:
type: boolean
example: true
paper_size:
type: string
example: letter
config_json:
type: object
properties:
colors:
type: object
properties: { }
fonts:
type: object
properties: { }
layout:
type: object
properties: { }
qr:
type: object
properties: { }
has_logo:
type: boolean
example: false
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T13:00:00-06:00'
message:
type: string
example: 'Logo eliminado correctamente.'
errors:
type: string
example: null
nullable: true
409:
description: 'Sin logo configurado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Esta plantilla no tiene logo configurado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Esta plantilla no tiene logo configurado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Plantillas PDF'
parameters:
-
in: path
name: id
description: 'ID de la plantilla.'
example: 1
required: true
schema:
type: integer
/email-settings:
get:
summary: 'Consultar la configuración de email del contribuyente.'
operationId: consultarLaConfiguracinDeEmailDelContribuyente
description: "Retorna la configuración de envío automático de comprobantes por\ncorreo electrónico. Si el contribuyente nunca ha configurado estos\nvalores, se retornan los valores por defecto que cumplen con la\nobligación de entrega del art. 18 del Reglamento: envío automático\nactivado, XML firmado y PDF adjuntos."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Valores por defecto (nunca configurado)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
auto_send:
type: boolean
example: true
description: '`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:
type: boolean
example: true
description: '`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:
type: boolean
example: true
description: '`true` si se adjunta el XML firmado del comprobante al correo. Art. 18 exige entregar "el comprobante electrónico" (= XML).'
attach_pdf:
type: boolean
example: true
description: '`true` si se adjunta la representación gráfica PDF al correo. Resolución art. 5 exige "representación gráfica".'
reply_to:
type: string
example: null
description: 'Correo al que llegarán las respuestas del receptor. `null` si no se configuró (usa el default del sistema).'
bcc:
type: array
example: []
description: '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:
type: string
example: null
description: 'Asunto personalizado del correo. Soporta placeholders: `{tipo}`, `{consecutivo}`, `{clave}`, `{receptor}`, `{total}`, `{moneda}`, `{emisor}`. `null` usa el asunto por defecto del sistema.'
custom_message:
type: string
example: null
description: 'Mensaje personalizado que aparece antes de los datos del comprobante en el cuerpo del correo. `null` usa el mensaje por defecto.'
message:
type: string
example: 'Configuración de email del contribuyente.'
errors:
type: string
example: null
nullable: true
-
description: 'Configuración personalizada'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
auto_send:
type: boolean
example: true
description: '`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:
type: boolean
example: true
description: '`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:
type: boolean
example: true
description: '`true` si se adjunta el XML firmado del comprobante al correo. Art. 18 exige entregar "el comprobante electrónico" (= XML).'
attach_pdf:
type: boolean
example: true
description: '`true` si se adjunta la representación gráfica PDF al correo. Resolución art. 5 exige "representación gráfica".'
reply_to:
type: string
example: facturas@miempresa.cr
description: 'Correo al que llegarán las respuestas del receptor. `null` si no se configuró (usa el default del sistema).'
bcc:
type: array
example:
- contabilidad@miempresa.cr
description: '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ó.'
items:
type: string
custom_subject:
type: string
example: '{emisor} — {tipo} {consecutivo}'
description: 'Asunto personalizado del correo. Soporta placeholders: `{tipo}`, `{consecutivo}`, `{clave}`, `{receptor}`, `{total}`, `{moneda}`, `{emisor}`. `null` usa el asunto por defecto del sistema.'
custom_message:
type: string
example: 'Adjunto su comprobante electrónico.'
description: 'Mensaje personalizado que aparece antes de los datos del comprobante en el cuerpo del correo. `null` usa el mensaje por defecto.'
message:
type: string
example: 'Configuración de email del contribuyente.'
errors:
type: string
example: null
nullable: true
tags:
- 'Configuración Email'
put:
summary: 'Actualizar la configuración de email del contribuyente.'
operationId: actualizarLaConfiguracinDeEmailDelContribuyente
description: "Soporta actualización parcial: envíe únicamente los campos que\ndesea modificar. Los campos omitidos conservan su valor actual\n(o el valor por defecto si nunca fueron configurados).\n\n**Advertencia normativa:** si desactiva `attach_xml` o `attach_pdf`,\nel contribuyente asume la responsabilidad de entregar los documentos\nal receptor por otro medio (descarga desde API, portal web, impresión),\nconforme al art. 18 del Reglamento de Comprobantes Electrónicos.\n\n**Placeholders disponibles para `custom_subject`:**\n`{tipo}`, `{consecutivo}`, `{clave}`, `{receptor}`, `{total}`,\n`{moneda}`, `{emisor}`"
parameters: []
responses:
200:
description: 'Configuración actualizada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
auto_send:
type: boolean
example: true
send_on_accepted_only:
type: boolean
example: true
attach_xml:
type: boolean
example: true
attach_pdf:
type: boolean
example: true
reply_to:
type: string
example: facturas@miempresa.cr
bcc:
type: array
example:
- contabilidad@miempresa.cr
items:
type: string
custom_subject:
type: string
example: '{emisor} — {tipo} {consecutivo}'
custom_message:
type: string
example: 'Adjunto su comprobante electrónico.'
message:
type: string
example: 'Configuración de email actualizada correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: 'Error de validación'
content:
application/json:
schema:
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
reply_to:
type: array
example:
- 'El campo reply_to debe ser un correo electrónico válido.'
items:
type: string
bcc.0:
type: array
example:
- 'El campo bcc.0 debe ser un correo electrónico válido.'
items:
type: string
tags:
- 'Configuración Email'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
auto_send:
type: boolean
description: 'Envío automático al aceptar comprobante. Default true (art. 18 Reglamento obliga entrega).'
example: true
send_on_accepted_only:
type: boolean
description: 'Enviar solo cuando Hacienda acepta (true) o también al emitir (false). Default true.'
example: true
attach_xml:
type: boolean
description: 'Adjuntar XML firmado (XAdES-EPES). Default true (art. 18: "comprobante electrónico").'
example: true
attach_pdf:
type: boolean
description: 'Adjuntar PDF con QR. Default true (art. 18 + Resolución art. 5: "representación gráfica").'
example: true
reply_to:
type: string
description: 'Dirección reply-to personalizada. Las respuestas del receptor llegarán a este correo. validation.email validation.max.'
example: facturas@miempresa.cr
nullable: true
bcc:
type: array
description: 'validation.email validation.max.'
example:
- b
items:
type: string
custom_subject:
type: string
description: 'Asunto personalizado. Placeholders: {tipo}, {consecutivo}, {clave}, {receptor}, {total}, {moneda}, {emisor}. validation.max.'
example: '{emisor} — {tipo} {consecutivo}'
nullable: true
custom_message:
type: string
description: 'Mensaje personalizado en el cuerpo del email. Se muestra antes de los datos del comprobante. validation.max.'
example: 'Adjunto encontrará su comprobante electrónico.'
nullable: true
/clients:
get:
summary: 'Listar clientes del contribuyente'
operationId: listarClientesDelContribuyente
description: "Devuelve los clientes (receptores reutilizables) registrados por el\ncontribuyente, con soporte para búsqueda por texto, filtro por tipo\nde identificación y filtro por estado.\n\nPor defecto, el listado incluye **solo clientes activos** ordenados\nalfabéticamente por nombre. Para ver los inactivos use\n`is_active=false`, y para ver todos use `is_active=all`.\n\nLa búsqueda por texto (`q`) es case-insensitive y busca en:\nnombre legal, nombre comercial y número de cédula."
parameters:
-
in: query
name: q
description: 'Búsqueda por nombre, nombre comercial o número de cédula (mínimo 2 caracteres).'
example: Empresa
required: false
schema:
type: string
description: 'Búsqueda por nombre, nombre comercial o número de cédula (mínimo 2 caracteres).'
example: Empresa
-
in: query
name: id_type
description: 'Filtrar por tipo de identificación. Valores: `01`, `02`, `03`, `04`, `05`, `06`.'
example: '02'
required: false
schema:
type: string
description: 'Filtrar por tipo de identificación. Valores: `01`, `02`, `03`, `04`, `05`, `06`.'
example: '02'
-
in: query
name: is_active
description: 'Filtrar por estado. `true` = solo activos (default), `false` = solo inactivos, `all` = todos.'
example: 'true'
required: false
schema:
type: string
description: 'Filtrar por estado. `true` = solo activos (default), `false` = solo inactivos, `all` = todos.'
example: 'true'
-
in: query
name: per_page
description: 'Resultados por página (1-100). Default: 15.'
example: 15
required: false
schema:
type: integer
description: 'Resultados por página (1-100). Default: 15.'
example: 15
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Listado paginado'
type: object
example:
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: ...
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: string
example: 019d867d-0001-7288-8ece-fd64da756001
id_type:
type: string
example: '02'
id_type_label:
type: string
example: 'Cédula Jurídica'
id_number:
type: string
example: '3101123456'
name:
type: string
example: 'EMPRESA EJEMPLO S.A.'
commercial_name:
type: string
example: 'Ejemplo Shop'
location:
type: object
properties:
province:
type: integer
example: 1
canton:
type: string
example: '01'
district:
type: string
example: '01'
neighborhood:
type: string
example: 'San Pedro'
address:
type: string
example: '200 metros norte del parque central'
phone:
type: object
properties:
country_code:
type: integer
example: 506
number:
type: string
example: '22001234'
emails:
type: array
example:
- facturacion@ejemplo.cr
items:
type: string
default_activity_code:
type: string
example: '6121.0'
notes:
type: string
example: 'Cliente preferencial'
is_active:
type: boolean
example: true
has_location:
type: boolean
example: true
has_email:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-10T14:30:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 4
per_page:
type: integer
example: 15
total:
type: integer
example: 50
from:
type: integer
example: 1
to:
type: integer
example: 15
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: ...
-
description: 'Sin resultados'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 0
from:
type: string
example: null
nullable: true
to:
type: string
example: null
nullable: true
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- Clientes
post:
summary: 'Crear un nuevo cliente'
operationId: crearUnNuevoCliente
description: "Registra un cliente (receptor reutilizable) en el catálogo del\ncontribuyente. Los datos corresponden a los campos del receptor\ndefinidos en la normativa de comprobantes electrónicos.\n\n**Reglas de validación:**\n\n- `id_type` + `id_number` deben ser **únicos** por contribuyente.\n- `default_activity_code` debe estar en **formato Hacienda `XXXX.X`**\n (cuatro dígitos, punto, un dígito). Ejemplo: `6121.0`.\n- `emails` es un arreglo con máximo 4 correos válidos.\n- `phone` debe tener entre 4 y 20 dígitos.\n\nLos campos de ubicación (`province`, `canton`, `district`) son\ntodos opcionales individualmente, pero si envía uno debería\nenviar los tres para que la ubicación sea coherente."
parameters: []
responses:
201:
description: 'Cliente creado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0002-7288-8ece-fd64da756002
id_type:
type: string
example: '02'
id_type_label:
type: string
example: 'Cédula Jurídica'
id_number:
type: string
example: '3101123456'
name:
type: string
example: 'EMPRESA EJEMPLO S.A.'
commercial_name:
type: string
example: 'Ejemplo Shop'
location:
type: object
properties:
province:
type: integer
example: 1
canton:
type: string
example: '01'
district:
type: string
example: '01'
neighborhood:
type: string
example: 'San Pedro'
address:
type: string
example: '200 metros norte del parque central'
phone:
type: object
properties:
country_code:
type: integer
example: 506
number:
type: string
example: '22001234'
emails:
type: array
example:
- facturacion@ejemplo.cr
items:
type: string
default_activity_code:
type: string
example: '6121.0'
notes:
type: string
example: 'Cliente preferencial'
is_active:
type: boolean
example: true
has_location:
type: boolean
example: true
has_email:
type: boolean
example: true
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T10:00:00-06:00'
message:
type: string
example: 'Cliente creado correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Identificación duplicada'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
id_number:
type: array
example:
- 'Ya existe un cliente con este tipo y número de identificación.'
items:
type: string
-
description: 'Formato de actividad económica inválido'
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
default_activity_code:
type: array
example:
- 'Debe tener el formato XXXX.X (ejemplo: 6121.0).'
items:
type: string
-
description: 'Tipo de identificación inválido'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
id_type:
- 'El tipo seleccionado no es válido.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
id_type:
type: array
example:
- 'El tipo seleccionado no es válido.'
items:
type: string
-
description: 'Límite del plan alcanzado'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
plan:
type: array
example:
- 'Ha alcanzado el límite de clientes de su plan. Actualice a un plan superior.'
items:
type: string
tags:
- Clientes
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id_type:
type: string
description: 'Tipo de identificación. Valores permitidos: `01` (Física), `02` (Jurídica), `03` (DIMEX), `04` (NITE), `05` (Extranjero), `06` (No Contribuyente).'
example: '02'
id_number:
type: string
description: 'Número de identificación (9-20 dígitos, solo números).'
example: '3101123456'
name:
type: string
description: 'Nombre legal o razón social (3-100 caracteres).'
example: 'EMPRESA EJEMPLO S.A.'
commercial_name:
type: string
description: 'Nombre comercial (3-80 caracteres).'
example: 'Ejemplo Shop'
nullable: true
province:
type: integer
description: 'Código de provincia (1-7).'
example: 1
nullable: true
canton:
type: integer
description: 'Código de cantón (1-99).'
example: 1
nullable: true
district:
type: integer
description: 'Código de distrito (1-99).'
example: 1
nullable: true
neighborhood:
type: string
description: 'Barrio (5-50 caracteres).'
example: 'San Pedro'
nullable: true
address:
type: string
description: 'Señas exactas (5-300 caracteres).'
example: '200 metros norte del parque central'
nullable: true
phone_country_code:
type: integer
description: 'Código de país del teléfono. Default: 506.'
example: 506
nullable: true
phone:
type: string
description: 'Número de teléfono (4-20 dígitos, solo números).'
example: '22001234'
nullable: true
emails:
type: array
description: 'Correos del cliente (máximo 4).'
example:
- facturacion@ejemplo.cr
items:
type: string
default_activity_code:
type: string
description: 'Código CIIU por defecto en formato Hacienda `XXXX.X`. Usado como `CodigoActividadReceptor` al emitir FEC (tipo 08).'
example: '6121.0'
nullable: true
notes:
type: string
description: 'Notas internas (máximo 1000 caracteres). No aparecen en el comprobante.'
example: 'Cliente preferencial'
nullable: true
is_active:
type: boolean
description: 'Si el cliente está activo (puede usarse al emitir). Default: `true`.'
example: true
required:
- id_type
- id_number
- name
'/clients/{id}':
get:
summary: 'Consultar un cliente por UUID'
operationId: consultarUnClientePorUUID
description: "Devuelve el detalle completo del cliente indicado. Solo se retornan\nclientes que pertenecen a su contribuyente. Los clientes eliminados\n(soft delete) retornan **404** — no se pueden consultar."
parameters: []
responses:
200:
description: 'Cliente encontrado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0001-7288-8ece-fd64da756001
description: 'UUID del cliente. Úselo como `client_id` en `POST /vouchers` para resolver automáticamente los datos del receptor.'
id_type:
type: string
example: '02'
description: 'Tipo de identificación del receptor: `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE, `05`=Extranjero, `06`=No Contribuyente.'
id_type_label:
type: string
example: 'Cédula Jurídica'
description: 'Nombre legible del tipo de identificación en español.'
id_number:
type: string
example: '3101123456'
description: 'Número de identificación (9-20 dígitos). Mapea a `IdentificacionType/Numero` del XSD.'
name:
type: string
example: 'EMPRESA EJEMPLO S.A.'
description: 'Nombre legal o razón social (3-100 chars). Mapea a `ReceptorType/Nombre` del XSD.'
commercial_name:
type: string
example: 'Ejemplo Shop'
description: 'Nombre comercial del receptor. Mapea a `NombreComercial` del XSD. Puede ser `null`.'
location:
type: object
properties:
province:
type: integer
example: 1
description: 'Código de provincia (1-7). Mapea a `Provincia` del XSD.'
canton:
type: string
example: '01'
description: 'Código de cantón (2 dígitos). Mapea a `Canton` del XSD.'
district:
type: string
example: '01'
description: 'Código de distrito (2 dígitos). Mapea a `Distrito` del XSD.'
description: 'Ubicación del receptor (UbicacionType del XSD). `null` si no tiene ubicación costarricense configurada.'
neighborhood:
type: string
example: 'San Pedro'
description: 'Barrio (5-50 chars). Campo opcional dentro de la ubicación.'
address:
type: string
example: '200 metros norte del parque central'
description: 'Señas exactas. Mapea a `OtrasSenas` (max 250) si tiene ubicación CR, o `OtrasSenasExtranjero` (max 300) si no.'
phone:
type: object
properties:
country_code:
type: integer
example: 506
description: 'Código de país del teléfono (1-3 dígitos). Default: `506` (Costa Rica).'
number:
type: string
example: '22001234'
description: 'Número de teléfono (4-20 dígitos).'
description: 'Teléfono del receptor (TelefonoType del XSD). `null` si no tiene teléfono configurado.'
emails:
type: array
example:
- facturacion@ejemplo.cr
description: '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.'
items:
type: string
default_activity_code:
type: string
example: '6121.0'
description: 'Código CIIU en formato Hacienda `XXXX.X`. Usado como `CodigoActividadReceptor` al emitir FEC (tipo 08). `null` si no aplica.'
notes:
type: string
example: 'Cliente preferencial'
description: 'Notas internas libres del integrador. No aparecen en el comprobante electrónico.'
is_active:
type: boolean
example: true
description: '`true` si el cliente está activo y puede usarse al emitir. `false` si fue desactivado manualmente.'
has_location:
type: boolean
example: true
description: '`true` si tiene provincia, cantón y distrito configurados. Útil para saber si el receptor tendrá nodo `Ubicacion` en el XML.'
has_email:
type: boolean
example: true
description: '`true` si tiene al menos un correo. Útil para saber si se enviará email transaccional automático al emitir.'
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
description: 'Fecha de creación del registro (ISO 8601).'
updated_at:
type: string
example: '2026-04-10T14:30:00-06:00'
description: 'Fecha de última actualización del registro (ISO 8601).'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
404:
description: 'No encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- Clientes
put:
summary: 'Editar un cliente existente'
operationId: editarUnClienteExistente
description: "Admite **actualización parcial** — envíe únicamente los campos que\ndesea modificar. Los campos no enviados conservan su valor actual.\n\n> **Importante:** editar un cliente **no afecta** a los comprobantes\n> ya emitidos con él. Los datos del receptor se copian al\n> comprobante en el momento de la emisión y no cambian\n> retroactivamente (inmutabilidad post-emisión por normativa fiscal).\n\nSi cambia el `id_type` o `id_number`, el sistema verifica que la\nnueva combinación no esté en uso por otro cliente del mismo\ncontribuyente. Si ya existe, responde **HTTP 422**."
parameters: []
responses:
200:
description: 'Cliente actualizado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0001-7288-8ece-fd64da756001
id_type:
type: string
example: '02'
id_type_label:
type: string
example: 'Cédula Jurídica'
id_number:
type: string
example: '3101123456'
name:
type: string
example: 'Nombre Actualizado S.A.'
commercial_name:
type: string
example: 'Nuevo Nombre Comercial'
location:
type: object
properties:
province:
type: integer
example: 1
canton:
type: string
example: '01'
district:
type: string
example: '01'
neighborhood:
type: string
example: 'San Pedro'
address:
type: string
example: '200 metros norte del parque central'
phone:
type: object
properties:
country_code:
type: integer
example: 506
number:
type: string
example: '22001234'
emails:
type: array
example:
- facturacion@ejemplo.cr
- contabilidad@ejemplo.cr
items:
type: string
default_activity_code:
type: string
example: '6121.0'
notes:
type: string
example: 'Cliente preferencial — actualizado'
is_active:
type: boolean
example: true
has_location:
type: boolean
example: true
has_email:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T11:00:00-06:00'
message:
type: string
example: 'Cliente actualizado correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Identificación duplicada'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ya existe un cliente con este tipo y número de identificación.'
errors:
type: object
properties:
id_number:
type: array
example:
- 'Duplicado para este contribuyente.'
items:
type: string
-
description: 'Formato de actividad económica inválido'
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
default_activity_code:
type: array
example:
- 'Debe tener el formato XXXX.X (ejemplo: 6121.0).'
items:
type: string
tags:
- Clientes
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
id_type:
type: string
description: 'Tipo de identificación.'
example: '02'
id_number:
type: string
description: 'Número de identificación (9-20 dígitos).'
example: '3101123456'
name:
type: string
description: 'Nombre legal o razón social (3-100 chars).'
example: 'Nombre Actualizado S.A.'
commercial_name:
type: string
description: 'Nombre comercial (3-80 chars).'
example: 'Nuevo Nombre Comercial'
province:
type: integer
description: 'Código de provincia (1-7).'
example: 1
canton:
type: integer
description: 'Código de cantón (1-99).'
example: 1
district:
type: integer
description: 'Código de distrito (1-99).'
example: 1
neighborhood:
type: string
description: 'Barrio (5-50 chars).'
example: Escalante
address:
type: string
description: 'Señas (5-300 chars).'
example: 'Frente al parque'
phone_country_code:
type: integer
description: 'Código de país del teléfono.'
example: 506
phone:
type: string
description: 'Número de teléfono (4-20 dígitos).'
example: '22009999'
emails:
type: array
description: 'Correos (máx 4).'
example:
- facturacion@ejemplo.cr
- contabilidad@ejemplo.cr
items:
type: string
default_activity_code:
type: string
description: 'Código CIIU formato `XXXX.X`.'
example: '6121.0'
notes:
type: string
description: 'Notas internas (máx 1000 chars).'
example: 'Cliente preferencial — actualizado'
is_active:
type: boolean
description: 'Estado del cliente.'
example: true
delete:
summary: 'Eliminar un cliente'
operationId: eliminarUnCliente
description: "Realiza un **soft delete** del cliente. El registro queda marcado\ncomo eliminado pero no se borra físicamente — por eso puede:\n\n- **Crear un nuevo cliente** con la misma combinación `id_type` +\n `id_number` después de eliminar uno. La restricción de unicidad\n solo aplica a registros activos (no eliminados).\n- Los **comprobantes ya emitidos** con este cliente **no se ven\n afectados** — los datos del receptor se copiaron al comprobante\n al momento de la emisión (inmutabilidad post-emisión)."
parameters: []
responses:
200:
description: 'Cliente eliminado'
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Cliente eliminado correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente eliminado correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'No encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- Clientes
parameters:
-
in: path
name: id
description: 'UUID del cliente.'
example: 019d867d-0001-7288-8ece-fd64da756001
required: true
schema:
type: string
/items:
get:
summary: 'Listar productos y servicios del contribuyente'
operationId: listarProductosYServiciosDelContribuyente
description: "Devuelve los items (productos/servicios reutilizables) registrados\npor el contribuyente, con soporte para búsqueda por texto, filtro\npor código CABYS exacto y filtro por estado.\n\nPor defecto, el listado incluye **solo items activos** ordenados\nalfabéticamente por descripción. Para ver los inactivos use\n`is_active=false`.\n\nLa búsqueda por texto (`q`) es case-insensitive y busca en:\ndescripción, código interno (SKU) y código CABYS."
parameters:
-
in: query
name: q
description: 'Búsqueda por descripción, código interno o código CABYS (mínimo 2 caracteres).'
example: consultoría
required: false
schema:
type: string
description: 'Búsqueda por descripción, código interno o código CABYS (mínimo 2 caracteres).'
example: consultoría
-
in: query
name: cabys_code
description: 'Filtrar por código CABYS exacto (13 dígitos).'
example: '4233201000000'
required: false
schema:
type: string
description: 'Filtrar por código CABYS exacto (13 dígitos).'
example: '4233201000000'
-
in: query
name: is_active
description: 'Filtrar por estado. `true` = solo activos (default), `false` = solo inactivos.'
example: 'true'
required: false
schema:
type: string
description: 'Filtrar por estado. `true` = solo activos (default), `false` = solo inactivos.'
example: 'true'
-
in: query
name: per_page
description: 'Resultados por página (1-100). Default: 15.'
example: 15
required: false
schema:
type: integer
description: 'Resultados por página (1-100). Default: 15.'
example: 15
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Listado paginado'
type: object
example:
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: ...
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: string
example: 019d867d-0010-7288-8ece-fd64da756010
cabys_code:
type: string
example: '4233201000000'
internal_code:
type: string
example: SKU-001
description:
type: string
example: 'Servicio de consultoría profesional'
unit_of_measure:
type: string
example: Sp
unit_of_measure_label:
type: string
example: 'Servicios Profesionales'
commercial_unit:
type: string
example: null
nullable: true
unit_price:
type: string
example: '50000.00000'
tax:
type: object
properties:
code:
type: string
example: '01'
rate_code:
type: string
example: '08'
rate:
type: string
example: '13.00'
label:
type: string
example: 'IVA 13%'
is_mercancia:
type: boolean
example: false
is_servicio:
type: boolean
example: true
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-10T14:30:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 2
per_page:
type: integer
example: 15
total:
type: integer
example: 30
from:
type: integer
example: 1
to:
type: integer
example: 15
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: ...
-
description: 'Sin resultados'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 0
from:
type: string
example: null
nullable: true
to:
type: string
example: null
nullable: true
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- 'Items / Productos'
post:
summary: 'Crear un nuevo producto o servicio'
operationId: crearUnNuevoProductoOServicio
description: "Registra un item reutilizable en el catálogo del contribuyente. Los\ndatos corresponden a los campos de una línea de detalle del\ncomprobante.\n\n**Reglas de validación:**\n\n- `cabys_code` es **obligatorio** y debe existir en el catálogo\n CABYS vigente (13 dígitos).\n- `internal_code` es opcional, pero si se envía debe ser **único**\n por contribuyente.\n- Si `tax_code` es `01` (IVA) o `07` (IVA cálculo especial),\n `tax_rate_code` es obligatorio.\n- Si `tax_code` se envía, `tax_rate` también es obligatorio.\n- Si `unit_of_measure` es `Otros`, `commercial_unit` es obligatorio."
parameters: []
responses:
201:
description: 'Item creado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0011-7288-8ece-fd64da756011
cabys_code:
type: string
example: '4233201000000'
internal_code:
type: string
example: SKU-001
description:
type: string
example: 'Servicio de consultoría profesional'
unit_of_measure:
type: string
example: Sp
unit_of_measure_label:
type: string
example: 'Servicios Profesionales'
commercial_unit:
type: string
example: null
nullable: true
unit_price:
type: string
example: '50000.00000'
tax:
type: object
properties:
code:
type: string
example: '01'
rate_code:
type: string
example: '08'
rate:
type: string
example: '13.00'
label:
type: string
example: 'IVA 13%'
is_mercancia:
type: boolean
example: false
is_servicio:
type: boolean
example: true
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T10:00:00-06:00'
message:
type: string
example: 'Item creado correctamente.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Código interno duplicado'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
internal_code:
- 'Ya existe un item con este código interno.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
internal_code:
type: array
example:
- 'Ya existe un item con este código interno.'
items:
type: string
-
description: 'Código CABYS no existe en el catálogo'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
cabys_code:
type: array
example:
- 'El código CABYS no existe en el catálogo vigente.'
items:
type: string
-
description: 'Falta tax_rate_code para IVA'
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07).'
errors:
type: object
properties:
tax_rate_code:
type: array
example:
- 'tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07).'
items:
type: string
-
description: "Unit_of_measure 'Otros' sin commercial_unit"
type: object
example:
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'."
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'."
errors:
type: object
properties:
commercial_unit:
type: array
example:
- "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'."
items:
type: string
-
description: 'Límite del plan alcanzado'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
plan:
type: array
example:
- 'Ha alcanzado el límite de items de su plan. Actualice a un plan superior.'
items:
type: string
tags:
- 'Items / Productos'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
cabys_code:
type: string
description: 'Código CABYS (13 dígitos exactos). Debe existir en el catálogo vigente.'
example: '4233201000000'
internal_code:
type: string
description: 'Código interno / SKU del producto (máximo 20 caracteres). Único por contribuyente.'
example: SKU-001
nullable: true
description:
type: string
description: 'Descripción del producto o servicio (3-200 caracteres).'
example: 'Servicio de consultoría profesional'
unit_of_measure:
type: string
description: 'Unidad de medida. Valores frecuentes: `Sp` (Servicios Profesionales), `Unid` (Unidades), `kg`, `l`, `m`, `m²`, `h`, `Al`, `Otros`. Si usa `Otros`, debe enviar `commercial_unit`.'
example: Sp
commercial_unit:
type: string
description: 'Unidad de medida comercial, obligatoria solo cuando `unit_of_measure` es `Otros` (máximo 20 caracteres).'
example: 'caja x 12'
nullable: true
unit_price:
type: number
description: 'Precio unitario (mayor a 0, hasta 5 decimales).'
example: 50000.0
tax_code:
type: string
description: 'Código del impuesto. Valores: `01` (IVA), `02` (ISC), `03` (Único combustibles), `04` (Específico bebidas alcohólicas), `05` (Específico bebidas envasadas), `06` (Específico tabaco), `07` (IVA cálculo especial), `08` (IVA bienes usados), `12` (Específico cemento), `99` (Otros).'
example: '01'
nullable: true
tax_rate_code:
type: string
description: 'Código de tarifa de IVA (obligatorio si `tax_code` es `01` o `07`). Valores: `01` (0% exento), `02` (1%), `03` (2%), `04` (4%), `05` (0.5%), `06` (4%), `07` (8%), `08` (13%), `09` (14%), `10` (0% exenta), `11` (0% no sujeta).'
example: '08'
nullable: true
tax_rate:
type: number
description: 'Tarifa efectiva en porcentaje (0-100, hasta 2 decimales).'
example: 13.0
nullable: true
is_active:
type: boolean
description: 'Si el item está activo. Default: `true`.'
example: true
required:
- cabys_code
- description
- unit_of_measure
- unit_price
'/items/{id}':
get:
summary: 'Consultar un item por UUID'
operationId: consultarUnItemPorUUID
description: "Devuelve el detalle completo del item indicado. Solo se retornan\nitems que pertenecen a su contribuyente. Los items eliminados\n(soft delete) retornan **404** — no se pueden consultar."
parameters: []
responses:
200:
description: 'Item encontrado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0010-7288-8ece-fd64da756010
description: '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:
type: string
example: '4233201000000'
description: 'Código CABYS de 13 dígitos exactos. Mapea a `CodigoCABYS` del XSD en cada `LineaDetalle`.'
internal_code:
type: string
example: SKU-001
description: '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:
type: string
example: 'Servicio de consultoría profesional'
description: 'Descripción del producto o servicio (3-200 chars). Mapea al campo `Detalle` de la `LineaDetalle` del XSD.'
unit_of_measure:
type: string
example: Sp
description: '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:
type: string
example: 'Servicios Profesionales'
description: 'Nombre legible de la unidad de medida en español.'
commercial_unit:
type: string
example: null
description: 'Unidad de medida comercial personalizada. Solo presente cuando `unit_of_measure` es `Otros` (ej. "caja x 12", "paquete"). `null` en caso contrario.'
unit_price:
type: string
example: '50000.00000'
description: 'Precio unitario con 5 decimales (Decimal 18,5). Mapea a `PrecioUnitario` del XSD.'
tax:
type: object
properties:
code:
type: string
example: '01'
description: 'Código del impuesto: `01`=IVA, `02`=ISC, `07`=IVA cálculo especial, etc. Mapea a `CodigoImpuesto` del XSD.'
rate_code:
type: string
example: '08'
description: '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:
type: string
example: '13.00'
description: 'Tarifa efectiva en porcentaje con 2 decimales (ej. `13.00`). Mapea a `Tarifa` del XSD.'
label:
type: string
example: 'IVA 13%'
description: 'Etiqueta legible del impuesto (ej. "IVA 13%", "IVA Exento", "ISC").'
description: 'Configuración simplificada de impuesto del item. `null` si el item no tiene impuesto configurado (exento sin código).'
is_mercancia:
type: boolean
example: false
description: '`true` si el código CABYS corresponde a mercancía. Afecta los totales `merc_gravadas`/`merc_exentas` del ResumenFactura.'
is_servicio:
type: boolean
example: true
description: '`true` si el código CABYS corresponde a servicio. Afecta los totales `serv_gravados`/`serv_exentos` del ResumenFactura.'
is_active:
type: boolean
example: true
description: '`true` si el item está activo y puede usarse al emitir. `false` si fue desactivado manualmente.'
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
description: 'Fecha de creación del registro (ISO 8601).'
updated_at:
type: string
example: '2026-04-10T14:30:00-06:00'
description: 'Fecha de última actualización del registro (ISO 8601).'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
404:
description: 'No encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Items / Productos'
put:
summary: 'Editar un item existente'
operationId: editarUnItemExistente
description: "Admite **actualización parcial** — envíe únicamente los campos que\ndesea modificar. Los campos no enviados conservan su valor actual.\n\n> **Importante:** editar un item **no afecta** a los comprobantes\n> ya emitidos con él. Los datos de la línea se copian al comprobante\n> en el momento de la emisión y no cambian retroactivamente\n> (inmutabilidad post-emisión por normativa fiscal).\n\n**Validaciones cruzadas que se aplican al editar:**\n\n- Si cambia `internal_code`, se verifica que no esté en uso por\n otro item del mismo contribuyente.\n- Si cambia `cabys_code`, se valida contra el catálogo CABYS vigente.\n- Si `tax_code` queda en `01` o `07` pero no hay `tax_rate_code`,\n la edición se rechaza.\n- Si `unit_of_measure` queda en `Otros` pero no hay `commercial_unit`,\n la edición se rechaza."
parameters: []
responses:
200:
description: 'Item actualizado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0010-7288-8ece-fd64da756010
cabys_code:
type: string
example: '4233201000000'
internal_code:
type: string
example: SKU-001-V2
description:
type: string
example: 'Servicio de consultoría profesional actualizado'
unit_of_measure:
type: string
example: Sp
unit_of_measure_label:
type: string
example: 'Servicios Profesionales'
commercial_unit:
type: string
example: null
nullable: true
unit_price:
type: string
example: '55000.00000'
tax:
type: object
properties:
code:
type: string
example: '01'
rate_code:
type: string
example: '08'
rate:
type: string
example: '13.00'
label:
type: string
example: 'IVA 13%'
is_mercancia:
type: boolean
example: false
is_servicio:
type: boolean
example: true
is_active:
type: boolean
example: true
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
updated_at:
type: string
example: '2026-04-16T11:00:00-06:00'
message:
type: string
example: 'Item actualizado correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'Item no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Código interno duplicado'
type: object
example:
success: false
data: null
message: 'Ya existe un item con el código interno [SKU-001-V2].'
errors:
internal_code:
- 'Duplicado para este contribuyente.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ya existe un item con el código interno [SKU-001-V2].'
errors:
type: object
properties:
internal_code:
type: array
example:
- 'Duplicado para este contribuyente.'
items:
type: string
-
description: 'Código CABYS no existe'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El código CABYS [9999999999999] no existe en el catálogo vigente.'
errors:
type: object
properties:
cabys_code:
type: array
example:
- 'Código CABYS no encontrado.'
items:
type: string
-
description: 'Falta tax_rate_code para IVA'
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07).'
errors:
type: object
properties:
tax_rate_code:
type: array
example:
- 'tax_rate_code es obligatorio cuando tax_code es IVA (01) o IVA cálculo especial (07).'
items:
type: string
-
description: 'Falta tax_rate cuando hay tax_code'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'tax_rate es obligatorio cuando se especifica tax_code.'
errors:
type: object
properties:
tax_rate:
type: array
example:
- 'Envíe tax_rate junto con tax_code.'
items:
type: string
-
description: "unit_of_measure 'Otros' sin commercial_unit"
type: object
example:
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'."
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'."
errors:
type: object
properties:
commercial_unit:
type: array
example:
- "commercial_unit es obligatoria cuando unit_of_measure es 'Otros'."
items:
type: string
tags:
- 'Items / Productos'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
cabys_code:
type: string
description: 'Código CABYS (13 dígitos). Debe existir en el catálogo vigente.'
example: '4233201000000'
internal_code:
type: string
description: 'Código interno / SKU (máx 20 chars). Único por contribuyente.'
example: SKU-001-V2
description:
type: string
description: 'Descripción (3-200 chars).'
example: 'Servicio de consultoría profesional actualizado'
unit_of_measure:
type: string
description: 'Unidad de medida (ver lista en Guía).'
example: Sp
commercial_unit:
type: string
description: 'Unidad comercial (obligatoria si `unit_of_measure` es `Otros`).'
example: 'caja x 12'
unit_price:
type: number
description: 'Precio unitario (> 0, hasta 5 decimales).'
example: 55000.0
tax_code:
type: string
description: 'Código de impuesto (ver lista en Guía).'
example: '01'
tax_rate_code:
type: string
description: 'Código de tarifa IVA (obligatorio si `tax_code` es `01` o `07`).'
example: '08'
tax_rate:
type: number
description: 'Tarifa efectiva en % (0-100, 2 decimales).'
example: 13.0
is_active:
type: boolean
description: 'Estado del item.'
example: true
delete:
summary: 'Eliminar un item'
operationId: eliminarUnItem
description: "Realiza un **soft delete** del item. El registro queda marcado como\neliminado pero no se borra físicamente — por eso puede:\n\n- **Crear un nuevo item** con el mismo `internal_code` después de\n eliminar uno. La restricción de unicidad solo aplica a items\n activos (no eliminados).\n- Los **comprobantes ya emitidos** con este item **no se ven\n afectados** — los datos de la línea se copiaron al comprobante\n al momento de la emisión (inmutabilidad post-emisión)."
parameters: []
responses:
200:
description: 'Item eliminado'
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Item eliminado correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Item eliminado correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'No encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Recurso no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Recurso no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Items / Productos'
parameters:
-
in: path
name: id
description: 'UUID del item.'
example: 019d867d-0010-7288-8ece-fd64da756010
required: true
schema:
type: string
/webhooks:
get:
summary: 'Listar webhook endpoints registrados'
operationId: listarWebhookEndpointsRegistrados
description: "Devuelve todos los endpoints de su cuenta (activos e inactivos),\nordenados del más reciente al más antiguo. Cada endpoint incluye su\nestado de salud actual y los timestamps del último intento de entrega.\n\nEl campo `health_status` puede tomar los valores:\n- `healthy` — activo, sin fallos recientes.\n- `degraded` — activo pero con fallos consecutivos (entre 1 y 9).\n- `disabled` — desactivado automáticamente tras 10 fallos consecutivos.\n- `inactive` — desactivado manualmente.\n- `never_used` — activo, aún no se ha disparado ningún evento hacia él."
parameters:
-
in: query
name: is_active
description: 'Filtrar por estado. Sin filtro: muestra todos.'
example: true
required: false
schema:
type: boolean
description: 'Filtrar por estado. Sin filtro: muestra todos.'
example: true
-
in: query
name: per_page
description: 'Resultados por página (1-100).'
example: 15
required: false
schema:
type: integer
description: 'Resultados por página (1-100).'
example: 15
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: success
content:
application/json:
schema:
type: object
example:
success: true
data:
-
id: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url: 'https://api.miempresa.cr/webhooks/almendrofec'
secret: 'whsec_****************************ab3f'
events:
- voucher.accepted
- voucher.rejected
events_detail:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
-
value: voucher.rejected
label: 'Comprobante rechazado por Hacienda'
group: voucher
description: 'Webhook principal e-commerce'
is_active: true
health_status: healthy
consecutive_failures: 0
last_triggered_at: '2026-03-22T10:30:00-06:00'
last_success_at: '2026-03-22T10:30:00-06:00'
last_failure_at: null
last_failure_reason: null
created_at: '2026-03-20T08:00:00-06:00'
updated_at: '2026-03-22T10:30:00-06:00'
message: ''
errors: null
meta:
current_page: 1
total: 3
per_page: 15
last_page: 1
links:
first: ...
last: ...
prev: null
next: null
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
id: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url: 'https://api.miempresa.cr/webhooks/almendrofec'
secret: 'whsec_****************************ab3f'
events:
- voucher.accepted
- voucher.rejected
events_detail:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
-
value: voucher.rejected
label: 'Comprobante rechazado por Hacienda'
group: voucher
description: 'Webhook principal e-commerce'
is_active: true
health_status: healthy
consecutive_failures: 0
last_triggered_at: '2026-03-22T10:30:00-06:00'
last_success_at: '2026-03-22T10:30:00-06:00'
last_failure_at: null
last_failure_reason: null
created_at: '2026-03-20T08:00:00-06:00'
updated_at: '2026-03-22T10:30:00-06:00'
items:
type: object
properties:
id:
type: string
example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url:
type: string
example: 'https://api.miempresa.cr/webhooks/almendrofec'
secret:
type: string
example: 'whsec_****************************ab3f'
events:
type: array
example:
- voucher.accepted
- voucher.rejected
items:
type: string
events_detail:
type: array
example:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
-
value: voucher.rejected
label: 'Comprobante rechazado por Hacienda'
group: voucher
items:
type: object
properties:
value:
type: string
example: voucher.accepted
label:
type: string
example: 'Comprobante aceptado por Hacienda'
group:
type: string
example: voucher
description:
type: string
example: 'Webhook principal e-commerce'
is_active:
type: boolean
example: true
health_status:
type: string
example: healthy
consecutive_failures:
type: integer
example: 0
last_triggered_at:
type: string
example: '2026-03-22T10:30:00-06:00'
last_success_at:
type: string
example: '2026-03-22T10:30:00-06:00'
last_failure_at:
type: string
example: null
nullable: true
last_failure_reason:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-03-20T08:00:00-06:00'
updated_at:
type: string
example: '2026-03-22T10:30:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
total:
type: integer
example: 3
per_page:
type: integer
example: 15
last_page:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- Webhooks
post:
summary: 'Crear un webhook endpoint'
operationId: crearUnWebhookEndpoint
description: "Registra una URL HTTPS que recibirá notificaciones para los eventos\nque usted seleccione. El sistema genera automáticamente un **secret\núnico de 64 caracteres** que deberá usar para verificar la firma de\ncada entrega.\n\n> **Importante:** el secret completo se devuelve **una única vez**\n> en esta respuesta, en el campo `secret`. Almacénelo de forma segura\n> en ese momento — en consultas posteriores (`GET /webhooks/{id}`) solo\n> verá una versión enmascarada (`whsec_****...ab3f`). Para obtener un\n> nuevo secret deberá eliminar el endpoint y crear uno nuevo.\n\n**Consejos para la URL:**\n\n- Debe empezar con `https://` — no se acepta HTTP plano.\n- El certificado TLS debe ser válido (no auto-firmado).\n- La URL debe responder POST en menos de 15 segundos.\n- Evite URLs con query string — el path es suficiente."
parameters: []
responses:
201:
description: created
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url: 'https://api.miempresa.cr/webhooks/almendrofec'
secret: a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f6ab3f
events:
- voucher.accepted
- voucher.rejected
events_detail:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
-
value: voucher.rejected
label: 'Comprobante rechazado por Hacienda'
group: voucher
description: 'Webhook principal e-commerce'
is_active: true
health_status: never_used
consecutive_failures: 0
last_triggered_at: null
last_success_at: null
last_failure_at: null
last_failure_reason: null
created_at: '2026-03-22T10:30:00-06:00'
updated_at: '2026-03-22T10:30:00-06:00'
message: 'Webhook endpoint creado. Almacene el secret — no se mostrará de nuevo.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
description: 'UUID del webhook endpoint. Úselo en `PUT`, `DELETE` y `GET /webhooks/{id}/logs`.'
url:
type: string
example: 'https://api.miempresa.cr/webhooks/almendrofec'
description: 'URL HTTPS registrada que recibirá los POST de cada evento.'
secret:
type: string
example: a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f6ab3f
description: '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:
type: array
example:
- voucher.accepted
- voucher.rejected
description: 'Códigos de los eventos suscritos. Valores posibles: `voucher.sent`, `voucher.accepted`, `voucher.rejected`, `receiver.confirmed`, `receiver.deadline`, `certificate.expiring`.'
items:
type: string
events_detail:
type: array
example:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
-
value: voucher.rejected
label: 'Comprobante rechazado por Hacienda'
group: voucher
description: '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`).'
items:
type: object
properties:
value:
type: string
example: voucher.accepted
label:
type: string
example: 'Comprobante aceptado por Hacienda'
group:
type: string
example: voucher
description:
type: string
example: 'Webhook principal e-commerce'
description: 'Descripción libre del endpoint para identificarlo en listados. Puede ser `null`.'
is_active:
type: boolean
example: true
description: '`true` si el endpoint está activo y recibirá entregas. `false` si fue desactivado manualmente o automáticamente tras 10 fallos consecutivos.'
health_status:
type: string
example: never_used
description: '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:
type: integer
example: 0
description: '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:
type: string
example: null
description: 'Fecha/hora del último intento de entrega (ISO 8601). `null` si nunca se disparó.'
last_success_at:
type: string
example: null
description: 'Fecha/hora de la última entrega exitosa — HTTP 2xx (ISO 8601). `null` si nunca hubo éxito.'
last_failure_at:
type: string
example: null
description: 'Fecha/hora del último fallo de entrega (ISO 8601). `null` si nunca falló.'
last_failure_reason:
type: string
example: null
description: 'Razón del último fallo para debugging rápido. Ejemplos: `HTTP 503 Service Unavailable`, `Connection timeout 15s`. `null` si nunca falló.'
created_at:
type: string
example: '2026-03-22T10:30:00-06:00'
description: 'Fecha de creación del endpoint (ISO 8601).'
updated_at:
type: string
example: '2026-03-22T10:30:00-06:00'
description: 'Fecha de última actualización del endpoint (ISO 8601).'
message:
type: string
example: 'Webhook endpoint creado. Almacene el secret — no se mostrará de nuevo.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: validation_error
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
url:
type: array
example:
- 'La URL debe usar HTTPS.'
items:
type: string
events.0:
type: array
example:
- 'El evento seleccionado no es válido.'
items:
type: string
-
description: plan_restriction
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
plan:
type: array
example:
- 'Su plan actual no incluye webhooks. Actualice su plan para usar esta funcionalidad.'
items:
type: string
-
description: limit_reached
type: object
example:
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).'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
plan:
type: array
example:
- 'Ha alcanzado el límite de webhooks de su plan (3 endpoints).'
items:
type: string
tags:
- Webhooks
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
description: 'URL HTTPS del endpoint receptor.'
example: 'https://api.miempresa.cr/webhooks/almendrofec'
events:
type: array
description: 'Lista de eventos a suscribir. Valores permitidos: voucher.sent, voucher.accepted, voucher.rejected, receiver.confirmed, receiver.deadline, certificate.expiring.'
example:
- voucher.accepted
- voucher.rejected
items:
type: string
description:
type: string
description: 'Descripción opcional para identificar el endpoint (máx 255).'
example: 'Webhook principal e-commerce'
nullable: true
is_active:
type: boolean
description: 'Estado del endpoint. Solo disponible en PUT para reactivar endpoints desactivados. Al reactivar (true), se resetean los fallos consecutivos.'
example: true
required:
- url
- events
'/webhooks/{id}':
get:
summary: 'Consultar un webhook endpoint'
operationId: consultarUnWebhookEndpoint
description: "Devuelve el detalle completo de un endpoint: su URL, eventos\nsuscritos, estado de salud actual, fallos consecutivos acumulados\ny timestamps de la última entrega (exitosa y fallida).\n\nEl campo `secret` se devuelve siempre enmascarado en este endpoint\n(`whsec_****...ab3f`), revelando solo los últimos 4 caracteres para\npermitir identificarlo visualmente. Si extravió el secret original,\ndeberá eliminar el endpoint y crear uno nuevo."
parameters: []
responses:
200:
description: found
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url: 'https://api.miempresa.cr/webhooks/almendrofec'
secret: 'whsec_****************************ab3f'
events:
- voucher.accepted
- voucher.rejected
events_detail:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
description: 'Webhook principal e-commerce'
is_active: true
health_status: healthy
consecutive_failures: 0
last_triggered_at: '2026-03-22T10:30:00-06:00'
last_success_at: '2026-03-22T10:30:00-06:00'
last_failure_at: null
last_failure_reason: null
created_at: '2026-03-20T08:00:00-06:00'
updated_at: '2026-03-22T10:30:00-06:00'
message: ''
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url:
type: string
example: 'https://api.miempresa.cr/webhooks/almendrofec'
secret:
type: string
example: 'whsec_****************************ab3f'
events:
type: array
example:
- voucher.accepted
- voucher.rejected
items:
type: string
events_detail:
type: array
example:
-
value: voucher.accepted
label: 'Comprobante aceptado por Hacienda'
group: voucher
items:
type: object
properties:
value:
type: string
example: voucher.accepted
label:
type: string
example: 'Comprobante aceptado por Hacienda'
group:
type: string
example: voucher
description:
type: string
example: 'Webhook principal e-commerce'
is_active:
type: boolean
example: true
health_status:
type: string
example: healthy
consecutive_failures:
type: integer
example: 0
last_triggered_at:
type: string
example: '2026-03-22T10:30:00-06:00'
last_success_at:
type: string
example: '2026-03-22T10:30:00-06:00'
last_failure_at:
type: string
example: null
nullable: true
last_failure_reason:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-03-20T08:00:00-06:00'
updated_at:
type: string
example: '2026-03-22T10:30:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
404:
description: not_found
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El recurso solicitado no existe.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El recurso solicitado no existe.'
errors:
type: string
example: null
nullable: true
tags:
- Webhooks
put:
summary: 'Editar un webhook endpoint'
operationId: editarUnWebhookEndpoint
description: "Admite actualización parcial — envíe solo los campos que desee\nmodificar. Los campos no enviados conservan su valor actual.\n\n**Reactivación de endpoints desactivados automáticamente:** si un\nendpoint fue desactivado por acumular 10 fallos consecutivos, puede\nreactivarlo enviando `is_active = true`. Esto reinicia el contador de\nfallos a 0 y vuelve a incluir el endpoint en futuras entregas.\nAsegúrese antes de corregir el problema que causó los fallos.\n\n**Campos editables:**\n\n- `url` — nueva URL HTTPS del receptor.\n- `events` — nueva lista de eventos a suscribir.\n- `description` — descripción opcional.\n- `is_active` — activar / desactivar el endpoint.\n\n> El secret **no es editable**. Si necesita rotarlo (por ejemplo,\n> tras una sospecha de compromiso), elimine el endpoint actual y\n> cree uno nuevo — recibirá un secret nuevo en la respuesta."
parameters: []
responses:
200:
description: updated
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
url:
type: string
example: 'https://api.miempresa.cr/webhooks/v2'
secret:
type: string
example: 'whsec_****************************ab3f'
events:
type: array
example:
- voucher.accepted
- voucher.rejected
- voucher.sent
items:
type: string
description:
type: string
example: 'Webhook e-commerce v2'
is_active:
type: boolean
example: true
health_status:
type: string
example: never_used
consecutive_failures:
type: integer
example: 0
last_triggered_at:
type: string
example: '2026-03-22T10:30:00-06:00'
last_success_at:
type: string
example: '2026-03-22T10:30:00-06:00'
last_failure_at:
type: string
example: null
nullable: true
last_failure_reason:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-03-20T08:00:00-06:00'
updated_at:
type: string
example: '2026-03-25T15:45:00-06:00'
message:
type: string
example: 'Webhook endpoint actualizado correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: not_found
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El recurso solicitado no existe.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El recurso solicitado no existe.'
errors:
type: string
example: null
nullable: true
tags:
- Webhooks
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
url:
type: string
description: 'URL HTTPS del endpoint receptor.'
example: 'https://api.miempresa.cr/webhooks/v2'
events:
type: array
description: 'Lista de eventos a suscribir.'
example:
- voucher.accepted
- voucher.rejected
- voucher.sent
items:
type: string
description:
type: string
description: 'Descripción opcional (máx 255).'
example: 'Webhook e-commerce v2'
nullable: true
is_active:
type: boolean
description: 'Activar o desactivar el endpoint.'
example: true
delete:
summary: 'Eliminar un webhook endpoint'
operationId: eliminarUnWebhookEndpoint
description: "Elimina el endpoint y deja de recibir notificaciones de todos sus\neventos. Los logs históricos de entregas se conservan durante el\nperíodo de retención para consulta y auditoría — eliminar el\nendpoint no borra sus logs.\n\nTras la eliminación, puede crear un endpoint nuevo con la misma URL\nsi lo desea. Recibirá un secret nuevo en la respuesta de creación."
parameters: []
responses:
200:
description: deleted
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Webhook endpoint eliminado correctamente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Webhook endpoint eliminado correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: not_found
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El recurso solicitado no existe.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El recurso solicitado no existe.'
errors:
type: string
example: null
nullable: true
tags:
- Webhooks
parameters:
-
in: path
name: id
description: 'UUID del webhook endpoint.'
example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
required: true
schema:
type: string
'/webhooks/{id}/logs':
get:
summary: 'Consultar el historial de entregas de un endpoint'
operationId: consultarElHistorialDeEntregasDeUnEndpoint
description: "Devuelve el registro de cada intento de entrega hacia el endpoint:\nevento disparado, ID de entrega (útil para deduplicar), resultado\n(éxito o fallo), código HTTP devuelto por su servidor y tiempo de\nrespuesta en milisegundos.\n\nLos logs se conservan durante **3 meses**. Útil para:\n\n- Verificar que un evento específico fue entregado.\n- Diagnosticar fallos: qué HTTP code devolvió su endpoint.\n- Medir latencia: campo `response_time_ms`.\n- Auditoría: trazabilidad completa de notificaciones enviadas.\n\nLos campos voluminosos (`payload` enviado, `response_body` recibido)\nse excluyen del listado por razones de volumen. Si necesita esa\ninformación para una entrega específica, contacte a soporte\nindicando el `delivery_id`.\n\n**Interpretando el campo `status`:**\n\n- `success` — el endpoint respondió con HTTP 2xx.\n- `failed` — el endpoint respondió con 4xx/5xx, o hubo timeout/error de red.\n\nCuando una entrega tiene reintentos, cada intento queda registrado\ncomo una fila separada con el mismo `delivery_id` pero distinto\nvalor de `attempt` (1, 2, 3 o 4)."
parameters:
-
in: query
name: event
description: 'Filtrar por tipo de evento.'
example: voucher.accepted
required: false
schema:
type: string
description: 'Filtrar por tipo de evento.'
example: voucher.accepted
-
in: query
name: status
description: 'Filtrar por resultado: success o failed.'
example: failed
required: false
schema:
type: string
description: 'Filtrar por resultado: success o failed.'
example: failed
-
in: query
name: per_page
description: 'Resultados por página (1-100).'
example: 25
required: false
schema:
type: integer
description: 'Resultados por página (1-100).'
example: 25
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: success
content:
application/json:
schema:
type: object
example:
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: ...
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: integer
example: 12345
event:
type: string
example: voucher.accepted
event_label:
type: string
example: 'Comprobante aceptado por Hacienda'
delivery_id:
type: string
example: 550e8400-e29b-41d4-a716-446655440000
attempt:
type: integer
example: 1
status:
type: string
example: success
response_status:
type: integer
example: 200
response_time_ms:
type: integer
example: 145
response_time_formatted:
type: string
example: 145ms
error_message:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-03-22T10:30:05-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
total:
type: integer
example: 87
per_page:
type: integer
example: 25
last_page:
type: integer
example: 4
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: ...
404:
description: endpoint_not_found
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El recurso solicitado no existe.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El recurso solicitado no existe.'
errors:
type: string
example: null
nullable: true
tags:
- Webhooks
parameters:
-
in: path
name: id
description: 'UUID del webhook endpoint.'
example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a
required: true
schema:
type: string
/reports/summary:
get:
summary: 'Resumen del dashboard.'
operationId: resumenDelDashboard
description: "Retorna conteos por estado Hacienda, montos agregados (venta, descuentos,\nimpuestos, total comprobante) y desglose por tipo de comprobante para\nel periodo solicitado.\n\nLos datos son base para la preparacion de la declaracion D-104\n(IVA mensual) y D-101 (renta anual).\n\n---\n\n### Modo Integrador\n\nCuando el token pertenece a un integrador, este endpoint agrega\nautomaticamente los comprobantes de **todos sus clientes gestionados**.\nLa respuesta incluye adicionalmente el campo `by_contributor` con el\ndesglose por cliente, ordenado de mayor a menor uso.\n\nPara filtrar solo un cliente especifico, use `?managed_contributor_id={uuid}`."
parameters:
-
in: query
name: date_from
description: 'Inicio del periodo (YYYY-MM-DD). Default: primer dia del mes actual.'
example: '2026-04-01'
required: false
schema:
type: string
description: 'Inicio del periodo (YYYY-MM-DD). Default: primer dia del mes actual.'
example: '2026-04-01'
-
in: query
name: date_to
description: 'Fin del periodo (YYYY-MM-DD). Default: hoy.'
example: '2026-04-30'
required: false
schema:
type: string
description: 'Fin del periodo (YYYY-MM-DD). Default: hoy.'
example: '2026-04-30'
-
in: query
name: voucher_type
description: 'CSV de tipos. `01`,`02`,`03`,`04`,`08`,`09`,`10`.'
example: '01,04'
required: false
schema:
type: string
description: 'CSV de tipos. `01`,`02`,`03`,`04`,`08`,`09`,`10`.'
example: '01,04'
-
in: query
name: status
description: 'CSV de estados. Default: `accepted`.'
example: 'accepted,rejected'
required: false
schema:
type: string
description: 'CSV de estados. Default: `accepted`.'
example: 'accepted,rejected'
-
in: query
name: currency_code
description: 'ISO 4217. Default: `CRC`.'
example: CRC
required: false
schema:
type: string
description: 'ISO 4217. Default: `CRC`.'
example: CRC
-
in: query
name: environment
description: '`production` o `sandbox`. Default: `production`.'
example: production
required: false
schema:
type: string
description: '`production` o `sandbox`. Default: `production`.'
example: production
-
in: query
name: receiver_id_number
description: 'Cedula del receptor para filtrar.'
example: '3101000001'
required: false
schema:
type: string
description: 'Cedula del receptor para filtrar.'
example: '3101000001'
-
in: query
name: sale_condition
description: 'CSV de condiciones de venta.'
example: '01,02'
required: false
schema:
type: string
description: 'CSV de condiciones de venta.'
example: '01,02'
-
in: query
name: payment_method
description: 'CSV de medios de pago.'
example: '01,02'
required: false
schema:
type: string
description: 'CSV de medios de pago.'
example: '01,02'
-
in: query
name: group_by
description: 'Agrupación temporal (solo sales-by-period). Valores: day, week, month. Auto-detectado según rango.'
example: month
required: false
schema:
type: string
description: 'Agrupación temporal (solo sales-by-period). Valores: day, week, month. Auto-detectado según rango.'
example: month
enum:
- day
- week
- month
-
in: query
name: limit
description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. validation.min validation.max.'
example: 10
required: false
schema:
type: integer
description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. validation.min validation.max.'
example: 10
-
in: query
name: managed_contributor_id
description: "UUID de un cliente gestionado.\n Solo para integradores. Filtra el resumen para mostrar unicamente\n los comprobantes de ese cliente. Sin este parametro se agregan todos."
example: 019d867d-0241-7288-8ece-fd64da75616d
required: false
schema:
type: string
description: "UUID de un cliente gestionado.\n Solo para integradores. Filtra el resumen para mostrar unicamente\n los comprobantes de ese cliente. Sin este parametro se agregan todos."
example: 019d867d-0241-7288-8ece-fd64da75616d
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Contribuyente normal'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
period:
type: object
properties:
date_from:
type: string
example: '2026-04-01'
description: 'Inicio del rango (YYYY-MM-DD). Default: primer día del mes actual.'
date_to:
type: string
example: '2026-04-30'
description: 'Fin del rango (YYYY-MM-DD). Default: hoy.'
currency_code:
type: string
example: CRC
description: 'Moneda ISO 4217 del reporte. Default: `CRC`.'
environment:
type: string
example: production
description: 'Ambiente consultado: `production` o `sandbox`.'
description: 'Parámetros del periodo consultado.'
counts:
type: object
properties:
total:
type: integer
example: 150
description: 'Total de comprobantes en el rango (todos los estados).'
accepted:
type: integer
example: 140
description: 'Comprobantes aceptados por Hacienda (Mensaje=1).'
rejected:
type: integer
example: 5
description: 'Comprobantes rechazados por Hacienda (Mensaje=3).'
sent:
type: integer
example: 3
description: 'Comprobantes enviados, esperando respuesta de Hacienda.'
error:
type: integer
example: 2
description: 'Comprobantes con error temporal (se reintentan automáticamente).'
cancelled:
type: integer
example: 0
description: 'Comprobantes anulados mediante Nota de Crédito.'
draft:
type: integer
example: 0
description: 'Comprobantes en borrador (no firmados aún).'
pending:
type: integer
example: 0
description: 'Comprobantes firmados, encolados para envío.'
acceptance_rate:
type: number
example: 96.55
description: 'Porcentaje de aceptación: `accepted / (accepted + rejected) * 100`. `0` si no hay comprobantes resueltos.'
description: 'Conteos de comprobantes por estado en el periodo.'
totals:
type: object
properties:
total_venta:
type: string
example: '15000000.00000'
description: 'Suma de TotalVenta de todos los comprobantes.'
total_descuentos:
type: string
example: '500000.00000'
description: 'Suma de TotalDescuentos.'
total_venta_neta:
type: string
example: '14500000.00000'
description: 'TotalVenta − TotalDescuentos.'
total_impuesto:
type: string
example: '1885000.00000'
description: 'Suma de todos los impuestos (IVA, selectivo, etc.).'
total_otros_cargos:
type: string
example: '0.00000'
description: 'Suma de otros cargos adicionales.'
total_comprobante:
type: string
example: '16385000.00000'
description: 'Monto final agregado de todos los comprobantes.'
average_per_voucher:
type: string
example: '117035.71429'
description: 'Promedio por comprobante: `total_comprobante / count`.'
description: 'Montos agregados del ResumenFactura (Decimal 18,5). Solo incluye comprobantes en los estados filtrados (default: `accepted`).'
by_type:
type: array
example:
-
voucher_type: '01'
voucher_type_label: 'Factura Electrónica'
count: 100
total_venta: '12000000.00000'
total_impuesto: '1560000.00000'
total_comprobante: '13560000.00000'
description: 'Desglose por tipo de comprobante. Cada objeto agrupa un tipo (01-10).'
items:
type: object
properties:
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
count:
type: integer
example: 100
total_venta:
type: string
example: '12000000.00000'
total_impuesto:
type: string
example: '1560000.00000'
total_comprobante:
type: string
example: '13560000.00000'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
-
description: 'Integrador (incluye desglose por cliente)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
period:
type: object
properties:
date_from:
type: string
example: '2026-04-01'
description: 'Inicio del rango (YYYY-MM-DD). Default: primer día del mes actual.'
date_to:
type: string
example: '2026-04-30'
description: 'Fin del rango (YYYY-MM-DD). Default: hoy.'
currency_code:
type: string
example: CRC
description: 'Moneda ISO 4217 del reporte. Default: `CRC`.'
environment:
type: string
example: production
description: 'Ambiente consultado: `production` o `sandbox`.'
description: 'Parámetros del periodo consultado.'
counts:
type: object
properties:
total:
type: integer
example: 8500
description: 'Total de comprobantes en el rango (todos los estados).'
accepted:
type: integer
example: 8200
description: 'Comprobantes aceptados por Hacienda (Mensaje=1).'
rejected:
type: integer
example: 180
description: 'Comprobantes rechazados por Hacienda (Mensaje=3).'
sent:
type: integer
example: 70
description: 'Comprobantes enviados, esperando respuesta de Hacienda.'
error:
type: integer
example: 50
description: 'Comprobantes con error temporal (se reintentan automáticamente).'
cancelled:
type: integer
example: 0
description: 'Comprobantes anulados mediante Nota de Crédito.'
draft:
type: integer
example: 0
description: 'Comprobantes en borrador (no firmados aún).'
pending:
type: integer
example: 0
description: 'Comprobantes firmados, encolados para envío.'
acceptance_rate:
type: number
example: 97.85
description: 'Porcentaje de aceptación: `accepted / (accepted + rejected) * 100`. `0` si no hay comprobantes resueltos.'
description: 'Conteos de comprobantes por estado en el periodo.'
totals:
type: object
properties:
total_venta:
type: string
example: '850000000.00000'
description: 'Suma de TotalVenta de todos los comprobantes.'
total_descuentos:
type: string
example: '0.00000'
description: 'Suma de TotalDescuentos.'
total_venta_neta:
type: string
example: '850000000.00000'
description: 'TotalVenta − TotalDescuentos.'
total_impuesto:
type: string
example: '110500000.00000'
description: 'Suma de todos los impuestos (IVA, selectivo, etc.).'
total_otros_cargos:
type: string
example: '0.00000'
description: 'Suma de otros cargos adicionales.'
total_comprobante:
type: string
example: '960500000.00000'
description: 'Monto final agregado de todos los comprobantes.'
average_per_voucher:
type: string
example: '117134.14634'
description: 'Promedio por comprobante: `total_comprobante / count`.'
description: 'Montos agregados del ResumenFactura (Decimal 18,5). Solo incluye comprobantes en los estados filtrados (default: `accepted`).'
by_type:
type: array
example:
-
voucher_type: '01'
voucher_type_label: 'Factura Electrónica'
count: 7200
total_venta: '720000000.00000'
total_impuesto: '93600000.00000'
total_comprobante: '813600000.00000'
description: 'Desglose por tipo de comprobante. Cada objeto agrupa un tipo (01-10).'
items:
type: object
properties:
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
count:
type: integer
example: 7200
total_venta:
type: string
example: '720000000.00000'
total_impuesto:
type: string
example: '93600000.00000'
total_comprobante:
type: string
example: '813600000.00000'
by_contributor:
type: array
example:
-
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'
description: 'Desglose por cliente gestionado. **Solo presente en modo integrador.** Ordenado de mayor a menor uso.'
items:
type: object
properties:
contributor_id:
type: string
example: 019d867d-a001-7288-8ece-fd64da756a01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
count:
type: integer
example: 5200
total_comprobante:
type: string
example: '621000000.00000'
total_impuesto:
type: string
example: '80730000.00000'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
tags:
- Reportes
/catalogs/cabys:
get:
summary: 'Buscar en el catalogo CABYS (Bienes y Servicios).'
operationId: buscarEnElCatalogoCABYSBienesYServicios
description: "Busqueda paginada en el catalogo oficial de bienes y servicios\nde Hacienda (~20,501 codigos). El codigo CABYS es obligatorio\nen todos los comprobantes electronicos (tipos 01-09).\n\nSi `q` contiene solo digitos, busca por prefijo de codigo. Si\ncontiene texto, busca por descripcion. Sin `q`, retorna todos\nlos codigos activos ordenados por codigo."
parameters:
-
in: query
name: q
description: 'Texto libre o prefijo numerico del codigo CABYS. Minimo 2 caracteres.'
example: arroz
required: false
schema:
type: string
description: 'Texto libre o prefijo numerico del codigo CABYS. Minimo 2 caracteres.'
example: arroz
-
in: query
name: per_page
description: 'Resultados por pagina (1-100). Default: 25.'
example: 10
required: false
schema:
type: integer
description: 'Resultados por pagina (1-100). Default: 25.'
example: 10
-
in: query
name: page
description: 'Pagina actual.'
example: 1
required: false
schema:
type: integer
description: 'Pagina actual.'
example: 1
responses:
200:
description: 'Busqueda por texto'
content:
application/json:
schema:
type: object
example:
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'
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
code:
type: string
example: '0111101000100'
description:
type: string
example: 'Arroz con cáscara (arroz paddy)'
tax_rate:
type: string
example: '01'
tax_code:
type: string
example: '08'
is_merchandise:
type: boolean
example: true
is_medicine:
type: boolean
example: false
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
description: 'Página actual.'
per_page:
type: integer
example: 25
description: 'Resultados por página.'
has_more:
type: boolean
example: true
description: '`true` si hay más páginas disponibles. CABYS usa `simplePaginate` (sin COUNT total) por rendimiento con 20K+ registros.'
links:
type: object
properties:
prev:
type: string
example: null
nullable: true
next:
type: string
example: '/api/v1/public/catalogs/cabys?q=arroz&page=2'
tags:
- Catálogos
/catalogs/activities:
get:
summary: 'Buscar actividad economica (CIIU).'
operationId: buscarActividadEconomicaCIIU
description: "Busqueda paginada en el catalogo de actividades economicas CIIU 4\n(~800 codigos). El codigo de actividad es obligatorio en el campo\n`CodigoActividadEmisor` de todos los comprobantes electronicos.\n\nSi `q` contiene solo digitos, busca por prefijo de codigo CIIU.\nSi contiene texto, busca por descripcion de la actividad."
parameters:
-
in: query
name: q
description: 'Texto libre o prefijo numerico. Minimo 2 caracteres.'
example: 'desarrollo software'
required: false
schema:
type: string
description: 'Texto libre o prefijo numerico. Minimo 2 caracteres.'
example: 'desarrollo software'
-
in: query
name: per_page
description: 'Resultados por pagina (1-100). Default: 25.'
example: 10
required: false
schema:
type: integer
description: 'Resultados por pagina (1-100). Default: 25.'
example: 10
-
in: query
name: page
description: 'Pagina actual.'
example: 1
required: false
schema:
type: integer
description: 'Pagina actual.'
example: 1
responses:
200:
description: 'Busqueda por texto'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
code:
type: string
example: '6201.0'
description:
type: string
example: 'Actividades de programación informática'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 25
total:
type: integer
example: 2
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- Catálogos
/catalogs/locations:
get:
summary: 'Consultar ubicaciones (provincia, canton, distrito).'
operationId: consultarUbicacionesprovinciaCantonDistrito
description: "Consulta jerarquica de la division territorial de Costa Rica, necesaria\npara construir el nodo `Ubicacion` del emisor y receptor en el XML\ndel comprobante.\n\n**Comportamiento jerarquico:**\n- Sin parametros → retorna las 7 provincias\n- `?province=1` → cantones de San Jose\n- `?province=1&canton=01` → distritos de San Jose central\n- `?q=escazu` → busqueda por nombre en cualquier nivel\n\nNo usa paginacion: el resultado maximo es de aproximadamente 16\nregistros (distritos de un canton)."
parameters:
-
in: query
name: province
description: 'Codigo de provincia (1-7) para obtener sus cantones.'
example: '1'
required: false
schema:
type: string
description: 'Codigo de provincia (1-7) para obtener sus cantones.'
example: '1'
-
in: query
name: canton
description: 'Codigo de canton (01-20) para obtener sus distritos. Requiere `province`.'
example: '01'
required: false
schema:
type: string
description: 'Codigo de canton (01-20) para obtener sus distritos. Requiere `province`.'
example: '01'
-
in: query
name: q
description: 'Busqueda por nombre de ubicacion. Minimo 2 caracteres.'
example: escazu
required: false
schema:
type: string
description: 'Busqueda por nombre de ubicacion. Minimo 2 caracteres.'
example: escazu
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: '7 provincias (sin parametros)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
level:
type: string
example: province
province_code:
type: integer
example: 1
canton_code:
type: string
example: null
nullable: true
district_code:
type: string
example: null
nullable: true
name:
type: string
example: 'San José'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
count:
type: integer
example: 7
-
description: 'Cantones de una provincia'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
level:
type: string
example: canton
province_code:
type: integer
example: 1
canton_code:
type: integer
example: 1
district_code:
type: string
example: null
nullable: true
name:
type: string
example: 'San José'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
count:
type: integer
example: 20
tags:
- Catálogos
'/taxpayer/{id_number}':
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
get:
summary: 'Consultar identificacion por numero.'
operationId: consultarIdentificacionPorNumero
description: "Busca primero en cache (24 horas), luego en el API publico de Hacienda.\nPara cedulas fisicas de 9 digitos no encontradas en Hacienda, y planes\nque incluyen consulta de padron, intenta el Padron Electoral TSE como\nfuente alternativa de nombre verificado."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Caso A — Contribuyente en Hacienda con TSE (plan business|integrator)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id_number:
type: string
example: '101230456'
description: 'Número de identificación consultado (9-12 dígitos).'
id_type:
type: string
example: '01'
description: 'Tipo de identificación: `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE, `05`=Extranjero, `06`=No Contribuyente.'
id_type_label:
type: string
example: 'Cédula Física'
description: 'Nombre legible del tipo de identificación.'
name:
type: string
example: 'JUAN PÉREZ SOLÍS'
description: 'Nombre o razón social. En Caso A proviene del RUT de Hacienda. En Caso B proviene del Padrón Electoral TSE.'
hacienda_registered:
type: boolean
example: true
description: '`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:
type: object
properties:
code:
type: string
example: '02'
description: 'Código del régimen (ej. `01`=Tradicional, `02`=Simplificado).'
description:
type: string
example: Simplificado
description: 'Descripción del régimen.'
description: 'Régimen tributario del contribuyente. `null` en Caso B (no inscrito en Hacienda).'
situation:
type: object
properties:
status:
type: string
example: Activo
description: 'Estado ante Hacienda: `Activo`, `Inactivo`, `Moroso`, etc.'
morphs_fijo:
type: boolean
example: true
description: 'Indicador de morphs fijo del contribuyente.'
description: 'Situación del contribuyente ante Hacienda. `null` en Caso B.'
activities:
type: array
example:
-
code: '620100'
status: A
description: 'Actividades de consultoría informática'
description: 'Actividades económicas CIIU inscritas ante Hacienda. Array vacío en Caso B. Usar solo las que tengan `status=A` (activas).'
items:
type: object
properties:
code:
type: string
example: '620100'
status:
type: string
example: A
description:
type: string
example: 'Actividades de consultoría informática'
cached:
type: boolean
example: false
description: '`true` si los datos provienen de la cache Redis (TTL 24h para Hacienda, 1h para TSE-only).'
cached_at:
type: string
example: '2026-04-08T10:30:00-06:00'
description: 'Fecha/hora en que se almacenaron en cache (ISO 8601). `null` si no están cacheados.'
tse:
type: object
properties:
found:
type: boolean
example: true
description: '`true` si la cédula fue encontrada en el padrón.'
nombre_completo:
type: string
example: 'JUAN PÉREZ SOLÍS'
description: 'Nombre completo en mayúsculas tal como el TSE lo almacena.'
nombre:
type: string
example: JUAN
description: 'Primer nombre del titular.'
apellido1:
type: string
example: PÉREZ
description: 'Primer apellido.'
apellido2:
type: string
example: SOLÍS
description: 'Segundo apellido.'
cedula_vence:
type: string
example: '2028-05-14'
description: 'Fecha de vencimiento de la cédula (YYYY-MM-DD). No es fecha de nacimiento.'
cedula_vigente:
type: boolean
example: true
description: '`true` si la cédula no ha expirado. Cédula vencida no invalida el comprobante — Hacienda no verifica vigencia del receptor.'
provincia:
type: string
example: 'San José'
description: 'Nombre de la provincia de inscripción electoral (no necesariamente domicilio actual).'
canton:
type: string
example: Escazú
description: 'Nombre del cantón de inscripción electoral.'
distrito_codigo:
type: string
example: '003'
description: 'Código TSE del distrito (3 dígitos). Distinto al sistema de 2 dígitos de Hacienda.'
padron_version:
type: string
example: '2026-04-01'
description: 'Fecha de corte del padrón fuente (YYYY-MM-DD). El TSE actualiza mensualmente.'
nota:
type: string
example: 'Datos del Padrón Electoral TSE (Código Electoral Art. 105).'
description: 'Nota legal sobre la fuente de los datos.'
cached:
type: boolean
example: false
description: '`true` si los datos TSE provienen de cache Redis (TTL 30 días).'
description: '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.'
message:
type: string
example: 'Contribuyente encontrado.'
errors:
type: string
example: null
nullable: true
-
description: 'Caso B — Consumidor final: no en Hacienda pero sí en padrón TSE (plan business|integrator)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id_number:
type: string
example: '101230456'
description: 'Número de identificación consultado (9-12 dígitos).'
id_type:
type: string
example: '01'
description: 'Tipo de identificación: `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE, `05`=Extranjero, `06`=No Contribuyente.'
id_type_label:
type: string
example: 'Cédula Física'
description: 'Nombre legible del tipo de identificación.'
name:
type: string
example: 'JUAN PÉREZ SOLÍS'
description: 'Nombre o razón social. En Caso A proviene del RUT de Hacienda. En Caso B proviene del Padrón Electoral TSE.'
hacienda_registered:
type: boolean
example: false
description: '`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:
type: string
example: null
description: 'Régimen tributario del contribuyente. `null` en Caso B (no inscrito en Hacienda).'
situation:
type: string
example: null
description: 'Situación del contribuyente ante Hacienda. `null` en Caso B.'
activities:
type: array
example: []
description: 'Actividades económicas CIIU inscritas ante Hacienda. Array vacío en Caso B. Usar solo las que tengan `status=A` (activas).'
cached:
type: boolean
example: false
description: '`true` si los datos provienen de la cache Redis (TTL 24h para Hacienda, 1h para TSE-only).'
cached_at:
type: string
example: '2026-04-08T10:30:00-06:00'
description: 'Fecha/hora en que se almacenaron en cache (ISO 8601). `null` si no están cacheados.'
tse:
type: object
properties:
found:
type: boolean
example: true
description: '`true` si la cédula fue encontrada en el padrón.'
nombre_completo:
type: string
example: 'JUAN PÉREZ SOLÍS'
description: 'Nombre completo en mayúsculas tal como el TSE lo almacena.'
nombre:
type: string
example: JUAN
description: 'Primer nombre del titular.'
apellido1:
type: string
example: PÉREZ
description: 'Primer apellido.'
apellido2:
type: string
example: SOLÍS
description: 'Segundo apellido.'
cedula_vence:
type: string
example: '2028-05-14'
description: 'Fecha de vencimiento de la cédula (YYYY-MM-DD). No es fecha de nacimiento.'
cedula_vigente:
type: boolean
example: true
description: '`true` si la cédula no ha expirado. Cédula vencida no invalida el comprobante — Hacienda no verifica vigencia del receptor.'
provincia:
type: string
example: 'San José'
description: 'Nombre de la provincia de inscripción electoral (no necesariamente domicilio actual).'
canton:
type: string
example: Escazú
description: 'Nombre del cantón de inscripción electoral.'
distrito_codigo:
type: string
example: '003'
description: 'Código TSE del distrito (3 dígitos). Distinto al sistema de 2 dígitos de Hacienda.'
padron_version:
type: string
example: '2026-04-01'
description: 'Fecha de corte del padrón fuente (YYYY-MM-DD). El TSE actualiza mensualmente.'
nota:
type: string
example: 'Datos del Padrón Electoral TSE (Código Electoral Art. 105).'
description: 'Nota legal sobre la fuente de los datos.'
cached:
type: boolean
example: false
description: '`true` si los datos TSE provienen de cache Redis (TTL 30 días).'
description: '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.'
message:
type: string
example: 'Identificación verificada en Padrón Electoral TSE. No inscrita como contribuyente ante Hacienda.'
errors:
type: string
example: null
nullable: true
-
description: 'Cédula jurídica o plan sin consulta de padrón (solo Hacienda)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id_number:
type: string
example: '3101234567'
description: 'Número de identificación consultado (9-12 dígitos).'
id_type:
type: string
example: '02'
description: 'Tipo de identificación: `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE, `05`=Extranjero, `06`=No Contribuyente.'
id_type_label:
type: string
example: 'Cédula Jurídica'
description: 'Nombre legible del tipo de identificación.'
name:
type: string
example: 'EMPRESA EJEMPLO S.A.'
description: 'Nombre o razón social. En Caso A proviene del RUT de Hacienda. En Caso B proviene del Padrón Electoral TSE.'
hacienda_registered:
type: boolean
example: true
description: '`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:
type: object
properties:
code:
type: string
example: '01'
description: 'Código del régimen (ej. `01`=Tradicional, `02`=Simplificado).'
description:
type: string
example: Tradicional
description: 'Descripción del régimen.'
description: 'Régimen tributario del contribuyente. `null` en Caso B (no inscrito en Hacienda).'
situation:
type: object
properties:
status:
type: string
example: Activo
description: 'Estado ante Hacienda: `Activo`, `Inactivo`, `Moroso`, etc.'
morphs_fijo:
type: boolean
example: true
description: 'Indicador de morphs fijo del contribuyente.'
description: 'Situación del contribuyente ante Hacienda. `null` en Caso B.'
activities:
type: array
example:
-
code: '620100'
status: A
description: 'Actividades de consultoría informática'
description: 'Actividades económicas CIIU inscritas ante Hacienda. Array vacío en Caso B. Usar solo las que tengan `status=A` (activas).'
items:
type: object
properties:
code:
type: string
example: '620100'
status:
type: string
example: A
description:
type: string
example: 'Actividades de consultoría informática'
cached:
type: boolean
example: false
description: '`true` si los datos provienen de la cache Redis (TTL 24h para Hacienda, 1h para TSE-only).'
cached_at:
type: string
example: '2026-04-08T10:30:00-06:00'
description: 'Fecha/hora en que se almacenaron en cache (ISO 8601). `null` si no están cacheados.'
message:
type: string
example: 'Contribuyente encontrado.'
errors:
type: string
example: null
nullable: true
404:
description: 'Caso C — No encontrado en Hacienda ni en padrón TSE'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: "No se encontró la identificación '999999999' en el registro de Hacienda ni en el Padrón Electoral TSE."
errors:
type: string
example: null
nullable: true
502:
description: 'Hacienda no disponible'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El API público de Hacienda retornó un error interno. Intente nuevamente más tarde.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El API público de Hacienda retornó un error interno. Intente nuevamente más tarde.'
errors:
type: string
example: null
nullable: true
504:
description: 'Timeout Hacienda'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El API público de Hacienda no respondió dentro del tiempo límite. Intente nuevamente.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El API público de Hacienda no respondió dentro del tiempo límite. Intente nuevamente.'
errors:
type: string
example: null
nullable: true
tags:
- 'Consulta de Contribuyentes'
parameters:
-
in: path
name: id_number
description: 'Número de identificación (9-12 dígitos).'
example: '3101234567'
required: true
schema:
type: string
/receiver/confirm:
servers:
-
url: 'https://fe.almendro.cr/api/v1/public'
description: 'Producción — valor fiscal'
-
url: 'https://fe.almendro.cr/api/v1/public/sandbox'
description: 'Sandbox — sin valor fiscal'
post:
summary: 'Confirmar o rechazar comprobante recibido.'
operationId: confirmarORechazarComprobanteRecibido
description: "Genera y envia un MensajeReceptor a Hacienda para aceptar (total o\nparcial) o rechazar un comprobante electronico recibido de un proveedor.\n\n**Valores del campo `message`:**\n- `1` = Aceptado → consecutivo tipo 05\n- `2` = Aceptado Parcialmente → consecutivo tipo 06\n- `3` = Rechazado → consecutivo tipo 07\n\n**Plazo:** 8 dias habiles desde la emision del documento (art. 15\nReglamento). Vencido el plazo se considera aceptacion tacita y no\nse puede enviar MensajeReceptor."
parameters: []
responses:
201:
description: 'Aceptación enviada exitosamente'
content:
application/json:
schema:
type: object
example:
success: true
data:
id: 019d867d-0030-7288-8ece-fd64da756030
voucher_key: '50619032600310199999900100001010000000001112345678'
voucher_id: 019d867d-0031-7288-8ece-fd64da756031
issuer_id_number: '3101999999'
doc_issued_at: '2026-03-26T08:00:00-06:00'
message: '1'
message_label: Aceptado
message_detail: null
total_tax: '130000.00000'
activity_code: '620100'
tax_condition: '01'
tax_condition_label: 'Genera crédito IVA'
tax_creditable_amount: '130000.00000'
expense_applicable_amount: null
total_invoice: '1130000.00000'
receiver_id_number: '3101000000'
receiver_consecutive: '00100001050000000001'
status: draft
has_xml: true
is_signed: true
environment: production
deadline:
business_days_elapsed: 2
business_days_remaining: 6
deadline_date: '2026-04-07'
is_within_deadline: true
is_near_deadline: false
is_overdue: false
hacienda:
status: null
message: null
sent_at: null
processed_at: null
issued_by: 019d867d-0032-7288-8ece-fd64da756032
created_at: '2026-04-16T10:00:00-06:00'
updated_at: '2026-04-16T10:00:00-06:00'
message: 'Aceptado del comprobante [506...001] generada y validada correctamente. Pendiente de envío a Hacienda.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-0030-7288-8ece-fd64da756030
description: 'UUID del MensajeReceptor generado.'
voucher_key:
type: string
example: '50619032600310199999900100001010000000001112345678'
description: 'Clave de 50 dígitos del comprobante al que se responde.'
voucher_id:
type: string
example: 019d867d-0031-7288-8ece-fd64da756031
description: 'UUID interno del voucher en AlmendroFEC. `null` si el comprobante no fue emitido por este sistema.'
issuer_id_number:
type: string
example: '3101999999'
description: 'Cédula del emisor del comprobante original (NumeroCedulaEmisor del MensajeReceptor XSD).'
doc_issued_at:
type: string
example: '2026-03-26T08:00:00-06:00'
description: 'Fecha de emisión del documento original (ISO 8601). Usada para calcular el plazo de 8 días hábiles.'
message:
type: string
example: '1'
description: 'Código del mensaje: `1`=Aceptado, `2`=Aceptado Parcialmente, `3`=Rechazado.'
message_label:
type: string
example: Aceptado
description: 'Nombre legible del tipo de mensaje en español.'
message_detail:
type: string
example: null
description: 'Detalle o razón del rechazo/aceptación parcial. `null` si fue aceptación total.'
total_tax:
type: string
example: '130000.00000'
description: 'MontoTotalImpuesto del comprobante (Decimal 18,5). Obligatorio cuando `message` es `1` o `2`.'
activity_code:
type: string
example: '620100'
description: 'Código de actividad económica CIIU (6 dígitos). Obligatorio cuando `message` es `1` o `2`.'
tax_condition:
type: string
example: '01'
description: '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:
type: string
example: 'Genera crédito IVA'
description: 'Nombre legible de la condición de impuesto.'
tax_creditable_amount:
type: string
example: '130000.00000'
description: 'MontoTotalImpuestoAcreditar (Decimal 18,5). Monto de IVA que el receptor puede acreditar. `null` si no aplica.'
expense_applicable_amount:
type: string
example: null
description: 'MontoTotalDeGastoAplicable (Decimal 18,5). Monto del gasto aplicable. `null` si no aplica.'
total_invoice:
type: string
example: '1130000.00000'
description: 'TotalFactura del comprobante original (Decimal 18,5).'
receiver_id_number:
type: string
example: '3101000000'
description: 'Cédula del receptor (quien emite este MensajeReceptor).'
receiver_consecutive:
type: string
example: '00100001050000000001'
description: 'Consecutivo de 20 dígitos del receptor (tipo 05/06/07 según el mensaje).'
status:
type: string
example: draft
description: 'Estado del envío: `draft` (generado, pendiente de envío), `sent`, `accepted`, `rejected`, `error`.'
has_xml:
type: boolean
example: true
description: '`true` si el XML del MensajeReceptor fue generado.'
is_signed:
type: boolean
example: true
description: '`true` si el XML fue firmado con XAdES-EPES.'
environment:
type: string
example: production
description: 'Ambiente: `production` o `sandbox`.'
deadline:
type: object
properties:
business_days_elapsed:
type: integer
example: 2
description: 'Días hábiles transcurridos desde la emisión del comprobante.'
business_days_remaining:
type: integer
example: 6
description: 'Días hábiles restantes para responder. `0` si venció.'
deadline_date:
type: string
example: '2026-04-07'
description: 'Fecha límite para responder (YYYY-MM-DD).'
is_within_deadline:
type: boolean
example: true
description: '`true` si aún está dentro del plazo.'
is_near_deadline:
type: boolean
example: false
description: '`true` si quedan 2 días hábiles o menos.'
is_overdue:
type: boolean
example: false
description: '`true` si el plazo venció.'
description: 'Información del plazo de 8 días hábiles (art. 15 Reglamento). Calculado considerando feriados de CR.'
hacienda:
type: object
properties:
status:
type: string
example: null
description: 'Estado: `aceptado`, `rechazado`, o `null` si no procesado aún.'
message:
type: string
example: null
description: 'Detalle del mensaje de Hacienda. `null` si pendiente.'
sent_at:
type: string
example: null
description: 'Fecha/hora de envío a Hacienda (ISO 8601). `null` si no enviado.'
processed_at:
type: string
example: null
description: 'Fecha/hora de procesamiento por Hacienda (ISO 8601). `null` si pendiente.'
description: 'Respuesta de Hacienda al MensajeReceptor.'
issued_by:
type: string
example: 019d867d-0032-7288-8ece-fd64da756032
description: 'UUID del usuario que generó el MensajeReceptor.'
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de creación del registro (ISO 8601).'
updated_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de última actualización (ISO 8601).'
message:
type: string
example: 'Aceptado del comprobante [506...001] generada y validada correctamente. Pendiente de envío a Hacienda.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Plazo de 8 días hábiles vencido'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El plazo de 8 días hábiles para confirmar/rechazar el comprobante ha vencido.'
errors:
type: object
properties:
voucher_key:
type: array
example:
- 'El plazo de 8 días hábiles ha vencido.'
items:
type: string
-
description: 'Ya existe confirmación para este comprobante'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ya existe una confirmación/rechazo para este comprobante.'
errors:
type: object
properties:
voucher_key:
type: array
example:
- 'Ya existe una confirmación/rechazo para este comprobante.'
items:
type: string
tags:
- 'Confirmación del Receptor'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
voucher_key:
type: string
description: 'Clave del comprobante recibido (50 dígitos exactos). Estructura: `[3 país][8 fecha][12 cédula][10 consecutivo][1 situación][8 seguridad][8 verificador]`. Must match the regex /^\d{50}$/.'
example: '50619032600310199999900100001010000000001112345678'
issuer_id_number:
type: string
description: 'Número de cédula del emisor del comprobante (máx 20 chars). validation.max.'
example: '3101999999'
doc_issued_at:
type: string
description: 'Fecha de emisión del comprobante recibido (xs:dateTime). No puede ser futura. validation.date validation.before_or_equal.'
example: '2026-03-18T10:00:00-06:00'
message:
type: string
description: 'Tipo de confirmación. `1`=Aceptado, `2`=Aceptado Parcialmente, `3`=Rechazado.'
example: '1'
enum:
- 1
- 2
- 3
message_detail:
type: string
description: 'Detalle del mensaje (1-160 chars). Motivo de aceptación o rechazo. validation.min validation.max.'
example: 'Comprobante aceptado conforme.'
total_tax:
type: number
description: 'Monto total de impuesto del comprobante (Decimal 18,5). Condicional: obligatorio si el comprobante tiene impuestos. validation.min.'
example: 6500.0
nullable: true
activity_code:
type: string
description: 'Código CIIU del receptor (6 dígitos). Obligatorio si `tax_condition` ≠ `05`. Prohibido si `tax_condition` = `05`. Must match the regex /^(\d{4}\.\d|\d{6})$/.'
example: '6201.0'
nullable: true
tax_condition:
type: string
description: 'Condición de impuesto del receptor. `01`=Crédito IVA general, `02`=Crédito parcial, `03`=Bienes de capital, `04`=Gasto sin crédito, `05`=Proporcionalidad.'
example: '01'
enum:
- 1
- 2
- 3
- 4
- 5
nullable: true
tax_creditable_amount:
type: number
description: 'Monto total de impuesto acreditable (Decimal 18,5). Obligatorio si `tax_condition` ≠ `05`. validation.min.'
example: 6500.0
nullable: true
expense_applicable_amount:
type: number
description: 'Monto total de gasto aplicable (Decimal 18,5). Obligatorio si `tax_condition` ≠ `05`. validation.min.'
example: 50000.0
nullable: true
total_invoice:
type: number
description: 'Monto total del comprobante (Decimal 18,5). Siempre obligatorio. validation.min.'
example: 56500.0
voucher_id:
type: integer
description: 'ID interno del comprobante en el sistema (opcional). Referencia para tracking. The id of an existing record in the vouchers table.'
example: null
nullable: true
required:
- voucher_key
- issuer_id_number
- doc_issued_at
- message
- message_detail
- total_invoice
/my-contributors:
get:
summary: 'Listar clientes gestionados por el integrador'
operationId: listarClientesGestionadosPorElIntegrador
description: "Devuelve los **contribuyentes-cliente** que el integrador\nautenticado gestiona actualmente, con búsqueda por texto y\npaginación. Ordenado por fecha de creación descendente (los más\nrecientes primero).\n\nSolo los clientes con vínculo **activo** se incluyen. Si un cliente\nfue desvinculado, no aparece en este listado aunque su cuenta siga\nexistiendo."
parameters:
-
in: query
name: search
description: 'Búsqueda por razón social o número de identificación (mínimo 2 caracteres).'
example: cliente
required: false
schema:
type: string
description: 'Búsqueda por razón social o número de identificación (mínimo 2 caracteres).'
example: cliente
-
in: query
name: per_page
description: 'Resultados por página (1-100). Default: 15.'
example: 15
required: false
schema:
type: integer
description: 'Resultados por página (1-100). Default: 15.'
example: 15
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Listado paginado de clientes gestionados (cliente exclusivo)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'CLIENTE DEL INTEGRADOR S.A.'
trade_name:
type: string
example: 'Cliente Shop'
id_type:
type: string
example: '02'
id_type_label:
type: string
example: 'Cédula Jurídica'
id_number:
type: string
example: '3101123456'
primary_email:
type: string
example: juan@cliente.cr
is_active:
type: boolean
example: true
can_emit_from_portal:
type: boolean
example: true
plan_code:
type: string
example: free
plan_name:
type: string
example: Gratis
integrators_count:
type: integer
example: 1
created_at:
type: string
example: '2026-04-10T09:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
from:
type: integer
example: 1
to:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
-
description: 'Cliente compartido con otros integradores (N:N)'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c02
legal_name:
type: string
example: 'CLIENTE COMPARTIDO S.A.'
trade_name:
type: string
example: null
nullable: true
id_type:
type: string
example: '02'
id_type_label:
type: string
example: 'Cédula Jurídica'
id_number:
type: string
example: '3101999999'
primary_email:
type: string
example: contacto@compartido.cr
is_active:
type: boolean
example: true
can_emit_from_portal:
type: boolean
example: true
plan_code:
type: string
example: pyme
plan_name:
type: string
example: Pyme
integrators_count:
type: integer
example: 3
created_at:
type: string
example: '2026-03-15T12:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
from:
type: integer
example: 1
to:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
-
description: 'Sin clientes gestionados'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 0
from:
type: string
example: null
nullable: true
to:
type: string
example: null
nullable: true
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
post:
summary: 'Crear un cliente gestionado'
operationId: crearUnClienteGestionado
description: "Crea un nuevo contribuyente-cliente en el sistema y establece un\nvínculo de gestión con el integrador autenticado. El cliente recibe\nun **magic link** en el correo indicado para activar su cuenta.\n\n**El cliente queda con:**\n\n- Plan **Gratis** (el cliente o el integrador pueden actualizarlo\n después).\n- Una cuenta de usuario administrador (`owner`) lista para recibir\n el magic link.\n- Un vínculo de gestión activo con este integrador.\n\n**El cliente NO queda con:**\n\n- Ningún certificado digital (debe subirlo después con\n `POST /my-contributors/{id}/certificates`).\n- Ningún grant activo para que el integrador firme por él. Para\n eso, debe solicitar un grant desde el grupo\n `Certificados de terceros`.\n\n**Tras crear:**\n\n1. El cliente recibe un email con el magic link.\n2. El cliente debe activar la cuenta en ≤ 72 horas.\n3. Si quiere firmar por él, solicite un grant con\n `POST /access-requests`."
parameters: []
responses:
201:
description: 'Cliente creado y magic link enviado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
description: 'UUID del contribuyente-cliente creado.'
id_type:
type: string
example: '02'
description: 'Tipo de identificación: `01`=Física, `02`=Jurídica, `03`=DIMEX, `04`=NITE.'
id_number:
type: string
example: '3101123456'
description: 'Número de identificación del cliente.'
legal_name:
type: string
example: 'CLIENTE DEL INTEGRADOR S.A.'
description: 'Razón social o nombre legal del cliente.'
commercial_name:
type: string
example: 'Cliente Shop'
description: 'Nombre comercial. `null` si no se envió.'
emails:
type: array
example:
- juan@cliente.cr
description: 'Correos del cliente.'
items:
type: string
economic_activities:
type: array
example:
- '6201.0'
description: 'Códigos CIIU en formato `XXXX.X`.'
items:
type: string
province:
type: integer
example: 1
description: 'Código de provincia (1-7). `null` si no se envió.'
canton:
type: integer
example: 1
description: 'Código de cantón. `null` si no se envió.'
district:
type: integer
example: 1
description: 'Código de distrito. `null` si no se envió.'
neighborhood:
type: string
example: 'San Pedro'
description: 'Barrio. `null` si no se envió.'
address:
type: string
example: '200m norte del parque central'
description: 'Señas exactas. `null` si no se envió.'
phone:
type: string
example: '22001234'
description: 'Teléfono de contacto. `null` si no se envió.'
phone_country_code:
type: integer
example: 506
description: 'Código de país del teléfono. Default: 506.'
is_active:
type: boolean
example: true
description: '`true` — el cliente se crea activo.'
plan:
type: object
properties:
code:
type: string
example: free
description: 'Código del plan: `free`.'
name:
type: string
example: Gratis
description: 'Nombre del plan: `Gratis`.'
description: 'Plan asignado al cliente (siempre Gratis al crear).'
has_active_grant:
type: boolean
example: false
description: '`false` al crear — el integrador debe solicitar un grant por separado vía `POST /access-requests`.'
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de creación (ISO 8601).'
updated_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de última actualización (ISO 8601).'
message:
type: string
example: 'Cliente creado correctamente. Se envió email de bienvenida.'
errors:
type: string
example: null
nullable: true
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Límite del plan alcanzado'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ha alcanzado el máximo de clientes gestionados de su plan.'
errors:
type: object
properties:
managed_contributors:
type: array
example:
- 'Ha alcanzado el máximo de clientes gestionados de su plan.'
items:
type: string
-
description: 'Correo del propietario ya registrado'
type: object
example:
success: false
data: null
message: 'Los datos proporcionados no son válidos.'
errors:
owner_email:
- 'Este correo ya está registrado en el sistema.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Los datos proporcionados no son válidos.'
errors:
type: object
properties:
owner_email:
type: array
example:
- 'Este correo ya está registrado en el sistema.'
items:
type: string
tags:
- 'Clientes del Integrador'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
legal_name:
type: string
description: 'Razón social o nombre legal del cliente (3-150 chars).'
example: 'CLIENTE DEL INTEGRADOR S.A.'
trade_name:
type: string
description: 'Nombre comercial del cliente (3-80 chars).'
example: 'Cliente Shop'
nullable: true
id_type:
type: string
description: 'Tipo de identificación del cliente. Valores: `01` (Física), `02` (Jurídica), `03` (DIMEX), `04` (NITE).'
example: '02'
id_number:
type: string
description: 'Número de identificación del cliente.'
example: '3101123456'
email:
type: string
description: 'Correo del cliente. Se usa para la cuenta en el portal y envío de facturación. Único en el sistema. validation.email validation.max.'
example: contacto@cliente.cr
phone:
type: string
description: 'Teléfono de contacto (4-20 dígitos).'
example: '22001234'
nullable: true
economic_activities:
type: array
description: 'Códigos CIIU en formato Hacienda `XXXX.X`.'
example:
- '6201.0'
items:
type: string
province:
type: integer
description: 'Código de provincia (1-7).'
example: 1
nullable: true
canton:
type: integer
description: 'Código de cantón (1-99).'
example: 1
nullable: true
district:
type: integer
description: 'Código de distrito (1-99).'
example: 1
nullable: true
neighborhood:
type: string
description: 'Barrio (5-50 chars).'
example: 'San Pedro'
nullable: true
address:
type: string
description: 'Señas exactas (5-300 chars).'
example: '200m norte del parque central'
nullable: true
owner_name:
type: string
description: 'Nombre completo del usuario administrador del cliente.'
example: 'Juan Cliente'
owner_email:
type: string
description: 'Correo del administrador del cliente (recibe el magic link).'
example: juan@cliente.cr
phone_country_code:
type: integer
description: 'Código de país del teléfono. Default: 506.'
example: 506
emails:
type: array
description: 'Correos de contacto adicionales del cliente (máx 4).'
example:
- contacto@cliente.cr
items:
type: string
required:
- legal_name
- id_type
- id_number
- email
- owner_name
- owner_email
'/my-contributors/{id}':
get:
summary: 'Consultar un cliente gestionado'
operationId: consultarUnClienteGestionado
description: "Devuelve el detalle completo del cliente indicado. Solo se retornan\nclientes con vínculo **activo** con el integrador autenticado. Si\nel UUID no corresponde a un cliente gestionado actualmente por usted,\nretorna HTTP 404."
parameters: []
responses:
200:
description: 'Cliente encontrado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101123456'
legal_name:
type: string
example: 'CLIENTE DEL INTEGRADOR S.A.'
commercial_name:
type: string
example: 'Cliente Shop'
emails:
type: array
example:
- juan@cliente.cr
items:
type: string
economic_activities:
type: array
example:
- '6201.0'
items:
type: string
province:
type: integer
example: 1
canton:
type: integer
example: 1
district:
type: integer
example: 1
neighborhood:
type: string
example: 'San Pedro'
address:
type: string
example: '200m norte del parque central'
phone:
type: string
example: '22001234'
phone_country_code:
type: integer
example: 506
is_active:
type: boolean
example: true
plan:
type: object
properties:
code:
type: string
example: free
name:
type: string
example: Gratis
has_active_grant:
type: boolean
example: true
active_grant_environment:
type: string
example: sandbox
created_at:
type: string
example: '2026-04-10T09:00:00-06:00'
updated_at:
type: string
example: '2026-04-10T09:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado o desvinculado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Cliente no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
put:
summary: 'Actualizar relación con el cliente gestionado'
operationId: actualizarRelacinConElClienteGestionado
description: "Permite modificar únicamente campos de la **relación de gestión**\nentre el integrador y el cliente. Los datos propios del cliente\n(razón social, dirección, teléfono, etc.) **no pueden ser\nmodificados por el integrador** — solo el cliente puede editarlos\ndesde su propio portal o API (Art. 16 Reglamento).\n\n**Campo editable:**\n\n- `default_pdf_template_id` — la plantilla PDF que este integrador\n usará por defecto al emitir por este cliente. Per-integrador:\n no afecta a otros integradores del mismo cliente.\n\n**Campos bloqueados (retornan 403):**\n\n- `legal_name`, `trade_name`, `emails`, `economic_activities`,\n `province`, `canton`, `district`, `neighborhood`, `address`,\n `phone`, `phone_country_code`"
parameters: []
responses:
200:
description: 'Relación actualizada'
content:
text/plain:
schema:
type: string
example: ''
403:
description: 'Intento de modificar datos del cliente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: '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:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
default_pdf_template_id:
type: integer
description: 'ID de la plantilla PDF.'
example: 3
nullable: true
delete:
summary: 'Desvincular al integrador del cliente'
operationId: desvincularAlIntegradorDelCliente
description: "**Rompe únicamente la relación de gestión** entre usted y este\ncliente. El cliente conserva:\n\n- Su cuenta y sus datos.\n- Sus certificados digitales.\n- Sus comprobantes emitidos.\n- Los vínculos con otros integradores (si los tiene).\n\n**Efectos inmediatos de la desvinculación:**\n\n1. La relación de gestión queda marcada como desvinculada.\n2. Todos los grants activos de este cliente hacia usted\n se **revocan automáticamente** (no podrá seguir firmando\n por él).\n3. Este cliente desaparecerá del listado\n `GET /my-contributors`.\n\n**No se puede** usar este endpoint para desvincularse a sí mismo\n(es decir, pasar su propio UUID de integrador) — retorna HTTP 403.\n\n> **Usos típicos:** finalización de contrato con el cliente,\n> transferencia del cliente a otro integrador, limpieza de\n> clientes inactivos."
parameters: []
responses:
200:
description: 'Cliente desvinculado'
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Cliente desvinculado correctamente. Se revocaron los accesos asociados.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente desvinculado correctamente. Se revocaron los accesos asociados.'
errors:
type: string
example: null
nullable: true
403:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Plan no es Integrador'
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
-
description: 'Intento de auto-desvinculación'
type: object
example:
success: false
data: null
message: 'No puede desvincularse a sí mismo desde este endpoint.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No puede desvincularse a sí mismo desde este endpoint.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado o ya desvinculado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Cliente no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
'/my-contributors/{id}/certificates':
get:
summary: 'Listar los certificados digitales de un cliente gestionado'
operationId: listarLosCertificadosDigitalesDeUnClienteGestionado
description: "Devuelve el historial completo de certificados del cliente\n(activo + desactivados), ordenados por fecha de creación\ndescendente. Los datos sensibles (contenido del `.p12`, contraseñas,\nPIN de Hacienda) **nunca se incluyen** en la respuesta.\n\nLa estructura es idéntica a `GET /api/v1/public/certificates`\n(grupo **Certificados Digitales**), pero opera sobre el cliente\nindicado en la URL."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Listado de certificados del cliente'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: string
example: 019d867d-1111-7288-8ece-fd64da756001
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 120
valid_from:
type: string
example: '2024-01-01T00:00:00-06:00'
valid_until:
type: string
example: '2026-08-15T23:59:59-06:00'
certificate_subject:
type: string
example: 'CN=CLIENTE DEL INTEGRADOR S.A., serialNumber=3101123456'
certificate_serial:
type: string
example: 0A1B2C3D4E5F
created_at:
type: string
example: '2026-04-09T10:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
-
description: 'Cliente sin certificados'
type: object
example:
success: true
data: []
message: ''
errors: null
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado o desvinculado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Cliente no encontrado o no pertenece a este integrador.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente no encontrado o no pertenece a este integrador.'
errors:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
post:
summary: 'Subir un certificado digital para el cliente'
operationId: subirUnCertificadoDigitalParaElCliente
description: "Carga el archivo `.p12` del cliente desde la cuenta del integrador.\nEl certificado queda **vinculado al cliente** (no al integrador):\nel integrador administra el `.p12` en nombre del cliente pero el\ncertificado pertenece al cliente.\n\nEste endpoint es equivalente a\n`POST /api/v1/public/certificates` del grupo\n**Certificados Digitales**, pero opera sobre el cliente\ngestionado indicado en la URL. Consulte la guía de ese grupo\npara entender el formato del `.p12`, el PIN de Hacienda, etc.\n\n**Request:** `multipart/form-data` (no JSON).\n\n**Ejemplo con `curl`:**\n\n```bash\ncurl -X POST \"https://api.almendro.cr/api/v1/public/my-contributors/{id}/certificates\" \\\n -H \"Authorization: Bearer {su_token_api}\" \\\n -F \"p12_file=@/ruta/cert-del-cliente.p12\" \\\n -F \"p12_password=contrasena-del-p12\" \\\n -F \"hacienda_pin=1234\" \\\n -F \"environment=sandbox\"\n```\n\n> **Recuerde:** cargar el certificado del cliente **no le da**\n> automáticamente permiso de firmar por él. Necesita además un\n> grant activo (ver grupo **Certificados de terceros**)."
parameters: []
responses:
201:
description: 'Certificado del cliente registrado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: string
example: 019d867d-3333-7288-8ece-fd64da756c03
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 730
valid_from:
type: string
example: '2026-01-01T00:00:00-06:00'
valid_until:
type: string
example: '2028-01-01T00:00:00-06:00'
certificate_subject:
type: string
example: 'CN=CLIENTE DEL INTEGRADOR S.A., serialNumber=3101123456'
certificate_serial:
type: string
example: 0A1B2C3D4E5F
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
message:
type: string
example: 'Certificado del cliente registrado y activado correctamente.'
errors:
type: string
example: null
nullable: true
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado o desvinculado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Cliente no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente no encontrado.'
errors:
type: string
example: null
nullable: true
422:
description: 'Contraseña del .p12 incorrecta'
content:
application/json:
schema:
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: '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:
type: object
properties:
p12_file:
type: array
example:
- 'No se pudo leer el certificado .p12. Verifique que la contraseña sea correcta y que el archivo sea un certificado digital válido.'
items:
type: string
tags:
- 'Clientes del Integrador'
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
p12_file:
type: string
format: binary
description: 'Archivo `.p12` o `.pfx` de firma digital emitida por BCCR al cliente. Máximo 5 MB.'
p12_password:
type: string
description: 'Contraseña que protege el archivo `.p12` (la que el cliente definió al generarlo).'
example: ContrasenaDelP12
hacienda_pin:
type: string
description: 'PIN de Hacienda del cliente (asignado por ATV al registrarse como emisor).'
example: '1234'
environment:
type: string
description: 'Ambiente al que se asigna el certificado. Valores: `sandbox` o `production`.'
example: sandbox
required:
- p12_file
- p12_password
- hacienda_pin
- environment
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
'/my-contributors/{id}/certificates/{certId}':
delete:
summary: 'Eliminar un certificado del cliente (siempre denegado)'
operationId: eliminarUnCertificadoDelClientesiempreDenegado
description: "Este endpoint **siempre retorna HTTP 403** y no realiza ningún\ncambio. El integrador **no puede eliminar** certificados del\ncliente — el certificado es **propiedad del contribuyente emisor**\ny solo él puede desactivarlo desde su propio portal.\n\n**Si quiere dejar de usar el certificado del cliente:**\n\n- Revoque su propio grant de acceso al certificado desde\n el grupo **Certificados de terceros** (`DELETE /my-access/{grantId}`).\n- Deje de emitir por ese cliente.\n- Desvincúlese del cliente con `DELETE /my-contributors/{id}` si\n ya no desea gestionarlo."
parameters: []
responses:
403:
description: 'Operación siempre denegada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: '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:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
-
in: path
name: certId
description: 'UUID del certificado (no relevante, siempre 403).'
example: 019d867d-1111-7288-8ece-fd64da756001
required: true
schema:
type: string
'/my-contributors/{id}/resend-magic-link':
post:
summary: 'Reenviar el magic link al cliente gestionado'
operationId: reenviarElMagicLinkAlClienteGestionado
description: "Genera un nuevo token de magic link y envía un correo de bienvenida\nal usuario propietario del cliente. Útil cuando el magic link\noriginal expiró (TTL 72 horas) y el cliente aún no activó su cuenta.\n\n**Restricciones:**\n\n- Solo se puede reenviar si el usuario del cliente tiene\n `must_change_password = true` (es decir, aún no estableció su\n contraseña definitiva).\n- Si el cliente ya activó su cuenta, este endpoint retorna 422."
parameters: []
responses:
200:
description: 'Magic link reenviado'
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Magic link reenviado al correo del cliente.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Magic link reenviado al correo del cliente.'
errors:
type: string
example: null
nullable: true
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Cliente no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente no encontrado.'
errors:
type: string
example: null
nullable: true
422:
description: 'Cliente ya activó su cuenta'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'El cliente ya estableció su contraseña. No se puede reenviar el magic link.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'El cliente ya estableció su contraseña. No se puede reenviar el magic link.'
errors:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
'/my-contributors/{id}/sequences':
get:
summary: 'Listar las secuencias de consecutivos del cliente'
operationId: listarLasSecuenciasDeConsecutivosDelCliente
description: "Devuelve todas las secuencias (`voucher_sequences`) del cliente\ngestionado **filtradas por la terminal asignada a este integrador**.\nCada secuencia corresponde a un tipo de comprobante (01–10).\n\n**¿Para qué sirve?**\n\nAl migrar un cliente desde otro facturador (Alegra, Gosocket, etc.),\nAlmendroFEC no tiene historial de los consecutivos previos. Si se\nemite desde la secuencia 1, Hacienda rechaza por \"consecutivo\nduplicado\". Este endpoint permite verificar en qué número va cada\ntipo antes de emitir, y el endpoint `PUT` permite ajustarlo.\n\n**Tipos de comprobante (01–10):**\n\n| Código | Tipo | Abrev. |\n|--------|------------------------------------|--------|\n| 01 | Factura Electrónica | FE |\n| 02 | Nota de Débito Electrónica | ND |\n| 03 | Nota de Crédito Electrónica | NC |\n| 04 | Tiquete Electrónico | TE |\n| 05 | Confirmación Aceptación Total | CA |\n| 06 | Confirmación Aceptación Parcial | CP |\n| 07 | Confirmación Rechazo | CR |\n| 08 | Factura Electrónica de Compra | FEC |\n| 09 | Factura Electrónica de Exportación | FEE |\n| 10 | Recibo Electrónico de Pago | REP |\n\nLas secuencias se crean **automáticamente** al emitir el primer\ncomprobante de cada tipo. Si nunca se ha emitido una FEC (tipo 08),\nno existirá secuencia para ella — eso es normal."
parameters:
-
in: query
name: include_all_terminals
description: 'Si `true`, incluye secuencias de TODAS las terminales del cliente (diagnóstico). Default: `false`.'
example: false
required: false
schema:
type: boolean
description: 'Si `true`, incluye secuencias de TODAS las terminales del cliente (diagnóstico). Default: `false`.'
example: false
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Secuencias de la terminal del integrador'
type: object
example:
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'
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
id:
type: integer
example: 42
local_code:
type: string
example: '001'
terminal_code:
type: string
example: '00002'
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
voucher_type_abbr:
type: string
example: FE
current_sequence:
type: integer
example: 9992
next_sequence:
type: integer
example: 9993
last_consecutive:
type: string
example: '00100002010000009992'
next_consecutive:
type: string
example: '00100002010000009993'
last_issued_at:
type: string
example: '2026-04-20T14:30:00-06:00'
last_issued_key:
type: string
example: null
nullable: true
is_active:
type: boolean
example: true
is_near_rollover:
type: boolean
example: false
display_key:
type: string
example: 001-00002-01
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
terminal:
type: string
example: '00002'
-
description: 'Sin secuencias (cliente recién integrado)'
type: object
example:
success: true
data: []
message: 'No hay secuencias creadas aún para la terminal 00002.'
errors: null
terminal: '00002'
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: 'No hay secuencias creadas aún para la terminal 00002.'
errors:
type: string
example: null
nullable: true
terminal:
type: string
example: '00002'
403:
description: 'Plan no es Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
404:
description: 'Cliente no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Cliente no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Cliente no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Clientes del Integrador'
post:
summary: 'Inicializar consecutivo para un tipo de comprobante'
operationId: inicializarConsecutivoParaUnTipoDeComprobante
description: "Crea una secuencia con un valor inicial para un tipo de comprobante\nque aún no tiene secuencia en la terminal del integrador. **Crítico\npara migración desde otro facturador.**\n\n**Caso de uso:** el cliente usaba Alegra y su última FE fue la 8475.\nSi AlmendroFEC emite desde 1, Hacienda rechaza por \"consecutivo\nduplicado\". Con este endpoint, setee `current_sequence=8475` para\nel tipo `01` → el próximo comprobante usará 8476.\n\n**Importante:** si la secuencia ya existe (porque ya se emitió al\nmenos un comprobante de ese tipo), use `PUT /sequences/{seqId}`\npara ajustarla.\n\n**Seguridad:** Requiere confirmación de contraseña."
parameters: []
responses:
201:
description: 'Secuencia inicializada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 100
local_code:
type: string
example: '001'
terminal_code:
type: string
example: '00002'
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
voucher_type_abbr:
type: string
example: FE
current_sequence:
type: integer
example: 8475
next_sequence:
type: integer
example: 8476
last_consecutive:
type: string
example: '00100002010000008475'
next_consecutive:
type: string
example: '00100002010000008476'
last_issued_at:
type: string
example: null
nullable: true
last_issued_key:
type: string
example: null
nullable: true
is_active:
type: boolean
example: true
is_near_rollover:
type: boolean
example: false
display_key:
type: string
example: 001-00002-01
message:
type: string
example: 'Secuencia tipo 01 (FE) inicializada en 8475. Próximo comprobante usará 8476.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Secuencia ya existe'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ya existe una secuencia para el tipo 01 en la terminal 00002. Use PUT para ajustarla.'
errors:
type: object
properties:
voucher_type:
type: array
example:
- 'Ya existe. Use PUT /sequences/{id} para ajustar.'
items:
type: string
-
description: 'Contraseña incorrecta'
type: object
example:
success: false
data: null
message: 'La contraseña es incorrecta.'
errors:
password:
- 'La contraseña proporcionada no coincide con la del usuario autenticado.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'La contraseña es incorrecta.'
errors:
type: object
properties:
password:
type: array
example:
- 'La contraseña proporcionada no coincide con la del usuario autenticado.'
items:
type: string
tags:
- 'Clientes del Integrador'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
voucher_type:
type: string
description: 'Tipo de comprobante: 01 (FE), 02 (ND), 03 (NC), 04 (TE), 08 (FEC), 09 (FEE), 10 (REP).'
example: '01'
current_sequence:
type: integer
description: 'Último consecutivo emitido en el facturador anterior. Próximo = este + 1. Rango: 0–9,999,999,999.'
example: 8475
reason:
type: string
description: 'Motivo (mín. 10 chars, bitácora Art. 6).'
example: 'Migración desde Alegra — último consecutivo tipo 01 fue 8475.'
password:
type: string
description: 'Contraseña actual del usuario.'
example: mi_contraseña
required:
- voucher_type
- current_sequence
- reason
- password
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
'/my-contributors/{id}/sequences/{seqId}':
put:
summary: 'Ajustar el consecutivo de una secuencia del cliente'
operationId: ajustarElConsecutivoDeUnaSecuenciaDelCliente
description: "Cambia manualmente el `current_sequence` de una secuencia. **El\npróximo comprobante emitido usará `current_sequence + 1`.**\n\n**Cuándo usar:**\n- Migración de facturador: el cliente usaba Alegra y su última FE\n fue la 9992. Setee `current_sequence=9992` → AlmendroFEC\n continúa desde 9993.\n- Corrección post-contingencia: ajustar al valor real.\n\n**Seguridad:** Requiere confirmación de contraseña.\n\n> ⚠️ Setear un valor **menor** al actual puede generar\n> consecutivos duplicados que Hacienda rechazará. Se permite\n> pero se registra advertencia en la bitácora."
parameters: []
responses:
200:
description: 'Secuencia ajustada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 42
local_code:
type: string
example: '001'
terminal_code:
type: string
example: '00002'
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
voucher_type_abbr:
type: string
example: FE
current_sequence:
type: integer
example: 9992
next_sequence:
type: integer
example: 9993
last_consecutive:
type: string
example: '00100002010000009992'
next_consecutive:
type: string
example: '00100002010000009993'
last_issued_at:
type: string
example: null
nullable: true
last_issued_key:
type: string
example: null
nullable: true
is_active:
type: boolean
example: true
is_near_rollover:
type: boolean
example: false
display_key:
type: string
example: 001-00002-01
message:
type: string
example: 'Consecutivo ajustado: tipo 01 (FE) actualizado de 0 a 9992. Próximo comprobante usará 9993.'
errors:
type: string
example: null
nullable: true
403:
description: 'Secuencia de otra terminal'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Solo puede ajustar secuencias de su propia terminal (00002).'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Solo puede ajustar secuencias de su propia terminal (00002).'
errors:
type: string
example: null
nullable: true
404:
description: 'Secuencia no encontrada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Secuencia no encontrada para este cliente.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Secuencia no encontrada para este cliente.'
errors:
type: string
example: null
nullable: true
422:
description: 'Contraseña incorrecta'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'La contraseña es incorrecta.'
errors:
password:
- 'La contraseña proporcionada no coincide con la del usuario autenticado.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'La contraseña es incorrecta.'
errors:
type: object
properties:
password:
type: array
example:
- 'La contraseña proporcionada no coincide con la del usuario autenticado.'
items:
type: string
tags:
- 'Clientes del Integrador'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
current_sequence:
type: integer
description: 'Último consecutivo emitido. Próximo = este + 1. Rango: 0–9,999,999,999.'
example: 9992
reason:
type: string
description: 'Motivo del ajuste (mín. 10 chars, bitácora art. 6).'
example: 'Migración desde Alegra — último consecutivo tipo 01 fue 9992.'
password:
type: string
description: 'Contraseña actual del usuario (confirmación de seguridad).'
example: mi_contraseña
required:
- current_sequence
- reason
- password
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
-
in: path
name: seqId
description: 'ID de la secuencia (del listado).'
example: 42
required: true
schema:
type: integer
'/my-contributors/{id}/terminal':
put:
summary: 'Cambiar la terminal asignada al integrador para este cliente'
operationId: cambiarLaTerminalAsignadaAlIntegradorParaEsteCliente
description: "Modifica la `assigned_terminal` en la relación de gestión. La\nterminal ocupa la posición 04-08 del NumeroConsecutivo (20 dígitos).\n\n**Cuándo usar:**\n- Migración: la terminal 00002 ya estaba comprometida → cambiar a 00003.\n- Reorganización operativa de terminales.\n\n**`migrate_sequences`:**\n- `false` (default): nueva terminal arranca limpia (secuencias en 0).\n- `true`: copia `current_sequence` de cada tipo desde la terminal\n anterior a la nueva.\n\n**Seguridad:** Requiere confirmación de contraseña.\n\n> Las secuencias de la terminal anterior NO se eliminan — los\n> vouchers ya emitidos las referencian."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Terminal cambiada con migración'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
previous_terminal:
type: string
example: '00002'
new_terminal:
type: string
example: '00003'
sequences_migrated:
type: integer
example: 3
sequences:
type: array
example:
-
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
items:
type: object
properties:
id:
type: integer
example: 100
local_code:
type: string
example: '001'
terminal_code:
type: string
example: '00003'
voucher_type:
type: string
example: '01'
voucher_type_label:
type: string
example: 'Factura Electrónica'
voucher_type_abbr:
type: string
example: FE
current_sequence:
type: integer
example: 9992
next_sequence:
type: integer
example: 9993
last_consecutive:
type: string
example: '00100003010000009992'
next_consecutive:
type: string
example: '00100003010000009993'
last_issued_at:
type: string
example: null
nullable: true
last_issued_key:
type: string
example: null
nullable: true
is_active:
type: boolean
example: true
is_near_rollover:
type: boolean
example: false
display_key:
type: string
example: 001-00003-01
message:
type: string
example: 'Terminal cambiada de 00002 a 00003. Se migraron 3 secuencias.'
errors:
type: string
example: null
nullable: true
-
description: 'Terminal cambiada sin migración'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
previous_terminal:
type: string
example: '00002'
new_terminal:
type: string
example: '00003'
sequences_migrated:
type: integer
example: 0
sequences:
type: array
example: []
message:
type: string
example: 'Terminal cambiada de 00002 a 00003. Las secuencias se crearán al emitir.'
errors:
type: string
example: null
nullable: true
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Terminal colisiona con otro integrador'
type: object
example:
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.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'La terminal 00003 ya está asignada a otro integrador de este cliente.'
errors:
type: object
properties:
assigned_terminal:
type: array
example:
- 'La terminal 00003 ya está asignada a otro integrador de este cliente.'
items:
type: string
-
description: 'Contraseña incorrecta'
type: object
example:
success: false
data: null
message: 'La contraseña es incorrecta.'
errors:
password:
- 'La contraseña proporcionada no coincide con la del usuario autenticado.'
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'La contraseña es incorrecta.'
errors:
type: object
properties:
password:
type: array
example:
- 'La contraseña proporcionada no coincide con la del usuario autenticado.'
items:
type: string
tags:
- 'Clientes del Integrador'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
assigned_terminal:
type: string
description: 'Nueva terminal de 5 dígitos (00002–99999).'
example: '00003'
local_code:
type: string
description: 'Código de sucursal de 3 dígitos numéricos (posición 01-03 del NumeroConsecutivo, pág. 65 Anexos). Rango 001–999. 001 = casa matriz. Opcional — si no se envía, se mantiene el actual.'
example: '001'
migrate_sequences:
type: boolean
description: 'Copiar secuencias de la terminal anterior. Default: `false`.'
example: true
reason:
type: string
description: 'Motivo del cambio (mín. 10 chars, bitácora art. 6).'
example: 'Terminal 00002 comprometida con otro facturador.'
password:
type: string
description: 'Contraseña actual del usuario (confirmación de seguridad).'
example: mi_contraseña
required:
- assigned_terminal
- reason
- password
parameters:
-
in: path
name: id
description: 'UUID del cliente gestionado.'
example: 019d867d-c001-7288-8ece-fd64da756c01
required: true
schema:
type: string
/my-integrators:
get:
summary: 'Listar los integradores que actualmente gestionan al contribuyente autenticado.'
operationId: listarLosIntegradoresQueActualmenteGestionanAlContribuyenteAutenticado
description: "Devuelve las relaciones managed ACTIVAS (unlinked_at IS NULL)\ndonde el tenant autenticado es el client_contributor_id. Cada\nentrada incluye el integrador, terminal asignada, grants activos\npor ambiente, retention_months_override y plantilla PDF default."
parameters:
-
in: query
name: per_page
description: 'Resultados por página (1-100, default 15).'
example: 15
required: false
schema:
type: integer
description: 'Resultados por página (1-100, default 15).'
example: 15
-
in: query
name: page
description: 'Número de página.'
example: 1
required: false
schema:
type: integer
description: 'Número de página.'
example: 1
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Integradores activos'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
relationship_id:
type: string
example: 019d870a-0001-7288-8ece-fd64da75a001
integrator:
type: object
properties:
id:
type: string
example: 019d865a-1234-7000-a000-abcdef123456
legal_name:
type: string
example: 'Sistemas Integrados S.A.'
id_number:
type: string
example: '3101999999'
assigned_terminal:
type: string
example: '00002'
linked_at:
type: string
example: '2026-04-10T09:00:00-06:00'
grants:
type: array
example:
-
environment: sandbox
is_active: true
items:
type: object
properties:
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
-
description: 'Sin integradores'
type: object
example:
success: true
data: []
message: ''
errors: null
meta:
current_page: 1
last_page: 1
per_page: 15
total: 0
properties:
success:
type: boolean
example: true
data:
type: array
example: []
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 0
tags:
- 'Mis Integradores'
'/my-integrators/{integratorContributorId}':
get:
summary: 'Consultar detalle de un integrador específico que gestiona al cliente.'
operationId: consultarDetalleDeUnIntegradorEspecficoQueGestionaAlCliente
description: ''
parameters: []
responses:
200:
description: 'Detalle del integrador'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
relationship_id:
type: string
example: 019d870a-0001-7288-8ece-fd64da75a001
integrator:
type: object
properties:
id:
type: string
example: 019d865a-1234-7000-a000-abcdef123456
legal_name:
type: string
example: 'Sistemas Integrados S.A.'
id_number:
type: string
example: '3101999999'
id_type:
type: string
example: '02'
emails:
type: array
example:
- soporte@integrador.cr
items:
type: string
assigned_terminal:
type: string
example: '00002'
retention_months_override:
type: string
example: null
nullable: true
default_pdf_template:
type: string
example: null
nullable: true
linked_at:
type: string
example: '2026-04-10T09:00:00-06:00'
grants:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: string
example: 019d870b-0001-7288-8ece-fd64da75b001
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
certificate:
type: object
properties:
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
valid_until:
type: string
example: '2028-01-01T00:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
404:
description: 'Integrador no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Integrador no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Integrador no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Mis Integradores'
delete:
summary: 'Liberarse de un integrador específico.'
operationId: liberarseDeUnIntegradorEspecfico
description: "El cliente rompe el vínculo managed con un integrador concreto.\nEfectos (ver UnlinkManagedRelationshipAction):\n\n · ManagedRelationship.unlinked_at = now() con metadata de auditoría.\n · Cascade revoke de TODOS los grants activos del integrador sobre\n los .p12 del cliente (vía RevokeGrantAction, escenario\n GrantAutoRevoked → notifica al integrador).\n · Otros integradores del cliente NO se afectan (modelo N:N).\n · El contributor del cliente permanece intacto — NO se soft-deletea.\n\nIrreversible: si ambas partes quieren retomar, el integrador debe\niniciar una nueva IntegratorAccessRequest desde cero. La nueva\nrelación recibirá una terminal distinta."
parameters: []
responses:
200:
description: 'Integrador desvinculado'
content:
application/json:
schema:
type: object
example:
success: true
data: null
message: 'Se liberó del integrador. Los accesos asociados fueron revocados.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Se liberó del integrador. Los accesos asociados fueron revocados.'
errors:
type: string
example: null
nullable: true
404:
description: 'Integrador no encontrado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Integrador no encontrado.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Integrador no encontrado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Mis Integradores'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
reason:
type: string
description: 'Motivo opcional (máx 500 chars).'
example: 'Cambio de proveedor tecnológico.'
nullable: true
parameters:
-
in: path
name: integratorContributorId
description: 'UUID del integrador.'
example: 9c1b4e5a-...
required: true
schema:
type: string
/access-requests:
post:
summary: 'Crear una solicitud de acceso hacia un cliente.'
operationId: crearUnaSolicitudDeAccesoHaciaUnCliente
description: "El integrador envia la cedula del cliente y los ambientes a los\nque desea acceder. Se crea una solicitud en estado `pending` y se\nnotifica al cliente por email y notificacion en el portal para\nque acepte o rechace."
parameters: []
responses:
201:
description: 'Solicitud creada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 1
description: 'ID numérico de la solicitud de acceso.'
status:
type: string
example: pending
description: 'Estado actual: `pending`, `accepted`, `partially_accepted`, `rejected`, `cancelled`.'
status_label:
type: string
example: Pendiente
description: 'Nombre legible del estado en español.'
status_color:
type: string
example: warning
description: 'Color sugerido para la UI: `warning`, `success`, `danger`, `secondary`.'
is_pending:
type: boolean
example: true
description: '`true` si la solicitud aún espera respuesta del cliente.'
is_final:
type: boolean
example: false
description: '`true` si la solicitud ya fue resuelta (aceptada, rechazada o cancelada).'
resulted_in_grants:
type: boolean
example: false
description: '`true` si la aceptación generó al menos un acceso (CertificateAccessGrant).'
was_fully_accepted:
type: boolean
example: false
description: '`true` si todos los ambientes solicitados fueron aceptados. `false` si fue parcial o rechazada.'
can_be_responded:
type: boolean
example: true
description: '`true` si el cliente aún puede aceptar/rechazar (estado pendiente).'
can_be_cancelled:
type: boolean
example: true
description: '`true` si el integrador puede cancelar (estado pendiente).'
requested_environments:
type: array
example:
- sandbox
- production
description: 'Ambientes solicitados por el integrador: `["sandbox"]`, `["production"]` o `["sandbox", "production"]`.'
items:
type: string
accepted_environments:
type: string
example: null
description: 'Ambientes aceptados por el cliente. `null` si pendiente o rechazada. Puede ser subconjunto de `requested_environments` (aceptación parcial).'
message:
type: string
example: 'Solicitamos acceso para integrar su facturación con nuestro POS.'
description: 'Mensaje opcional del integrador al cliente justificando la solicitud. `null` si no se envió.'
rejection_reason:
type: string
example: null
description: 'Razón del rechazo proporcionada por el cliente. `null` si no aplica.'
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
description: 'UUID del contribuyente cliente.'
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
description: 'Razón social del cliente.'
commercial_name:
type: string
example: 'Las Palmas'
description: 'Nombre comercial. `null` si no tiene.'
id_type:
type: string
example: '02'
description: 'Tipo de identificación: `01`-`06`.'
id_number:
type: string
example: '3101456789'
description: 'Número de identificación.'
display_name:
type: string
example: 'Las Palmas'
description: 'Nombre para mostrar (commercial_name o legal_name).'
description: 'Datos del cliente (receptor de la solicitud).'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
display_name:
type: string
example: SistemasPOS
description: 'Datos del integrador (emisor de la solicitud). Misma estructura que `client`.'
requested_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha/hora en que se creó la solicitud (ISO 8601).'
responded_at:
type: string
example: null
description: 'Fecha/hora en que el cliente respondió (ISO 8601). `null` si pendiente.'
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
description: 'Fecha de creación del registro (ISO 8601).'
message:
type: string
example: 'Solicitud de acceso creada. El cliente recibirá un email y notificación en su portal.'
errors:
type: string
example: null
nullable: true
422:
description: 'Error de validación'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Ya existe una solicitud pendiente para este cliente.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Ya existe una solicitud pendiente para este cliente.'
errors:
type: string
example: null
nullable: true
tags:
- 'Solicitudes de Acceso'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
client_id_number:
type: string
description: 'Cédula del cliente al que se solicita acceso. Debe corresponder a un Contributor existente en la plataforma. Must match the regex /^[0-9A-Z\-]+$/i. validation.min validation.max.'
example: '3101000050'
environments:
type: array
description: 'Cada ambiente debe ser "sandbox" o "production".'
example:
- sandbox
items:
type: string
enum:
- sandbox
- production
message:
type: string
description: 'Mensaje opcional al cliente (máx 500 caracteres). Se muestra en el email + dashboard al revisar la solicitud. validation.max.'
example: 'Hola, somos su proveedor tecnológico y gustaría gestionar su facturación.'
nullable: true
required:
- client_id_number
- environments
/access-requests/sent:
get:
summary: 'Listar solicitudes de acceso enviadas por el integrador.'
operationId: listarSolicitudesDeAccesoEnviadasPorElIntegrador
description: "Retorna el historial completo de solicitudes enviadas por el\nintegrador autenticado, incluyendo todos los estados (pendientes,\naceptadas, rechazadas, canceladas). Paginacion de 15 por pagina."
parameters:
-
in: query
name: status
description: 'Filtrar por estado: `pending`, `accepted`, `partially_accepted`, `rejected`, `cancelled`.'
example: null
required: false
schema:
type: string
description: 'Filtrar por estado: `pending`, `accepted`, `partially_accepted`, `rejected`, `cancelled`.'
example: null
-
in: query
name: per_page
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
required: false
schema:
type: integer
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
responses:
200:
description: 'Solicitudes enviadas por el integrador'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
id:
type: integer
example: 1
status:
type: string
example: accepted
status_label:
type: string
example: Aceptada
status_color:
type: string
example: success
is_pending:
type: boolean
example: false
is_final:
type: boolean
example: true
resulted_in_grants:
type: boolean
example: true
was_fully_accepted:
type: boolean
example: true
can_be_responded:
type: boolean
example: false
can_be_cancelled:
type: boolean
example: false
requested_environments:
type: array
example:
- sandbox
- production
items:
type: string
accepted_environments:
type: array
example:
- sandbox
- production
items:
type: string
message:
type: string
example: 'Solicitamos acceso para integrar su facturación.'
rejection_reason:
type: string
example: null
nullable: true
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
display_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
commercial_name:
type: string
example: 'Las Palmas'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
display_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
commercial_name:
type: string
example: SistemasPOS
requested_at:
type: string
example: '2026-04-10T10:00:00-06:00'
responded_at:
type: string
example: '2026-04-11T08:00:00-06:00'
created_at:
type: string
example: '2026-04-10T10:00:00-06:00'
grants_count:
type: integer
example: 2
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- 'Solicitudes de Acceso'
/access-requests/received:
get:
summary: 'Listar solicitudes de acceso recibidas por el cliente.'
operationId: listarSolicitudesDeAccesoRecibidasPorElCliente
description: "El cliente ve que integradores le han solicitado acceso a sus\ncertificados digitales. Las solicitudes pendientes aparecen primero."
parameters:
-
in: query
name: status
description: 'Filtrar por estado.'
example: null
required: false
schema:
type: string
description: 'Filtrar por estado.'
example: null
-
in: query
name: per_page
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
required: false
schema:
type: integer
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
responses:
200:
description: 'Solicitudes recibidas por el cliente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: integer
example: 2
status:
type: string
example: pending
status_label:
type: string
example: Pendiente
status_color:
type: string
example: warning
is_pending:
type: boolean
example: true
is_final:
type: boolean
example: false
resulted_in_grants:
type: boolean
example: false
was_fully_accepted:
type: boolean
example: false
can_be_responded:
type: boolean
example: true
can_be_cancelled:
type: boolean
example: false
requested_environments:
type: array
example:
- sandbox
items:
type: string
accepted_environments:
type: string
example: null
nullable: true
message:
type: string
example: 'Necesitamos acceso para pruebas de integración.'
rejection_reason:
type: string
example: null
nullable: true
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
display_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
commercial_name:
type: string
example: 'Las Palmas'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
display_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
commercial_name:
type: string
example: SistemasPOS
requested_at:
type: string
example: '2026-04-15T14:00:00-06:00'
responded_at:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-04-15T14:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- 'Solicitudes de Acceso'
'/access-requests/{id}/accept':
post:
summary: 'Aceptar una solicitud de acceso (total o parcialmente).'
operationId: aceptarUnaSolicitudDeAccesototalOParcialmente
description: "El cliente elige que ambientes autoriza. Se crean los accesos\ncorrespondientes con terminal asignada. El integrador recibe\nnotificacion por email y en su portal."
parameters: []
responses:
200:
description: 'Solicitud aceptada completamente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 1
status:
type: string
example: accepted
status_label:
type: string
example: Aceptada
status_color:
type: string
example: success
is_pending:
type: boolean
example: false
is_final:
type: boolean
example: true
resulted_in_grants:
type: boolean
example: true
was_fully_accepted:
type: boolean
example: true
can_be_responded:
type: boolean
example: false
can_be_cancelled:
type: boolean
example: false
requested_environments:
type: array
example:
- sandbox
- production
items:
type: string
accepted_environments:
type: array
example:
- sandbox
- production
items:
type: string
message:
type: string
example: 'Solicitamos acceso para integrar su facturación.'
rejection_reason:
type: string
example: null
nullable: true
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
display_name:
type: string
example: 'Las Palmas'
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
commercial_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
display_name:
type: string
example: SistemasPOS
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
requested_at:
type: string
example: '2026-04-10T10:00:00-06:00'
responded_at:
type: string
example: '2026-04-16T11:00:00-06:00'
created_at:
type: string
example: '2026-04-10T10:00:00-06:00'
grants_count:
type: integer
example: 2
message:
type: string
example: 'Solicitud aceptada correctamente. El integrador recibirá notificación y email.'
errors:
type: string
example: null
nullable: true
422:
description: 'Error de validación'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Solo se pueden responder solicitudes en estado pendiente.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Solo se pueden responder solicitudes en estado pendiente.'
errors:
type: string
example: null
nullable: true
tags:
- 'Solicitudes de Acceso'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
rejection_reason:
type: string
description: 'OPCIONAL en /reject. Motivo del rechazo (texto libre, máx 500 chars). IGNORADO en /accept. validation.max.'
example: 'Ya tenemos un proveedor asignado para esta tarea.'
nullable: true
accepted_environments:
type: array
description: 'Cada ambiente debe ser "sandbox" o "production".'
example:
- sandbox
items:
type: string
parameters:
-
in: path
name: id
description: 'ID de la solicitud a aceptar.'
example: 16
required: true
schema:
type: integer
'/access-requests/{id}/reject':
post:
summary: 'Rechazar una solicitud de acceso.'
operationId: rechazarUnaSolicitudDeAcceso
description: "No se crean accesos. El integrador recibe notificacion con el\nmotivo opcional del rechazo."
parameters: []
responses:
200:
description: 'Solicitud rechazada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 3
status:
type: string
example: rejected
status_label:
type: string
example: Rechazada
status_color:
type: string
example: danger
is_pending:
type: boolean
example: false
is_final:
type: boolean
example: true
resulted_in_grants:
type: boolean
example: false
was_fully_accepted:
type: boolean
example: false
can_be_responded:
type: boolean
example: false
can_be_cancelled:
type: boolean
example: false
requested_environments:
type: array
example:
- sandbox
- production
items:
type: string
accepted_environments:
type: string
example: null
nullable: true
message:
type: string
example: 'Solicitamos acceso para integrar su facturación.'
rejection_reason:
type: string
example: 'Ya contamos con otro proveedor de integración.'
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
display_name:
type: string
example: 'Las Palmas'
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
commercial_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
display_name:
type: string
example: SistemasPOS
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
requested_at:
type: string
example: '2026-04-10T10:00:00-06:00'
responded_at:
type: string
example: '2026-04-16T12:00:00-06:00'
created_at:
type: string
example: '2026-04-10T10:00:00-06:00'
message:
type: string
example: 'Solicitud rechazada. El integrador recibirá notificación.'
errors:
type: string
example: null
nullable: true
tags:
- 'Solicitudes de Acceso'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
rejection_reason:
type: string
description: 'OPCIONAL en /reject. Motivo del rechazo (texto libre, máx 500 chars). IGNORADO en /accept. validation.max.'
example: 'Ya tenemos un proveedor asignado para esta tarea.'
nullable: true
accepted_environments:
type: array
description: 'Cada ambiente debe ser "sandbox" o "production".'
example:
- sandbox
items:
type: string
parameters:
-
in: path
name: id
description: 'ID de la solicitud a rechazar.'
example: 16
required: true
schema:
type: integer
'/access-requests/{id}/cancel':
post:
summary: 'Cancelar una solicitud de acceso antes de que el cliente responda.'
operationId: cancelarUnaSolicitudDeAccesoAntesDeQueElClienteResponda
description: "Solo se puede cancelar si la solicitud esta en estado `pending`.\nEl cliente recibe una notificacion informativa."
parameters: []
responses:
200:
description: 'Solicitud cancelada'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 4
status:
type: string
example: cancelled
status_label:
type: string
example: Cancelada
status_color:
type: string
example: secondary
is_pending:
type: boolean
example: false
is_final:
type: boolean
example: true
resulted_in_grants:
type: boolean
example: false
was_fully_accepted:
type: boolean
example: false
can_be_responded:
type: boolean
example: false
can_be_cancelled:
type: boolean
example: false
requested_environments:
type: array
example:
- sandbox
items:
type: string
accepted_environments:
type: string
example: null
nullable: true
message:
type: string
example: null
nullable: true
rejection_reason:
type: string
example: null
nullable: true
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
display_name:
type: string
example: 'Las Palmas'
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
commercial_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
display_name:
type: string
example: SistemasPOS
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
requested_at:
type: string
example: '2026-04-14T09:00:00-06:00'
responded_at:
type: string
example: '2026-04-16T13:00:00-06:00'
created_at:
type: string
example: '2026-04-14T09:00:00-06:00'
message:
type: string
example: 'Solicitud cancelada correctamente.'
errors:
type: string
example: null
nullable: true
404:
description: 'No encontrada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Solicitud no encontrada.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Solicitud no encontrada.'
errors:
type: string
example: null
nullable: true
422:
description: 'Estado no permite cancelación'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'La solicitud no puede cancelarse en su estado actual (accepted). Solo se pueden cancelar solicitudes pendientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'La solicitud no puede cancelarse en su estado actual (accepted). Solo se pueden cancelar solicitudes pendientes.'
errors:
type: string
example: null
nullable: true
tags:
- 'Solicitudes de Acceso'
parameters:
-
in: path
name: id
description: 'ID de la solicitud a cancelar.'
example: 16
required: true
schema:
type: integer
/certificate-grants:
get:
summary: 'Listar los accesos otorgados sobre los certificados del cliente.'
operationId: listarLosAccesosOtorgadosSobreLosCertificadosDelCliente
description: "El cliente ve que integradores tienen permiso de firmar con sus\ncertificados digitales y en que ambiente. Incluye accesos activos\ny revocados (historial completo). Los activos se muestran primero."
parameters:
-
in: query
name: active
description: 'Filtrar: `true` = solo activos, `false` = solo revocados. Omitir para ver todos.'
example: false
required: false
schema:
type: boolean
description: 'Filtrar: `true` = solo activos, `false` = solo revocados. Omitir para ver todos.'
example: false
-
in: query
name: environment
description: 'Filtrar por ambiente: `sandbox` o `production`.'
example: null
required: false
schema:
type: string
description: 'Filtrar por ambiente: `sandbox` o `production`.'
example: null
-
in: query
name: per_page
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
required: false
schema:
type: integer
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
responses:
200:
description: 'Accesos del cliente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: integer
example: 1
assigned_terminal:
type: string
example: '00002'
is_active:
type: boolean
example: true
is_revoked:
type: boolean
example: false
certificate:
type: object
properties:
id:
type: string
example: 019d867d-d001-7288-8ece-fd64da756d01
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 1440
valid_from:
type: string
example: '2026-01-01T00:00:00-06:00'
valid_until:
type: string
example: '2030-01-01T00:00:00-06:00'
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
commercial_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
display_name:
type: string
example: 'Las Palmas'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
display_name:
type: string
example: SistemasPOS
access_request_id:
type: integer
example: 1
granted_at:
type: string
example: '2026-04-11T08:00:00-06:00'
revoked_at:
type: string
example: null
nullable: true
revoke_reason:
type: string
example: null
nullable: true
revoked_by:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-04-11T08:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- 'Grants de Certificados'
'/certificate-grants/{id}':
delete:
summary: 'Revocar el acceso de un integrador a un certificado.'
operationId: revocarElAccesoDeUnIntegradorAUnCertificado
description: "El integrador pierde la capacidad de firmar comprobantes con este\ncertificado. Los comprobantes previamente emitidos siguen siendo\nvalidos. El integrador recibe notificacion de la revocacion."
parameters: []
responses:
200:
description: 'Acceso revocado'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 1
assigned_terminal:
type: string
example: '00002'
is_active:
type: boolean
example: false
is_revoked:
type: boolean
example: true
certificate:
type: object
properties:
id:
type: string
example: 019d867d-d001-7288-8ece-fd64da756d01
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 1440
valid_from:
type: string
example: '2026-01-01T00:00:00-06:00'
valid_until:
type: string
example: '2030-01-01T00:00:00-06:00'
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
commercial_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
display_name:
type: string
example: 'Las Palmas'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
display_name:
type: string
example: SistemasPOS
access_request_id:
type: integer
example: 1
granted_at:
type: string
example: '2026-04-11T08:00:00-06:00'
revoked_at:
type: string
example: '2026-04-16T14:00:00-06:00'
revoke_reason:
type: string
example: 'Cambio de proveedor de integración.'
revoked_by:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
display_name:
type: string
example: 'Las Palmas'
created_at:
type: string
example: '2026-04-11T08:00:00-06:00'
message:
type: string
example: 'Acceso revocado correctamente. El integrador ya no puede emitir con este certificado.'
errors:
type: string
example: null
nullable: true
422:
description: 'Grant ya revocado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este acceso ya fue revocado anteriormente.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este acceso ya fue revocado anteriormente.'
errors:
type: string
example: null
nullable: true
tags:
- 'Grants de Certificados'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
reason:
type: string
description: 'Motivo opcional de la revocacion (max 500 caracteres).'
example: architecto
nullable: true
parameters:
-
in: path
name: id
description: 'ID del grant a revocar.'
example: 16
required: true
schema:
type: integer
/my-access:
get:
summary: 'Listar los accesos recibidos por el integrador autenticado.'
operationId: listarLosAccesosRecibidosPorElIntegradorAutenticado
description: "El integrador ve sobre que certificados de que clientes tiene\nacceso activo (o revocado, según filtro). Cada grant incluye el\ncert, el cliente, la terminal asignada y el ambiente.\n\nOpcionalmente filtrable por `client_id` para obtener los grants\ndel integrador sobre un cliente específico — útil en el perfil\ndel managed client para mostrar la sección \"Mi acceso\".\n\n Requiere plan INTEGRATOR. Retorna 403 para cualquier otro plan.\n Espeja la semántica de PublicManagedContributorController."
parameters:
-
in: query
name: client_id
description: 'UUID del cliente managed para filtrar grants hacia ese cliente.'
example: null
required: false
schema:
type: string
description: 'UUID del cliente managed para filtrar grants hacia ese cliente.'
example: null
-
in: query
name: active
description: 'Filtrar: `true` = solo activos, `false` = solo revocados. Omitir para ver todos.'
example: false
required: false
schema:
type: boolean
description: 'Filtrar: `true` = solo activos, `false` = solo revocados. Omitir para ver todos.'
example: false
-
in: query
name: environment
description: 'Filtrar por ambiente: `sandbox` o `production`.'
example: null
required: false
schema:
type: string
description: 'Filtrar por ambiente: `sandbox` o `production`.'
example: null
-
in: query
name: per_page
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
required: false
schema:
type: integer
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
responses:
200:
description: 'Accesos del integrador'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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'
items:
type: object
properties:
id:
type: integer
example: 1
assigned_terminal:
type: string
example: '00002'
is_active:
type: boolean
example: true
is_revoked:
type: boolean
example: false
certificate:
type: object
properties:
id:
type: string
example: 019d867d-d001-7288-8ece-fd64da756d01
environment:
type: string
example: sandbox
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 1440
valid_from:
type: string
example: '2026-01-01T00:00:00-06:00'
valid_until:
type: string
example: '2030-01-01T00:00:00-06:00'
client:
type: object
properties:
id:
type: string
example: 019d867d-c001-7288-8ece-fd64da756c01
legal_name:
type: string
example: 'Hotel Las Palmas S.A.'
commercial_name:
type: string
example: 'Las Palmas'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101456789'
display_name:
type: string
example: 'Las Palmas'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
display_name:
type: string
example: SistemasPOS
access_request_id:
type: integer
example: 1
granted_at:
type: string
example: '2026-04-11T08:00:00-06:00'
revoked_at:
type: string
example: null
nullable: true
revoke_reason:
type: string
example: null
nullable: true
revoked_by:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-04-11T08:00:00-06:00'
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 1
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
403:
description: 'Plan no permite gestionar clientes'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Este endpoint solo está disponible para el plan Integrador. Actualice su plan para gestionar clientes.'
errors:
type: string
example: null
nullable: true
tags:
- 'Grants de Certificados'
'/my-access/{grantId}':
delete:
summary: 'El integrador renuncia voluntariamente a un acceso que recibio.'
operationId: elIntegradorRenunciaVoluntariamenteAUnAccesoQueRecibio
description: "Util cuando el integrador deja de gestionar al cliente y quiere\nlimpiar su listado de accesos activos. El cliente es notificado\nde la renuncia."
parameters: []
responses:
200:
description: 'Renuncia exitosa'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
id:
type: integer
example: 3
assigned_terminal:
type: string
example: '00003'
is_active:
type: boolean
example: false
is_revoked:
type: boolean
example: true
certificate:
type: object
properties:
id:
type: string
example: 019d867d-d003-7288-8ece-fd64da756d03
environment:
type: string
example: production
is_active:
type: boolean
example: true
is_expired:
type: boolean
example: false
days_remaining:
type: integer
example: 1200
valid_from:
type: string
example: '2025-06-01T00:00:00-06:00'
valid_until:
type: string
example: '2029-08-01T00:00:00-06:00'
client:
type: object
properties:
id:
type: string
example: 019d867d-c002-7288-8ece-fd64da756c02
legal_name:
type: string
example: 'Restaurante El Mango S.A.'
commercial_name:
type: string
example: 'El Mango'
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101789012'
display_name:
type: string
example: 'El Mango'
integrator:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
commercial_name:
type: string
example: SistemasPOS
id_type:
type: string
example: '02'
id_number:
type: string
example: '3101999888'
display_name:
type: string
example: SistemasPOS
access_request_id:
type: integer
example: 2
granted_at:
type: string
example: '2026-03-15T10:00:00-06:00'
revoked_at:
type: string
example: '2026-04-16T15:00:00-06:00'
revoke_reason:
type: string
example: 'Fin de contrato de servicio.'
revoked_by:
type: object
properties:
id:
type: string
example: 019d867d-0241-7288-8ece-fd64da75616d
legal_name:
type: string
example: 'SistemasPOS de Costa Rica S.A.'
display_name:
type: string
example: SistemasPOS
created_at:
type: string
example: '2026-03-15T10:00:00-06:00'
message:
type: string
example: 'Ha renunciado al acceso correctamente. El cliente será notificado.'
errors:
type: string
example: null
nullable: true
tags:
- 'Grants de Certificados'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
reason:
type: string
description: 'Motivo opcional de la renuncia (max 500 caracteres).'
example: architecto
nullable: true
parameters:
-
in: path
name: grantId
description: 'ID del grant al que renuncia.'
example: 16
required: true
schema:
type: integer
/notifications:
get:
summary: 'Listar notificaciones del usuario autenticado.'
operationId: listarNotificacionesDelUsuarioAutenticado
description: "Retorna las notificaciones personales del usuario y las notificaciones\ngenerales (broadcasts) del contribuyente. Las no leidas aparecen\nprimero, luego por fecha descendente. No incluye notificaciones\nexpiradas.\n\nLa respuesta incluye `unread_count` fuera del array `data` para\nactualizar el badge de notificaciones sin una consulta adicional."
parameters:
-
in: query
name: unread_only
description: 'Solo retornar notificaciones no leidas. Default: false.'
example: false
required: false
schema:
type: boolean
description: 'Solo retornar notificaciones no leidas. Default: false.'
example: false
-
in: query
name: type
description: 'Filtrar por tipo de notificacion (ej: `access_request_received`).'
example: null
required: false
schema:
type: string
description: 'Filtrar por tipo de notificacion (ej: `access_request_received`).'
example: null
-
in: query
name: per_page
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
required: false
schema:
type: integer
description: 'Resultados por pagina (1-50). Default: 15.'
example: 16
responses:
200:
description: 'Lista con notificaciones'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: array
example:
-
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
items:
type: object
properties:
id:
type: integer
example: 42
type:
type: string
example: access_request_received
type_label:
type: string
example: 'Solicitud de acceso recibida'
type_color:
type: string
example: info
type_icon:
type: string
example: key
type_category:
type: string
example: access_control
title:
type: string
example: 'Nueva solicitud de acceso'
message:
type: string
example: 'SistemasPOS de Costa Rica S.A. solicita acceso a sus certificados digitales.'
action_url:
type: string
example: /portal/access-requests/1
action_label:
type: string
example: 'Revisar solicitud'
has_action:
type: boolean
example: true
metadata:
type: object
properties:
access_request_id:
type: integer
example: 1
integrator_name:
type: string
example: SistemasPOS
is_read:
type: boolean
example: false
is_unread:
type: boolean
example: true
read_at:
type: string
example: null
nullable: true
requires_action:
type: boolean
example: true
is_broadcast:
type: boolean
example: false
is_expired:
type: boolean
example: false
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
expires_at:
type: string
example: null
nullable: true
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
unread_count:
type: integer
example: 3
description: 'Total de notificaciones no leídas del usuario. Incluido fuera del array `data` para actualizar el badge del header sin consulta adicional.'
meta:
type: object
properties:
current_page:
type: integer
example: 1
last_page:
type: integer
example: 1
per_page:
type: integer
example: 15
total:
type: integer
example: 5
links:
type: object
properties:
first:
type: string
example: ...
last:
type: string
example: ...
prev:
type: string
example: null
nullable: true
next:
type: string
example: null
nullable: true
tags:
- Notificaciones
/notifications/read-all:
post:
summary: 'Marcar todas las notificaciones no leidas como leidas.'
operationId: marcarTodasLasNotificacionesNoLeidasComoLeidas
description: "Operacion en lote que marca todas las notificaciones pendientes\ndel usuario como leidas. Retorna la cantidad de notificaciones\nque fueron marcadas (0 si ya estaban todas leidas)."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Notificaciones marcadas'
type: object
example:
success: true
data:
marked_count: 5
unread_count: 0
message: '5 notificaciones marcadas como leídas.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
marked_count:
type: integer
example: 5
unread_count:
type: integer
example: 0
message:
type: string
example: '5 notificaciones marcadas como leídas.'
errors:
type: string
example: null
nullable: true
-
description: 'Sin notificaciones pendientes'
type: object
example:
success: true
data:
marked_count: 0
unread_count: 0
message: 'No hay notificaciones pendientes.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
marked_count:
type: integer
example: 0
unread_count:
type: integer
example: 0
message:
type: string
example: 'No hay notificaciones pendientes.'
errors:
type: string
example: null
nullable: true
tags:
- Notificaciones
'/notifications/{id}/read':
post:
summary: 'Marcar una notificacion como leida.'
operationId: marcarUnaNotificacionComoLeida
description: "Operacion idempotente: si la notificacion ya estaba leida, retorna\n200 con el mismo estado sin error. La respuesta incluye la\nnotificacion actualizada y el nuevo `unread_count`."
parameters: []
responses:
200:
description: 'Notificación marcada como leída'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
notification:
type: object
properties:
id:
type: integer
example: 42
type:
type: string
example: access_request_received
type_label:
type: string
example: 'Solicitud de acceso recibida'
type_color:
type: string
example: info
type_icon:
type: string
example: key
type_category:
type: string
example: access_control
title:
type: string
example: 'Nueva solicitud de acceso'
message:
type: string
example: 'SistemasPOS solicita acceso a sus certificados.'
action_url:
type: string
example: /portal/access-requests/1
action_label:
type: string
example: 'Revisar solicitud'
has_action:
type: boolean
example: true
metadata:
type: object
properties:
access_request_id:
type: integer
example: 1
is_read:
type: boolean
example: true
is_unread:
type: boolean
example: false
read_at:
type: string
example: '2026-04-16T11:00:00-06:00'
requires_action:
type: boolean
example: true
is_broadcast:
type: boolean
example: false
is_expired:
type: boolean
example: false
created_at:
type: string
example: '2026-04-16T10:00:00-06:00'
expires_at:
type: string
example: null
nullable: true
unread_count:
type: integer
example: 2
message:
type: string
example: 'Notificación marcada como leída.'
errors:
type: string
example: null
nullable: true
404:
description: 'No encontrada'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Notificación no encontrada.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Notificación no encontrada.'
errors:
type: string
example: null
nullable: true
tags:
- Notificaciones
parameters:
-
in: path
name: id
description: 'ID de la notificacion a marcar.'
example: 16
required: true
schema:
type: integer
/tokens/current:
get:
summary: 'Consultar metadatos del token API de integración'
operationId: consultarMetadatosDelTokenAPIDeIntegracin
description: "Devuelve información del token API actualmente activo del contribuyente\n(fecha de creación, último uso, permisos) **sin exponer el valor del\ntoken**. Si aún no ha generado un token API, retorna `data: null`.\n\nÚtil para:\n\n- Verificar en el portal cuándo fue el último uso del token (si\n lleva mucho sin usarse, quizá la integración esté caída).\n- Detectar si ya existe un token antes de regenerar.\n\nRecuerde: este endpoint solo muestra **metadatos**. El valor del\ntoken solo se devuelve una vez, en la respuesta de\n`POST /public/tokens`."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Token API existente'
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
name:
type: string
example: api-integration
created_at:
type: string
example: '2026-04-01T10:00:00-06:00'
last_used_at:
type: string
example: '2026-04-16T14:30:00-06:00'
abilities:
type: array
example:
- '*'
items:
type: string
token_hint:
type: string
example: ••••••••••••••••••••••••••••••••
message:
type: string
example: ''
errors:
type: string
example: null
nullable: true
-
description: 'Sin token API generado aún'
type: object
example:
success: true
data: null
message: 'No tiene un token API generado aún.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No tiene un token API generado aún.'
errors:
type: string
example: null
nullable: true
401:
description: 'No autenticado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: Unauthenticated.
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: Unauthenticated.
errors:
type: string
example: null
nullable: true
tags:
- 'Token API'
/tokens:
post:
summary: 'Generar o regenerar el token API Bearer'
operationId: generarORegenerarElTokenAPIBearer
description: "Crea un nuevo token API permanente para uso desde sistemas externos\n(POS, ERP, e-commerce, CRM). Si el contribuyente ya tenía un token\nAPI, el anterior **se revoca automáticamente** al crear el nuevo.\n\n> **Importante:** el valor del token se devuelve en texto plano\n> **una única vez** en esta respuesta, en el campo `bearer_token`.\n> Guárdelo inmediatamente — no hay forma de recuperarlo después.\n> Para saber si ya existe un token (sin exponerlo), use\n> `GET /public/tokens/current`.\n\n**Aislamiento de sesiones:** regenerar el token API **no afecta** a\nsu sesión actual del portal. Puede regenerar el token sin cerrar\nla ventana del navegador.\n\n**Impacto en sistemas productivos:** al regenerar, el token anterior\ndeja de funcionar inmediatamente. Todas sus integraciones que\nusaban el token viejo empezarán a recibir HTTP 401 hasta que\nactualice la configuración con el nuevo token."
parameters: []
responses:
201:
description: 'Token generado exitosamente'
content:
application/json:
schema:
type: object
example:
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
properties:
success:
type: boolean
example: true
data:
type: object
properties:
bearer_token:
type: string
example: 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
description: '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:
type: string
example: '2026-04-16T15:00:00-06:00'
description: 'Fecha de creación del token (ISO 8601).'
message:
type: string
example: 'Token generado. Guárdelo ahora — no se mostrará nuevamente.'
errors:
type: string
example: null
nullable: true
401:
description: 'No autenticado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: Unauthenticated.
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: Unauthenticated.
errors:
type: string
example: null
nullable: true
tags:
- 'Token API'
/upgrade-request:
post:
summary: 'Solicitar upgrade al plan Integrador.'
operationId: solicitarUpgradeAlPlanIntegrador
description: "Envia un correo al usuario con las instrucciones de pago y notifica\nal equipo de AlmendroFEC para procesar la solicitud. No requiere\ncuerpo en el request — la identidad del contribuyente se obtiene\ndel token de autenticacion.\n\nSi el contribuyente ya tiene el plan Integrador activo, retorna 422.\n\nEste endpoint tiene rate limit de 1 solicitud por hora para evitar\nenvios duplicados."
parameters: []
responses:
200:
description: 'Solicitud enviada exitosamente'
content:
application/json:
schema:
type: object
example:
success: true
data:
email_sent_to: usuario@empresa.cr
message: 'Solicitud enviada. Revisá tu correo para las instrucciones de pago.'
errors: null
properties:
success:
type: boolean
example: true
data:
type: object
properties:
email_sent_to:
type: string
example: usuario@empresa.cr
message:
type: string
example: 'Solicitud enviada. Revisá tu correo para las instrucciones de pago.'
errors:
type: string
example: null
nullable: true
404:
description: 'Sin contribuyente asociado'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'No se encontró un contribuyente asociado a esta cuenta.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'No se encontró un contribuyente asociado a esta cuenta.'
errors:
type: string
example: null
nullable: true
422:
description: 'Ya tiene plan Integrador'
content:
application/json:
schema:
type: object
example:
success: false
data: null
message: 'Tu cuenta ya tiene el plan Integrador activo.'
errors: null
properties:
success:
type: boolean
example: false
data:
type: string
example: null
nullable: true
message:
type: string
example: 'Tu cuenta ya tiene el plan Integrador activo.'
errors:
type: string
example: null
nullable: true
tags:
- 'Solicitud de Upgrade'