Documento de Implementación

API de Facturación Electrónica

Guía de implementación del backend de facturación electrónica de Vision Total ERP, publicado en https://sandbox.vision-total.rogcr.com. Cubre autenticación multi-tenant por credencial de máquina (M2M), datos maestros, administración de clientes e inventario, emisión ante Hacienda (ATV v4.4) de 01 - Factura Electrónica (FE), 02 - Nota de Débito Electrónica (ND), 03 - Nota de Crédito Electrónica (NC), 04 - Tiquete Electrónico (TE), 08 - Factura Electrónica de Compra (FEC), 09 - Factura Electrónica de Exportación (FEE) y 10 - Recibo Electrónico de Pago (REP), además del flujo de compras con el 05 - Mensaje de aceptación total, 06 - Mensaje de aceptación parcial y 07 - Mensaje de rechazo, con seguimiento del estado, con ejemplos verificables.

Hacienda ATV v4.4 Facturador independiente Multi-tenant por credencial M2M vision-total.rogcr.com/api · siempre con prefijo REST + JSON + multipart Clientes · Inventario por API Mensaje del Receptor en compras Anticipos RC / PP aplicables al pago Firmas XAdES en backend, nunca en el cliente
Versión del documento
1.13 · 5 de noviembre de 2026
URL base del servicio
https://sandbox.vision-total.rogcr.com
Prefijo de ruta
/api · siempre
Documentos v4.4
01 - Factura Electrónica · 02 - Nota de Débito Electrónica · 03 - Nota de Crédito Electrónica · 04 - Tiquete Electrónico · 05 - Mensaje de aceptación total · 06 - Mensaje de aceptación parcial · 07 - Mensaje de rechazo · 08 - Factura Electrónica de Compra · 09 - Factura Electrónica de Exportación · 10 - Recibo Electrónico de Pago

01 Resumen ejecutivo

Este documento describe la API de facturación electrónica de Vision Total ERP tal como se publica en https://sandbox.vision-total.rogcr.com. Es una API HTTP sin SDK propietario: lo que un integrador llama como /api/invoices es exactamente lo que se puede llamar desde cualquier lenguaje, con cualquier cliente HTTP y desde cualquier servidor.

Lo que resuelve el administrador del ERP

Alta de la organización y de sus usuarios, roles y permisos, emisión de la credencial de máquina (M2M) del servicio, habilitación de la facturación electrónica y consecutivos por sucursal y terminal. Se hace una sola vez y no forma parte de la integración.

Lo que resuelve el servicio externo por API

Autenticarse con la credencial de máquina, cargar el catálogo operativo (clientes, productos, proveedores), administrar clientes e inventario, crear la factura, registrar el pago, emitir el comprobante, seguir su estado y descargar el PDF, el XML firmado y el MensajeHacienda.

Un camino de emisión

Facturador (/api/invoices): factura de venta con líneas libres, pagos y conversión a 01 - Factura Electrónica (FE), 04 - Tiquete Electrónico (TE), 08 - Factura Electrónica de Compra (FEC) o 09 - Factura Electrónica de Exportación (FEE). El comprobante nace del documento comercial y del pago, no de una operación de caja.

Modelo multi-tenant

La credencial de máquina lleva la organización activa y el rol. Cada consulta se filtra por ese identificador, por lo que el token es la única credencial necesaria para operar dentro de un tenant.

Reglas de oro antes de escribir código

  1. La URL base es https://sandbox.vision-total.rogcr.com. Todas las rutas llevan el prefijo /api: /api/invoices, /api/organizations, /api/facturacion/documents. Sin prefijo, el host público responde 404.
  2. La sesión se inicia con la credencial de máquina. No hay login ni select-organization: la primera llamada que lleva Authorization: Bearer vt_m2m_… ya opera sobre la organización del token. Véase 4.
  3. Todo lo que se emite está atado a un terminal. La sucursal y el terminalparticipant en la composición de la clave de 50 dígitos; sin consecutivos configurados no hay clave posible.
  4. El comprobante se crea y se paga antes de enviarse. Hacienda no acepta documentos con saldo pendiente en un flujo de crédito.
  5. La firma ocurre en el servidor. El servicio externo nunca manipula material criptográfico: solo recibe el comprobante ya firmado.
  6. El estado final no es inmediato. La emisión responde 202 con un jobId; el resultado real se consulta en GET /api/facturacion/documents/:documentId/status.
  7. Dos datos no validados viajan en la línea y en el receptor. La unidadMedida se escribe tal cual en el XML —su código sale del catálogo de GET /api/units-of-measure— y la identificación del receptor tiene que existir en el padrón de Hacienda. El API acepta ambas cosas sin comprobar nada: el rechazo llega después, en el estado del comprobante.
  8. El veredicto lo da el estado, nunca el detalle. Un comprobante aceptado puede traer advertencias en el detalleMensaje.
  9. El medio de pago se nombra de dos maneras, según la ruta. En POST /api/invoices/:id/payments y en el medioReembolso de las notas (NC/ND) se manda el nombre interno (cash, sinpe, …); en el medioPago del 10 - Recibo Electrónico de Pago (REP) se manda el código de la Nota 6 ("01"…"99"). Véase 14.9.
  10. El 10 - Recibo Electrónico de Pago (REP) no se crea como factura. Se emite exclusivamente con POST /api/invoices/:id/recibo-pago, sobre un comprobante comercial ya aceptado. Véase 11.3.
Alcance del documento

Esta guía cubre la superficie HTTP del módulo de facturación electrónica: autenticación, datos maestros, emisión, notas derivadas y seguimiento. Incluye además la administración operativa que la facturación necesita como insumo —el padrón de clientes y el catálogo de productos con sus existencias—, porque se expone por la misma API pública y con el mismo token. La habilitación de la facturación para una organización es una tarea administrativa del ERP y queda fuera del alcance. Los detalles criptográficos del XSD v4.4 y de la firma XAdES son responsabilidad del servidor: un integrador no necesita conocerlos para operar correctamente.

02 El mapa de la API

Toda la superficie HTTP vive bajo el mismo prefijo /api, se autentica con el mismo token y opera sobre la misma organización activa. El mapa de abajo es el inventario completo de lo que un servicio externo necesita.

Diagrama 1 Superficie de la API pública
Su servicio curl · JS · Python cron · worker Bearer vt_m2m_… API pública https://sandbox.vision-total.rogcr.com SESIÓN · CREDENCIAL M2M GET /api/m2m/me GET /api/permissions GET /api/organizations Header: Authorization: Bearer vt_m2m_… Sin login · sin select-organization EMISIÓN POST /api/invoices POST /api/invoices/:id/payments POST /api/invoices/:id/facturar POST /api/invoices/:id/nota-credito POST /api/invoices/:id/nota-debito CATÁLOGOS GET|POST /api/customers GET|POST /api/products GET|POST /api/suppliers GET /api/cabys GET /api/economic-activities GET /api/locations/costa-rica POST /api/hacienda/exoneraciones/lookup COMPROBANTES GET /api/facturacion/documents GET /api/facturacion/documents/:id/status GET /api/facturacion/documents/:id/pdf GET /api/facturacion/documents/:id/xml GET /api/facturacion/connection OPERACIÓN · CLIENTES, INVENTARIO, ANTICIPOS Y COMPRAS GET|POST|PUT|DELETE /api/customers GET|POST|PUT|DELETE /api/products GET|POST|PUT|DELETE /api/warehouses GET|POST|PATCH /api/advances POST /api/products/:id/movements POST /api/waste POST /api/inventory/reconcile GET|POST|PATCH /api/purchases HTTPS
Cinco grupos, un solo prefijo, un solo token. El mapa es un resumen: el inventario completo de cada ruta está en el catálogo de endpoints.

Tres reglas que explican casi todo

  1. Todo lleva el mismo token. El Authorization: Bearer lleva la credencial de máquina (vt_m2m_…), que identifica la organización activa y el rol con el que se opera. No hay login ni cambio de token entre llamadas.
  2. Todo ocurre bajo la organización del token. No se envía organizationId en el cuerpo de las peticiones de negocio: es el token el que determina a qué tenant pertenecen los datos.
  3. Emitir es asíncrono. Las operaciones de emisión devuelven 202 Accepted con un jobId. El resultado real ante Hacienda se consulta después en /api/facturacion/documents/:documentId/status.
Los secretos no viajan por la API

La firma criptográfica del comprobante ocurre íntegramente en el servidor. El servicio externo nunca recibe, almacena ni transmite material criptográfico: solo recibe el comprobante ya firmado, el PDF y el mensaje de respuesta.

03 Conexión

URL base y prefijo /api

La API se publica en un único endpoint: https://sandbox.vision-total.rogcr.com. Todas las rutas de esta guía empiezan por /api y todas las llamadas salen contra ese host, ya sea desde un cron, un worker, un webhook o un backend propio.

El prefijo /api es parte del contrato público

Envíe siempre /api/invoices, /api/organizations, /api/facturacion/documents. Si omite el prefijo contra el host público recibirá un 404.

EscenarioURL baseRutas
Producción (este documento) https://sandbox.vision-total.rogcr.com /api/<ruta>
Desarrollo, mismo contrato http://localhost:4200/api /api/<ruta>
Health check https://sandbox.vision-total.rogcr.com/api/health Público. Devuelve { "status": "ok" }. Úselo para readiness.

El prefijo es parte de la URL

El prefijo /api se publica junto con el resto de la API y se aplica por igual a todas las rutas. No hay variantes con y sin prefijo: lo que existe es /api/invoices, y no existe /invoices.

Regla práctica

Configure la URL base del servicio externo como https://sandbox.vision-total.rogcr.com y construya las rutas a partir de ahí. Use una sola variable de entorno (VT_API_BASE) y no disperse el prefijo por el código: si mañana se enruta por otro proxy, basta cambiar esa variable.

Requisitos para consumir el servicio

  • HTTPS. La API transporta la credencial de máquina y los datos de los comprobantes. Use siempre TLS y no envíe credenciales por otros medios.
  • CORS. El backend responde con Access-Control-Allow-Origin: *, por lo que un cliente web externo puede llamar directamente sin configuración adicional.
  • Una sola forma de autenticarse. La API se consume con la credencial de máquina que el administrador crea en Ajustes → Claves API (M2M) (véase 4). No hay una API key clásica de por medio ni un login de usuario: la credencial es la sesión. Trátela como el secreto que es —transfiérela solo por HTTPS, guárdela fuera del código y pida una nueva cuando se revoque o expire.
  • Readiness. Antes de la primera llamada, verifique que GET /api/health responde 200 con { "status": "ok" }.

Cabeceras y autenticación

Toda llamada protegida lleva la credencial de máquina en la cabecera Authorization. No hay cookies de sesión, ni tokens de refresco, ni paso previo: la credencial es el estado completo de la sesión —organización y rol incluidos— y se renueva sólo cuando el administrador crea una nueva.

