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### Seguridad y tratamiento de sus datos\n\nEsta es la respuesta técnica a la pregunta que más hacen las empresas antes de integrarse:\nquién puede ver la información que pasa por la API.\n\n**Credenciales de firma cifradas.** El certificado .p12, su contraseña y el PIN de Hacienda\nse guardan cifrados con AES-256-GCM, con una llave dedicada que no forma parte del código\nde la aplicación. Estos valores no se devuelven en ninguna respuesta de la API, ni siquiera\nal dueño del certificado: se usan para firmar y nunca salen del servidor.\n\n**Aislamiento entre contribuyentes.** Cada token pertenece a un contribuyente y toda\nconsulta se filtra por él automáticamente. Además, los endpoints que leen o escriben\ncomprobantes exigen que el token esté vinculado a un contribuyente activo: una petición sin\ncontribuyente asociado se rechaza con `403` antes de tocar la base de datos. No existe\nningún parámetro que permita salir de ese ámbito.\n\nUn integrador solo alcanza los comprobantes de un cliente gestionado cuando existe una\nrelación activa, y el alcance depende del rol de esa relación: en rol **integrador** ve\núnicamente los comprobantes que él mismo emitió por el cliente; en rol **contador** ve la\nfacturación completa del cliente. Lo que el cliente emita por su cuenta o con otro\nproveedor no aparece en el primer caso. El permiso lo otorga el cliente sobre su propio\ncertificado y lo puede revocar cuando quiera.\n\n**Qué ve el personal de Almendro.** El panel administrativo no tiene ninguna pantalla que\nliste, abra o exporte comprobantes de un cliente. Lo que el equipo ve son cifras de\noperación de la cuenta: cantidad de comprobantes emitidos, usuarios, plan y estado de pago.\nLas acciones administrativas sensibles quedan registradas con autor y fecha.\n\n**Respaldos.** Se genera un respaldo diario que se cifra en el servidor con criptografía\nasimétrica: el servidor solo tiene la llave pública, de modo que produce respaldos que él\nmismo no puede leer. La llave privada vive fuera del servidor.\n\n**El límite honesto.** Como en cualquier plataforma en la nube, el personal técnico con\nacceso al servidor tiene acceso material a la base de datos. Eso no lo resuelve el software\nsino el contrato: firmamos acuerdo de confidencialidad y de tratamiento de datos conforme a\nla Ley 8968 de Costa Rica. Los respaldos se almacenan en Estados Unidos, siempre cifrados,\nlo que constituye una transferencia internacional declarada en dicho acuerdo.\n\nEl detalle completo, con la referencia al código que sostiene cada afirmación, está en el\ndocumento de tratamiento de datos que entregamos a solicitud.\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:** support@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: 'Comprobantes / Borradores' description: 'Guarde, edite y emita borradores de comprobante. Útil para flujos de captura por pasos antes de enviar a Hacienda.' - name: 'Buzón de Recibidos' description: 'Cargue y consulte comprobantes recibidos de otros facturadores (XML de terceros). Anote alias y observaciones del receptor.' - name: Secuencias description: 'Administre las secuencias de consecutivos por sucursal, terminal y tipo de comprobante.' - name: 'Sucursales y cajas' description: 'Gestione sucursales (local_code) y cajas/terminales para la numeración de consecutivos.' - name: 'Usuarios de la Cuenta' description: 'Invite, edite y elimine usuarios del contribuyente, con roles y permisos por usuario.' - name: 'Cambio de Contribuyente' description: 'Liste los contribuyentes a los que tiene acceso y cambie el contribuyente activo de la sesión.' - name: 'Suscripción y Facturación' description: 'Consulte su plan y pagos, inicie checkout, suscríbase, cancele la recurrencia o programe un downgrade (pasarela Tilopay).' - name: Soporte description: 'Abra y consulte conversaciones de soporte in-app y responda a los mensajes del equipo de Almendro.' - name: 'Soporte Prioritario' description: 'Consulte, contrate o cancele el nivel de soporte prioritario (Premium / Premium 24-7).' - name: Onboarding description: 'Consulte el estado de onboarding, vea horarios disponibles, agende citas de capacitación y compre sesiones adicionales.' - 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.' - name: 'Perfil del contribuyente' description: '' 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: external_reference description: "Su identificador del documento, tal como lo envió al\n emitir. Coincidencia exacta. Es la forma de averiguar si una emisión se completó cuando la\n respuesta no le llegó: consulte por la referencia de su orden antes de reintentar." example: ORD-12345 required: false schema: type: string description: "Su identificador del documento, tal como lo envió al\n emitir. Coincidencia exacta. Es la forma de averiguar si una emisión se completó cuando la\n respuesta no le llegó: consulte por la referencia de su orden antes de reintentar." example: ORD-12345 - 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 quiere ver.\n Solo para integradores con relación activa. Sin él ve los suyos." example: 019d867d-0241-7288-8ece-fd64da75616d required: false schema: type: string description: "UUID del cliente gestionado cuyos comprobantes quiere ver.\n Solo para integradores con relación activa. Sin él ve los suyos." example: 019d867d-0241-7288-8ece-fd64da75616d - in: query name: scope description: '`all` (default) o `own` para excluir los emitidos a nombre de clientes gestionados.' example: own required: false schema: type: string description: '`all` (default) o `own` para excluir los emitidos a nombre de clientes gestionados.' example: own - 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: - in: header name: Idempotency-Key description: '' example: 'Identificador único de ESTA emisión, generado por usted (recomendado: un' schema: type: string 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 429: description: 'Cupo mensual agotado (plan sin overage)' content: application/json: schema: type: object example: success: false data: null message: 'Límite mensual de comprobantes alcanzado (50/50). Actualice a PyME para emitir más. Período: junio 2026. Precio: $29.99/mes ≈ ₡15,745.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Límite mensual de comprobantes alcanzado (50/50). Actualice a PyME para emitir más. Período: junio 2026. Precio: $29.99/mes ≈ ₡15,745.' errors: type: string example: null nullable: true 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' local_code: type: string description: 'Must match the regex /^\d{3}$/.' example: '564' terminal_code: type: string description: 'Must match the regex /^\d{5}$/.' example: '56425' voucher_key: type: string description: 'Must match the regex /^\d{50}$/.' example: '56425593142326822820492192294472586671521743076933' consecutive_number: type: string description: 'Must match the regex /^\d{20}$/.' example: '56425593142326822820' 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). El campo value no puede tener más de 250 caracteres.' example: null nullable: true external_reference: type: string description: "Su identificador del documento que origina este\n comprobante (número de orden, ticket, folio). No se valida ni se interpreta: se guarda tal\n cual y se puede filtrar con `GET /vouchers?external_reference=...`. Envíelo siempre: es lo\n que le permite averiguar si una emisión llegó a completarse cuando la conexión se corta.\n No se exige único, porque contra una misma orden puede emitir una factura y después su nota\n de crédito. Máx. 150 caracteres." example: ORD-12345 nullable: true xml_otros: type: array description: "Contenido del nodo `` del XML. A diferencia de\n `external_reference`, esto SÍ queda dentro del documento fiscal firmado ante Hacienda.\n Máx. 10 entradas. Use `external_reference` si solo necesita poder consultar después; use\n este si la referencia debe constar en el comprobante." example: - [] items: type: object nullable: true properties: tipo: type: string description: "`texto` (genera ``) o `contenido`\n (genera ``)." example: texto codigo: type: string description: 'Atributo `codigo` del nodo. Máx. 255.' example: orden nullable: true valor: type: string description: 'Contenido del nodo. 1 a 500 caracteres.' example: ORD-0001 required: - tipo - valor 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: 'El campo value no puede tener más de 80 caracteres.' example: p 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: 'El campo value debe estar entre 1 y 7.' example: 1 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: 'El campo value debe tener al menos 5 caracteres. El campo value no puede tener más de 250 caracteres.' example: i nullable: true phone_country_code: type: integer description: 'El campo value debe ser al menos 1. El campo value no puede ser mayor a 999.' 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: 'El campo value no puede tener más de 5 elementos.' example: null items: type: object nullable: true properties: monto: type: number description: 'El campo value debe ser al menos 0.' 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 factorCalculo: type: number description: '' example: 4326.41688 nullable: true monto: type: string description: 'Monto del impuesto.' example: '1300.00000' monto_exportacion: type: number description: 'El campo value debe ser al menos 0.' example: 77 nullable: true exoneracion: type: object description: '' example: null properties: tipoDocumento: type: string description: 'This field is required when line_items.*.taxes.*.exoneracion is present. El campo value no puede tener más de 2 caracteres.' example: iy numeroDocumento: type: string description: 'This field is required when line_items.*.taxes.*.exoneracion is present. El campo value no puede tener más de 40 caracteres.' example: v nombreInstitucion: type: string description: 'This field is required when line_items.*.taxes.*.exoneracion is present. El campo value no puede tener más de 160 caracteres.' example: d fechaEmision: type: string description: 'This field is required when line_items.*.taxes.*.exoneracion is present. El campo value no es una fecha válida.' example: '2026-08-11T21:37:46' tarifaExonerada: type: number description: 'This field is required when line_items.*.taxes.*.exoneracion is present. El campo value debe ser al menos 0.' example: 37 montoExoneracion: type: number description: 'This field is required when line_items.*.taxes.*.exoneracion is present. El campo value debe ser al menos 0.' example: 9 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: 'El campo value no puede tener más de 100 caracteres.' example: 'n' nullable: true pharma_form_code: type: string description: 'Must match the regex /^\d{3}$/.' example: '564' nullable: true commercial_codes: type: array description: 'El campo value no puede tener más de 5 elementos.' example: null items: type: object nullable: true properties: tipo: type: string description: '' example: 4 enum: - 1 - 2 - 3 - 4 - 99 codigo: type: string description: 'El campo value no puede tener más de 20 caracteres.' example: yvdljnikhwaykcmy required: - tipo - codigo vin_numbers: type: array description: 'El campo value no puede tener más de 50 caracteres.' 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. El campo value debe ser al menos 0.' 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: 'El campo value no puede tener más de 80 caracteres.' example: 'Quidem nostrum qui commodi incidunt iure odit.' nullable: true percentage: type: number description: 'El campo value debe ser al menos 0. El campo value no puede ser mayor a 100.' example: 10 nullable: true amount: type: number description: 'Monto del cargo (Decimal 18,5). El campo value debe ser al menos 0.' 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/issuers: get: summary: 'GET /api/v1/public/vouchers/issuers' operationId: gETapiv1publicvouchersissuers description: "Opciones disponibles para los filtros del listado, calculadas a partir\nde los comprobantes que realmente existen (acotados al ambiente). Evita\nofrecer en los desplegables valores que no devolverían nada.\n\nRetorna:\n has_own → true si la cuenta tiene comprobantes propios\n clients → clientes gestionados con al menos un comprobante\n voucher_types → tipos de comprobante presentes (01, 03, …)\n statuses → estados presentes (accepted, rejected, …)" parameters: [] responses: { } tags: - 'Comprobantes Electrónicos' /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: - in: header name: Idempotency-Key description: '' example: 'Identificador único de ESTA anulación, generado por usted. Es OPCIONAL,' schema: type: string 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}/resend': post: summary: 'Reenviar a Hacienda un comprobante que quedó sin enviarse' operationId: reenviarAHaciendaUnComprobanteQueQuedSinEnviarse description: "Reintenta la emisión de un comprobante que NO llegó a Hacienda y por lo\ntanto NO consumió consecutivo allá: típicamente quedó en `draft` con su\nXML generado pero sin firmar (falló la firma por falta de certificado).\nUna vez subido el .p12, este endpoint lo firma y lo encola | **reusando\nel mismo comprobante y consecutivo**, sin generar huecos de numeración.\n\nTambién cubre el estado `error` (Hacienda no lo procesó por un fallo\ntemporal; ya está firmado y solo se reencola).\n\nEl ambiente del reenvío es el del comprobante original (un comprobante\nsandbox se firma con el certificado sandbox y se envía a sandbox)." parameters: [] responses: { } tags: - 'Comprobantes Electrónicos' parameters: - in: path name: key description: 'Clave de 50 dígitos.' example: '50617062600020627065200100001010000000101173247335' 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 /vouchers/drafts: get: summary: 'Listar borradores del contribuyente' operationId: listarBorradoresDelContribuyente description: "Devuelve los borradores del wizard de emisión guardados por el\ncontribuyente, ordenados del más reciente al más antiguo (por fecha de\núltima actualización). Un borrador es el estado parcial de un comprobante\nen construcción; **no es un comprobante fiscal** ni consume consecutivo." 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: 'Listado paginado' type: object example: success: true data: - id: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: '01' payload: client_id: null line_items: [] currency_code: CRC totals_preview: currency_code: CRC line_count: 0 total_venta: '0.00000' total_descuento: '0.00000' total_gravado: '0.00000' total_exento: '0.00000' total_venta_neta: '0.00000' total_impuesto: '0.00000' total_otros_cargos: '0.00000' total_comprobante: '0.00000' comments: 'Esperando datos del cliente.' validation: null created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T10:30:00-06:00' message: '' errors: null meta: current_page: 1 last_page: 1 per_page: 15 total: 1 from: 1 to: 1 links: first: ... last: ... prev: null next: null properties: success: type: boolean example: true data: type: array example: - id: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: '01' payload: client_id: null line_items: [] currency_code: CRC totals_preview: currency_code: CRC line_count: 0 total_venta: '0.00000' total_descuento: '0.00000' total_gravado: '0.00000' total_exento: '0.00000' total_venta_neta: '0.00000' total_impuesto: '0.00000' total_otros_cargos: '0.00000' total_comprobante: '0.00000' comments: 'Esperando datos del cliente.' validation: null created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T10:30:00-06:00' items: type: object properties: id: type: string example: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: type: string example: '01' payload: type: object properties: client_id: type: string example: null nullable: true line_items: type: array example: [] currency_code: type: string example: CRC totals_preview: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 0 total_venta: type: string example: '0.00000' total_descuento: type: string example: '0.00000' total_gravado: type: string example: '0.00000' total_exento: type: string example: '0.00000' total_venta_neta: type: string example: '0.00000' total_impuesto: type: string example: '0.00000' total_otros_cargos: type: string example: '0.00000' total_comprobante: type: string example: '0.00000' comments: type: string example: 'Esperando datos del cliente.' validation: type: string example: null nullable: true created_at: type: string example: '2026-04-16T10:00:00-06:00' updated_at: type: string example: '2026-04-16T10: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: 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 borradores' 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: - 'Comprobantes / Borradores' post: summary: 'Crear un borrador' operationId: crearUnBorrador description: "Guarda el estado parcial del wizard. NO consume consecutivo. Devuelve la\nvalidación en vivo (errores condicionales + preview de totales).\n\nEl `payload` es el cuerpo PARCIAL del comprobante (mismo contrato que\n`POST /vouchers`, pero puede estar incompleto entre pasos del wizard)." parameters: [] responses: 201: description: 'Borrador guardado' content: application/json: schema: type: object example: success: true data: id: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: '01' payload: client_id: null line_items: [] currency_code: CRC totals_preview: currency_code: CRC line_count: 0 total_venta: '0.00000' total_descuento: '0.00000' total_gravado: '0.00000' total_exento: '0.00000' total_venta_neta: '0.00000' total_impuesto: '0.00000' total_otros_cargos: '0.00000' total_comprobante: '0.00000' comments: 'Esperando datos del cliente.' validation: valid: false errors: line_items: - 'Agregá al menos una línea de detalle.' sale_condition: - 'Indicá la condición de venta.' totals: currency_code: CRC line_count: 0 total_comprobante: '0.00000' created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T10:00:00-06:00' message: 'Borrador guardado.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: type: string example: '01' payload: type: object properties: client_id: type: string example: null nullable: true line_items: type: array example: [] currency_code: type: string example: CRC totals_preview: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 0 total_venta: type: string example: '0.00000' total_descuento: type: string example: '0.00000' total_gravado: type: string example: '0.00000' total_exento: type: string example: '0.00000' total_venta_neta: type: string example: '0.00000' total_impuesto: type: string example: '0.00000' total_otros_cargos: type: string example: '0.00000' total_comprobante: type: string example: '0.00000' comments: type: string example: 'Esperando datos del cliente.' validation: type: object properties: valid: type: boolean example: false errors: type: object properties: line_items: type: array example: - 'Agregá al menos una línea de detalle.' items: type: string sale_condition: type: array example: - 'Indicá la condición de venta.' items: type: string totals: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 0 total_comprobante: type: string example: '0.00000' 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: 'Borrador guardado.' errors: type: string example: null nullable: true 422: description: 'Payload ausente o inválido' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: payload: - 'El cuerpo del borrador (payload) 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: payload: type: array example: - 'El cuerpo del borrador (payload) es obligatorio.' items: type: string tags: - 'Comprobantes / Borradores' requestBody: required: true content: application/json: schema: type: object properties: voucher_type: type: string description: 'Tipo de comprobante en construcción: `01` (FE), `02` (ND), `03` (NC), `04` (TE), `08` (FEC), `09` (FEE), `10` (REP). `null` mientras el usuario no elige.' example: '01' nullable: true payload: type: object description: 'Cuerpo PARCIAL del comprobante (mismo contrato que `POST /vouchers`, pero puede estar incompleto). El borrador no se valida ni firma hasta emitir.' example: client_id: null line_items: [] currency_code: CRC properties: { } comments: type: string description: 'Notas internas del borrador (máx. 1000 caracteres). No viajan al XML.' example: 'Esperando datos del cliente.' nullable: true required: - payload '/vouchers/drafts/{id}': get: summary: 'Consultar un borrador (con validación en vivo)' operationId: consultarUnBorradorconValidacinEnVivo description: "Devuelve el detalle del borrador indicado, recalculando en el momento la\n**validación en vivo** (errores condicionales + preview de totales) sin\npersistirla. Solo se retornan borradores que pertenecen a su\ncontribuyente. Un `id` que no sea un UUID válido retorna **404**." parameters: [] responses: 200: description: 'Borrador encontrado' content: application/json: schema: type: object example: success: true data: id: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: '01' payload: client_id: null line_items: [] currency_code: CRC totals_preview: currency_code: CRC line_count: 0 total_comprobante: '0.00000' comments: 'Esperando datos del cliente.' validation: valid: false errors: line_items: - 'Agregá al menos una línea de detalle.' totals: currency_code: CRC line_count: 0 total_comprobante: '0.00000' created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T10:30:00-06:00' message: '' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-aaaa-7288-8ece-fd64da756001 description: 'UUID del borrador. Úselo en las rutas `/{id}`, `/{id}/issue` y `DELETE /{id}`.' voucher_type: type: string example: '01' description: 'Tipo de comprobante en construcción (`01`, `02`, `03`, `04`, `08`, `09`, `10`). `null` mientras el usuario no elige.' payload: type: object properties: client_id: type: string example: null nullable: true line_items: type: array example: [] currency_code: type: string example: CRC description: 'Cuerpo PARCIAL del comprobante (mismo contrato que `POST /vouchers`, posiblemente incompleto).' totals_preview: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 0 total_comprobante: type: string example: '0.00000' description: 'Preview de totales persistido en el último `store`/`update`. `null` si nunca se calculó.' comments: type: string example: 'Esperando datos del cliente.' description: 'Notas internas del borrador. No viajan al XML. Puede ser `null`.' validation: type: object properties: valid: type: boolean example: false errors: type: object properties: line_items: type: array example: - 'Agregá al menos una línea de detalle.' items: type: string totals: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 0 total_comprobante: type: string example: '0.00000' description: 'Validación en vivo (dry-run): `valid` (bool), `errors` (mapa campo→mensajes condicionales) y `totals` (preview recalculado). `null` si no se adjuntó.' created_at: type: string example: '2026-04-16T10:00:00-06:00' description: 'Fecha de creación del borrador (ISO 8601).' updated_at: type: string example: '2026-04-16T10:30:00-06:00' description: 'Fecha de última actualización del borrador (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: - 'Comprobantes / Borradores' put: summary: 'Guardar un paso del wizard (actualización del borrador)' operationId: guardarUnPasoDelWizardactualizacinDelBorrador description: "Reemplaza el estado del borrador con lo enviado (`voucher_type`,\n`payload` y `comments`) y vuelve a calcular la validación en vivo\n(errores condicionales + preview de totales), persistiendo el preview.\nNO consume consecutivo ni firma. Un `id` que no sea un UUID válido\nretorna **404**." parameters: [] responses: 200: description: 'Borrador actualizado' content: application/json: schema: type: object example: success: true data: id: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: '01' payload: client_id: 019d867d-0001-7288-8ece-fd64da756001 sale_condition: '01' line_items: - quantity: 1 unit_price: 1000 totals_preview: currency_code: CRC line_count: 1 total_venta: '1000.00000' total_descuento: '0.00000' total_gravado: '1000.00000' total_exento: '0.00000' total_venta_neta: '1000.00000' total_impuesto: '130.00000' total_otros_cargos: '0.00000' total_comprobante: '1130.00000' comments: 'Listo para emitir.' validation: valid: true errors: [] totals: currency_code: CRC line_count: 1 total_comprobante: '1130.00000' created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T11:00:00-06:00' message: 'Borrador actualizado.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-aaaa-7288-8ece-fd64da756001 voucher_type: type: string example: '01' payload: type: object properties: client_id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 sale_condition: type: string example: '01' line_items: type: array example: - quantity: 1 unit_price: 1000 items: type: object properties: quantity: type: integer example: 1 unit_price: type: integer example: 1000 totals_preview: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 1 total_venta: type: string example: '1000.00000' total_descuento: type: string example: '0.00000' total_gravado: type: string example: '1000.00000' total_exento: type: string example: '0.00000' total_venta_neta: type: string example: '1000.00000' total_impuesto: type: string example: '130.00000' total_otros_cargos: type: string example: '0.00000' total_comprobante: type: string example: '1130.00000' comments: type: string example: 'Listo para emitir.' validation: type: object properties: valid: type: boolean example: true errors: type: array example: [] totals: type: object properties: currency_code: type: string example: CRC line_count: type: integer example: 1 total_comprobante: type: string example: '1130.00000' created_at: type: string example: '2026-04-16T10:00:00-06:00' updated_at: type: string example: '2026-04-16T11:00:00-06:00' message: type: string example: 'Borrador actualizado.' 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 422: description: 'Payload ausente o inválido' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: payload: - 'El cuerpo del borrador (payload) 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: payload: type: array example: - 'El cuerpo del borrador (payload) es obligatorio.' items: type: string tags: - 'Comprobantes / Borradores' requestBody: required: true content: application/json: schema: type: object properties: voucher_type: type: string description: 'Tipo de comprobante en construcción: `01` (FE), `02` (ND), `03` (NC), `04` (TE), `08` (FEC), `09` (FEE), `10` (REP).' example: '01' nullable: true payload: type: object description: 'Cuerpo PARCIAL del comprobante (mismo contrato que `POST /vouchers`, posiblemente incompleto).' example: client_id: 019d867d-0001-7288-8ece-fd64da756001 sale_condition: '01' line_items: - quantity: 1 unit_price: 1000 properties: { } comments: type: string description: 'Notas internas del borrador (máx. 1000 caracteres). No viajan al XML.' example: 'Listo para emitir.' nullable: true required: - payload delete: summary: 'Descartar un borrador' operationId: descartarUnBorrador description: "Elimina el borrador indicado. No afecta a comprobantes ya emitidos (el\nborrador no es un comprobante fiscal). Un `id` que no sea un UUID válido\nretorna **404**." parameters: [] responses: 200: description: 'Borrador descartado' content: application/json: schema: type: object example: success: true data: null message: 'Borrador descartado.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Borrador descartado.' 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: - 'Comprobantes / Borradores' parameters: - in: path name: id description: 'UUID del borrador.' example: 019d867d-aaaa-7288-8ece-fd64da756001 required: true schema: type: string '/vouchers/drafts/{id}/issue': post: summary: 'Emitir el comprobante a partir del borrador' operationId: emitirElComprobanteAPartirDelBorrador description: "Valida en seco (reglas condicionales); si pasa, reproduce el pipeline\nreal de POST /vouchers (consume consecutivo, firma, valida XSD, encola\nel envío a Hacienda) y ELIMINA el borrador al tener éxito.\n\nNo recibe cuerpo: emite a partir del `payload` ya guardado en el\nborrador. La respuesta es **liviana** (no serializa el comprobante\ncompleto); el detalle se consulta en `GET /vouchers/{key}`. Un `id` que\nno sea un UUID válido retorna **404**." parameters: [] responses: 201: description: 'Comprobante emitido' content: application/json: schema: type: object example: success: true data: id: 019d867d-bbbb-7288-8ece-fd64da756999 voucher_key: '50624062600310112345600100001010000000001100000001' voucher_type: '01' consecutive_number: '00100001010000000001' status: pending message: 'Comprobante emitido.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-bbbb-7288-8ece-fd64da756999 voucher_key: type: string example: '50624062600310112345600100001010000000001100000001' voucher_type: type: string example: '01' consecutive_number: type: string example: '00100001010000000001' status: type: string example: pending message: type: string example: 'Comprobante emitido.' 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 422: description: '' content: application/json: schema: oneOf: - description: 'Borrador incompleto' type: object example: success: false data: null message: 'El borrador tiene datos incompletos para emitir.' errors: line_items: - 'Agregá al menos una línea de detalle.' sale_condition: - 'Indicá la condición de venta.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El borrador tiene datos incompletos para emitir.' errors: type: object properties: line_items: type: array example: - 'Agregá al menos una línea de detalle.' items: type: string sale_condition: type: array example: - 'Indicá la condición de venta.' items: type: string - description: 'Falla del esquema oficial (XSD)' type: object example: success: false data: null message: 'El comprobante no cumple el esquema oficial (XSD).' errors: xsd: - "El elemento 'CodigoMoneda' no es válido." properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El comprobante no cumple el esquema oficial (XSD).' errors: type: object properties: xsd: type: array example: - "El elemento 'CodigoMoneda' no es válido." items: type: string - description: 'Sucursal/terminal inexistente' type: object example: success: false data: null message: 'La sucursal/terminal indicada no existe.' errors: terminal_code: - 'Créela primero en Sucursales y terminales.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La sucursal/terminal indicada no existe.' errors: type: object properties: terminal_code: type: array example: - 'Créela primero en Sucursales y terminales.' items: type: string tags: - 'Comprobantes / Borradores' parameters: - in: path name: id description: 'UUID del borrador a emitir.' example: 019d867d-aaaa-7288-8ece-fd64da756001 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_this_month: 340 emails_limit: 2250 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_this_month: type: integer example: 340 description: 'Correos transaccionales enviados en el mes en curso.' emails_limit: type: integer example: 2250 description: 'Límite MENSUAL de correos del plan. `null` = sin tope.' 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_this_month: 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_this_month: type: integer example: 0 description: 'Correos transaccionales enviados en el mes en curso.' emails_limit: type: integer example: 5000 description: 'Límite MENSUAL de correos del plan. `null` = sin tope.' 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: '3101999999' emails: - facturacion@empresa.cr economic_activities: - '6201.0' location: province: 1 canton: 1 district: 1 neighborhood: 'San Pedro' address: '200 metros norte del parque central' phone: country_code: 506 number: '22001234' is_active: true production_enabled: false hacienda_environment: sandbox plan_id: 019d0001-0000-0000-0000-000000000001 is_managed: false is_integrator: false plan: id: 019d0001-0000-0000-0000-000000000001 code: pyme name: Pyme description: 'Automatice su facturación con hasta 500 comprobantes/mes.' monthly_price_usd: null annual_price_usd: 96.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_monthly_emails: 2250 max_webhook_endpoints: 1 max_branch_terminals: 2 max_managed_contributors: 1 api_rate_limit_per_minute: 20 retention_months: 3 extended_retention_eligible: true pdf_branding: platform api_write_enabled: true api_readonly_enabled: false sandbox_access: true webhooks_enabled: false multi_contributor: false receiver_module: true bulk_emission: false sla_support: false created_at: '2026-04-01T10:00:00-06:00' updated_at: '2026-04-13T14:30:00-06:00' message: 'Perfil del contribuyente.' errors: null 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: '3101999999' 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_monthly_emails: type: integer example: 2250 description: 'Máximo de correos por mes.' 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: '3101999999' emails: - facturacion@empresa.cr - contabilidad@empresa.cr economic_activities: - '6201.0' - '4711.0' location: province: 1 canton: 1 district: 1 neighborhood: 'San Pedro' address: '200 metros norte del parque central' phone: country_code: 506 number: '22001234' is_active: true production_enabled: false hacienda_environment: sandbox plan_id: 019d0001-0000-0000-0000-000000000001 is_managed: false is_integrator: false plan: id: 019d0001-0000-0000-0000-000000000001 code: pyme name: Pyme vouchers_per_month: 500 max_clients: 300 sandbox_access: true created_at: '2026-04-01T10:00:00-06:00' updated_at: '2026-04-16T09:15:00-06:00' message: 'Perfil actualizado correctamente.' errors: null 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: '3101999999' 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: '3101999999' id_type: type: string description: '' example: 2 enum: - 1 - 2 - 3 - 4 - 5 - 6 hacienda_confirmed: type: boolean description: '' example: true legal_name: type: string description: 'Razon social o nombre completo. Min 5, max 100 caracteres. El campo value debe tener al menos 5 caracteres. El campo value no puede tener más de 100 caracteres.' example: 'Empresa Ejemplo S.A.' trade_name: type: string description: 'Nombre comercial. Opcional, min 3, max 80 caracteres. El campo value debe tener al menos 3 caracteres. El campo value no puede tener más de 80 caracteres.' example: 'Ejemplo Tienda' nullable: true emails: type: array description: 'Correo electrónico individual. Max 160 chars. El campo value no es un correo electrónico válido. El campo value no puede tener más de 160 caracteres.' 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). El campo value debe estar entre 1 y 999.' example: 506 nullable: true province: type: integer description: 'Código de provincia (1-7). Si se envía, canton y district son obligatorios. El campo value debe estar entre 1 y 7.' example: 1 nullable: true canton: type: integer description: 'Código de cantón (1-99). Obligatorio si province presente. El campo value debe estar entre 1 y 99.' example: 1 nullable: true district: type: integer description: 'Código de distrito (1-99). Obligatorio si province presente. El campo value debe estar entre 1 y 99.' example: 1 nullable: true neighborhood: type: string description: 'Barrio. Opcional, min 5, max 50 chars. El campo value debe tener al menos 5 caracteres. El campo value no puede tener más de 50 caracteres.' example: 'San Pedro' nullable: true address: type: string description: 'Senas exactas de la direccion. Min 5, max 250 caracteres. El campo value debe tener al menos 5 caracteres. El campo value no puede tener más de 250 caracteres.' example: '200 metros norte del parque central' nullable: true required: - economic_activities /users: get: summary: 'Listar usuarios del contribuyente' operationId: listarUsuariosDelContribuyente description: "Devuelve todos los usuarios (cuentas de login) asociados al\ncontribuyente activo: los **activos**, los que están **pendientes**\nde establecer su contraseña y los **desactivados**. Los usuarios cuyo\nacceso fue revocado **no** aparecen.\n\nCada usuario es una cuenta de login (nombre + correo + rol), **sin\ncédula** | la cédula vive en el contribuyente, no en el usuario.\n\nSolo el **owner** o un **admin** del contribuyente pueden consultar\nesta lista. Otros roles reciben **HTTP 403**." parameters: [] responses: 200: description: 'Listado de usuarios' content: application/json: schema: type: object example: success: true data: - id: 019d867d-0001-7288-8ece-fd64da756001 name: 'Ana Propietaria' email: ana@empresa.cr role: owner is_active: true is_owner: true status: active created_at: '2026-04-01T10:00:00-06:00' - id: 019d867d-0002-7288-8ece-fd64da756002 name: 'María Pérez' email: maria@empresa.cr role: cashier is_active: true is_owner: false status: pending created_at: '2026-04-10T14:30:00-06:00' message: '' errors: null properties: success: type: boolean example: true data: type: array example: - id: 019d867d-0001-7288-8ece-fd64da756001 name: 'Ana Propietaria' email: ana@empresa.cr role: owner is_active: true is_owner: true status: active created_at: '2026-04-01T10:00:00-06:00' - id: 019d867d-0002-7288-8ece-fd64da756002 name: 'María Pérez' email: maria@empresa.cr role: cashier is_active: true is_owner: false status: pending created_at: '2026-04-10T14:30:00-06:00' items: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 name: type: string example: 'Ana Propietaria' email: type: string example: ana@empresa.cr role: type: string example: owner is_active: type: boolean example: true is_owner: type: boolean example: true status: type: string example: active created_at: type: string example: '2026-04-01T10:00:00-06:00' message: type: string example: '' errors: type: string example: null nullable: true 403: description: 'Sin permisos' content: application/json: schema: type: object example: success: false data: null message: 'No tenés permisos para gestionar usuarios.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No tenés permisos para gestionar usuarios.' errors: type: string example: null nullable: true tags: - 'Usuarios de la Cuenta' post: summary: 'Invitar un usuario' operationId: invitarUnUsuario description: "Crea una cuenta de login (sin cédula) y la asocia al contribuyente\nactivo. El usuario recibe un **correo con un enlace** para establecer\nsu contraseña; mientras tanto queda en estado `pending`.\n\n**Reglas:**\n\n- El `email` debe ser **único** en toda la plataforma. Si ya existe un\n usuario con ese correo, responde **HTTP 422**.\n- El `role` no puede ser `owner`; solo se admiten `admin`,\n `accountant`, `cashier`, `viewer` o `api_user`.\n\nSolo el **owner** o un **admin** pueden invitar usuarios. Otros roles\nreciben **HTTP 403**." parameters: [] responses: 201: description: 'Usuario invitado' content: application/json: schema: type: object example: success: true data: id: 019d867d-0002-7288-8ece-fd64da756002 name: 'María Pérez' email: maria@empresa.cr role: cashier is_active: true is_owner: false status: pending created_at: '2026-04-16T10:00:00-06:00' message: 'Usuario invitado. Recibirá un correo para establecer su contraseña.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0002-7288-8ece-fd64da756002 name: type: string example: 'María Pérez' email: type: string example: maria@empresa.cr role: type: string example: cashier is_active: type: boolean example: true is_owner: type: boolean example: false status: type: string example: pending created_at: type: string example: '2026-04-16T10:00:00-06:00' message: type: string example: 'Usuario invitado. Recibirá un correo para establecer su contraseña.' errors: type: string example: null nullable: true 403: description: 'Sin permisos' content: application/json: schema: type: object example: success: false data: null message: 'No tenés permisos para gestionar usuarios.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No tenés permisos para gestionar usuarios.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Correo duplicado' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: email: - 'Ya existe un usuario con ese correo.' 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: email: type: array example: - 'Ya existe un usuario con ese correo.' items: type: string - description: 'Rol inválido' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: role: - 'El rol 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: role: type: array example: - 'El rol seleccionado no es válido.' items: type: string tags: - 'Usuarios de la Cuenta' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Nombre del usuario a invitar (máx. 150 chars).' example: 'María Pérez' email: type: string description: 'Correo del usuario (único). Recibe la invitación / magic link.' example: maria@empresa.cr role: type: string description: 'Rol del usuario: `admin`, `accountant`, `cashier`, `viewer` o `api_user`.' example: cashier required: - name - email - role '/users/{id}': put: summary: 'Editar un usuario' operationId: editarUnUsuario description: "Permite cambiar el **rol** y/o el **estado** (activar / desactivar) de\nun usuario del contribuyente. Admite actualización parcial | envíe\nsolo los campos que desea modificar.\n\n> **Acción sensible:** cambiar el estado `is_active` requiere enviar el\n> campo `password` con la contraseña del usuario que opera. Si falta o\n> es incorrecta, responde **HTTP 401**.\n\nAl **activar** un usuario se respeta el `max_users` del plan: si ya se\nalcanzó el límite de usuarios activos, responde **HTTP 422**.\n\nEl **propietario** de la cuenta no puede modificarse (**HTTP 403**), y\nel rol `owner` no puede asignarse desde aquí. Solo el owner o un admin\npueden editar usuarios." parameters: [] responses: 200: description: 'Usuario actualizado' content: application/json: schema: type: object example: success: true data: id: 019d867d-0002-7288-8ece-fd64da756002 name: 'María Pérez' email: maria@empresa.cr role: viewer is_active: true is_owner: false status: active created_at: '2026-04-10T14:30:00-06:00' message: 'Usuario actualizado.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0002-7288-8ece-fd64da756002 name: type: string example: 'María Pérez' email: type: string example: maria@empresa.cr role: type: string example: viewer is_active: type: boolean example: true is_owner: type: boolean example: false status: type: string example: active created_at: type: string example: '2026-04-10T14:30:00-06:00' message: type: string example: 'Usuario actualizado.' errors: type: string example: null nullable: true 401: description: 'Contraseña incorrecta' content: application/json: schema: type: object example: success: false data: null message: 'Contraseña incorrecta.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Contraseña incorrecta.' errors: type: string example: null nullable: true 403: description: '' content: application/json: schema: oneOf: - description: 'Sin permisos' type: object example: success: false data: null message: 'No tenés permisos para gestionar usuarios.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No tenés permisos para gestionar usuarios.' errors: type: string example: null nullable: true - description: 'Es el propietario' type: object example: success: false data: null message: 'No se puede modificar al propietario de la cuenta.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se puede modificar al propietario de la cuenta.' errors: type: string example: null nullable: true 404: description: 'Usuario no encontrado' content: application/json: schema: type: object example: success: false data: null message: 'Usuario no encontrado en este contribuyente.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Usuario no encontrado en este contribuyente.' errors: type: string example: null nullable: true 422: description: 'Límite del plan alcanzado' content: application/json: schema: type: object example: success: false data: null message: 'Alcanzaste el límite de usuarios activos de tu plan (3).' errors: is_active: - 'Desactivá otro usuario o cambiá de plan.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Alcanzaste el límite de usuarios activos de tu plan (3).' errors: type: object properties: is_active: type: array example: - 'Desactivá otro usuario o cambiá de plan.' items: type: string tags: - 'Usuarios de la Cuenta' requestBody: required: false content: application/json: schema: type: object properties: role: type: string description: 'Nuevo rol del usuario: `admin`, `accountant`, `cashier`, `viewer` o `api_user`.' example: viewer is_active: type: boolean description: 'Activar (`true`) o desactivar (`false`) el usuario.' example: true password: type: string description: 'Contraseña del usuario que opera. Requerida solo cuando cambia `is_active`.' example: MiClaveSegura123 delete: summary: 'Revocar el acceso de un usuario' operationId: revocarElAccesoDeUnUsuario description: "Desvincula al usuario del contribuyente activo, liberando su asiento\ndel plan. La cuenta de login **global** del usuario **no se elimina**:\nsigue viva para que pueda asociarse a otro contribuyente o facturar\npor su cuenta. Simplemente deja de pertenecer a esta cuenta y\ndesaparece del listado.\n\n> **Acción sensible:** requiere enviar el campo `password` con la\n> contraseña del usuario que opera. Si falta o es incorrecta, responde\n> **HTTP 401**.\n\nNo puede revocar su **propio** acceso ni el del **propietario** de la\ncuenta (ambos casos responden **HTTP 403**). Solo el owner o un admin\npueden revocar usuarios." parameters: [] responses: 200: description: 'Acceso revocado' content: application/json: schema: type: object example: success: true data: null message: 'Usuario desvinculado de esta cuenta.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Usuario desvinculado de esta cuenta.' errors: type: string example: null nullable: true 401: description: 'Contraseña incorrecta' content: application/json: schema: type: object example: success: false data: null message: 'Contraseña incorrecta.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Contraseña incorrecta.' errors: type: string example: null nullable: true 403: description: '' content: application/json: schema: oneOf: - description: 'Sin permisos' type: object example: success: false data: null message: 'No tenés permisos para gestionar usuarios.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No tenés permisos para gestionar usuarios.' errors: type: string example: null nullable: true - description: 'No puede revocarse a sí mismo' type: object example: success: false data: null message: 'No podés revocar tu propio acceso.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No podés revocar tu propio acceso.' errors: type: string example: null nullable: true - description: 'Es el propietario' type: object example: success: false data: null message: 'No se puede revocar al propietario de la cuenta.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se puede revocar al propietario de la cuenta.' errors: type: string example: null nullable: true 404: description: 'Usuario no encontrado' content: application/json: schema: type: object example: success: false data: null message: 'Usuario no encontrado en este contribuyente.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Usuario no encontrado en este contribuyente.' errors: type: string example: null nullable: true tags: - 'Usuarios de la Cuenta' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: 'Contraseña del usuario que opera (confirmación de la acción sensible).' example: MiClaveSegura123 required: - password parameters: - in: path name: id description: 'UUID del usuario a editar.' example: 019d867d-0002-7288-8ece-fd64da756002 required: true schema: type: string /my-access-contributors: get: summary: 'Listar contribuyentes accesibles' operationId: listarContribuyentesAccesibles description: "Devuelve los contribuyentes a los que el usuario autenticado tiene\nacceso operativo (pivote `user_contributors` activo y aceptado), por\nejemplo como dueño de su propia empresa o como contador de varios\nclientes. Solo se incluyen los contribuyentes que estén **activos**.\n\nEl campo `is_current` indica cuál de ellos es el contribuyente activo\nde la sesión (el que lee el middleware multi-tenant). Use el endpoint\n`POST /switch-contributor` para cambiarlo." parameters: [] responses: 200: description: 'Contribuyentes accesibles' content: application/json: schema: type: object example: success: true data: - contributor_id: 019d867d-0001-7288-8ece-fd64da756001 legal_name: 'EMPRESA EJEMPLO S.A.' id_number: '3101123456' role: owner is_current: true - contributor_id: 019d867d-0002-7288-8ece-fd64da756002 legal_name: 'CLIENTE CONTABLE S.A.' id_number: '3101987654' role: accountant is_current: false message: '' errors: null properties: success: type: boolean example: true data: type: array example: - contributor_id: 019d867d-0001-7288-8ece-fd64da756001 legal_name: 'EMPRESA EJEMPLO S.A.' id_number: '3101123456' role: owner is_current: true - contributor_id: 019d867d-0002-7288-8ece-fd64da756002 legal_name: 'CLIENTE CONTABLE S.A.' id_number: '3101987654' role: accountant is_current: false items: type: object properties: contributor_id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 description: 'UUID del contribuyente. Úselo como `contributor_id` en `POST /switch-contributor`.' legal_name: type: string example: 'EMPRESA EJEMPLO S.A.' description: 'Nombre legal o razón social del contribuyente.' id_number: type: string example: '3101123456' description: 'Número de identificación (cédula) del contribuyente.' role: type: string example: owner description: 'Rol del usuario sobre ese contribuyente (por ejemplo `owner` o `accountant`).' is_current: type: boolean example: true description: '`true` si es el contribuyente activo de la sesión actual.' message: type: string example: '' errors: type: string example: null nullable: true tags: - 'Cambio de Contribuyente' /switch-contributor: post: summary: 'Cambiar el contribuyente activo' operationId: cambiarElContribuyenteActivo description: "Cambia el contribuyente activo de la sesión del usuario autenticado.\nEl usuario debe tener acceso operativo (pivote `user_contributors`\nactivo y aceptado) al contribuyente destino, y este debe estar\n**activo**. El mismo token sigue siendo válido tras el cambio.\n\nSi el usuario no tiene acceso al contribuyente indicado, responde\n**HTTP 403**. Si el contribuyente no existe o está inactivo, responde\n**HTTP 404**." parameters: [] responses: 200: description: 'Contribuyente cambiado' content: application/json: schema: type: object example: success: true data: contributor_id: 019d867d-0002-7288-8ece-fd64da756002 legal_name: 'CLIENTE CONTABLE S.A.' role: accountant message: 'Contribuyente activo cambiado.' errors: null properties: success: type: boolean example: true data: type: object properties: contributor_id: type: string example: 019d867d-0002-7288-8ece-fd64da756002 legal_name: type: string example: 'CLIENTE CONTABLE S.A.' role: type: string example: accountant message: type: string example: 'Contribuyente activo cambiado.' errors: type: string example: null nullable: true 403: description: 'Sin acceso al contribuyente' content: application/json: schema: type: object example: success: false data: null message: 'No tenés acceso a ese contribuyente.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No tenés acceso a ese contribuyente.' errors: type: string example: null nullable: true 404: description: 'Contribuyente inexistente o inactivo' content: application/json: schema: type: object example: success: false data: null message: 'El contribuyente no existe o está inactivo.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El contribuyente no existe o está inactivo.' errors: type: string example: null nullable: true 422: description: 'Validación fallida' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: contributor_id: - 'El campo contributor id 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: contributor_id: type: array example: - 'El campo contributor id es obligatorio.' items: type: string tags: - 'Cambio de Contribuyente' requestBody: required: true content: application/json: schema: type: object properties: contributor_id: type: string description: 'UUID del contribuyente al que se quiere cambiar.' example: 019d867d-0002-7288-8ece-fd64da756002 required: - contributor_id /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: 'Listado de certificados' content: application/json: schema: 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 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 la contraseña\ndel IdP de Hacienda, y lo activa como certificado para firmar\ncomprobantes en el ambiente indicado.\n\nEste request **debe enviarse como `multipart/form-data`** (no JSON),\nporque incluye un archivo binario.\n\n**Validaciones que se ejecutan ANTES de guardar:**\n\n1. **Disponibilidad del ambiente**: solo puede subir cert si NO hay\n uno activo en ese ambiente. Si ya hay uno, debe desactivarlo\n primero con `DELETE /certificates/{id}` o usar el endpoint\n `GET /certificates/availability` para chequear estado.\n2. **PIN del .p12**: el archivo debe abrirse con la contraseña\n provista en `p12_password`.\n3. **Llave privada RSA**: el .p12 debe contener una llave RSA válida\n (Hacienda CR exige RSA-SHA256 para XAdES-EPES).\n4. **Subject del certificado**: el cert debe pertenecer al\n contribuyente que lo está subiendo (anti-fraude | REGLAMENTO art. 16).\n5. **Vigencia del certificado**: el X.509 no puede estar vencido\n ni ser prematuro.\n6. **Credenciales IdP de Hacienda**: el `hacienda_pin` debe autenticar\n correctamente contra el IdP del ambiente solicitado (POST\n grant_type=password). Si falla, el cert NO se guarda | sería\n inutilizable sin token OAuth2 válido.\n\nSi **CUALQUIERA** de las 6 validaciones falla, se retorna HTTP 422\n(o 503 si el IdP de Hacienda no responde) y NADA se guarda en DB.\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=PIN-del-archivo-p12\" \\\n -F \"hacienda_pin=Contraseña-del-IdP-de-Hacienda\" \\\n -F \"environment=sandbox\"\n```" parameters: [] responses: 201: description: 'Certificado registrado exitosamente' content: application/json: schema: type: object example: success: true data: id: 019d867d-1111-7288-8ece-fd64da756001 environment: sandbox is_active: true is_expired: false days_remaining: 596 valid_from: '2025-12-15T10:30:00-06:00' valid_until: '2027-12-15T10:30:00-06:00' certificate_subject: 'CN=ALBERT MENDEZ, serialNumber=CPF-02-0627-0652' certificate_serial: 019DD6C62577 created_at: '2026-04-29T08:00:00-06:00' message: 'Certificado registrado y activado correctamente.' errors: null properties: success: type: boolean example: true data: 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: 596 valid_from: type: string example: '2025-12-15T10:30:00-06:00' valid_until: type: string example: '2027-12-15T10:30:00-06:00' certificate_subject: type: string example: 'CN=ALBERT MENDEZ, serialNumber=CPF-02-0627-0652' certificate_serial: type: string example: 019DD6C62577 created_at: type: string example: '2026-04-29T08:00:00-06:00' message: type: string example: 'Certificado registrado y activado correctamente.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'PIN del archivo .p12 incorrecto (5001)' type: object example: success: false data: null message: 'El PIN del archivo .p12 es incorrecto, el archivo está corrupto, o no es un PKCS#12 válido emitido por el BCCR. ...' errors: p12_password: - 'El PIN del archivo .p12 es incorrecto, ...' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El PIN del archivo .p12 es incorrecto, el archivo está corrupto, o no es un PKCS#12 válido emitido por el BCCR. ...' errors: type: object properties: p12_password: type: array example: - 'El PIN del archivo .p12 es incorrecto, ...' items: type: string - description: 'Cert no pertenece al contribuyente (5002)' type: object example: success: false data: null message: 'El certificado .p12 NO pertenece a este contribuyente. ...' errors: p12_file: - 'El certificado .p12 NO pertenece a este contribuyente. ...' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El certificado .p12 NO pertenece a este contribuyente. ...' errors: type: object properties: p12_file: type: array example: - 'El certificado .p12 NO pertenece a este contribuyente. ...' items: type: string - description: 'Credenciales IdP rechazadas (5003)' type: object example: success: false data: null message: 'El IdP de Hacienda rechazó las credenciales con HTTP 401. ...' errors: hacienda_pin: - 'El IdP de Hacienda rechazó las credenciales con HTTP 401. ...' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El IdP de Hacienda rechazó las credenciales con HTTP 401. ...' errors: type: object properties: hacienda_pin: type: array example: - 'El IdP de Hacienda rechazó las credenciales con HTTP 401. ...' items: type: string - description: 'Ya hay cert activo en el ambiente (5004)' type: object example: success: false data: null message: "Ya existe un certificado activo en el ambiente 'sandbox'. ..." errors: environment: - "Ya existe un certificado activo en el ambiente 'sandbox'. ..." properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: "Ya existe un certificado activo en el ambiente 'sandbox'. ..." errors: type: object properties: environment: type: array example: - "Ya existe un certificado activo en el ambiente 'sandbox'. ..." items: type: string - description: 'El .p12 pertenece al otro ambiente (5005)' type: object example: success: false data: null message: 'El certificado .p12 pertenece al ambiente de sandbox (pruebas) pero usted lo está subiendo como producción. ...' errors: p12_file: - 'El certificado .p12 pertenece al ambiente de sandbox (pruebas) pero usted lo está subiendo como producción. ...' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El certificado .p12 pertenece al ambiente de sandbox (pruebas) pero usted lo está subiendo como producción. ...' errors: type: object properties: p12_file: type: array example: - 'El certificado .p12 pertenece al ambiente de sandbox (pruebas) pero usted lo está subiendo como producción. ...' items: type: string - description: 'Perfil sin identificación registrada (5006)' type: object example: success: false data: null message: 'No es posible validar el certificado porque el contribuyente todavía no tiene registrada su identificación (tipo y número de cédula) en el perfil. ...' errors: p12_file: - 'No es posible validar el certificado porque el contribuyente todavía no tiene registrada su identificación ...' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No es posible validar el certificado porque el contribuyente todavía no tiene registrada su identificación (tipo y número de cédula) en el perfil. ...' errors: type: object properties: p12_file: type: array example: - 'No es posible validar el certificado porque el contribuyente todavía no tiene registrada su identificación ...' items: type: string - description: 'Certificado vencido (2002)' type: object example: success: false data: null message: "El certificado del contribuyente '...' no está vigente. ..." errors: p12_file: - "El certificado del contribuyente '...' no está vigente. ..." properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: "El certificado del contribuyente '...' no está vigente. ..." errors: type: object properties: p12_file: type: array example: - "El certificado del contribuyente '...' no está vigente. ..." items: type: string 503: description: 'IdP de Hacienda no responde temporalmente' content: application/json: schema: type: object example: success: false data: null message: 'El IdP de Hacienda no responde temporalmente. Intente nuevamente en unos minutos.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El IdP de Hacienda no responde temporalmente. Intente nuevamente en unos minutos.' errors: type: string example: null nullable: true 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: 5 MB.' p12_password: type: string description: 'Contraseña del archivo `.p12` (PIN del archivo). NO confunda con la contraseña del IdP de Hacienda | son distintas.' example: architecto hacienda_pin: type: string description: 'Contraseña del IdP de Hacienda (asignada por TRIBU-CR al registrarse como emisor). NO confunda con el PIN del archivo .p12.' example: architecto environment: type: string description: 'Ambiente: `sandbox` o `production`.' example: architecto required: - p12_file - p12_password - hacienda_pin - environment /certificates/availability: get: summary: 'Consultar disponibilidad de subida de certificado por ambiente' operationId: consultarDisponibilidadDeSubidaDeCertificadoPorAmbiente description: "Indica para cada ambiente (`sandbox`, `production`) si está\n**disponible** para subir un certificado nuevo o si ya hay uno\nactivo que debe desactivarse primero.\n\nÚtil para mostrar el formulario de subida con el selector de\nambiente correctamente deshabilitado, evitando que el usuario\nintente subir un .p12 que sería rechazado con HTTP 422.\n\n**Reglas:**\n\n- `available = true` → no hay certificado activo en ese ambiente,\n se puede subir uno nuevo.\n- `available = false` → ya hay certificado activo. Para subir uno\n nuevo en este ambiente debe desactivar el actual con\n `DELETE /api/v1/public/certificates/{id}`.\n\nCuando `available = false`, el campo `active_certificate` incluye\nlos datos del certificado vigente para que el frontend pueda\nmostrar al usuario qué cert está bloqueando el upload." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: 'Sandbox disponible, producción ocupada' type: object example: success: true data: sandbox: available: true active_certificate: null production: available: false active_certificate: id: 019d867d-1111-7288-8ece-fd64da756001 certificate_serial: 019DD6D90F43 certificate_subject: 'CN=ALBERT MENDEZ, serialNumber=CPF-02-0627-0652' valid_from: '2025-12-15T10:30:00-06:00' valid_until: '2027-12-15T10:30:00-06:00' days_remaining: 596 is_expired: false created_at: '2026-04-15T08:00:00-06:00' message: '' errors: null properties: success: type: boolean example: true data: type: object properties: sandbox: type: object properties: available: type: boolean example: true description: '`true` si el ambiente sandbox está disponible para subir un cert nuevo, `false` si ya hay uno activo.' active_certificate: type: string example: null description: 'Datos del certificado activo en sandbox. `null` si no hay activo.' production: type: object properties: available: type: boolean example: false description: 'Igual al de sandbox pero para producción.' active_certificate: type: object properties: id: type: string example: 019d867d-1111-7288-8ece-fd64da756001 certificate_serial: type: string example: 019DD6D90F43 certificate_subject: type: string example: 'CN=ALBERT MENDEZ, serialNumber=CPF-02-0627-0652' valid_from: type: string example: '2025-12-15T10:30:00-06:00' valid_until: type: string example: '2027-12-15T10:30:00-06:00' days_remaining: type: integer example: 596 is_expired: type: boolean example: false created_at: type: string example: '2026-04-15T08:00:00-06:00' description: 'Datos del certificado activo en producción. `null` si no hay activo.' message: type: string example: '' errors: type: string example: null nullable: true - description: 'Ambos ambientes disponibles' type: object example: success: true data: sandbox: available: true active_certificate: null production: available: true active_certificate: null message: '' errors: null properties: success: type: boolean example: true data: type: object properties: sandbox: type: object properties: available: type: boolean example: true description: '`true` si el ambiente sandbox está disponible para subir un cert nuevo, `false` si ya hay uno activo.' active_certificate: type: string example: null description: 'Datos del certificado activo en sandbox. `null` si no hay activo.' production: type: object properties: available: type: boolean example: true description: 'Igual al de sandbox pero para producción.' active_certificate: type: string example: null description: 'Datos del certificado activo en producción. `null` si no hay activo.' message: type: string example: '' errors: type: string example: null nullable: true tags: - 'Certificados Digitales' '/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 certificado nuevo (debe desactivar el vigente\n> primero | el sistema no permite dos activos en el mismo ambiente).\n> - Migración de un contribuyente a otra plataforma." 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_header: 14 size_body: 9 layout: show_logo: true logo_position: left logo_max_height: 60 qr: enabled: true size: 100 position: bottom-right has_logo: true is_active: true created_at: '2026-04-01T10:00:00-06:00' updated_at: '2026-04-10T14:30:00-06:00' - id: 2 name: Minimal is_default: false paper_size: A4 config_json: colors: primary: '#000000' secondary: '#666666' accent: '#000000' fonts: family: Helvetica size_header: 12 size_body: 8 layout: show_logo: false logo_position: left logo_max_height: 60 qr: enabled: true size: 80 position: bottom-right has_logo: false is_active: true created_at: '2026-04-05T08:00:00-06:00' updated_at: '2026-04-05T08:00:00-06:00' message: '' errors: null 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_header: 14 size_body: 9 layout: show_logo: true logo_position: left logo_max_height: 60 qr: enabled: true size: 100 position: bottom-right has_logo: true is_active: true created_at: '2026-04-01T10:00:00-06:00' updated_at: '2026-04-10T14:30:00-06:00' - id: 2 name: Minimal is_default: false paper_size: A4 config_json: colors: primary: '#000000' secondary: '#666666' accent: '#000000' fonts: family: Helvetica size_header: 12 size_body: 8 layout: show_logo: false logo_position: left logo_max_height: 60 qr: enabled: true size: 80 position: bottom-right has_logo: false is_active: true created_at: '2026-04-05T08:00:00-06:00' updated_at: '2026-04-05T08:00:00-06:00' 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_header: 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_header: 14 size_body: 9 layout: show_logo: true logo_position: left logo_max_height: 60 qr: enabled: true size: 100 position: bottom-right has_logo: false is_active: true created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T10:00:00-06:00' message: 'Plantilla creada correctamente.' errors: null 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_header: type: integer example: 14 description: 'Tamaño del encabezado/título principal en pt (8-24).' 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_header: 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' muted: '#666666' 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' muted: type: string description: 'Must match the regex /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.' example: '#666666' 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: 'Tamaño del encabezado/título (8-24).' example: 14 size_body: type: integer description: 'Tamaño del cuerpo (6-14).' example: 9 size_footer: type: integer description: 'Tamaño del pie de página (5-10).' example: 7 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: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 15 right: type: integer description: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 15 bottom: type: integer description: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 15 left: type: integer description: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 15 style: type: string description: '' example: classic enum: - classic - modern - minimal - bold - split footer_text: type: string description: 'El campo value no puede tener más de 200 caracteres.' 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_header: 14 size_body: 9 layout: show_logo: true logo_position: left logo_max_height: 60 qr: enabled: true size: 100 position: bottom-right has_logo: true is_active: true created_at: '2026-04-01T10:00:00-06:00' updated_at: '2026-04-16T11:00:00-06:00' message: 'Plantilla actualizada correctamente.' errors: null 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_header: 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' muted: type: string description: 'Must match the regex /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.' example: '#2815Df' fonts: type: object description: '' example: null properties: family: type: string description: 'Familia tipográfica.' example: Helvetica size_header: type: integer description: 'Tamaño del encabezado/título (8-24).' example: 14 size_body: type: integer description: 'Tamaño del cuerpo (6-14).' example: 9 size_footer: type: integer description: 'Tamaño del pie de página (5-10).' example: 7 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: true show_payment_methods: type: boolean description: '' example: false 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: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 22 right: type: integer description: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 24 bottom: type: integer description: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 18 left: type: integer description: 'El campo value debe ser al menos 5. El campo value no puede ser mayor a 50.' example: 8 style: type: string description: '' example: bold enum: - classic - modern - minimal - bold - split footer_text: type: string description: 'El campo value no puede tener más de 200 caracteres.' 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. El campo value no es un correo electrónico válido. El campo value no puede tener más de 160 caracteres.' example: facturas@miempresa.cr nullable: true bcc: type: array description: 'El campo value no es un correo electrónico válido. El campo value no puede tener más de 160 caracteres.' example: - b items: type: string custom_subject: type: string description: 'Asunto personalizado. Placeholders: {tipo}, {consecutivo}, {clave}, {receptor}, {total}, {moneda}, {emisor}. El campo value no puede tener más de 200 caracteres.' 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. El campo value no puede tener más de 500 caracteres.' 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: for_contributor_id description: 'Selector al facturar: UUID del contribuyente seleccionado. Devuelve los items de su alcance MÁS el pozo general. Sin este parámetro se listan todos los items del catálogo.' example: 0b3f1c2a-0000-4000-8000-000000000000 required: false schema: type: string description: 'Selector al facturar: UUID del contribuyente seleccionado. Devuelve los items de su alcance MÁS el pozo general. Sin este parámetro se listan todos los items del catálogo.' example: 0b3f1c2a-0000-4000-8000-000000000000 - 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: for_contributor_id: type: string description: 'Alcance del item en su catálogo. null = pozo general (visible al facturar por cualquier cliente gestionado). UUID de un contribuyente que gestiona (o su propia cuenta) = visible solo al facturar por ese contribuyente. No comparte el item con el contribuyente; solo filtra su selector. El campo value debe ser un UUID válido.' example: null nullable: true 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_factor: type: number description: '' example: 4326.41688 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 /sequences: get: summary: 'Listar secuencias del contribuyente' operationId: listarSecuenciasDelContribuyente description: "Devuelve todas las secuencias de consecutivos (sucursal + terminal +\ntipo de comprobante) registradas por el contribuyente autenticado,\nordenadas por sucursal, terminal y tipo.\n\nCada secuencia representa un contador independiente del consecutivo de\n20 dígitos definido en la normativa (pág. 65 Anexos): los 3 dígitos de\nsucursal (`local_code`), 5 de terminal (`terminal_code`), 2 del tipo\n(`voucher_type`) y 10 del consecutivo correlativo." parameters: [] responses: 200: description: 'Listado de secuencias' content: application/json: schema: type: object example: success: true data: - id: 019d867d-0001-7288-8ece-fd64da756001 local_code: '001' terminal_code: '00001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' voucher_type_abbr: FE current_sequence: 42 next_sequence: 43 last_consecutive: '00100001010000000042' next_consecutive: '00100001010000000043' last_issued_at: '2026-04-10T14:30:00-06:00' last_issued_key: 506041026...00000042 is_active: true is_near_rollover: false display_key: 001-00001-01 message: 'Secuencias del contribuyente.' errors: null properties: success: type: boolean example: true data: type: array example: - id: 019d867d-0001-7288-8ece-fd64da756001 local_code: '001' terminal_code: '00001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' voucher_type_abbr: FE current_sequence: 42 next_sequence: 43 last_consecutive: '00100001010000000042' next_consecutive: '00100001010000000043' last_issued_at: '2026-04-10T14:30:00-06:00' last_issued_key: 506041026...00000042 is_active: true is_near_rollover: false display_key: 001-00001-01 items: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 local_code: type: string example: '001' terminal_code: type: string example: '00001' 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: 42 next_sequence: type: integer example: 43 last_consecutive: type: string example: '00100001010000000042' next_consecutive: type: string example: '00100001010000000043' last_issued_at: type: string example: '2026-04-10T14:30:00-06:00' last_issued_key: type: string example: 506041026...00000042 is_active: type: boolean example: true is_near_rollover: type: boolean example: false display_key: type: string example: 001-00001-01 message: type: string example: 'Secuencias del contribuyente.' errors: type: string example: null nullable: true tags: - Secuencias post: summary: 'Crear una secuencia (sucursal/terminal)' operationId: crearUnaSecuenciasucursalterminal description: "Registra una nueva secuencia de consecutivos para una combinación de\n**sucursal + terminal + tipo de comprobante** del contribuyente\nautenticado. Útil para dar de alta una nueva caja, sucursal o un tipo\nde comprobante que aún no tiene secuencia.\n\n**Reglas de validación:**\n\n- `local_code` + `terminal_code` + `voucher_type` deben ser una\n combinación **única**. Duplicar responde **HTTP 422**.\n- `local_code` son 3 dígitos y no puede ser `000`.\n- `terminal_code` son 5 dígitos y no puede ser `00000`.\n- `voucher_type` debe ser uno de los 7 tipos de emisión directa.\n- Crear una terminal nueva consume cupo del plan\n (`max_branch_terminals`); si lo supera responde **HTTP 422**.\n\nLa secuencia se crea con `is_active=true`." parameters: [] responses: 201: description: 'Secuencia creada' content: application/json: schema: type: object example: success: true data: id: 019d867d-0002-7288-8ece-fd64da756002 local_code: '001' terminal_code: '00001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' voucher_type_abbr: FE current_sequence: 9992 next_sequence: 9993 last_consecutive: '00100001010000009992' next_consecutive: '00100001010000009993' last_issued_at: null last_issued_key: null is_active: true is_near_rollover: false display_key: 001-00001-01 message: 'Secuencia creada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0002-7288-8ece-fd64da756002 local_code: type: string example: '001' terminal_code: type: string example: '00001' 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: '00100001010000009992' next_consecutive: type: string example: '00100001010000009993' 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-00001-01 message: type: string example: 'Secuencia creada.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Secuencia duplicada' type: object example: success: false data: null message: 'Ya existe una secuencia para esa sucursal, terminal y tipo.' errors: voucher_type: - 'Secuencia duplicada.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Ya existe una secuencia para esa sucursal, terminal y tipo.' errors: type: object properties: voucher_type: type: array example: - 'Secuencia duplicada.' items: type: string - description: 'Límite del plan alcanzado' type: object example: success: false data: null message: 'Ha alcanzado el límite de terminales de su plan.' errors: terminal_code: - 'Ha alcanzado el límite de terminales de su plan.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Ha alcanzado el límite de terminales de su plan.' errors: type: object properties: terminal_code: type: array example: - 'Ha alcanzado el límite de terminales de su plan.' items: type: string tags: - Secuencias requestBody: required: true content: application/json: schema: type: object properties: local_code: type: string description: 'Código de sucursal (3 dígitos). No puede ser `000`.' example: '001' terminal_code: type: string description: 'Código de terminal (5 dígitos). No puede ser `00000`.' example: '00001' voucher_type: type: string description: 'Tipo de comprobante (pos 09-10 del consecutivo). Valores: `01` (FE), `02` (ND), `03` (NC), `04` (TE), `08` (FEC), `09` (FEE), `10` (REP).' example: '01' current_sequence: type: integer description: 'Último consecutivo emitido. El próximo comprobante usará este valor + 1. Rango: 0 a 9,999,999,999.' example: 9992 required: - local_code - terminal_code - voucher_type - current_sequence '/sequences/{id}': put: summary: 'Ajustar una secuencia existente' operationId: ajustarUnaSecuenciaExistente description: "Admite **actualización parcial** del `current_sequence` y/o del estado\n`is_active` de una secuencia del contribuyente autenticado. Solo se\npueden ajustar secuencias propias; una secuencia inexistente o de otro\ncontribuyente responde **HTTP 404**.\n\n> **Importante:** bajar `current_sequence` por debajo del último\n> consecutivo **ya emitido** se rechaza con 422: el próximo comprobante\n> repetiría un número existente y Hacienda lo rechazaría. Sí se permite\n> ajustarlo por encima de ese piso, que es el escenario legítimo de\n> migración desde otro facturador.\n>\n> Cambiar el consecutivo exige **confirmación de contraseña** y queda\n> registrado en la bitácora de auditoría con el valor anterior, el\n> nuevo, el motivo, el usuario y la IP (Resolución MH-DGT-RES-0027-2024,\n> art. 6).\n\nEl motivo (`reason`) es **obligatorio** en toda actualización." parameters: [] responses: 200: description: 'Secuencia actualizada' content: application/json: schema: type: object example: success: true data: id: 019d867d-0001-7288-8ece-fd64da756001 local_code: '001' terminal_code: '00001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' voucher_type_abbr: FE current_sequence: 9992 next_sequence: 9993 last_consecutive: '00100001010000009992' next_consecutive: '00100001010000009993' last_issued_at: '2026-04-10T14:30:00-06:00' last_issued_key: 506041026...00009992 is_active: true is_near_rollover: false display_key: 001-00001-01 message: 'Secuencia actualizada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 local_code: type: string example: '001' terminal_code: type: string example: '00001' 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: '00100001010000009992' next_consecutive: type: string example: '00100001010000009993' last_issued_at: type: string example: '2026-04-10T14:30:00-06:00' last_issued_key: type: string example: 506041026...00009992 is_active: type: boolean example: true is_near_rollover: type: boolean example: false display_key: type: string example: 001-00001-01 message: type: string example: 'Secuencia actualizada.' 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.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Secuencia no encontrada.' errors: type: string example: null nullable: true tags: - Secuencias requestBody: required: true content: application/json: schema: type: object properties: current_sequence: type: integer description: 'Último consecutivo emitido. El próximo comprobante usará este valor + 1. No puede ser menor al último realmente emitido en esta secuencia. Rango: 0 a 9,999,999,999.' example: 9992 is_active: type: boolean description: 'Si la terminal está habilitada para este tipo de comprobante.' example: true reason: type: string description: 'Motivo del ajuste (5-500 caracteres). Se registra en la bitácora de auditoría (Resolución art. 6).' example: 'Migración desde Alegra, último consecutivo tipo 01 fue 9992.' password: type: string description: 'Contraseña del usuario autenticado. Obligatoria cuando se cambia `current_sequence`.' example: MiC0ntr4señ4 required: - reason parameters: - in: path name: id description: 'UUID de la secuencia.' example: 019d867d-0001-7288-8ece-fd64da756001 required: true schema: type: string /branches: get: summary: 'Listar sucursales y cajas propias' operationId: listarSucursalesYCajasPropias description: "Devuelve las sucursales (branches) directas del contribuyente autenticado,\ncada una con sus cajas (terminals). La sucursal directa usa slot `00` en el\n`terminal_code` (= `00` + caja de 3 díg.); la primera caja es `00001`." parameters: [] responses: 200: description: 'Sucursales del contribuyente' content: application/json: schema: type: object example: success: true data: - id: 019ee0-... local_code: '001' name: 'Casa Matriz' is_active: true is_direct: true terminals: - id: 019ee1-... terminal_code: '00001' slot: '00' box: '001' name: 'Caja 1' is_active: true created_at: '2026-06-19T15:00:00-06:00' message: 'Sucursales del contribuyente.' errors: null properties: success: type: boolean example: true data: type: array example: - id: 019ee0-... local_code: '001' name: 'Casa Matriz' is_active: true is_direct: true terminals: - id: 019ee1-... terminal_code: '00001' slot: '00' box: '001' name: 'Caja 1' is_active: true created_at: '2026-06-19T15:00:00-06:00' items: type: object properties: id: type: string example: 019ee0-... local_code: type: string example: '001' name: type: string example: 'Casa Matriz' is_active: type: boolean example: true is_direct: type: boolean example: true terminals: type: array example: - id: 019ee1-... terminal_code: '00001' slot: '00' box: '001' name: 'Caja 1' is_active: true items: type: object properties: id: type: string example: 019ee1-... terminal_code: type: string example: '00001' slot: type: string example: '00' box: type: string example: '001' name: type: string example: 'Caja 1' is_active: type: boolean example: true created_at: type: string example: '2026-06-19T15:00:00-06:00' message: type: string example: 'Sucursales del contribuyente.' errors: type: string example: null nullable: true tags: - 'Sucursales y cajas' post: summary: 'Crear una sucursal' operationId: crearUnaSucursal description: "Crea una sucursal directa del contribuyente y, automáticamente, su primera\ncaja (toda sucursal nace con ≥1 caja). Consume cupo del POZO del plan\n(`max_branches`). La primera caja directa es `00001`." parameters: [] responses: 201: description: 'Sucursal creada (con su primera caja)' content: application/json: schema: type: object example: success: true data: id: 019ee0-... local_code: '002' name: 'Sucursal Heredia' is_direct: true terminals: - terminal_code: '00002' name: 'Caja Principal' is_active: true message: 'Sucursal creada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... local_code: type: string example: '002' name: type: string example: 'Sucursal Heredia' is_direct: type: boolean example: true terminals: type: array example: - terminal_code: '00002' name: 'Caja Principal' is_active: true items: type: object properties: terminal_code: type: string example: '00002' name: type: string example: 'Caja Principal' is_active: type: boolean example: true message: type: string example: 'Sucursal creada.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Sucursal duplicada' type: object example: success: false data: null message: 'Esa sucursal ya existe.' errors: local_code: - 'Ya tenés una sucursal con ese código.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Esa sucursal ya existe.' errors: type: object properties: local_code: type: array example: - 'Ya tenés una sucursal con ese código.' items: type: string - description: 'Pozo del plan agotado' type: object example: success: false data: null message: 'Alcanzó el pozo de 2 sucursal(es) de su plan. Actualice su plan para agregar más.' errors: local_code: - 'Alcanzó el pozo de 2 sucursal(es) de su plan. Actualice su plan para agregar más.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Alcanzó el pozo de 2 sucursal(es) de su plan. Actualice su plan para agregar más.' errors: type: object properties: local_code: type: array example: - 'Alcanzó el pozo de 2 sucursal(es) de su plan. Actualice su plan para agregar más.' items: type: string tags: - 'Sucursales y cajas' requestBody: required: true content: application/json: schema: type: object properties: local_code: type: string description: 'Código de sucursal Hacienda, 3 díg., distinto de `000`.' example: '002' name: type: string description: 'Nombre de la sucursal (2-100 chars).' example: 'Sucursal Heredia' box_name: type: string description: 'Nombre de la primera caja (2-100 chars). Default: "Caja 1".' example: 'Caja Principal' required: - local_code - name '/branches/{id}': put: summary: 'Actualizar una sucursal (nombre y/o código)' operationId: actualizarUnaSucursalnombreYoCdigo description: "Cambia el nombre de la sucursal y/o su `local_code` (posiciones 01-03 del\nNumeroConsecutivo). Cambiar el código exige confirmar la contraseña y\nmigra las secuencias existentes al código nuevo (nunca las reinicia), de\nmodo que la numeración continúa donde iba. Pensado para cuentas que\nmigran desde otro sistema de facturación y necesitan conservar su\nnumeración de sucursal (p. ej. casa matriz 002 en vez de 001)." parameters: [] responses: 200: description: 'Sucursal actualizada' content: application/json: schema: type: object example: success: true data: id: 019ee0-... local_code: '002' name: 'Casa Matriz San José' message: 'Sucursal actualizada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... local_code: type: string example: '002' name: type: string example: 'Casa Matriz San José' message: type: string example: 'Sucursal actualizada.' errors: type: string example: null nullable: true 404: description: 'Sucursal inexistente' content: application/json: schema: type: object example: success: false data: null message: 'La sucursal no existe.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La sucursal no existe.' errors: type: string example: null nullable: true 422: description: 'Código duplicado' content: application/json: schema: type: object example: success: false data: null message: 'Ya tenés una sucursal con ese código.' errors: local_code: - 'Ya tenés una sucursal con ese código.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Ya tenés una sucursal con ese código.' errors: type: object properties: local_code: type: array example: - 'Ya tenés una sucursal con ese código.' items: type: string tags: - 'Sucursales y cajas' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Nuevo nombre (2-100 chars). Opcional.' example: 'Casa Matriz San José' local_code: type: string description: 'Nuevo código de sucursal (3 dígitos, 001-999). Opcional; al cambiarlo se migran las secuencias.' example: '002' reason: type: string description: 'Motivo del cambio de código (bitácora). Opcional, máx 500 chars.' example: 'Numeración heredada del sistema anterior' nullable: true password: type: string description: 'Contraseña del usuario autenticado. Obligatoria cuando cambia `local_code`.' example: MiC0ntr4señ4 delete: summary: 'Eliminar una sucursal' operationId: eliminarUnaSucursal description: "Borra (soft delete) la sucursal junto con sus cajas. No se puede eliminar la\núltima sucursal del contribuyente (siempre debe quedar ≥1)." parameters: [] responses: 200: description: 'Sucursal eliminada' content: application/json: schema: type: object example: success: true data: null message: 'Sucursal eliminada.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Sucursal eliminada.' errors: type: string example: null nullable: true 404: description: 'Sucursal inexistente' content: application/json: schema: type: object example: success: false data: null message: 'La sucursal no existe.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La sucursal no existe.' errors: type: string example: null nullable: true 409: description: 'Es la última sucursal' content: application/json: schema: type: object example: success: false data: null message: 'Debés mantener al menos una sucursal.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Debés mantener al menos una sucursal.' errors: type: string example: null nullable: true tags: - 'Sucursales y cajas' parameters: - in: path name: id description: 'UUID de la sucursal.' example: 019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee required: true schema: type: string '/branches/{id}/terminals': post: summary: 'Agregar una caja a una sucursal' operationId: agregarUnaCajaAUnaSucursal description: "Crea una caja (terminal) en la sucursal directa indicada. El `terminal_code`\nse asigna automáticamente: slot `00` + la siguiente caja libre (los códigos\nborrados no se reutilizan). Respeta el tope de cajas por sucursal del plan." parameters: [] responses: 201: description: 'Caja creada' content: application/json: schema: type: object example: success: true data: id: 019ee0-... local_code: '001' terminals: - terminal_code: '00001' name: 'Caja 1' - terminal_code: '00002' name: 'Caja 2' message: 'Caja creada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... local_code: type: string example: '001' terminals: type: array example: - terminal_code: '00001' name: 'Caja 1' - terminal_code: '00002' name: 'Caja 2' items: type: object properties: terminal_code: type: string example: '00001' name: type: string example: 'Caja 1' message: type: string example: 'Caja creada.' errors: type: string example: null nullable: true 404: description: 'Sucursal inexistente' content: application/json: schema: type: object example: success: false data: null message: 'La sucursal no existe.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La sucursal no existe.' errors: type: string example: null nullable: true 422: description: 'Tope de cajas alcanzado' content: application/json: schema: type: object example: success: false data: null message: 'Alcanzó el tope de 3 caja(s) por sucursal de su plan.' errors: name: - 'Actualice su plan para agregar más cajas.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Alcanzó el tope de 3 caja(s) por sucursal de su plan.' errors: type: object properties: name: type: array example: - 'Actualice su plan para agregar más cajas.' items: type: string tags: - 'Sucursales y cajas' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Nombre de la caja (2-100 chars).' example: 'Caja 2' required: - name parameters: - in: path name: id description: 'UUID de la sucursal.' example: 019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee required: true schema: type: string '/branches/{id}/terminals/{tid}': put: summary: 'Actualizar una caja (nombre y/o código)' operationId: actualizarUnaCajanombreYoCdigo description: "Cambia el nombre de la caja y/o su `terminal_code` (posiciones 04-08 del\nNumeroConsecutivo). Cambiar el código exige confirmar la contraseña y\nmigra las secuencias existentes al código nuevo (nunca las reinicia).\nPensado para cuentas que migran desde otro sistema y necesitan conservar\nsu numeración de caja (p. ej. 00003 en vez de 00001). No se permite un\ncódigo ya reservado por la terminal de un integrador que gestione (o\nhaya gestionado) esta cuenta." parameters: [] responses: 200: description: 'Caja actualizada' content: application/json: schema: type: object example: success: true data: id: 019ee0-... terminals: - terminal_code: '00003' name: 'POS Entrada' message: 'Caja actualizada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... terminals: type: array example: - terminal_code: '00003' name: 'POS Entrada' items: type: object properties: terminal_code: type: string example: '00003' name: type: string example: 'POS Entrada' message: type: string example: 'Caja actualizada.' errors: type: string example: null nullable: true 404: description: 'Caja inexistente' content: application/json: schema: type: object example: success: false data: null message: 'La caja no existe.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La caja no existe.' errors: type: string example: null nullable: true 422: description: 'Código en uso' content: application/json: schema: type: object example: success: false data: null message: 'Ya tenés una caja con ese código.' errors: terminal_code: - 'Ya tenés una caja con ese código.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Ya tenés una caja con ese código.' errors: type: object properties: terminal_code: type: array example: - 'Ya tenés una caja con ese código.' items: type: string tags: - 'Sucursales y cajas' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Nuevo nombre (2-100 chars). Opcional.' example: 'POS Entrada' terminal_code: type: string description: 'Nuevo código de caja (5 dígitos, ≠00000). Opcional; al cambiarlo se migran las secuencias.' example: '00003' delegated_issuance: type: boolean description: 'Modo terminal delegada (aditivo POS): la caja genera su propia Clave de 50 dígitos y POST /vouchers la acepta. Cambiarlo exige contraseña.' example: true reason: type: string description: 'Motivo del cambio (bitácora). Opcional, máx 500 chars.' example: 'Numeración heredada del sistema anterior' nullable: true password: type: string description: 'Contraseña del usuario autenticado. Obligatoria cuando cambia `terminal_code` o `delegated_issuance`.' example: MiC0ntr4señ4 delete: summary: 'Eliminar una caja' operationId: eliminarUnaCaja description: "Borra (soft delete) una caja de la sucursal. No se puede eliminar la última\ncaja activa de la sucursal (toda sucursal debe conservar ≥1 caja). El código\nde la caja borrada no se reutiliza." parameters: [] responses: 200: description: 'Caja eliminada' content: application/json: schema: type: object example: success: true data: null message: 'Caja eliminada.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Caja eliminada.' errors: type: string example: null nullable: true 404: description: 'Caja inexistente' content: application/json: schema: type: object example: success: false data: null message: 'La caja no existe.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La caja no existe.' errors: type: string example: null nullable: true 409: description: 'Es la última caja de la sucursal' content: application/json: schema: type: object example: success: false data: null message: 'Debés mantener al menos una caja en la sucursal.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Debés mantener al menos una caja en la sucursal.' errors: type: string example: null nullable: true tags: - 'Sucursales y cajas' parameters: - in: path name: id description: 'UUID de la sucursal.' example: 019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee required: true schema: type: string - in: path name: tid description: 'UUID de la caja.' example: 019ee1bb-cccc-dddd-eeee-ffffffffffff 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/Almendro' secret: 'whsec_****************************ab3f' events: - voucher.accepted - voucher.rejected events_detail: - value: voucher.accepted label: 'Comprobante aceptado por Hacienda' group: voucher - value: voucher.rejected label: 'Comprobante rechazado por Hacienda' group: voucher description: 'Webhook principal e-commerce' is_active: true health_status: healthy consecutive_failures: 0 last_triggered_at: '2026-03-22T10:30:00-06:00' last_success_at: '2026-03-22T10:30:00-06:00' last_failure_at: null last_failure_reason: null created_at: '2026-03-20T08:00:00-06:00' updated_at: '2026-03-22T10:30:00-06:00' message: '' errors: null meta: current_page: 1 total: 3 per_page: 15 last_page: 1 links: first: ... last: ... prev: null next: null properties: success: type: boolean example: true data: type: array example: - id: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a url: 'https://api.miempresa.cr/webhooks/Almendro' secret: 'whsec_****************************ab3f' events: - voucher.accepted - voucher.rejected events_detail: - value: voucher.accepted label: 'Comprobante aceptado por Hacienda' group: voucher - value: voucher.rejected label: 'Comprobante rechazado por Hacienda' group: voucher description: 'Webhook principal e-commerce' is_active: true health_status: healthy consecutive_failures: 0 last_triggered_at: '2026-03-22T10:30:00-06:00' last_success_at: '2026-03-22T10:30:00-06:00' last_failure_at: null last_failure_reason: null created_at: '2026-03-20T08:00:00-06:00' updated_at: '2026-03-22T10:30:00-06:00' items: type: object properties: id: type: string example: 9c1b4e5a-3b2c-4d5e-6f7a-8b9c0d1e2f3a url: type: string example: 'https://api.miempresa.cr/webhooks/Almendro' 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/Almendro' secret: a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f6ab3f events: - voucher.accepted - voucher.rejected events_detail: - value: voucher.accepted label: 'Comprobante aceptado por Hacienda' group: voucher - value: voucher.rejected label: 'Comprobante rechazado por Hacienda' group: voucher description: 'Webhook principal e-commerce' is_active: true health_status: never_used consecutive_failures: 0 last_triggered_at: null last_success_at: null last_failure_at: null last_failure_reason: null created_at: '2026-03-22T10:30:00-06:00' updated_at: '2026-03-22T10:30:00-06:00' message: 'Webhook endpoint creado. Almacene el secret | no se mostrará de nuevo.' errors: null 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/Almendro' 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/Almendro' 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/Almendro' secret: 'whsec_****************************ab3f' events: - voucher.accepted - voucher.rejected events_detail: - value: voucher.accepted label: 'Comprobante aceptado por Hacienda' group: voucher description: 'Webhook principal e-commerce' is_active: true health_status: healthy consecutive_failures: 0 last_triggered_at: '2026-03-22T10:30:00-06:00' last_success_at: '2026-03-22T10:30:00-06:00' last_failure_at: null last_failure_reason: null created_at: '2026-03-20T08:00:00-06:00' updated_at: '2026-03-22T10:30:00-06:00' message: '' errors: null 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/Almendro' 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 '/webhooks/{id}/test': post: summary: 'Enviar un evento de prueba' operationId: enviarUnEventoDePrueba description: "Envía SÍNCRONAMENTE un payload `webhook.test` al endpoint, firmado con la\nmisma HMAC-SHA256 y los mismos headers `X-Almendro-*` que las entregas\nreales. Sirve para verificar que el receptor está accesible, valida la\nfirma y responde 2xx, sin tener que emitir un comprobante real.\n\nLa entrega queda registrada en el historial (`/webhooks/{id}/logs`) pero\nNO afecta los contadores de salud del endpoint (no auto-desactiva ni\nresetea fallos reales)." parameters: [] responses: 200: description: delivered content: application/json: schema: type: object example: success: true data: success: true response_status: 200 response_time_ms: 142 error_message: null delivery_id: … message: 'Evento de prueba entregado correctamente.' errors: null properties: success: type: boolean example: true data: type: object properties: success: type: boolean example: true response_status: type: integer example: 200 response_time_ms: type: integer example: 142 error_message: type: string example: null nullable: true delivery_id: type: string example: … message: type: string example: 'Evento de prueba entregado correctamente.' 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 150\n(IVA mensual) y 101/102/103 (utilidades).\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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' 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 /reports/sales-by-period: get: summary: 'Ventas agrupadas por periodo.' operationId: ventasAgrupadasPorPeriodo description: "Retorna series temporales de ventas agrupadas por dia, semana o mes.\nLa agrupacion se controla con el parametro `group_by` (day, week, month)\ny se auto-detecta segun el rango de fechas si no se especifica.\n\nUtil para graficos de tendencia en el dashboard y para verificar\ndatos antes de la declaracion 150 mensual." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 4 required: true schema: type: array description: '' example: - 4 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - draft required: true schema: type: array description: '' example: - draft items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 4 required: true schema: type: array description: '' example: - 4 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 6 required: true schema: type: array description: '' example: - 6 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: success: true data: period: date_from: '2026-03-01' date_to: '2026-03-31' currency_code: CRC environment: production group_by: week data: - period_start: '2026-03-02' period_label: 'Sem. 02 mar 2026' count: 48 total_venta: '3120500.00000' total_descuentos: '45000.00000' total_impuesto: '399815.00000' total_comprobante: '3475315.00000' - period_start: '2026-03-09' period_label: 'Sem. 09 mar 2026' count: 52 total_venta: '3410820.50000' total_descuentos: '38000.00000' total_impuesto: '438846.67000' total_comprobante: '3811667.17000' - period_start: '2026-03-16' period_label: 'Sem. 16 mar 2026' count: 46 total_venta: '2719000.00000' total_descuentos: '42000.00000' total_impuesto: '298880.00000' total_comprobante: '2975880.00000' grand_total: count: 146 total_venta: '9250320.50000' total_impuesto: '1137541.67000' total_comprobante: '10262862.17000' message: '' errors: null properties: success: type: boolean example: true data: type: object properties: period: type: object properties: date_from: type: string example: '2026-03-01' date_to: type: string example: '2026-03-31' currency_code: type: string example: CRC environment: type: string example: production group_by: type: string example: week data: type: array example: - period_start: '2026-03-02' period_label: 'Sem. 02 mar 2026' count: 48 total_venta: '3120500.00000' total_descuentos: '45000.00000' total_impuesto: '399815.00000' total_comprobante: '3475315.00000' - period_start: '2026-03-09' period_label: 'Sem. 09 mar 2026' count: 52 total_venta: '3410820.50000' total_descuentos: '38000.00000' total_impuesto: '438846.67000' total_comprobante: '3811667.17000' - period_start: '2026-03-16' period_label: 'Sem. 16 mar 2026' count: 46 total_venta: '2719000.00000' total_descuentos: '42000.00000' total_impuesto: '298880.00000' total_comprobante: '2975880.00000' items: type: object properties: period_start: type: string example: '2026-03-02' period_label: type: string example: 'Sem. 02 mar 2026' count: type: integer example: 48 total_venta: type: string example: '3120500.00000' total_descuentos: type: string example: '45000.00000' total_impuesto: type: string example: '399815.00000' total_comprobante: type: string example: '3475315.00000' grand_total: type: object properties: count: type: integer example: 146 total_venta: type: string example: '9250320.50000' total_impuesto: type: string example: '1137541.67000' total_comprobante: type: string example: '10262862.17000' message: type: string example: '' errors: type: string example: null nullable: true tags: - Reportes /reports/sales-by-receiver: get: summary: 'Ranking de ventas por receptor.' operationId: rankingDeVentasPorReceptor description: "Retorna los receptores (clientes) con mayor monto facturado en el\nperiodo, ordenados de mayor a menor. El parametro `limit` controla\ncuantos registros retornar (default 10, maximo 100).\n\nUtil para identificar clientes principales y para conciliacion\nfiscal por cedula de receptor." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 9 required: true schema: type: array description: '' example: - 9 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - pending required: true schema: type: array description: '' example: - pending items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 11 required: true schema: type: array description: '' example: - 11 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 2 required: true schema: type: array description: '' example: - 2 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: success: true data: period: date_from: '2026-03-01' date_to: '2026-03-31' currency_code: CRC environment: production limit: 10 data: - receiver_id_type: '02' receiver_id_number: '3101123456' receiver_name: 'Comercializadora La Catedral S.A.' count: 24 total_venta: '2980000.00000' total_impuesto: '387400.00000' total_comprobante: '3367400.00000' percentage: 32.81 - receiver_id_type: '02' receiver_id_number: '3101998877' receiver_name: 'Inversiones del Valle S.R.L.' count: 18 total_venta: '1740500.50000' total_impuesto: '226265.07000' total_comprobante: '1966765.57000' percentage: 19.16 - receiver_id_type: '01' receiver_id_number: '108880777' receiver_name: 'María Fernanda Rojas Alvarado' count: 11 total_venta: '845000.00000' total_impuesto: '109850.00000' total_comprobante: '954850.00000' percentage: 9.3 others: count: 93 total_comprobante: '3973846.60000' percentage: 38.73 grand_total: count: 146 total_comprobante: '10262862.17000' unique_receivers: 47 message: '' errors: null properties: success: type: boolean example: true data: type: object properties: period: type: object properties: date_from: type: string example: '2026-03-01' date_to: type: string example: '2026-03-31' currency_code: type: string example: CRC environment: type: string example: production limit: type: integer example: 10 data: type: array example: - receiver_id_type: '02' receiver_id_number: '3101123456' receiver_name: 'Comercializadora La Catedral S.A.' count: 24 total_venta: '2980000.00000' total_impuesto: '387400.00000' total_comprobante: '3367400.00000' percentage: 32.81 - receiver_id_type: '02' receiver_id_number: '3101998877' receiver_name: 'Inversiones del Valle S.R.L.' count: 18 total_venta: '1740500.50000' total_impuesto: '226265.07000' total_comprobante: '1966765.57000' percentage: 19.16 - receiver_id_type: '01' receiver_id_number: '108880777' receiver_name: 'María Fernanda Rojas Alvarado' count: 11 total_venta: '845000.00000' total_impuesto: '109850.00000' total_comprobante: '954850.00000' percentage: 9.3 items: type: object properties: receiver_id_type: type: string example: '02' receiver_id_number: type: string example: '3101123456' receiver_name: type: string example: 'Comercializadora La Catedral S.A.' count: type: integer example: 24 total_venta: type: string example: '2980000.00000' total_impuesto: type: string example: '387400.00000' total_comprobante: type: string example: '3367400.00000' percentage: type: number example: 32.81 others: type: object properties: count: type: integer example: 93 total_comprobante: type: string example: '3973846.60000' percentage: type: number example: 38.73 grand_total: type: object properties: count: type: integer example: 146 total_comprobante: type: string example: '10262862.17000' unique_receivers: type: integer example: 47 message: type: string example: '' errors: type: string example: null nullable: true tags: - Reportes /reports/sales-by-activity: get: summary: 'Ventas agrupadas por actividad economica.' operationId: ventasAgrupadasPorActividadEconomica description: "Retorna montos y conteos agrupados por codigo de actividad economica\nCIIU del emisor. Esencial para la declaracion 150 que requiere\nreportar IVA desglosado por actividad economica." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 8 required: true schema: type: array description: '' example: - 8 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - sent required: true schema: type: array description: '' example: - sent items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 10 required: true schema: type: array description: '' example: - 10 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 6 required: true schema: type: array description: '' example: - 6 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: success: true data: period: date_from: '2026-03-01' date_to: '2026-03-31' currency_code: CRC environment: production data: - activity_code: '620100' activity_description: 'Actividades de programación informática' count: 86 total_venta: '6120000.00000' total_impuesto: '795600.00000' total_comprobante: '6915600.00000' percentage: 67.38 - activity_code: '702000' activity_description: 'Actividades de consultoría de gestión' count: 41 total_venta: '2380320.50000' total_impuesto: '309441.67000' total_comprobante: '2689762.17000' percentage: 26.21 - activity_code: '631100' activity_description: 'Procesamiento de datos y hospedaje' count: 19 total_venta: '750000.00000' total_impuesto: '97500.00000' total_comprobante: '657500.00000' percentage: 6.41 grand_total: count: 146 total_comprobante: '10262862.17000' unique_activities: 3 message: '' errors: null properties: success: type: boolean example: true data: type: object properties: period: type: object properties: date_from: type: string example: '2026-03-01' date_to: type: string example: '2026-03-31' currency_code: type: string example: CRC environment: type: string example: production data: type: array example: - activity_code: '620100' activity_description: 'Actividades de programación informática' count: 86 total_venta: '6120000.00000' total_impuesto: '795600.00000' total_comprobante: '6915600.00000' percentage: 67.38 - activity_code: '702000' activity_description: 'Actividades de consultoría de gestión' count: 41 total_venta: '2380320.50000' total_impuesto: '309441.67000' total_comprobante: '2689762.17000' percentage: 26.21 - activity_code: '631100' activity_description: 'Procesamiento de datos y hospedaje' count: 19 total_venta: '750000.00000' total_impuesto: '97500.00000' total_comprobante: '657500.00000' percentage: 6.41 items: type: object properties: activity_code: type: string example: '620100' activity_description: type: string example: 'Actividades de programación informática' count: type: integer example: 86 total_venta: type: string example: '6120000.00000' total_impuesto: type: string example: '795600.00000' total_comprobante: type: string example: '6915600.00000' percentage: type: number example: 67.38 grand_total: type: object properties: count: type: integer example: 146 total_comprobante: type: string example: '10262862.17000' unique_activities: type: integer example: 3 message: type: string example: '' errors: type: string example: null nullable: true tags: - Reportes /reports/tax-summary: get: summary: 'Desglose de IVA por tarifa | datos para la declaracion 150.' operationId: desgloseDeIVAPorTarifaDatosParaLaDeclaracion150 description: "Retorna el impuesto agrupado por codigo de tarifa IVA (0%, 1%, 2%,\n4%, 13%, exento, etc.). La declaracion 150 ante Hacienda requiere\nreportar el IVA desglosado exactamente por estas tarifas." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 2 required: true schema: type: array description: '' example: - 2 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - draft required: true schema: type: array description: '' example: - draft items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 4 required: true schema: type: array description: '' example: - 4 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 99 required: true schema: type: array description: '' example: - 99 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: success: true data: period: date_from: '2026-03-01' date_to: '2026-03-31' currency_code: CRC environment: production by_tax_code: - tax_code: '01' tax_code_label: IVA total_monto: '1118041.67000' voucher_count: 138 breakdown_by_rate: - rate_code: '08' rate_label: '13% General' rate_percentage: 13.0 total_monto: '1095541.67000' voucher_count: 132 - rate_code: '04' rate_label: '4% Bienes y servicios especiales' rate_percentage: 4.0 total_monto: '22500.00000' voucher_count: 6 - tax_code: '07' tax_code_label: 'Impuesto específico' total_monto: '19500.00000' voucher_count: 8 breakdown_by_rate: - rate_code: '11' rate_label: 'Tarifa especial' rate_percentage: 0.0 total_monto: '19500.00000' voucher_count: 8 totals: total_impuesto: '1137541.67000' total_iva_devuelto: '0.00000' voucher_count: 146 message: '' errors: null properties: success: type: boolean example: true data: type: object properties: period: type: object properties: date_from: type: string example: '2026-03-01' date_to: type: string example: '2026-03-31' currency_code: type: string example: CRC environment: type: string example: production by_tax_code: type: array example: - tax_code: '01' tax_code_label: IVA total_monto: '1118041.67000' voucher_count: 138 breakdown_by_rate: - rate_code: '08' rate_label: '13% General' rate_percentage: 13 total_monto: '1095541.67000' voucher_count: 132 - rate_code: '04' rate_label: '4% Bienes y servicios especiales' rate_percentage: 4 total_monto: '22500.00000' voucher_count: 6 - tax_code: '07' tax_code_label: 'Impuesto específico' total_monto: '19500.00000' voucher_count: 8 breakdown_by_rate: - rate_code: '11' rate_label: 'Tarifa especial' rate_percentage: 0 total_monto: '19500.00000' voucher_count: 8 items: type: object properties: tax_code: type: string example: '01' tax_code_label: type: string example: IVA total_monto: type: string example: '1118041.67000' voucher_count: type: integer example: 138 breakdown_by_rate: type: array example: - rate_code: '08' rate_label: '13% General' rate_percentage: 13 total_monto: '1095541.67000' voucher_count: 132 - rate_code: '04' rate_label: '4% Bienes y servicios especiales' rate_percentage: 4 total_monto: '22500.00000' voucher_count: 6 items: type: object properties: rate_code: type: string example: '08' rate_label: type: string example: '13% General' rate_percentage: type: number example: 13.0 total_monto: type: string example: '1095541.67000' voucher_count: type: integer example: 132 totals: type: object properties: total_impuesto: type: string example: '1137541.67000' total_iva_devuelto: type: string example: '0.00000' voucher_count: type: integer example: 146 message: type: string example: '' errors: type: string example: null nullable: true tags: - Reportes /reports/receiver-messages: get: summary: 'Resumen de confirmaciones de receptor (MensajeReceptor).' operationId: resumenDeConfirmacionesDeReceptorMensajeReceptor description: "Retorna el conteo de mensajes de receptor agrupados por estado:\nconfirmados, pendientes y vencidos. Incluye alertas de plazo segun\nel art. 15 del Reglamento (8 dias habiles para confirmar).\n\nEste endpoint solo acepta los filtros `date_from`, `date_to` y\n`environment`. Los demas filtros (voucher_type, currency_code, etc.)\nno aplican a mensajes de receptor." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 2 required: true schema: type: array description: '' example: - 2 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - accepted required: true schema: type: array description: '' example: - accepted items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 10 required: true schema: type: array description: '' example: - 10 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 4 required: true schema: type: array description: '' example: - 4 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: success: true data: period: date_from: '2026-03-01' date_to: '2026-03-31' environment: production summary: total: 64 by_status: accepted: 58 sent: 3 draft: 2 error: 1 by_message: accepted_total: 55 accepted_partial: 3 rejected: 2 deadline_alerts: overdue: 2 due_soon: 5 on_time: 57 deadline_business_days: 8 alert_threshold_days: 2 by_issuer: - issuer_id_number: '3101456789' count: 31 total_invoice: '4120000.00000' total_tax: '535600.00000' accepted: 29 rejected: 1 - issuer_id_number: '3101778899' count: 22 total_invoice: '1890500.00000' total_tax: '245765.00000' accepted: 21 rejected: 0 - issuer_id_number: '108450321' count: 11 total_invoice: '612000.00000' total_tax: '79560.00000' accepted: 8 rejected: 1 message: '' errors: null properties: success: type: boolean example: true data: type: object properties: period: type: object properties: date_from: type: string example: '2026-03-01' date_to: type: string example: '2026-03-31' environment: type: string example: production summary: type: object properties: total: type: integer example: 64 by_status: type: object properties: accepted: type: integer example: 58 sent: type: integer example: 3 draft: type: integer example: 2 error: type: integer example: 1 by_message: type: object properties: accepted_total: type: integer example: 55 accepted_partial: type: integer example: 3 rejected: type: integer example: 2 deadline_alerts: type: object properties: overdue: type: integer example: 2 due_soon: type: integer example: 5 on_time: type: integer example: 57 deadline_business_days: type: integer example: 8 alert_threshold_days: type: integer example: 2 by_issuer: type: array example: - issuer_id_number: '3101456789' count: 31 total_invoice: '4120000.00000' total_tax: '535600.00000' accepted: 29 rejected: 1 - issuer_id_number: '3101778899' count: 22 total_invoice: '1890500.00000' total_tax: '245765.00000' accepted: 21 rejected: 0 - issuer_id_number: '108450321' count: 11 total_invoice: '612000.00000' total_tax: '79560.00000' accepted: 8 rejected: 1 items: type: object properties: issuer_id_number: type: string example: '3101456789' count: type: integer example: 31 total_invoice: type: string example: '4120000.00000' total_tax: type: string example: '535600.00000' accepted: type: integer example: 29 rejected: type: integer example: 1 message: type: string example: '' errors: type: string example: null nullable: true tags: - Reportes /reports/vouchers-by-status: get: summary: 'Comprobantes agrupados por estado Hacienda.' operationId: comprobantesAgrupadosPorEstadoHacienda description: "Retorna conteo y montos agrupados por el estado de confirmacion de\nHacienda (aceptado, rechazado, enviado, error, pendiente). Incluye\nmetricas como tasa de aceptacion, tasa de rechazo y conteo de\ncomprobantes pendientes.\n\nA diferencia de otros reportes, este endpoint no filtra por estado\n(ya que el proposito es mostrar TODOS los estados agrupados)." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 9 required: true schema: type: array description: '' example: - 9 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - error required: true schema: type: array description: '' example: - error items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 14 required: true schema: type: array description: '' example: - 14 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 99 required: true schema: type: array description: '' example: - 99 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: success: true data: period: date_from: '2026-03-01' date_to: '2026-03-31' currency_code: CRC environment: production data: - status: accepted status_label: Aceptado count: 138 total_venta: '8920320.50000' total_impuesto: '1118041.67000' total_comprobante: '9912362.17000' percentage_count: 94.52 percentage_amount: 96.58 - status: rejected status_label: Rechazado count: 4 total_venta: '210000.00000' total_impuesto: '16000.00000' total_comprobante: '275500.00000' percentage_count: 2.74 percentage_amount: 2.68 - status: pending status_label: Pendiente count: 3 total_venta: '108000.00000' total_impuesto: '3500.00000' total_comprobante: '62500.00000' percentage_count: 2.05 percentage_amount: 0.61 - status: error status_label: Error count: 1 total_venta: '12000.00000' total_impuesto: '0.00000' total_comprobante: '12500.00000' percentage_count: 0.68 percentage_amount: 0.12 grand_total: count: 146 total_comprobante: '10262862.17000' metrics: acceptance_rate: 94.52 rejection_rate: 2.74 pending_count: 3 error_count: 1 avg_processing_time_hours: 1.4 message: '' errors: null properties: success: type: boolean example: true data: type: object properties: period: type: object properties: date_from: type: string example: '2026-03-01' date_to: type: string example: '2026-03-31' currency_code: type: string example: CRC environment: type: string example: production data: type: array example: - status: accepted status_label: Aceptado count: 138 total_venta: '8920320.50000' total_impuesto: '1118041.67000' total_comprobante: '9912362.17000' percentage_count: 94.52 percentage_amount: 96.58 - status: rejected status_label: Rechazado count: 4 total_venta: '210000.00000' total_impuesto: '16000.00000' total_comprobante: '275500.00000' percentage_count: 2.74 percentage_amount: 2.68 - status: pending status_label: Pendiente count: 3 total_venta: '108000.00000' total_impuesto: '3500.00000' total_comprobante: '62500.00000' percentage_count: 2.05 percentage_amount: 0.61 - status: error status_label: Error count: 1 total_venta: '12000.00000' total_impuesto: '0.00000' total_comprobante: '12500.00000' percentage_count: 0.68 percentage_amount: 0.12 items: type: object properties: status: type: string example: accepted status_label: type: string example: Aceptado count: type: integer example: 138 total_venta: type: string example: '8920320.50000' total_impuesto: type: string example: '1118041.67000' total_comprobante: type: string example: '9912362.17000' percentage_count: type: number example: 94.52 percentage_amount: type: number example: 96.58 grand_total: type: object properties: count: type: integer example: 146 total_comprobante: type: string example: '10262862.17000' metrics: type: object properties: acceptance_rate: type: number example: 94.52 rejection_rate: type: number example: 2.74 pending_count: type: integer example: 3 error_count: type: integer example: 1 avg_processing_time_hours: type: number example: 1.4 message: type: string example: '' errors: type: string example: null nullable: true tags: - Reportes /reports/iva-auxiliary: get: summary: 'Auxiliar del IVA (D-150): ventas y compras por tarifa.' operationId: auxiliarDelIVAD150VentasYComprasPorTarifa description: "Produce los dos auxiliares de la declaracion autoliquidativa 150 de\nTRIBU-CR: ventas generales por tarifa (base + impuesto), ventas sin\nIVA, compras por tarifa (importe + impuesto soportado), y el detalle\npor cliente y por proveedor. Solo comprobantes ACEPTADOS; las notas\nde credito restan." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 1 required: true schema: type: array description: '' example: - 1 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - accepted required: true schema: type: array description: '' example: - accepted items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 10 required: true schema: type: array description: '' example: - 10 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 6 required: true schema: type: array description: '' example: - 6 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: { } tags: - Reportes /reports/iva-auxiliary/export: get: summary: 'Exporta el auxiliar del IVA (D-150) en CSV o PDF.' operationId: exportaElAuxiliarDelIVAD150EnCSVOPDF description: "format=csv§ion=sales|purchases descarga el auxiliar pedido en CSV;\nformat=pdf descarga el documento completo (ventas + compras) listo\npara imprimir o adjuntar al expediente del periodo." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 9 required: true schema: type: array description: '' example: - 9 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - rejected required: true schema: type: array description: '' example: - rejected items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 9 required: true schema: type: array description: '' example: - 9 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 6 required: true schema: type: array description: '' example: - 6 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: { } tags: - Reportes '/reports/{report}/export': get: summary: 'Exporta un reporte del portal en CSV o PDF.' operationId: exportaUnReporteDelPortalEnCSVOPDF description: "Reportes: sales-by-period, sales-by-receiver, sales-by-activity.\nformat=csv descarga la tabla; format=pdf el documento membretado." parameters: - in: query name: date_from description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' required: false schema: type: string description: 'Inicio del rango de fecha de emisión (YYYY-MM-DD). Default: primer día del mes actual. Must be a valid date in the format Y-m-d.' example: '2026-08-01' - in: query name: date_to description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' required: false schema: type: string description: 'Fin del rango de fecha de emisión (YYYY-MM-DD). Default: hoy. Debe ser >= date_from. Must be a valid date in the format Y-m-d. El campo value debe ser una fecha posterior o igual a date_from.' example: '2026-08-11' - in: query name: voucher_type description: '' example: - 3 required: true schema: type: array description: '' example: - 3 items: type: string enum: - 1 - 2 - 3 - 4 - 8 - 9 - 10 - in: query name: status description: '' example: - sent required: true schema: type: array description: '' example: - sent items: type: string enum: - draft - pending - sent - accepted - rejected - error - cancelled - in: query name: currency_code description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC required: false schema: type: string description: 'Moneda ISO 4217 (3 letras). Default: CRC (150 en colones). Must match the regex /^[A-Z]{3}$/. El campo value debe tener 3 caracteres.' example: CRC - in: query name: environment description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production required: false schema: type: string description: 'Ambiente: sandbox o production. Default: production (solo comprobantes con validez fiscal).' example: production enum: - sandbox - production - in: query name: receiver_id_number description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' required: false schema: type: string description: 'Cédula del receptor (9-12 dígitos). Art. 13.10 criterio 1. Must match the regex /^\d{9,12}$/.' example: '3101234567' - in: query name: sale_condition description: '' example: - 11 required: true schema: type: array description: '' example: - 11 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 99 - in: query name: payment_method description: '' example: - 4 required: true schema: type: array description: '' example: - 4 items: type: string enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 99 - 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. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 required: false schema: type: integer description: 'Cantidad de registros top (solo sales-by-receiver). Default: 10, máximo: 100. El campo value debe ser al menos 1. El campo value no puede ser mayor a 100.' example: 10 responses: { } tags: - Reportes parameters: - in: path name: report description: 'The report.' example: sales-by-period|sales-by-receiver|sales-by-activity required: true schema: type: string /reports/filters: get: summary: 'Facets para los filtros de reportes.' operationId: facetsParaLosFiltrosDeReportes description: '' parameters: [] responses: { } 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 /catalogs/currencies: get: summary: "Catálogo de monedas activas (ISO 4217) para el selector de moneda en la\nemisión de comprobantes." operationId: catlogoDeMonedasActivasISO4217ParaElSelectorDeMonedaEnLaEmisinDeComprobantes description: "Las más usadas en Costa Rica (CRC, USD, EUR) se devuelven primero; el\nresto en orden alfabético por código. `bccr_rate` trae el tipo de cambio\nde referencia del BCCR cuando aplica (USD), para prellenar el campo de\ntipo de cambio en la UI; si es null, el usuario lo ingresa a mano." parameters: [] responses: { } 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: - in: header name: Idempotency-Key description: '' example: 'Identificador único de ESTA confirmación, generado por usted. Es' schema: type: string 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: '3101999999' receiver_consecutive: '00100001050000000001' status: draft has_xml: true is_signed: true environment: production deadline: business_days_elapsed: 2 business_days_remaining: 6 deadline_date: '2026-04-07' is_within_deadline: true is_near_deadline: false is_overdue: false hacienda: status: null message: null sent_at: null processed_at: null issued_by: 019d867d-0032-7288-8ece-fd64da756032 created_at: '2026-04-16T10:00:00-06:00' updated_at: '2026-04-16T10:00:00-06:00' message: 'Aceptado del comprobante [506...001] generada y validada correctamente. Pendiente de envío a Hacienda.' errors: null 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 Almendro. `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: '3101999999' 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). El campo value no puede tener más de 20 caracteres.' example: '3101999999' doc_issued_at: type: string description: 'Fecha de emisión del comprobante recibido (xs:dateTime). No puede ser futura. El campo value no es una fecha válida. El campo value debe ser una fecha anterior o igual a now.' 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. El campo value debe tener al menos 1 caracteres. El campo value no puede tener más de 160 caracteres.' 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. El campo value debe ser al menos 0.' 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`. El campo value debe ser al menos 0.' example: 6500.0 nullable: true expense_applicable_amount: type: number description: 'Monto total de gasto aplicable (Decimal 18,5). Obligatorio si `tax_condition` ≠ `05`. El campo value debe ser al menos 0.' example: 50000.0 nullable: true total_invoice: type: number description: 'Monto total del comprobante (Decimal 18,5). Siempre obligatorio. El campo value debe ser al menos 0.' 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 /inbox: get: summary: 'Listar comprobantes del buzón de recibidos' operationId: listarComprobantesDelBuznDeRecibidos description: "Devuelve, paginados, los comprobantes que el contribuyente ha\n**recibido** como receptor: tanto los emitidos por otros usuarios de\nAlmendro como los cargados manualmente (XML de otro facturador). Es un\nlistado de **solo lectura** | la aceptación o rechazo se hace por el\nflujo de `POST /receiver/confirm` (MensajeReceptor).\n\nCada fila expone solo la información pública del documento (Art. 16):\nemisor, clave, tipo, fecha, moneda y totales, junto al plazo de\nrespuesta (8 días hábiles), el estado de respuesta del receptor y el\norigen del comprobante en el buzón.\n\nLa búsqueda por texto (`search`) filtra por clave, nombre del emisor o\nnúmero de identificación del emisor." parameters: - in: query name: search description: 'Filtra por clave, nombre del emisor o número de identificación del emisor.' example: '3101123456' required: false schema: type: string description: 'Filtra por clave, nombre del emisor o número de identificación del emisor.' example: '3101123456' - in: query name: per_page description: 'Resultados por página (1-100). Default: 20.' example: 20 required: false schema: type: integer description: 'Resultados por página (1-100). Default: 20.' example: 20 - 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: - voucher_key: '50601012600310112345600100001010000000001199999999' consecutive_number: '00100001010000000001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' issued_at: '2026-06-01T10:00:00-06:00' currency_code: CRC emisor: name: 'PROVEEDOR EJEMPLO S.A.' id_number: '3101123456' alias: 'Hosting junio' notes: 'Pagado con tarjeta corporativa' xsd_valid: null totals: total_impuesto: '1300.00000' total_comprobante: '11300.00000' deadline: deadline_at: '2026-06-11' remaining_business_days: 5 overdue: false status: accepted environment: prod source: almendro source_label: 'Emitido en Almendro' response_status: pending response_status_label: Pendiente is_automatic: false confirmation: confirmed: false message: null message_label: null status: null line_items: null message: 'Comprobantes recibidos (buzón).' errors: null meta: current_page: 1 last_page: 3 per_page: 20 total: 45 from: 1 to: 20 links: first: ... last: ... prev: null next: ... properties: success: type: boolean example: true data: type: array example: - voucher_key: '50601012600310112345600100001010000000001199999999' consecutive_number: '00100001010000000001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' issued_at: '2026-06-01T10:00:00-06:00' currency_code: CRC emisor: name: 'PROVEEDOR EJEMPLO S.A.' id_number: '3101123456' alias: 'Hosting junio' notes: 'Pagado con tarjeta corporativa' xsd_valid: null totals: total_impuesto: '1300.00000' total_comprobante: '11300.00000' deadline: deadline_at: '2026-06-11' remaining_business_days: 5 overdue: false status: accepted environment: prod source: almendro source_label: 'Emitido en Almendro' response_status: pending response_status_label: Pendiente is_automatic: false confirmation: confirmed: false message: null message_label: null status: null line_items: null items: type: object properties: voucher_key: type: string example: '50601012600310112345600100001010000000001199999999' consecutive_number: type: string example: '00100001010000000001' voucher_type: type: string example: '01' voucher_type_label: type: string example: 'Factura Electrónica' issued_at: type: string example: '2026-06-01T10:00:00-06:00' currency_code: type: string example: CRC emisor: type: object properties: name: type: string example: 'PROVEEDOR EJEMPLO S.A.' id_number: type: string example: '3101123456' alias: type: string example: 'Hosting junio' notes: type: string example: 'Pagado con tarjeta corporativa' xsd_valid: type: string example: null nullable: true totals: type: object properties: total_impuesto: type: string example: '1300.00000' total_comprobante: type: string example: '11300.00000' deadline: type: object properties: deadline_at: type: string example: '2026-06-11' remaining_business_days: type: integer example: 5 overdue: type: boolean example: false status: type: string example: accepted environment: type: string example: prod source: type: string example: almendro source_label: type: string example: 'Emitido en Almendro' response_status: type: string example: pending response_status_label: type: string example: Pendiente is_automatic: type: boolean example: false confirmation: type: object properties: confirmed: type: boolean example: false message: type: string example: null nullable: true message_label: type: string example: null nullable: true status: type: string example: null nullable: true line_items: type: string example: null nullable: true message: type: string example: 'Comprobantes recibidos (buzón).' errors: type: string example: null nullable: true meta: type: object properties: current_page: type: integer example: 1 last_page: type: integer example: 3 per_page: type: integer example: 20 total: type: integer example: 45 from: type: integer example: 1 to: type: integer example: 20 links: type: object properties: first: type: string example: ... last: type: string example: ... prev: type: string example: null nullable: true next: type: string example: ... - description: 'Buzón vacío' type: object example: success: true data: [] message: 'Comprobantes recibidos (buzón).' errors: null meta: current_page: 1 last_page: 1 per_page: 20 total: 0 from: null to: null links: first: ... last: ... prev: null next: null properties: success: type: boolean example: true data: type: array example: [] message: type: string example: 'Comprobantes recibidos (buzón).' 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: 20 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: - 'Buzón de Recibidos' /inbox/upload: post: summary: 'Cargar un comprobante externo al buzón' operationId: cargarUnComprobanteExternoAlBuzn description: "Sube el XML firmado de un comprobante (generado por **cualquier\nfacturador**, dentro o fuera de Almendro) al buzón del receptor. El\nsistema parsea el XML, valida que el **receptor** del documento sea el\ncontribuyente autenticado (la cédula del receptor debe coincidir con la\ndel certificado `.p12`) y consulta su estado en Hacienda.\n\nSi el comprobante ya estaba en el buzón, la carga **refresca** su\nestado en vez de duplicarlo.\n\nEnvíe el XML de **una** de estas dos formas (al menos una es\nobligatoria):\n\n- `xml_file`: el archivo `.xml` (multipart/form-data).\n- `xml`: el contenido del XML como string (JSON o form)." parameters: [] responses: 201: description: 'Comprobante cargado' content: application/json: schema: type: object example: success: true data: voucher_key: '50601012600310112345600100001010000000001199999999' consecutive_number: '00100001010000000001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' issued_at: '2026-06-01T10:00:00-06:00' currency_code: CRC emisor: name: 'PROVEEDOR EJEMPLO S.A.' id_number: '3101123456' alias: null notes: null xsd_valid: true totals: total_impuesto: '1300.00000' total_comprobante: '11300.00000' deadline: deadline_at: '2026-06-11' remaining_business_days: 5 overdue: false status: accepted environment: prod source: api_upload source_label: 'Cargado por API' response_status: pending response_status_label: Pendiente is_automatic: false confirmation: confirmed: false message: null message_label: null status: null line_items: null message: 'Comprobante cargado en el buzón.' errors: null properties: success: type: boolean example: true data: type: object properties: voucher_key: type: string example: '50601012600310112345600100001010000000001199999999' consecutive_number: type: string example: '00100001010000000001' voucher_type: type: string example: '01' voucher_type_label: type: string example: 'Factura Electrónica' issued_at: type: string example: '2026-06-01T10:00:00-06:00' currency_code: type: string example: CRC emisor: type: object properties: name: type: string example: 'PROVEEDOR EJEMPLO S.A.' id_number: type: string example: '3101123456' alias: type: string example: null nullable: true notes: type: string example: null nullable: true xsd_valid: type: boolean example: true totals: type: object properties: total_impuesto: type: string example: '1300.00000' total_comprobante: type: string example: '11300.00000' deadline: type: object properties: deadline_at: type: string example: '2026-06-11' remaining_business_days: type: integer example: 5 overdue: type: boolean example: false status: type: string example: accepted environment: type: string example: prod source: type: string example: api_upload source_label: type: string example: 'Cargado por API' response_status: type: string example: pending response_status_label: type: string example: Pendiente is_automatic: type: boolean example: false confirmation: type: object properties: confirmed: type: boolean example: false message: type: string example: null nullable: true message_label: type: string example: null nullable: true status: type: string example: null nullable: true line_items: type: string example: null nullable: true message: type: string example: 'Comprobante cargado en el buzón.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Falta el XML' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: xml: - 'Debe enviar el comprobante en el campo "xml" (string) o "xml_file" (archivo).' 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: xml: type: array example: - 'Debe enviar el comprobante en el campo "xml" (string) o "xml_file" (archivo).' items: type: string - description: 'No está a nombre del contribuyente' type: object example: success: false data: null message: 'El comprobante no está a nombre de su contribuyente.' errors: xml: - 'El comprobante no está a nombre de su contribuyente.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El comprobante no está a nombre de su contribuyente.' errors: type: object properties: xml: type: array example: - 'El comprobante no está a nombre de su contribuyente.' items: type: string - description: 'XML inválido o no respondible' type: object example: success: false data: null message: 'No se pudo procesar el XML del comprobante.' errors: xml: - 'No se pudo procesar el XML del comprobante.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo procesar el XML del comprobante.' errors: type: object properties: xml: type: array example: - 'No se pudo procesar el XML del comprobante.' items: type: string tags: - 'Buzón de Recibidos' requestBody: required: false content: multipart/form-data: schema: type: object properties: xml: type: string description: 'El XML firmado del comprobante como texto. Obligatorio si no envía `xml_file`.' example: '...' nullable: true xml_file: type: string format: binary description: 'Archivo .xml del comprobante (máximo 5 MB). Obligatorio si no envía `xml`.' nullable: true source: type: string description: 'Origen de la carga. Valores: `gui_upload` (portal web), `api_upload` (integración). Default: `api_upload`.' example: api_upload nullable: true target_contributor_id: type: string description: 'El campo value debe ser un UUID válido.' example: a4855dc5-0acb-33c3-b921-f4291f719ca0 nullable: true /inbox/upload-batch: post: summary: 'Cargar comprobantes externos en lote' operationId: cargarComprobantesExternosEnLote description: "Carga masiva al buzón: procesa cada comprobante de forma\n**independiente**, de modo que un XML inválido o ajeno **no aborta el\nlote** | el resultado se reporta por ítem.\n\nEnvíe el lote de una o ambas de estas formas (al menos una es\nobligatoria):\n\n- `files[]`: varios archivos `.xml` (multipart/form-data), máximo 50.\n- `zip`: un archivo `.zip` cuyas entradas `.xml` se procesan una a una.\n\nEl total de comprobantes procesados se limita a 50 (archivos sueltos +\nentradas del ZIP). Siempre responde **HTTP 200** con el detalle por\nítem y un resumen, aunque algunos o todos hayan fallado." parameters: [] responses: 200: description: 'Lote procesado (con un fallo)' content: application/json: schema: type: object example: success: true data: results: - name: factura-001.xml status: ok voucher_key: '50601012600310112345600100001010000000001199999999' message: null - name: factura-002.xml status: error voucher_key: null message: 'El comprobante no está a nombre de su contribuyente.' summary: total: 2 ok: 1 failed: 1 message: 'Lote procesado: 1 de 2 comprobante(s) cargado(s).' errors: null properties: success: type: boolean example: true data: type: object properties: results: type: array example: - name: factura-001.xml status: ok voucher_key: '50601012600310112345600100001010000000001199999999' message: null - name: factura-002.xml status: error voucher_key: null message: 'El comprobante no está a nombre de su contribuyente.' description: 'Resultado individual de cada comprobante del lote.' items: type: object properties: name: type: string example: factura-001.xml status: type: string example: ok voucher_key: type: string example: '50601012600310112345600100001010000000001199999999' message: type: string example: null nullable: true summary: type: object properties: total: type: integer example: 2 description: 'Total de comprobantes procesados.' ok: type: integer example: 1 description: 'Cantidad cargada correctamente.' failed: type: integer example: 1 description: 'Cantidad que falló.' description: 'Resumen agregado del lote.' message: type: string example: 'Lote procesado: 1 de 2 comprobante(s) cargado(s).' errors: type: string example: null nullable: true 422: description: 'Lote vacío' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: files: - 'Debe enviar al menos un archivo en "files[]" o un "zip" con comprobantes.' 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: files: type: array example: - 'Debe enviar al menos un archivo en "files[]" o un "zip" con comprobantes.' items: type: string tags: - 'Buzón de Recibidos' requestBody: required: false content: multipart/form-data: schema: type: object properties: files: type: array description: 'Archivos .xml de comprobantes (máximo 50 archivos, 5 MB cada uno). Obligatorio si no envía `zip`.' items: type: string format: binary zip: type: string format: binary description: 'Archivo .zip con comprobantes .xml (máximo 20 MB). Obligatorio si no envía `files[]`.' nullable: true source: type: string description: 'Origen de la carga. Valores: `gui_upload` (portal web), `api_upload` (integración). Default: `api_upload`.' example: api_upload nullable: true '/inbox/lookup/{clave}': get: summary: 'Validar una clave contra Hacienda' operationId: validarUnaClaveContraHacienda description: "Consulta directamente en Hacienda el estado de una **clave** (aceptado\n/ rechazado), incluso de comprobantes emitidos por **otros\nfacturadores** y que no estén en su buzón. Sirve para validar la\nfactura de un proveedor antes de aceptarla.\n\nLa consulta usa el **certificado activo** del contribuyente para el\nambiente en curso. Si no hay certificado activo para ese ambiente,\nresponde **HTTP 422**. Si Hacienda no responde, responde **HTTP 502**.\n\nEl campo `in_inbox` indica si esa clave ya figura como recibida por el\ncontribuyente (su cédula es la del receptor del comprobante)." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: 'Clave encontrada en Hacienda' type: object example: success: true data: clave: '50601012600310112345600100001010000000001199999999' found: true ind_estado: aceptado detail: 'Comprobante aceptado por Hacienda.' in_inbox: false message: 'Estado consultado en Hacienda.' errors: null properties: success: type: boolean example: true data: type: object properties: clave: type: string example: '50601012600310112345600100001010000000001199999999' description: 'La clave consultada (eco del parámetro de URL).' found: type: boolean example: true description: '`true` si Hacienda tiene registro de la clave.' ind_estado: type: string example: aceptado description: 'Indicador de estado devuelto por Hacienda (ej. `aceptado`, `rechazado`). `null` si no se encontró.' detail: type: string example: 'Comprobante aceptado por Hacienda.' description: 'Detalle textual del estado devuelto por Hacienda. `null` si no se encontró.' in_inbox: type: boolean example: false description: '`true` si esa clave ya figura como comprobante recibido por el contribuyente.' message: type: string example: 'Estado consultado en Hacienda.' errors: type: string example: null nullable: true - description: 'Clave no registrada en Hacienda' type: object example: success: true data: clave: '50601012600310112345600100001010000000001199999999' found: false ind_estado: null detail: null in_inbox: false message: 'Hacienda no tiene registro de esta clave.' errors: null properties: success: type: boolean example: true data: type: object properties: clave: type: string example: '50601012600310112345600100001010000000001199999999' description: 'La clave consultada (eco del parámetro de URL).' found: type: boolean example: false description: '`true` si Hacienda tiene registro de la clave.' ind_estado: type: string example: null description: 'Indicador de estado devuelto por Hacienda (ej. `aceptado`, `rechazado`). `null` si no se encontró.' detail: type: string example: null description: 'Detalle textual del estado devuelto por Hacienda. `null` si no se encontró.' in_inbox: type: boolean example: false description: '`true` si esa clave ya figura como comprobante recibido por el contribuyente.' message: type: string example: 'Hacienda no tiene registro de esta clave.' errors: type: string example: null nullable: true 422: description: 'Sin certificado activo' content: application/json: schema: type: object example: success: false data: null message: 'No hay un certificado activo para consultar a Hacienda en este ambiente.' errors: certificate: - 'Falta el certificado de sandbox.' properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No hay un certificado activo para consultar a Hacienda en este ambiente.' errors: type: object properties: certificate: type: array example: - 'Falta el certificado de sandbox.' items: type: string 502: description: 'Hacienda no disponible' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo consultar a Hacienda: Connection timed out.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo consultar a Hacienda: Connection timed out.' errors: type: string example: null nullable: true tags: - 'Buzón de Recibidos' parameters: - in: path name: clave description: 'Clave de 50 dígitos a consultar en Hacienda.' example: '50601012600310112345600100001010000000001199999999' required: true schema: type: string '/inbox/{key}': get: summary: 'Consultar un comprobante del buzón por clave' operationId: consultarUnComprobanteDelBuznPorClave description: "Devuelve el detalle de un comprobante recibido, identificado por su\n**clave** (50 dígitos). A diferencia del listado, el detalle incluye\nademás las **líneas** del comprobante (`line_items`): provienen del\ncomprobante interno cuando el emisor es de Almendro, o del snapshot\nparseado del XML cuando es un comprobante externo.\n\nSolo se retornan comprobantes que están en el buzón del contribuyente\nautenticado. Si la clave no está en su buzón, responde **HTTP 404**." parameters: [] responses: 200: description: 'Comprobante encontrado' content: application/json: schema: type: object example: success: true data: voucher_key: '50601012600310112345600100001010000000001199999999' consecutive_number: '00100001010000000001' voucher_type: '01' voucher_type_label: 'Factura Electrónica' issued_at: '2026-06-01T10:00:00-06:00' currency_code: CRC emisor: name: 'PROVEEDOR EJEMPLO S.A.' id_number: '3101123456' alias: 'Hosting junio' notes: 'Pagado con tarjeta corporativa' xsd_valid: null totals: total_impuesto: '1300.00000' total_comprobante: '11300.00000' deadline: deadline_at: '2026-06-11' remaining_business_days: 5 overdue: false status: accepted environment: prod source: almendro source_label: 'Emitido en Almendro' response_status: pending response_status_label: Pendiente is_automatic: false confirmation: confirmed: false message: null message_label: null status: null line_items: - line_number: 1 detail: 'Servicio de hosting mensual' quantity: '1.00000' unit_price: '10000.00000' total_line_amount: '10000.00000' message: 'Detalle del comprobante recibido.' errors: null properties: success: type: boolean example: true data: type: object properties: voucher_key: type: string example: '50601012600310112345600100001010000000001199999999' description: 'Clave de 50 dígitos del comprobante. Identificador único del documento ante Hacienda.' consecutive_number: type: string example: '00100001010000000001' description: 'Consecutivo del comprobante. `null` si es externo y no se pudo resolver el comprobante interno.' voucher_type: type: string example: '01' description: 'Tipo de comprobante: `01`=Factura, `02`=Nota Débito, `03`=Nota Crédito, `04`=Tiquete, `08`=FEC, etc.' voucher_type_label: type: string example: 'Factura Electrónica' description: 'Nombre legible del tipo de comprobante en español. `null` si el tipo no es reconocido.' issued_at: type: string example: '2026-06-01T10:00:00-06:00' description: 'Fecha de emisión del documento (ISO 8601).' currency_code: type: string example: CRC description: 'Código de moneda del comprobante (ej. `CRC`, `USD`).' emisor: type: object properties: name: type: string example: 'PROVEEDOR EJEMPLO S.A.' description: 'Nombre o razón social del emisor.' id_number: type: string example: '3101123456' description: 'Número de identificación del emisor.' description: 'Datos públicos del emisor (dueño del documento).' alias: type: string example: 'Hosting junio' description: 'Alias privado que el receptor le puso al comprobante. `null` si no tiene. No fiscal.' notes: type: string example: 'Pagado con tarjeta corporativa' description: 'Observaciones internas privadas del receptor. `null` si no tiene. No fiscal.' xsd_valid: type: string example: null description: 'Resultado de validar el XML cargado contra el XSD v4.4. `null`=no validado (internos), `true`=válido, `false`=no válido.' totals: type: object properties: total_impuesto: type: string example: '1300.00000' description: 'Total de impuestos del comprobante.' total_comprobante: type: string example: '11300.00000' description: 'Total general del comprobante.' description: 'Totales del comprobante.' deadline: type: object properties: deadline_at: type: string example: '2026-06-11' description: 'Fecha límite para responder (Y-m-d). `null` si no se pudo calcular.' remaining_business_days: type: integer example: 5 description: 'Días hábiles restantes para responder. `null` si no aplica.' overdue: type: boolean example: false description: '`true` si ya venció el plazo de respuesta. `null` si no aplica.' description: 'Plazo de respuesta del receptor (8 días hábiles, Art. 15 del Reglamento).' status: type: string example: accepted description: 'Estado fiscal del comprobante: el del comprobante interno (puede ser `cancelled`) o el `ind-estado` de Hacienda para los externos.' environment: type: string example: prod description: 'Ambiente del comprobante: `sandbox` o `prod`.' source: type: string example: almendro description: 'Origen del comprobante en el buzón: `almendro`, `gui_upload`, `api_upload`, `email_inbound` (recibido por correo en facturas@almendro.cr).' source_label: type: string example: 'Emitido en Almendro' description: 'Nombre legible del origen del comprobante.' response_status: type: string example: pending description: 'Estado de respuesta del receptor: `pending`, `accepted`, `partial`, `rejected`, `tacit`.' response_status_label: type: string example: Pendiente description: 'Nombre legible del estado de respuesta.' is_automatic: type: boolean example: false description: '`true` si la aceptación fue tácita por vencimiento del plazo (sin tratamiento manual).' confirmation: type: object properties: confirmed: type: boolean example: false description: '`true` si el receptor ya resolvió (aceptó o rechazó) el comprobante.' message: type: string example: null description: 'Código del mensaje de respuesta del receptor. `null` si no ha respondido.' message_label: type: string example: null description: 'Nombre legible del mensaje de respuesta. `null` si no ha respondido.' status: type: string example: null description: 'Estado del envío del MensajeReceptor a Hacienda. `null` si no ha respondido.' description: 'Datos del MensajeReceptor de este receptor, si respondió.' line_items: type: array example: - line_number: 1 detail: 'Servicio de hosting mensual' quantity: '1.00000' unit_price: '10000.00000' total_line_amount: '10000.00000' description: 'Líneas de detalle del comprobante. Solo presente en el detalle (no en el listado). `null` si no hay líneas disponibles.' items: type: object properties: line_number: type: integer example: 1 detail: type: string example: 'Servicio de hosting mensual' quantity: type: string example: '1.00000' unit_price: type: string example: '10000.00000' total_line_amount: type: string example: '10000.00000' message: type: string example: 'Detalle del comprobante recibido.' errors: type: string example: null nullable: true 404: description: 'No está en el buzón' content: application/json: schema: type: object example: success: false data: null message: 'El comprobante no está en su buzón.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El comprobante no está en su buzón.' errors: type: string example: null nullable: true tags: - 'Buzón de Recibidos' parameters: - in: path name: key description: 'Clave de 50 dígitos del comprobante recibido.' example: '50601012600310112345600100001010000000001199999999' required: true schema: type: string /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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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. El campo value no es un correo electrónico válido. El campo value no puede tener más de 160 caracteres.' 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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 identifier_color: type: string description: 'Must match the regex /^#[0-9A-Fa-f]{6}$/.' example: '#fEEeDb' 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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.),\nAlmendro 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 - in: query name: terminal_code description: 'Filtra por una caja específica (5 díg., modelo nuevo).' example: '01001' required: false schema: type: string description: 'Filtra por una caja específica (5 díg., modelo nuevo).' example: '01001' 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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 Almendro 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' terminal_code: type: string description: 'Caja (5 díg., modelo nuevo) sobre la que se fija el consecutivo. Debe ser una caja creada por este integrador para el cliente. Si se omite, usa la terminal por defecto del grant.' example: '01001' 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` → Almendro\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}/branches': get: summary: 'Listar sucursales y cajas de un cliente gestionado' operationId: listarSucursalesYCajasDeUnClienteGestionado description: "Devuelve las sucursales (branches) + cajas (terminals) que ESTE integrador\ncreó para el cliente. Las cajas usan el slot del integrador en el\n`terminal_code` (= [slot 2 díg.][caja 3 díg.])." parameters: [] responses: 200: description: 'Sucursales del cliente' content: application/json: schema: type: object example: success: true data: - id: 019ee0-... local_code: '001' name: 'Casa Matriz' is_direct: false terminals: - terminal_code: '01001' slot: '01' box: '001' name: 'Caja 1' message: 'Sucursales del cliente gestionado.' errors: null properties: success: type: boolean example: true data: type: array example: - id: 019ee0-... local_code: '001' name: 'Casa Matriz' is_direct: false terminals: - terminal_code: '01001' slot: '01' box: '001' name: 'Caja 1' items: type: object properties: id: type: string example: 019ee0-... local_code: type: string example: '001' name: type: string example: 'Casa Matriz' is_direct: type: boolean example: false terminals: type: array example: - terminal_code: '01001' slot: '01' box: '001' name: 'Caja 1' items: type: object properties: terminal_code: type: string example: '01001' slot: type: string example: '01' box: type: string example: '001' name: type: string example: 'Caja 1' message: type: string example: 'Sucursales del cliente gestionado.' errors: type: string example: null nullable: true 404: description: 'Cliente no gestionado' 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: 'Crear una sucursal para un cliente gestionado' operationId: crearUnaSucursalParaUnClienteGestionado description: "Crea una sucursal del cliente gestionada por el integrador y su primera caja\n(en el slot del integrador). Consume cupo del POZO del plan del INTEGRADOR\n(`max_branches`), repartido entre todos sus clientes." parameters: [] responses: 201: description: 'Sucursal creada (con su primera caja en el slot)' content: application/json: schema: type: object example: success: true data: id: 019ee0-... local_code: '001' name: 'Casa Matriz' is_direct: false terminals: - terminal_code: '01001' slot: '01' name: 'Caja 1' message: 'Sucursal creada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... local_code: type: string example: '001' name: type: string example: 'Casa Matriz' is_direct: type: boolean example: false terminals: type: array example: - terminal_code: '01001' slot: '01' name: 'Caja 1' items: type: object properties: terminal_code: type: string example: '01001' slot: type: string example: '01' name: type: string example: 'Caja 1' message: type: string example: 'Sucursal creada.' errors: type: string example: null nullable: true 422: description: 'Pozo del integrador agotado' content: application/json: schema: type: object example: success: false data: null message: 'Alcanzó el pozo de 50 sucursal(es) de su plan. Actualice su plan para agregar más.' errors: local_code: - ... properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Alcanzó el pozo de 50 sucursal(es) de su plan. Actualice su plan para agregar más.' errors: type: object properties: local_code: type: array example: - ... items: type: string tags: - 'Clientes del Integrador' requestBody: required: true content: application/json: schema: type: object properties: local_code: type: string description: 'Código de sucursal Hacienda, 3 díg., distinto de `000`.' example: '001' name: type: string description: 'Nombre de la sucursal (2-100 chars).' example: 'Casa Matriz' box_name: type: string description: 'Nombre de la primera caja (2-100 chars). Default: "Caja 1".' example: 'Caja 1' required: - local_code - name parameters: - in: path name: id description: 'UUID del contribuyente-cliente gestionado.' example: 019d867d-c001-7288-8ece-fd64da756c01 required: true schema: type: string '/my-contributors/{id}/branches/{branchId}': put: summary: 'Renombrar una sucursal de un cliente gestionado' operationId: renombrarUnaSucursalDeUnClienteGestionado description: '' parameters: [] responses: 200: description: 'Sucursal renombrada' content: application/json: schema: type: object example: success: true data: id: 019ee0-... name: 'Sucursal Heredia' message: 'Sucursal actualizada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... name: type: string example: 'Sucursal Heredia' message: type: string example: 'Sucursal actualizada.' errors: type: string example: null nullable: true tags: - 'Clientes del Integrador' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Nuevo nombre (2-100 chars).' example: 'Sucursal Heredia' required: - name delete: summary: 'Eliminar una sucursal de un cliente gestionado' operationId: eliminarUnaSucursalDeUnClienteGestionado description: "Borra la sucursal junto con sus cajas. No se puede eliminar la última\nsucursal que el integrador tiene para ese cliente." parameters: [] responses: 200: description: 'Sucursal eliminada' content: application/json: schema: type: object example: success: true data: null message: 'Sucursal eliminada.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Sucursal eliminada.' errors: type: string example: null nullable: true 409: description: 'Es la última sucursal del cliente' content: application/json: schema: type: object example: success: false data: null message: 'Debés mantener al menos una sucursal para este cliente.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Debés mantener al menos una sucursal para este cliente.' 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: branchId description: 'UUID de la sucursal.' example: 019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee required: true schema: type: string '/my-contributors/{id}/branches/{branchId}/terminals': post: summary: 'Agregar una caja a una sucursal de un cliente gestionado' operationId: agregarUnaCajaAUnaSucursalDeUnClienteGestionado description: "Crea una caja en la sucursal del cliente, en el slot del integrador\n(`terminal_code` = [slot][caja]). Respeta el tope de cajas por sucursal del\nplan del integrador." parameters: [] responses: 201: description: 'Caja creada' content: application/json: schema: type: object example: success: true data: id: 019ee0-... terminals: - terminal_code: '01002' name: 'Caja 2' message: 'Caja creada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... terminals: type: array example: - terminal_code: '01002' name: 'Caja 2' items: type: object properties: terminal_code: type: string example: '01002' name: type: string example: 'Caja 2' message: type: string example: 'Caja creada.' errors: type: string example: null nullable: true 422: description: 'Tope de cajas alcanzado' content: application/json: schema: type: object example: success: false data: null message: 'Alcanzó el tope de 20 caja(s) por sucursal de su plan.' errors: name: - ... properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Alcanzó el tope de 20 caja(s) por sucursal de su plan.' errors: type: object properties: name: type: array example: - ... items: type: string tags: - 'Clientes del Integrador' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Nombre de la caja (2-100 chars).' example: 'Caja 2' required: - name parameters: - in: path name: id description: 'UUID del cliente gestionado.' example: 019d867d-c001-7288-8ece-fd64da756c01 required: true schema: type: string - in: path name: branchId description: 'UUID de la sucursal.' example: 019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee required: true schema: type: string '/my-contributors/{id}/branches/{branchId}/terminals/{tid}': put: summary: 'Renombrar una caja de un cliente gestionado' operationId: renombrarUnaCajaDeUnClienteGestionado description: '' parameters: [] responses: 200: description: 'Caja renombrada' content: application/json: schema: type: object example: success: true data: id: 019ee0-... terminals: - terminal_code: '01001' name: 'POS 2' message: 'Caja actualizada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019ee0-... terminals: type: array example: - terminal_code: '01001' name: 'POS 2' items: type: object properties: terminal_code: type: string example: '01001' name: type: string example: 'POS 2' message: type: string example: 'Caja actualizada.' errors: type: string example: null nullable: true tags: - 'Clientes del Integrador' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Nuevo nombre (2-100 chars).' example: 'POS 2' required: - name delete: summary: 'Eliminar una caja de un cliente gestionado' operationId: eliminarUnaCajaDeUnClienteGestionado description: 'No se puede eliminar la última caja activa de la sucursal.' parameters: [] responses: 200: description: 'Caja eliminada' content: application/json: schema: type: object example: success: true data: null message: 'Caja eliminada.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Caja eliminada.' errors: type: string example: null nullable: true 409: description: 'Es la última caja de la sucursal' content: application/json: schema: type: object example: success: false data: null message: 'Debés mantener al menos una caja en la sucursal.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Debés mantener al menos una caja en la sucursal.' 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: branchId description: 'UUID de la sucursal.' example: 019ee0aa-bbbb-cccc-dddd-eeeeeeeeeeee required: true schema: type: string - in: path name: tid description: 'UUID de la caja.' example: 019ee1bb-cccc-dddd-eeee-ffffffffffff required: true schema: type: string '/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 '/my-integrators/{integratorContributorId}/role': patch: summary: 'Cambiar el ROL de un gestor sobre el cliente autenticado.' operationId: cambiarElROLDeUnGestorSobreElClienteAutenticado description: "La información del cliente es del cliente: él decide qué ve cada gestor.\n · accountant (contador) → ve la facturación completa + recibidos.\n · integrator → solo ve las facturas que él mismo emitió.\n\nAl pasar a `integrator` se retira el puente GUI (switch-contributor):\nlos usuarios del gestor dejan de poder operar al cliente por la interfaz." parameters: [] responses: { } tags: - 'Mis Integradores' requestBody: required: true content: application/json: schema: type: object properties: role: type: string description: '' example: architecto required: - role parameters: - in: path name: integratorContributorId description: '' example: e507fb2b94-c9e62d073743f8140a658fd5f 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. El campo value debe tener al menos 5 caracteres. El campo value no puede tener más de 20 caracteres.' 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. El campo value no puede tener más de 500 caracteres.' 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-100). Default: 15.' example: 16 required: false schema: type: integer description: 'Resultados por pagina (1-100). 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-100). Default: 15.' example: 16 required: false schema: type: integer description: 'Resultados por pagina (1-100). 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. El campo value no puede tener más de 500 caracteres.' 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. El campo value no puede tener más de 500 caracteres.' 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-100). Default: 15.' example: 16 required: false schema: type: integer description: 'Resultados por pagina (1-100). 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-100). Default: 15.' example: 16 required: false schema: type: integer description: 'Resultados por pagina (1-100). 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: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de clientes.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su plan no permite gestionar clientes. Actualice a Contadores o Integrador para gestionar la cartera de 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 /subscription: get: summary: 'Consultar el estado de la suscripción' operationId: consultarElEstadoDeLaSuscripcin description: "Devuelve el estado completo de billing del contribuyente autenticado:\nel plan vigente, los datos de la suscripción (ciclo y período pagado),\nel consumo de comprobantes del mes en curso, el catálogo de planes\ndisponibles para cambiar y, si existe, la baja de plan programada.\n\nEl **consumo del mes** (`usage`) se calcula sobre los comprobantes de\nproducción emitidos por el contribuyente y se compara contra el límite\nmensual del plan (`vouchers_limit` puede ser `null` = ilimitado).\n\nEl bloque `scheduled_downgrade` solo está presente (no `null`) cuando\nel contribuyente programó una baja de plan que se aplicará al fin del\nperíodo pagado (ver `POST /subscription/downgrade`)." parameters: [] responses: 200: description: 'Estado de la suscripción' content: application/json: schema: type: object example: success: true data: plan: code: pyme name: Pyme is_free: false monthly_price_usd: '29.00' annual_price_usd: '290.00' monthly_voucher_limit: 1000 tier: 3 max_clients: 300 max_items: 1000 max_users: 5 max_managed_contributors: 0 max_branches: 3 max_terminals_per_branch: 5 has_api: true subscription: status: active billing_cycle: monthly current_period_start: '2026-06-01T00:00:00-06:00' current_period_end: '2026-07-01T00:00:00-06:00' days_remaining: 8 is_past_due: false is_recurring: true usage: vouchers_used: 142 vouchers_limit: 1000 available_plans: - code: free name: Gratis is_free: true monthly_price_usd: '0.00' annual_price_usd: '0.00' monthly_voucher_limit: 10 tier: 1 max_clients: 5 max_items: 10 max_users: 1 max_managed_contributors: 0 max_branches: 1 max_terminals_per_branch: 1 has_api: false scheduled_downgrade: null message: '' errors: null properties: success: type: boolean example: true data: type: object properties: plan: type: object properties: code: type: string example: pyme description: 'Código del plan (ej. `free`, `pyme`). Úselo como `plan_code` en checkout/subscribe/downgrade.' name: type: string example: Pyme description: 'Nombre legible del plan.' is_free: type: boolean example: false description: '`true` si es el plan gratuito (no requiere pago).' monthly_price_usd: type: string example: '29.00' description: 'Precio mensual en USD.' annual_price_usd: type: string example: '290.00' description: 'Precio anual en USD.' monthly_voucher_limit: type: integer example: 1000 description: 'Límite mensual de comprobantes. `null` = ilimitado.' tier: type: integer example: 3 description: 'Rango del plan (mayor = plan superior). Sirve para distinguir upgrade de downgrade.' max_clients: type: integer example: 300 description: 'Máximo de clientes. `null` = ilimitado.' max_items: type: integer example: 1000 description: 'Máximo de productos/servicios. `null` = ilimitado.' max_users: type: integer example: 5 description: 'Máximo de usuarios. `null` = ilimitado.' max_managed_contributors: type: integer example: 0 description: 'Máximo de contribuyentes gestionados (planes de contador). `null` = ilimitado.' max_branches: type: integer example: 3 description: 'Máximo de sucursales. `null` = ilimitado.' max_terminals_per_branch: type: integer example: 5 description: 'Máximo de terminales por sucursal. `null` = ilimitado.' has_api: type: boolean example: true description: '`true` si el plan incluye acceso a la API pública.' description: 'Plan vigente del contribuyente. `null` si no tiene plan asignado.' subscription: type: object properties: status: type: string example: active description: 'Estado de la suscripción (ej. `active`, `past_due`).' billing_cycle: type: string example: monthly description: 'Ciclo de cobro: `monthly` o `annual`.' current_period_start: type: string example: '2026-06-01T00:00:00-06:00' description: 'Inicio del período pagado actual (ISO 8601).' current_period_end: type: string example: '2026-07-01T00:00:00-06:00' description: 'Fin del período pagado actual (ISO 8601).' days_remaining: type: integer example: 8 description: 'Días restantes del período pagado.' is_past_due: type: boolean example: false description: '`true` si la suscripción está vencida o en mora.' is_recurring: type: boolean example: true description: '`true` si hay una suscripción recurrente activa en Tilopay (habilita "Cancelar suscripción").' description: 'Datos de la suscripción activa. `null` si no hay suscripción.' usage: type: object properties: vouchers_used: type: integer example: 142 description: 'Comprobantes de producción emitidos este mes.' vouchers_limit: type: integer example: 1000 description: 'Límite mensual del plan. `null` = ilimitado.' description: 'Consumo de comprobantes del mes en curso.' available_plans: type: array example: - code: free name: Gratis is_free: true monthly_price_usd: '0.00' annual_price_usd: '0.00' monthly_voucher_limit: 10 tier: 1 max_clients: 5 max_items: 10 max_users: 1 max_managed_contributors: 0 max_branches: 1 max_terminals_per_branch: 1 has_api: false description: 'Catálogo de planes activos disponibles para cambiar (misma forma que `data.plan`).' items: type: object properties: code: type: string example: free name: type: string example: Gratis is_free: type: boolean example: true monthly_price_usd: type: string example: '0.00' annual_price_usd: type: string example: '0.00' monthly_voucher_limit: type: integer example: 10 tier: type: integer example: 1 max_clients: type: integer example: 5 max_items: type: integer example: 10 max_users: type: integer example: 1 max_managed_contributors: type: integer example: 0 max_branches: type: integer example: 1 max_terminals_per_branch: type: integer example: 1 has_api: type: boolean example: false scheduled_downgrade: type: string example: null description: 'Baja de plan programada al fin del período. `null` si no hay ninguna.' message: type: string example: '' errors: type: string example: null nullable: true tags: - 'Suscripción y Facturación' /subscription/payments: get: summary: 'Listar el historial de pagos' operationId: listarElHistorialDePagos description: "Devuelve hasta los **100 pagos más recientes** del contribuyente\n(mensualidades y cargos de suscripción), ordenados del más nuevo al\nmás antiguo. Cada pago incluye, si existe, la **factura electrónica**\nemitida por Almendro al cobrar.\n\nEl campo `data` es un **arreglo plano** de pagos (sin paginación). El\nbloque `invoice` es `null` mientras el comprobante de la mensualidad\naún no se ha emitido o vinculado." parameters: [] responses: 200: description: 'Historial de pagos' content: application/json: schema: type: object example: success: true data: - id: 019d867d-0009-7288-8ece-fd64da756009 date: '2026-06-01T08:30:00-06:00' type: subscription status: paid amount_usd: '29.00' currency: USD amount_local: '15080.00' description: 'Suscripción Pyme (monthly)' period_start: '2026-06-01' period_end: '2026-07-01' gateway: tilopay invoice: voucher_key: '50601062600310112345600100001010000000001100000001' status: accepted hacienda_status: aceptado - id: 019d867d-0008-7288-8ece-fd64da756008 date: '2026-05-01T08:30:00-06:00' type: subscription status: paid amount_usd: '29.00' currency: USD amount_local: '15010.00' description: 'Suscripción Pyme (monthly)' period_start: '2026-05-01' period_end: '2026-06-01' gateway: tilopay invoice: null message: '' errors: null properties: success: type: boolean example: true data: type: array example: - id: 019d867d-0009-7288-8ece-fd64da756009 date: '2026-06-01T08:30:00-06:00' type: subscription status: paid amount_usd: '29.00' currency: USD amount_local: '15080.00' description: 'Suscripción Pyme (monthly)' period_start: '2026-06-01' period_end: '2026-07-01' gateway: tilopay invoice: voucher_key: '50601062600310112345600100001010000000001100000001' status: accepted hacienda_status: aceptado - id: 019d867d-0008-7288-8ece-fd64da756008 date: '2026-05-01T08:30:00-06:00' type: subscription status: paid amount_usd: '29.00' currency: USD amount_local: '15010.00' description: 'Suscripción Pyme (monthly)' period_start: '2026-05-01' period_end: '2026-06-01' gateway: tilopay invoice: null description: 'Lista de pagos (máximo 100, más recientes primero).' items: type: object properties: id: type: string example: 019d867d-0009-7288-8ece-fd64da756009 date: type: string example: '2026-06-01T08:30:00-06:00' type: type: string example: subscription status: type: string example: paid amount_usd: type: string example: '29.00' currency: type: string example: USD amount_local: type: string example: '15080.00' description: type: string example: 'Suscripción Pyme (monthly)' period_start: type: string example: '2026-06-01' period_end: type: string example: '2026-07-01' gateway: type: string example: tilopay invoice: type: object properties: voucher_key: type: string example: '50601062600310112345600100001010000000001100000001' status: type: string example: accepted hacienda_status: type: string example: aceptado message: type: string example: '' errors: type: string example: null nullable: true tags: - 'Suscripción y Facturación' /subscription/scheduled-downgrade: delete: summary: 'Cancelar una baja de plan programada' operationId: cancelarUnaBajaDePlanProgramada description: "Revierte una **baja de plan programada** (downgrade o cancelación de\nsuscripción aún no ejecutada) y conserva el plan actual sin cambios ni\ncobros nuevos. Solo tiene efecto mientras la baja no se haya aplicado.\n\nPor seguridad exige la **contraseña** del usuario autenticado. Requiere\nque exista una baja programada; si no, responde **404**." parameters: [] responses: 200: description: 'Baja cancelada' content: application/json: schema: type: object example: success: true data: null message: 'Baja de plan cancelada. Mantenés tu plan actual.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Baja de plan cancelada. Mantenés tu plan actual.' errors: type: string example: null nullable: true 404: description: 'Sin baja programada' content: application/json: schema: type: object example: success: false data: null message: 'No hay una baja de plan programada.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No hay una baja de plan programada.' errors: type: string example: null nullable: true 422: description: 'Contraseña incorrecta' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: password: - 'La contraseña es incorrecta.' 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: password: type: array example: - 'La contraseña es incorrecta.' items: type: string tags: - 'Suscripción y Facturación' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: 'Contraseña del usuario autenticado, para confirmar el cambio.' example: mi-contrasena-secreta required: - password /subscription/checkout: post: summary: 'Iniciar el pago único de un plan (checkout)' operationId: iniciarElPagonicoDeUnPlancheckout description: "Inicia el **checkout de Tilopay** para contratar un plan de pago por un\núnico período (mensual o anual). Crea una sesión de pago en Tilopay,\nregistra un pago **PENDIENTE** y devuelve la `redirect_url` a la que su\nbackend debe **redirigir** al cliente para completar el pago.\n\nLa activación del plan **no** ocurre en esta respuesta: sucede en el\nretorno firmado de Tilopay una vez que el pago se confirma.\n\nPagar **no** exige contraseña (el cliente entrega dinero, no hay riesgo).\nEl plan **gratis** no puede contratarse por esta vía." parameters: [] responses: 200: description: 'Sesión de pago creada' content: application/json: schema: type: object example: success: true data: redirect_url: 'https://app.tilopay.com/checkout/abc123' order: TPAY-019d867d-0001 message: 'Redirigiendo a la pasarela de pago.' errors: null properties: success: type: boolean example: true data: type: object properties: redirect_url: type: string example: 'https://app.tilopay.com/checkout/abc123' order: type: string example: TPAY-019d867d-0001 message: type: string example: 'Redirigiendo a la pasarela de pago.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Plan no disponible' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: plan_code: - 'El plan solicitado no existe o no está disponible.' 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_code: type: array example: - 'El plan solicitado no existe o no está disponible.' items: type: string - description: 'Plan gratuito' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: plan_code: - 'El plan gratuito no requiere pago.' 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_code: type: array example: - 'El plan gratuito no requiere pago.' items: type: string - description: 'Sin precio para el ciclo' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: billing_cycle: - 'El plan no tiene precio para el ciclo seleccionado.' 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: billing_cycle: type: array example: - 'El plan no tiene precio para el ciclo seleccionado.' items: type: string 502: description: 'Fallo de la pasarela' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo iniciar el pago con Tilopay. Intente de nuevo en unos minutos.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo iniciar el pago con Tilopay. Intente de nuevo en unos minutos.' errors: type: string example: null nullable: true tags: - 'Suscripción y Facturación' requestBody: required: true content: application/json: schema: type: object properties: plan_code: type: string description: 'Código del plan a contratar. Debe existir y estar activo. No puede ser el plan gratuito.' example: pyme billing_cycle: type: string description: 'Ciclo de cobro. Valores: `monthly` o `annual`.' example: monthly required: - plan_code - billing_cycle /subscription/subscribe: post: summary: 'Iniciar la suscripción recurrente de un plan' operationId: iniciarLaSuscripcinRecurrenteDeUnPlan description: "Inicia una **suscripción recurrente** (cobro automático periódico) de un\nplan de pago en Tilopay. Genera la URL de registro de la suscripción con\nel correo del contribuyente pre-cargado y devuelve la `redirect_url` a la\nque su backend debe **redirigir** al cliente.\n\nEl plan se activa cuando llega el **webhook** de suscripción/cobro de\nTilopay, no en esta respuesta. Si el contribuyente ya estaba registrado,\nse devuelve la URL de renovación.\n\nRequiere que exista un **plan de pasarela** configurado para la\ncombinación plan + ciclo, y que la cuenta tenga al menos un correo\nregistrado. El plan **gratis** no admite suscripción." parameters: [] responses: 200: description: 'Registro de suscripción creado' content: application/json: schema: type: object example: success: true data: redirect_url: 'https://app.tilopay.com/subscribe/register/xyz789' message: 'Redirigiendo al registro de la suscripción.' errors: null properties: success: type: boolean example: true data: type: object properties: redirect_url: type: string example: 'https://app.tilopay.com/subscribe/register/xyz789' message: type: string example: 'Redirigiendo al registro de la suscripción.' errors: type: string example: null nullable: true 409: description: 'Suscripción recurrente no disponible para el plan' content: application/json: schema: type: object example: success: false data: null message: 'La suscripción recurrente para este plan aún no está disponible. Contacte a soporte.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'La suscripción recurrente para este plan aún no está disponible. Contacte a soporte.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Plan no disponible o gratuito' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: plan_code: - 'El plan solicitado no existe, no está disponible o es gratuito.' 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_code: type: array example: - 'El plan solicitado no existe, no está disponible o es gratuito.' items: type: string - description: 'Sin correo registrado' type: object example: success: false data: null message: 'Su cuenta no tiene un correo registrado para la suscripción.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Su cuenta no tiene un correo registrado para la suscripción.' errors: type: string example: null nullable: true 502: description: 'Fallo de la pasarela' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo iniciar la suscripción con Tilopay. Intente más tarde.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo iniciar la suscripción con Tilopay. Intente más tarde.' errors: type: string example: null nullable: true tags: - 'Suscripción y Facturación' requestBody: required: true content: application/json: schema: type: object properties: plan_code: type: string description: 'Código del plan a suscribir. Debe existir, estar activo y no ser gratuito.' example: pyme billing_cycle: type: string description: 'Ciclo de cobro. Valores: `monthly` o `annual`.' example: monthly required: - plan_code - billing_cycle /subscription/cancel-recurring: post: summary: 'Cancelar la suscripción recurrente' operationId: cancelarLaSuscripcinRecurrente description: "Cancela el **cobro automático** de la suscripción recurrente en Tilopay\ny programa la baja al plan **gratis** al fin del período pagado. El\ncontribuyente conserva su plan actual hasta entonces y **no hay\nreembolso**.\n\nPor tratarse de un cambio de plan (no un pago), exige la **contraseña**\ndel usuario autenticado y un **motivo** de al menos 100 caracteres\n(retención y analítica). Requiere una suscripción recurrente activa en\nTilopay." parameters: [] responses: 200: description: 'Suscripción cancelada' content: application/json: schema: type: object example: success: true data: null message: 'Suscripción cancelada. Mantenés el plan hasta el fin del período pagado.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Suscripción cancelada. Mantenés el plan hasta el fin del período pagado.' errors: type: string example: null nullable: true 404: description: 'Sin suscripción recurrente activa' content: application/json: schema: type: object example: success: false data: null message: 'No hay una suscripción recurrente activa para cancelar.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No hay una suscripción recurrente activa para cancelar.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Contraseña incorrecta' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: password: - 'La contraseña es incorrecta.' 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: password: type: array example: - 'La contraseña es incorrecta.' items: type: string - description: 'Motivo demasiado corto' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: reason: - 'El motivo debe tener al menos 100 caracteres.' 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: reason: type: array example: - 'El motivo debe tener al menos 100 caracteres.' items: type: string 502: description: 'Fallo de la pasarela' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo cancelar la suscripción con Tilopay. Intente más tarde.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo cancelar la suscripción con Tilopay. Intente más tarde.' errors: type: string example: null nullable: true tags: - 'Suscripción y Facturación' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: 'Contraseña del usuario autenticado, para confirmar el cambio.' example: mi-contrasena-secreta reason: type: string description: 'Motivo de la cancelación (mínimo 100 caracteres).' example: 'Cancelo la suscripcion porque por ahora estoy facturando muy poco y prefiero volver al plan gratuito mientras crece mi negocio.' required: - password - reason /subscription/downgrade: post: summary: 'Programar una baja de plan (downgrade)' operationId: programarUnaBajaDePlandowngrade description: "Programa una **baja a un plan inferior** que se aplicará al fin del\nperíodo pagado. Si existe una suscripción recurrente en Tilopay, se\ncancela el cobro automático para que no haya cargos futuros del plan\nsuperior. El contribuyente conserva su plan actual hasta que venza el\nperíodo pagado y entonces pasa al plan destino. **No hay reembolso**.\n\nPor tratarse de un cambio de plan (no un pago), exige la **contraseña**\ndel usuario y un **motivo** de al menos 100 caracteres.\n\nEl plan destino debe ser de **menor rango** (`tier`/`display_order`) que\nel actual y no puede ser el plan que ya tiene activo. Para **subir** de\nplan use la suscripción (`POST /subscription/subscribe`)." parameters: [] responses: 200: description: 'Baja programada' content: application/json: schema: type: object example: success: true data: null message: 'Baja de plan programada. Mantenés tu plan actual hasta el fin del período pagado.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Baja de plan programada. Mantenés tu plan actual hasta el fin del período pagado.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Contraseña incorrecta' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: password: - 'La contraseña es incorrecta.' 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: password: type: array example: - 'La contraseña es incorrecta.' items: type: string - description: 'Motivo demasiado corto' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: reason: - 'El motivo debe tener al menos 100 caracteres.' 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: reason: type: array example: - 'El motivo debe tener al menos 100 caracteres.' items: type: string - description: 'Plan no disponible' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: plan_code: - 'El plan solicitado no existe o no está disponible.' 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_code: type: array example: - 'El plan solicitado no existe o no está disponible.' items: type: string - description: 'Ya tiene este plan' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: plan_code: - 'Ya tenés este plan activo.' 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_code: type: array example: - 'Ya tenés este plan activo.' items: type: string - description: 'No es una baja real' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: plan_code: - 'Para subir de plan, usá la suscripción (cobro automático).' 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_code: type: array example: - 'Para subir de plan, usá la suscripción (cobro automático).' items: type: string 502: description: 'Fallo de la pasarela' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo procesar la baja con Tilopay. Intente más tarde.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo procesar la baja con Tilopay. Intente más tarde.' errors: type: string example: null nullable: true tags: - 'Suscripción y Facturación' requestBody: required: true content: application/json: schema: type: object properties: plan_code: type: string description: 'Código del plan destino. Debe existir, estar activo y ser de menor rango que el actual.' example: emprendedor password: type: string description: 'Contraseña del usuario autenticado, para confirmar el cambio.' example: mi-contrasena-secreta reason: type: string description: 'Motivo de la baja (mínimo 100 caracteres).' example: 'Bajo de plan porque mi volumen de facturacion disminuyo bastante este trimestre y el plan inferior cubre de sobra mis necesidades actuales.' required: - plan_code - password - reason /support/conversations: get: summary: 'Listar conversaciones de soporte' operationId: listarConversacionesDeSoporte description: "Devuelve todas las conversaciones de soporte abiertas por el\ncontribuyente autenticado, ordenadas con las más recientes primero\n(por última actividad). Cada conversación incluye el conteo de\nmensajes, pero **no** el hilo completo | para ver los mensajes use\n`GET /support/conversations/{id}`.\n\nLas conversaciones son tipo chat in-app entre el cliente y el equipo\nde soporte, dentro de la plataforma (sin correos). El alcance está\nlimitado al contribuyente del token (multi-tenant)." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: 'Listado de conversaciones' type: object example: success: true data: - id: 019d867d-0001-7288-8ece-fd64da756001 subject: 'No me llega el correo al cliente' status: open priority: normal unread_for_client: false messages_count: 3 last_message_at: '2026-06-23T10:00:00-06:00' created_at: '2026-06-22T09:00:00-06:00' message: 'Conversaciones de soporte.' errors: null properties: success: type: boolean example: true data: type: array example: - id: 019d867d-0001-7288-8ece-fd64da756001 subject: 'No me llega el correo al cliente' status: open priority: normal unread_for_client: false messages_count: 3 last_message_at: '2026-06-23T10:00:00-06:00' created_at: '2026-06-22T09:00:00-06:00' items: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 subject: type: string example: 'No me llega el correo al cliente' status: type: string example: open priority: type: string example: normal unread_for_client: type: boolean example: false messages_count: type: integer example: 3 last_message_at: type: string example: '2026-06-23T10:00:00-06:00' created_at: type: string example: '2026-06-22T09:00:00-06:00' message: type: string example: 'Conversaciones de soporte.' errors: type: string example: null nullable: true - description: 'Sin conversaciones' type: object example: success: true data: [] message: 'Conversaciones de soporte.' errors: null properties: success: type: boolean example: true data: type: array example: [] message: type: string example: 'Conversaciones de soporte.' errors: type: string example: null nullable: true tags: - Soporte post: summary: 'Abrir una conversación de soporte' operationId: abrirUnaConversacinDeSoporte description: "Crea una nueva conversación de soporte con su **primer mensaje**. La\nconversación nace en estado `open` y con la prioridad correspondiente\nal plan del contribuyente (`priority` si tiene soporte prioritario,\n`normal` en caso contrario).\n\nAl crearla se envía un correo de acuse al cliente, pero el hilo sigue\nsiendo in-app.\n\n**Límite:** el cliente solo puede enviar **un mensaje de soporte por\nhora**. Si el último mensaje del cliente (en cualquiera de sus\nconversaciones) tiene menos de una hora y el equipo aún no lo\nrespondió, la API responde **HTTP 429** con el tiempo restante." parameters: [] responses: 201: description: 'Conversación creada' content: application/json: schema: type: object example: success: true data: id: 019d867d-0001-7288-8ece-fd64da756001 subject: 'No me llega el correo al cliente' status: open priority: normal unread_for_client: false messages_count: 1 last_message_at: '2026-06-23T10:00:00-06:00' created_at: '2026-06-23T10:00:00-06:00' client_can_write: false cooldown_seconds: 3600 messages: - id: 019d867d-0002-7288-8ece-fd64da756002 sender_type: client body: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' created_at: '2026-06-23T10:00:00-06:00' message: 'Conversación creada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 subject: type: string example: 'No me llega el correo al cliente' status: type: string example: open priority: type: string example: normal unread_for_client: type: boolean example: false messages_count: type: integer example: 1 last_message_at: type: string example: '2026-06-23T10:00:00-06:00' created_at: type: string example: '2026-06-23T10:00:00-06:00' client_can_write: type: boolean example: false cooldown_seconds: type: integer example: 3600 messages: type: array example: - id: 019d867d-0002-7288-8ece-fd64da756002 sender_type: client body: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' created_at: '2026-06-23T10:00:00-06:00' items: type: object properties: id: type: string example: 019d867d-0002-7288-8ece-fd64da756002 sender_type: type: string example: client body: type: string example: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' created_at: type: string example: '2026-06-23T10:00:00-06:00' message: type: string example: 'Conversación creada.' errors: type: string example: null nullable: true 422: description: 'Mensaje requerido' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: message: - 'El campo message 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: message: type: array example: - 'El campo message es obligatorio.' items: type: string 429: description: 'Límite de un mensaje por hora' content: application/json: schema: type: object example: success: false data: cooldown_seconds: 2400 message: 'Solo puedes enviar un mensaje de soporte cada hora. Podrás escribir de nuevo en 40 min, o cuando el equipo responda.' errors: null properties: success: type: boolean example: false data: type: object properties: cooldown_seconds: type: integer example: 2400 message: type: string example: 'Solo puedes enviar un mensaje de soporte cada hora. Podrás escribir de nuevo en 40 min, o cuando el equipo responda.' errors: type: string example: null nullable: true tags: - Soporte requestBody: required: true content: application/json: schema: type: object properties: subject: type: string description: 'Asunto de la conversación (máximo 160 caracteres). Opcional.' example: 'No me llega el correo al cliente' nullable: true message: type: string description: 'Primer mensaje de la conversación (2 a 5000 caracteres).' example: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' required: - message '/support/conversations/{id}': get: summary: 'Consultar una conversación de soporte' operationId: consultarUnaConversacinDeSoporte description: "Devuelve el detalle de una conversación con su **hilo completo de\nmensajes** ordenados cronológicamente. Solo se retornan conversaciones\nque pertenecen al contribuyente autenticado.\n\nAl consultar el detalle, la conversación se marca como **leída por el\ncliente** (`unread_for_client` pasa a `false`).\n\nLa respuesta incluye además `client_can_write` y `cooldown_seconds`,\nque reflejan el límite de un mensaje por hora para que el frontend\nhabilite o deshabilite el cuadro de respuesta." parameters: [] responses: 200: description: 'Detalle con hilo de mensajes' content: application/json: schema: type: object example: success: true data: id: 019d867d-0001-7288-8ece-fd64da756001 subject: 'No me llega el correo al cliente' status: open priority: normal unread_for_client: false messages_count: 2 last_message_at: '2026-06-23T11:00:00-06:00' created_at: '2026-06-23T10:00:00-06:00' client_can_write: true cooldown_seconds: 0 messages: - id: 019d867d-0002-7288-8ece-fd64da756002 sender_type: client body: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' created_at: '2026-06-23T10:00:00-06:00' - id: 019d867d-0003-7288-8ece-fd64da756003 sender_type: admin body: 'Hola, ya revisamos y el correo salió correctamente. Pídale al cliente revisar spam.' created_at: '2026-06-23T11:00:00-06:00' message: 'Detalle de la conversación.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 subject: type: string example: 'No me llega el correo al cliente' status: type: string example: open priority: type: string example: normal unread_for_client: type: boolean example: false messages_count: type: integer example: 2 last_message_at: type: string example: '2026-06-23T11:00:00-06:00' created_at: type: string example: '2026-06-23T10:00:00-06:00' client_can_write: type: boolean example: true cooldown_seconds: type: integer example: 0 messages: type: array example: - id: 019d867d-0002-7288-8ece-fd64da756002 sender_type: client body: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' created_at: '2026-06-23T10:00:00-06:00' - id: 019d867d-0003-7288-8ece-fd64da756003 sender_type: admin body: 'Hola, ya revisamos y el correo salió correctamente. Pídale al cliente revisar spam.' created_at: '2026-06-23T11:00:00-06:00' items: type: object properties: id: type: string example: 019d867d-0002-7288-8ece-fd64da756002 sender_type: type: string example: client body: type: string example: 'Hola, emití una factura pero el cliente dice que no le llegó el correo.' created_at: type: string example: '2026-06-23T10:00:00-06:00' message: type: string example: 'Detalle de la conversación.' errors: type: string example: null nullable: true 404: description: 'No encontrada' content: application/json: schema: type: object example: success: false data: null message: 'Conversación no encontrada.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Conversación no encontrada.' errors: type: string example: null nullable: true tags: - Soporte parameters: - in: path name: id description: 'UUID de la conversación.' example: 019d867d-0001-7288-8ece-fd64da756001 required: true schema: type: string '/support/conversations/{id}/reply': post: summary: 'Responder en una conversación de soporte' operationId: responderEnUnaConversacinDeSoporte description: "Añade un nuevo mensaje del cliente al hilo de una conversación\nexistente. Si la conversación estaba resuelta o cerrada, se **reabre**\nautomáticamente (`status` vuelve a `open`) y se notifica al equipo.\n\nDevuelve la conversación con el hilo completo de mensajes actualizado.\n\n**Límite:** aplica el mismo límite de **un mensaje por hora** que al\nabrir una conversación. Si el último mensaje del cliente tiene menos\nde una hora y el equipo aún no respondió, la API responde **HTTP 429**\ncon el tiempo restante." parameters: [] responses: 200: description: 'Mensaje enviado' content: application/json: schema: type: object example: success: true data: id: 019d867d-0001-7288-8ece-fd64da756001 subject: 'No me llega el correo al cliente' status: open priority: normal unread_for_client: false messages_count: 3 last_message_at: '2026-06-23T11:30:00-06:00' created_at: '2026-06-23T10:00:00-06:00' client_can_write: false cooldown_seconds: 3600 messages: - id: 019d867d-0004-7288-8ece-fd64da756004 sender_type: client body: 'Listo, el cliente lo encontró en spam. Muchas gracias.' created_at: '2026-06-23T11:30:00-06:00' message: 'Mensaje enviado.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 subject: type: string example: 'No me llega el correo al cliente' status: type: string example: open priority: type: string example: normal unread_for_client: type: boolean example: false messages_count: type: integer example: 3 last_message_at: type: string example: '2026-06-23T11:30:00-06:00' created_at: type: string example: '2026-06-23T10:00:00-06:00' client_can_write: type: boolean example: false cooldown_seconds: type: integer example: 3600 messages: type: array example: - id: 019d867d-0004-7288-8ece-fd64da756004 sender_type: client body: 'Listo, el cliente lo encontró en spam. Muchas gracias.' created_at: '2026-06-23T11:30:00-06:00' items: type: object properties: id: type: string example: 019d867d-0004-7288-8ece-fd64da756004 sender_type: type: string example: client body: type: string example: 'Listo, el cliente lo encontró en spam. Muchas gracias.' created_at: type: string example: '2026-06-23T11:30:00-06:00' message: type: string example: 'Mensaje enviado.' errors: type: string example: null nullable: true 404: description: 'No encontrada' content: application/json: schema: type: object example: success: false data: null message: 'Conversación no encontrada.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Conversación no encontrada.' errors: type: string example: null nullable: true 422: description: 'Mensaje requerido' content: application/json: schema: type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: message: - 'El campo message 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: message: type: array example: - 'El campo message es obligatorio.' items: type: string 429: description: 'Límite de un mensaje por hora' content: application/json: schema: type: object example: success: false data: cooldown_seconds: 1800 message: 'Solo puedes enviar un mensaje de soporte cada hora. Podrás escribir de nuevo en 30 min, o cuando el equipo responda.' errors: null properties: success: type: boolean example: false data: type: object properties: cooldown_seconds: type: integer example: 1800 message: type: string example: 'Solo puedes enviar un mensaje de soporte cada hora. Podrás escribir de nuevo en 30 min, o cuando el equipo responda.' errors: type: string example: null nullable: true tags: - Soporte requestBody: required: true content: application/json: schema: type: object properties: message: type: string description: 'Mensaje a agregar al hilo (2 a 5000 caracteres).' example: 'Listo, el cliente lo encontró en spam. Muchas gracias.' required: - message parameters: - in: path name: id description: 'UUID de la conversación.' example: 019d867d-0001-7288-8ece-fd64da756001 required: true schema: type: string /support-tier: get: summary: 'Consultar el estado del soporte prioritario' operationId: consultarElEstadoDelSoportePrioritario description: "Devuelve el tier de soporte actual del contribuyente, si tiene una\nsuscripción recurrente activa, los precios vigentes de cada tier y la\ndisponibilidad de cada uno para contratación en línea.\n\nLos precios provienen de la configuración del sistema y están\nexpresados en dólares estadounidenses (USD). La disponibilidad\ndepende de que exista un plan de pasarela (Tilopay) configurado para\nel addon correspondiente con ciclo mensual." parameters: [] responses: 200: description: 'Estado del soporte' content: application/json: schema: type: object example: success: true data: current_tier: standard has_subscription: false premium_usd: 500 premium_247_usd: 2400 premium_available: true premium_247_available: true message: '' errors: null properties: success: type: boolean example: true data: type: object properties: current_tier: type: string example: standard description: 'Tier de soporte actual del contribuyente: `standard`, `premium` o `premium_247`.' has_subscription: type: boolean example: false description: '`true` si el contribuyente tiene una suscripción de soporte recurrente activa.' premium_usd: type: integer example: 500 description: 'Precio mensual del tier premium en dólares (USD).' premium_247_usd: type: integer example: 2400 description: 'Precio mensual del tier premium 24/7 en dólares (USD).' premium_available: type: boolean example: true description: '`true` si el tier premium se puede contratar en línea actualmente.' premium_247_available: type: boolean example: true description: '`true` si el tier premium 24/7 se puede contratar en línea actualmente.' message: type: string example: '' errors: type: string example: null nullable: true tags: - 'Soporte Prioritario' /support-tier/subscribe: post: summary: 'Contratar soporte prioritario' operationId: contratarSoportePrioritario description: "Inicia la suscripción recurrente del tier elegido y devuelve una\n**URL de redirección** a la pasarela Tilopay, donde el contribuyente\nregistra su medio de pago para el cobro mensual.\n\nEl tier solo se activa de forma efectiva cuando la pasarela confirma\nel registro mediante sus webhooks recurrentes | esta llamada solo\ninicia el proceso.\n\n**Requisitos:**\n\n- El tier solicitado (`premium` o `premium_247`) debe estar\n **disponible** para contratación en línea (vea `premium_available`\n y `premium_247_available` en `GET /support-tier`). Si no hay plan de\n pasarela configurado, responde **HTTP 409**.\n- El contribuyente debe tener al menos un **correo** en su perfil. Si\n no lo tiene, responde **HTTP 422**." parameters: [] responses: 200: description: 'Redirección a la pasarela' content: application/json: schema: type: object example: success: true data: redirect_url: 'https://app.tilopay.com/recurrent/register/abc123' message: 'Redirigiendo a la pasarela de pago.' errors: null properties: success: type: boolean example: true data: type: object properties: redirect_url: type: string example: 'https://app.tilopay.com/recurrent/register/abc123' message: type: string example: 'Redirigiendo a la pasarela de pago.' errors: type: string example: null nullable: true 409: description: 'Tier no disponible para contratar en línea' content: application/json: schema: type: object example: success: false data: null message: 'El soporte prioritario aún no está disponible para contratar en línea. Escríbenos y lo activamos.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'El soporte prioritario aún no está disponible para contratar en línea. Escríbenos y lo activamos.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Sin correo en el perfil' type: object example: success: false data: null message: 'Necesitas un correo en tu perfil para contratar el soporte.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'Necesitas un correo en tu perfil para contratar el soporte.' errors: type: string example: null nullable: true - description: 'Tier inválido' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: tier: - 'El tier 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: tier: type: array example: - 'El tier seleccionado no es válido.' items: type: string 502: description: 'Error al iniciar la suscripción en la pasarela' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo iniciar la suscripción. Intenta de nuevo en unos minutos.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo iniciar la suscripción. Intenta de nuevo en unos minutos.' errors: type: string example: null nullable: true tags: - 'Soporte Prioritario' requestBody: required: true content: application/json: schema: type: object properties: tier: type: string description: 'Tier a contratar. Valores permitidos: `premium`, `premium_247`.' example: premium required: - tier /support-tier/cancel: post: summary: 'Cancelar soporte prioritario' operationId: cancelarSoportePrioritario description: "Cancela la suscripción recurrente de soporte: elimina el suscriptor en\nla pasarela Tilopay y devuelve al contribuyente al tier `standard`,\nlimpiando su identificador de suscripción.\n\nSi el contribuyente **no tiene** una suscripción de soporte activa,\nresponde **HTTP 404**." parameters: [] responses: 200: description: 'Soporte cancelado' content: application/json: schema: type: object example: success: true data: null message: 'Soporte prioritario cancelado.' errors: null properties: success: type: boolean example: true data: type: string example: null nullable: true message: type: string example: 'Soporte prioritario cancelado.' errors: type: string example: null nullable: true 404: description: 'Sin suscripción activa' content: application/json: schema: type: object example: success: false data: null message: 'No tienes una suscripción de soporte activa para cancelar.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No tienes una suscripción de soporte activa para cancelar.' errors: type: string example: null nullable: true 502: description: 'Error al cancelar en la pasarela' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo cancelar la suscripción. Intenta de nuevo.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo cancelar la suscripción. Intenta de nuevo.' errors: type: string example: null nullable: true tags: - 'Soporte Prioritario' /onboarding: get: summary: 'Estado del onboarding del contribuyente' operationId: estadoDelOnboardingDelContribuyente description: "Devuelve un resumen del onboarding del contribuyente autenticado: sus\ncréditos disponibles (gratuito anual + comprados), si puede autoagendar\nen este momento, el precio vigente de la hora y la próxima cita futura\nagendada (si existe).\n\nEl campo `can_schedule` es `true` solo cuando hay al menos un crédito\ndisponible **y** hay agentes activos en la agenda. Úselo para habilitar\no deshabilitar el botón de agendar en el portal.\n\nEl bloque `appointment` es `null` cuando no hay una cita de onboarding o\npagada futura con estado agendado; de lo contrario trae los datos de la\npróxima sesión." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: 'Con crédito y cita agendada' type: object example: success: true data: credits: free: 1 purchased: 0 total: 1 can_schedule: true price_usd: 100 appointment: id: 019d867d-0001-7288-8ece-fd64da756001 starts_at: '2026-07-01T09:00:00-06:00' ends_at: '2026-07-01T09:50:00-06:00' agent_name: 'Ana Soporte' meeting_url: 'https://meet.google.com/abc-defg-hij' status: scheduled message: '' errors: null properties: success: type: boolean example: true data: type: object properties: credits: type: object properties: free: type: integer example: 1 description: 'Créditos gratuitos anuales disponibles.' purchased: type: integer example: 0 description: 'Créditos comprados disponibles.' total: type: integer example: 1 description: 'Suma de créditos disponibles (`free` + `purchased`).' description: 'Resumen de créditos de onboarding disponibles.' can_schedule: type: boolean example: true description: '`true` si tiene al menos un crédito y hay agentes activos en la agenda.' price_usd: type: integer example: 100 description: 'Precio vigente de la hora de onboarding en USD.' appointment: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 description: 'UUID de la cita.' starts_at: type: string example: '2026-07-01T09:00:00-06:00' description: 'Inicio de la cita (ISO 8601).' ends_at: type: string example: '2026-07-01T09:50:00-06:00' description: 'Fin de la cita (ISO 8601).' agent_name: type: string example: 'Ana Soporte' description: 'Nombre del agente asignado. Puede ser `null`.' meeting_url: type: string example: 'https://meet.google.com/abc-defg-hij' description: 'Enlace de la reunión (Google Meet). Puede ser `null`.' status: type: string example: scheduled description: 'Estado de la cita (por ejemplo `scheduled`).' description: 'Próxima cita futura agendada, o `null` si no hay ninguna.' message: type: string example: '' errors: type: string example: null nullable: true - description: 'Sin créditos ni cita' type: object example: success: true data: credits: free: 0 purchased: 0 total: 0 can_schedule: false price_usd: 100 appointment: null message: '' errors: null properties: success: type: boolean example: true data: type: object properties: credits: type: object properties: free: type: integer example: 0 description: 'Créditos gratuitos anuales disponibles.' purchased: type: integer example: 0 description: 'Créditos comprados disponibles.' total: type: integer example: 0 description: 'Suma de créditos disponibles (`free` + `purchased`).' description: 'Resumen de créditos de onboarding disponibles.' can_schedule: type: boolean example: false description: '`true` si tiene al menos un crédito y hay agentes activos en la agenda.' price_usd: type: integer example: 100 description: 'Precio vigente de la hora de onboarding en USD.' appointment: type: string example: null description: 'Próxima cita futura agendada, o `null` si no hay ninguna.' message: type: string example: '' errors: type: string example: null nullable: true tags: - Onboarding /onboarding/slots: get: summary: 'Huecos disponibles para autoagendar' operationId: huecosDisponiblesParaAutoagendar description: "Devuelve los horarios libres en los que el contribuyente puede agendar\nsu sesión de onboarding, calculados a partir de las agendas de los\nagentes activos. La ventana de búsqueda va desde ahora hasta **120\ndías** hacia adelante y respeta la antelación mínima configurada.\n\nPor privacidad, la respuesta **no incluye el agente** de cada hueco;\nsolo el inicio y fin del horario. El agente se asigna internamente al\nconfirmar la cita en `POST /onboarding/appointments`.\n\nUse el valor `starts_at` de un hueco tal cual al agendar." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: 'Huecos disponibles' type: object example: success: true data: slots: - starts_at: '2026-07-01T09:00:00-06:00' ends_at: '2026-07-01T09:50:00-06:00' - starts_at: '2026-07-01T10:00:00-06:00' ends_at: '2026-07-01T10:50:00-06:00' message: '' errors: null properties: success: type: boolean example: true data: type: object properties: slots: type: array example: - starts_at: '2026-07-01T09:00:00-06:00' ends_at: '2026-07-01T09:50:00-06:00' - starts_at: '2026-07-01T10:00:00-06:00' ends_at: '2026-07-01T10:50:00-06:00' description: 'Lista de horarios libres disponibles para agendar.' items: type: object properties: starts_at: type: string example: '2026-07-01T09:00:00-06:00' ends_at: type: string example: '2026-07-01T09:50:00-06:00' message: type: string example: '' errors: type: string example: null nullable: true - description: 'Sin huecos disponibles' type: object example: success: true data: slots: [] message: '' errors: null properties: success: type: boolean example: true data: type: object properties: slots: type: array example: [] description: 'Lista de horarios libres disponibles para agendar.' message: type: string example: '' errors: type: string example: null nullable: true tags: - Onboarding /onboarding/appointments: post: summary: 'Agendar la sesión de onboarding' operationId: agendarLaSesinDeOnboarding description: "Reserva un hueco para la sesión de onboarding y **consume 1 crédito**\ndisponible (se usa primero el crédito gratuito anual; si no hay, se usa\nuno comprado). El `starts_at` enviado debe coincidir exactamente con un\nhueco devuelto por `GET /onboarding/slots`.\n\nAntes de crear la cita, el sistema **reconfirma la disponibilidad** del\nhorario contra la agenda actual. Si el hueco ya fue tomado o el agente\ntiene un conflicto, responde **HTTP 422** y debe elegir otro horario.\nSi no tiene créditos disponibles, también responde **422**.\n\nAl agendar correctamente se crea el evento (y enlace de Google Meet, si\nla integración está activa) y se envía un correo de confirmación al\ncontribuyente cuando tiene un email registrado." parameters: [] responses: 200: description: 'Cita agendada' content: application/json: schema: type: object example: success: true data: id: 019d867d-0001-7288-8ece-fd64da756001 starts_at: '2026-07-01T09:00:00-06:00' ends_at: '2026-07-01T09:50:00-06:00' agent_name: 'Ana Soporte' meeting_url: 'https://meet.google.com/abc-defg-hij' status: scheduled message: 'Cita de onboarding agendada.' errors: null properties: success: type: boolean example: true data: type: object properties: id: type: string example: 019d867d-0001-7288-8ece-fd64da756001 description: 'UUID de la cita creada.' starts_at: type: string example: '2026-07-01T09:00:00-06:00' description: 'Inicio de la cita (ISO 8601).' ends_at: type: string example: '2026-07-01T09:50:00-06:00' description: 'Fin de la cita (ISO 8601).' agent_name: type: string example: 'Ana Soporte' description: 'Nombre del agente asignado. Puede ser `null`.' meeting_url: type: string example: 'https://meet.google.com/abc-defg-hij' description: 'Enlace de la reunión (Google Meet). Puede ser `null` si la integración no está activa.' status: type: string example: scheduled description: 'Estado de la cita (por ejemplo `scheduled`).' message: type: string example: 'Cita de onboarding agendada.' errors: type: string example: null nullable: true 422: description: '' content: application/json: schema: oneOf: - description: 'Sin créditos disponibles' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: credit: - 'No tienes créditos de onboarding disponibles.' 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: credit: type: array example: - 'No tienes créditos de onboarding disponibles.' items: type: string - description: 'Horario ya no disponible' type: object example: success: false data: null message: 'Los datos proporcionados no son válidos.' errors: starts_at: - 'Ese horario ya no está disponible. Elige otro.' 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: starts_at: type: array example: - 'Ese horario ya no está disponible. Elige otro.' items: type: string tags: - Onboarding requestBody: required: true content: application/json: schema: type: object properties: starts_at: type: string description: 'Inicio del hueco a reservar (fecha/hora válida, ISO 8601). Debe coincidir con un `starts_at` devuelto por `GET /onboarding/slots`.' example: '2026-07-01T09:00:00-06:00' required: - starts_at /onboarding/purchase: post: summary: 'Comprar una hora de onboarding' operationId: comprarUnaHoraDeOnboarding description: "Inicia el checkout de Tilopay para comprar **1 hora de onboarding** a la\ntarifa vigente (USD). Disponible para todos los planes. Devuelve la URL\nde redirección a la pasarela de pago y el número de orden.\n\n> **Importante:** el crédito **no se otorga en este endpoint**. Solo se\n> inicia el cobro y se registra un pago en estado pendiente. El crédito\n> `purchased` se crea cuando el pago se confirma mediante el retorno\n> firmado de Tilopay (procesado por el webhook de retorno).\n\nSi la pasarela falla al iniciar el checkout, responde **HTTP 502**." parameters: [] responses: 200: description: 'Checkout iniciado' content: application/json: schema: type: object example: success: true data: redirect_url: 'https://app.tilopay.com/checkout/abc123' order: TPAY-0001 message: 'Redirigiendo a la pasarela de pago.' errors: null properties: success: type: boolean example: true data: type: object properties: redirect_url: type: string example: 'https://app.tilopay.com/checkout/abc123' description: 'URL de la pasarela de Tilopay a la que debe redirigir al usuario.' order: type: string example: TPAY-0001 description: 'Identificador de la orden/pago en la pasarela.' message: type: string example: 'Redirigiendo a la pasarela de pago.' errors: type: string example: null nullable: true 502: description: 'Pasarela no disponible' content: application/json: schema: type: object example: success: false data: null message: 'No se pudo iniciar el pago con Tilopay. Intente de nuevo en unos minutos.' errors: null properties: success: type: boolean example: false data: type: string example: null nullable: true message: type: string example: 'No se pudo iniciar el pago con Tilopay. Intente de nuevo en unos minutos.' errors: type: string example: null nullable: true tags: - Onboarding /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 Almendro 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' /item-shares: post: summary: '' operationId: postApiV1PublicItemShares description: '' parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: to_contributor_id: type: string description: 'El campo value debe ser un UUID válido.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed item_ids: type: array description: 'El campo value debe ser un UUID válido.' example: - 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 items: type: string message: type: string description: 'El campo value no puede tener más de 500 caracteres.' example: m nullable: true required: - to_contributor_id /item-shares/sent: get: summary: '' operationId: getApiV1PublicItemSharesSent description: '' parameters: [] responses: { } tags: - Otros /item-shares/received: get: summary: '' operationId: getApiV1PublicItemSharesReceived description: '' parameters: [] responses: { } tags: - Otros '/item-shares/{id}': get: summary: '' operationId: getApiV1PublicItemSharesId description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the item share.' example: architecto required: true schema: type: string '/item-shares/{id}/accept-preview': get: summary: '' operationId: getApiV1PublicItemSharesIdAcceptPreview description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the item share.' example: architecto required: true schema: type: string '/item-shares/{id}/accept': post: summary: '' operationId: postApiV1PublicItemSharesIdAccept description: '' parameters: [] responses: { } tags: - Otros requestBody: required: false content: application/json: schema: type: object properties: lines: type: array description: '' example: null items: type: object properties: line_id: type: integer description: '' example: 16 action: type: string description: '' example: architecto internal_code: type: string description: 'El campo value no puede tener más de 20 caracteres.' example: ngzmiyvdljnikhwa nullable: true required: - line_id - action parameters: - in: path name: id description: 'The ID of the item share.' example: architecto required: true schema: type: string '/item-shares/{id}/reject': post: summary: '' operationId: postApiV1PublicItemSharesIdReject description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the item share.' example: architecto required: true schema: type: string '/item-shares/{id}/cancel': post: summary: '' operationId: postApiV1PublicItemSharesIdCancel description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the item share.' example: architecto required: true schema: type: string /inbox/accessible-contributors: get: summary: "Lista los contribuyentes cuyo buzón puede ver/operar el autenticado:\nél mismo más los clientes sobre los que tiene un grant de certificado\nactivo. Alimenta el selector de contribuyente del buzón y de la carga." operationId: listaLosContribuyentesCuyoBuznPuedeVeroperarElAutenticadolMismoMsLosClientesSobreLosQueTieneUnGrantDeCertificadoActivoAlimentaElSelectorDeContribuyenteDelBuznYDeLaCarga description: '' parameters: [] responses: { } tags: - Otros /inbox/email-reception: get: summary: 'Estado de la recepción automática de facturas por correo (F2).' operationId: estadoDeLaRecepcinAutomticaDeFacturasPorCorreoF2 description: "`enabled`: si los XML dirigidos a su cédula que llegan a\nfacturas@almendro.cr se cargan solos a su buzón. `quarantined`: cuántos\ncomprobantes esperan en cuarentena (72 horas) por tener la recepción\napagada; se cargan si la reactiva a tiempo." parameters: [] responses: { } tags: - Otros patch: summary: 'Enciende o apaga la recepción automática de facturas por correo (F2).' operationId: enciendeOApagaLaRecepcinAutomticaDeFacturasPorCorreoF2 description: "Al APAGARLA, los comprobantes que lleguen dirigidos a su cédula quedan\nen cuarentena 72 horas en lugar de cargarse (no consumen su pozo). Al\nENCENDERLA, lo que esté en cuarentena vigente se carga automáticamente." parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Encender (`true`) o apagar (`false`).' example: true required: - enabled '/inbox/{key}/annotation': patch: summary: '' operationId: patchApiV1PublicInboxKeyAnnotation description: '' parameters: [] responses: { } tags: - Otros requestBody: required: false content: application/json: schema: type: object properties: alias: type: string description: 'El campo value no puede tener más de 120 caracteres.' example: b nullable: true notes: type: string description: 'El campo value no puede tener más de 1000 caracteres.' example: 'n' nullable: true parameters: - in: path name: key description: '' example: '56425593142326822820492192294472586671521743076933' required: true schema: type: string '/reminders/{type}/snooze': post: summary: '"No mostrar más" | posponer un recordatorio de puesta a punto por 6 meses.' operationId: NoMostrarMsPosponerUnRecordatorioDePuestaAPuntoPor6Meses description: "Solo disponible para integradores que gestionan al menos un contribuyente\ncon certificado de producción vigente (facturan por sus clientes y no\nnecesitan su propio .p12/perfil). Para el resto responde 422." parameters: [] responses: { } tags: - Otros parameters: - in: path name: type description: '' example: architecto required: true schema: type: string '/subscription/payments/{id}/pdf': get: summary: 'Descarga el PDF de la factura (FE) asociada a un pago.' operationId: descargaElPDFDeLaFacturaFEAsociadaAUnPago description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the payment.' example: e507fb2b94-c9e62d073743f8140a658fd5f required: true schema: type: string '/subscription/payments/{id}/xml': get: summary: 'Descarga el XML firmado de la factura (FE) asociada a un pago.' operationId: descargaElXMLFirmadoDeLaFacturaFEAsociadaAUnPago description: "La FE la emite Almendro HACIA el contribuyente, así que el voucher es del\nemisor; se autoriza por el PAGO (que sí es del contribuyente)." parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the payment.' example: e507fb2b94-c9e62d073743f8140a658fd5f required: true schema: type: string /discount-codes/validate: post: summary: '' operationId: postApiV1PublicDiscountCodesValidate description: '' parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: 'El campo value no puede tener más de 40 caracteres.' example: b nullable: true plan_code: type: string description: 'The code of an existing record in the plans table.' example: architecto billing_cycle: type: string description: '' example: architecto required: - plan_code - billing_cycle /discount-codes/availability: get: summary: "Combinaciones plan/ciclo que tienen una promoción activa para este\ncontribuyente. El checkout muestra el campo de descuento solo ahí." operationId: combinacionesPlancicloQueTienenUnaPromocinActivaParaEsteContribuyenteElCheckoutMuestraElCampoDeDescuentoSoloAh description: '' parameters: [] responses: { } tags: - Otros /subscription/subscribe-tokenized: post: summary: 'Suscribirse / subir de plan COBRANDO la tarjeta tokenizada (server-to-server).' operationId: suscribirseSubirDePlanCOBRANDOLaTarjetaTokenizadaserverToServer description: "Requiere tarjeta guardada (si no, el front manda primero a \"agregar tarjeta\").\nSi el contribuyente ya está en un plan de pago vigente del mismo ciclo, cobra\nsolo la **diferencia prorrateada** por lo que resta del período y MANTIENE la\nfecha de aniversario; si viene de Gratis/sin plan, cobra el precio completo y\narranca el período desde hoy. Las renovaciones siguen con `subscription:charge-due`.\n\nSolo SUBIDAS: para bajar de plan se usa la baja programada (`downgrade`)." parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: plan_code: type: string description: 'The code of an existing record in the plans table.' example: architecto billing_cycle: type: string description: '' example: architecto discount_code: type: string description: 'El campo value no puede tener más de 40 caracteres.' example: 'n' nullable: true required: - plan_code - billing_cycle /subscription/card-session: post: summary: 'Sesión para el SDK V2 (alta de tarjeta embebida).' operationId: sesinParaElSDKV2altaDeTarjetaEmbebida description: "Devuelve el token del SDK (`loginSdk`) y los parámetros para inicializar\n`Tilopay.InitTokenize()` en el frontend. El token de la tarjeta vuelve del\nlado cliente y se guarda con `saveCard()`. El email es el del contribuyente:\nel cobro posterior se hace con ese mismo par email + token." parameters: [] responses: { } tags: - Otros /subscription/save-card: post: summary: 'Guarda el token de la tarjeta tokenizada por el SDK contra el contribuyente.' operationId: guardaElTokenDeLaTarjetaTokenizadaPorElSDKContraElContribuyente description: "El email almacenado es el mismo que usó cardSession() (Tilopay rutea el cobro\npor email + token)." parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'El campo value no puede tener más de 191 caracteres.' example: b last4: type: string description: 'El campo value no puede tener más de 4 caracteres.' example: ngzm nullable: true brand: type: string description: 'El campo value no puede tener más de 20 caracteres.' example: iyvdljnikhwaykcm nullable: true required: - token /subscription/sdk-pay-session: post: summary: 'Sesión de PAGO con el SDK (cobro único que guarda la tarjeta, `Init({subscription:1})`).' operationId: sesinDePAGOConElSDKcobronicoQueGuardaLaTarjetaInitsubscription1 description: "Se usa cuando el contribuyente NO tiene tarjeta guardada: el primer pago cobra\ny guarda la tarjeta en un solo cargo (modelo Uber). Crea un Payment pendiente\ncon la intención (suscribirse o retención) y devuelve los params del SDK." parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: purpose: type: string description: '' example: architecto plan_code: type: string description: 'The code of an existing record in the plans table.' example: architecto nullable: true billing_cycle: type: string description: '' example: architecto nullable: true discount_code: type: string description: 'El campo value no puede tener más de 40 caracteres.' example: 'n' nullable: true beneficiary_contributor_id: type: string description: 'Retención por un cliente gestionado (opcional). Default: uno mismo. El campo value debe ser un UUID válido.' example: 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 nullable: true tier_code: type: string description: 'Pozo del programa Distribuidor Autorizado. El campo value no puede tener más de 10 caracteres.' example: miyvdl nullable: true required: - purpose /subscription/sdk-pay-confirm: post: summary: "Confirma un pago hecho con el SDK: verifica con `consult`, guarda la tarjeta y\nejecuta la compra (activar plan o crear la extensión de retención). No cobra de\nnuevo (el SDK ya cobró)." operationId: confirmaUnPagoHechoConElSDKVerificaConconsultGuardaLaTarjetaYEjecutaLaCompraactivarPlanOCrearLaExtensinDeRetencinNoCobraDeNuevoelSDKYaCobr description: '' parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: order: type: string description: 'El campo value no puede tener más de 191 caracteres.' example: b token: type: string description: 'El campo value no puede tener más de 191 caracteres.' example: 'n' last4: type: string description: 'El campo value no puede tener más de 4 caracteres.' example: gzmi nullable: true brand: type: string description: 'El campo value no puede tener más de 20 caracteres.' example: yvdljnikhwaykcmy nullable: true required: - order - token /subscription/card: delete: summary: "Elimina la tarjeta guardada. Intenta el `card-remove` en Tilopay (si el\ncomercio tuviera el permiso) y, pase o no, suelta el token de nuestro lado:\nsin token no se cobra más." operationId: eliminaLaTarjetaGuardadaIntentaElcardRemoveEnTilopaysiElComercioTuvieraElPermisoYPaseONoSueltaElTokenDeNuestroLadoSinTokenNoSeCobraMs description: '' parameters: [] responses: { } tags: - Otros /onboarding/purchase-tokenized: post: summary: "Compra la hora de onboarding COBRANDO la tarjeta guardada (sin salir del\nportal). Mismo patrón que la retención tokenizada: chargeToken + crédito\ninmediato + FE. Sin tarjeta guardada responde 409 (el front abre el SDK)." operationId: compraLaHoraDeOnboardingCOBRANDOLaTarjetaGuardadasinSalirDelPortalMismoPatrnQueLaRetencinTokenizadaChargeToken+CrditoInmediato+FESinTarjetaGuardadaResponde409elFrontAbreElSDK description: '' parameters: [] responses: { } tags: - Otros /retention-extension/purchase: post: summary: 'Inicia el checkout de la retención para el contribuyente autenticado.' operationId: iniciaElCheckoutDeLaRetencinParaElContribuyenteAutenticado description: '' parameters: [] responses: { } tags: - Otros /retention-extension/charge-tokenized: post: summary: 'El contribuyente cobra su propia retención con la tarjeta guardada.' operationId: elContribuyenteCobraSuPropiaRetencinConLaTarjetaGuardada description: '' parameters: [] responses: { } tags: - Otros /retention-extension/cancel: post: summary: "Desactiva la auto-renovación: la retención sigue ACTIVA hasta el fin de su\nperíodo y luego no se renueva (charge-due la omite)." operationId: desactivaLaAutoRenovacinLaRetencinSigueACTIVAHastaElFinDeSuPerodoYLuegoNoSeRenuevachargeDueLaOmite description: '' parameters: [] responses: { } tags: - Otros /retention-extension/resume: post: summary: 'Reactiva la auto-renovación de una retención cancelada pero aún vigente.' operationId: reactivaLaAutoRenovacinDeUnaRetencinCanceladaPeroAnVigente description: '' parameters: [] responses: { } tags: - Otros /api-access/purchase: post: summary: '' operationId: postApiV1PublicApiAccessPurchase description: '' parameters: [] responses: { } tags: - Otros /api-access/charge-tokenized: post: summary: '' operationId: postApiV1PublicApiAccessChargeTokenized description: '' parameters: [] responses: { } tags: - Otros /api-access/cancel: post: summary: "Desactiva la auto-renovación: el acceso sigue ACTIVO hasta el fin del\nperíodo pagado y luego no se renueva (charge-due lo omite)." operationId: desactivaLaAutoRenovacinElAccesoSigueACTIVOHastaElFinDelPerodoPagadoYLuegoNoSeRenuevachargeDueLoOmite description: '' parameters: [] responses: { } tags: - Otros /api-access/resume: post: summary: 'Reactiva la auto-renovación de un add-on cancelado pero aún vigente.' operationId: reactivaLaAutoRenovacinDeUnAddOnCanceladoPeroAnVigente description: '' parameters: [] responses: { } tags: - Otros /distributor/info: get: summary: 'GET /distributor/info' operationId: gETdistributorinfo description: "Estado del programa para la sección de Mi Plan: inscripción, niveles\na la venta con el precio aplicable (reposición incluida si califica),\ny el resumen de pozos propios si ya es distribuidor." parameters: [] responses: { } tags: - Otros /distributor/purchase: post: summary: '' operationId: postApiV1PublicDistributorPurchase description: '' parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: tier_code: type: string description: 'El campo value no puede tener más de 10 caracteres.' example: bngzmi required: - tier_code /distributor/charge-tokenized: post: summary: '' operationId: postApiV1PublicDistributorChargeTokenized description: '' parameters: [] responses: { } tags: - Otros requestBody: required: true content: application/json: schema: type: object properties: tier_code: type: string description: 'El campo value no puede tener más de 10 caracteres.' example: bngzmi required: - tier_code /managed-sales: get: summary: '' operationId: getApiV1PublicManagedSales description: '' parameters: [] responses: { } tags: - Otros '/managed-sales/{activationId}': patch: summary: "Registro PRIVADO de la venta del gestor (lo que él cobra a su\ncliente): monto, moneda y modo de pago. El admin nunca lo ve." operationId: registroPRIVADODeLaVentaDelGestorloQuelCobraASuClienteMontoMonedaYModoDePagoElAdminNuncaLoVe description: '' parameters: [] responses: { } tags: - Otros requestBody: required: false content: application/json: schema: type: object properties: sale_amount: type: number description: 'El campo value debe ser al menos 0. El campo value no puede ser mayor a 99999999.' example: 1 nullable: true sale_currency: type: string description: '' example: USD enum: - CRC - USD nullable: true sale_payment_method: type: string description: '' example: architecto nullable: true parameters: - in: path name: activationId description: '' example: '564' required: true schema: type: string '/my-contributors/{id}/retention-extension': post: summary: "Inicia el checkout de la retención que un gestor paga para un cliente\ngestionado. El gestor autenticado paga; el cliente es el beneficiario." operationId: iniciaElCheckoutDeLaRetencinQueUnGestorPagaParaUnClienteGestionadoElGestorAutenticadoPagaElClienteEsElBeneficiario description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the my contributor.' example: e507fb2b94-c9e62d073743f8140a658fd5f required: true schema: type: string '/my-contributors/{id}/retention-extension/charge-tokenized': post: summary: 'Un gestor cobra la retención de un cliente gestionado con SU tarjeta guardada.' operationId: unGestorCobraLaRetencinDeUnClienteGestionadoConSUTarjetaGuardada description: '' parameters: [] responses: { } tags: - Otros parameters: - in: path name: id description: 'The ID of the my contributor.' example: e507fb2b94-c9e62d073743f8140a658fd5f required: true schema: type: string /entitlement: get: summary: 'Entitlement del plan' operationId: entitlementDelPlan description: "Devuelve el plan vigente del contribuyente autenticado con TODOS sus\nlímites resueltos (override por contribuyente > valor del plan;\n`null` = sin tope). Pensado para que integraciones (POS, canal\ne-commerce) lo cacheen y apliquen los límites localmente." parameters: [] responses: 200: description: 'Entitlement resuelto' content: application/json: schema: type: object example: success: true data: plan: code: professional name: Profesional billing_mode: annual_or_monthly features: - api_access - receiver_module - webhooks limits: monthly_voucher_limit: 500 max_users: 5 max_clients: 1500 max_items: 3000 max_branches: 5 max_terminals_per_branch: 5 max_pos_items: 15000 max_pos_cashiers_per_terminal: 10 max_channel_pages: 15 message: 'Entitlement del plan (límites resueltos con overrides).' errors: null properties: success: type: boolean example: true data: type: object properties: plan: type: object properties: code: type: string example: professional name: type: string example: Profesional billing_mode: type: string example: annual_or_monthly features: type: array example: - api_access - receiver_module - webhooks items: type: string limits: type: object properties: monthly_voucher_limit: type: integer example: 500 max_users: type: integer example: 5 max_clients: type: integer example: 1500 max_items: type: integer example: 3000 max_branches: type: integer example: 5 max_terminals_per_branch: type: integer example: 5 max_pos_items: type: integer example: 15000 max_pos_cashiers_per_terminal: type: integer example: 10 max_channel_pages: type: integer example: 15 description: 'Cada límite YA resuelto: override del contribuyente sobre el valor del plan; `null` = sin tope.' message: type: string example: 'Entitlement del plan (límites resueltos con overrides).' errors: type: string example: null nullable: true tags: - 'Perfil del contribuyente'