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'