GET /api/facturacion/connection
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/connection" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"
CabeceraObligatoriaValor
AuthorizationSíBearer vt_m2m_…: la credencial de máquina de 4.1. Es el único formato de token que acepta esta API. La query string ?token= no es una alternativa general: la aceptan sólo tres descargas concretas —el proxy de medios (/api/media/*), los pases de wallet (/api/appointments/:id/wallet/apple y google) y el .ics (/api/appointments/:id/calendar.ics)— y cada una con su propio esquema de token.
Content-TypeSí en cuerposapplication/json, o multipart/form-data para la carga de comprobantes de compra.
AcceptNoÚtil para las descargas de PDF/XML, que devuelven bytes crudos.

Formato de errores

El manejador global de errores serializa las excepciones de negocio con una forma uniforme, pero los endpoints heredados usan variantes más simples. Maneje ambos casos leyendo siempre message.

POST /api/invoices · error de validación
{
  "success": false,
  "message": "El documento de identificación es inválido para el tipo seleccionado.",
  "code": "BAD_REQUEST"
}
POST /api/invoices · error heredado (sin wrapper)
{
  "message": "motivo requerido (3-180 caracteres)"
}

Paginación y listados

El API usa cuatro convenciones distintas de listado. Identifíquelas antes de consumir un endpoint.

ConvenciónFormaEndpoints representativos
Paginado { data: [], total, page, limit, totalPages } GET /api/invoices, GET /api/facturacion/documents
Paginado con metadatos { success: true, data: [], meta: { page, limit, total, totalPages } } GET /api/products/:id/movements
Array plano [ ... ] GET /api/customers, GET /api/products, GET /api/terminals, GET /api/warehouses, GET /api/categories, GET /api/units-of-measure, GET /api/organizations/:id/activities, GET /api/economic-activities
Envoltura success { success: true, data: [] } GET /api/suppliers, GET /api/purchases
Objeto con data suelta { data: [] } GET /api/invoicing/records, GET /api/exchange-rates/:currency
Objeto con items { total: 42, items: [] } GET /api/cabys
Informe { filters, count, totals, rows } GET /api/reports/inventory, GET /api/reports/inventory-all
No confíe en el tipo del número

Los importes vienen como string en algunos endpoints y como number en otros. Normalice con Number(v) antes de comparar o sumar. Es el caso de price, costPrice, stock y minStock en los productos, y de quantity, previousQuantity y balanceAfter en los movimientos de stock.

El listado de facturas no trae líneas ni pagos

GET /api/invoices devuelve los documentos con sus importes, su customerName y su estado, pero sin items ni payments: esos vienen en GET /api/invoices/:id, que además trae haciendaDocuments con el numeroConsecutivo. Resuelva el listado con los campos de cabecera y pida el detalle solo al abrir una factura, porque cada detalle es una llamada.

04 Autenticación: la credencial de máquina (M2M)

La API se consume con una credencial de máquina (M2M). No hay login de usuario ni paso de selección de organización: la sesión del servicio empieza con el token. La credencial la emite un administrador del ERP en Ajustes → Claves API (M2M) y la usa el servicio externo en todas sus llamadas.

Una sola credencial, una sola sesión

El token vt_m2m_… es a la vez la credencial y el estado de la sesión: lleva la organización activa y el rol con el que se opera. No hay POST /api/auth/login, no hay POST /api/auth/select-organization y no hay token de refresco: la sesión del servicio empieza en la primera llamada que lleva el token y dura hasta que la credencial se revoca o expira.

Diagrama 2 Secuencia de autenticación del servicio
Integrador            API REST

   │  ① GET /api/m2m/me
   │  Authorization: Bearer vt_m2m_…
   ├──────────────────────────▶│
   │                           │  resuelve la credencial, su expiración y su revocación
   │                           ├───────────────────▶│
   │                           │◀───────────────────┤
   │  200 { token, organization, role }
   │      ▲ organization.id es el tenant con el que se opera
   │
   │  ② POST /api/invoices
   │  Authorization: Bearer vt_m2m_…   ← el MISMO token, sin transformarlo
   ├──────────────────────────▶│  la organización sale del token
   │  201 { data: { id, total, ... } }
   │
   │  ③ GET /api/facturacion/connection
   ├──────────────────────────▶│  comprueba que el tenant está listo
   │  200 { facturacionConfigured, ... }
   │
   │  ④ GET /api/facturacion/documents/:id/status
   ├──────────────────────────▶│  sondea el comprobante ante Hacienda
   │  200 { data: { estado, ... } }
   │

4.1 · La credencial: quién la crea y qué trae

El servicio no crea credenciales. La crea un administrador del ERP en Ajustes → Claves API (M2M), eligiendo el rol que tendrá y, si se quiere, la fecha de expiración. Ese rol es el único control de alcance: la credencial puede hacer exactamente lo que ese rol puede hacer, ni más ni menos.

TOKEN formato de la credencial de máquina
vt_m2m_<prefijo>.<secreto>
#         └─ público ─┘ └─ autoriza ┘

# Ejemplo (el secreto es inventado para la documentación):
vt_m2m_k3P9xQ7mR2tY.Tq8mZ2vL4nB6xD8fH0jK3mP5sR7wY9aC1eG3iK5oQ7uW9yA1bC3e

El prefijo es la parte pública: identifica la credencial en los listados y en los registros de uso. El secreto es lo que autoriza, y se muestra una sola vez: sale en la respuesta del alta y no vuelve a aparecer. Después sólo se ve el prefijo. Si se pierde, hay que revocar la credencial y crear otra.

Detrás de la credencial hay una cuenta de servicio

Cada credencial M2M tiene detrás una cuenta de servicio (users.is_service_account = true), que no es una persona: no aparece en las listas de miembros ni de usuarios, no puede registrar passkeys ni cambiar su contraseña. Su membresía en la organización es la que porta el rol, de modo que los permisos se resuelven exactamente igual que para un usuario humano. Por eso la credencial no puede administra otras credenciales —ni escalar el rol de las existentes—: eso lo hace el administrador humano con el permiso organization:manage_api_tokens.

4.2 · Iniciar la sesión

Iniciar la sesión es, literalmente, enviar el token en la cabecera Authorization. No hay cuerpo, ni cookies, ni paso previo: la primera llamada autenticada ya abre la sesión sobre la organización del token.

GET /api/m2m/me · la sesión ya está abierta
curl -X GET "https://sandbox.vision-total.rogcr.com/api/m2m/me" \
  -H "Authorization: Bearer vt_m2m_k3P9xQ7mR2tY.Tq8mZ2v…"
En todos los ejemplos de esta guía

La variable $TOKEN es la credencial de máquina (vt_m2m_…), tal como la entrega el administrador. Se envía sin transformarlo: no se decodifica, no se reescribe y no se convierte en otro token entre una llamada y la siguiente.

4.3 · Verificar la sesión

GET /api/m2m/me es el chequeo de vida de la credencial: devuelve el nombre de la credencial, la organización sobre la que opera y el rol con el que se está operando. Es la primera llamada que conviene hacer en el arranque de un servicio.

GET /api/m2m/me · respuesta 200
{
  "token": {
    "id": "c4e1...",
    "name": "Sync contable",
    "organizationId": "3b91c..."
  },
  "organization": {
    "id": "3b91c...",
    "name": "Comercial del Este",
    "legalName": "Comercial del Este Sociedad Anónima",
    "taxId": "3101234567",
    "taxIdType": "02",
    "currency": "CRC",
    "type": "retail"
  },
  "role": { "id": "c7d2...", "name": "Facturación" }
}
Una credencial revocada responde 401

Si el secreto es incorrecto, la credencial está revocada o venció, la API responde 401 con { "message": "Token M2M inválido, expirado o revocado." } y no cae al flujo de los JWT de usuario: un token vt_m2m_… nunca se reinterpreta como otra cosa. No hay forma de «renovar» el token desde la API: hay que crear una credencial nueva.

4.4 · Cómo se aísla el tenant

La organización sale del token

La organización activa se lee del organizationId que la credencial lleva grabado y la API la aplica a todas las consultas. No se acepta un organizationId en el cuerpo de la request para cambiar de alcance: el cambio de tenant exige otra credencial, creada por un administrador para otra organización.

Tampoco existe un paso de «seleccionar organización» para la credencial: la API rechaza POST /api/auth/select-organization con una sesión de máquina, porque ese endpoint acuña un JWT de usuario y una credencial no debe poder convertirse en una sesión interactiva que sobreviva a su revocación.

POST /api/auth/select-organization · con credencial M2M
HTTP 403
{
  "message": "Las credenciales de máquina no pueden usar esta operación."
}

4.5 · Ciclo de vida, límites y errores

La credencial no caduca sola: su vida depende de lo que el administrador decida al crearla. Estos son los límites que gobiernan su uso.

  • Nace atado a una organización. No hay paso de select-organization: la organización es la que el administrador eligió al crear la credencial y la API no permite cambiarla. Con ese token se opera sobre los datos de esa organización en cualquier ruta.
  • Hereda los permisos de un rol. El rol se elige al crear la credencial y es el único control de alcance: la credencial puede hacer exactamente lo que ese rol puede hacer, ni más ni menos. Use el de mínimo privilegio que el servicio necesite (véase 06).
  • Se muestra una sola vez. El secreto en claro sale en la respuesta del alta y no vuelve a aparecer: después sólo se ve el prefijo. Si se pierde, hay que revocar la credencial y crear otra.
  • Puede expirar y se puede revocar. La fecha de expiración es opcional y se fija en el alta; la revocación surte efecto de inmediato, y la siguiente llamada con ese token responde 401.
  • No administra otras credenciales. Las rutas de administración de claves (/api/organizations/:orgId/m2m-tokens) rechazan una sesión de máquina: una credencial usa la API, pero no puede crear, editar ni revocar otras credenciales —ni escalar el rol de las existentes—. Eso lo hace un administrador humano con el permiso organization:manage_api_tokens, desde Ajustes → Claves API (M2M). Una credencial que intente hacerlo recibe 403.
  • Verificación. GET /api/m2m/me devuelve la organización, el rol y el nombre de la credencial. Con un JWT de usuario responde 403: ese endpoint es sólo para credenciales de máquina.
Dos límites que conviene respetar
  • El token se acepta sólo en la cabecera Authorization. La query string (?token=) no es una alternativa para la API: la aceptan únicamente tres descargas concretas —el proxy de medios (/api/media/*), los pases de wallet (/api/appointments/:id/wallet/apple y /google) y el .ics (/api/appointments/:id/calendar.ics)— y cada una con su propio esquema de token, no con la credencial de máquina.
  • El gate de suscripción aplica a las escrituras que la credencial hace sobre los datos del negocio: una organización sin suscripción activa no puede escribir con la credencial, aunque el token sea válido. La administración de las credenciales en sí está exenta del gate —mismo trato que el resto de /api/organizations/*—, así que un tenant con la suscripción vencida todavía puede crear y revocar claves.
Los errores de autenticación que hay que distinguir
HTTPmessageCausaQué hacer
401 Token M2M inválido, expirado o revocado. El secreto no corresponde a ninguna credencial vigente: está mal copiado, la credencial se revocó o venció. Pedir una credencial nueva al administrador. No hay renovación por API.
401 Authorization header missing La llamada no llevaba la cabecera Authorization. Revisar el cliente HTTP: el token va en la cabecera, no en la query string.
403 Las credenciales de máquina no pueden usar esta operación. Se intentó usar una ruta interactiva con la credencial (por ejemplo POST /api/auth/select-organization). Usar la ruta propia de la API de negocio; la sesión interactiva es del ERP web.
403 User is not a member of this organization La membresía de la cuenta de servicio se revocó o la credencial quedó sin organización. Pedir al administrador que recree la credencial.

4.6 · Lo que no es el camino del integrador

El ERP web autentica a las personas con un login de dos etapas (correo y contraseña → select-organization → JWT). Ese mecanismo existe para la sesión interactiva de un usuario humano y no es el mecanismo de esta API: una integración que consume la API por Machine-to-Machine se autentica con la credencial de 4.1.

RutaCon una credencial M2MPara quién es
POST /api/auth/loginSin efecto sobre la sesión de máquina.Personas (ERP web).
POST /api/auth/login/challengeSin efecto sobre la sesión de máquina.Personas con doble factor.
POST /api/auth/select-organization403 — está bloqueada para las cuentas de servicio.Personas (ERP web).
GET /api/auth/meDescribe la cuenta de servicio, no al humano.Personas (ERP web).
PUT /api/auth/change-password403 — una cuenta de servicio no tiene contraseña.Personas (ERP web).
GET /api/m2m/me200 — este es el endpoint de sesión del integrador.Integraciones por API.
Por qué la credencial no puede volverse una sesión de usuario

Si una credencial de máquina pudiera acuñar un JWT de usuario, ese JWT sobreviviría a la revocación del token: bastaría con llamar una vez a select-organization para quedarse con una sesión permanente que el administrador ya no puede cerrar. Por eso el backend bloquea esa ruta —y también el cambio de contraseña y el alta de passkeys— para las cuentas de servicio.

05 La organización y sus usuarios

La organización y sus usuarios no los crea el servicio externo. Los crea un administrador del ERP. El servicio se limita a autenticarse con la credencial que le provisionen y a operar dentro del tenant.

División de responsabilidades

La habilitación de la facturación electrónica de una organización es una tarea administrativa del ERP: se hace una sola vez y queda disponible para cualquier servicio que se conecte después. Esta guía no la documenta porque no es parte de la integración.

Cómo el servicio obtiene su organizationId

No hay que hardcodearlo ni elegirlo: la organización es la que el administrador eligió al crear la credencial. El servicio la lee de GET /api/m2m/me —el campo organization.id— y ese mismo identificador es el que devuelve GET /api/organizations para la credencial: un arreglo con la organización de la credencial y nada más.

GET /api/organizations
curl -X GET "https://sandbox.vision-total.rogcr.com/api/organizations" \
  -H "Authorization: Bearer $TOKEN"
GET /api/organizations · respuesta 200 (extracto)
[
  {
    "id": "3b91c...",
    "name": "Comercial del Este",
    "legalName": "Comercial del Este Sociedad Anónima",
    "taxId": "3101234567",
    "taxIdType": "02",
    "address": "Avenida Central, 500 metros al sur",
    "country": "Costa Rica",
    "email": "facturacion@miempresa.cr",
    "currency": "CRC",
    "type": "retail",
    "facturacionConfigured": true,
    "hasHaciendaCredentials": true,
    "hasCertificate": true
  }
]
CampoPara qué le sirve al servicio
idEl organizationId de la credencial. No se envía en el cuerpo de ninguna petición: ya viaja en el token.
taxId / taxIdTypeLa identificación fiscal del emisor. Aparece en el XML, no se envía.
currencyLa moneda por defecto de los comprobantes.
facturacionConfiguredtrue cuando el administrador ya activó la facturación electrónica.
El servicio no puede "arreglar" la configuración

Si facturacionConfigured es false, el servicio no puede emitir. No existe ningún endpoint que le habilite la facturación a una organización: debe pedirlo al administrador del ERP.

06 Permisos del servicio externo

El rol de la credencial con la que el servicio se autentica lo elige un administrador del ERP al crearla. El servicio solo necesita conocer qué permisos exige cada operación para poder documentar qué rol debe tener esa credencial.

Permisos que exige la emisión

Clave de permisoPermite
invoices:view_invoicesListar y ver facturas, comprobantes y cotizaciones.
invoices:create_invoiceCrear facturas y cotizaciones (POST /api/invoices).
invoices:edit_invoiceEditar facturas y sus líneas.
invoices:add_paymentRegistrar y quitar pagos.
invoices:transition_invoiceEmitir el comprobante ante Hacienda (POST /api/invoices/:id/facturar).
invoices:issue_credit_noteEmitir 03 - Nota de Crédito Electrónica (NC) y 10 - Recibo Electrónico de Pago (REP).
invoices:issue_debit_noteEmitir 02 - Nota de Débito Electrónica (ND).
invoices:view_documentsConsultar notas derivadas de un comprobante.
customers:view_customersBuscar clientes para asignarlos a una factura.
customers:create_customerRegistrar clientes nuevos.
customers:edit_customerEditar clientes existentes, incluido el tipo de identificación.
customers:delete_customerDar de baja lógica un cliente.
inventory:view_productsConsultar el catálogo de productos y servicios.
inventory:create_productRegistrar productos y servicios nuevos.
inventory:edit_productEditar productos, categorías y atributos fiscales.
inventory:delete_productArchivar productos.
inventory:manage_stockRegistrar entradas, salidas, ajustes, mermas y conciliaciones.
product_categories:create_category / edit_category / delete_categoryAdministrar las categorías del catálogo.
suppliers:view / suppliers:manageConsultar y administrar proveedores: son los emisores de la 08 - Factura Electrónica de Compra (FEC).
invoices:transition_quotePasar una factura de draft a quoting.
invoices:transition_payPasar una factura de quoting a paid.
invoices:transition_cancelAnular una factura que todavía no se ha facturado. Una factura facturada se anula con una nota de crédito.
invoices:delete_invoiceBaja lógica de una factura en borrador.
purchases:viewListar y ver comprobantes de compra, sus comentarios y sus archivos.
purchases:uploadCargar comprobantes de compra en XML y generar el Mensaje del Receptor.
purchases:editEditar las notas de un comprobante de compra, sus comentarios y sus adjuntos.
purchases:deleteBaja lógica de un comprobante de compra.
purchases:sync_haciendaVolver a sincronizar un comprobante de compra o un MR con Hacienda.
purchases:view_reportVer el Libro de Compras y exportarlo a XLSX.
advances:view / create / edit / approve / collect / voidAdministrar los anticipos (RC y PP) que se aplican como pago de una factura.
reports:view_reportsVer los reportes de inventario. Es un permiso de administrador de organización.
organization:edit_organizationDar de alta y modificar las actividades económicas de la organización (CIIU4 del emisor).

El catálogo completo de permisos vive en GET /api/permissions y devuelve un objeto módulo → [acciones]. Consúltelo si necesita validar el rol con el que se creó la credencial.

Qué exige cada ruta en la práctica

El catálogo de permisos es la referencia oficial, pero no todas las rutas lo aplican de forma uniforme. Clientes, bodegas, categorías, terminales y las rutas de stock (/api/products/:id/movements, /api/waste, /api/inventory/reconcile) verifican únicamente que haya una credencial válida y una organización activa: no comprueban una acción concreta del rol. En cambio, /api/work-orders sí exige los permisos granulares work_orders:*.

Consecuencia práctica: un rol con permisos de invoices:* y customers:* opera el catálogo operativo completo, pero quien solo emite comprobantes y además administra inventario necesita que su rol incluya inventory:*. Y quien use el flujo de compras necesita purchases:*: sin ellos, POST /api/purchases/upload responde 403.

Rol mínimo recomendado

Un rol con invoices:*, customers:* e inventory:view_products cubre el flujo completo de emisión sin exponer la configuración de la organización. Si además administra inventario, añada inventory:manage_stock e inventory:edit_product.

07 Requisitos para emitir

Para emitir comprobantes, la organización debe tener la facturación electrónica habilitada. Esa habilitación la realiza un administrador del ERP: el servicio externo no participa de ella, solo necesita verificarla antes de trabajar.

Diagrama 3 Verificación de que la organización está lista
Su servicio arranca · reintenta · emite con el token del tenant GET /api/facturacion/ connection comprueba el estado facturacion Configured Emitir /api/invoices Escalar pedir al admin del ERP true false
La verificación es de solo lectura y no tiene efectos secundarios: se puede repetir en cada arranque del servicio.

Consultar el estado

GET /api/facturacion/connection
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/connection" \
  -H "Authorization: Bearer $TOKEN"
GET /api/facturacion/connection · respuesta 200
{
  "organizationId": "3b91c...",
  "haciendaConfigReady": true,
  "facturacionConfigured": true,
  "credentialsConfigured": true,
  "certificateConfigured": true,
  "environment": "sandbox",
  "clientId": "api-stag",
  "username": "usuario-idp@hacienda.go.cr",
  "certificate": {
    "issuedAt": "2026-01-15T00:00:00.000Z",
    "expiresAt": "2027-01-15T00:00:00.000Z",
    "isActive": true
  }
}
CampoPara qué le sirve al servicio
facturacionConfiguredtrue cuando la organización puede emitir. Es el campo que decide si el servicio arranca o escala.
haciendaConfigReadyLa conexión con Hacienda está completa.
certificate.isActivefalse cuando la habilitación venció. La emisión fallará hasta que se renueve.
environmentsandbox o production. Útil para no confundir una prueba con una emisión real.
Cuando la organización no está lista

Cualquier ruta de comprobantes responde 400 con { "message": "Facturación electrónica no configurada para esta organización", "code": "FACTURACION_NOT_CONFIGURED" }. El servicio debe traducir ese error a un mensaje accionable para su operador, no a un fallo genérico: la solución siempre está del lado del administrador del ERP.

Consecutivos y numeración

El servicio no gestiona los consecutivos: se llevan de forma automática y atómica por organización + sucursal + terminal + tipo de documento. Lo que el servicio sí necesita es poder consultarlos para reconciliar su propia numeración, y entender cómo se compone la clave que devuelve cada emisión.

GET /api/facturacion/consecutivos
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/consecutivos" \
  -H "Authorization: Bearer $TOKEN"
GET /api/facturacion/consecutivos · respuesta 200
{
  "data": [
    { "sucursalCodigo": "001", "terminalCodigo": "00001", "tipoDocumento": "01", "lastNumber": 42, "updatedAt": "2026-10-24T15:02:00.000Z" },
    { "sucursalCodigo": "001", "terminalCodigo": "00001", "tipoDocumento": "04", "lastNumber": 87, "updatedAt": "2026-10-24T15:02:00.000Z" }
  ]
}

El campo tipoDocumento lleva el código del tipo de comprobante, sin el nombre: 01 es la 01 - Factura Electrónica (FE) y 04 es el 04 - Tiquete Electrónico (TE). El catálogo completo está en 14 · Catálogos de Hacienda.

Es una consulta de solo lectura. El detalle del formato de la clave está en el diagrama 5.

08 Datos maestros para facturar

Antes de emitir hay que tener clientes, productos con su tratamiento fiscal y, cuando aplica, proveedores. Estos registros se cargan con la misma API. Su ciclo de vida completo —alta, edición, baja y los recursos de operación que los entouran: bodegas e inventario— está en la sección 09 · Administrar clientes e inventario.

8.1 · Clientes (receptores)

Los nombres de campo no son los del XSD

El modelo de cliente usa governmentId y governmentIdType, no taxId ni identificacion. El tipo de identificación es el catálogo de la Nota 4 del esquema v4.4 y admite los seis códigos: 01 - Cédula Física, 02 - Cédula Jurídica, 03 - DIMEX, 04 - NITE, 05 - Extranjero No Domiciliado y 06 - No Contribuyente. El valor por defecto es 01. El catálogo completo, con las restricciones de uso de cada código, está en 14.7 · Tipo de identificación.

POST /api/customers
curl -X POST "https://sandbox.vision-total.rogcr.com/api/customers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Corporación Wegner S.A.",
    "governmentId": "3109876543",
    "governmentIdType": "02",
    "email": "facturacion@wegner.cr",
    "phone": "+50627778899",
    "address": "Zona Industrial Heredia, bodega 12",
    "provinceCode": "2",
    "cantonCode": "01",
    "districtCode": "03",
    "economicActivities": ["4610.0", "4711.0", "7020.0"]
  }'
CampoReq.Reglas
nameSíNombre o razón social del receptor.
governmentIdSíIdentificación del receptor. Única por organización. Debe existir en el padrón de Hacienda: la API no lo comprueba al dar de alta, y el rechazo aparece al facturar. El formato depende del tipo: 01, 03 y 04 aceptan de 9 a 12 dígitos; 02 - Cédula Jurídica admite alfanuméricos de 9 a 30 caracteres; 05 - Extranjero No Domiciliado admite de 5 a 30 caracteres alfanuméricos con guiones y puntos; 06 - No Contribuyente admite de 9 a 20 alfanuméricos. Véase 8.6 · El padrón de contribuyentes.
governmentIdTypeNoCódigo del catálogo de la Nota 4: 01 · 02 · 03 · 04 · 05 · 06. Por defecto 01 - Cédula Física. Un código que no está en el catálogo no es un error: el backend lo normaliza a 01. Si se cambia en un PUT, la identificación almacenada se revalida contra el nuevo tipo. Véase 14.7 · Tipo de identificación.
emailSíCorreo válido, normalizado a minúsculas. Es el destinatario del comprobante por correo.
isForeignNoMarca al receptor como extranjero. Exigido para la 09 - Factura Electrónica de Exportación (FEE), que además exige dirección de al menos 5 caracteres.
economicActivitiesNoCódigos CIIU4 del cliente, con punto (6201.0, no 620100). Se usan en <CodigoActividadReceptor>. La fuente es el padrón de Hacienda: actividades[]. Véase 8.7 · Consultar las actividades económicas.
provinceCode / cantonCode / districtCodeNoUbicación geográfica del receptor (codificación INEC/Hacienda v4.4): provincia de 1 dígito, cantón y distrito de 2 dígitos. Van juntos o no van: una terna incompleta se rechaza con 400. Con la terna completa, el comprobante sale con el <Ubicacion> del receptor (tipos 01, 02, 03, 04 y 08); sin ella, el comprobante sale sin nodo de dirección. Un cliente con isForeign: true nunca lleva <Ubicacion>: su dirección va como <OtrasSenasExtranjero>. Véase 8.9 · La ubicación geográfica.
addressNoDirección del receptor. Con provinceCode/cantonCode/districtCode cargados es el <OtrasSenas> del <Ubicacion> y pasa a ser obligatoria (5 a 250 caracteres, el mínimo del XSD). Sin ubicación es texto libre y no viaja al XML.
phone / countryNoDatos de contacto del receptor. El teléfono debe tener la estructura que Hacienda espera, o el comprobante sale con una advertencia -20.
Un formato válido no es un contribuyente válido

La API valida el formato de la identificación contra governmentIdType, y nada más. Un número de 10 dígitos con prefijo 90, por ejemplo, se acepta al dar de alta el cliente y después Hacienda rechaza el comprobante con -38. Valide contra GET /api/facturacion/taxpayers/:identificacion antes de marcar a un cliente como facturable.

Listado: GET /api/customers devuelve un array plano sin paginación. Detalle: GET /api/customers/:id. Actualización: PUT /api/customers/:id. Baja lógica: DELETE /api/customers/:id (responde 204).

8.2 · Productos y servicios

POST /api/products
curl -X POST "https://sandbox.vision-total.rogcr.com/api/products" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Café molido 250 g",
    "sku": "CAF-250",
    "price": 2850.00,
    "costPrice": 1700.00,
    "stock": 120,
    "productType": "product",
    "cabys": "5897000000000",
    "codigoImpuesto": "01",
    "tarifaIvaCode": "08",
    "codigoMoneda": "CRC"
  }'
CampoSignificado fiscal
codigoImpuestoCatálogo CodigoImpuesto de la Nota 8: 01 IVA · 02 Selectivo al consumo · 03 Combustibles · 04 Bebidas alcohólicas · 05 Bebidas envasadas · 06 Tabaco · 07 IVA (cálculo especial) · 08 IVA bienes usados · 12 Cemento · 99 Otros. La descripción oficial de cada código y las reglas por código están en 14.3; los diez códigos se emiten.
tarifaIvaCodeCatálogo CodigoTarifaIVA: 08 - 13 % general · 02 - 1 % · 03 - 2 % · 04 - 4 % · 01 - 0 % art. 32 · 10 - Exenta · 11 - 0 % sin crédito.
cabysCódigo de artículo del CABYS (13 caracteres). Es el <CodigoCABYS> de la línea. Un producto sin CABYS real no se puede facturar: la API rechaza la factura con 400 PRODUCT_MISSING_CABYS en vez de dejarla avanzar con el marcador 0000000000000.
productTypesimple · product · composite · ingredient · service.
unitOfMeasureIdIdentificador de la fila del catálogo global GET /api/units-of-measure. En el detalle del producto viene como el objeto anidado unitOfMeasure, cuyo code es el valor que debe mandarse en unidadMedida de la línea. Ese código se consulta en la API, no se escribe a mano, y la API no lo copia solo a la línea: hay que enviarlo. Véase 8.5 · Unidad de medida.
taxDetailObligatorio para codigoImpuesto 03, 04, 05 y 06: exige cantidadUnidadMedida e impuestoUnidad. Para 07 admite factorBaseImponible; para 08, factorCalculoIVA; para 99, exige descripcionImpuesto (5 a 100 caracteres) y admite porcentaje.
GET /api/products/:id · la unidad del producto
{
  "id": "5b70...",
  "name": "Café molido 250 g",
  "cabys": "5897000000000",
  "unitOfMeasureId": "3a7e...",
  "unitOfMeasure": {
    "id": "3a7e...",
    "code": "Unid",
    "name": "Unidad",
    "symbol": "Unid",
    "baseUnitCode": null,
    "conversionFactor": "1.00000000"
  }
}
La unidad del producto no se copia a la línea

Que el producto declare su unidad no significa que la línea la herede: si unidadMedida se omite, el API escribe Unid en el XML, y un producto que se vende por peso queda facturado en «unidades». Hay que leer product.unitOfMeasure.code —que viene del catálogo de GET /api/units-of-measure— y mandarlo explícitamente. La guía lo explica en detalle en 8.5 · Unidad de medida.

La exoneración vive en la línea, no en el producto

Los campos exo* (tipo de documento de exoneración, número, artículo, inciso, institución, fecha) se envían por línea al crear la factura, no en el producto. Una exoneración puede válidamente aplicar solo a una línea de un comprobante.

8.3 · Proveedores (para la 08 - Factura Electrónica de Compra)

POST /api/suppliers
curl -X POST "https://sandbox.vision-total.rogcr.com/api/suppliers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "identificationType": "02",
    "identificationNumber": "3012345678",
    "name": "Distribuidora Nacional S.A.",
    "email": "ventas@distribuidora.cr",
    "isForeign": false,
    "provinceCode": "1",
    "cantonCode": "01",
    "districtCode": "01",
    "streetAddress": "Zona Industrial,-calle 4, nave 22",
    "economicActivities": ["4610.0"]
  }'

El proveedor es el emisor de una 08 - Factura Electrónica de Compra (FEC) y la organización es el receptor. El proveedor necesita al menos una actividad económica, y su dirección debe ser completa si es domicilado en Costa Rica. Listado: GET /api/suppliers (devuelve { success: true, data: [] }).

Los tres códigos de ubicación (provinceCode, cantonCode, districtCode) no se escriben a mano: salen del catálogo GET /api/locations/costa-rica. Véase 8.9 · La ubicación geográfica de Costa Rica.

El tipo de identificación del proveedor es el mismo catálogo

identificationType usa la Nota 4 del esquema v4.4: 01 - Cédula Física, 02 - Cédula Jurídica, 03 - DIMEX, 04 - NITE, 05 - Extranjero No Domiciliado y 06 - No Contribuyente. El tipo 05 es el que el emisor declara cuando el proveedor está en el exterior; el tipo 06 es el de la adquisición de bienes usados a un no contribuyente. Véase 14.7 · Tipo de identificación.

8.4 · Consulta de CABYS y de exoneraciones

GET /api/cabys?q=cafe&limit=20
curl -X GET "https://sandbox.vision-total.rogcr.com/api/cabys?q=cafe&limit=20" \
  -H "Authorization: Bearer $TOKEN"
GET /api/cabys?q=cafe&limit=20 · respuesta 200
{
  "total": 2,
  "items": [
    { "codigo": "5897000000000", "descripcion": "Café molido, sin azúcar" },
    { "codigo": "5897000000001", "descripcion": "Café en grano" }
  ]
}
ParámetroPor defectoNotas
q""Busca en el código y en la descripción, sin distinguir mayúsculas.
limit50Se acota entre 1 y 100.

El catálogo del CABYS es un archivo local del backend: no necesita conexión con Hacienda y no depende de la organización. Para consultar una ficha concreta por código: GET /api/cabys/:codigo, donde el código son exactamente 13 dígitos (si no, la API responde 400 con codigo must be 13 digits; si el código no existe, 404).

Para validar una autorización de exoneración contra el catálogo de Hacienda:

POST /api/hacienda/exoneraciones/lookup
curl -X POST "https://sandbox.vision-total.rogcr.com/api/hacienda/exoneraciones/lookup" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "autorizacion": "AL-00001234-01" }'
POST /api/hacienda/exoneraciones/lookup · respuesta 200
{
  "success": true,
  "data": {
    "numeroDocumento": "12-34",
    "tipoDocumento": "08",
    "nombreInstitucion": "02",
    "fechaEmision": "2026-01-01",
    "tarifaExonerada": 13,
    "cabys": "5897000000000",
    "poseeCabys": true,
    "institucionNombre": "Ministerio de Hacienda",
    "fechaVencimiento": "2027-12-31"
  }
}
El flujo completo de una exoneración por línea

Buscar el CABYS, mandar el VIN, calcular el monto exonerado y pagar el total correcto es un flujo de cuatro llamadas. Está documentado de punta a punta —con el body íntegro del POST /api/invoices, la matemática del <MontoExoneracion> y las tres verificaciones para confirmar que la exoneración quedó en el XML— en 10.5 · Caso completo: venta de vehículo eléctrico.

8.5 · Unidad de medida de la línea

La unidad de la línea viaja en unidadMedida y se escribe tal cual en <UnidadMedida> del XML. El API no lo valida: acepta cualquier cadena y devuelve 201. El valor correcto sale de una sola fuente: el catálogo que publica la propia API en GET /api/units-of-measure.

La API es la única fuente de verdad

El code que devuelve GET /api/units-of-measure es el valor que va en unidadMedida, tal cual: sin transformaciones y sin variantes. No debe mantenerse una lista propia de unidades en el servicio, ni constantes como "Unid" o "Kg" escritas a mano. Si el catálogo cambia, una lista propia queda desactualizada y el comprobante se rechaza entero con -1. Cuando la interfaz del servicio ofrezca unidades, las opciones son las filas de esa respuesta.

ReglaComportamiento real
El código se consulta unidadMedida se copia del code que devuelve GET /api/units-of-measure — el mismo valor que aparece en product.unitOfMeasure.code. No se escribe a mano.
Valor por defecto Unid. Si la línea omite unidadMedida, el XML lleva Unid y el comprobante es válido… pero la unidad puede ser incorrecta si el producto se mide en otra cosa.
No se hereda del producto Que el producto declare unitOfMeasureId no hace que la línea lo herede. El único camino es leer product.unitOfMeasure.code y mandarlo explícitamente.
Se escribe sin validar Cualquier valor llega al XML. Un valor fuera de la enumeración de Hacienda produce el rechazo -1 del comprobante completo.
Catálogo global El catálogo no depende de la organización: es el mismo para todos los tenants y puede cachearse en el servicio. Cambia muy rara vez.

Consultar el catálogo

GET /api/units-of-measure
curl -X GET "https://sandbox.vision-total.rogcr.com/api/units-of-measure" \
  -H "Authorization: Bearer $TOKEN"
GET /api/units-of-measure · respuesta 200 (extracto)
[
  {
    "id": "3a7e...",
    "code": "Unid",
    "name": "Unidad",
    "symbol": "Unid",
    "baseUnitCode": null,
    "conversionFactor": "1.00000000"
  },
  {
    "id": "8d26...",
    "code": "Kg",
    "name": "Kilogramo",
    "symbol": "Kg",
    "baseUnitCode": null,
    "conversionFactor": "1.00000000"
  },
  {
    "id": "c13e...",
    "code": "G",
    "name": "Gramo",
    "symbol": "G",
    "baseUnitCode": "kg",
    "conversionFactor": "0.00100000"
  }
]
CampoQué es
codeEl valor que debe mandarse en unidadMedida. Distingue mayúsculas, minúsculas y acentos: Kg no es kg. El symbol de la fila trae el mismo valor.
nameDescripción de la unidad, para mostrarla en pantalla.
idIdentificador de la fila: es el valor de unitOfMeasureId al crear o editar un producto.
baseUnitCode / conversionFactorUnidad base y factor de conversión. La facturación no los usa: la línea se manda siempre en la unidad del producto, sin convertir cantidades.

Los códigos que Hacienda acepta

La enumeración oficial para <UnidadMedida> es la Nota 15 del documento de estructuras v4.4 de la Dirección General de Tributación (estándar RTC 443:2010): 101 códigos. Es la lista contra la que el ATV valida la línea, y la referencia para leer de un vistazo lo que devuelve el catálogo.

CódigoDescripción
1uno (índice de refracción)
1/m1 por metro
AAmpere
A/mampere por metro
A/m²ampere por metro cuadrado
AcvActivo Virtual
AlAlquiler de uso habitacional
AlcAlquiler de uso comercial
Bbel
BqBecquerel
Ccoulomb
C/kgcoulomb por kilogramo
C/m²coulomb por metro cuadrado
C/m³coulomb por metro cúbico
CcCajuela de café
CdCandela
CmComisiones
Cucuartillos de café
Ddía
Ffarad
F/mfarad por metro
Fafanega de café
GGramo
GalGalón
Gygray
Gy/sgray por segundo
Hhenry
H/mhenry por metro
Hzhertz
IIntereses
JJoule
J/(kg·K)joule por kilogramo kelvin
J/(mol·K)joule por mol kelvin
J/Kjoule por kelvin
J/kgjoule por kilogramo
J/moljoule por mol
J/m³joule por metro cúbico
KKelvin
Katkatal
KgKilogramo
KmKilometro
Kwkilovatios
Llitro
Lmlumen
Lnpulgada
Lxlux
MMetro
Minminuto
MmMilímetro
MolMol
Nnewton
N/mnewton por metro
Npneper
N·mnewton metro
OsOtro tipo de servicio
OtrosSe debe indicar la descripción de la medida a utilizar
OzOnzas
Papascal
Pa·spascal segundo
QqQuintal
Radradián
SSegundo
SpServicios Profesionales
SpeServicios personales
Srestereorradián
StServicios técnicos
Svsievert
Ttonelada
Uunidad de masa atómica unificada
Uaunidad astronómica
UnidUnidad
Vvolt
V/mvolt por metro
WWatt
W/(m²·sr)watt por metro cuadrado estereorradián
W/(m·K)watt por metro kevin
W/m²watt por metro cuadrado
W/srwatt por estereorradián
Wbweber
cd/m²candela por metro cuadrado
cmcentímetro
eVelectronvolt
hhora
kWhKilovatios por hora
kat/m³katal por metro cúbico
kg/m³kilogramo por metro cúbico
m/smetro por segundo
m/s²metro por segundo cuadrado
mLmililitro
mol/m³mol por metro cúbico
m²metro cuadrado
m³metro cúbico
rad/sradián por segundo
rad/s²radián por segundo cuadrado
ssiemens
ttesla
°Cgrado Celsius
´minuto
´´segundo
ºgrado
Ωohm
Dónde se comprueba

Como el API no valida la unidad, la comprobación va en el cliente y su única referencia es la respuesta de GET /api/units-of-measure: se indexa por code y se contrasta con la enumeración de Hacienda antes de construir la línea. El catálogo es global y cambia muy rara vez, así que puede cachearse en memoria. Lo que no debe hacerse es sustituir esa consulta por una lista de códigos escrita a mano.

8.6 · El padrón de contribuyentes de Hacienda

La identificación del receptor no solo tiene que tener un formato válido: tiene que existir en el registro de contribuyentes de Hacienda. La API de clientes no comprueba eso —acepta identificadores de estructura plausible que no corresponden a nadie— y el rechazo aparece al emitir, con el código -38.

GET /api/facturacion/taxpayers/:identificacion
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/taxpayers/3109876543" \
  -H "Authorization: Bearer $TOKEN"
GET /api/facturacion/taxpayers/:identificacion · respuesta 200
{
  "nombre": "CORPORACIÓN WEGNER S.A.",
  "tipoIdentificacion": "02",
  "regimen": { "codigo": 1, "descripcion": "Régimen general" },
  "situacion": {
    "omiso": "NO",
    "estado": "Inscrito",
    "moroso": "NO",
    "administracionTributaria": "San José"
  },
  "actividades": [
    { "estado": "A", "tipo": "S", "codigo": "4610.0", "descripcion": "Venta al por mayor a cambio de una retribución o por contrato" },
    { "estado": "A", "tipo": "S", "codigo": "4711.0", "descripcion": "Venta al por menor en comercios no especializados" },
    { "estado": "A", "tipo": "S", "codigo": "7020.0", "descripcion": "Actividades de consultoría y administración de empresas" }
  ]
}
GET /api/facturacion/taxpayers/:identificacion · respuesta 404
HTTP 404
{ "message": "Taxpayer not found" }
RespuestaSignificadoQué hacer
200 El contribuyente existe en el padrón. Use nombre y actividades[] como fuente de verdad.
404 No existe en el padrón. La factura a ese receptor será rechazada con -38. Corrija la identificación antes de dejar al cliente como facturable.
400 La identificación no tiene 9 a 12 dígitos. Revise el formato. Las identificaciones jurídicas con guiones no pasan por este endpoint.
502 Hacienda no pudo responder a la consulta del padrón. Es un fallo de la conexión con Hacienda, no del dato: reintente más tarde.
«Desinscrito» no impide emitir

situacion.estado describe la situación del contribuyente frente a la administración tributaria y no bloquea la emisión: un contribuyente «Desinscrito» sigue pudiendo ser receptor de un comprobante aceptado. Lo que bloquea la emisión es no existir en el padrón, no su situación.

Tres usos de una sola llamada

FIX nombre y actividad del receptor, tomados del padrón
// El código de actividad sale del padrón, no de un campo escrito a mano.
const contribuyente = await consultarContribuyente(customer.governmentId)

if (!contribuyente.registrado) {
  // No se puede facturar a este receptor: la factura sería rechazada.
  throw new Error(`El contribuyente ${customer.governmentId} no está en el padrón de Hacienda`)
}

// El nombre que va al XML es el del padrón, no el que escribió el usuario.
const nombreReceptor = contribuyente.contribuyente?.nombre ?? customer.name

// La primera actividad activa sirve como CodigoActividadReceptor.
const actividades = contribuyente.contribuyente?.actividades?.filter((a) => a.estado === "A") ?? []
const actividad = actividades[0]
const actividadEconomicaReceptor = actividad?.codigo   // p. ej. "4610.0"

// El mismo catálogo se guarda en la ficha del cliente: el API exige que
// `actividadEconomicaReceptor` sea uno de los `customer.economicActivities`.
await api(`/customers/${customer.id}`, {
  method: "PUT",
  body: { economicActivities: actividades.map((a) => a.codigo) },
})
Por qué customer.economicActivities no basta

El campo economicActivities del cliente lo escribe el integrador a mano: si queda vacío, el comprobante sale sin <CodigoActividadReceptor>. El padrón es la fuente real de ese dato, y de paso corrige el nombre del receptor y valida la identificación en la misma llamada.

8.7 · Consultar las actividades económicas

El CIIU4 es el código de actividad económica que viaja en el XML, y aparece en tres lugares distintos: el emisor, el receptor y —en la 08 - Factura Electrónica de Compra (FEC)— el proveedor. Los tres se consultan por la misma API. Ninguno se inventa.

El código lleva punto

El CIIU4 se escribe con punto y con un máximo de 6 caracteres: 6201.0, 4711.1, 4610.0. Sin el punto, Hacienda rechaza el comprobante entero con CodigoActividad must have a . and be max 6 characters. El API no normaliza el valor: lo copia tal cual al XML, y por eso no hay forma de arreglar un código mal escrito desde el lado del servidor.

Las cuatro fuentes

Necesitás…Se consulta enPara qué
El CIIU del receptor (cliente) GET /api/facturacion/taxpayers/:identificacion actividades[] → customer.economicActivities[] y actividadEconomicaReceptor
El CIIU del emisor (tu organización) GET /api/organizations/:id/activities codigoActividadEmisor → <CodigoActividadEmisor>
El CIIU del proveedor (08 - Factura Electrónica de Compra (FEC)) GET /api/suppliers economicActivities[] → supplierActivityCode → <CodigoActividadEmisor> de la 08 - Factura Electrónica de Compra (FEC)
El catálogo completo (qué códigos existen) GET /api/economic-activities Validar el formato del código y resolver su descripción. No dice qué actividades tiene un contribuyente: para eso, el padrón.

El emisor: las actividades de tu organización

GET /api/organizations/:id/activities
curl -X GET "https://sandbox.vision-total.rogcr.com/api/organizations/$ORG_ID/activities" \
  -H "Authorization: Bearer $TOKEN"
GET /api/organizations/:id/activities · respuesta 200
[
  {
    "organizationId": "3b91c...",
    "codigo": "4711.1",
    "descripcion": "Venta al por menor en supermercados, almacenes y similares",
    "esPrincipal": true,
    "createdAt": "2026-10-01T12:00:00.000Z"
  },
  {
    "organizationId": "3b91c...",
    "codigo": "4711.2",
    "descripcion": "Venta al por menor en minisuper, pulperias y similares",
    "esPrincipal": false,
    "createdAt": "2026-10-01T12:00:00.000Z"
  }
]

Devuelve un array plano, ordenado por principal primero y luego por código. La fila con esPrincipal: true es la que el API usa como <CodigoActividadEmisor> cuando la factura no manda codigoActividadEmisor explícitamente. Dar de alta o modificar estas actividades es tarea del administrador del ERP (permiso organization:edit_organization); el servicio externo solo las consulta.

El receptor: el padrón de Hacienda

Ya está documentado en 8.6: la respuesta de GET /api/facturacion/taxpayers/:identificacion trae actividades[] con la forma { estado, tipo, codigo, descripcion }. Solo las que tienen estado: "A" son actividades activas.

FIX cargar las actividades del receptor sin escribirlas a mano
// 1 · El padrón: fuente de verdad del CIIU del receptor.
const padron = await consultarContribuyente("3109876543", token)
if (!padron.registrado) throw new Error("El contribuyente no está en el padrón de Hacienda")

const actividades = padron.contribuyente.actividades
  .filter((a) => a.estado === "A")
  .map((a) => a.codigo)          // p. ej. ["4610.0", "4711.0", "7020.0"]

// 2 · La ficha del cliente guarda ese mismo catálogo.
const customer = await api("/customers", {
  method: "POST",
  token,
  body: {
    name: padron.contribuyente.nombre,
    governmentId: "3109876543",
    governmentIdType: "02",
    email: "facturacion@wegner.cr",
    economicActivities: actividades,
  },
})

// 3 · La factura usa uno de los códigos ya registrados.
const invoice = await api("/invoices", {
  method: "POST",
  token,
  body: {
    documentType: "01",
    customerId: customer.id,
    actividadEconomicaReceptor: actividades[0],
    // …
  },
})

Qué valida el API y qué no

Dato¿Lo valida el API?Qué pasa si está mal
customer.economicActivities No Se guarda tal cual. El error aparece al emitir, no al dar de alta.
actividadEconomicaReceptor Sí Debe estar entre los customer.economicActivities; si no, 400 con «La actividad económica del receptor no está registrada en el cliente».
codigoActividadEmisor Sí Debe estar entre las actividades de la organización; si no, 400 con «La actividad económica "X" no está configurada para esta organización».
supplierActivityCode Sí Debe estar entre los supplier.economicActivities; si no, 400 con «El CIIU "X" seleccionado para esta FEC no está en el catálogo de actividades del proveedor».
El formato (con punto, ≤ 6 caracteres) No Nadie lo comprueba: el valor llega tal cual al XML y Hacienda rechaza el comprobante entero.
El catálogo completo y el padrón son cosas distintas

El catálogo completo de CIIU4 se publica en GET /api/economic-activities (véase 8.8): son los 368 códigos que existen y su descripción. Eso no dice qué actividades tiene un contribuyente concreto — para eso está el padrón. Los códigos que puede usar una organización son los que tiene registrados en GET /api/organizations/:id/activities, y los del proveedor, los que tiene en su ficha. Si necesita un código que no aparece en ninguno de esos tres lugares, hay que darlo de alta en el ERP (Configuración > Organización > Actividades, o la ficha del proveedor) antes de poder usarlo.

El 04 - Tiquete Electrónico (TE) no lleva actividad del receptor

El XSD v4.4 del Tiquete Electrónico no declara <CodigoActividadReceptor>. Si manda actividadEconomicaReceptor en una factura que se emite como 04 - Tiquete Electrónico (TE), el API lo descarta y el comprobante sale sin ese nodo. En la 08 - Factura Electrónica de Compra (FEC) ocurre lo contrario: los roles se invierten y el <CodigoActividadReceptor> lleva la actividad de su organización, no la del proveedor.

8.8 · El catálogo completo de actividades económicas

Los 368 códigos CIIU4 que el ERP conoce, con su descripción. Es la lista contra la que hay que contrastar un código antes de mandarlo a Hacienda: el formato es con punto y máximo 6 caracteres (6201.0, 4711.1, 0000.1).

GET /api/economic-activities
curl -X GET "https://sandbox.vision-total.rogcr.com/api/economic-activities" \
  -H "Authorization: Bearer $TOKEN"
GET /api/economic-activities · respuesta 200 (extracto)
[
  { "codigo": "0000.1", "descripcion": "Preoperativa" },
  { "codigo": "0114.0", "descripcion": "Cultivo de caña de azúcar" },
  { "codigo": "4610.0", "descripcion": "Venta al por mayor a cambio de una retribución o por contrato" },
  { "codigo": "4711.1", "descripcion": "Venta al por menor en supermercados, almacenes y similares" },
  { "codigo": "6201.0", "descripcion": "Actividades de programación informática" },
  { "codigo": "9900.0", "descripcion": "Actividades de organizaciones y órganos extraterritoriales" }
]
Cómo se usa esta lista

El catálogo es global: es el mismo para todos los tenants, no cambia y puede cachearse en memoria sin miedo a que se quede viejo. Indexe los códigos por codigo y úsela para validar antes de enviar: si el CIIU que va a salir en el XML no está en esta tabla, el comprobante se rechaza. Para el receptor la fuente de verdad sigue siendo el padrón (actividades[] de 8.6); esta lista sirve para validar el formato y resolver la descripción.

Código CIIU4Descripción
0000.1Preoperativa
0000.2Servicios digitales o de telecomunicaciones e intangibles prestados desde el exterior por proveedores
0000.3Servicios digitales o de telecomunicaciones e intangibles prestados desde el exterior por intermediarios
0111.9Cultivo de otros cereales, legumbres y semillas oleaginosas n.c.p
0113.9Cultivo de otras hortalizas, raíces o tubérculos n.c.p
0114.0Cultivo de caña de azúcar
0115.0Cultivo de tabaco
0116.0Cultivo de plantas de fibras
0119.1Cultivo de Flores
0122.1Cultivo de banano
0122.3Cultivo de piña
0122.9Cultivo de otras frutas tropicales y subtropicales
0125.0Cultivo de otros frutos y nueces de árboles y arbustos
0126.1Cultivo de palma africana (aceitera)
0127.1Cultivo de café
0127.9Cultivo de otras plantas con las que se preparan bebidas, excepto el cultivo de café n.c.p
0128.0Cultivo de especies de plantas aromáticas, medicinales y farmacéuticas
0130.0Propagación de plantas
0141.1Cría de ganado Bovino (vacuno) para carne
0141.2Cría de ganado Bovino (vacuno) para producción de leche cruda
0141.9Cría de Búfalos y otra producción de ganado bovino (vacuno)
0142.0Cría de caballos y otros equinos
0144.0Cría de ovejas y cabras
0145.0Cría de cerdos
0146.2Actividades de granja para la producción de huevos de gallina
0146.9Cría de otras aves de corral, excepto pollos
0149.0Cría de otros animales
0150.0Cultivo de productos agrícolas en combinación con la cría de animales (explotación mixta)
0161.2Servicios de fumigación y riego agrícola
0161.9Otros servicios de apoyo a la agricultura
0162.9Otras actividades de apoyo a la ganadería
0163.1Beneficio de café
0163.9Otras actividades posteriores a la cosecha
0170.0Caza ordinaria, mediante trampas y actividades de servicio conexas
0210.0Silvicultura y otras actividades forestales
0220.0Extracción de madera
0311.0Pesca marítima
0321.0Acuicultura marina
0510.0Extracción de carbón de piedra
0620.0Extracción de gas natural
0721.0Extracción de minerales metalíferos no ferrosos
0810.0Extracción de piedra, arena y arcilla
0891.0Extracción de minerales para la fabricación de abonos y productos químicos
0893.0Extracción de sal
0899.0Explotación de otras minas y canteras n.c.p
1010.1Elaboración y conservación de carne y embutidos de ganado vacuno y porcino
1010.9Elaboración y conservación de otro tipo de carnes n.c.p
1020.0Elaboración y conservación de pescados, crustáceos y moluscos
1030.0Elaboración y conservación de frutas, legumbres y hortalizas (vegetales)
1040.0Elaboración de aceites y grasas de origen vegetal y animal
1050.0Elaboración de productos lácteos
1061.1Beneficio de arroz
1061.9Elaboración de productos de molinería excepto arroz
1062.0Elaboración de almidones y productos derivados del almidón
1071.9Elaboración de otros productos de panadería, excepto elaboración de galletas
1072.0Elaboración de azúcar y otros productos derivados de la caña de azúcar
1073.0Elaboración de cacao, chocolate y productos de confitería
1074.0Elaboración de macarrones, fideos, alcuzcuz y productos farináceos similares
1079.9Elaboración de otros productos alimenticios n.c.p
1080.0Elaboración de alimentos preparados para animales
1101.0Destilación, rectificación y mezcla de bebidas alcohólicas
1103.0Elaboración de bebidas malteadas y de malta
1104.0Elaboración de bebidas no alcohólicas; producción de aguas minerales y otras aguas embotelladas
1200.0Elaboración de productos de tabaco
1312.0Tejedura de productos textiles
1313.0Acabado de productos textiles
1391.0Fabricación de tejidos de punto y ganchillo
1392.0Fabricación de artículos confeccionados con materiales textiles, excepto prendas de vestir
1393.0Fabricación de tapices y alfombras
1394.0Fabricación de cuerdas, cordeles, bramantes y redes
1410.9Fabricación de otras prendas de vestir, excepto ropa interior y prendas de piel
1511.0Curtido y adobo de cueros; adobo y teñido de pieles
1512.0Fabricación de maletas, bolsos de mano y de artículos similares, y artículos de talabartería o guarnicionería
1520.0Fabricación de calzado
1610.0Aserrado y acepilladura de madera
1621.0Fabricación de hojas de madera para enchapado y tableros a base de madera
1622.0Fabricación de partes y piezas de carpimtería para edificios y construcciones
1701.0Fabricación de pasta de madera , papel y cartón
1709.0Fabricación de otros artículos del papel y cartón
1811.0Impresión
1820.0Reproducción de grabaciones
1920.0Fabricación de los productos de la refinación del petróleo
2011.0Fabricación de sustancias químicas básicas
2012.0Fabricación abonos y compuestos de nitrógeno
2013.0Fabricación de plásticos y de caucho sintético en formas primarias
2021.0Fabricación de plaguicidas y de otros productos químicos de uso agropecuario
2022.0Fabricación de pinturas, barnices y productos de revestimiento similares, tintas de imprenta y masillas
2023.1Fabricación de jabones y detergentes, preparados para limpiar y pulir
2023.2Fabricación de perfumes y preparados de tocador
2029.0Fabricación de otros productos químicos n.c.p
2030.0Fabricación de fibras artificiales
2100.0Fabricación de productos farmacéuticos, sustancias químicas medicinales y de productos botánicos de uso farmacéutico
2211.0Fabricación de cubiertas y cámaras de caucho; recauchutado y renovación de cubiertas de caucho
2219.0Fabricación de otros productos de caucho
2220.9Fabricación de otros productos de plástico
2310.0Fabricación de vidrio y de productos de vidrio
2391.0Fabricación de productos refractarios
2392.0Fabricación material de arcilla para la construcción
2393.0Fabricación de otros productos de cerámica y porcelana
2394.0Fabricación de cemento, de la cal y del yeso
2395.0Fabricación de artículos de hormigón, de cemento y de yeso
2396.0Corte, tallado y acabado de la piedra
2399.0Fabricación de otros productos minerales no metálicos n.c.p
2410.0Industrias básicas de hierro y acero
2420.0Fabricación de productos primarios de metales preciosos y metales no ferrosos
2431.0Fundición de hierro y acero
2432.0Fundición de metales no ferrosos
2511.0Fabricación de productos metálicos para uso estructural
2512.0Fabricación de tanques, depósitos y recipientes de metal
2513.0Fabricación de los generadores del vapor, excepto calderas de agua caliente para calefacción central
2520.0Fabricación de armas y municiones
2592.0Tratamiento y revestimiento de metales
2599.0Fabricación de otros productos de metal n.c.p
2610.0Fabricación de componentes y tableros electrónicos
2630.0Fabricación de equipos de comunicaciones
2651.0Fabricación de equipos de medición, prueba, verificación, navegación y de equipo de control
2652.0Fabricación de relojes
2670.0Fabricación de instrumentos ópticos de equipo fotográfico
2710.0Fabricación de motores eléctricos, generadores, transformadores eléctricos y aparatos de distribución y control de la energía eléctrica
2720.0Fabricación de baterias y acumuladores
2732.0Fabricación de otros hilos y cables eléctricos
2740.0Fabricación de equipos de iluminación eléctricos
2750.0Fabricación de aparatos de uso doméstico
2790.0Fabricación de otro equipo eléctrico
2811.0Fabricación de motores y turbinas, excepto motores para aeronaves, vehículos automotores y motocicletas
2813.0Fabricación de otras bombas, compresores, grifos y válvulas
2815.0Fabricación de hornos, hogueras y quemadores
2817.0Fabricación de maquinaria y equipo de oficina (excepto computadoras y equipos periféricos)
2821.0Fabricación de maquinaria agropecuaria y forestal
2822.0Fabricación de maquinarias para la conformación de metales y de máquinas herramienstas
2823.0Fabricación de maquinaria para metalúrgica
2824.0Fabricación de maquinaria para explotación de minas y canteras y para obras de construcción
2825.0Fabricación de maquinaria para la elaboración de alimentos, bebidas y tabaco
2826.0Fabricación de maquinaria para la elaboración de productos textiles, prendas de vestir y cueros
2829.0Fabricación de otros tipos de maquinaria de uso especial
2910.0Fabricación de vehículos automotores
2920.0Fabricación de carrocerias para vehículos automotores; fabricación de remolques y semiremolques
2930.0Fabricación de partes y accesorios para motores de vehículos
3012.0Construcción de embarcaciones de recreo y deporte
3020.0Fabricación de locomotoras y de material rodante
3030.0Fabricación de aeronaves y naves espaciales y de maquinaria conexa
3091.0Fabricación de motocicletas
3092.0Fabricación de bicicletas y sillas de ruedas para personas con discapacidad
3099.0Fabricación de otros tipos de equipo de transporte n.c.p
3100.1Fabricación de muebles de madera
3100.9Fabricación de muebles de otro tipo de material (excepto de madera, piedra, cemento o cerámica)
3211.0Fabricación de joyas y artículos conexos
3212.0Fabricación de bisutería y artículos conexos
3220.0Fabricación de Instrumentos musicales
3230.0Fabricación de artículos de deporte
3240.0Fabricación de juegos y juguetes
3250.0Fabricación de instrumentos y suministros médicos y odontológicos
3290.0Otras industrias manufactureras n.c.p
3311.0Reparación de productos elaborados de metal
3312.0Reparación de maquinaria
3314.0Reparación de equipo eléctrico
3315.0Reparación de equipo de transporte, excepto los vehículos automotores
3510.0Generación , transmisión y distribución de energía eléctrica
3520.0Fabricación de gas; distribución de combustibles gaseosos por tuberías
3530.0Suministro de vapor y aire acondicionado
3600.0Captación, tratamiento y distribución de agua
3700.0Evacuación de aguas residuales
3811.0Recolección de desechos no peligrosos
3830.0Recuperación de materiales
4100.0Construcción de edificios
4210.0Construcción de carreteras y vías férreas
4311.0Demolición
4312.0Preparación del terreno
4321.0Instalación eléctrica y de telecomunicaciones
4322.0Instalación de fontanería e instalación de calefacción y aire acondicionado
4329.0Otro tipo de instalaciones de construcción
4330.0Terminación y acabados de edificios
4390.0Otras actividades especializadas de la construcción
4510.0Venta de vehículos automotores
4520.0Mantenimiento y reparación de vehículos automotores
4530.0Venta de partes, piezas y accesorios de vehículos automotores
4540.0Venta, mantenimiento y reparación de motocicletas y de sus partes, piezas y accesorios
4610.0Venta al por mayor a cambio de una retribución o por contrato
4620.0Venta al por mayor de materias primas agropecuarias y animales vivos
4630.3Venta al por mayor de frutas y verduras frescas
4630.4Venta al por mayor de licores, bebidas y tabaco
4630.9Venta al por mayor de otros alimentos
4641.1Venta al por mayor de productos textiles y prendas de vestir
4649.1Venta al por mayor de aparatos, artículos y equipo de uso doméstico
4649.2Venta al por mayor de productos farmacéuticos, veterinarios y artículos de tocador
4649.3Venta al por mayor de juegos, juguetes, artículos deportivos y de esparcimiento
4649.9Venta al por mayor de otros enseres domésticos n.c.p
4651.0Venta al por mayor de computadoras, equipo informático periférico y programas informáticos
4652.0Venta al por mayor de equipo, partes y piezas electrónicas y de telecomunicaciones
4653.0Venta al por mayor de maquinaria, equipo y materiales agropecuarios
4659.0Venta al por mayor de otro tipo de maquinaria y equipo
4661.0Venta al por mayor de combustibles sólidos, líquidos y gaseosos y de productos conexos
4662.0Venta al por mayor de metales y de minerales metalíferos
4663.0Venta al por mayor de materiales de construcción, articulos de ferretería y equipo y materiales de fontanería y calefacción
4669.1Venta al por mayor de fertilizantes y productos agroquímicos
4669.9Venta al por mayor de basura, desechos y otros productos n.c.p
4690.0Venta al por mayor de otros productos no especializada
4711.1Venta al por menor en supermercados, almacenes y similares
4711.2Venta al por menor en minisuper, pulperias y similares
4719.1Venta al por menor en tiendas por departamentos
4719.9Venta al por menor en bazares y otros establecimientos no especializados n.c.p
4721.1Venta al por menor de carne de bovino, porcino y sus derivados
4721.3Venta al por menor de pescados y mariscos
4721.4Venta al por menor de frutas y verduras frescas
4721.9Venta al por menor de otros alimentos
4722.0Venta al por menor de bebidas en comercios especializados
4723.0Venta al por menor de tabaco en comercios especializados
4730.0Venta al por menor de combustibles para vehículos automotores en comercios especializados
4741.0Venta al por menor de computadoras, equipo periféricos, programas de información y telecomunicaciones en comercios especializados
4742.0Venta al por menor de equipo de audio y video en comercios especializados
4751.0Venta al por menor de productos textiles en comercios especializados
4752.1Venta al por menor de artículos de ferretería y pinturas en comercios especializados
4752.2Venta al por menor de productos de vidrio en comercios especializados
4753.0Venta al por menor de tapices, alfombras, cubrimientos para paredes y pisos en comercios especializados
4759.0Venta al por menor de aparatos eléctricos de uso doméstico, muebles, equipos de iluminación y otros enseres domésticos en comercios especializados
4761.0Venta al por menor de libros, periódicos y artículos de papelería en comercios especializados
4762.0Venta al por menor de grabaciones musicales y videográficas en comercios especializados
4763.0Venta al por menor de artículos de deporte en comercios especializados
4764.0Venta al por menor de juegos y de juguetes en almacenes especializados
4771.1Venta al por menor de prendas de vestir y artículos de cuero en almacenes especializados
4771.2Venta al por menor de zapatos en comercios especializados
4772.0Venta al por menor de productos farmacéuticos y medicinales, cosméticos y artículos de tocador en comercios especializados
4773.0Otra venta al por menor de otros productos nuevos en comercios especializados
4774.0Venta al por menor de artículos de segunda mano
4781.0Venta al por menor en puestos de venta y mercados de: alimentos, bebidas y de productos del tabaco
4799.0Otros tipos de venta al por menor no realizada en comercios, puestos de venta o mercados
4911.0Transporte de pasajeros por ferrocarril
4921.0Transporte terrestre de pasajeros del área urbana, suburbana por vía terrestre
4922.1Transporte terrestre de estudiantes y de empleados
4922.2Servicios de taxi
4922.3Transporte terrestre no regular de pasajeros, en buses
4923.1Transporte terrestre local e internacional de contenedores
4923.9Otro transporte de carga n.c.p
4930.0Transporte por tuberías
5012.0Transporte marítimo y de cabotaje de carga
5110.0Transporte de pasajeros por vía aérea
5120.0Transporte de carga por vía aérea
5210.0Almacenamiento y depósito
5221.1Remolque y asistencia para vehículo destinado al transporte terrestre
5221.2Servicios de estacionamientos y garajes
5221.9Servicios de administración de carreteras, puentes y otros servicios auxiliares
5223.0Actividades de servicio secundario de transporte por vía aérea
5224.0Manipulación de carga
5229.0Otras actividades de apoyo al transporte
5310.0Servicios postales (Correo)
5510.1Actividades de alojamiento para estancias cortas en hoteles y moteles
5510.9Otras actividades de alojamiento para estancias cortas n.c.p
5590.0Otras actividades de alojamientos
5610.0Actividades de restaurantes y de servicios móviles de comidas
5621.0Suministros de comidas por encargo
5630.0Actividades de servicio de bebidas
5811.0Edición de libros
5813.0Edición de periódicos, diarios y revistas
5911.0Actividades de producción de películas cinematográficas, videos y programas de televisión
5914.0Actividades de proyección de películas
5920.0Actividades de grabación y publicación de grabaciones sonoras
6010.0Transmisiones de radio
6020.0Programación y transmisión de televisión
6110.0Actividades de telecomunicaciones alámbricas
6120.0Actividades de telecomunicaciones inalámbricas
6190.1Servicios de café internet
6190.9Otras actividades de telecomunicación
6201.0Actividades de programación informática
6202.0Actividades de consultoría informática y gestión de instalaciones informáticas
6209.0Otras actividades de la tecnología de información y servicio informáticos
6311.0Procesamiento de datos, hospedaje y actividades conexas
6312.0Portal Web
6391.0Actividades de agencias de noticias
6411.0Banca Central
6419.0Otros tipos de intermediación monetaria
6420.0Actividades de sociedades de cartera
6430.0Fondo y sociedades de Inversión y entidades financieras similares
6491.0Arriendamiento financiero
6492.0Otros tipos de crédito
6499.0Otras actividades de servicios financiero, excepto las de seguros y fondos de pensiones n.c.p
6511.0Seguros de vida
6512.0Seguros generales
6530.0Fondos de pensión
6611.0Administración de mercados financieros
6612.0Corretaje de valores y de contratos de productos básicos
6619.0Otras actividades auxiliares a las actividades de servicios financieros
6622.0Actividades de agentes y corredores de seguro
6629.0Otras actividades auxiliares de seguros y fondos de pensiones
6810.0Actividades inmobiliarias con bienes propios o arrendados
6820.0Actividades inmobiliarias realizadas a cambio de una retribución o por contrata
6910.0Actividades jurídicas
6920.0Actividades de contabilidad, teneduría de libros y auditorías; consultoría fiscal
7010.0Actividades de oficinas principales
7020.0Actividades de consultoría en gestión
7110.0Actividades de arquitectura e ingeniería y actividades conexas de consultoría técnica
7120.0Ensayos y análisis técnicos
7210.0Investigaciones y desarrollo experimental en el campo de las ciencias naturales y la ingeniería
7220.0Investigación y desarrollo experimental en el campo de las ciencias sociales y las humanidades
7310.0Publicidad
7320.0Estudios de mercados y encuestas de opinión pública
7410.0Actividades especializadas de diseño
7420.0Actividades de fotografía
7490.0Otras actividades profesionales, científicas y técnicas n.c.p.
7500.0Actividades veterinarias
7710.1Alquiler de automóviles sin conductor
7721.0Alquiler y arrendamiento de equipo recreativo y deportivo
7722.0Alquiler de cintas de vídeo y discos
7729.0Alquiler y arrendamiento de otros efectos personales y enseres domésticos
7730.0Alquiler y arrendamiento de otros tipo de maquinaria, equipo y bienes tangibles
7740.0Arrendamiento de propiedad intelectual y productos similares excepto obras protegidas por derechos de autor
7830.0Otras actividades de dotación de recursos humanos
7911.0Actividades de agencias de viajes
7912.0Actividades de operadores turísticos
8010.0Actividades de seguridad privada
8110.0Actividades combinadas de apoyo a instalaciones
8121.0Limpieza general de edificios
8129.0Otras actividades de limpieza de edificios e instalaciones industriales
8130.0Actividades de paisajismo y servicios de mantenimiento conexos
8219.0Fotocopiado, preparación de documentos y otras actividades especializadas de apoyo de oficina
8220.0Actividades de centros de llamadas
8291.0Actividades de agencias de cobro y agencias de calificación crediticia
8292.0Actividades de envasado y empacado
8299.0Otras actividades de servicios de apoyo a las empresas n.c.p
8411.0Actividades de la administración pública en general
8413.0Regulación y facilitación de la actividad económica
8421.0Relaciones exteriores
8423.0Actividades de mantenimiento del orden público y de seguridad
8430.0Actividades de planes de seguridad social de afiliación obligatoria
8510.0Enseñanza preescolar y primaria
8521.0Enseñanza secundaria de formación general
8522.0Enseñanza de formación técnica y profesional
8530.1Enseñanza universitaria
8530.2Enseñanza parauniversitaria
8541.0Educación deportiva y recreativa
8542.0Educación cultural
8549.0Otros tipos de enseñanza n.c.p
8550.0Actividades de apoyo a la enseñanza
8610.0Actividades de hospitales
8620.1Actividades de médicos
8620.2Actividades de odontólogos
8690.1Laboratorios médicos y de diagnóstico
8690.9Otras actividades relacionadas con la salud humana
8720.0Actividades de atención en instituciones para personas con retraso mental, enfermos mentales y toxicómanos
8790.0Otras actividades de atención en instituciones n.c.p
8810.0Asistencia social sin alojamiento para el adulto mayor y personas con discapacidad
8890.0Otras actividades de asistencia social sin alojamiento n.c.p
9000.0Actividades creativas, artísticas y de entretenimiento
9101.0Actividades de Bibliotecas y archivos
9102.0Actividades de museos y preservación de lugares históricos
9103.0Actividades de jardines botánicos, zoológicos y parques naturales
9200.0Actividades de juego de azar y apuestas
9311.0Gestión de instalaciones deportivas
9319.0Otras actividades deportivas
9329.0Otras actividades de esparcimiento y recreativas n.c.p
9411.0Actividades de Asociaciones empresariales y de empleadores
9412.0Actividades de asociaciones profesionales
9420.0Actividades de sindicatos
9491.0Actividades de organizaciones religiosas
9492.0Actividades de organizaciones políticas
9499.0Actividades de otras asociaciones n.c.p
9511.0Reparación de computadoras y equipo periférico
9512.0Reparación de equipos de comunicaciones
9521.0Reparación de aparatos electrónicos de consumo eléctrico
9522.0Reparación de aparatos de uso doméstico y equipo doméstico y de jardinería
9523.0Reparación de calzado y artículos de cuero
9524.0Reparación de muebles y accesorios domésticos
9529.0Reparación de otros bienes personales y enseres domésticos n.c.p
9601.0Lavado y limpieza, incluida la limpieza en seco, de productos textiles y piel
9602.0Actividades de peluquería y otros tratamientos de belleza
9603.0Funerales y actividades conexas
9609.0Otras actividades de servicios personales n.c.p
9700.0Actividades de los hogares como empleadores de personal doméstico
9900.0Actividades de organizaciones y órganos extraterritoriales

8.9 · La ubicación geográfica de Costa Rica

La <Ubicacion> del XML —provincia, cantón y distrito— no admite texto libre: exige los códigos oficiales de la «Codificación de Ubicación v4.4» del INEC y de Hacienda. Esos códigos no se escriben a mano ni se inventan: se consultan en un catálogo que publica la propia API.

GET /api/locations/costa-rica
curl -X GET "https://sandbox.vision-total.rogcr.com/api/locations/costa-rica"
GET /api/locations/costa-rica · respuesta 200 (extracto)
{
  "pais": "Costa Rica",
  "provincias": [
    {
      "codigo": "1",
      "nombre": "San José",
      "cantones": [
        {
          "codigo": "01",
          "nombre": "San José",
          "distritos": [
            { "codigo": "01", "nombre": "Carmen" },
            { "codigo": "02", "nombre": "Merced" }
          ]
        },
        {
          "codigo": "02",
          "nombre": "Escazú",
          "distritos": [
            { "codigo": "01", "nombre": "Escazú" },
            { "codigo": "02", "nombre": "San Antonio" }
          ]
        }
      ]
    }
  ]
}
Una ruta pública, sin token

Este endpoint no exige autenticación: es un catálogo público de referencia, no depende de la organización ni toca datos de negocio, y el mismo catálogo sirve para todos los tenants. Enviar el Authorization: Bearer tampoco molesta. Se puede llamar antes de iniciar sesión.

Cobertura del catálogo

CódigoProvinciaCantonesDistritos
1San José20123
2Alajuela16116
3Cartago853
4Heredia1048
5Guanacaste1161
6Puntarenas1361
7Limón630

Son 7 provincias, 84 cantones y 492 distritos, en unos 23 KB de JSON. La respuesta completa trae todas las provincias; el ejemplo anterior es un extracto.

El formato de los códigos

NivelCampoFormatoEjemplo
Provincia codigo Un dígito (1–7), sin ceros a la izquierda. 1
Cantón codigo Dos dígitos, con cero a la izquierda. 01
Distrito codigo Dos dígitos, con cero a la izquierda. 01
La provincia es de un solo dígito

"1", no "01". El XSD v4.4 tipa <Provincia> como un entero positivo de un dígito, y el API valida el formato del proveedor con /^\d$/: un "01" se rechaza con 400 al dar de alta el proveedor. Los cantones y los distritos sí llevan los dos dígitos.

Dónde se usan estos códigos

DatoCampoDónde se configura
Ubicación de la organización (el emisor) provinciaCodigo · cantonCodigo · distritoCodigo Por el administrador del ERP, en Configuración > Organización. Vienen en GET /api/organizations/:id. Si la organización no los tiene, el API no emite: la ubicación del emisor es obligatoria en el XML.
Ubicación del proveedor (emisor de la 08 - Factura Electrónica de Compra (FEC)) provinceCode · cantonCode · districtCode Por el servicio, en POST /api/suppliers y PATCH /api/suppliers/:id. Obligatoria si el proveedor es domiciliado en Costa Rica (isForeign: false).
Ubicación del cliente (receptor) provinceCode · cantonCode · districtCode Por el servicio, en POST /api/customers y PUT /api/customers/:id. Opcional, pero los tres van juntos o no van: con la terna completa el comprobante sale con el <Ubicacion> del receptor; sin ella, sale sin nodo de dirección.

En la ficha del proveedor, esos tres códigos van juntos con la dirección en texto libre (streetAddress), que es el <OtrasSenas> del XML. Los tres se validan contra el formato del XSD, no contra el catálogo.

El API valida el formato, no la combinación

Una terna que no exista en el catálogo pasa la validación y se guarda: la provincia 2 - Alajuela con el cantón 01 - San José —que es San José— y el distrito 01 - Carmen son un ejemplo. El API solo exige que la provincia sea de un dígito y el cantón y el distrito de dos; que la combinación exista es cosa del fisco, que valida el <Ubicacion> contra la codificación oficial al revisar el comprobante.

La consecuencia práctica es la misma que con el padrón de contribuyentes: el problema no aparece en la llamada, aparece en el estado del comprobante. Consulte el catálogo y arme la terna de la jerarquía, no al revés. Véase 11 · Seguimiento para el sondeo.

FIX resolver los nombres desde los códigos
// El catálogo es global y estático: se consulta una vez y se cachea.
const ubicaciones = await fetch(`${BASE}/locations/costa-rica`).then((r) => r.json());

// Índices para resolver en cascada sin recorrer el árbol en cada render.
const porProvincia = new Map(ubicaciones.provincias.map((p) => [p.codigo, p]));
const cantonesDe = (provincia) => porProvincia.get(provincia)?.cantones ?? [];
const distritosDe = (provincia, canton) =>
  cantonesDe(provincia).find((c) => c.codigo === canton)?.distritos ?? [];

// El nombre legible sale del catálogo, no de una copia local.
const nombreUbicacion = (provincia, canton, distrito) => {
  const p = porProvincia.get(provincia);
  const c = p?.cantones.find((x) => x.codigo === canton);
  const d = c?.distritos.find((x) => x.codigo === distrito);
  return [p?.nombre, c?.nombre, d?.nombre].filter(Boolean).join(", ");
};

// Se manda el código tal cual: sin padding en la provincia.
const body = {
  provinceCode: "1",     // San José
  cantonCode: "01",      // San José
  districtCode: "01",    // Carmen
  streetAddress: "Zona Industrial, calle 4, nave 22",
};
Un solo catálogo, sin copias locales

La codificación oficial vive en el ERP y se publica por API. Un servicio que mantiene su propia lista de provincias y cantones se desincroniza en cuanto el INEC publica una corrección, y el síntoma aparece meses después: una 08 - Factura Electrónica de Compra (FEC) rechazada por Hacienda porque el cantón que el servicio mandó ya no existe. Consulte el catálogo y guarde los códigos, no los nombres.

Cachear el catálogo

ReglaComportamiento
Catálogo global No depende de la organización: es el mismo para todos los tenants y puede cachearse en el servicio sin riesgo de mezclar datos entre clientes.
Cambia muy rara vez Es la codificación del INEC, revisada por años. Con una caché de horas o de un día basta; no hace falta pedirlo en cada arranque.
Sin paginación La respuesta trae el árbol completo. No hay page ni limit: no tiene sentido pedir una parte.
Sin autenticación Se puede leer antes de tener token, lo que permite pintar los selects de ubicación en una pantalla de alta sin sesión previa.

8.10 · Tipo de cambio

Cuando la factura no está en colones (codigoMoneda distinto de CRC), el XML exige un TipoCambio mayor que cero. El tipo de cambio del día se consulta en la API: no lo escriba a mano ni lo calcule.

GET /api/exchange-rates/USD
curl -X GET "https://sandbox.vision-total.rogcr.com/api/exchange-rates/USD" \
  -H "Authorization: Bearer $TOKEN"
GET /api/exchange-rates/USD · respuesta 200
{
  "data": {
    "currency": "USD",
    "rate": 512.35,
    "fetchedAt": "2026-11-05T08:00:00.000Z"
  }
}
Sólo USD y EUR

El parámetro currency admite únicamente USD y EUR (el backend normaliza las mayúsculas). Cualquier otra moneda responde 400 con currency debe ser USD o EUR. Si Hacienda no puede responder, la API devuelve 502 con No se pudo obtener el tipo de cambio.

09 Administrar clientes e inventario

La facturación no trabaja sola: necesita un padrón de clientes y un catálogo de productos con su tratamiento fiscal y sus existencias. Los dos se administran por la misma API pública, con el mismo token y dentro de la organización activa.

Clientes

Alta, consulta, edición y baja lógica. El documento de identificación es único por organización y se revalida contra el tipo de persona.

Bodegas

El origen físico de las existencias. Una sola bodega predeterminada por organización; es la que recibe el stock inicial y los movimientos que no indican bodega.

Productos

Alta y edición del catálogo, con su tratamiento fiscal y su saldo disponible calculado.

Movimientos

Entradas, salidas, ajustes, mermas y conciliación de saldos. Todo el stock se mueve por aquí, nunca se escribe a mano.

Un solo token para todo

Estas rutas no tienen su propio mecanismo de autenticación: usan exactamente el mismo Authorization: Bearer y la misma organización activa que la emisión. Un servicio que factura puede, con el mismo token, mantener el padrón de clientes y el catálogo operativo.

A quién aplica el bloque de inventario

Lo que sigue está pensado para organizaciones comerciales (retail y finance): un catálogo de productos que se compra, se vende y se controla por existencias. En organizaciones de tipo restaurant la operación de bodega se apoya en recetas y producción de lotes, y esa superficie no se documenta aquí: es otra dinámica y no interviene en la emisión de comprobantes.

9.1 · Clientes: ciclo completo

Los campos fiscales del receptor (governmentId, governmentIdType, email, isForeign y economicActivities) están documentados en 8.1 · Clientes (receptores). Aquí está el ciclo completo de administración del padrón.

MétodoRutaQué haceRespuesta
POST/api/customersAlta de cliente.201 con el cliente.
GET/api/customersLista los clientes activos.Array plano, sin paginación.
GET/api/customers/:idDetalle de un cliente.Objeto cliente.
PUT/api/customers/:idEdición: solo se aplica lo que viene en el cuerpo.Objeto cliente actualizado.
DELETE/api/customers/:idBaja lógica.204 sin cuerpo.
PUT /api/customers/:id
curl -X PUT "https://sandbox.vision-total.rogcr.com/api/customers/$CUSTOMER_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+50689998877",
    "address": "Zona Industrial Heredia, bodega 12"
  }'
HTTPmessageCausa
400name is requiredFalta el nombre del cliente.
400El documento es obligatorio y no cumple el formato para el tipo de identificación seleccionado.La identificación no valida contra governmentIdType.
400El correo electrónico es obligatorio y debe tener un formato válido.Falta el correo o no es válido.
400La ubicación del cliente se completa de una vez: elegí provincia, cantón y distrito, o dejalos vacíos para usar solo la dirección.Se envió sólo parte de la terna provinceCode/cantonCode/districtCode.
400La provincia debe ser un código de 1 dígito (1 a 7).El formato del código de provincia no es el del XSD v4.4.
409Ya existe un cliente con ese documento en tu organización.La identificación ya está registrada.
404Customer not foundEl id no existe o pertenece a otra organización.
Cambiar el tipo de identificación revalida el documento

En un PUT, si cambia governmentIdType pero no governmentId, el API revalida el documento almacenado contra el nuevo tipo y responde 400 si ya no calza. Envíe siempre ambos campos juntos. Del mismo modo, una baja lógica no elimina las facturas históricas del cliente: solo lo esconde de los listados.

9.2 · Bodegas

Las bodegas son el origen físico del inventario. Cada producto tiene un saldo por bodega y todos los movimientos de stock apuntan a una de ellas.

POST /api/warehouses
curl -X POST "https://sandbox.vision-total.rogcr.com/api/warehouses" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Bodega central", "isDefault": true }'
MétodoRutaNotas
GET/api/warehousesArray plano.
GET/api/warehouses/:idDetalle de una bodega.
GET/api/warehouses/:id/balancesSaldos de la bodega: array de { productId, warehouseId, quantity }.
POST/api/warehousesname es obligatorio.
PUT/api/warehouses/:idEdita name e isDefault.
DELETE/api/warehouses/:idBaja lógica. Responde 204.
Una sola bodega predeterminada

Marcar una bodega con isDefault: true degrada automáticamente a false la que era predeterminada. La bodega predeterminada es la que se usa cuando un movimiento no indica warehouseId; sin ella, la API responde 400 con No hay bodega predeterminada configurada para la organización.

9.3 · Productos: alta y edición

El catálogo de productos guarda a la vez el dato fiscal (lo que va al XML de Hacienda) y el dato operativo (stock y unidad de medida). La sección 8.2 · Productos y servicios explica el significado fiscal de codigoImpuesto, tarifaIvaCode y cabys; aquí está el ciclo de vida administrativo.

MétodoRutaNotas
GET/api/productsArray plano, sin paginación. Filtros: ?sellableOnly=true y ?categoryIds= (uno o varios).
GET/api/products/:idDetalle con los saldos calculados.
POST/api/productsAlta de producto o servicio.
PUT/api/products/:idEdición: los campos omitidos conservan su valor.
DELETE/api/products/:idBaja lógica. Responde 204.
GET/api/categoriesArray plano, con productsCount en cada fila. Filtros: ?search= y ?withProducts=true.
GET/api/categories/:idDetalle, con el arreglo products completo.
POST / PUT / DELETE/api/categoriesAlta (name obligatorio), edición y baja. El DELETE responde 200 con { message }, no 204. Un nombre repetido en la organización responde 409.
Los DELETE no siempre responden 204

La mayoría de las bajas lógicas responden 204 sin cuerpo, pero hay tres excepciones que conviene conocer antes de deserializar la respuesta: DELETE /api/categories/:id responde 200 con { message }, y DELETE /api/suppliers/:id y DELETE /api/purchases/:id responden 200 con { success: true }. El 204 sí es la respuesta de clientes, productos, bodegas y terminales.

CampoReq.Reglas
nameSíNombre del producto o del servicio.
priceNoPrecio de venta sin impuestos. Por defecto 0.
costPriceNoCosto de adquisición.
skuNoCódigo interno del producto.
productTypeNosimple (por defecto) · product · service. Los valores composite e ingredient existen para producción de lotes y quedan fuera de esta guía.
cabysNoCódigo CABYS de 13 caracteres. Obligatorio para poder facturar el producto.
codigoImpuestoNoCatálogo CodigoImpuesto (Nota 8). Por defecto 01 (IVA). Las descripciones oficiales y las reglas por código están en 14.3.
tarifaIvaCodeNoCatálogo CodigoTarifaIVA. Por defecto 08 - 13 % general.
taxDetailNoObligatorio para los impuestos 03 - Combustibles, 04 - Bebidas alcohólicas, 05 - Bebidas envasadas y 06 - Tabaco: exige cantidadUnidadMedida e impuestoUnidad. Para 07 - IVA cálculo especial admite factorBaseImponible; para 08 - IVA bienes usados, factorCalculoIVA; para 99 - Otros, exige descripcionImpuesto (5 a 100 caracteres) y admite porcentaje.
unitOfMeasureIdNoUnidad de medida del catálogo (GET /api/units-of-measure). Obligatoria solo para ingredient y composite.
categoryIdsNoIDs de GET /api/categories. En el PUT, un array vacío quita todas las categorías.
codigoMonedaNoCRC, USD o EUR.
isSellableNoPor defecto true. Un producto no vendible no aparece en los listados de venta.
allowNegativeStockNoPor defecto false: sin existencias no se puede descontar.
minStockNoUmbral de existencias mínimas que usa el reporte de inventario.
El stock se mueve, no se escribe

En el POST, el campo stock se interpreta como stock inicial y se registra como movimiento de ajuste en la bodega predeterminada. En el PUT, en cambio, stock se escribe directo en el producto y no actualiza los saldos por bodega. Para mover existencias use siempre POST /api/products/:id/movements (o POST /api/waste), que deja la trazabilidad completa.

Si la organización no tiene bodega predeterminada, el stock inicial se descarta en silencio: cree primero la bodega.

Saldos calculados en la respuesta

El listado y el detalle de un producto añaden dos campos calculados: stock (existencias disponibles) y rawStock (suma de los saldos de todas las bodegas). En productos compuestos, que quedan fuera de esta guía, stock es la capacidad que dan los ingredientes.

9.4 · Movimientos de stock

Toda entrada, salida o ajuste de existencias pasa por un movimiento. El movimiento se agrupa en una transacción de inventario, que es lo que permite auditar y revertir.

POST /api/products/:id/movements
curl -X POST "https://sandbox.vision-total.rogcr.com/api/products/$PRODUCT_ID/movements" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "quantity": 24,
    "type": "in",
    "reason": "Recepción de mercadería",
    "warehouseId": "8c31...",
    "referenceId": "rec-2026-0042",
    "uomId": "3a7e..."
  }'
CampoReq.Reglas
quantitySíCantidad movida. En una salida es el consumo.
typeSíin entrada · out salida · adjustment ajuste · return devolución.
reasonNoMotivo del movimiento. En las salidas se genera uno por defecto.
warehouseIdNoBodega destino. Si se omite, se usa la predeterminada.
referenceIdNoReferencia externa (orden, documento, guía).
uomIdNoUnidad de medida. Debe coincidir con la del producto: no hay conversiones.
MétodoRutaNotas
GET/api/products/:id/movementsHistorial del producto. Parámetros: page (1), limit (20), type, sortBy (createdAt) y sortOrder (ASC / DESC). Respuesta { data: [], meta: { page, limit, total, totalPages } }.
GET/api/transactions/:idTransacción de inventario con todos sus movimientos.
POST/api/wasteRegistra una merma: producto, bodega, cantidad y motivo.
POST/api/inventory/reconcileRecalcula el saldo de un producto en una bodega a partir de los movimientos confirmados y deja un ajuste por la diferencia.
CampoReq.Reglas
productIdSíProducto afectado por la merma.
warehouseIdSíBodega donde se registra la merma.
quantitySíDebe ser mayor que cero.
reasonSíspoilage · expired · spill · drop · contamination · customer_return · other. El valor preparation_loss es interno y se rechaza.
Una salida sin existencias falla

Un movimiento de tipo out que excede el saldo responde 400 con el detalle del faltante, salvo que el producto tenga allowNegativeStock: true. Corrija el saldo con un movimiento adjustment o con POST /api/inventory/reconcile.

9.5 · Mermas y conciliación de saldos

Dos operaciones de cierre. La merma descuenta existencias que se perdieron o no se pudieron vender, con un motivo trazable. La conciliación recalcula el saldo de un producto en una bodega a partir de los movimientos ya confirmados y deja un ajuste por la diferencia, sin tocar el histórico.

POST /api/waste
curl -X POST "https://sandbox.vision-total.rogcr.com/api/waste" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "5b70...",
    "warehouseId": "8c31...",
    "quantity": 3,
    "reason": "spoilage",
    "notes": "Lote dañado por humedad"
  }'
POST /api/inventory/reconcile
curl -X POST "https://sandbox.vision-total.rogcr.com/api/inventory/reconcile" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "productId": "5b70...", "warehouseId": "8c31..." }'
MétodoRutaNotas
POST/api/wasteRegistra una merma. Devuelve { success: true, transactionId }.
POST/api/inventory/reconcileRecalcula el saldo. Devuelve { previous, computed, delta, txId }.
Motivos de merma admitidos

spoilage · expired · spill · drop · contamination · customer_return · other. El valor preparation_loss es interno del ERP y la API lo rechaza con 400.

9.6 · Anticipos (RC / PP)

Un anticipo es un dinero recibido del cliente (RC, recibo de cliente) o un pago a un proveedor (PP, pago a proveedor) por adelantado. El anticipo aprobado y cobrado se puede aplicar como pago de una factura y, si la factura ya está facturada, se emite un 10 - Recibo Electrónico de Pago (REP) con TipoMedioPago = 99.

MétodoRutaPermisoPropósito
GET/api/advancesadvances:viewListar anticipos.
GET/api/advances/:idadvances:viewDetalle de un anticipo.
POST/api/advancesadvances:createCrear un anticipo en borrador.
PATCH/api/advances/:idadvances:editEditar un anticipo en borrador o pendiente.
POST/api/advances/:id/submitadvances:createEnviar el anticipo a aprobación.
POST/api/advances/:id/approveadvances:approveAprobar un anticipo pendiente.
POST/api/advances/:id/collectadvances:collectMarcar el anticipo como cobrado o pagado.
POST/api/advances/:id/voidadvances:voidAnular un anticipo que todavía no se aplicó.
GET/api/customers/:id/available-advancesadvances:viewAnticipos del cliente con saldo disponible.
GET/api/suppliers/:id/advancesadvances:viewAnticipos del proveedor.
GET/api/advances/:id/pdfadvances:viewRecibo de dinero de anticipo en PDF.
La máquina de estados del anticipo

draft → pending → approved → collected → applied. Un anticipo con saldo a medias queda en partial, y uno anulado en voided. Sólo los anticipos collected y partial tienen saldo disponible para aplicarse como pago. No existe un DELETE: la anulación se hace con POST /api/advances/:id/void, que deja la traza.

Aplicar el anticipo a una factura

Un anticipo se aplica como una línea de pago con method: "advance" y el advanceId del anticipo. Si el anticipo pertenece a otro cliente, la API responde 400 con ADVANCE_CUSTOMER_MISMATCH; si no tiene saldo disponible, ADVANCE_NOT_AVAILABLE; y si la aplicación supera el saldo pendiente de la factura, PAYMENT_EXCEEDS_BALANCE. Véase 10 · Paso 2 · Registrar el pago.

10 Emitir comprobantes electrónicos

El Facturador del ERP separa el documento comercial de la emisión fiscal. Primero se crea la factura con sus líneas, luego se registran los pagos y solo entonces se convierte en un comprobante ante Hacienda. Esta separación permite cotizar, cobrar por anticipado y revisar antes de emitir.

Diagrama 4 Pipeline de emisión de una factura electrónica
1

Crear el documento comercial

La factura nace en estado draft con sus líneas, impuestos, moneda y condición de venta. Todavía no toca Hacienda.

POST /api/invoices
↓
2

Registrar el pago

Los medios de pago se acumulan hasta cubrir el total. La factura pasa a paid y queda habilitada para facturar.

POST /api/invoices/:id/payments
↓
3

Solicitar la emisión

Se elige el tipo de comprobante y el receptor. El backend asigna consecutivo, arma la clave de 50 dígitos y encola el trabajo de firma.

POST /api/invoices/:id/facturar
↓
4

Firma y envío en segundo plano

Fuera del ciclo de la request: el servidor firma el XML con el certificado de la organización y lo envía al ATV. El servicio no interviene.

202 Accepted + jobId
↓
5

Sondeo del estado ante Hacienda

El backend consulta a Hacienda con la clave y guarda el MensajeHacienda. El estado final se lee por API.

GET /api/facturacion/documents/:documentId/status

Paso 1 · Crear la factura

POST /api/invoices
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "documentType": "01",
    "customerId": "9f2c...",
    "terminalId": "7a41...",
    "sucursalCodigo": "001",
    "numeroTerminal": "00001",
    "codigoActividadEmisor": "4711.1",
    "condicionVenta": "01",
    "codigoMoneda": "CRC",
    "tipoCambio": 1,
    "notes": "Venta de mercadería",
    "items": [
      {
        "productId": "5b70...",
        "quantity": 2,
        "price": 2850.00,
        "taxRate": 0.13,
        "tarifaIvaCode": "08",
        "detalle": "Café molido 250 g",
        "unidadMedida": "Unid"
      },
      {
        "productId": null,
        "quantity": 1,
        "price": 5000.00,
        "taxRate": 0.13,
        "tarifaIvaCode": "08",
        "detalle": "Servicio de instalación",
        "unidadMedida": "Unid"
      }
    ]
  }'
La unidad se manda siempre, y sale del catálogo

unidadMedida no se hereda del producto: si la línea lo omite, el API escribe Unid y el comprobante puede quedar con una unidad equivocada sin que nada falle. Y si se manda un valor que no está en la enumeración de Hacienda, el comprobante entero se rechaza después, con el código -1. Tome el code que devuelve GET /api/units-of-measure —el mismo de product.unitOfMeasure.code— y mándelo tal cual. Véase 8.5 · Unidad de medida.

Campo de cabeceraReq.Reglas
customerIdSíReceptor. Envíe null para una 08 - Factura Electrónica de Compra (FEC), que no lleva cliente, y para un 04 - Tiquete Electrónico (TE) de consumidor final. La identificación del receptor debe existir en el padrón de Hacienda o el comprobante se rechaza con -17 / -38: véase 8.6.
documentTypeNo01 - Factura Electrónica (FE) · 04 - Tiquete Electrónico (TE) · 08 - Factura Electrónica de Compra (FEC) · 09 - Factura Electrónica de Exportación (FEE). Si se omite, la factura queda como cotización. El valor 10 se acepta al guardar (queda registrado en la cabecera) pero no se puede emitir con él: el 10 - Recibo Electrónico de Pago (REP) sale de /api/invoices/:id/recibo-pago.
terminalIdCond.Alternativa a sucursalCodigo + numeroTerminal. Basta con uno de los dos esquemas. Si el terminal no existe en la organización, la API responde 400 con Terminal no encontrado.
sucursalCodigoCond.Exactamente 3 dígitos.
numeroTerminalCond.Exactamente 5 dígitos.
codigoActividadEmisorNoDebe pertenecer a la organización. Si se omite se usa la actividad principal.
actividadEconomicaReceptorNoCIIU4 del receptor, con punto (4711.0). Fuente recomendada: actividades[] del padrón de Hacienda (GET /api/facturacion/taxpayers/:id). Debe coincidir con uno de los customer.economicActivities, o la API responde 400. En la 08 - Factura Electrónica de Compra (FEC) los roles se invierten y este campo lleva el CIIU de la organización. Véase 8.7.
condicionVentaSíCatálogo de condición de venta (14): 01 - Contado · 02 - Crédito · 10 - Venta a crédito IVA hasta 90 días… La API guarda el valor tal cual al crear la factura; el subconjunto restringido de la FEC y la FEE se valida al facturar (véase Paso 3).
condicionVentaOtrosNoObligatorio si condicionVenta es 99 - Otros.
plazoCreditoNoDías de crédito. Se usa con las condiciones 02, 08 y 10.
codigoMonedaNoCRC, USD o EUR. Cualquier otro valor responde 400 con codigoMoneda debe ser CRC, USD o EUR.
tipoCambioNoObligatorio y mayor que cero si la moneda no es CRC. Consúltelo en GET /api/exchange-rates/:currency (véase 8.10).
itemsSíAl menos una línea.
supplierIdNoProveedor emisor, solo para la 08 - Factura Electrónica de Compra (FEC). Si el proveedor no existe o no pertenece a la organización, la API responde 400 con El proveedor seleccionado no existe o no pertenece a esta organización.
supplierActivityCodeNoCIIU4 del proveedor (el emisor de la FEC). Debe estar entre los supplier.economicActivities; si no, la API responde 400 con «El CIIU "X" seleccionado para esta FEC no está en el catálogo de actividades del proveedor».
partidaArancelariaNoSolo para la 09 - Factura Electrónica de Exportación (FEE). Mapa de número de línea a código arancelario de 12 caracteres: la clave del objeto es el número de línea (1, 2, 3…) y el valor el código. En la FEE es obligatorio en todas las líneas.
otrosCargosNoVéase 14.6 · Otros cargos. Máximo 15 conceptos.
notesNoNota interna de la factura. No viaja al XML.
Campo de líneaReq.Reglas
productIdNoSi se omite, el codigoCabys por defecto es 0000000000000, que Hacienda rechaza en producción. Si se envía, debe apuntar a un producto activo de esta organización con CABYS real: si el id no existe, es de otra organización, el producto está archivado o no tiene CABYS, la API responde 400 con PRODUCT_NOT_FOUND o PRODUCT_MISSING_CABYS.
quantitySíDebe ser mayor que cero.
priceSíPrecio unitario sin impuestos.
taxRateSíFracción decimal. 0.13 produce <Tarifa>13.00</Tarifa>.
tarifaIvaCodeSíCódigo de tarifa del catálogo de Hacienda.
detalleSíDescripción de la línea, va a <Detalle>. Máximo 255 caracteres.
codigoCabysNoCABYS propio de la línea, 13 dígitos exactos. Es un override: si la línea no lo manda, hereda el del producto; si lo manda, gana sobre el del producto. Un valor mal formado se rechaza con 400 y el código INVALID_CABYS.
codigoImpuestoNoCódigo de impuesto de la línea (catálogo de la Nota 8). Es un override: si la línea no lo manda, hereda el del producto, y si el producto tampoco lo tiene, se usa 01 - IVA. Un código fuera del catálogo se rechaza con 400 y INVALID_CODIGO_IMPUESTO. Véase 14.3.
taxDetailNoDatos fiscales de la línea. Es un override por campo: lo que trae la línea pisa a lo del producto y lo que no trae se hereda de él. Campos: cantidadUnidadMedida, volumenUnidadConsumo, porcentaje, proporcion, impuestoUnidad, factorCalculoIVA, factorBaseImponible, descripcionImpuesto. El backend valida al guardar lo que la Nota 8 exige por código de impuesto. Véase 14.3.
numeroVINoSerieNoVIN o número de serie del medio de transporte (máximo 17 caracteres). Va entre <Detalle> y <PrecioUnitario>. El backend solo valida el largo: no comprueba que el CABYS sea de un vehículo, así que la obligatoriedad la aplica Hacienda (Nota 6) y hay que mandarlo explícitamente en la línea del vehículo. Una línea por vehículo: el nodo se emite una sola vez por línea. No aplica al REP (10).
registroMedicamentoNoNúmero de registro sanitario del medicamento (máximo 100 caracteres, Nota 7). Opcional; se vuelve obligatorio cuando el CABYS corresponde a un medicamento con registro sanitario. No aplica al REP (10).
formaFarmaceuticaNoCódigo de la forma farmacéutica (máximo 3 caracteres, Nota 19). Opcional. No aplica al REP (10).
unidadMedidaNoPor defecto Unid. No se copia del producto: mande el code que devuelve GET /api/units-of-measure (o product.unitOfMeasure.code). El valor se escribe tal cual en <UnidadMedida> y el API no lo valida, así que un valor fuera de la enumeración de Hacienda hace que Hacienda rechace el comprobante entero con -1. Véase 8.5 · Unidad de medida.
discountNoDescuento monetario de la línea. No puede superar el monto total de la línea (400 con INVALID_MONTO_DESCUENTO). Con el código de descuento 01 o 03 tiene que ser el 100 % del monto total (INVALID_CODIGO_DESCUENTO_TOTAL). Véase 14.10 · Descuentos de la línea.
codigoDescuentoNoCódigo de descuento de la Nota 20 (01–09, 99). Catálogo y reglas en 14.10.
codigoDescuentoOtroNoDescripción del descuento cuando el código es 99 (5–100 caracteres).
naturalezaDescuentoNoNaturaleza del descuento (3–80 caracteres). Obligatoria con cualquier descuento; si se omite, el backend precarga la del código elegido.
exoTipoDocumentoNoTipo de documento de exoneración (Nota 10.1): 01, 02, 03, 04, 05, 06, 08, 09, 10, 11, 99. Catálogo en 14.4. Es uno de los cuatro campos que hacen que el nodo exista: un código fuera del catálogo no da error, pero el nodo no se emite y la línea se factura gravada.
exoNumeroDocumentoNoNúmero del documento de exoneración: 3 a 40 caracteres, obligatorio. Fuera de ese rango, la API responde 400 con INVALID_EXONERACION_NUMERO_DOCUMENTO. El formato AL-NNNNNNNN-NN es sólo del tipo 01; los demás tipos llevan la referencia de la ley, el decreto o la resolución (3 a 40 caracteres). Segundo de los cuatro campos que hacen que el nodo exista.
exoArticulo / exoIncisoNoArtículo e inciso de la ley que exonera. Números enteros (sin decimales). Obligatorios en el XSD con los códigos 02, 03, 06 y 08 de la Nota 10.1.
exoNombreInstitucionNoInstitución que otorga la exoneración (catálogo 01–12, 99; el 12 es ARESEP). Con el 99 se completa exoNombreInstitucionOtros. Cuarto y último de los cuatro campos que hacen que el nodo exista.
exoFechaEmisionNoFecha de emisión del documento de exoneración (ISO: 2026-03-14). Sin ella, la línea no lleva nodo <Exoneracion> y el impuesto se cobra íntegro. Tercer de los cuatro campos que hacen que el nodo exista.
exoTipoDocumentoOtros / exoNombreInstitucionOtrosNoDescripciones libres de los códigos 99 de la Nota 10.1 (5 a 100 caracteres cada una).
exoTarifaExoneradaOverrideNoPorcentaje de la tarifa de IVA de la línea que queda exonerado (los «puntos de la tarifa_integer» del anexo v4.4, pág. 46). Si se omite, se toma la tarifa de IVA de la línea: en una línea de 13 % eso es 13, es decir el 100 % del IVA de esa línea. El valor 0 no es «cero de exoneración»: el backend lo recalcula.
exoMontoExoneradoOverrideNoMonto exonerado explícito, para un surtido con tarifas distintas. El valor 0 se trata como «no informado» y el backend recalcula. Por defecto el monto es derivado: tarifa exonerada × base imponible, y nunca puede superar el impuesto de la línea.
notesNoNota interna de la línea. No viaja al XML.
La línea hereda del producto, pero el override es por campo

El CABYS, el tipo de impuesto y los datos fiscales de la línea ganan sobre los del producto. Pero el taxDetail se hereda campo por campo: una línea que cambia un solo dato no pierde el resto del detalle fiscal del producto — por ejemplo, la descripcionImpuesto del código 99. El merge se aplica en los tres momentos: al guardar la factura, al calcular el impuesto persistido y al armar el XML, así que lo que se ve en la base de datos y lo que Hacienda valida coinciden.

POST /api/invoices · respuesta 201 (extracto)
{
  "success": true,
  "data": {
    "id": "0e77...",
    "documentType": "01",
    "documentTypeLabel": "Factura Electrónica",
    "status": "draft",
    "customerId": "9f2c...",
    "customerName": "Corporación Wegner S.A.",
    "customer": { "identificationType": "02", "identificationNumber": "3109876543" },
    "organization": {
      "name": "Comercial del Este",
      "legalName": "Comercial del Este Sociedad Anónima",
      "taxId": "3101234567",
      "taxIdType": "02",
      "email": "facturacion@miempresa.cr"
    },
    "terminalId": "7a41...",
    "terminalName": "POS Principal",
    "sucursalCodigo": "001",
    "numeroTerminal": "00001",
    "codigoActividadEmisor": "4711.1",
    "condicionVenta": "01",
    "codigoMoneda": "CRC",
    "tipoCambio": 1,
    "subtotal": 10700,
    "taxTotal": 1391,
    "discountTotal": 0,
    "total": 12091,
    "paidBalance": 0,
    "remainingBalance": 12091,
    "createdByUserId": "8a5f...",
    "createdAt": "2026-10-24T15:10:00.000Z",
    "haciendaStatus": null
  }
}
POST /api/invoices · producto sin CABYS
{
  "message": "El producto \"Café molido 250 g\" (SKU CAF-250) no tiene CABYS asignado. Asignalo en Configuración > Productos antes de facturar.",
  "code": "PRODUCT_MISSING_CABYS"
}
El CABYS se valida al crear, no al facturar

Un productId que no resuelve, que pertenece a otra organización, que apunta a un producto archivado o a un producto sin CABYS real, hace que el POST falle de inmediato con 400. Antes el error aparecía tres llamadas después, al firmar el comprobante. Las líneas sin productId (servicios escritos a mano) siguen aceptándose con el marcador 0000000000000: el bloqueo de producción sigue aplicando en facturar.

Consultar y editar las facturas

MétodoRutaQué hace
GET/api/invoicesLista paginada. Devuelve { data: [], total, page, limit, totalPages } sin líneas ni pagos: pida el detalle para eso.
GET/api/invoices/:idDetalle con líneas, pagos y comprobantes de Hacienda. Aquí está numeroConsecutivo.
PATCH/api/invoices/:idEdición parcial. Sólo se puede editar una factura en draft o quoting: en cuanto la factura se envía a Hacienda queda en invoiced y la API responde 400 con «Solo se pueden editar facturas en borrador o cotización». Un comprobante ya enviado se corrige con una nota de crédito, no editando la factura. Véase 10.5.
DELETE/api/invoices/:idBaja lógica. Sólo se puede eliminar una factura en draft; en cualquier otro estado la API responde 400.
GET/api/invoices/:id/quotation-pdfDescarga el PDF de la cotización. Acepta ?version=.
GET/api/invoicing/recordsListado unificado de lo facturado (POS y Facturador), con el consecutivo oficial de Hacienda de cada comprobante.
Filtro de GET /api/invoicesPor defectoNotas
page1Mínimo 1.
limit20Se acota entre 1 y 100.
status—draft · quoting · paid · invoiced · cancelled.
documentType—01 · 04 · 08 · 09 · 10.
customerId—Filtra por cliente.
terminalId—Filtra por terminal.
from / to—Rango de fechas de creación.
En el PATCH, undefined y null no son lo mismo

Un campo omitido conserva su valor; un campo enviado como null lo borra. Eso vale para customerId, supplierId, partidaArancelaria, supplierActivityCode y actividadEconomicaReceptor. Y enviar items reemplaza todas las líneas: las anteriores se dan de baja y se crean las nuevas.

Cambiar el tipo de una factura a 04 - Tiquete Electrónico (TE) borra automáticamente la actividadEconomicaReceptor, porque el XSD del TE no declara ese nodo. Editar la factura invalida además el PDF de cotización cacheado.

Y nada de esto aplica a una factura ya enviada a Hacienda. El PATCH sólo opera en draft o quoting: después del facturar la factura es un comprobante firmado y hay que corregirlo con una nota de crédito. Véase 10.5.

Paso 2 · Registrar el pago

POST /api/invoices/:id/payments
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices/$INVOICE_ID/payments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payments": [
      { "method": "cash", "amount": 5000, "reference": "Efectivo", "change": 0, "customerAmount": 5000 },
      { "method": "sinpe", "amount": 7091, "reference": "SINPE-884512" }
    ]
  }'
POST /api/invoices/:id/payments · respuesta 201 (extracto)
{
  "success": true,
  "data": {
    "payments": [
      {
        "id": "c1a0...",
        "method": "cash",
        "amount": 5000,
        "reference": "Efectivo",
        "change": 0,
        "customerAmount": 5000,
        "receivedAt": "2026-10-24T15:14:00.000Z",
        "haciendaDocumentId": null,
        "haciendaClave": null
      }
    ],
    "payment": { "id": "c1a0...", "method": "cash", "amount": 5000 },
    "invoice": { "id": "0e77...", "status": "paid", "paidBalance": 12091, "remainingBalance": 0 },
    "advanceApplications": []
  }
}
method internoTipoMedioPago en HaciendaDescripción oficial (Nota 6)
cash01Efectivo
card02Tarjeta
check03Cheque
transfer04Transferencia – depósito bancario
third_party05Recaudado por terceros 27
sinpe06SINPE Móvil
digital_platform07Plataforma Digital
other99Otros — la descripción del medio se indica en la representación gráfica
advance99Anticipo — no es un medio de la Nota 6: es la aplicación de un anticipo (RC o PP). El backend escribe <MedioPagoOtros> con la referencia del anticipo.
Campo del pagoReq.Reglas
methodSíEl nombre interno del medio. Con advance es obligatorio advanceId.
amountSíMonto del pago. No puede superar el saldo pendiente de la factura: la API responde 400 con PAYMENT_EXCEEDS_BALANCE.
customerAmountNoLo que entregó el cliente. Por defecto es igual a amount.
changeNoCambio devuelto. Por defecto 0.
referenceNoReferencia del pago (número de aprobación, folio, etc.). Con method: "advance" el backend la sustituye por el consecutivo del anticipo (RC-<n> o PP-<n>).
advanceIdNoAnticipo que se aplica. Obligatorio con method: "advance". Si el anticipo es de otro cliente, la API responde ADVANCE_CUSTOMER_MISMATCH; si no tiene saldo, ADVANCE_NOT_AVAILABLE.
motivoNoMotivo del pago. Sólo lo usa el flujo de anticipos sobre una factura ya facturada.
El method es el nombre interno, no el código

En POST /api/invoices/:id/payments el campo method toma el nombre interno del medio (cash, sinpe, …). El backend lo traduce al TipoMedioPago de Hacienda según el catálogo de la Nota 6; el servicio externo nunca escribe el código de dos dígitos en esta ruta. La única excepción es el medioPago del 10 - Recibo Electrónico de Pago (REP), que sí lleva el código de Hacienda ("01"…"07", "99"). El catálogo completo está en 14.9 · Medio de pago.

Nota 6 y Nota 7: qué se imprime

La Nota 6 fija la codificación de los medios de pago del esquema v4.4. La Nota 7 exige que, para efectos de visualización e impresión, se muestre su descripción: en el PDF del comprobante el medio aparece como «Efectivo», «Tarjeta», «SINPE Móvil»… y nunca como un código suelto. Con 99 - Otros la descripción del medio se agrega entre paréntesis y es obligatoria: el XML exige el nodo <MedioPagoOtros> y Hacienda rechaza el comprobante si falta.

Nota 27 (pie del catálogo): «El código 06, Recaudado por terceros se utilizará cuando el pago sea recibido por un tercero y no por quien emite el servicio o mercancía, ejemplo: pago de recibos telefónicos en supermarkets». Use third_party sólo en ese caso: es un pago que el emisor recibe a través de un tercero. Ojo con el pie: dice «el código 06», pero en la tabla el 05 es «Recaudado por terceros» y el 06 es «SINPE Móvil». Manda la tabla.

Pago parcial, crédito y anticipos

Si el total no queda cubierto, la factura permanece con remainingBalance > 0 y puede facturarse con condicionVenta: "02" y plazoCredito. La API ajusta automáticamente la condición de venta según el saldo pendiente.

Un anticipo se aplica como una línea de pago con method: "advance" y el advanceId del anticipo. Si la factura ya estaba facturada, el pago se registra y el 10 - Recibo Electrónico de Pago (REP) se emite con TipoMedioPago = 99 y la referencia del anticipo. Véase 9.6 · Anticipos.

Paso 3 · Enviar a Hacienda

POST /api/invoices/:id/facturar
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices/$INVOICE_ID/facturar" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "documentType": "01",
    "customerId": "9f2c...",
    "actividadEconomicaReceptor": "4711.0"
  }'
POST /api/invoices/:id/facturar · respuesta 202
{
  "success": true,
  "data": {
    "haciendaDocumentId": "d41b...",
    "clave": "50624102600310123456700100001010000000042112345678",
    "jobId": "14"
  }
}
CampoReq.Reglas
documentTypeNo01 - Factura Electrónica (FE) · 04 - Tiquete Electrónico (TE) · 08 - Factura Electrónica de Compra (FEC) · 09 - Factura Electrónica de Exportación (FEE). Si se omite se usa el tipo de la cabecera. El 10 - Recibo Electrónico de Pago (REP) no se emite por esta ruta: sale de /api/invoices/:id/recibo-pago. Si el tipo resuelto no es uno de los cuatro, la API responde 400 con «Debe elegir el tipo de documento (FE, TE, FEC o FEE) para facturar».
customerIdNoReceptor. Obligatorio para la 01 - Factura Electrónica (FE) y para la 09 - Factura Electrónica de Exportación (FEE); debe ser null en una 08 - Factura Electrónica de Compra (FEC). En el 04 - Tiquete Electrónico (TE) es opcional: null lo deja como consumidor final y omitido conserva el que ya tuviera.
supplierIdCond.Proveedor emisor: obligatorio en la 08 - Factura Electrónica de Compra (FEC) (la API responde 400 con «La FEC requiere un proveedor seleccionado como emisor…» si falta). Se ignora en la FE, el TE y la FEE.
actividadEconomicaReceptorNoCIIU4 del receptor, con punto (4711.0). Se puede leer del padrón de Hacienda en lugar de escribirlo a mano: GET /api/facturacion/taxpayers/:id → actividades[]. Debe coincidir con uno de los customer.economicActivities. En la 08 - Factura Electrónica de Compra (FEC) los roles se invierten y este campo lleva el CIIU de la organización (es obligatorio: el XSD v4.4 declara <CodigoActividadReceptor>). Véase 8.7.
supplierActivityCodeNoCIIU4 del proveedor (el emisor de la FEC), con punto. Debe estar entre los supplier.economicActivities; si no, la API responde 400 con «El CIIU "X" seleccionado para esta FEC no está en el catálogo de actividades del proveedor».
Los requisitos que sólo se comprueban al facturar

La factura se puede guardar en borrador con datos incompletos; las reglas de emisión se aplican cuando se pide el comprobante. Si alguna falla, la API responde 400 con el motivo exacto:

  • Sólo se factura una factura en estado paid. En otro estado: «Solo se pueden facturar invoices en estado Pagado».
  • La FE exige un cliente identificado. Sin governmentId: «Cliente requiere identificación para FE».
  • La FEE exige un cliente en el exterior (isForeign: true) con una dirección de al menos 5 caracteres.
  • La FEE exige partida arancelaria en todas las líneas, de 12 caracteres. Si faltan, el error dice en qué líneas.
  • La FEC y la FEE tienen un subconjunto propio de condiciones de venta. El XSD v4.4 admite 01, 02, 03, 04, 05, 06, 07, 08, 10, 13, 14, 15 y 99: los códigos 09 y 11 se rechazan con «La condición de venta "X" no está permitida para la FEC/la FEE…».
  • En producción no vale el CABYS marcador. Una línea sin CABYS real detiene la emisión con «Producción bloqueada: línea "X" sin CABYS real».
El tipo documental no se cambia después

Cada tipo tiene su propia serie de consecutivos. Si una factura se emite como 04 - Tiquete Electrónico (TE) y el cliente pide una 01 - Factura Electrónica (FE), la vía correcta es una nota de crédito sobre el 04 - Tiquete Electrónico (TE) y una nueva factura por el importe correcto, no un segundo facturar con otro tipo sobre el mismo documento.

La respuesta no trae el consecutivo

El 202 devuelve clave, jobId y haciendaDocumentId: el número consecutivo de 20 dígitos no está ahí. Se lee del detalle de la factura, una vez emitido. Véase 12.6 · Dónde leer el consecutivo.

Anatomía de la clave de 50 dígitos

La clave es el identificador único del comprobante ante Hacienda y la que se usa para consultar su estado. El backend la compone automáticamente: entender su estructura ayuda a interpretar los datos y a detectar errores de configuración de terminal.

Diagrama 5 Composición de la clave (50) y el consecutivo (20)
506País
|
24Día
|
10Mes
|
26Año
|
003101234567Identificación del emisor
|
00100001010000000042Consecutivo de 20 dígitos
|
1Situación
|
12345678Código de seguridad
Clave de 50 dígitos tal como la devuelve la API
001Sucursal
+
00001Terminal
+
01Tipo de documento
+
0000000042Número consecutivo
=
00100001010000000042NumeroConsecutivo del XML
Los 20 dígitos del consecutivo se repeaten dentro de la clave
El tipo de documento va en el segundo bloque del consecutivo: 01 - Factura Electrónica (FE), 02 - Nota de Débito Electrónica (ND), 03 - Nota de Crédito Electrónica (NC), 04 - Tiquete Electrónico (TE), 08 - Factura Electrónica de Compra (FEC), 09 - Factura Electrónica de Exportación (FEE), 10 - Recibo Electrónico de Pago (REP).
Las 12 posiciones de la identificación del emisor

El bloque de la identificación del emisor se rellena con doce caracteres, agregando ceros a la izquierda según el tipo de identificación (Nota 4.1 del esquema v4.4): 01 - Cédula Física → 3 ceros · 02 - Cédula Jurídica → 2 ceros · 03 - DIMEX → 1 cero si la cédula tiene 11 dígitos · 04 - NITE → 2 ceros. En el ejemplo, la cédula jurídica 3101234567 (10 caracteres) queda como 003101234567. El backend hace este relleno: el servicio externo nunca escribe la clave. Véase 14.8 · La identificación en la clave numérica.

Los consecutivos son por terminal

El numeroTerminal y el sucursalCodigo de la factura participating en la clave. Si dos terminales de la misma organización emiten 01 - Factura Electrónica (FE), cada una necesita su propio grupo de consecutivos, creado con POST /api/organizations/:id/hacienda-consecutivos.

Estados del comprobante

draft → → accepted · rejected · error →
Diagrama 6 Transiciones y quién las dispara
  draft ──POST /api/invoices/:id/facturar──▶ submitted
     │                                      │
     │                                      ├──▶ accepted   (aceptado por Hacienda)
     │                                      ├──▶ rejected   (rechazado por Hacienda)
     │                                      └──▶ error      (fallo técnico o de red)
     │
     └──POST /api/facturacion/documents/:id/resend──▶ nuevo draft con clave y consecutivo NUEVOS
                                                  (Hacienda no admite reenviar la misma clave)

  submitted ──GET /api/facturacion/.../status──▶ consulta en vivo a Hacienda
                              └──▶ respuesta cacheada si el envío tiene más de 60 s

Transiciones de la factura

El estado de la factura comercial es independiente del estado del comprobante de Hacienda. Los cinco estados son draft (borrador), quoting (cotización), paid (pagada), invoiced (facturada) y cancelled (anulada). Las transiciones se disparan por POST /api/invoices/:id/transitions, y cada una exige su propio permiso.

actionPermisoDe → aNotas
quoteinvoices:transition_quotedraft → quotingConvierte la factura en cotización.
payinvoices:transition_payquoting → paidCon condición de venta 01 - Contado exige que los pagos cubran el total; si no, 400 con «Pagos insuficientes para marcar como Pagado».
invoiceinvoices:transition_invoicepaid → invoicedEmite el comprobante. Equivale a facturar.
quote_and_invoiceinvoices:transition_invoicedraft \| quoting → invoicedAtajo: emite de una vez. Exige documentType; si falta, 400 con «Debe elegir el tipo de comprobante (FE, TE, FEC o FEE)».
cancelinvoices:transition_cancel→ cancelledGuarda el motivo como cancelledReason. Una factura ya facturada no se anula por aquí: se anula con una nota de crédito.
El cuerpo de la transición

action es obligatorio (un valor fuera de la lista responde 400 con action inválida). Opcionalmente: motivo (texto del motivo de la anulación), documentType (01, 04, 08 o 09), customerId y supplierId. La respuesta es 200 con la factura actualizada.

Se puede volver a pagar una factura ya facturada

Cuando la factura está invoiced pero su condición de venta no es 01 - Contado y le queda saldo pendiente, la API acepta registrar pagos adicionales. En cualquier otro caso responde 400 con «No se pueden agregar pagos en este estado o condición de venta». Para quitar un pago, use DELETE /api/invoices/:id/payments/:paymentId, que sólo funciona en estado quoting o paid.

10.5 · Caso completo: venta de vehículo eléctrico con CABYS, VIN y exoneración

Un caso real de punta a punta: una venta de tres líneas —un vehículo eléctrico exonerado, un kit de carga y unos accesorios— con el CABYS correcto en el producto, el VIN del vehículo en su línea y la exoneración del incentivo de carro eléctrico en esa misma línea. Es la forma de usar juntas las tres piezas que más se confunden: el CABYS que se hereda del producto, el VIN que viaja por línea y la exoneración que se calcula por línea.

La exoneración es de la línea, no del producto

Los campos exo* van en items[]. Una exoneración puede válidamente aplicar a una sola línea de un comprobante: en este ejemplo el vehículo va exonerado y el kit y los accesorios van gravados. El producto nunca lleva datos de exoneración.

Paso 0 · El producto con su CABYS

El CABYS se busca en el catálogo local del backend, que no necesita conexión con Hacienda ni depende de la organización:

GET /api/cabys?q=vehiculo%20electrico&limit=20
curl -X GET "https://sandbox.vision-total.rogcr.com/api/cabys?q=vehiculo%20electrico&limit=20" \
  -H "Authorization: Bearer $TOKEN"
GET /api/cabys?q=… · respuesta 200
{
  "total": 2,
  "items": [
    { "codigo": "8711100000000", "descripcion": "Automóviles y camionetas, eléctricos" },
    { "codigo": "8711990000000", "descripcion": "Partes y accesorios de vehículos eléctricos" }
  ]
}
El API no valida el contenido del CABYS

Sólo comprueba que sean 13 dígitos: un código inventado pasa la validación del API y el rechazo llega después, de Hacienda. Los códigos del ejemplo son ilustrativos — saquelos del catálogo con GET /api/cabys?q=… y elija el que corresponda al artículo—.

Con el CABYS resuelto, el producto queda listo para facturar:

POST /api/products · el vehículo
curl -X POST "https://sandbox.vision-total.rogcr.com/api/products" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Vehículo eléctrico BYD Dolphin 2026",
    "sku": "VEH-0001",
    "price": 18500000,
    "costPrice": 16000000,
    "productType": "product",
    "cabys": "8711100000000",
    "codigoImpuesto": "01",
    "tarifaIvaCode": "08",
    "codigoMoneda": "CRC",
    "unitOfMeasureId": "3a7e..."
  }'

El CABYS de la línea se resuelve en este orden: el items[].codigoCabys que mande el operador (override, 13 dígitos exactos) → el cabys del producto → el marcador 0000000000000, que bloquea la emisión en producción. Por eso el CABYS se deja en el producto: las tres líneas heredan el suyo y no hay que escribirlo en cada factura. Si dos modelos comparten la misma línea de negocio pero tienen CABYS distintos, el override por línea resuelve el caso sin duplicar el producto.

Paso 1 · La factura con varias líneas

POST /api/invoices · tres líneas, VIN y exoneración
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "documentType": "01",
    "customerId": "9f2c...",
    "sucursalCodigo": "001",
    "numeroTerminal": "00001",
    "codigoActividadEmisor": "4510.0",
    "actividadEconomicaReceptor": "4711.0",
    "condicionVenta": "01",
    "codigoMoneda": "CRC",
    "tipoCambio": 1,
    "notes": "Venta de vehículo eléctrico con exoneración ARESEP",
    "items": [
      {
        "productId": "ID_VEHICULO",
        "quantity": 1,
        "price": 18500000,
        "taxRate": 0.13,
        "tarifaIvaCode": "08",
        "codigoImpuesto": "01",
        "detalle": "Vehículo eléctrico BYD Dolphin 2026",
        "unidadMedida": "Unid",
        "numeroVINoSerie": "JTDDA3T7KA012345",

        "exoTipoDocumento": "03",
        "exoNumeroDocumento": "RES-ARESEP-2026-04512",
        "exoArticulo": 5,
        "exoInciso": 3,
        "exoNombreInstitucion": "12",
        "exoFechaEmision": "2026-03-14",
        "exoTarifaExoneradaOverride": 13,
        "exoMontoExoneradoOverride": 2405000
      },
      {
        "productId": "ID_KIT_CARGA",
        "quantity": 1,
        "price": 450000,
        "taxRate": 0.13,
        "tarifaIvaCode": "08",
        "codigoImpuesto": "01",
        "detalle": "Kit de carga doméstica 7.4 kW",
        "unidadMedida": "Unid"
      },
      {
        "productId": "ID_ACCESORIOS",
        "quantity": 1,
        "price": 85000,
        "taxRate": 0.13,
        "tarifaIvaCode": "08",
        "codigoImpuesto": "01",
        "detalle": "Alfombrillas y kit de seguridad",
        "unidadMedida": "Unid"
      }
    ]
  }'
La línea del vehículo va en tarifa 08, no en 0 %

El camino correcto no es poner la línea del carro eléctrico en tarifaIvaCode: "01" (0 % Art. 32) ni en "11" (0 % sin crédito): una línea No Sujeto no tiene IVA que exonerar, y el backend rechaza la combinación con 400 y este mensaje: «No se puede adjuntar el documento de exoneración "…" a una línea con CodigoTarifaIVA=01 (No Sujeto según Hacienda…). La línea no tiene IVA a cobrar, así que no aplica exoneración.»

La exoneración va sobre una línea gravada (08 = 13 %): es el nodo <Exoneracion> el que deja el IVA en cero para esa línea, sin quitarle al cliente su derecho a crédito ni clasificar la venta como No Sujeto.

Los cuatro campos que hacen que la exoneración exista

El nodo <Exoneracion> sólo se emite si la línea trae los cuatro: exoTipoDocumento, exoNumeroDocumento, exoFechaEmision y exoNombreInstitucion. Si falta uno —o si el tipo o la institución no están en el catálogo— no hay error: el nodo no se emite, la línea se factura gravada y Hacienda acepta el comprobante con el IVA cobrado. Es el fallo más caro del flujo, porque la API responde 201 y nada falla. Las tres verificaciones del final de esta sección son obligatorias.

Los dos campos del monto de la exoneración

CampoQué esEn el ejemplo
exoTarifaExoneradaOverride Porcentaje de la tarifa de IVA de la línea que queda exonerado: los «puntos de la tarifa_integer» del anexo v4.4 (pág. 46). Si se omite se usa la tarifa de la línea, que en una línea de 13 % es 13 — es decir, el 100 % del IVA de esa línea. El valor 0 no significa «cero de exoneración»: el backend lo recalcula. 13
exoMontoExoneradoOverride Monto exonerado, en colones. Si se omite —o si vale 0— el backend lo deriva: base imponible × (tarifa exonerada ÷ 100), y nunca puede superar el IVA de la línea (ANEXOS pág. 44). Sólo hace falta mandarlo explícito cuando hay un surtido con tarifas distintas. 2405000

Con esos dos campos, el XML lleva el nodo <Exoneracion> dentro del <Impuesto> de la línea, con su <TipoDocumento>, <NumeroDocumento>, <Articulo>, <Inciso>, <NombreInstitucion>, <FechaEmision>, <TarifaExonerada> y <MontoExoneracion>. El <ImpuestoNeto> de la línea queda en cero y el <TotalDesgloseImpuesto> del resumen suma sólo el IVA cobrado.

Qué le pasa a los totales

ConceptoVehículo (exonerado)Kit de cargaAccesorios
Base imponible18.500.000450.00085.000
IVA bruto2.405.00058.50011.050
MontoExoneracion2.405.000——
ImpuestoNeto de la línea058.50011.050
Total de la línea18.500.000508.50096.050
POST /api/invoices/:id/payments · el total ya viene exonerado
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices/$INVOICE_ID/payments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payments": [
      { "method": "transfer", "amount": 19104550, "reference": "BAC #884512" }
    ]
  }'
El pago es contra el total exonerado

La exoneración baja el total de la factura: el tax_amount que se persiste es el impuesto neto (bruto − exonerado), así que la factura de este ejemplo vale 19.104.550 y no 21.509.550. Pague el total que devuelve el POST /api/invoices: con un pago de más, la API responde 400 PAYMENT_EXCEEDS_BALANCE; con un pago de menos, la factura no llega a paid y facturar responde «Solo se pueden facturar invoices en estado Pagado».

Las tres verificaciones obligatorias

  1. En la factura: GET /api/invoices/:id → la línea del vehículo debe traer taxAmount: 0 y los campos exo* guardados.
  2. En el XML firmado: GET /api/facturacion/documents/:id/xml → el nodo <Exoneracion> dentro de <Impuesto>, con su <MontoExoneracion> y el <ImpuestoNeto> en cero.
  3. En el resumen: el <TotalDesgloseImpuesto> debe traer sólo el IVA cobrado (69.550 en el ejemplo), nunca el IVA exonerado, y el <TotalComprobante> debe coincidir con el total de la factura.
GET /api/facturacion/documents/:id/xml · la línea del vehículo
<Detalle>
  <CodigoCABYS>8711100000000</CodigoCABYS>
  <Cantidad>1</Cantidad>
  <UnidadMedida>Unid</UnidadMedida>
  <Detalle>Vehículo eléctrico BYD Dolphin 2026</Detalle>
  <NumeroVINoSerie>JTDDA3T7KA012345</NumeroVINoSerie>
  <PrecioUnitario>18500000.00000</PrecioUnitario>
  <MontoTotal>18500000.00000</MontoTotal>
  <SubTotal>18500000.00000</SubTotal>
  <BaseImponible>18500000.00000</BaseImponible>
  <Impuesto>
    <Codigo>01</Codigo>
    <CodigoTarifaIVA>08</CodigoTarifaIVA>
    <Tarifa>13.00000</Tarifa>
    <Monto>2405000.00000</Monto>
    <Exoneracion>
      <TipoDocumento>03</TipoDocumento>
      <NumeroDocumento>RES-ARESEP-2026-04512</NumeroDocumento>
      <Articulo>5</Articulo>
      <Inciso>3</Inciso>
      <NombreInstitucion>12</NombreInstitucion>
      <FechaEmision>2026-03-14T00:00:00</FechaEmision>
      <TarifaExonerada>13.00000</TarifaExonerada>
      <MontoExoneracion>2405000.00000</MontoExoneracion>
    </Exoneracion>
    <ImpuestoNeto>0.00000</ImpuestoNeto>
  </Impuesto>
  <MontoTotalLinea>18500000.00000</MontoTotalLinea>
</Detalle>
GET /api/invoices/:id · la línea exonerada en el detalle
{
  "data": {
    "items": [
      {
        "detalle": "Vehículo eléctrico BYD Dolphin 2026",
        "total": 18500000,
        "taxAmount": 0,
        "tarifaIvaCode": "08",
        "codigoCabys": "8711100000000",
        "numeroVINoSerie": "JTDDA3T7KA012345",
        "exoTipoDocumento": "03",
        "exoNumeroDocumento": "RES-ARESEP-2026-04512",
        "exoArticulo": 5,
        "exoInciso": 3,
        "exoNombreInstitucion": "12",
        "exoFechaEmision": "2026-03-14T00:00:00.000Z",
        "exoTarifaExoneradaOverride": 13,
        "exoMontoExoneradoOverride": 2405000
      }
    ],
    "subtotal": 19035000,
    "taxTotal": 69550,
    "total": 19104550
  }
}

Editar la factura para corregir la exoneración

Una factura ya enviada a Hacienda no se puede editar

PATCH /api/invoices/:id sólo funciona con la factura en draft o quoting. En cuanto la factura se envía a Hacienda —con POST /api/invoices/:id/facturar o con la transición invoice— queda en estado invoiced y la API responde 400 con «Solo se pueden editar facturas en borrador o cotización»: ya no se pueden agregar, cambiar ni quitar líneas, VIN ni exoneraciones.

No es una restricción de la interfaz sino del documento: el comprobante ya salió firmado con su clave de 50 dígitos y su consecutivo, y Hacienda no admite dos comprobantes con la misma clave. Un comprobante ya enviado se corrige, no se edita.

Mientras la factura está en borrador

Se puede corregir con PATCH /api/invoices/:id. Ojo: enviar items reemplaza todas las líneas —las anteriores se dan de baja y se crean las nuevas—, así que hay que mandar el array completo, con la exoneración ya en la línea del vehículo. La factura se recalcula y el total vuelve a bajar.

Después de enviarla

La única vía es emitir un 03 - Nota de Crédito Electrónica (NC) sobre el comprobante aceptado y crear una factura nueva con el contenido correcto. La nota se referencia por la clave del comprobante original, así que primero hay que confirmar que el original quedó aceptado. Véase 11 · Notas de crédito, débito y recibo de pago.

Resumen de los tres tiempos

Borrador → se puede editar todo, incluido el VIN y la exoneración. Pagada → se puede emitir: facturar es el punto sin retorno. Facturada → el documento ya está en Hacienda: sólo cabe una nota de crédito y una factura nueva.

11 Notas de crédito, débito y recibo de pago

Las notas no son facturas nuevas: son documentos derivados que referencian una clave aceptada. Solo pueden emitirse contra un comprobante comercial ya aceptado por Hacienda.

11.0 · Qué comprobante puede corregir un documento derivado

Los tres documentos derivados —03 - Nota de Crédito Electrónica (NC), 02 - Nota de Débito Electrónica (ND) y 10 - Recibo Electrónico de Pago (REP)— pueden colgar de cualquier comprobante comercial que el Facturador haya emitido:

Comprobante que se corrigeEmisor del derivadoReceptor del derivadoTipoDocIR
01 - Factura Electrónica (FE)La organizaciónEl cliente01
04 - Tiquete Electrónico (TE)La organizaciónEl cliente (o ninguno)04
08 - Factura Electrónica de Compra (FEC)El proveedorLa organización17 (NC) · 18 (ND)
09 - Factura Electrónica de Exportación (FEE)La organizaciónEl cliente en el exterior19
10 - Recibo Electrónico de Pago (REP)La organizaciónEl cliente20
Cuál de los dos se corrige

Una factura puede tener más de un comprobante aceptado: por ejemplo, una 01 - Factura Electrónica (FE) y después su 10 - Recibo Electrónico de Pago (REP). El backend elige por sí solo el comprobante comercial —la venta, la compra o la exportación— y solo cae al REP cuando la factura no tiene otro. Para una corrección explícita sobre un comprobante concreto, use las rutas /api/facturacion/documents/:documentId/nota-*.

Sobre una FEC los roles se invierten

En la 08 - Factura Electrónica de Compra (FEC) el <Emisor> del XML es el proveedor y el <Receptor> es la organización —que es quien firma con su certificado—. Un derivado de esa FEC conserva esa inversión: sigue siendo un documento del proveedor. Por eso su TipoDocIR no es 08 sino 17 (nota de crédito a una FEC) o 18 (nota de débito a una FEC), según el catálogo de la Nota 10.

Dos límites que se traducen en error 400
  • El comprobante original tiene que estar aceptado. Referenciar uno en borrador, enviado o rechazado produce un rechazo del derivado sin diagnóstico útil, así que la API lo impide con un mensaje explícito.
  • Sobre una FEC de un proveedor en el exterior no se puede emitir. El EmisorType de la NC, la ND y el REP no declara <OtrasSenasExtranjero> —solo lo declara el de la FEC—, así que la dirección en el exterior no tiene dónde escribirse. Para ese caso hay que emitir la corrección en el portal del proveedor.
Dos rutas para la misma nota

Hay una variante por factura y otra por comprobante, y no aceptan los mismos campos:

  • POST /api/invoices/:id/nota-credito · POST /api/invoices/:id/nota-debito · POST /api/invoices/:id/recibo-pago — parten de la factura. El backend localiza solo el comprobante original válido: el aceptado y, si hubo un reenvío, el más reciente.
  • POST /api/facturacion/documents/:documentId/nota-credito · POST /api/facturacion/documents/:documentId/nota-debito — parten del comprobante de Hacienda, así que el original es explícito. En esta variante el campo del medio de pago se llama medioPago, no medioReembolso.

El 10 - Recibo Electrónico de Pago (REP) solo existe en la primera variante.

11.1 · 03 - Nota de Crédito Electrónica (NC)

POST /api/invoices/:id/nota-credito
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices/$INVOICE_ID/nota-credito" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "motivo": "Devolución de 2 unidades de café por producto dañado",
    "codigoReferencia": "01",
    "medioReembolso": "01"
  }'
CampoReq.Reglas
motivoSíEntre 3 y 180 caracteres. Va a <Detalle> de la nota.
codigoReferenciaSíDebe pertenecer al catálogo de códigos de referencia de la Nota 9 (véase 14.5). Si no, la API responde 400 con codigoReferencia inválido.
medioReembolsoNoMedio por el que se devuelve el importe, con el nombre interno (cash, card, sinpe, …). La NC no lleva nodo <MedioPago> en el XML: el valor se registra en el comprobante y el backend lo usa para el asiento contable de la nota cuando Hacienda la acepta. No es el código de la Nota 6. Véase 14.9.

11.2 · 02 - Nota de Débito Electrónica (ND)

POST /api/invoices/:id/nota-debito
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices/$INVOICE_ID/nota-debito" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "motivo": "Intereses por mora en el pago",
    "codigoReferencia": "10",
    "lineas": [
      {
        "codigoCabys": "5897000000000",
        "detalle": "Interés moratorio 30 días",
        "cantidad": 1,
        "precioUnitario": 1500,
        "tarifaIvaCode": "08"
      }
    ],
    "medioReembolso": "01"
  }'
CampoReq.Reglas
motivoSíEntre 3 y 180 caracteres. Va a <Detalle> de la nota.
codigoReferenciaSíDebe pertenecer al catálogo de códigos de referencia de la Nota 9 (véase 14.5). Si no, la API responde 400 con codigoReferencia inválido.
lineasNoLíneas de la nota. Sin ellas, la nota es total; con ellas, es parcial y el importe se calcula a partir de ellas. Cada línea lleva codigoCabys (obligatorio), detalle, cantidad (mayor que cero), precioUnitario (mayor o igual a cero) y, opcionalmente, tarifaIvaCode.
totalManualNoImporte total explícito, para la nota total sin líneas.
medioReembolsoNoMedio por el que se devuelve el importe, con el nombre interno (cash, card, …), igual que en la nota de crédito. La ND no lleva nodo <MedioPago> en el XML: el valor se registra en el comprobante y el backend lo usa para el asiento contable. Ojo: esta ruta lee medioReembolso; un medioPago se ignora en silencio. En la variante /api/facturacion/documents/:documentId/nota-debito el campo se llama medioPago (y significa lo mismo). Véase 14.9.

11.3 · 10 - Recibo Electrónico de Pago (REP)

POST /api/invoices/:id/recibo-pago
curl -X POST "https://sandbox.vision-total.rogcr.com/api/invoices/$INVOICE_ID/recibo-pago" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "monto": 5000,
    "motivo": "Abono a cuenta de la factura 001-00000000042",
    "medioPago": "01",
    "medioPagoOtros": null
  }'
CampoReq.Reglas
montoSíMayor que cero.
motivoSíMínimo 3 caracteres.
medioPagoSíMedio de pago con el código de Hacienda de la Nota 6: "01" Efectivo · "02" Tarjeta · "03" Cheque · "04" Transferencia · "05" Recaudado por terceros · "06" SINPE Móvil · "07" Plataforma Digital · "99" Otros. Cualquier otro valor se rechaza con 400. Aquí sí es el código de Hacienda, a diferencia del medioReembolso de la NC y la ND. Catálogo completo en 14.9.
medioPagoOtrosCond.Texto cuando el medio es "99" - Otros: es la descripción que aparece en la representación gráfica (Nota 7) y el XML exige que el nodo no venga vacío (3 a 100 caracteres). Si falta, la API responde 400 con medioPagoOtros requerido cuando medioPago es 99. Con advanceId el backend lo completa solo.
advanceIdNoSi se indica, el anticipo se aplica a la factura y el 10 - Recibo Electrónico de Pago (REP) se emite con TipoMedioPago = 99 y la referencia del anticipo como <MedioPagoOtros>.
El código de referencia del REP es siempre el 17

El REP no acepta un codigoReferencia del cuerpo: el backend escribe siempre el 17 - Pago a comprobante electrónico de la Nota 9, que es el que corresponde a un recibo de pago. Además, el monto no puede superar el saldo pendiente de la factura: si lo hace, la API responde 400 con «El monto del REP (X) excede el saldo pendiente (Y)».

11.4 · Consultar los documentos derivados

GET /api/facturacion/documents/:documentId/children?type=credit
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/children?type=credit" \
  -H "Authorization: Bearer $TOKEN"

12 Seguimiento, artefactos y correo

12.1 · Estado del comprobante

GET /api/facturacion/documents/:documentId/status
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/status" \
  -H "Authorization: Bearer $TOKEN"
GET /api/facturacion/documents/:documentId/status · respuesta 200
{
  "success": true,
  "data": {
    "documentId": "d41b...",
    "clave": "50624102600310123456700100001010000000042112345678",
    "documentType": "01",
    "estado": "aceptado",
    "detalleMensaje": "El documento fue aceptado por el sistema de facturación electrónica.",
    "mensajeHacienda": "<MensajeHacienda ...>...</MensajeHacienda>",
    "submittedAt": "2026-10-24T15:16:00.000Z",
    "acceptedAt": "2026-10-24T15:16:42.000Z",
    "rejectedAt": null,
    "cached": false
  }
}
Dos vocabularios de estado en el mismo campo

Cuando el estado viene de una consulta en vivo a Hacienda se observan aceptado, rechazado, procesando o error. Cuando proviene de la copia local del estado se observan draft, submitted, accepted, rejected o error. Normalice comparando con el conjunto de valores aceptados, no con igualdad estricta.

El estado decide, el detalle solo informa

Nunca decida el resultado de una factura leyendo el texto del detalleMensaje. Un comprobante aceptado puede traer advertencias en el detalle, y un detalle no vacío no significa que algo falló:

GET /api/facturacion/documents/:id/status · aceptado con advertencia
{
  "success": true,
  "data": {
    "estado": "accepted",
    "detalleMensaje": "Este comprobante fue recibido en el ambiente de pruebas, por lo cual no tiene validez para fines tributarios.\n\ncodigo, mensaje, fila, columna\n-20, \"\"El nodo denominado 'Número de teléfono receptor' no posee la estructura establecida para el mismo\"\", 0, 0"
  }
}

El estado era accepted: la factura es válida. El detalle traía un -20 sobre el formato del teléfono del receptor que no la invalidaba. Trate el estado como el veredicto y el detalle como contexto: sus códigos sirven para avisar de mejoras pendientes, no para dar por caída la emisión.

FIX decidir el resultado de la factura
// Mal: el detalle no está vacío, luego «algo falló».
// const fallida = Boolean(detalleMensaje)

// Bien: el estado es el que decide.
const ESTADOS_FINALES = new Set(["aceptado", "accepted", "rechazado", "rejected", "error"])
const fallida = !ESTADOS_FINALES.has(estado)

// Y las advertencias se pueden mostrar aparte, sin cambiar el veredicto.
const avisos = parseCodigosHacienda(detalleMensaje)   // p. ej. el -20 del teléfono

12.2 · Descargar PDF, XML firmado y MensajeHacienda

RutaDevuelveSi falta
GET /api/facturacion/documents/:id/pdfBytes del PDF (application/pdf).404 PDF not yet generated for this document
GET /api/facturacion/documents/:id/xmlBytes del XML firmado (application/xml).404 Signed XML not yet generated for this document
GET /api/facturacion/documents/:id/mensaje-xmlBytes del MensajeHacienda oficial.404 Mensaje de Hacienda not yet available for this document
GET /api/facturacion/documents/:documentId/pdf
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/pdf" \
  -H "Authorization: Bearer $TOKEN" \
  -o comprobante.pdf

12.3 · Listar comprobantes

GET /api/facturacion/documents?page=1&limit=20&estado=accepted
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/documents?page=1&limit=20&estado=accepted" \
  -H "Authorization: Bearer $TOKEN"
Este listado devuelve las filas tal cual

A diferencia del detalle de la factura (GET /api/invoices/:id), que pasa por un DTO, GET /api/facturacion/documents devuelve la entidad cruda del comprobante: incluye campos internos que no debería usar un integrador, como las claves de S3 del PDF y del XML firmado (pdfS3Key, signedXmlS3Key, mensajeHaciendaS3Key), el XML de respuesta completo (hiciendaResponseXml) y el deletedAt. Úselo para filtrar y paginar; para el detalle de un comprobante, use GET /api/invoices/:id, que devuelve un objeto limpio con hasPdf, hasSignedXml y hasMensajeHacienda.

ParámetroPor defectoNotas
page1Mínimo 1.
limit20Se acota entre 1 y 100.
estado—Filtro exacto sobre el estado almacenado.
from—Fecha mínima de creación.
to—Fecha máxima de creación.

12.4 · Reenviar el comprobante por correo

POST /api/facturacion/documents/:documentId/email
curl -X POST "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/email" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "additionalRecipients": ["contabilidad@wegner.cr"] }'

El destinatario principal es el correo del cliente del comprobante y no se puede reemplazar. Se aceptan hasta 10 destinatarios adicionales. El documento debe estar accepted; en caso contrario la API responde 409.

12.5 · Reenviar un comprobante rechazado

POST /api/facturacion/documents/:documentId/resend
curl -X POST "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/resend" \
  -H "Authorization: Bearer $TOKEN"
El reenvío crea un documento nuevo

Hacienda no admite la misma clave dos veces. El endpoint crea un nuevo HaciendaDocument con consecutivo y clave nuevos, hereda el tipo, la sucursal y el terminal, y enlaza al original mediante relatedDocumentId. Solo se puede reenviar un documento en estado rejected o error; en cualquier otro caso la API responde 409.

12.6 · Dónde leer el consecutivo de un comprobante emitido

El consecutivo de 20 dígitos es el número visible en el comprobante impreso, y es lo que permite auditar una factura contra el papel. La respuesta del facturar no lo incluye: se lee del detalle de la factura, en el array de documentos de Hacienda.

GET /api/invoices/:id · detalle con el consecutivo
{
  "data": {
    "id": "0e77...",
    "status": "invoiced",
    "haciendaStatus": "accepted",
    "haciendaDocuments": [
      {
        "id": "d41b...",
        "documentType": "01",
        "clave": "50630102600310987654300100001010000000138111089578",
        "numeroConsecutivo": "138",
        "estado": "accepted",
        "tenantEnvironment": "sandbox"
      }
    ]
  }
}
FIX después de POST /api/invoices/:id/facturar
const issued = await api(`/invoices/${id}/facturar`, { method: "POST", body: payload })

// El consecutivo no viene en la respuesta de emisión: se lee del detalle.
const detail = await api(`/invoices/${id}`)
const consecutivo = detail.data.haciendaDocuments?.[0]?.numeroConsecutivo

// Clave y consecutivo juntos permiten auditar:
//   clave 506 30 10 26 003109876543 00100001010000000138 1 11089578
//                                        └──── consecutivo 138 ────┘
No es lo mismo que /api/facturacion/consecutivos

El endpoint 7 · Consecutivos y numeración devuelve el último número usado por terminal, no el de una factura concreta. Para auditar un comprobante hay que leer numeroConsecutivo de su documento.

12.7 · Reencolar y configurar el tenant

MétodoRutaPropósito
POST/api/facturacion/documents/:documentId/submitReencolar el envío de un documento que se quedó en borrador. Responde 202 con { jobId, clave, documentId }.
GET/api/facturacion/tenant/statusEstado de la habilitación: ACTIVE o PENDING_SETUP, con las credenciales, el certificado, readyForActivation y la lista missing.
POST/api/facturacion/tenant/activateActiva el tenant en Hacienda. Responde 400 con FACTURACION_SETUP_INCOMPLETE y la lista missing si falta algo.
POST/api/facturacion/tenant/certificatesSube el certificado .p12 (multipart, campo file, máximo 10 MB) con pin, issuedAt y expiresAt.
POST/api/facturacion/tenant/hacienda-credentialsRegistra las credenciales del IDP: environment (sandbox o production), username, password y clientId.
La habilitación sigue siendo tarea del administrador

Estas rutas existen y funcionan, pero la habilitación de la facturación de una organización sigue siendo una tarea administrativa del ERP: la recomienda el usuario de referencia de 7. El servicio externo las consulta para diagnóstico, no para configurar.

13 Comprobantes de compra y Mensaje del Receptor

El lado de la compra es el espejo: en lugar de emitir, la organización recibe comprobantes de terceros y confirma su aceptación con el Mensaje del Receptor (05 - Mensaje de aceptación total, 06 - Mensaje de aceptación parcial y 07 - Mensaje de rechazo).

Quién emite cada documento

Los tres tipos de Mensaje del Receptor son los únicos que emite la organización como receptor de una compra. El tipo lo determina el tipoMensaje del formulario: 1 produce un 05, 2 un 06 y 3 un 07. El comprobante que se está confirmando —FE, ND, NC, TE, FEC, FEE o REP— lo emite el vendedor y solo se recibe aquí.

13.1 · Cargar el comprobante y generar el MR

POST /api/purchases/upload
curl -X POST "https://sandbox.vision-total.rogcr.com/api/purchases/upload" \
  -H "Authorization: Bearer $TOKEN" \
  -F "files=@/ruta/comprobante.xml" \
  -F 'messageConfig={"tipoMensaje":"1","condicionImpuesto":"01","codigoActividad":"4711.1","clasificacion":"compra"}'
Campo del MRReq.Valores
tipoMensajeSí1 - Aceptación total · 2 - Aceptación parcial · 3 - Rechazo.
condicionImpuestoNo01 - Genera crédito IVA · 02 - Crédito parcial · 03 - Bienes de capital · 04 - Gasto corriente no genera crédito · 05 - Proporcionalidad.
codigoActividadNoCIIU4 del receptor, con punto (4711.1), máximo 6 caracteres. Debe estar entre las actividades económicas registradas para la organización, que se listan en GET /api/organizations/:id/activities. Véase 8.7.
detalleMensajeNoTexto libre, máximo 160 caracteres.
montoImpuestoAcreditarNoImporte del crédito de IVA. Número JSON (no texto) y mayor o igual a cero.
montoGastoAplicableNoImporte del gasto aplicable. Número JSON (no texto) y mayor o igual a cero.
clasificacionNocompra o gasto. Es metadata interna, no se envía a Hacienda. Por defecto compra.
Parte del multipartReglas
filesEl XML firmado del comprobante. Hasta 50 archivos, 10 MB cada uno, y sólo con extensión .xml (el backend no filtra por MIME type, pero rechaza lo que no sea .xml).
messageConfigUn único campo con el JSON del MR. Si se omite, el comprobante se registra pero no se genera ningún MR.
Qué comprobantes se pueden cargar

El backend reconoce el tipo por el namespace del XML y acepta siete comprobantes: FE (01), TE (04), NC (03), ND (02), FEC (08), FEE (09) y REP (10). El documentTypeLabel que devuelve la respuesta trae el código y el nombre completo («01 FE — Factura Electrónica»). Un XML de MensajeReceptor o de MensajeHacienda se rechaza: no son comprobantes.

POST /api/purchases/upload · respuesta 201
{
  "success": true,
  "data": {
    "exitosos": 1,
    "fallidos": 0,
    "advertencias": [],
    "comprasAfectadas": ["c3f1..."],
    "archivos": [
      {
        "filename": "comprobante.xml",
        "size": 24817,
        "status": "ok",
        "documentType": "01",
        "documentTypeLabel": "01 FE — Factura Electrónica",
        "clave": "50624102600310123456700100001010000000042112345678",
        "detectedNamespaceKey": "FE",
        "purchaseInvoiceId": "c3f1...",
        "purchaseMessageId": "a91c...",
        "s3Key": "compras/c3f1.../comprobante.xml",
        "mensajeReceptor": {
          "purchaseInvoiceId": "c3f1...",
          "status": "ok",
          "clave": "50624102600310123456700100001010000000042112345678",
          "estadoEnvio": "no_enviado"
        }
      }
    ],
    "mensajes": {
      "exitosos": 1,
      "fallidos": 0,
      "omitidos": 0,
      "resultados": [
        {
          "purchaseInvoiceId": "c3f1...",
          "status": "ok",
          "clave": "50624102600310123456700100001010000000042112345678",
          "estadoEnvio": "no_enviado"
        }
      ]
    }
  }
}
Un archivo puede fallar sin que falle la llamada

La respuesta es 201 aunque algunos archivos se rechacen. El detalle está en archivos[].status y archivos[].errorCode:

  • NOT_XML_FILE — la extensión no es .xml.
  • FILE_TOO_LARGE — supera los 10 MB.
  • EMPTY_FILE — el archivo está vacío.
  • INVALID_HACIENDA_NAMESPACE — el XML no es de un comprobante de Hacienda.
  • MENSAJE_HACIENDA_NOT_SUPPORTED · MENSAJE_RECEPTOR_NOT_SUPPORTED — es un mensaje, no un comprobante.
  • NO_CLAVE — no tiene una clave válida de 50 dígitos.

El MR se firma y se encola después de la respuesta: la llamada no espera a Hacienda. El MR también se puede consultar por separado, con POST /api/purchases/mensaje-recepcion/sync.

Prerrequisitos para enviar el MR

Si a la organización le falta cualquiera de estos, el MR no sale y el resultado aparece como fallido, sin romper la carga de los comprobantes (que ya quedaron registrados):

  • Organization.taxId con formato de 9 a 12 dígitos.
  • Organization.certificate_s3_key y pin_encrypted: el certificado .p12 vigente. Si venció, el código es CERTIFICATE_EXPIRED; si el PIN no abre el certificado, CERTIFICATE_INVALID_PIN.
  • Organization.hacienda_client_id, username y password: las credenciales del IDP.
  • Al menos una fila en organization_activities (preferiblemente con es_principal = true): es la que alimenta el codigoActividad del MR.

13.2 · Consultar el comprobante de compra

MétodoRutaPermisoPropósito
GET/api/purchasespurchases:viewListar comprobantes de compra. Devuelve { success: true, data: [] }, sin paginación.
GET/api/purchases/:idpurchases:viewDetalle del comprobante, con líneas, pagos, totales, archivos, comentarios y el bloque mensajeReceptor.
PATCH/api/purchases/:idpurchases:editEditar las notas ({ notes }, máximo 2000 caracteres; null las borra).
DELETE/api/purchases/:idpurchases:deleteBaja lógica. Responde 200 con { success: true }.
POST/api/purchases/:id/sync-haciendapurchases:sync_haciendaVolver a sincronizar el comprobante con Hacienda.
POST/api/purchases/mensaje-recepcion/syncpurchases:sync_haciendaConsultar el estado de un MR concreto. Cuerpo: { purchaseMessageId } o { clave }.
GET/api/purchases/:id/files/:fileId/urlpurchases:viewURL prefirmada de S3 para el XML o un PDF adjunto. Vence a los 300 segundos.
GET/api/purchases/:id/commentspurchases:viewListar los comentarios del comprobante.
POST/api/purchases/:id/commentspurchases:editAgregar un comentario ({ body }, máximo 2000 caracteres). Responde 201.
DELETE/api/purchases/:id/comments/:commentIdpurchases:editBorrar un comentario. Sólo su autor puede: otro usuario recibe 403.
POST/api/purchases/:id/attachmentspurchases:editAdjuntar archivos (multipart, campo files, hasta 10 archivos de 10 MB, sin filtro de extensión).
GET/api/reports/purchasespurchases:view_reportLibro de Compras: filas, totales por moneda y totales por día.
GET/api/reports/purchases/export.xlsxpurchases:view_reportEl Libro de Compras en XLSX.
Filtro de GET /api/purchasesNotas
documentTypeArreglo de códigos 01…10. Se envía como arreglo JSON, no como cadena separada por comas.
dateFrom / dateToFecha y hora en formato ISO (2026-11-01T00:00:00.000Z).
emisorNumeroCoincidencia exacta con la identificación del emisor.
medioPagoCódigo del medio de pago de la Nota 6 (01…07, 99).
textBúsqueda parcial en el nombre del emisor o en la clave.
El bloque mensajeReceptor

El detalle de GET /api/purchases/:id incluye un bloque mensajeReceptor con los datos del último MR generado:

  • id · tipo (1, 2 o 3) · detalle · montoAceptado.
  • estadoEnvio — no_enviado · procesando · aceptado · rechazado · error.
  • clave · consecutivoReceptor (20 dígitos) · fechaEmision.
  • respuestaAt · respuestaDetalle · respuestaCodigo · mensajeHaciendaXml.
Sondeo con la clave del comprobante original

El MR se consulta en Hacienda con la clave del comprobante que se está confirmando, no con la clave del propio MR. El NumeroConsecutivoReceptor que devuelve la respuesta es lo que permite correlacionar el mensaje correcto.

El Libro de Compras tiene sus reglas

GET /api/reports/purchases sólo incluye los comprobantes en estado aceptado, y excluye los tipos 08 (FEC) y 10 (REP), porque no son compras. Las notas de crédito (03) entran con el signo cambiado. El XLSX trae los totales en colones y en dólares, cada uno con su propio pie.

14 Catálogos de Hacienda

Los códigos que acepta la API son los del catálogo oficial de la DGT (v4.4). Este es el referencia rápida para los valores que aparecen en los cuerpos de la request.

14.1 · Tipo de documento

Tipo de documentoEmisorSe emite con
01 - Factura Electrónica (FE)VendedorPOST /api/invoices/:id/facturar
02 - Nota de Débito Electrónica (ND)VendedorPOST /api/invoices/:id/nota-debito
03 - Nota de Crédito Electrónica (NC)VendedorPOST /api/invoices/:id/nota-credito
04 - Tiquete Electrónico (TE)VendedorPOST /api/invoices/:id/facturar
05 - Mensaje de aceptación totalReceptorPOST /api/purchases/upload
06 - Mensaje de aceptación parcialReceptorPOST /api/purchases/upload
07 - Mensaje de rechazoReceptorPOST /api/purchases/upload
08 - Factura Electrónica de Compra (FEC)ReceptorPOST /api/invoices/:id/facturar
09 - Factura Electrónica de Exportación (FEE)VendedorPOST /api/invoices/:id/facturar
10 - Recibo Electrónico de Pago (REP)VendedorPOST /api/invoices/:id/recibo-pago
Qué emite cada flujo

El Facturador emite siete de los diez tipos: 01 - Factura Electrónica (FE), 04 - Tiquete Electrónico (TE), 08 - Factura Electrónica de Compra (FEC), 09 - Factura Electrónica de Exportación (FEE) y las tres derivadas 02 - Nota de Débito Electrónica (ND), 03 - Nota de Crédito Electrónica (NC) y 10 - Recibo Electrónico de Pago (REP). Los tres restantes — 05, 06 y 07— los emite la organización como receptor en el flujo de compras, a través del POST /api/purchases/upload. Véase 13 · Comprobantes de compra y Mensaje del Receptor.

El mismo código de dos dígitos es el que viaja en el NumeroConsecutivo de 20 dígitos y en la clave de 50 dígitos, así que sirve para reconciliar un comprobante contra el papel.

14.2 · Condición de venta

Condición de venta
01 - Contado
02 - Crédito
03 - Consignación
04 - Apartado
05 - Arrendamiento con opción de compra
06 - Arrendamiento en función financiera
07 - Cobro a favor de un tercero
08 - Servicios prestados al Estado
09 - Pago de servicios al Estado
10 - Venta a crédito IVA hasta 90 días
11 - Pago de venta a crédito IVA 90 días
12 - Venta de mercancía no nacionalizada
13 - Venta de bienes usados
14 - Arrendamiento operativo
15 - Arrendamiento financiero
99 - Otros (requiere condicionVentaOtros)
Dos códigos no valen para la FEC ni la FEE

La 08 - Factura Electrónica de Compra (FEC) y la 09 - Factura Electrónica de Exportación (FEE) aceptan un subconjunto restringido de condiciones de venta: 09 - Pago de servicios al Estado y 11 - Pago de venta a crédito IVA 90 días quedan fuera y la API los rechaza. Para la FE, el TE y las notas derivadas no hay restricción: valen los dieciséis códigos.

14.3 · Impuesto y tarifa de IVA

Son dos catálogos distintos y se validan por separado: CodigoImpuesto dice qué impuesto grava la línea; CodigoTarifaIVA dice qué porcentaje de ese impuesto se aplica. En el XML viajan en nodos separados (<Codigo> y <CodigoTarifaIVA>), dentro del mismo <Impuesto> de la línea.

CodigoImpuesto — Nota 8

CódigoDescripción oficialReglas de la API
01Impuesto al Valor AgregadoValor por defecto. Exige un tarifaIvaCode válido.
02Impuesto Selectivo de ConsumoSin desglose de tarifa: el nodo de tarifa no viaja.
03Impuesto Único a los CombustiblesExige taxDetail (cantidadUnidadMedida, impuestoUnidad).
04Impuesto específico de Bebidas AlcohólicasExige taxDetail.
05Impuesto Específico sobre las bebidas envasadas sin contenido alcohólico y jabones de tocadorExige taxDetail.
06Impuesto a los Productos de TabacoExige taxDetail.
07IVA (cálculo especial) 28Monto = BaseImponible × Tarifa. Exige un tarifaIvaCode válido. La base imponible puede editarse con taxDetail.factorBaseImponible: BaseImponible = Subtotal × factor (sin factor, la base es el subtotal).
08IVA Régimen de Bienes Usados (Factor)El monto se calcula como BaseImponible × factorCalculoIVA, no como un porcentaje.
12Impuesto Específico al CementoTarifa fija del 5 %: Monto = Subtotal × 5 % (sobre el precio neto de la venta). No usa taxDetail.
99OtrosExige taxDetail.descripcionImpuesto (5 a 100 caracteres), que viaja como <CodigoImpuestoOTRO> (obligatorio con este código). La tarifa es editable y se envía como entero.
Nota 28 · para qué sirve el código 07

«Este código se utilizará cuando se realicen ventas de bienes o servicios donde el cálculo de la base imponible del IVA sea especial, por ejemplo, bienes usados, ventas por catálogo, bebidas alcohólicas o no alcohólicas». Es el 07 - IVA (cálculo especial): se usa cuando la base imponible del IVA no se calcula de forma ordinaria.

Los diez códigos de la Nota 8 se emiten

POST y PUT /api/products validan el catálogo completo y el desglose de la línea se arma para los diez códigos: 01, 02, 03, 04, 05, 06, 07, 08, 12 y 99. El cálculo vive en una sola función (calcularImpuestoLinea), compartida por el POS y el Facturador, así que un mismo producto produce el mismo desglose en los dos flujos.

Dos códigos necesitan datos extra en el producto: 07 acepta taxDetail.factorBaseImponible y 99 exige taxDetail.descripcionImpuesto. Sin ese dato, la emisión falla con un 400 que dice exactamente qué corregir en la ficha del producto.

CodigoTarifaIVA — Nota 8.1

CódigoTarifa oficial%Notas
01Tarifa 0% (Artículo 32, num 1, RLIVA)0Solo transacciones del artículo 32.1 del RLIVA, con derecho a crédito pleno (por ejemplo, ventas a la CCSS o municipalidades).
02Tarifa reducida 1%1—
03Tarifa reducida 2%2—
04Tarifa reducida 4%4—
05Transitorio 0%0Solo para Notas de Crédito y Notas de Débito que lo requieran.
06Transitorio 4%4Solo para Notas de Crédito y Notas de Débito que lo requieran.
07Tarifa transitoria 8%8Código inhabilitado en la versión 4.4.
08Tarifa general 13%13Valor por defecto cuando no se envía tarifaIvaCode.
09Tarifa reducida 0.5%0.5—
10Tarifa Exenta0Tarifa exenta de la Ley 9635, artículo 8.
11Tarifa 0% sin derecho a crédito0Bien o servicio no sujeto que no otorga derecho a crédito.

El nodo <CodigoTarifaIVA> solo se emite cuando el código de impuesto es 01 o 07. Para el resto de los códigos el porcentaje se declara en <Tarifa> (o no aplica), y el catálogo de tarifas no viaja en el XML.

14.4 · Exoneraciones

La exoneración es un nodo por línea (<Exoneracion>, dentro del <Impuesto> de la línea) y se declara con los campos exo* de items[]. El backend valida los dos catálogos: un código fuera de cualquiera de ellos no es un error de la API, pero el nodo no se emite y la línea se factura gravada.

Tipo de documento de exoneración
01 - Compras autorizadas por la DGT
02 - Ventas exentas a diplomáticos
03 - Autorizado por Ley especial
04 - Exenciones DGT, autorización local genérica
05 - Exenciones DGT, transitorio V
06 - Servicios turísticos (ICT)
08 - Exoneración a zona franca
09 - Servicios complementarios para la exportación
10 - Órgano de las corporaciones municipales
11 - Exenciones DGT, autorización local concreta
99 - Otros

Institución que otorga la exoneración

CódigoInstitución
01Ministerio de Hacienda
02Ministerio de Relaciones Exteriores y Culto
03Ministerio de Agricultura y Ganadería
04Ministerio de Economía, Industria y Comercio
05Cruz Roja Costarricense
06Benemérito Cuerpo de Bomberos de Costa Rica
07Asociación Obras del Espíritu Santo
08Fecrunapa
09EARTH
10INCAE
11Junta de Protección Social (JPS)
12ARESEP — la que autoriza el incentivo de los vehículos eléctricos
99Otros (con exoNombreInstitucionOtros)

Las reglas que el backend aplica

ReglaComportamiento real
Cuatro campos obligatorios exoTipoDocumento, exoNumeroDocumento, exoFechaEmision y exoNombreInstitucion. Si falta uno, el nodo <Exoneracion> no se emite y la línea se factura gravada — sin error.
El monto es derivado MontoExoneracion = base imponible × (tarifa exonerada ÷ 100), y nunca puede superar el IVA de la línea. Los overrides de tarifa y de monto son opcionales; el de monto vale para un surtido con tarifas distintas.
Sólo en líneas gravadas Una exoneración en una línea con tarifaIvaCode 01 (0 % Art. 32) o 11 (0 % sin crédito) se rechaza con 400: esa línea es No Sujeto y no tiene IVA que exonerar. Deje la línea en 08 (13 %).
El número es genérico 3 a 40 caracteres. El formato AL-NNNNNNNN-NN es sólo del tipo 01 (compras autorizadas por la DGT); los demás tipos llevan la referencia de la ley, el decreto o la resolución.
Efecto en los totales El impuesto neto de la línea baja a cero y el total de la factura baja con él: hay que pagar el total nuevo. Véase 10.5 · Caso completo.

14.5 · Códigos de referencia para notas

Código de referencia
01 - Anula documento de referencia
02 - Corrige monto
04 - Referencia a otro documento
05 - Sustituye comprobante por contingencia
06 - Devolución de mercancía
07 - Sustituye comprobante electrónico
08 - Factura endosada
09 - Nota de crédito financiera
10 - Nota de débito financiera
11 - Proveedor no domiciliado
12 - Crédito por exoneración posterior
13 - Anula por error material
14 - Corrige monto por error material
15 - Sustituye por error material
16 - Comprobante electrónico rechazado
17 - Pago a comprobante electrónico
99 - Otros

14.6 · Otros cargos

Concepto (tipoDocumentoOC)
01 - Contribución parafiscal
02 - Timbre de la Cruz Roja
03 - Timbre de bomberos
04 - Cobro de un tercero
05 - Costos de exportación
06 - Impuesto de servicio 10 %
07 - Timbre de colegios profesionales
08 - Depósitos de garantía
09 - Multas o penalizaciones
10 - Intereses moratorios
99 - Otros cargos

El tipo 99 - Otros cargos exige tipoDocumentoOTROS con 5 a 100 caracteres, y el tipo 04 - Cobro de un tercero exige la identificación y el nombre del tercero. Esa identificación del tercero usa el mismo catálogo de la 14.7: 01 a 06.

La forma de cada cargo

El campo otrosCargos de la factura es un arreglo con un máximo de 15 conceptos. Cada objeto acepta estos campos:

CampoReq.Reglas
tipoDocumentoOCSíEl concepto del catálogo de la Nota 16 (la tabla de arriba). Fuera de él, la API responde 400 con OTRO_CARGO_TIPO_INVALIDO.
detalleSíDescripción del cargo. Máximo 160 caracteres.
montoCargoSíImporte del cargo. Debe ser un número mayor que cero.
porcentajeOCNoPorcentaje del cargo, cuando se calcula sobre una base.
tipoDocumentoOTROSCond.Obligatorio con el código 99: 5 a 100 caracteres.
identificacionTerceroCond.Objeto { tipo, numero } con el tipo del catálogo de la Nota 4 (01…06). Obligatorio con el código 04.
nombreTerceroCond.Nombre del tercero. Obligatorio con el código 04.
Los otros cargos no son impuestos

Un cargo se suma al total de la factura, pero no genera impuesto: no lleva <CodigoImpuesto> ni aparece en el desglose de IVA. Por eso no se suman al taxTotal de la factura, sino al totalotrosCargos, que viaja por separado en el XML.

14.7 · Tipo de identificación (Nota 4)

El catálogo de la Nota 4 del esquema v4.4 dice qué tipo de identificación es la que declara un contribuyente. Es el valor que viaja en <Identificacion><Tipo> del emisor, del receptor y del tercero de Otros Cargos, y el que la API expone como governmentIdType en los clientes y identificationType en los proveedores.

CódigoTipo de identificaciónFormato admitidoDónde aplica
01Cédula Física9 a 12 dígitos.Emisor, receptor y tercero.
02Cédula Jurídica9 a 30 caracteres alfanuméricos.Emisor, receptor y tercero.
03DIMEX9 a 12 dígitos.Emisor, receptor y tercero.
04NITE9 a 12 dígitos.Emisor, receptor y tercero.
05Extranjero No Domiciliado 175 a 30 caracteres alfanuméricos, con guiones y puntos.Emisor solo en la FEC; receptor de la FE solo con condicionVenta 12; también habilitada en la FEE y el TE.
06No Contribuyente 189 a 20 caracteres alfanuméricos.Emisor solo en la FEC y solo con condicionVenta 13.
Los dos códigos restringidos

Nota 17 — Extranjero No Domiciliado (05): estará disponible en los Datos del Emisor de la Factura Electrónica de Compra únicamente para el registro de las adquisiciones de servicios o bienes intangibles a proveedores no domiciliados en Costa Rica, según el artículo 29 inciso d) y el Transitorio XV del Reglamento del IVA. En Factura Electrónica estará disponible únicamente en los Datos del Receptor, y solo cuando en el campo «Condición de la Venta» se haya seleccionado el código 12 «Ventas de Bienes No Nacionalizados». Este código también estará habilitado para su uso en la Factura Electrónica de Exportación y el Tiquete Electrónico.

Nota 18 — No Contribuyente (06): estará disponible en los Datos del Emisor de la Factura Electrónica de Compra únicamente para el registro de las adquisiciones de bienes usados vendidos por un no contribuyente, para los efectos contables que corresponda. Estará disponible únicamente cuando en el campo «Condición de la Venta» se haya seleccionado el código 13 «Ventas Bienes Usados No Contribuyentes».

14.8 · La identificación en la clave numérica (Nota 4.1)

La clave de 50 dígitos reserva doce posiciones para la identificación del emisor. Para llenarlas hay que agregar ceros al inicio del número de identificación, y cuántos ceros dependen del tipo:

Tipo de identificaciónRegla de rellenoEjemplo
01 - Cédula FísicaAgregar tres ceros al inicio hasta completar 12 dígitos.123456789 → 000123456789
02 - Cédula JurídicaAgregar dos ceros al inicio hasta completar 12 caracteres.3101234567 → 003101234567
03 - DIMEXSi la cédula tiene 11 dígitos, agregar un cero al inicio hasta completar 12 dígitos.12345678901 → 012345678901
04 - NITEAgregar dos ceros al inicio hasta completar 12 dígitos.1234567890 → 001234567890
El relleno es del backend

El servicio externo nunca escribe la clave: el backend compone la clave de 50 dígitos y aplica este relleno antes de firmar. Lo único que el integrador puede hacer es escribir la identificación del emisor sin ceros —la del cliente, la del proveedor— y dejar que el backend la complete. Véase el diagrama 5.

14.9 · Medio de pago (Notas 6 y 7)

El código del medio de pago viaja en <TipoMedioPago> dentro del nodo <MedioPago> del resumen. La Nota 6 del esquema v4.4 fija la codificación; la Nota 7 exige que, para efectos de visualización e impresión, se muestre la descripción del medio y no su código.

CódigoDescripción oficial (Nota 6)method internoNotas
01Efectivocash—
02Tarjetacard—
03Chequecheck—
04Transferencia – depósito bancariotransfer—
05Recaudado por terceros 27third_partySólo cuando el pago lo recibe un tercero y no quien emite el servicio o la mercancía.
06SINPE Móvilsinpe—
07Plataforma Digitaldigital_platform—
99OtrosotherLa descripción del medio se indica en la representación gráfica: el XML exige <MedioPagoOtros>.
Nota 27 · cuándo usar «Recaudado por terceros»

«El código 06, Recaudado por terceros se utilizará cuando el pago sea recibido por un tercero y no por quien emite el servicio o mercancía, ejemplo: pago de recibos telefónicos en supermercados».

Ojo con el pie de nota: dice «el código 06», pero en la tabla el 05 es «Recaudado por terceros» y el 06 es «SINPE Móvil». Manda la tabla, que es la que usa el backend.

Nota 7 · lo que se imprime

En el PDF del comprobante el medio de pago aparece con su descripción («Efectivo», «Tarjeta», «Cheque», «Transferencia - depósito bancario», «Recaudado por terceros», «SINPE Móvil», «Plataforma Digital»), nunca con el código suelto. Con 99 - Otros la descripción del medio se agrega entre paréntesis y es obligatoria: el nodo <MedioPagoOtros> del XML exige entre 3 y 100 caracteres y Hacienda rechaza el comprobante si falta.

Dónde se usa cada vocabulario

CampoRutaQué valor espera
payments[].method POST /api/invoices/:id/payments El nombre interno (cash, card, check, transfer, third_party, sinpe, digital_platform, other, advance). El backend lo traduce al TipoMedioPago de la Nota 6.
medioPago POST /api/invoices/:id/recibo-pago El código de Hacienda ("01"…"07", "99"). Con "99" el campo medioPagoOtros es obligatorio.
medioReembolso POST /api/invoices/:id/nota-credito
POST /api/invoices/:id/nota-debito
El nombre interno (cash, card, …), como en payments[].method. La NC y la ND no llevan nodo <MedioPago> en el XML: el valor queda registrado en el comprobante y el backend lo usa para el asiento contable de la nota.
medioPago POST /api/facturacion/documents/:id/nota-debito El nombre interno, como en el medioReembolso de la nota de débito sobre factura. Es la variante del POS que usa el nombre medioPago en lugar de medioReembolso, pero el valor significa lo mismo.
No escriba el código donde va el nombre

Mandar "01" en payments[].method rompe la llamada antes de llegar al XML: la columna method es un enum de Postgres que sólo admite los nombres internos, y el valor se rechaza al guardar el pago. Y mandar "cash" en el medioPago de un REP es peor: ese campo se escribe tal cual en <TipoMedioPago>, donde Hacienda sólo admite los códigos de la Nota 6, y el comprobante se rechaza entero. Es el único punto de la API donde conviven los dos vocabularios.

El advance no es un medio de la Nota 6

La línea de pago con method: "advance" aplica un anticipo (RC o PP). El backend lo traduce a TipoMedioPago = 99 y escribe en <MedioPagoOtros> la referencia del anticipo, que es lo que Hacienda ve en la representación gráfica. Para el servicio externo, advance es un valor más de method, con la condición de que vaya acompañado de advanceId.

El catálogo de impuestos (CodigoImpuesto, Nota 8) está en 14.3, y el de las tarifas de IVA (CodigoTarifaIVA) en la misma sección: son catálogos separados y no se sustituyen uno al otro.

14.10 Descuentos de la línea (Nota 20)

El nodo <Descuento> de cada línea de detalle lleva el monto del descuento, el código de la Nota 20 y la naturaleza del descuento. Los códigos se mandan en items[].codigoDescuento.

CódigoTipo de descuento (Nota 20)Notas
01Descuento por RegalíaEl IVA de la línea lo asume el emisor: el descuento debe ser el 100 % del monto total de la línea. Véase el recuadro de abajo.
02Descuento por Regalía o Bonificación. IVA Cobrado al ClienteEl IVA sí se cobra al cliente sobre la base neta. Admite descuento parcial.
03Descuento por BonificaciónComo el 01: el IVA lo asume el emisor y el descuento es del 100 %.
04Descuento por volumen—
05Descuento por temporada (estacional)—
06Descuento promocional—
07Descuento comercial—
08Descuento por frecuencia—
09Descuento sostenido—
99Otros descuentosExige codigoDescuentoOtro (5–100 caracteres).
CampoRequeridoReglas
items[].discountNoMonto del descuento de la línea. No puede superar el monto total de la línea (cantidad × precio); si lo supera, la API responde 400 con INVALID_MONTO_DESCUENTO.
items[].codigoDescuentoNoCódigo de la Nota 20. Sin monto de descuento no se persiste ningún código. Un código fuera del catálogo se rechaza con INVALID_CODIGO_DESCUENTO.
items[].codigoDescuentoOtroNoObligatorio con el código 99 (5–100 caracteres). Con otro código se ignora.
items[].naturalezaDescuentoNoLa <NaturalezaDescuento> es obligatoria con cualquier descuento (3–80 caracteres). Si no se manda, el backend precarga la del código elegido.
Los datos fiscales de la línea se heredan del producto

items[].taxDetail es un override: los campos que trae la línea pisan a los del producto y los que no trae se heredan de él. Por eso una línea que sólo cambia un campo no pierde el resto del detalle fiscal del producto (por ejemplo, la descripcionImpuesto del código 99).

El backend valida al guardar la factura lo que la Nota 8 exige por código de impuesto, y responde 400 en vez de dejar que el comprobante sea rechazado por Hacienda:

  • 01 / 07 — tarifaIvaCode válido (INVALID_TARIFA_IVA).
  • 03 – 06 — cantidadUnidadMedida e impuestoUnidad (INVALID_DATOS_IMPUESTO_ESPECIFICO).
  • 08 — factorCalculoIVA mayor que cero (INVALID_FACTOR_CALCULO_IVA).
  • 99 — descripcionImpuesto de 5 a 100 caracteres (INVALID_DESCRIPCION_IMPUESTO) y una tarifa mayor que cero (INVALID_TARIFA_OTROS).
Regalías y bonificaciones (códigos 01 y 03)

Con los códigos 01 y 03 la línea no es una venta: es una regalía o una bonificación, y por eso el IVA lo asume el emisor. Hacienda corre tres validaciones de runtime que no están en el XSD:

  • -518 — el descuento debe ser el 100 % del MontoTotal de la línea. Un descuento parcial se rechaza en la API con 400 (INVALID_CODIGO_DESCUENTO_TOTAL) antes de llegar a Hacienda.
  • -45 — el IVA se calcula sobre el MontoTotal de la línea, no sobre la base imponible.
  • -476 / -488 — ese IVA se declara en <ImpuestoAsumidoEmisorFabrica> y no entra al TotalDesgloseImpuesto (el desglose del IVA cobrado).

Ejemplo de una línea de $150 con el 100 % de descuento a tarifa 13 %: MontoDescuento 150.00, SubTotal 0.00, Impuesto/Monto 19.50, ImpuestoAsumidoEmisorFabrica 19.50, ImpuestoNeto 0.00. El total de la factura no incluye ese IVA: coincide con el <TotalComprobante> del XML.

Si lo que quiere es un descuento parcial normal (una promoción, un descuento comercial), use el código 06, 07 u otro de la Nota 20 que admita descuento parcial: el 02 también, porque su IVA se cobra al cliente.

15 Catálogo de endpoints

Resumen operativo de la superficie HTTP. Todas las rutas son relativas a la URL base https://sandbox.vision-total.rogcr.com y llevan el prefijo /api.

Sesión de la API (integraciones)

La API se consume con la credencial de máquina (Authorization: Bearer vt_m2m_…). La sesión ya está abierta en la primera llamada: no hay login ni select-organization. Véase 4.

MétodoRutaAutenticaciónPropósito
GET/api/m2m/meCredencial M2MÚnico endpoint de sesión del integrador. Verifica la credencial: organización, rol y nombre. Con un JWT de usuario responde 403. Véase 4.3.
GET/api/permissionsCredencial M2MCatálogo de permisos del sistema, para validar el rol de la credencial.

Sesión interactiva del ERP web (no aplica a integraciones)

Estas rutas autentican a una persona en el ERP web. No son el camino de una integración por API: con una credencial de máquina, las que acuñan tokens devuelven 403. Véase 4.6.

MétodoRutaAutenticaciónPropósito
POST/api/auth/login—Iniciar sesión de usuario (correo y contraseña).
POST/api/auth/login/challenge—Resolver el segundo factor.
POST/api/auth/select-organizationJWT de usuarioEmitir el token con organizationId. 403 con una credencial de máquina.
GET/api/auth/meJWT de usuarioEstado de la sesión y organizaciones del usuario.
PUT/api/auth/change-passwordJWT de usuarioCambiar la contraseña propia. 403 con una credencial de máquina.

Organización y permisos

MétodoRutaPermisoPropósito
GET/api/organizationsCredencial M2MLa organización de la credencial, con su id y el estado de facturación.
GET/api/organizations/:idCredencial M2MDetalle de la organización activa.
GET/api/organizations/:id/activitiesCredencial M2MActividades económicas (CIIU4) registradas para la organización. La principal es la que se usa como <CodigoActividadEmisor>. Véase 8.7.
POST/api/organizations/:id/activitiesorganization:edit_organizationDar de alta una actividad económica (CIIU4). Cuerpo: codigo (1 a 6 caracteres), descripcion, esPrincipal.
PUT/api/organizations/:id/activities/:codigoorganization:edit_organizationEditar la descripción o marcar la actividad principal.
DELETE/api/organizations/:id/activities/:codigoorganization:edit_organizationEliminar una actividad económica.
GET/api/permissionsCredencial M2MCatálogo de permisos del sistema.

Facturador (facturas de venta)

MétodoRutaPermisoPropósito
POST/api/invoicesinvoices:create_invoiceCrear la factura o cotización.
GET/api/invoicesinvoices:view_invoicesListar con filtros y paginación. Sin líneas ni pagos: pida el detalle para eso.
GET/api/invoices/:idinvoices:view_invoicesDetalle con líneas, pagos y comprobantes. Aquí está numeroConsecutivo.
PATCH/api/invoices/:idinvoices:edit_invoiceEditar la factura.
DELETE/api/invoices/:idinvoices:delete_invoiceBaja lógica.
POST/api/invoices/:id/paymentsinvoices:add_paymentRegistrar pagos.
DELETE/api/invoices/:id/payments/:paymentIdinvoices:add_paymentEliminar un pago.
POST/api/invoices/:id/transitionsSegún la acciónquote · pay · invoice · quote_and_invoice · cancel. Cada acción exige su propio permiso. Véase 10.4.
POST/api/invoices/:id/facturarinvoices:transition_invoiceEmitir ante Hacienda.
POST/api/invoices/:id/nota-creditoinvoices:issue_credit_noteEmitir 03 - Nota de Crédito Electrónica (NC) sobre el comprobante aceptado de la factura: FE, TE, FEC, FEE o REP.
POST/api/invoices/:id/nota-debitoinvoices:issue_debit_noteEmitir 02 - Nota de Débito Electrónica (ND) sobre el comprobante aceptado de la factura: FE, TE, FEC, FEE o REP.
POST/api/invoices/:id/recibo-pagoinvoices:issue_credit_noteEmitir 10 - Recibo Electrónico de Pago (REP) sobre el comprobante aceptado de la factura: FE, TE, FEC, FEE o REP.
GET/api/invoices/:id/quotation-pdfinvoices:view_invoicesDescargar la cotización en PDF.
GET/api/invoicing/recordsinvoices:view_invoicesListado unificado de registros facturados, con el consecutivo oficial de Hacienda de cada comprobante.

Documentos de Hacienda

MétodoRutaPropósito
POST/api/facturacion/documentsEmitir el comprobante de una orden del POS ya pagada. El tipo se deriva de requiereFactura: true produce un 01 - Factura Electrónica (FE) y false un 04 - Tiquete Electrónico (TE). Campos: orderId, requiereFactura, customerId, economicActivityCode.
POST/api/facturacion/documents/:id/submitReencolar el envío de un documento en borrador.
POST/api/facturacion/documents/:id/resendReemitir un documento rechazado o con error.
POST/api/facturacion/documents/:id/nota-creditoEmitir una 03 - Nota de Crédito Electrónica (NC) sobre el comprobante. Parte del documento de Hacienda, no de la factura, y admite 01 - Factura Electrónica (FE) y 04 - Tiquete Electrónico (TE). Campos: motivo, codigoReferencia, medioReembolso.
POST/api/facturacion/documents/:id/nota-debitoEmitir una 02 - Nota de Débito Electrónica (ND) sobre el comprobante. Campos: motivo, codigoReferencia, lineas, totalManual, medioPago.
GET/api/facturacion/documents/:id/childrenNotas derivadas del comprobante.
GET/api/facturacion/documents/:id/statusEstado ante Hacienda.
GET/api/facturacion/documents/:id/pdfDescargar el PDF.
GET/api/facturacion/documents/:id/xmlDescargar el XML firmado.
GET/api/facturacion/documents/:id/mensaje-xmlDescargar el MensajeHacienda.
POST/api/facturacion/documents/:id/emailReenviar por correo.
GET/api/facturacion/documentsListar comprobantes con paginación.
GET/api/facturacion/taxpayers/:identificacionDirectorio de contribuyentes de Hacienda: nombre real, situación y actividades económicas del receptor (CIIU4, con punto). 404 = no registrado en el padrón. Véase 8.6 y 8.7.
GET/api/facturacion/consecutivosConsultar los consecutivos (lectura).
GET/api/facturacion/connectionEstado de la configuración de facturación.
GET/api/facturacion/tenant/statusEstado de la habilitación del tenant en Hacienda, con lo que falta para activarlo.
POST/api/facturacion/tenant/activateActivar el tenant en Hacienda.
POST/api/facturacion/tenant/certificatesSubir el certificado .p12 (multipart, campo file).
POST/api/facturacion/tenant/hacienda-credentialsRegistrar las credenciales del IDP de Hacienda.

Datos maestros y apoyo

MétodoRutaPropósito
GET /api/ POST /api/ PUT /api/ DELETE/api/customersClientes (receptores).
GET /api/ POST /api/ PUT /api/ DELETE/api/productsProductos y servicios.
GET /api/ POST /api/ PUT /api/ DELETE/api/terminalsTerminales de la organización: sucursal y número de terminal. Son los que se usan en sucursalCodigo + numeroTerminal o en terminalId al facturar.
GET /api/ POST /api/ PATCH /api/ DELETE/api/suppliersProveedores para la 08 - Factura Electrónica de Compra (FEC). Cada uno lleva su catálogo de actividades económicas (economicActivities), que es el CIIU del emisor en esa factura, y sus tres códigos de ubicación (provinceCode, cantonCode, districtCode).
GET/api/suppliers/suggestAutocompletado de proveedores activos (máximo 20 resultados).
GET/api/cabys?q=&limit=Buscar códigos CABYS. Devuelve { total, items }.
GET/api/cabys/:codigoConsultar un CABYS por sus 13 dígitos.
GET/api/units-of-measureCatálogo de unidades de medida. Es la única fuente de verdad para el code que se manda en unidadMedida. Véase 8.5.
GET/api/economic-activitiesCatálogo global de actividades económicas (CIIU4): 368 códigos con su descripción. Catálogo global, cacheable. Véase 8.8.
GET/api/locations/costa-ricaCatálogo de ubicaciones de Costa Rica (7 provincias, 84 cantones, 492 distritos) con los códigos oficiales de provincia, cantón y distrito. Catálogo global, cacheable. Sin autenticación. Véase 8.9.
POST/api/hacienda/exoneraciones/lookupValidar una autorización de exoneración.
GET/api/exchange-rates/:currencyTipo de cambio del día (USD o EUR). Devuelve { data: {...} }.

Compras y Mensaje del Receptor

MétodoRutaPermisoPropósito
GET/api/purchasespurchases:viewListar comprobantes de compra. { success: true, data: [] }, sin paginación.
GET/api/purchases/:idpurchases:viewDetalle del comprobante de compra, con el bloque mensajeReceptor: estado del MR (estadoEnvio), respuesta de Hacienda (respuestaDetalle, respuestaCodigo) y el XML mensajeHaciendaXml.
POST/api/purchases/uploadpurchases:uploadCargar comprobantes de compra en XML (multipart, campo files, hasta 50 archivos de 10 MB) y generar el Mensaje del Receptor 05, 06 o 07 según el messageConfig.
POST/api/purchases/:id/sync-haciendapurchases:sync_haciendaVolver a sincronizar el comprobante de compra con Hacienda.
POST/api/purchases/mensaje-recepcion/syncpurchases:sync_haciendaConsultar el estado de un MR concreto.
GET/api/purchases/:id/files/:fileId/urlpurchases:viewURL prefirmada de un archivo del comprobante.
GET /api/ POST /api/ DELETE/api/purchases/:id/commentspurchases:view · purchases:editComentarios del comprobante de compra.
POST/api/purchases/:id/attachmentspurchases:editAdjuntar archivos al comprobante de compra.
PATCH /api/ DELETE/api/purchases/:idpurchases:edit · purchases:deleteEditar las notas o dar de baja lógica el comprobante.
GET/api/reports/purchasespurchases:view_reportLibro de Compras.
GET/api/reports/purchases/export.xlsxpurchases:view_reportLibro de Compras en XLSX.

Anticipos (RC / PP)

MétodoRutaPermisoPropósito
GET/api/advancesadvances:viewListar anticipos.
GET/api/advances/:idadvances:viewDetalle de un anticipo.
POST/api/advancesadvances:createCrear un anticipo.
PATCH/api/advances/:idadvances:editEditar un anticipo en borrador o pendiente.
POST/api/advances/:id/submitadvances:createEnviar a aprobación.
POST/api/advances/:id/approveadvances:approveAprobar.
POST/api/advances/:id/collectadvances:collectMarcar como cobrado o pagado.
POST/api/advances/:id/voidadvances:voidAnular.
GET/api/customers/:id/available-advancesadvances:viewAnticipos del cliente con saldo disponible.
GET/api/suppliers/:id/advancesadvances:viewAnticipos del proveedor.
GET/api/advances/:id/pdfadvances:viewRecibo de dinero de anticipo en PDF.

Clientes e inventario

Rutas de operación del tenant. Todas usan la misma credencial M2M y la misma organización activa que el resto de la API. Véase la sección 09 para los cuerpos de la request y las reglas.

MétodoRutaPropósito
GET /api/ POST /api/ PUT /api/ DELETE/api/customersPadrón de clientes (receptores).
GET /api/ POST /api/ PUT /api/ DELETE/api/warehousesBodegas de inventario.
GET/api/warehouses/:id/balancesSaldos por producto de una bodega.
GET /api/ POST /api/ PUT /api/ DELETE/api/categoriesCategorías del catálogo.
POST/api/products/:id/movementsRegistrar una entrada, salida o ajuste de stock.
GET/api/products/:id/movementsHistorial de movimientos del producto.
GET/api/transactions/:idTransacción de inventario con sus movimientos.
POST/api/wasteRegistrar una merma.
POST/api/inventory/reconcileRecalcular el saldo de un producto en una bodega.
GET/api/reports/inventoryReporte de existencias y productos bajo mínimo.
GET/api/reports/inventory-allReporte completo de inventario por producto y bodega.
GET/api/reports/inventory/export.xlsxEl reporte de existencias en XLSX.
GET/api/reports/inventory-all/export.xlsxEl reporte completo de inventario en XLSX.
GET/api/reports/inventory/movementsMovimientos de inventario del período.
GET/api/reports/inventory/waste-trendTendencia de mermas. Devuelve { data }.
GET/api/reports/inventory/yield-performanceRendimiento de producción. Devuelve { data }.
GET/api/reports/inventory/transactionsTransacciones de inventario recientes. Devuelve { data }.
GET/api/reports/inventory/waste-by-productMermas agrupadas por producto. Devuelve { data }.
Los reportes exigen permiso de administrador

A diferencia del resto de la API, las rutas de /api/reports/* exigen el permiso reports:view_reports (o ser super administrador). Con el mismo token de emisión, un rol sin ese permiso recibe 403 con «Solo los administradores de la organización pueden ver reportes».

16 Errores y códigos

Los códigos de dominio viajan en el campo code y son estables; los mensajes pueden cambiar. Programe contra el código, no contra el texto.

Dos vocabularios de error que no se mezclan

Los códigos de esta tabla son del API: describen por qué falló la llamada. Los que emite Hacienda no llegan en el campo code: vienen dentro del texto del detalleMensaje del comprobante, con otro formato. Véase Códigos de rechazo de Hacienda.

HTTPcodeCausaQué hacer
400FACTURACION_NOT_CONFIGUREDLa organización no tiene la facturación electrónica habilitada.Escalar al administrador del ERP y reintentar más tarde.
400ORG_MISSING_TAX_IDLa organización no tiene identificación fiscal.Actualice la organización antes de emitir.
400PRODUCT_MISSING_CABYSUna línea apunta a un producto sin CABYS.Asigne el CABYS al producto.
400PRODUCT_NOT_FOUNDEl productId de una línea no existe, está archivado o pertenece a otra organización.Verifique el identificador y que el token apunte al tenant correcto.
400CABYS_PLACEHOLDER_NOT_ALLOWED_IN_PRODUCTIONSe usó el CABYS genérico 0000000000000.Vincule las líneas a productos con CABYS real.
401—La credencial está ausente, es inválida, venció o fue revocada.Pida una credencial nueva al administrador (Ajustes → Claves API (M2M)). No hay login ni renovación por API: véase 4.5.
403—Falta el permiso del rol de la credencial, o la credencial intentó usar una ruta interactiva.Revise el mapa de permisos del rol con el que se creó la credencial (véase 06).
403—La suscripción no está activa.Regularice la suscripción del tenant.
404—El recurso no existe o no pertenece a la organización.Verifique el identificador y que el token apunte al tenant correcto.
409—Conflicto de estado o duplicado.Por ejemplo, reenviar un documento que no está en rejected o error, o un correo ya existente.
422 / 400—Validación de campos.Revise los mensajes: suelen indicar el nombre exacto del campo rechazado.

Los códigos de dominio de la factura

Cuando la validación de una factura define un código, el error llega como { message, code } (o { message, code, details } cuando el mapper añade contexto). Estos son estables: programe contra ellos.

codeCausaDónde
PRODUCT_NOT_FOUNDEl productId de una línea no existe, está archivado o es de otra organización.POST/PATCH /api/invoices
PRODUCT_MISSING_CABYSEl producto no tiene CABYS real y la línea no lo corrige.POST/PATCH /api/invoices
INVALID_CABYSEl codigoCabys de la línea no tiene 13 dígitos.POST/PATCH /api/invoices
INVALID_CODIGO_IMPUESTOEl codigoImpuesto de la línea no está en el catálogo de la Nota 8.POST/PATCH /api/invoices
INVALID_TARIFA_IVALa tarifa no es válida para el código de impuesto 01 o 07.POST/PATCH /api/invoices
INVALID_DATOS_IMPUESTO_ESPECIFICOLos códigos 03–06 exigen cantidadUnidadMedida e impuestoUnidad.POST/PATCH /api/invoices
INVALID_FACTOR_CALCULO_IVAEl código 08 exige un factorCalculoIVA mayor que cero.POST/PATCH /api/invoices
INVALID_DESCRIPCION_IMPUESTOEl código 99 exige una descripcionImpuesto de 5 a 100 caracteres.POST/PATCH /api/invoices
INVALID_TARIFA_OTROSEl código 99 exige una tarifa mayor que cero.POST/PATCH /api/invoices
INVALID_CODIGO_DESCUENTOEl codigoDescuento no está en el catálogo de la Nota 20.POST/PATCH /api/invoices
INVALID_MONTO_DESCUENTOEl descuento supera el monto total de la línea.POST/PATCH /api/invoices
INVALID_CODIGO_DESCUENTO_TOTALCon el código 01 o 03, el descuento no es el 100 % del total de la línea.POST/PATCH /api/invoices
INVALID_CODIGO_DESCUENTO_OTROEl código 99 exige un codigoDescuentoOtro de 5 a 100 caracteres.POST/PATCH /api/invoices
INVALID_NATURALEZA_DESCUENTOLa naturalezaDescuento no tiene entre 3 y 80 caracteres.POST/PATCH /api/invoices
INVALID_EXONERACION_NUMERO_DOCUMENTOEl exoNumeroDocumento no tiene entre 3 y 40 caracteres.POST/PATCH /api/invoices
InvoiceMapperError (no es un código estable)Exoneración en una línea No Sujeto: la línea lleva tarifaIvaCode 01 (0 % Art. 32) o 11 (0 % sin crédito) y no tiene IVA que exonerar. El mensaje dice qué cambiar: la línea debe quedar gravada (08).POST/PATCH /api/invoices
—El numeroVINoSerie supera los 17 caracteres: «El número de VIN o serie no puede superar 17 caracteres (llegó con N).»POST/PATCH /api/invoices
PAYMENT_EXCEEDS_BALANCEEl pago supera el saldo pendiente de la factura.POST /api/invoices/:id/payments
ADVANCE_NOT_AVAILABLEEl anticipo no existe o no tiene saldo disponible.POST /api/invoices/:id/payments
ADVANCE_CUSTOMER_MISMATCHEl anticipo pertenece a otro cliente.POST /api/invoices/:id/payments
FACTURACION_SETUP_INCOMPLETEFalta algo para activar el tenant: la respuesta incluye la lista missing.POST /api/facturacion/tenant/activate
SUPPLIER_NOT_FOUNDEl proveedor no existe en esta organización./api/suppliers
SUPPLIER_DUPLICATE_IDENTIFICATIONYa existe un proveedor con esa identificación en la organización.POST /api/suppliers
VALIDATION_ERRORError de esquema en un proveedor: la respuesta incluye issues con el campo y el mensaje./api/suppliers
NOT_XML_FILE · FILE_TOO_LARGE · EMPTY_FILE · NO_CLAVE · INVALID_HACIENDA_NAMESPACEEl archivo cargado no es un comprobante válido. El código va en archivos[].errorCode, no en la respuesta HTTP.POST /api/purchases/upload
CERTIFICATE_EXPIRED · CERTIFICATE_INVALID_PINEl certificado .p12 venció o el PIN no lo abre: el MR no se puede firmar.POST /api/purchases/upload
Los otros cargos también tienen su familia de códigos

Cuando el otrosCargos no cumple el contrato de la Nota 16, el mensaje del error es el código: OTROS_CARGOS_INPUT_INVALIDO · OTROS_CARGOS_MAX_15 · OTRO_CARGO_TIPO_INVALIDO · OTRO_CARGO_DETALLE_REQUERIDO · OTRO_CARGO_DETALLE_MAX_160 · OTRO_CARGO_MONTO_INVALIDO · OTRO_CARGO_PORCENTAJE_INVALIDO · OTRO_CARGO_99_REQUIERE_DESCRIPCION · OTRO_CARGO_DESCRIPCION_LONGITUD · OTRO_CARGO_04_REQUIERE_TERCERO · OTRO_CARGO_04_IDENTIFICACION_INVALIDA · OTRO_CARGO_IDENTIFICACION_INVALIDA · OTRO_CARGO_NOMBRE_TERCERO_LONGITUD.

Códigos de rechazo de Hacienda

Cuando Hacienda rechaza un comprobante, su detalleMensaje trae una tabla con el formato codigo, mensaje, fila, columna. Estos son los códigos que aparecen al ejecutar el flujo completo:

CódigoSignificadoQué hacer
-1 El valor no cumple una enumeración del XSD (típicamente la unidad de medida de la línea). Revisar unidadMedida contra el code que devuelve GET /api/units-of-measure. Véase 8.5.
-17 El número de identificación del receptor no tiene la estructura del tipo declarado. Corregir governmentId / governmentIdType del cliente.
-38 El número de identificación del receptor no corresponde a un registro válido. Verificar el cliente contra el directorio de taxpayers. Véase 8.6.
-20 El teléfono del receptor no tiene la estructura esperada. Corregir el teléfono. Puede ser solo advertencia, no rechazo.
-518 El descuento de una línea con código de regalía o bonificación (01 o 03) no es el 100 % del total de la línea. El backend lo rechaza antes, con INVALID_CODIGO_DESCUENTO_TOTAL. Véase 14.10.
-45 El IVA de una línea con regalía se calculó sobre la base imponible en vez de sobre el monto total de la línea. Es un error de cálculo interno: si aparece, abra un ticket. La API ya calcula sobre el monto total.
-476 Una línea con IVA asumido por el emisor no lo declara en <ImpuestoAsumidoEmisorFabrica>. Igual que el anterior: es un error interno del backend.
-488 El TotalDesgloseImpuesto del resumen incluye el IVA asumido. Igual que el anterior: es un error interno del backend.
GET /api/facturacion/documents/:id/status · detalleMensaje rechazado
{
  "estado": "rechazado",
  "detalleMensaje": "-17, \"Número de cédula física/jurídica/NITE/DIMEX receptor no posee la estructura establecida para el mismo\", 0, 0\n-38, \"El número de identificación del receptor no corresponde a un registro válido, Favor verificar\", 0, 0"
}
FIX normalizar el detalle de Hacienda
// El detalle trae líneas «-17, "mensaje", fila, columna».
const errores = (detalle ?? "")
  .split("\n")
  .map((linea) => linea.match(/^\s*(-\d+),\s*"(.*)",\s*(\d+),\s*(\d+)/))
  .filter(Boolean)
  .map((m) => ({ codigo: m[1], mensaje: m[2], fila: Number(m[3]), columna: Number(m[4]) }))

// -1   → unidad de medida inválida
// -17  → identificación del receptor mal formada
// -38  → identificación del receptor no registrada en Hacienda
Tres fallos silenciosos

La unidad de medida inválida y la identificación del receptor no registrada no producen ningún error del API: el POST /api/invoices devuelve 201 y la factura parece correcta. El problema aparece después, en el estado ante Hacienda. Por eso el sondeo no es opcional: un 202 no significa factura aceptada.

El tercero es la exoneración incompleta: si a la línea le falta alguno de los cuatro campos exo* (o el tipo o la institución no están en el catálogo), el nodo <Exoneracion> no se emite, la línea se factura gravada y el comprobante tampoco es rechazado: el IVA se cobra sin que nadie se entere. Verifique el XML firmado antes de dar por buena la factura. Véase 10.5 · Caso completo.

Errores de la operación (clientes e inventario)

Estas rutas usan a veces el manejador global ({ success: false, message, code }) y a veces un mensaje plano ({ message }). En ambos casos el texto trae la causa exacta.

HTTPcodeCausaQué hacer
400No hay bodega predeterminada configurada para la organizaciónUn movimiento o una recepción no indicó warehouseId y la organización no tiene bodega predeterminada.Cree o marque una bodega con isDefault: true.
400Insufficient stock: missing <n>La salida excede el saldo disponible.Ajuste el saldo o habilite allowNegativeStock.
400Invalid productType / Invalid movement typeUn valor del catálogo no es válido.Revise el valor contra los catálogos de la sección 14.
409—Duplicado: documento de cliente o nombre de bodega.Consulte el mensaje: indica qué dato está repetido.

17 Ejemplos de implementación

Tres implementaciones completas del mismo flujo contra la API pública: autenticarse con la credencial de máquina, configurar el tenant y emitir una factura electrónica. Las tres usan una sola variable de entorno para la URL base y dejan el prefijo /api en esa variable. Son código de referencia, listo para copiar y adaptar: cámbialo según tu stack, pero conserva la secuencia de llamadas.

Variables de entorno

VariableValorUso
VT_API_BASEhttps://sandbox.vision-total.rogcr.com/apiURL base de la API. Ya incluye el prefijo /api.
VT_TOKENvt_m2m_k3P9xQ7mR2tY.…Credencial de máquina (M2M). Es la sesión completa: no hay login ni token de refresco. Véase 4.
VT_ORG_ID3b91c...Identificador de la organización. Opcional: se puede leer de GET /api/m2m/me.

17.1 · Flujo completo en JavaScript

JS facturacion.js · con fetch nativo
// La URL base ya incluye el prefijo /api. No lo duplique en las rutas.
const BASE = process.env.VT_API_BASE ?? "https://sandbox.vision-total.rogcr.com/api";

async function api(path, { method = "GET", body, token } = {}) {
  const headers = { Accept: "application/json" };
  if (token) headers.Authorization = `Bearer ${token}`;
  if (body) headers["Content-Type"] = "application/json";

  const res = await fetch(`${BASE}${path}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });

  const text = await res.text();
  const data = text ? JSON.parse(text) : null;

  if (!res.ok) {
    const err = new Error(data?.message ?? `HTTP ${res.status}`);
    err.code = data?.code;
    err.status = res.status;
    throw err;
  }
  return data;
}

// Un 404 en el padrón NO es un fallo del API: es la respuesta que el servicio
// necesita para decidir si el receptor es facturable.
async function consultarContribuyente(identificacion, token) {
  try {
    const contribuyente = await api(`/facturacion/taxpayers/${identificacion}`, { token });
    return { registrado: true, contribuyente };
  } catch (err) {
    if (err.status === 404) return { registrado: false, contribuyente: null };
    throw err;
  }
}

// Los códigos de rechazo de Hacienda vienen en el texto del detalle, no en el
// campo `code` de la respuesta.
function parseCodigosHacienda(detalle) {
  return (detalle ?? "")
    .split("\n")
    .map((linea) => linea.match(/^\s*(-\d+),\s*"(.*)",\s*(\d+),\s*(\d+)/))
    .filter(Boolean)
    .map((m) => ({ codigo: m[1], mensaje: m[2], fila: Number(m[3]), columna: Number(m[4]) }));
}

// 1 · La credencial de máquina ES la sesión. No hay login ni select-organization:
//     el token lo entrega el administrador (Ajustes → Claves API (M2M)) y se
//     envía tal cual en cada llamada.
const token = process.env.VT_TOKEN;

// 2 · Verificar la credencial: organización, rol y nombre de la credencial.
const sesion = await api("/m2m/me", { token });
if (!sesion?.organization?.id) throw new Error("Credencial M2M inválida o sin organización.");
// La organización sale del token: nunca se envía en el cuerpo de la petición.
const organizationId = sesion.organization.id;

// 3 · Estado de la configuración de facturación
const connection = await api("/facturacion/connection", { token });
if (!connection.facturacionConfigured) {
  throw new Error("La organización aún no tiene la facturación electrónica activada.");
}

// 4 · Catálogo de unidades de medida: la única fuente de verdad del código
//     que se manda en `unidadMedida`. Se consulta una vez y se cachea.
const unidades = await api("/units-of-measure", { token });   // array plano

const porCodigo = new Map(unidades.map((u) => [u.code, u]));

// La línea lleva el `code` que devuelve el catálogo, tal cual.
const unidadDe = (codigo) => porCodigo.get(codigo)?.code ?? porCodigo.get("Unid").code;

// 5 · Verificar el receptor en el padrón de Hacienda ANTES de facturar.
//     Un 404 aquí significa que el comprobante sería rechazado con -38.
const padron = await consultarContribuyente("3109876543", token);
if (!padron.registrado) {
  throw new Error("El contribuyente 3109876543 no está en el padrón de Hacienda.");
}

// El nombre y la actividad económica salen del padrón, no de un campo a mano.
// Los códigos CIIU4 llevan punto ("4610.0"), tal como los devuelve Hacienda.
const nombreReceptor = padron.contribuyente.nombre;
const actividadesReceptor = padron.contribuyente.actividades?.filter((a) => a.estado === "A") ?? [];
const actividadReceptor = actividadesReceptor[0]?.codigo;

// 6 · Cliente. El catálogo de actividades se guarda tal cual: el API exige
//     que `actividadEconomicaReceptor` sea uno de estos códigos.
const customer = await api("/customers", {
  method: "POST",
  token,
  body: {
    name: nombreReceptor,
    governmentId: "3109876543",
    governmentIdType: "02",
    email: "facturacion@wegner.cr",
    economicActivities: actividadesReceptor.map((a) => a.codigo),
  },
});

// 7 · Producto con CABYS (sin CABYS real no hay comprobante válido).
//     La unidad se toma del catálogo: `unitOfMeasureId` es el id de la fila.
const product = await api("/products", {
  method: "POST",
  token,
  body: {
    name: "Café molido 250 g",
    sku: "CAF-250",
    price: 10700,
    costPrice: 6500,
    productType: "product",
    cabys: "5897000000000",
    codigoImpuesto: "01",
    tarifaIvaCode: "08",
    codigoMoneda: "CRC",
    unitOfMeasureId: porCodigo.get("Unid").id,
  },
});

// 8 · Factura en borrador. La unidad sale del catálogo de la API: si se omite,
//     el API escribe «Unid».
const invoice = await api("/invoices", {
  method: "POST",
  token,
  body: {
    documentType: "01",
    customerId: customer.id,
    sucursalCodigo: "001",
    numeroTerminal: "00001",
    condicionVenta: "01",
    actividadEconomicaReceptor: actividadReceptor,
    items: [
      {
        productId: product.id,
        quantity: 1,
        price: 10700,
        taxRate: 0.13,
        tarifaIvaCode: "08",
        detalle: "Café molido 250 g",
        unidadMedida: unidadDe(product.unitOfMeasure?.code),
      },
    ],
  },
});

// 9 · Pago
await api(`/invoices/${invoice.data.id}/payments`, {
  method: "POST",
  token,
  body: { payments: [{ method: "cash", amount: 12091, customerAmount: 12091 }] },
});

// 10 · Emisión
const issued = await api(`/invoices/${invoice.data.id}/facturar`, {
  method: "POST",
  token,
  body: {
    documentType: "01",
    customerId: customer.id,
    actividadEconomicaReceptor: actividadReceptor,
  },
});

// 11 · Sondeo del estado: el veredicto lo da `estado`, nunca el detalle.
const ESTADOS_FINALES = new Set(["aceptado", "accepted", "rechazado", "rejected", "error"]);
let status;
for (let i = 0; i < 20; i++) {
  await new Promise((r) => setTimeout(r, 3000));
  status = await api(`/facturacion/documents/${issued.data.haciendaDocumentId}/status`, { token });
  if (ESTADOS_FINALES.has(status.data.estado)) break;
}

const avisos = parseCodigosHacienda(status.data.detalleMensaje);  // p. ej. el -20 del teléfono
console.log(status.data.estado, issued.data.clave, avisos);

// 12 · El consecutivo no viene en la respuesta de emisión: se lee del detalle.
const detail = await api(`/invoices/${invoice.data.id}`, { token });
const consecutivo = detail.data.haciendaDocuments?.[0]?.numeroConsecutivo;
console.log("Consecutivo", consecutivo);

17.2 · Flujo completo en Python

PY facturacion.py · con requests
import os
import re
import time
import requests

# La URL base ya incluye el prefijo /api. No lo duplique en las rutas.
BASE = os.environ.get("VT_API_BASE", "https://sandbox.vision-total.rogcr.com/api")
SESSION = requests.Session()

# Códigos de rechazo de Hacienda: vienen en el texto del detalle, no en `code`.
PATRON_CODIGOS = re.compile(r'^\s*(-\d+),\s*"(.*)",\s*(\d+),\s*(\d+)')

# La unidad de la línea sale del catálogo de la API: es la única fuente de
# verdad. No se escribe el código a mano ni se mantiene una lista local.
ESTADOS_FINALES = {"aceptado", "accepted", "rechazado", "rejected", "error"}


def api(path, method="GET", **kwargs):
    response = SESSION.request(method, f"{BASE}{path}", timeout=30, **kwargs)
    if not response.ok:
        raise RuntimeError(f"{response.status_code} {response.text}")
    return response.json() if response.content else None


def consultar_contribuyente(identificacion):
    """Un 404 significa «no registrado en el padrón», no un fallo del API."""
    response = SESSION.get(f"{BASE}/facturacion/taxpayers/{identificacion}", timeout=30)
    if response.status_code == 404:
        return None
    if not response.ok:
        raise RuntimeError(f"{response.status_code} {response.text}")
    return response.json()


def codigos_hacienda(detalle):
    errores = []
    for linea in (detalle or "").split("\n"):
        m = PATRON_CODIGOS.match(linea)
        if m:
            errores.append({"codigo": m.group(1), "mensaje": m.group(2),
                            "fila": int(m.group(3)), "columna": int(m.group(4))})
    return errores


def unidad_de(codigo):
    """El `code` del catálogo, sin transformaciones. Si no hay unidad, «Unid»."""
    return POR_CODIGO.get(codigo, POR_CODIGO["Unid"])["code"]


# La credencial de máquina ES la sesión: no hay login ni select-organization.
# El token lo entrega el administrador (Ajustes → Claves API (M2M)) y se envía
# tal cual en cada llamada.
TOKEN = os.environ.get("VT_TOKEN")
if not TOKEN:
    raise RuntimeError("Falta VT_TOKEN: la credencial de máquina (M2M)")

SESSION.headers.update({"Authorization": f"Bearer {TOKEN}"})

# Verificar la credencial: organización, rol y nombre de la credencial.
sesion = api("/m2m/me")
# La organización sale del token: nunca se envía en el cuerpo de la petición.
organization_id = sesion["organization"]["id"]

# Catálogo de unidades de medida: la única fuente de verdad del código que se
# manda en `unidadMedida`. Se consulta una vez y se cachea.
UNIDADES = api("/units-of-measure")

POR_CODIGO = {u["code"]: u for u in UNIDADES}

connection = api("/facturacion/connection")
if not connection["facturacionConfigured"]:
    raise RuntimeError("Facturación electrónica no activada")

# Verificar el receptor en el padrón ANTES de facturar: si no existe,
# Hacienda rechazará el comprobante con -38.
contribuyente = consultar_contribuyente("3109876543")
if contribuyente is None:
    raise RuntimeError("El contribuyente 3109876543 no está en el padrón de Hacienda")

nombre_receptor = contribuyente["nombre"]
actividades = [a for a in contribuyente.get("actividades", []) if a.get("estado") == "A"]
actividad_receptor = actividades[0]["codigo"] if actividades else None

# El catálogo de actividades se guarda tal cual (los CIIU4 llevan punto:
# "4610.0"): el API exige que `actividadEconomicaReceptor` sea uno de ellos.
customer = api("/customers", method="POST", json={
    "name": nombre_receptor,
    "governmentId": "3109876543",
    "governmentIdType": "02",
    "email": "facturacion@wegner.cr",
    "economicActivities": [a["codigo"] for a in actividades],
})

# Producto con CABYS real: sin él no hay comprobante válido en producción.
# La unidad sale del catálogo: `unitOfMeasureId` es el id de la fila.
product = api("/products", method="POST", json={
    "name": "Café molido 250 g",
    "sku": "CAF-250",
    "price": 10700,
    "costPrice": 6500,
    "productType": "product",
    "cabys": "5897000000000",
    "codigoImpuesto": "01",
    "tarifaIvaCode": "08",
    "codigoMoneda": "CRC",
    "unitOfMeasureId": POR_CODIGO["Unid"]["id"],
})

invoice = api("/invoices", method="POST", json={
    "documentType": "01",
    "customerId": customer["id"],
    "sucursalCodigo": "001",
    "numeroTerminal": "00001",
    "condicionVenta": "01",
    "actividadEconomicaReceptor": actividad_receptor,
    "items": [{
        "productId": product["id"],
        "quantity": 1,
        "price": 10700,
        "taxRate": 0.13,
        "tarifaIvaCode": "08",
        "detalle": "Café molido 250 g",
        "unidadMedida": unidad_de((product.get("unitOfMeasure") or {}).get("code")),
    }],
})

api(f"/invoices/{invoice['data']['id']}/payments", method="POST", json={
    "payments": [{"method": "card", "amount": 12091, "customerAmount": 12091}],
})

issued = api(f"/invoices/{invoice['data']['id']}/facturar", method="POST",
             json={"documentType": "01", "customerId": customer["id"],
                   "actividadEconomicaReceptor": actividad_receptor})

# Sondear hasta el final: el veredicto lo da `estado`, nunca el detalle.
for _ in range(20):
    time.sleep(3)
    status = api(f"/facturacion/documents/{issued['data']['haciendaDocumentId']}/status")
    if status["data"]["estado"] in ESTADOS_FINALES:
        break

print(status["data"]["estado"], issued["data"]["clave"])
print("Avisos:", codigos_hacienda(status["data"].get("detalleMensaje")))

# El consecutivo no viene en la respuesta de emisión: se lee del detalle.
detail = api(f"/invoices/{invoice['data']['id']}")
documentos = detail["data"].get("haciendaDocuments") or []
print("Consecutivo:", documentos[0]["numeroConsecutivo"] if documentos else None)
Cuatro detalles que hacen fallar el ejemplo

El price de la línea es sin impuestos. La API calcula total = Σ (cantidad × precio) + Σ impuestos. Con price: 10700 y taxRate: 0.13 el total da 12091, que es justo lo que paga el ejemplo. Si el precio incluyera el IVA, el pago quedaría corto, la factura no llegaría a paid y no se podría emitir.

Una línea sin productId no es facturable en producción. Se acepta con el marcador 0000000000000, pero Hacienda la rechaza. Vincule la línea a un producto con CABYS real: por eso el ejemplo crea el producto antes de facturar. Véanse las reglas de la tabla de campos de línea.

La unidad se manda siempre y se consulta en la API. Si unidadMedida se omite, el API escribe Unid y un producto que se mide en gramos queda facturado en unidades. El código sale de GET /api/units-of-measure, nunca de una lista escrita a mano: si se manda un valor fuera de la enumeración de Hacienda, el comprobante entero se rechaza con -1. Véase 8.5.

El receptor tiene que estar en el padrón. La API acepta el alta del cliente con una identificación inexistente; el rechazo con -38 llega después, al facturar. Véase 8.6.

17.3 · Descargar y archivar los artefactos

SH PDF, XML y MensajeHacienda de un comprobante
DOC_ID="d41b..."
TOKEN="..."

for kind in pdf xml mensaje-xml; do
  curl -sS -f "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOC_ID/$kind" \
    -H "Authorization: Bearer $TOKEN" \
    -o "comprobante.$kind" \
    || echo "El artefacto $kind todavía no está disponible"
done

ls -lh comprobante.*

18 Checklist de puesta en marcha

Los pasos están numerados en orden de ejecución: cada uno depende del anterior y las emisiones fallan de forma confusa si se saltan.

Prerrequisitos que resuelve el administrador del ERP

No forman parte de la integración, pero el servicio no puede emitir sin ellos. Confirme que están listos antes de empezar.

  1. Organización creadaCon su identificación fiscal y su actividad económica principal, por un administrador del ERP.
  2. Credencial de máquina (M2M) con su rolCreada por un administrador en Ajustes → Claves API (M2M), con los permisos de facturación y, si el servicio administra inventario, con inventory:*; si usa el flujo de compras, con purchases:*; y si aplica anticipos, con advances:*. El secreto se entrega una sola vez.
  3. Facturación electrónica habilitadaLa organización ya puede emitir comprobantes ante Hacienda.
  4. Bodega predeterminadaUna bodega con isDefault: true. Es la que recibe el stock inicial y los movimientos que no indican warehouseId.
  5. Certificado y credenciales vigentesSi el servicio genera Mensajes del Receptor, la organización necesita el .p12 con su PIN y las credenciales del IDP. Véase la sección 13.

Pasos del servicio externo

  1. Recibir la credencial de máquinaEl token vt_m2m_… que entrega el administrador. Guárdelo fuera del código, en un secret manager o en una variable de entorno.
  2. Verificar la credencialGET /api/m2m/me debe devolver la organización, el rol y el nombre de la credencial. Un 401 significa que el token está mal copiado, revocado o vencido.
  3. Verificar el tenantGET /api/facturacion/connection debe devolver facturacionConfigured: true.
  4. Verificar cada receptor en el padrón de HaciendaGET /api/facturacion/taxpayers/:id: un 404 significa que la factura será rechazada con -38. De paso, tome el nombre real y la actividad económica del receptor. Véase la sección 8.6.
  5. Cargar el catálogo operativoClientes con identificación, productos con CABYS, tarifa de IVA y unidad de medida, proveedores si se emitirán 08 - Factura Electrónica de Compra (FEC). Véase la sección 08.
  6. Registrar el padrón y el inventarioBodegas, clientes y saldos iniciales de los productos. Véase la sección 09.
  7. Mandar la unidad de medida en cada líneaConsultar GET /api/units-of-measure y mandar el code del catálogo en unidadMedida (o product.unitOfMeasure.code). El código nunca se escribe a mano: se contrasta contra la enumeración de Hacienda. Véase la sección 8.5.
  8. Validar los códigos de actividad económicaConsultar GET /api/economic-activities (catálogo global, cacheable) y comprobar que el CIIU que va en el XML existe y lleva punto (6201.0, no 620100). El del receptor sale del padrón (GET /api/facturacion/taxpayers/:id); el de la organización, de GET /api/organizations/:id/activities. Véase la sección 8.7.
  9. Emitir una primera factura de pruebaCrear, pagar y facturar. Confirmar que estado llegue a aceptado. El 202 no es una factura aceptada: hay que sondear.
  10. Guardar el consecutivo del comprobanteLeerlo de GET /api/invoices/:id → haciendaDocuments[0].numeroConsecutivo. Véase la sección 12.6.
  11. Probar el flujo de compras, si aplicaPOST /api/purchases/upload con un XML de prueba y un messageConfig. Confirmar que el mensajeReceptor.estadoEnvio pasa de no_enviado a aceptado. Véase la sección 13.
  12. Pasar a producciónEl administrador habilita la facturación en el ambiente de producción. El servicio no interviene.
Prueba de humo

Si la factura se crea, se paga y el comprobante llega a aceptado, la cadena del lado del servicio está bien montada: token, organización, permisos, receptor en el padrón, unidad de medida, formato de líneas y pago. Si el estado se queda en rejected o en error, el detalleMensaje trae los códigos de Hacienda que indican la causa exacta del rechazo: -1 es la unidad de medida, -17 y -38 son el receptor.

Dos rechazos que no dan error en la API

La unidad de medida inválida y la identificación del receptor no registrada no producen ningún error: el POST devuelve 201 y el facturar devuelve 202. La diferencia entre «funciona» y «no funciona» aparece únicamente en el estado final. Un curl contra cada endpoint crítico antes de integrar es más barato que un rechazo de Hacienda en producción.