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
- 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 responde404. - La sesión se inicia con la credencial de máquina. No hay login ni
select-organization: la primera llamada que llevaAuthorization: Bearer vt_m2m_…ya opera sobre la organización del token. Véase 4. - 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.
- El comprobante se crea y se paga antes de enviarse. Hacienda no acepta documentos con saldo pendiente en un flujo de crédito.
- La firma ocurre en el servidor. El servicio externo nunca manipula material criptográfico: solo recibe el comprobante ya firmado.
- El estado final no es inmediato. La emisión responde
202con unjobId; el resultado real se consulta enGET /api/facturacion/documents/:documentId/status. - Dos datos no validados viajan en la línea y en el receptor. La
unidadMedidase escribe tal cual en el XML —su código sale del catálogo deGET /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. - El veredicto lo da el
estado, nunca el detalle. Un comprobante aceptado puede traer advertencias en eldetalleMensaje. - El medio de pago se nombra de dos maneras, según la ruta. En
POST /api/invoices/:id/paymentsy en elmedioReembolsode las notas (NC/ND) se manda el nombre interno (cash,sinpe, …); en elmedioPagodel10 - Recibo Electrónico de Pago (REP)se manda el código de la Nota 6 ("01"…"99"). Véase 14.9. - El
10 - Recibo Electrónico de Pago (REP)no se crea como factura. Se emite exclusivamente conPOST /api/invoices/:id/recibo-pago, sobre un comprobante comercial ya aceptado. Véase 11.3.
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.
Tres reglas que explican casi todo
- Todo lleva el mismo token. El
Authorization: Bearerlleva 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. - Todo ocurre bajo la organización del token. No se envía
organizationIden el cuerpo de las peticiones de negocio: es el token el que determina a qué tenant pertenecen los datos. - Emitir es asíncrono. Las operaciones de emisión devuelven
202 Acceptedcon unjobId. El resultado real ante Hacienda se consulta después en/api/facturacion/documents/:documentId/status.
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.
/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.
| Escenario | URL base | Rutas |
|---|---|---|
| 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.
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/healthresponde200con{ "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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/connection" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
| Cabecera | Obligatoria | Valor |
|---|---|---|
| Authorization | Sí | 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-Type | Sí en cuerpos | application/json, o multipart/form-data para la carga de comprobantes de compra. |
| Accept | No | Ú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.
{
"success": false,
"message": "El documento de identificación es inválido para el tipo seleccionado.",
"code": "BAD_REQUEST"
}
{
"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ón | Forma | Endpoints 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 |
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.
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.
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.
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.
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.
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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/m2m/me" \
-H "Authorization: Bearer vt_m2m_k3P9xQ7mR2tY.Tq8mZ2v…"
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.
{
"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" }
}
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 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.
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 permisoorganization:manage_api_tokens, desde Ajustes → Claves API (M2M). Una credencial que intente hacerlo recibe403. - Verificación.
GET /api/m2m/medevuelve la organización, el rol y el nombre de la credencial. Con un JWT de usuario responde403: ese endpoint es sólo para credenciales de máquina.
- 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/appley/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.
| HTTP | message | Causa | Qué 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.
| Ruta | Con una credencial M2M | Para quién es |
|---|---|---|
| POST /api/auth/login | Sin efecto sobre la sesión de máquina. | Personas (ERP web). |
| POST /api/auth/login/challenge | Sin efecto sobre la sesión de máquina. | Personas con doble factor. |
| POST /api/auth/select-organization | 403 — está bloqueada para las cuentas de servicio. | Personas (ERP web). |
| GET /api/auth/me | Describe la cuenta de servicio, no al humano. | Personas (ERP web). |
| PUT /api/auth/change-password | 403 — una cuenta de servicio no tiene contraseña. | Personas (ERP web). |
| GET /api/m2m/me | 200 — este es el endpoint de sesión del integrador. | Integraciones por API. |
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.
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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/organizations" \
-H "Authorization: Bearer $TOKEN"
[
{
"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
}
]
| Campo | Para qué le sirve al servicio |
|---|---|
| id | El organizationId de la credencial. No se envía en el cuerpo de ninguna petición: ya viaja en el token. |
| taxId / taxIdType | La identificación fiscal del emisor. Aparece en el XML, no se envía. |
| currency | La moneda por defecto de los comprobantes. |
| facturacionConfigured | true cuando el administrador ya activó la facturación electrónica. |
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 permiso | Permite |
|---|---|
| invoices:view_invoices | Listar y ver facturas, comprobantes y cotizaciones. |
| invoices:create_invoice | Crear facturas y cotizaciones (POST /api/invoices). |
| invoices:edit_invoice | Editar facturas y sus líneas. |
| invoices:add_payment | Registrar y quitar pagos. |
| invoices:transition_invoice | Emitir el comprobante ante Hacienda (POST /api/invoices/:id/facturar). |
| invoices:issue_credit_note | Emitir 03 - Nota de Crédito Electrónica (NC) y 10 - Recibo Electrónico de Pago (REP). |
| invoices:issue_debit_note | Emitir 02 - Nota de Débito Electrónica (ND). |
| invoices:view_documents | Consultar notas derivadas de un comprobante. |
| customers:view_customers | Buscar clientes para asignarlos a una factura. |
| customers:create_customer | Registrar clientes nuevos. |
| customers:edit_customer | Editar clientes existentes, incluido el tipo de identificación. |
| customers:delete_customer | Dar de baja lógica un cliente. |
| inventory:view_products | Consultar el catálogo de productos y servicios. |
| inventory:create_product | Registrar productos y servicios nuevos. |
| inventory:edit_product | Editar productos, categorías y atributos fiscales. |
| inventory:delete_product | Archivar productos. |
| inventory:manage_stock | Registrar entradas, salidas, ajustes, mermas y conciliaciones. |
| product_categories:create_category / edit_category / delete_category | Administrar las categorías del catálogo. |
| suppliers:view / suppliers:manage | Consultar y administrar proveedores: son los emisores de la 08 - Factura Electrónica de Compra (FEC). |
| invoices:transition_quote | Pasar una factura de draft a quoting. |
| invoices:transition_pay | Pasar una factura de quoting a paid. |
| invoices:transition_cancel | Anular una factura que todavía no se ha facturado. Una factura facturada se anula con una nota de crédito. |
| invoices:delete_invoice | Baja lógica de una factura en borrador. |
| purchases:view | Listar y ver comprobantes de compra, sus comentarios y sus archivos. |
| purchases:upload | Cargar comprobantes de compra en XML y generar el Mensaje del Receptor. |
| purchases:edit | Editar las notas de un comprobante de compra, sus comentarios y sus adjuntos. |
| purchases:delete | Baja lógica de un comprobante de compra. |
| purchases:sync_hacienda | Volver a sincronizar un comprobante de compra o un MR con Hacienda. |
| purchases:view_report | Ver el Libro de Compras y exportarlo a XLSX. |
| advances:view / create / edit / approve / collect / void | Administrar los anticipos (RC y PP) que se aplican como pago de una factura. |
| reports:view_reports | Ver los reportes de inventario. Es un permiso de administrador de organización. |
| organization:edit_organization | Dar 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.
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.
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.
Consultar el estado
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/connection" \
-H "Authorization: Bearer $TOKEN"
{
"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
}
}
| Campo | Para qué le sirve al servicio |
|---|---|
| facturacionConfigured | true cuando la organización puede emitir. Es el campo que decide si el servicio arranca o escala. |
| haciendaConfigReady | La conexión con Hacienda está completa. |
| certificate.isActive | false cuando la habilitación venció. La emisión fallará hasta que se renueve. |
| environment | sandbox o production. Útil para no confundir una prueba con una emisión real. |
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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/consecutivos" \
-H "Authorization: Bearer $TOKEN"
{
"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)
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.
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"]
}'
| Campo | Req. | Reglas |
|---|---|---|
| name | Sí | Nombre o razón social del receptor. |
| governmentId | Sí | 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. |
| governmentIdType | No | Có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. |
| Sí | Correo válido, normalizado a minúsculas. Es el destinatario del comprobante por correo. | |
| isForeign | No | Marca 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. |
| economicActivities | No | Có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 / districtCode | No | Ubicació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. |
| address | No | Direcció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 / country | No | Datos de contacto del receptor. El teléfono debe tener la estructura que Hacienda espera, o el comprobante sale con una advertencia -20. |
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
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"
}'
| Campo | Significado fiscal |
|---|---|
| codigoImpuesto | Catá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. |
| tarifaIvaCode | Catálogo CodigoTarifaIVA: 08 - 13 % general · 02 - 1 % · 03 - 2 % · 04 - 4 % · 01 - 0 % art. 32 · 10 - Exenta · 11 - 0 % sin crédito. |
| cabys | Có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. |
| productType | simple · product · composite · ingredient · service. |
| unitOfMeasureId | Identificador 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. |
| taxDetail | Obligatorio 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. |
{
"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"
}
}
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.
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)
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.
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
curl -X GET "https://sandbox.vision-total.rogcr.com/api/cabys?q=cafe&limit=20" \
-H "Authorization: Bearer $TOKEN"
{
"total": 2,
"items": [
{ "codigo": "5897000000000", "descripcion": "Café molido, sin azúcar" },
{ "codigo": "5897000000001", "descripcion": "Café en grano" }
]
}
| Parámetro | Por defecto | Notas |
|---|---|---|
| q | "" | Busca en el código y en la descripción, sin distinguir mayúsculas. |
| limit | 50 | Se 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:
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" }'
{
"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"
}
}
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.
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.
| Regla | Comportamiento 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
curl -X GET "https://sandbox.vision-total.rogcr.com/api/units-of-measure" \
-H "Authorization: Bearer $TOKEN"
[
{
"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"
}
]
| Campo | Qué es |
|---|---|
| code | El 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. |
| name | Descripción de la unidad, para mostrarla en pantalla. |
| id | Identificador de la fila: es el valor de unitOfMeasureId al crear o editar un producto. |
| baseUnitCode / conversionFactor | Unidad 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ódigo | Descripción |
|---|---|
| 1 | uno (índice de refracción) |
| 1/m | 1 por metro |
| A | Ampere |
| A/m | ampere por metro |
| A/m² | ampere por metro cuadrado |
| Acv | Activo Virtual |
| Al | Alquiler de uso habitacional |
| Alc | Alquiler de uso comercial |
| B | bel |
| Bq | Becquerel |
| C | coulomb |
| C/kg | coulomb por kilogramo |
| C/m² | coulomb por metro cuadrado |
| C/m³ | coulomb por metro cúbico |
| Cc | Cajuela de café |
| Cd | Candela |
| Cm | Comisiones |
| Cu | cuartillos de café |
| D | día |
| F | farad |
| F/m | farad por metro |
| Fa | fanega de café |
| G | Gramo |
| Gal | Galón |
| Gy | gray |
| Gy/s | gray por segundo |
| H | henry |
| H/m | henry por metro |
| Hz | hertz |
| I | Intereses |
| J | Joule |
| J/(kg·K) | joule por kilogramo kelvin |
| J/(mol·K) | joule por mol kelvin |
| J/K | joule por kelvin |
| J/kg | joule por kilogramo |
| J/mol | joule por mol |
| J/m³ | joule por metro cúbico |
| K | Kelvin |
| Kat | katal |
| Kg | Kilogramo |
| Km | Kilometro |
| Kw | kilovatios |
| L | litro |
| Lm | lumen |
| Ln | pulgada |
| Lx | lux |
| M | Metro |
| Min | minuto |
| Mm | Milímetro |
| Mol | Mol |
| N | newton |
| N/m | newton por metro |
| Np | neper |
| N·m | newton metro |
| Os | Otro tipo de servicio |
| Otros | Se debe indicar la descripción de la medida a utilizar |
| Oz | Onzas |
| Pa | pascal |
| Pa·s | pascal segundo |
| Quintal | |
| Rad | radián |
| S | Segundo |
| Sp | Servicios Profesionales |
| Spe | Servicios personales |
| Sr | estereorradián |
| St | Servicios técnicos |
| Sv | sievert |
| T | tonelada |
| U | unidad de masa atómica unificada |
| Ua | unidad astronómica |
| Unid | Unidad |
| V | volt |
| V/m | volt por metro |
| W | Watt |
| W/(m²·sr) | watt por metro cuadrado estereorradián |
| W/(m·K) | watt por metro kevin |
| W/m² | watt por metro cuadrado |
| W/sr | watt por estereorradián |
| Wb | weber |
| cd/m² | candela por metro cuadrado |
| cm | centímetro |
| eV | electronvolt |
| h | hora |
| kWh | Kilovatios por hora |
| kat/m³ | katal por metro cúbico |
| kg/m³ | kilogramo por metro cúbico |
| m/s | metro por segundo |
| m/s² | metro por segundo cuadrado |
| mL | mililitro |
| mol/m³ | mol por metro cúbico |
| m² | metro cuadrado |
| m³ | metro cúbico |
| rad/s | radián por segundo |
| rad/s² | radián por segundo cuadrado |
| s | siemens |
| t | tesla |
| °C | grado Celsius |
| ´ | minuto |
| ´´ | segundo |
| º | grado |
| Ω | ohm |
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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/taxpayers/3109876543" \
-H "Authorization: Bearer $TOKEN"
{
"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" }
]
}
HTTP 404
{ "message": "Taxpayer not found" }
| Respuesta | Significado | Qué 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. |
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
// 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) },
})
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 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 en | Para 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
curl -X GET "https://sandbox.vision-total.rogcr.com/api/organizations/$ORG_ID/activities" \
-H "Authorization: Bearer $TOKEN"
[
{
"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.
// 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 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.
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).
curl -X GET "https://sandbox.vision-total.rogcr.com/api/economic-activities" \
-H "Authorization: Bearer $TOKEN"
[
{ "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" }
]
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 CIIU4 | Descripción |
|---|---|
| 0000.1 | Preoperativa |
| 0000.2 | Servicios digitales o de telecomunicaciones e intangibles prestados desde el exterior por proveedores |
| 0000.3 | Servicios digitales o de telecomunicaciones e intangibles prestados desde el exterior por intermediarios |
| 0111.9 | Cultivo de otros cereales, legumbres y semillas oleaginosas n.c.p |
| 0113.9 | Cultivo de otras hortalizas, raíces o tubérculos n.c.p |
| 0114.0 | Cultivo de caña de azúcar |
| 0115.0 | Cultivo de tabaco |
| 0116.0 | Cultivo de plantas de fibras |
| 0119.1 | Cultivo de Flores |
| 0122.1 | Cultivo de banano |
| 0122.3 | Cultivo de piña |
| 0122.9 | Cultivo de otras frutas tropicales y subtropicales |
| 0125.0 | Cultivo de otros frutos y nueces de árboles y arbustos |
| 0126.1 | Cultivo de palma africana (aceitera) |
| 0127.1 | Cultivo de café |
| 0127.9 | Cultivo de otras plantas con las que se preparan bebidas, excepto el cultivo de café n.c.p |
| 0128.0 | Cultivo de especies de plantas aromáticas, medicinales y farmacéuticas |
| 0130.0 | Propagación de plantas |
| 0141.1 | Cría de ganado Bovino (vacuno) para carne |
| 0141.2 | Cría de ganado Bovino (vacuno) para producción de leche cruda |
| 0141.9 | Cría de Búfalos y otra producción de ganado bovino (vacuno) |
| 0142.0 | Cría de caballos y otros equinos |
| 0144.0 | Cría de ovejas y cabras |
| 0145.0 | Cría de cerdos |
| 0146.2 | Actividades de granja para la producción de huevos de gallina |
| 0146.9 | Cría de otras aves de corral, excepto pollos |
| 0149.0 | Cría de otros animales |
| 0150.0 | Cultivo de productos agrícolas en combinación con la cría de animales (explotación mixta) |
| 0161.2 | Servicios de fumigación y riego agrícola |
| 0161.9 | Otros servicios de apoyo a la agricultura |
| 0162.9 | Otras actividades de apoyo a la ganadería |
| 0163.1 | Beneficio de café |
| 0163.9 | Otras actividades posteriores a la cosecha |
| 0170.0 | Caza ordinaria, mediante trampas y actividades de servicio conexas |
| 0210.0 | Silvicultura y otras actividades forestales |
| 0220.0 | Extracción de madera |
| 0311.0 | Pesca marítima |
| 0321.0 | Acuicultura marina |
| 0510.0 | Extracción de carbón de piedra |
| 0620.0 | Extracción de gas natural |
| 0721.0 | Extracción de minerales metalíferos no ferrosos |
| 0810.0 | Extracción de piedra, arena y arcilla |
| 0891.0 | Extracción de minerales para la fabricación de abonos y productos químicos |
| 0893.0 | Extracción de sal |
| 0899.0 | Explotación de otras minas y canteras n.c.p |
| 1010.1 | Elaboración y conservación de carne y embutidos de ganado vacuno y porcino |
| 1010.9 | Elaboración y conservación de otro tipo de carnes n.c.p |
| 1020.0 | Elaboración y conservación de pescados, crustáceos y moluscos |
| 1030.0 | Elaboración y conservación de frutas, legumbres y hortalizas (vegetales) |
| 1040.0 | Elaboración de aceites y grasas de origen vegetal y animal |
| 1050.0 | Elaboración de productos lácteos |
| 1061.1 | Beneficio de arroz |
| 1061.9 | Elaboración de productos de molinería excepto arroz |
| 1062.0 | Elaboración de almidones y productos derivados del almidón |
| 1071.9 | Elaboración de otros productos de panadería, excepto elaboración de galletas |
| 1072.0 | Elaboración de azúcar y otros productos derivados de la caña de azúcar |
| 1073.0 | Elaboración de cacao, chocolate y productos de confitería |
| 1074.0 | Elaboración de macarrones, fideos, alcuzcuz y productos farináceos similares |
| 1079.9 | Elaboración de otros productos alimenticios n.c.p |
| 1080.0 | Elaboración de alimentos preparados para animales |
| 1101.0 | Destilación, rectificación y mezcla de bebidas alcohólicas |
| 1103.0 | Elaboración de bebidas malteadas y de malta |
| 1104.0 | Elaboración de bebidas no alcohólicas; producción de aguas minerales y otras aguas embotelladas |
| 1200.0 | Elaboración de productos de tabaco |
| 1312.0 | Tejedura de productos textiles |
| 1313.0 | Acabado de productos textiles |
| 1391.0 | Fabricación de tejidos de punto y ganchillo |
| 1392.0 | Fabricación de artículos confeccionados con materiales textiles, excepto prendas de vestir |
| 1393.0 | Fabricación de tapices y alfombras |
| 1394.0 | Fabricación de cuerdas, cordeles, bramantes y redes |
| 1410.9 | Fabricación de otras prendas de vestir, excepto ropa interior y prendas de piel |
| 1511.0 | Curtido y adobo de cueros; adobo y teñido de pieles |
| 1512.0 | Fabricación de maletas, bolsos de mano y de artículos similares, y artículos de talabartería o guarnicionería |
| 1520.0 | Fabricación de calzado |
| 1610.0 | Aserrado y acepilladura de madera |
| 1621.0 | Fabricación de hojas de madera para enchapado y tableros a base de madera |
| 1622.0 | Fabricación de partes y piezas de carpimtería para edificios y construcciones |
| 1701.0 | Fabricación de pasta de madera , papel y cartón |
| 1709.0 | Fabricación de otros artículos del papel y cartón |
| 1811.0 | Impresión |
| 1820.0 | Reproducción de grabaciones |
| 1920.0 | Fabricación de los productos de la refinación del petróleo |
| 2011.0 | Fabricación de sustancias químicas básicas |
| 2012.0 | Fabricación abonos y compuestos de nitrógeno |
| 2013.0 | Fabricación de plásticos y de caucho sintético en formas primarias |
| 2021.0 | Fabricación de plaguicidas y de otros productos químicos de uso agropecuario |
| 2022.0 | Fabricación de pinturas, barnices y productos de revestimiento similares, tintas de imprenta y masillas |
| 2023.1 | Fabricación de jabones y detergentes, preparados para limpiar y pulir |
| 2023.2 | Fabricación de perfumes y preparados de tocador |
| 2029.0 | Fabricación de otros productos químicos n.c.p |
| 2030.0 | Fabricación de fibras artificiales |
| 2100.0 | Fabricación de productos farmacéuticos, sustancias químicas medicinales y de productos botánicos de uso farmacéutico |
| 2211.0 | Fabricación de cubiertas y cámaras de caucho; recauchutado y renovación de cubiertas de caucho |
| 2219.0 | Fabricación de otros productos de caucho |
| 2220.9 | Fabricación de otros productos de plástico |
| 2310.0 | Fabricación de vidrio y de productos de vidrio |
| 2391.0 | Fabricación de productos refractarios |
| 2392.0 | Fabricación material de arcilla para la construcción |
| 2393.0 | Fabricación de otros productos de cerámica y porcelana |
| 2394.0 | Fabricación de cemento, de la cal y del yeso |
| 2395.0 | Fabricación de artículos de hormigón, de cemento y de yeso |
| 2396.0 | Corte, tallado y acabado de la piedra |
| 2399.0 | Fabricación de otros productos minerales no metálicos n.c.p |
| 2410.0 | Industrias básicas de hierro y acero |
| 2420.0 | Fabricación de productos primarios de metales preciosos y metales no ferrosos |
| 2431.0 | Fundición de hierro y acero |
| 2432.0 | Fundición de metales no ferrosos |
| 2511.0 | Fabricación de productos metálicos para uso estructural |
| 2512.0 | Fabricación de tanques, depósitos y recipientes de metal |
| 2513.0 | Fabricación de los generadores del vapor, excepto calderas de agua caliente para calefacción central |
| 2520.0 | Fabricación de armas y municiones |
| 2592.0 | Tratamiento y revestimiento de metales |
| 2599.0 | Fabricación de otros productos de metal n.c.p |
| 2610.0 | Fabricación de componentes y tableros electrónicos |
| 2630.0 | Fabricación de equipos de comunicaciones |
| 2651.0 | Fabricación de equipos de medición, prueba, verificación, navegación y de equipo de control |
| 2652.0 | Fabricación de relojes |
| 2670.0 | Fabricación de instrumentos ópticos de equipo fotográfico |
| 2710.0 | Fabricación de motores eléctricos, generadores, transformadores eléctricos y aparatos de distribución y control de la energía eléctrica |
| 2720.0 | Fabricación de baterias y acumuladores |
| 2732.0 | Fabricación de otros hilos y cables eléctricos |
| 2740.0 | Fabricación de equipos de iluminación eléctricos |
| 2750.0 | Fabricación de aparatos de uso doméstico |
| 2790.0 | Fabricación de otro equipo eléctrico |
| 2811.0 | Fabricación de motores y turbinas, excepto motores para aeronaves, vehículos automotores y motocicletas |
| 2813.0 | Fabricación de otras bombas, compresores, grifos y válvulas |
| 2815.0 | Fabricación de hornos, hogueras y quemadores |
| 2817.0 | Fabricación de maquinaria y equipo de oficina (excepto computadoras y equipos periféricos) |
| 2821.0 | Fabricación de maquinaria agropecuaria y forestal |
| 2822.0 | Fabricación de maquinarias para la conformación de metales y de máquinas herramienstas |
| 2823.0 | Fabricación de maquinaria para metalúrgica |
| 2824.0 | Fabricación de maquinaria para explotación de minas y canteras y para obras de construcción |
| 2825.0 | Fabricación de maquinaria para la elaboración de alimentos, bebidas y tabaco |
| 2826.0 | Fabricación de maquinaria para la elaboración de productos textiles, prendas de vestir y cueros |
| 2829.0 | Fabricación de otros tipos de maquinaria de uso especial |
| 2910.0 | Fabricación de vehículos automotores |
| 2920.0 | Fabricación de carrocerias para vehículos automotores; fabricación de remolques y semiremolques |
| 2930.0 | Fabricación de partes y accesorios para motores de vehículos |
| 3012.0 | Construcción de embarcaciones de recreo y deporte |
| 3020.0 | Fabricación de locomotoras y de material rodante |
| 3030.0 | Fabricación de aeronaves y naves espaciales y de maquinaria conexa |
| 3091.0 | Fabricación de motocicletas |
| 3092.0 | Fabricación de bicicletas y sillas de ruedas para personas con discapacidad |
| 3099.0 | Fabricación de otros tipos de equipo de transporte n.c.p |
| 3100.1 | Fabricación de muebles de madera |
| 3100.9 | Fabricación de muebles de otro tipo de material (excepto de madera, piedra, cemento o cerámica) |
| 3211.0 | Fabricación de joyas y artículos conexos |
| 3212.0 | Fabricación de bisutería y artículos conexos |
| 3220.0 | Fabricación de Instrumentos musicales |
| 3230.0 | Fabricación de artículos de deporte |
| 3240.0 | Fabricación de juegos y juguetes |
| 3250.0 | Fabricación de instrumentos y suministros médicos y odontológicos |
| 3290.0 | Otras industrias manufactureras n.c.p |
| 3311.0 | Reparación de productos elaborados de metal |
| 3312.0 | Reparación de maquinaria |
| 3314.0 | Reparación de equipo eléctrico |
| 3315.0 | Reparación de equipo de transporte, excepto los vehículos automotores |
| 3510.0 | Generación , transmisión y distribución de energía eléctrica |
| 3520.0 | Fabricación de gas; distribución de combustibles gaseosos por tuberías |
| 3530.0 | Suministro de vapor y aire acondicionado |
| 3600.0 | Captación, tratamiento y distribución de agua |
| 3700.0 | Evacuación de aguas residuales |
| 3811.0 | Recolección de desechos no peligrosos |
| 3830.0 | Recuperación de materiales |
| 4100.0 | Construcción de edificios |
| 4210.0 | Construcción de carreteras y vías férreas |
| 4311.0 | Demolición |
| 4312.0 | Preparación del terreno |
| 4321.0 | Instalación eléctrica y de telecomunicaciones |
| 4322.0 | Instalación de fontanería e instalación de calefacción y aire acondicionado |
| 4329.0 | Otro tipo de instalaciones de construcción |
| 4330.0 | Terminación y acabados de edificios |
| 4390.0 | Otras actividades especializadas de la construcción |
| 4510.0 | Venta de vehículos automotores |
| 4520.0 | Mantenimiento y reparación de vehículos automotores |
| 4530.0 | Venta de partes, piezas y accesorios de vehículos automotores |
| 4540.0 | Venta, mantenimiento y reparación de motocicletas y de sus partes, piezas y accesorios |
| 4610.0 | Venta al por mayor a cambio de una retribución o por contrato |
| 4620.0 | Venta al por mayor de materias primas agropecuarias y animales vivos |
| 4630.3 | Venta al por mayor de frutas y verduras frescas |
| 4630.4 | Venta al por mayor de licores, bebidas y tabaco |
| 4630.9 | Venta al por mayor de otros alimentos |
| 4641.1 | Venta al por mayor de productos textiles y prendas de vestir |
| 4649.1 | Venta al por mayor de aparatos, artículos y equipo de uso doméstico |
| 4649.2 | Venta al por mayor de productos farmacéuticos, veterinarios y artículos de tocador |
| 4649.3 | Venta al por mayor de juegos, juguetes, artículos deportivos y de esparcimiento |
| 4649.9 | Venta al por mayor de otros enseres domésticos n.c.p |
| 4651.0 | Venta al por mayor de computadoras, equipo informático periférico y programas informáticos |
| 4652.0 | Venta al por mayor de equipo, partes y piezas electrónicas y de telecomunicaciones |
| 4653.0 | Venta al por mayor de maquinaria, equipo y materiales agropecuarios |
| 4659.0 | Venta al por mayor de otro tipo de maquinaria y equipo |
| 4661.0 | Venta al por mayor de combustibles sólidos, líquidos y gaseosos y de productos conexos |
| 4662.0 | Venta al por mayor de metales y de minerales metalíferos |
| 4663.0 | Venta al por mayor de materiales de construcción, articulos de ferretería y equipo y materiales de fontanería y calefacción |
| 4669.1 | Venta al por mayor de fertilizantes y productos agroquímicos |
| 4669.9 | Venta al por mayor de basura, desechos y otros productos n.c.p |
| 4690.0 | Venta al por mayor de otros productos no especializada |
| 4711.1 | Venta al por menor en supermercados, almacenes y similares |
| 4711.2 | Venta al por menor en minisuper, pulperias y similares |
| 4719.1 | Venta al por menor en tiendas por departamentos |
| 4719.9 | Venta al por menor en bazares y otros establecimientos no especializados n.c.p |
| 4721.1 | Venta al por menor de carne de bovino, porcino y sus derivados |
| 4721.3 | Venta al por menor de pescados y mariscos |
| 4721.4 | Venta al por menor de frutas y verduras frescas |
| 4721.9 | Venta al por menor de otros alimentos |
| 4722.0 | Venta al por menor de bebidas en comercios especializados |
| 4723.0 | Venta al por menor de tabaco en comercios especializados |
| 4730.0 | Venta al por menor de combustibles para vehículos automotores en comercios especializados |
| 4741.0 | Venta al por menor de computadoras, equipo periféricos, programas de información y telecomunicaciones en comercios especializados |
| 4742.0 | Venta al por menor de equipo de audio y video en comercios especializados |
| 4751.0 | Venta al por menor de productos textiles en comercios especializados |
| 4752.1 | Venta al por menor de artículos de ferretería y pinturas en comercios especializados |
| 4752.2 | Venta al por menor de productos de vidrio en comercios especializados |
| 4753.0 | Venta al por menor de tapices, alfombras, cubrimientos para paredes y pisos en comercios especializados |
| 4759.0 | Venta 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.0 | Venta al por menor de libros, periódicos y artículos de papelería en comercios especializados |
| 4762.0 | Venta al por menor de grabaciones musicales y videográficas en comercios especializados |
| 4763.0 | Venta al por menor de artículos de deporte en comercios especializados |
| 4764.0 | Venta al por menor de juegos y de juguetes en almacenes especializados |
| 4771.1 | Venta al por menor de prendas de vestir y artículos de cuero en almacenes especializados |
| 4771.2 | Venta al por menor de zapatos en comercios especializados |
| 4772.0 | Venta al por menor de productos farmacéuticos y medicinales, cosméticos y artículos de tocador en comercios especializados |
| 4773.0 | Otra venta al por menor de otros productos nuevos en comercios especializados |
| 4774.0 | Venta al por menor de artículos de segunda mano |
| 4781.0 | Venta al por menor en puestos de venta y mercados de: alimentos, bebidas y de productos del tabaco |
| 4799.0 | Otros tipos de venta al por menor no realizada en comercios, puestos de venta o mercados |
| 4911.0 | Transporte de pasajeros por ferrocarril |
| 4921.0 | Transporte terrestre de pasajeros del área urbana, suburbana por vía terrestre |
| 4922.1 | Transporte terrestre de estudiantes y de empleados |
| 4922.2 | Servicios de taxi |
| 4922.3 | Transporte terrestre no regular de pasajeros, en buses |
| 4923.1 | Transporte terrestre local e internacional de contenedores |
| 4923.9 | Otro transporte de carga n.c.p |
| 4930.0 | Transporte por tuberías |
| 5012.0 | Transporte marítimo y de cabotaje de carga |
| 5110.0 | Transporte de pasajeros por vía aérea |
| 5120.0 | Transporte de carga por vía aérea |
| 5210.0 | Almacenamiento y depósito |
| 5221.1 | Remolque y asistencia para vehículo destinado al transporte terrestre |
| 5221.2 | Servicios de estacionamientos y garajes |
| 5221.9 | Servicios de administración de carreteras, puentes y otros servicios auxiliares |
| 5223.0 | Actividades de servicio secundario de transporte por vía aérea |
| 5224.0 | Manipulación de carga |
| 5229.0 | Otras actividades de apoyo al transporte |
| 5310.0 | Servicios postales (Correo) |
| 5510.1 | Actividades de alojamiento para estancias cortas en hoteles y moteles |
| 5510.9 | Otras actividades de alojamiento para estancias cortas n.c.p |
| 5590.0 | Otras actividades de alojamientos |
| 5610.0 | Actividades de restaurantes y de servicios móviles de comidas |
| 5621.0 | Suministros de comidas por encargo |
| 5630.0 | Actividades de servicio de bebidas |
| 5811.0 | Edición de libros |
| 5813.0 | Edición de periódicos, diarios y revistas |
| 5911.0 | Actividades de producción de películas cinematográficas, videos y programas de televisión |
| 5914.0 | Actividades de proyección de películas |
| 5920.0 | Actividades de grabación y publicación de grabaciones sonoras |
| 6010.0 | Transmisiones de radio |
| 6020.0 | Programación y transmisión de televisión |
| 6110.0 | Actividades de telecomunicaciones alámbricas |
| 6120.0 | Actividades de telecomunicaciones inalámbricas |
| 6190.1 | Servicios de café internet |
| 6190.9 | Otras actividades de telecomunicación |
| 6201.0 | Actividades de programación informática |
| 6202.0 | Actividades de consultoría informática y gestión de instalaciones informáticas |
| 6209.0 | Otras actividades de la tecnología de información y servicio informáticos |
| 6311.0 | Procesamiento de datos, hospedaje y actividades conexas |
| 6312.0 | Portal Web |
| 6391.0 | Actividades de agencias de noticias |
| 6411.0 | Banca Central |
| 6419.0 | Otros tipos de intermediación monetaria |
| 6420.0 | Actividades de sociedades de cartera |
| 6430.0 | Fondo y sociedades de Inversión y entidades financieras similares |
| 6491.0 | Arriendamiento financiero |
| 6492.0 | Otros tipos de crédito |
| 6499.0 | Otras actividades de servicios financiero, excepto las de seguros y fondos de pensiones n.c.p |
| 6511.0 | Seguros de vida |
| 6512.0 | Seguros generales |
| 6530.0 | Fondos de pensión |
| 6611.0 | Administración de mercados financieros |
| 6612.0 | Corretaje de valores y de contratos de productos básicos |
| 6619.0 | Otras actividades auxiliares a las actividades de servicios financieros |
| 6622.0 | Actividades de agentes y corredores de seguro |
| 6629.0 | Otras actividades auxiliares de seguros y fondos de pensiones |
| 6810.0 | Actividades inmobiliarias con bienes propios o arrendados |
| 6820.0 | Actividades inmobiliarias realizadas a cambio de una retribución o por contrata |
| 6910.0 | Actividades jurídicas |
| 6920.0 | Actividades de contabilidad, teneduría de libros y auditorías; consultoría fiscal |
| 7010.0 | Actividades de oficinas principales |
| 7020.0 | Actividades de consultoría en gestión |
| 7110.0 | Actividades de arquitectura e ingeniería y actividades conexas de consultoría técnica |
| 7120.0 | Ensayos y análisis técnicos |
| 7210.0 | Investigaciones y desarrollo experimental en el campo de las ciencias naturales y la ingeniería |
| 7220.0 | Investigación y desarrollo experimental en el campo de las ciencias sociales y las humanidades |
| 7310.0 | Publicidad |
| 7320.0 | Estudios de mercados y encuestas de opinión pública |
| 7410.0 | Actividades especializadas de diseño |
| 7420.0 | Actividades de fotografía |
| 7490.0 | Otras actividades profesionales, científicas y técnicas n.c.p. |
| 7500.0 | Actividades veterinarias |
| 7710.1 | Alquiler de automóviles sin conductor |
| 7721.0 | Alquiler y arrendamiento de equipo recreativo y deportivo |
| 7722.0 | Alquiler de cintas de vídeo y discos |
| 7729.0 | Alquiler y arrendamiento de otros efectos personales y enseres domésticos |
| 7730.0 | Alquiler y arrendamiento de otros tipo de maquinaria, equipo y bienes tangibles |
| 7740.0 | Arrendamiento de propiedad intelectual y productos similares excepto obras protegidas por derechos de autor |
| 7830.0 | Otras actividades de dotación de recursos humanos |
| 7911.0 | Actividades de agencias de viajes |
| 7912.0 | Actividades de operadores turísticos |
| 8010.0 | Actividades de seguridad privada |
| 8110.0 | Actividades combinadas de apoyo a instalaciones |
| 8121.0 | Limpieza general de edificios |
| 8129.0 | Otras actividades de limpieza de edificios e instalaciones industriales |
| 8130.0 | Actividades de paisajismo y servicios de mantenimiento conexos |
| 8219.0 | Fotocopiado, preparación de documentos y otras actividades especializadas de apoyo de oficina |
| 8220.0 | Actividades de centros de llamadas |
| 8291.0 | Actividades de agencias de cobro y agencias de calificación crediticia |
| 8292.0 | Actividades de envasado y empacado |
| 8299.0 | Otras actividades de servicios de apoyo a las empresas n.c.p |
| 8411.0 | Actividades de la administración pública en general |
| 8413.0 | Regulación y facilitación de la actividad económica |
| 8421.0 | Relaciones exteriores |
| 8423.0 | Actividades de mantenimiento del orden público y de seguridad |
| 8430.0 | Actividades de planes de seguridad social de afiliación obligatoria |
| 8510.0 | Enseñanza preescolar y primaria |
| 8521.0 | Enseñanza secundaria de formación general |
| 8522.0 | Enseñanza de formación técnica y profesional |
| 8530.1 | Enseñanza universitaria |
| 8530.2 | Enseñanza parauniversitaria |
| 8541.0 | Educación deportiva y recreativa |
| 8542.0 | Educación cultural |
| 8549.0 | Otros tipos de enseñanza n.c.p |
| 8550.0 | Actividades de apoyo a la enseñanza |
| 8610.0 | Actividades de hospitales |
| 8620.1 | Actividades de médicos |
| 8620.2 | Actividades de odontólogos |
| 8690.1 | Laboratorios médicos y de diagnóstico |
| 8690.9 | Otras actividades relacionadas con la salud humana |
| 8720.0 | Actividades de atención en instituciones para personas con retraso mental, enfermos mentales y toxicómanos |
| 8790.0 | Otras actividades de atención en instituciones n.c.p |
| 8810.0 | Asistencia social sin alojamiento para el adulto mayor y personas con discapacidad |
| 8890.0 | Otras actividades de asistencia social sin alojamiento n.c.p |
| 9000.0 | Actividades creativas, artísticas y de entretenimiento |
| 9101.0 | Actividades de Bibliotecas y archivos |
| 9102.0 | Actividades de museos y preservación de lugares históricos |
| 9103.0 | Actividades de jardines botánicos, zoológicos y parques naturales |
| 9200.0 | Actividades de juego de azar y apuestas |
| 9311.0 | Gestión de instalaciones deportivas |
| 9319.0 | Otras actividades deportivas |
| 9329.0 | Otras actividades de esparcimiento y recreativas n.c.p |
| 9411.0 | Actividades de Asociaciones empresariales y de empleadores |
| 9412.0 | Actividades de asociaciones profesionales |
| 9420.0 | Actividades de sindicatos |
| 9491.0 | Actividades de organizaciones religiosas |
| 9492.0 | Actividades de organizaciones políticas |
| 9499.0 | Actividades de otras asociaciones n.c.p |
| 9511.0 | Reparación de computadoras y equipo periférico |
| 9512.0 | Reparación de equipos de comunicaciones |
| 9521.0 | Reparación de aparatos electrónicos de consumo eléctrico |
| 9522.0 | Reparación de aparatos de uso doméstico y equipo doméstico y de jardinería |
| 9523.0 | Reparación de calzado y artículos de cuero |
| 9524.0 | Reparación de muebles y accesorios domésticos |
| 9529.0 | Reparación de otros bienes personales y enseres domésticos n.c.p |
| 9601.0 | Lavado y limpieza, incluida la limpieza en seco, de productos textiles y piel |
| 9602.0 | Actividades de peluquería y otros tratamientos de belleza |
| 9603.0 | Funerales y actividades conexas |
| 9609.0 | Otras actividades de servicios personales n.c.p |
| 9700.0 | Actividades de los hogares como empleadores de personal doméstico |
| 9900.0 | Actividades 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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/locations/costa-rica"
{
"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" }
]
}
]
}
]
}
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ódigo | Provincia | Cantones | Distritos |
|---|---|---|---|
| 1 | San José | 20 | 123 |
| 2 | Alajuela | 16 | 116 |
| 3 | Cartago | 8 | 53 |
| 4 | Heredia | 10 | 48 |
| 5 | Guanacaste | 11 | 61 |
| 6 | Puntarenas | 13 | 61 |
| 7 | Limón | 6 | 30 |
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
| Nivel | Campo | Formato | Ejemplo |
|---|---|---|---|
| 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 |
"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
| Dato | Campo | Dó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.
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.
// 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",
};
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
| Regla | Comportamiento |
|---|---|
| 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.
curl -X GET "https://sandbox.vision-total.rogcr.com/api/exchange-rates/USD" \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"currency": "USD",
"rate": 512.35,
"fetchedAt": "2026-11-05T08:00:00.000Z"
}
}
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.
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.
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étodo | Ruta | Qué hace | Respuesta |
|---|---|---|---|
| POST | /api/customers | Alta de cliente. | 201 con el cliente. |
| GET | /api/customers | Lista los clientes activos. | Array plano, sin paginación. |
| GET | /api/customers/:id | Detalle de un cliente. | Objeto cliente. |
| PUT | /api/customers/:id | Edición: solo se aplica lo que viene en el cuerpo. | Objeto cliente actualizado. |
| DELETE | /api/customers/:id | Baja lógica. | 204 sin cuerpo. |
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"
}'
| HTTP | message | Causa |
|---|---|---|
| 400 | name is required | Falta el nombre del cliente. |
| 400 | El documento es obligatorio y no cumple el formato para el tipo de identificación seleccionado. | La identificación no valida contra governmentIdType. |
| 400 | El correo electrónico es obligatorio y debe tener un formato válido. | Falta el correo o no es válido. |
| 400 | La 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. |
| 400 | La 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. |
| 409 | Ya existe un cliente con ese documento en tu organización. | La identificación ya está registrada. |
| 404 | Customer not found | El id no existe o pertenece a otra organización. |
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.
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étodo | Ruta | Notas |
|---|---|---|
| GET | /api/warehouses | Array plano. |
| GET | /api/warehouses/:id | Detalle de una bodega. |
| GET | /api/warehouses/:id/balances | Saldos de la bodega: array de { productId, warehouseId, quantity }. |
| POST | /api/warehouses | name es obligatorio. |
| PUT | /api/warehouses/:id | Edita name e isDefault. |
| DELETE | /api/warehouses/:id | Baja lógica. Responde 204. |
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étodo | Ruta | Notas |
|---|---|---|
| GET | /api/products | Array plano, sin paginación. Filtros: ?sellableOnly=true y ?categoryIds= (uno o varios). |
| GET | /api/products/:id | Detalle con los saldos calculados. |
| POST | /api/products | Alta de producto o servicio. |
| PUT | /api/products/:id | Edición: los campos omitidos conservan su valor. |
| DELETE | /api/products/:id | Baja lógica. Responde 204. |
| GET | /api/categories | Array plano, con productsCount en cada fila. Filtros: ?search= y ?withProducts=true. |
| GET | /api/categories/:id | Detalle, con el arreglo products completo. |
| POST / PUT / DELETE | /api/categories | Alta (name obligatorio), edición y baja. El DELETE responde 200 con { message }, no 204. Un nombre repetido en la organización responde 409. |
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.
| Campo | Req. | Reglas |
|---|---|---|
| name | Sí | Nombre del producto o del servicio. |
| price | No | Precio de venta sin impuestos. Por defecto 0. |
| costPrice | No | Costo de adquisición. |
| sku | No | Código interno del producto. |
| productType | No | simple (por defecto) · product · service. Los valores composite e ingredient existen para producción de lotes y quedan fuera de esta guía. |
| cabys | No | Código CABYS de 13 caracteres. Obligatorio para poder facturar el producto. |
| codigoImpuesto | No | Catálogo CodigoImpuesto (Nota 8). Por defecto 01 (IVA). Las descripciones oficiales y las reglas por código están en 14.3. |
| tarifaIvaCode | No | Catálogo CodigoTarifaIVA. Por defecto 08 - 13 % general. |
| taxDetail | No | Obligatorio 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. |
| unitOfMeasureId | No | Unidad de medida del catálogo (GET /api/units-of-measure). Obligatoria solo para ingredient y composite. |
| categoryIds | No | IDs de GET /api/categories. En el PUT, un array vacío quita todas las categorías. |
| codigoMoneda | No | CRC, USD o EUR. |
| isSellable | No | Por defecto true. Un producto no vendible no aparece en los listados de venta. |
| allowNegativeStock | No | Por defecto false: sin existencias no se puede descontar. |
| minStock | No | Umbral de existencias mínimas que usa el reporte de inventario. |
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.
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.
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..."
}'
| Campo | Req. | Reglas |
|---|---|---|
| quantity | Sí | Cantidad movida. En una salida es el consumo. |
| type | Sí | in entrada · out salida · adjustment ajuste · return devolución. |
| reason | No | Motivo del movimiento. En las salidas se genera uno por defecto. |
| warehouseId | No | Bodega destino. Si se omite, se usa la predeterminada. |
| referenceId | No | Referencia externa (orden, documento, guía). |
| uomId | No | Unidad de medida. Debe coincidir con la del producto: no hay conversiones. |
| Método | Ruta | Notas |
|---|---|---|
| GET | /api/products/:id/movements | Historial 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/:id | Transacción de inventario con todos sus movimientos. |
| POST | /api/waste | Registra una merma: producto, bodega, cantidad y motivo. |
| POST | /api/inventory/reconcile | Recalcula el saldo de un producto en una bodega a partir de los movimientos confirmados y deja un ajuste por la diferencia. |
| Campo | Req. | Reglas |
|---|---|---|
| productId | Sí | Producto afectado por la merma. |
| warehouseId | Sí | Bodega donde se registra la merma. |
| quantity | Sí | Debe ser mayor que cero. |
| reason | Sí | spoilage · expired · spill · drop · contamination · customer_return · other. El valor preparation_loss es interno y se rechaza. |
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.
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"
}'
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étodo | Ruta | Notas |
|---|---|---|
| POST | /api/waste | Registra una merma. Devuelve { success: true, transactionId }. |
| POST | /api/inventory/reconcile | Recalcula el saldo. Devuelve { previous, computed, delta, txId }. |
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étodo | Ruta | Permiso | Propósito |
|---|---|---|---|
| GET | /api/advances | advances:view | Listar anticipos. |
| GET | /api/advances/:id | advances:view | Detalle de un anticipo. |
| POST | /api/advances | advances:create | Crear un anticipo en borrador. |
| PATCH | /api/advances/:id | advances:edit | Editar un anticipo en borrador o pendiente. |
| POST | /api/advances/:id/submit | advances:create | Enviar el anticipo a aprobación. |
| POST | /api/advances/:id/approve | advances:approve | Aprobar un anticipo pendiente. |
| POST | /api/advances/:id/collect | advances:collect | Marcar el anticipo como cobrado o pagado. |
| POST | /api/advances/:id/void | advances:void | Anular un anticipo que todavía no se aplicó. |
| GET | /api/customers/:id/available-advances | advances:view | Anticipos del cliente con saldo disponible. |
| GET | /api/suppliers/:id/advances | advances:view | Anticipos del proveedor. |
| GET | /api/advances/:id/pdf | advances:view | Recibo de dinero de anticipo en PDF. |
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.
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.
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.
Registrar el pago
Los medios de pago se acumulan hasta cubrir el total. La factura pasa a
paid y queda habilitada para facturar.
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/facturarFirma 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 + jobIdSondeo del estado ante Hacienda
El backend consulta a Hacienda con la clave y guarda el
MensajeHacienda. El estado final se lee por API.
Paso 1 · Crear la factura
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"
}
]
}'
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 cabecera | Req. | Reglas |
|---|---|---|
| customerId | Sí | 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. |
| documentType | No | 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). 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. |
| terminalId | Cond. | 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. |
| sucursalCodigo | Cond. | Exactamente 3 dígitos. |
| numeroTerminal | Cond. | Exactamente 5 dígitos. |
| codigoActividadEmisor | No | Debe pertenecer a la organización. Si se omite se usa la actividad principal. |
| actividadEconomicaReceptor | No | CIIU4 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. |
| condicionVenta | Sí | 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). |
| condicionVentaOtros | No | Obligatorio si condicionVenta es 99 - Otros. |
| plazoCredito | No | Días de crédito. Se usa con las condiciones 02, 08 y 10. |
| codigoMoneda | No | CRC, USD o EUR. Cualquier otro valor responde 400 con codigoMoneda debe ser CRC, USD o EUR. |
| tipoCambio | No | Obligatorio y mayor que cero si la moneda no es CRC. Consúltelo en GET /api/exchange-rates/:currency (véase 8.10). |
| items | Sí | Al menos una línea. |
| supplierId | No | Proveedor 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. |
| supplierActivityCode | No | CIIU4 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». |
| partidaArancelaria | No | Solo 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. |
| otrosCargos | No | Véase 14.6 · Otros cargos. Máximo 15 conceptos. |
| notes | No | Nota interna de la factura. No viaja al XML. |
| Campo de línea | Req. | Reglas |
|---|---|---|
| productId | No | Si 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. |
| quantity | Sí | Debe ser mayor que cero. |
| price | Sí | Precio unitario sin impuestos. |
| taxRate | Sí | Fracción decimal. 0.13 produce <Tarifa>13.00</Tarifa>. |
| tarifaIvaCode | Sí | Código de tarifa del catálogo de Hacienda. |
| detalle | Sí | Descripción de la línea, va a <Detalle>. Máximo 255 caracteres. |
| codigoCabys | No | CABYS 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. |
| codigoImpuesto | No | Có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. |
| taxDetail | No | Datos 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. |
| numeroVINoSerie | No | VIN 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). |
| registroMedicamento | No | Nú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). |
| formaFarmaceutica | No | Código de la forma farmacéutica (máximo 3 caracteres, Nota 19). Opcional. No aplica al REP (10). |
| unidadMedida | No | Por 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. |
| discount | No | Descuento 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. |
| codigoDescuento | No | Código de descuento de la Nota 20 (01–09, 99). Catálogo y reglas en 14.10. |
| codigoDescuentoOtro | No | Descripción del descuento cuando el código es 99 (5–100 caracteres). |
| naturalezaDescuento | No | Naturaleza del descuento (3–80 caracteres). Obligatoria con cualquier descuento; si se omite, el backend precarga la del código elegido. |
| exoTipoDocumento | No | Tipo 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. |
| exoNumeroDocumento | No | Nú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 / exoInciso | No | Artí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. |
| exoNombreInstitucion | No | Institució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. |
| exoFechaEmision | No | Fecha 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 / exoNombreInstitucionOtros | No | Descripciones libres de los códigos 99 de la Nota 10.1 (5 a 100 caracteres cada una). |
| exoTarifaExoneradaOverride | No | 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 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. |
| exoMontoExoneradoOverride | No | Monto 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. |
| notes | No | Nota interna de la línea. No viaja al XML. |
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.
{
"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
}
}
{
"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"
}
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étodo | Ruta | Qué hace |
|---|---|---|
| GET | /api/invoices | Lista paginada. Devuelve { data: [], total, page, limit, totalPages } sin líneas ni pagos: pida el detalle para eso. |
| GET | /api/invoices/:id | Detalle con líneas, pagos y comprobantes de Hacienda. Aquí está numeroConsecutivo. |
| PATCH | /api/invoices/:id | Edició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/:id | Baja lógica. Sólo se puede eliminar una factura en draft; en cualquier otro estado la API responde 400. |
| GET | /api/invoices/:id/quotation-pdf | Descarga el PDF de la cotización. Acepta ?version=. |
| GET | /api/invoicing/records | Listado unificado de lo facturado (POS y Facturador), con el consecutivo oficial de Hacienda de cada comprobante. |
Filtro de GET /api/invoices | Por defecto | Notas |
|---|---|---|
| page | 1 | Mínimo 1. |
| limit | 20 | Se 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. |
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
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" }
]
}'
{
"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 interno | TipoMedioPago en Hacienda | Descripción oficial (Nota 6) |
|---|---|---|
| cash | 01 | Efectivo |
| card | 02 | Tarjeta |
| check | 03 | Cheque |
| transfer | 04 | Transferencia – depósito bancario |
| third_party | 05 | Recaudado por terceros 27 |
| sinpe | 06 | SINPE Móvil |
| digital_platform | 07 | Plataforma Digital |
| other | 99 | Otros — la descripción del medio se indica en la representación gráfica |
| advance | 99 | Anticipo — 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 pago | Req. | Reglas |
|---|---|---|
| method | Sí | El nombre interno del medio. Con advance es obligatorio advanceId. |
| amount | Sí | Monto del pago. No puede superar el saldo pendiente de la factura: la API responde 400 con PAYMENT_EXCEEDS_BALANCE. |
| customerAmount | No | Lo que entregó el cliente. Por defecto es igual a amount. |
| change | No | Cambio devuelto. Por defecto 0. |
| reference | No | Referencia 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>). |
| advanceId | No | Anticipo 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. |
| motivo | No | Motivo del pago. Sólo lo usa el flujo de anticipos sobre una factura ya facturada. |
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.
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.
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
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"
}'
{
"success": true,
"data": {
"haciendaDocumentId": "d41b...",
"clave": "50624102600310123456700100001010000000042112345678",
"jobId": "14"
}
}
| Campo | Req. | Reglas |
|---|---|---|
| documentType | No | 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). 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». |
| customerId | No | Receptor. 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. |
| supplierId | Cond. | 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. |
| actividadEconomicaReceptor | No | CIIU4 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. |
| supplierActivityCode | No | CIIU4 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». |
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,15y99: los códigos09y11se 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».
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.
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.
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).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.
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 ──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.
action | Permiso | De → a | Notas |
|---|---|---|---|
| quote | invoices:transition_quote | draft → quoting | Convierte la factura en cotización. |
| pay | invoices:transition_pay | quoting → paid | Con condición de venta 01 - Contado exige que los pagos cubran el total; si no, 400 con «Pagos insuficientes para marcar como Pagado». |
| invoice | invoices:transition_invoice | paid → invoiced | Emite el comprobante. Equivale a facturar. |
| quote_and_invoice | invoices:transition_invoice | draft \| quoting → invoiced | Atajo: emite de una vez. Exige documentType; si falta, 400 con «Debe elegir el tipo de comprobante (FE, TE, FEC o FEE)». |
| cancel | invoices:transition_cancel | → cancelled | Guarda el motivo como cancelledReason. Una factura ya facturada no se anula por aquí: se anula con una nota de crédito. |
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.
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.
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:
curl -X GET "https://sandbox.vision-total.rogcr.com/api/cabys?q=vehiculo%20electrico&limit=20" \
-H "Authorization: Bearer $TOKEN"
{
"total": 2,
"items": [
{ "codigo": "8711100000000", "descripcion": "Automóviles y camionetas, eléctricos" },
{ "codigo": "8711990000000", "descripcion": "Partes y accesorios de vehículos eléctricos" }
]
}
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:
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
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"
}
]
}'
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.
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
| Campo | Qué es | En 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
| Concepto | Vehículo (exonerado) | Kit de carga | Accesorios |
|---|---|---|---|
| Base imponible | 18.500.000 | 450.000 | 85.000 |
| IVA bruto | 2.405.000 | 58.500 | 11.050 |
MontoExoneracion | 2.405.000 | — | — |
ImpuestoNeto de la línea | 0 | 58.500 | 11.050 |
| Total de la línea | 18.500.000 | 508.500 | 96.050 |
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" }
]
}'
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
- En la factura:
GET /api/invoices/:id→ la línea del vehículo debe traertaxAmount: 0y los camposexo*guardados. - En el XML firmado:
GET /api/facturacion/documents/:id/xml→ el nodo<Exoneracion>dentro de<Impuesto>, con su<MontoExoneracion>y el<ImpuestoNeto>en cero. - 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.
<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>
{
"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
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.
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 corrige | Emisor del derivado | Receptor del derivado | TipoDocIR |
|---|---|---|---|
01 - Factura Electrónica (FE) | La organización | El cliente | 01 |
04 - Tiquete Electrónico (TE) | La organización | El cliente (o ninguno) | 04 |
08 - Factura Electrónica de Compra (FEC) | El proveedor | La organización | 17 (NC) · 18 (ND) |
09 - Factura Electrónica de Exportación (FEE) | La organización | El cliente en el exterior | 19 |
10 - Recibo Electrónico de Pago (REP) | La organización | El cliente | 20 |
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-*.
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.
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
EmisorTypede 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.
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 llamamedioPago, nomedioReembolso.
El 10 - Recibo Electrónico de Pago (REP) solo existe en la primera
variante.
11.1 · 03 - Nota de Crédito Electrónica (NC)
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"
}'
| Campo | Req. | Reglas |
|---|---|---|
| motivo | Sí | Entre 3 y 180 caracteres. Va a <Detalle> de la nota. |
| codigoReferencia | Sí | 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. |
| medioReembolso | No | Medio 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)
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"
}'
| Campo | Req. | Reglas |
|---|---|---|
| motivo | Sí | Entre 3 y 180 caracteres. Va a <Detalle> de la nota. |
| codigoReferencia | Sí | 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. |
| lineas | No | Lí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. |
| totalManual | No | Importe total explícito, para la nota total sin líneas. |
| medioReembolso | No | Medio 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)
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
}'
| Campo | Req. | Reglas |
|---|---|---|
| monto | Sí | Mayor que cero. |
| motivo | Sí | Mínimo 3 caracteres. |
| medioPago | Sí | 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. |
| medioPagoOtros | Cond. | 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. |
| advanceId | No | Si 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>. |
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
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
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/status" \
-H "Authorization: Bearer $TOKEN"
{
"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
}
}
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.
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ó:
{
"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.
// 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
| Ruta | Devuelve | Si falta |
|---|---|---|
| GET /api/facturacion/documents/:id/pdf | Bytes del PDF (application/pdf). | 404 PDF not yet generated for this document |
| GET /api/facturacion/documents/:id/xml | Bytes del XML firmado (application/xml). | 404 Signed XML not yet generated for this document |
| GET /api/facturacion/documents/:id/mensaje-xml | Bytes del MensajeHacienda oficial. | 404 Mensaje de Hacienda not yet available for this document |
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
curl -X GET "https://sandbox.vision-total.rogcr.com/api/facturacion/documents?page=1&limit=20&estado=accepted" \
-H "Authorization: Bearer $TOKEN"
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ámetro | Por defecto | Notas |
|---|---|---|
| page | 1 | Mínimo 1. |
| limit | 20 | Se 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
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
curl -X POST "https://sandbox.vision-total.rogcr.com/api/facturacion/documents/$DOCUMENT_ID/resend" \
-H "Authorization: Bearer $TOKEN"
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.
{
"data": {
"id": "0e77...",
"status": "invoiced",
"haciendaStatus": "accepted",
"haciendaDocuments": [
{
"id": "d41b...",
"documentType": "01",
"clave": "50630102600310987654300100001010000000138111089578",
"numeroConsecutivo": "138",
"estado": "accepted",
"tenantEnvironment": "sandbox"
}
]
}
}
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 ────┘
/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étodo | Ruta | Propósito |
|---|---|---|
| POST | /api/facturacion/documents/:documentId/submit | Reencolar el envío de un documento que se quedó en borrador. Responde 202 con { jobId, clave, documentId }. |
| GET | /api/facturacion/tenant/status | Estado de la habilitación: ACTIVE o PENDING_SETUP, con las credenciales, el certificado, readyForActivation y la lista missing. |
| POST | /api/facturacion/tenant/activate | Activa el tenant en Hacienda. Responde 400 con FACTURACION_SETUP_INCOMPLETE y la lista missing si falta algo. |
| POST | /api/facturacion/tenant/certificates | Sube el certificado .p12 (multipart, campo file, máximo 10 MB) con pin, issuedAt y expiresAt. |
| POST | /api/facturacion/tenant/hacienda-credentials | Registra las credenciales del IDP: environment (sandbox o production), username, password y clientId. |
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).
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
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 MR | Req. | Valores |
|---|---|---|
| tipoMensaje | Sí | 1 - Aceptación total · 2 - Aceptación parcial · 3 - Rechazo. |
| condicionImpuesto | No | 01 - Genera crédito IVA · 02 - Crédito parcial · 03 - Bienes de capital · 04 - Gasto corriente no genera crédito · 05 - Proporcionalidad. |
| codigoActividad | No | CIIU4 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. |
| detalleMensaje | No | Texto libre, máximo 160 caracteres. |
| montoImpuestoAcreditar | No | Importe del crédito de IVA. Número JSON (no texto) y mayor o igual a cero. |
| montoGastoAplicable | No | Importe del gasto aplicable. Número JSON (no texto) y mayor o igual a cero. |
| clasificacion | No | compra o gasto. Es metadata interna, no se envía a Hacienda. Por defecto compra. |
Parte del multipart | Reglas |
|---|---|
| files | El 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). |
| messageConfig | Un único campo con el JSON del MR. Si se omite, el comprobante se registra pero no se genera ningún MR. |
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.
{
"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"
}
]
}
}
}
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.
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.taxIdcon formato de 9 a 12 dígitos.Organization.certificate_s3_keyypin_encrypted: el certificado.p12vigente. Si venció, el código esCERTIFICATE_EXPIRED; si el PIN no abre el certificado,CERTIFICATE_INVALID_PIN.Organization.hacienda_client_id,usernameypassword: las credenciales del IDP.- Al menos una fila en
organization_activities(preferiblemente cones_principal = true): es la que alimenta elcodigoActividaddel MR.
13.2 · Consultar el comprobante de compra
| Método | Ruta | Permiso | Propósito |
|---|---|---|---|
| GET | /api/purchases | purchases:view | Listar comprobantes de compra. Devuelve { success: true, data: [] }, sin paginación. |
| GET | /api/purchases/:id | purchases:view | Detalle del comprobante, con líneas, pagos, totales, archivos, comentarios y el bloque mensajeReceptor. |
| PATCH | /api/purchases/:id | purchases:edit | Editar las notas ({ notes }, máximo 2000 caracteres; null las borra). |
| DELETE | /api/purchases/:id | purchases:delete | Baja lógica. Responde 200 con { success: true }. |
| POST | /api/purchases/:id/sync-hacienda | purchases:sync_hacienda | Volver a sincronizar el comprobante con Hacienda. |
| POST | /api/purchases/mensaje-recepcion/sync | purchases:sync_hacienda | Consultar el estado de un MR concreto. Cuerpo: { purchaseMessageId } o { clave }. |
| GET | /api/purchases/:id/files/:fileId/url | purchases:view | URL prefirmada de S3 para el XML o un PDF adjunto. Vence a los 300 segundos. |
| GET | /api/purchases/:id/comments | purchases:view | Listar los comentarios del comprobante. |
| POST | /api/purchases/:id/comments | purchases:edit | Agregar un comentario ({ body }, máximo 2000 caracteres). Responde 201. |
| DELETE | /api/purchases/:id/comments/:commentId | purchases:edit | Borrar un comentario. Sólo su autor puede: otro usuario recibe 403. |
| POST | /api/purchases/:id/attachments | purchases:edit | Adjuntar archivos (multipart, campo files, hasta 10 archivos de 10 MB, sin filtro de extensión). |
| GET | /api/reports/purchases | purchases:view_report | Libro de Compras: filas, totales por moneda y totales por día. |
| GET | /api/reports/purchases/export.xlsx | purchases:view_report | El Libro de Compras en XLSX. |
Filtro de GET /api/purchases | Notas |
|---|---|
| documentType | Arreglo de códigos 01…10. Se envía como arreglo JSON, no como cadena separada por comas. |
| dateFrom / dateTo | Fecha y hora en formato ISO (2026-11-01T00:00:00.000Z). |
| emisorNumero | Coincidencia exacta con la identificación del emisor. |
| medioPago | Código del medio de pago de la Nota 6 (01…07, 99). |
| text | Búsqueda parcial en el nombre del emisor o en la clave. |
mensajeReceptor
El detalle de GET /api/purchases/:id incluye un bloque
mensajeReceptor con los datos del último MR generado:
id·tipo(1,2o3) ·detalle·montoAceptado.estadoEnvio—no_enviado·procesando·aceptado·rechazado·error.clave·consecutivoReceptor(20 dígitos) ·fechaEmision.respuestaAt·respuestaDetalle·respuestaCodigo·mensajeHaciendaXml.
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.
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 documento | Emisor | Se emite con |
|---|---|---|
01 - Factura Electrónica (FE) | Vendedor | POST /api/invoices/:id/facturar |
02 - Nota de Débito Electrónica (ND) | Vendedor | POST /api/invoices/:id/nota-debito |
03 - Nota de Crédito Electrónica (NC) | Vendedor | POST /api/invoices/:id/nota-credito |
04 - Tiquete Electrónico (TE) | Vendedor | POST /api/invoices/:id/facturar |
05 - Mensaje de aceptación total | Receptor | POST /api/purchases/upload |
06 - Mensaje de aceptación parcial | Receptor | POST /api/purchases/upload |
07 - Mensaje de rechazo | Receptor | POST /api/purchases/upload |
08 - Factura Electrónica de Compra (FEC) | Receptor | POST /api/invoices/:id/facturar |
09 - Factura Electrónica de Exportación (FEE) | Vendedor | POST /api/invoices/:id/facturar |
10 - Recibo Electrónico de Pago (REP) | Vendedor | POST /api/invoices/:id/recibo-pago |
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) |
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ódigo | Descripción oficial | Reglas de la API |
|---|---|---|
| 01 | Impuesto al Valor Agregado | Valor por defecto. Exige un tarifaIvaCode válido. |
| 02 | Impuesto Selectivo de Consumo | Sin desglose de tarifa: el nodo de tarifa no viaja. |
| 03 | Impuesto Único a los Combustibles | Exige taxDetail (cantidadUnidadMedida, impuestoUnidad). |
| 04 | Impuesto específico de Bebidas Alcohólicas | Exige taxDetail. |
| 05 | Impuesto Específico sobre las bebidas envasadas sin contenido alcohólico y jabones de tocador | Exige taxDetail. |
| 06 | Impuesto a los Productos de Tabaco | Exige taxDetail. |
| 07 | IVA (cálculo especial) 28 | Monto = 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). |
| 08 | IVA Régimen de Bienes Usados (Factor) | El monto se calcula como BaseImponible × factorCalculoIVA, no como un porcentaje. |
| 12 | Impuesto Específico al Cemento | Tarifa fija del 5 %: Monto = Subtotal × 5 % (sobre el precio neto de la venta). No usa taxDetail. |
| 99 | Otros | Exige 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. |
«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.
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ódigo | Tarifa oficial | % | Notas |
|---|---|---|---|
| 01 | Tarifa 0% (Artículo 32, num 1, RLIVA) | 0 | Solo transacciones del artículo 32.1 del RLIVA, con derecho a crédito pleno (por ejemplo, ventas a la CCSS o municipalidades). |
| 02 | Tarifa reducida 1% | 1 | — |
| 03 | Tarifa reducida 2% | 2 | — |
| 04 | Tarifa reducida 4% | 4 | — |
| 05 | Transitorio 0% | 0 | Solo para Notas de Crédito y Notas de Débito que lo requieran. |
| 06 | Transitorio 4% | 4 | Solo para Notas de Crédito y Notas de Débito que lo requieran. |
| 07 | Tarifa transitoria 8% | 8 | Código inhabilitado en la versión 4.4. |
| 08 | Tarifa general 13% | 13 | Valor por defecto cuando no se envía tarifaIvaCode. |
| 09 | Tarifa reducida 0.5% | 0.5 | — |
| 10 | Tarifa Exenta | 0 | Tarifa exenta de la Ley 9635, artículo 8. |
| 11 | Tarifa 0% sin derecho a crédito | 0 | Bien 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ódigo | Institución |
|---|---|
| 01 | Ministerio de Hacienda |
| 02 | Ministerio de Relaciones Exteriores y Culto |
| 03 | Ministerio de Agricultura y Ganadería |
| 04 | Ministerio de Economía, Industria y Comercio |
| 05 | Cruz Roja Costarricense |
| 06 | Benemérito Cuerpo de Bomberos de Costa Rica |
| 07 | Asociación Obras del Espíritu Santo |
| 08 | Fecrunapa |
| 09 | EARTH |
| 10 | INCAE |
| 11 | Junta de Protección Social (JPS) |
| 12 | ARESEP — la que autoriza el incentivo de los vehículos eléctricos |
| 99 | Otros (con exoNombreInstitucionOtros) |
Las reglas que el backend aplica
| Regla | Comportamiento 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:
| Campo | Req. | Reglas |
|---|---|---|
| tipoDocumentoOC | Sí | 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. |
| detalle | Sí | Descripción del cargo. Máximo 160 caracteres. |
| montoCargo | Sí | Importe del cargo. Debe ser un número mayor que cero. |
| porcentajeOC | No | Porcentaje del cargo, cuando se calcula sobre una base. |
| tipoDocumentoOTROS | Cond. | Obligatorio con el código 99: 5 a 100 caracteres. |
| identificacionTercero | Cond. | Objeto { tipo, numero } con el tipo del catálogo de la Nota 4 (01…06). Obligatorio con el código 04. |
| nombreTercero | Cond. | Nombre del tercero. Obligatorio con el código 04. |
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ódigo | Tipo de identificación | Formato admitido | Dónde aplica |
|---|---|---|---|
| 01 | Cédula Física | 9 a 12 dígitos. | Emisor, receptor y tercero. |
| 02 | Cédula Jurídica | 9 a 30 caracteres alfanuméricos. | Emisor, receptor y tercero. |
| 03 | DIMEX | 9 a 12 dígitos. | Emisor, receptor y tercero. |
| 04 | NITE | 9 a 12 dígitos. | Emisor, receptor y tercero. |
| 05 | Extranjero No Domiciliado 17 | 5 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. |
| 06 | No Contribuyente 18 | 9 a 20 caracteres alfanuméricos. | Emisor solo en la FEC y solo con condicionVenta 13. |
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ón | Regla de relleno | Ejemplo |
|---|---|---|
| 01 - Cédula Física | Agregar tres ceros al inicio hasta completar 12 dígitos. | 123456789 → 000123456789 |
| 02 - Cédula Jurídica | Agregar dos ceros al inicio hasta completar 12 caracteres. | 3101234567 → 003101234567 |
| 03 - DIMEX | Si la cédula tiene 11 dígitos, agregar un cero al inicio hasta completar 12 dígitos. | 12345678901 → 012345678901 |
| 04 - NITE | Agregar dos ceros al inicio hasta completar 12 dígitos. | 1234567890 → 001234567890 |
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ódigo | Descripción oficial (Nota 6) | method interno | Notas |
|---|---|---|---|
| 01 | Efectivo | cash | — |
| 02 | Tarjeta | card | — |
| 03 | Cheque | check | — |
| 04 | Transferencia – depósito bancario | transfer | — |
| 05 | Recaudado por terceros 27 | third_party | Sólo cuando el pago lo recibe un tercero y no quien emite el servicio o la mercancía. |
| 06 | SINPE Móvil | sinpe | — |
| 07 | Plataforma Digital | digital_platform | — |
| 99 | Otros | other | La descripción del medio se indica en la representación gráfica: el XML exige <MedioPagoOtros>. |
«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.
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
| Campo | Ruta | Qué 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. |
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.
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ódigo | Tipo de descuento (Nota 20) | Notas |
|---|---|---|
| 01 | Descuento por Regalía | El 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. |
| 02 | Descuento por Regalía o Bonificación. IVA Cobrado al Cliente | El IVA sí se cobra al cliente sobre la base neta. Admite descuento parcial. |
| 03 | Descuento por Bonificación | Como el 01: el IVA lo asume el emisor y el descuento es del 100 %. |
| 04 | Descuento por volumen | — |
| 05 | Descuento por temporada (estacional) | — |
| 06 | Descuento promocional | — |
| 07 | Descuento comercial | — |
| 08 | Descuento por frecuencia | — |
| 09 | Descuento sostenido | — |
| 99 | Otros descuentos | Exige codigoDescuentoOtro (5–100 caracteres). |
| Campo | Requerido | Reglas |
|---|---|---|
| items[].discount | No | Monto 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[].codigoDescuento | No | Có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[].codigoDescuentoOtro | No | Obligatorio con el código 99 (5–100 caracteres). Con otro código se ignora. |
| items[].naturalezaDescuento | No | La <NaturalezaDescuento> es obligatoria con cualquier descuento (3–80 caracteres). Si no se manda, el backend precarga la del código elegido. |
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—tarifaIvaCodeválido (INVALID_TARIFA_IVA).03–06—cantidadUnidadMedidaeimpuestoUnidad(INVALID_DATOS_IMPUESTO_ESPECIFICO).08—factorCalculoIVAmayor que cero (INVALID_FACTOR_CALCULO_IVA).99—descripcionImpuestode 5 a 100 caracteres (INVALID_DESCRIPCION_IMPUESTO) y una tarifa mayor que cero (INVALID_TARIFA_OTROS).
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 % delMontoTotalde la línea. Un descuento parcial se rechaza en la API con400(INVALID_CODIGO_DESCUENTO_TOTAL) antes de llegar a Hacienda.-45— el IVA se calcula sobre elMontoTotalde la línea, no sobre la base imponible.-476/-488— ese IVA se declara en<ImpuestoAsumidoEmisorFabrica>y no entra alTotalDesgloseImpuesto(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étodo | Ruta | Autenticación | Propósito |
|---|---|---|---|
| GET | /api/m2m/me | Credencial 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/permissions | Credencial M2M | Catá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étodo | Ruta | Autenticación | Propó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-organization | JWT de usuario | Emitir el token con organizationId. 403 con una credencial de máquina. |
| GET | /api/auth/me | JWT de usuario | Estado de la sesión y organizaciones del usuario. |
| PUT | /api/auth/change-password | JWT de usuario | Cambiar la contraseña propia. 403 con una credencial de máquina. |
Organización y permisos
| Método | Ruta | Permiso | Propósito |
|---|---|---|---|
| GET | /api/organizations | Credencial M2M | La organización de la credencial, con su id y el estado de facturación. |
| GET | /api/organizations/:id | Credencial M2M | Detalle de la organización activa. |
| GET | /api/organizations/:id/activities | Credencial M2M | Actividades 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/activities | organization:edit_organization | Dar de alta una actividad económica (CIIU4). Cuerpo: codigo (1 a 6 caracteres), descripcion, esPrincipal. |
| PUT | /api/organizations/:id/activities/:codigo | organization:edit_organization | Editar la descripción o marcar la actividad principal. |
| DELETE | /api/organizations/:id/activities/:codigo | organization:edit_organization | Eliminar una actividad económica. |
| GET | /api/permissions | Credencial M2M | Catálogo de permisos del sistema. |
Facturador (facturas de venta)
| Método | Ruta | Permiso | Propósito |
|---|---|---|---|
| POST | /api/invoices | invoices:create_invoice | Crear la factura o cotización. |
| GET | /api/invoices | invoices:view_invoices | Listar con filtros y paginación. Sin líneas ni pagos: pida el detalle para eso. |
| GET | /api/invoices/:id | invoices:view_invoices | Detalle con líneas, pagos y comprobantes. Aquí está numeroConsecutivo. |
| PATCH | /api/invoices/:id | invoices:edit_invoice | Editar la factura. |
| DELETE | /api/invoices/:id | invoices:delete_invoice | Baja lógica. |
| POST | /api/invoices/:id/payments | invoices:add_payment | Registrar pagos. |
| DELETE | /api/invoices/:id/payments/:paymentId | invoices:add_payment | Eliminar un pago. |
| POST | /api/invoices/:id/transitions | Según la acción | quote · pay · invoice · quote_and_invoice · cancel. Cada acción exige su propio permiso. Véase 10.4. |
| POST | /api/invoices/:id/facturar | invoices:transition_invoice | Emitir ante Hacienda. |
| POST | /api/invoices/:id/nota-credito | invoices:issue_credit_note | Emitir 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-debito | invoices:issue_debit_note | Emitir 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-pago | invoices:issue_credit_note | Emitir 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-pdf | invoices:view_invoices | Descargar la cotización en PDF. |
| GET | /api/invoicing/records | invoices:view_invoices | Listado unificado de registros facturados, con el consecutivo oficial de Hacienda de cada comprobante. |
Documentos de Hacienda
| Método | Ruta | Propósito |
|---|---|---|
| POST | /api/facturacion/documents | Emitir 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/submit | Reencolar el envío de un documento en borrador. |
| POST | /api/facturacion/documents/:id/resend | Reemitir un documento rechazado o con error. |
| POST | /api/facturacion/documents/:id/nota-credito | Emitir 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-debito | Emitir una 02 - Nota de Débito Electrónica (ND) sobre el comprobante. Campos: motivo, codigoReferencia, lineas, totalManual, medioPago. |
| GET | /api/facturacion/documents/:id/children | Notas derivadas del comprobante. |
| GET | /api/facturacion/documents/:id/status | Estado ante Hacienda. |
| GET | /api/facturacion/documents/:id/pdf | Descargar el PDF. |
| GET | /api/facturacion/documents/:id/xml | Descargar el XML firmado. |
| GET | /api/facturacion/documents/:id/mensaje-xml | Descargar el MensajeHacienda. |
| POST | /api/facturacion/documents/:id/email | Reenviar por correo. |
| GET | /api/facturacion/documents | Listar comprobantes con paginación. |
| GET | /api/facturacion/taxpayers/:identificacion | Directorio 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/consecutivos | Consultar los consecutivos (lectura). |
| GET | /api/facturacion/connection | Estado de la configuración de facturación. |
| GET | /api/facturacion/tenant/status | Estado de la habilitación del tenant en Hacienda, con lo que falta para activarlo. |
| POST | /api/facturacion/tenant/activate | Activar el tenant en Hacienda. |
| POST | /api/facturacion/tenant/certificates | Subir el certificado .p12 (multipart, campo file). |
| POST | /api/facturacion/tenant/hacienda-credentials | Registrar las credenciales del IDP de Hacienda. |
Datos maestros y apoyo
| Método | Ruta | Propósito |
|---|---|---|
| GET /api/ POST /api/ PUT /api/ DELETE | /api/customers | Clientes (receptores). |
| GET /api/ POST /api/ PUT /api/ DELETE | /api/products | Productos y servicios. |
| GET /api/ POST /api/ PUT /api/ DELETE | /api/terminals | Terminales 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/suppliers | Proveedores 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/suggest | Autocompletado de proveedores activos (máximo 20 resultados). |
| GET | /api/cabys?q=&limit= | Buscar códigos CABYS. Devuelve { total, items }. |
| GET | /api/cabys/:codigo | Consultar un CABYS por sus 13 dígitos. |
| GET | /api/units-of-measure | Catá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-activities | Catá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-rica | Catá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/lookup | Validar una autorización de exoneración. |
| GET | /api/exchange-rates/:currency | Tipo de cambio del día (USD o EUR). Devuelve { data: {...} }. |
Compras y Mensaje del Receptor
| Método | Ruta | Permiso | Propósito |
|---|---|---|---|
| GET | /api/purchases | purchases:view | Listar comprobantes de compra. { success: true, data: [] }, sin paginación. |
| GET | /api/purchases/:id | purchases:view | Detalle del comprobante de compra, con el bloque mensajeReceptor: estado del MR (estadoEnvio), respuesta de Hacienda (respuestaDetalle, respuestaCodigo) y el XML mensajeHaciendaXml. |
| POST | /api/purchases/upload | purchases:upload | Cargar 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-hacienda | purchases:sync_hacienda | Volver a sincronizar el comprobante de compra con Hacienda. |
| POST | /api/purchases/mensaje-recepcion/sync | purchases:sync_hacienda | Consultar el estado de un MR concreto. |
| GET | /api/purchases/:id/files/:fileId/url | purchases:view | URL prefirmada de un archivo del comprobante. |
| GET /api/ POST /api/ DELETE | /api/purchases/:id/comments | purchases:view · purchases:edit | Comentarios del comprobante de compra. |
| POST | /api/purchases/:id/attachments | purchases:edit | Adjuntar archivos al comprobante de compra. |
| PATCH /api/ DELETE | /api/purchases/:id | purchases:edit · purchases:delete | Editar las notas o dar de baja lógica el comprobante. |
| GET | /api/reports/purchases | purchases:view_report | Libro de Compras. |
| GET | /api/reports/purchases/export.xlsx | purchases:view_report | Libro de Compras en XLSX. |
Anticipos (RC / PP)
| Método | Ruta | Permiso | Propósito |
|---|---|---|---|
| GET | /api/advances | advances:view | Listar anticipos. |
| GET | /api/advances/:id | advances:view | Detalle de un anticipo. |
| POST | /api/advances | advances:create | Crear un anticipo. |
| PATCH | /api/advances/:id | advances:edit | Editar un anticipo en borrador o pendiente. |
| POST | /api/advances/:id/submit | advances:create | Enviar a aprobación. |
| POST | /api/advances/:id/approve | advances:approve | Aprobar. |
| POST | /api/advances/:id/collect | advances:collect | Marcar como cobrado o pagado. |
| POST | /api/advances/:id/void | advances:void | Anular. |
| GET | /api/customers/:id/available-advances | advances:view | Anticipos del cliente con saldo disponible. |
| GET | /api/suppliers/:id/advances | advances:view | Anticipos del proveedor. |
| GET | /api/advances/:id/pdf | advances:view | Recibo 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étodo | Ruta | Propósito |
|---|---|---|
| GET /api/ POST /api/ PUT /api/ DELETE | /api/customers | Padrón de clientes (receptores). |
| GET /api/ POST /api/ PUT /api/ DELETE | /api/warehouses | Bodegas de inventario. |
| GET | /api/warehouses/:id/balances | Saldos por producto de una bodega. |
| GET /api/ POST /api/ PUT /api/ DELETE | /api/categories | Categorías del catálogo. |
| POST | /api/products/:id/movements | Registrar una entrada, salida o ajuste de stock. |
| GET | /api/products/:id/movements | Historial de movimientos del producto. |
| GET | /api/transactions/:id | Transacción de inventario con sus movimientos. |
| POST | /api/waste | Registrar una merma. |
| POST | /api/inventory/reconcile | Recalcular el saldo de un producto en una bodega. |
| GET | /api/reports/inventory | Reporte de existencias y productos bajo mínimo. |
| GET | /api/reports/inventory-all | Reporte completo de inventario por producto y bodega. |
| GET | /api/reports/inventory/export.xlsx | El reporte de existencias en XLSX. |
| GET | /api/reports/inventory-all/export.xlsx | El reporte completo de inventario en XLSX. |
| GET | /api/reports/inventory/movements | Movimientos de inventario del período. |
| GET | /api/reports/inventory/waste-trend | Tendencia de mermas. Devuelve { data }. |
| GET | /api/reports/inventory/yield-performance | Rendimiento de producción. Devuelve { data }. |
| GET | /api/reports/inventory/transactions | Transacciones de inventario recientes. Devuelve { data }. |
| GET | /api/reports/inventory/waste-by-product | Mermas agrupadas por producto. Devuelve { data }. |
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.
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.
| HTTP | code | Causa | Qué hacer |
|---|---|---|---|
| 400 | FACTURACION_NOT_CONFIGURED | La organización no tiene la facturación electrónica habilitada. | Escalar al administrador del ERP y reintentar más tarde. |
| 400 | ORG_MISSING_TAX_ID | La organización no tiene identificación fiscal. | Actualice la organización antes de emitir. |
| 400 | PRODUCT_MISSING_CABYS | Una línea apunta a un producto sin CABYS. | Asigne el CABYS al producto. |
| 400 | PRODUCT_NOT_FOUND | El 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. |
| 400 | CABYS_PLACEHOLDER_NOT_ALLOWED_IN_PRODUCTION | Se 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.
code | Causa | Dónde |
|---|---|---|
| PRODUCT_NOT_FOUND | El productId de una línea no existe, está archivado o es de otra organización. | POST/PATCH /api/invoices |
| PRODUCT_MISSING_CABYS | El producto no tiene CABYS real y la línea no lo corrige. | POST/PATCH /api/invoices |
| INVALID_CABYS | El codigoCabys de la línea no tiene 13 dígitos. | POST/PATCH /api/invoices |
| INVALID_CODIGO_IMPUESTO | El codigoImpuesto de la línea no está en el catálogo de la Nota 8. | POST/PATCH /api/invoices |
| INVALID_TARIFA_IVA | La tarifa no es válida para el código de impuesto 01 o 07. | POST/PATCH /api/invoices |
| INVALID_DATOS_IMPUESTO_ESPECIFICO | Los códigos 03–06 exigen cantidadUnidadMedida e impuestoUnidad. | POST/PATCH /api/invoices |
| INVALID_FACTOR_CALCULO_IVA | El código 08 exige un factorCalculoIVA mayor que cero. | POST/PATCH /api/invoices |
| INVALID_DESCRIPCION_IMPUESTO | El código 99 exige una descripcionImpuesto de 5 a 100 caracteres. | POST/PATCH /api/invoices |
| INVALID_TARIFA_OTROS | El código 99 exige una tarifa mayor que cero. | POST/PATCH /api/invoices |
| INVALID_CODIGO_DESCUENTO | El codigoDescuento no está en el catálogo de la Nota 20. | POST/PATCH /api/invoices |
| INVALID_MONTO_DESCUENTO | El descuento supera el monto total de la línea. | POST/PATCH /api/invoices |
| INVALID_CODIGO_DESCUENTO_TOTAL | Con 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_OTRO | El código 99 exige un codigoDescuentoOtro de 5 a 100 caracteres. | POST/PATCH /api/invoices |
| INVALID_NATURALEZA_DESCUENTO | La naturalezaDescuento no tiene entre 3 y 80 caracteres. | POST/PATCH /api/invoices |
| INVALID_EXONERACION_NUMERO_DOCUMENTO | El 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_BALANCE | El pago supera el saldo pendiente de la factura. | POST /api/invoices/:id/payments |
| ADVANCE_NOT_AVAILABLE | El anticipo no existe o no tiene saldo disponible. | POST /api/invoices/:id/payments |
| ADVANCE_CUSTOMER_MISMATCH | El anticipo pertenece a otro cliente. | POST /api/invoices/:id/payments |
| FACTURACION_SETUP_INCOMPLETE | Falta algo para activar el tenant: la respuesta incluye la lista missing. | POST /api/facturacion/tenant/activate |
| SUPPLIER_NOT_FOUND | El proveedor no existe en esta organización. | /api/suppliers |
| SUPPLIER_DUPLICATE_IDENTIFICATION | Ya existe un proveedor con esa identificación en la organización. | POST /api/suppliers |
| VALIDATION_ERROR | Error 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_NAMESPACE | El 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_PIN | El certificado .p12 venció o el PIN no lo abre: el MR no se puede firmar. | POST /api/purchases/upload |
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ódigo | Significado | Qué 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. |
{
"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"
}
// 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
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.
| HTTP | code | Causa | Qué hacer |
|---|---|---|---|
| 400 | No hay bodega predeterminada configurada para la organización | Un movimiento o una recepción no indicó warehouseId y la organización no tiene bodega predeterminada. | Cree o marque una bodega con isDefault: true. |
| 400 | Insufficient stock: missing <n> | La salida excede el saldo disponible. | Ajuste el saldo o habilite allowNegativeStock. |
| 400 | Invalid productType / Invalid movement type | Un 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
| Variable | Valor | Uso |
|---|---|---|
| VT_API_BASE | https://sandbox.vision-total.rogcr.com/api | URL base de la API. Ya incluye el prefijo /api. |
| VT_TOKEN | vt_m2m_k3P9xQ7mR2tY.… | Credencial de máquina (M2M). Es la sesión completa: no hay login ni token de refresco. Véase 4. |
| VT_ORG_ID | 3b91c... | Identificador de la organización. Opcional: se puede leer de GET /api/m2m/me. |
17.1 · Flujo completo en JavaScript
// 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
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)
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
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.
- Organización creadaCon su identificación fiscal y su actividad económica principal, por un administrador del ERP.
- 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, conpurchases:*; y si aplica anticipos, conadvances:*. El secreto se entrega una sola vez. - Facturación electrónica habilitadaLa organización ya puede emitir comprobantes ante Hacienda.
- Bodega predeterminadaUna bodega con
isDefault: true. Es la que recibe el stock inicial y los movimientos que no indicanwarehouseId. - Certificado y credenciales vigentesSi el servicio genera Mensajes del Receptor, la organización necesita el
.p12con su PIN y las credenciales del IDP. Véase la sección 13.
Pasos del servicio externo
- 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. - Verificar la credencial
GET /api/m2m/medebe devolver la organización, el rol y el nombre de la credencial. Un401significa que el token está mal copiado, revocado o vencido. - Verificar el tenant
GET /api/facturacion/connectiondebe devolverfacturacionConfigured: true. - Verificar cada receptor en el padrón de Hacienda
GET /api/facturacion/taxpayers/:id: un404significa 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. - 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. - Registrar el padrón y el inventarioBodegas, clientes y saldos iniciales de los productos. Véase la sección 09.
- Mandar la unidad de medida en cada líneaConsultar
GET /api/units-of-measurey mandar elcodedel catálogo enunidadMedida(oproduct.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. - 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, no620100). El del receptor sale del padrón (GET /api/facturacion/taxpayers/:id); el de la organización, deGET /api/organizations/:id/activities. Véase la sección 8.7. - Emitir una primera factura de pruebaCrear, pagar y facturar. Confirmar que
estadollegue aaceptado. El202no es una factura aceptada: hay que sondear. - Guardar el consecutivo del comprobanteLeerlo de
GET /api/invoices/:id→haciendaDocuments[0].numeroConsecutivo. Véase la sección 12.6. - Probar el flujo de compras, si aplica
POST /api/purchases/uploadcon un XML de prueba y unmessageConfig. Confirmar que elmensajeReceptor.estadoEnviopasa deno_enviadoaaceptado. Véase la sección 13. - Pasar a producciónEl administrador habilita la facturación en el ambiente de producción. El servicio no interviene.
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.
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.