Integrar con la API Fiscal de Valhalla
Para los equipos de AdminPos, MHP, AdminPro, AdminFood, CondoSaaS y cualquier otro sistema hermano que necesite emitir documentos fiscales venezolanos bajo la modalidad de imprenta digital (Providencia Administrativa SNAT/2024/000121) en nombre de sus propios clientes.
Modelo de negocio: quién es quién
Es fundamental entender esta relación antes de escribir una sola línea de integración.
Cliente final (ej. un hotel) │ │ usa el sistema SaaS de ustedes (ej. MHP) ▼ Sistema SaaS hermano (MHP, AdminPos, AdminFood, CondoSaaS...) │ │ llama a la API de Valhalla EN NOMBRE del cliente final ▼ Valhalla Imprenta Digital (este sistema) │ │ emite el documento fiscal, lo homologa, genera PDF/XML ▼ SENIAT (vía folios/control numbers autorizados)
Puntos clave
- El cliente final es el tenant en Valhalla, no el SaaS hermano. Cada uno tiene su propia base de datos aislada y su propio token de API.
- El SaaS hermano nunca maneja folios directamente. Llama a la API con el token del cliente final y recibe el documento fiscal ya emitido.
- El cliente final paga sus folios vía SimplexPay, dentro del panel de Valhalla (
/app→ Billetera). No se puede disparar desde la API — ver sección 6. - Un mismo cliente final puede usar más de un SaaS hermano contra el mismo tenant, siempre que ambos usen el mismo token de API.
Antes de integrar
- El cliente final ya fue dado de alta como tenant en Valhalla (equipo Methacortex, panel Admin — sección 3).
- Tenés el API token del cliente final (el mismo valor visible en el panel Admin al crear el tenant).
- Sabés el email asociado a ese token.
- El cliente final tiene una suscripción activa con folios en su billetera — sin esto, todo intento de emitir documentos responde
402. - Revisaste el catálogo de endpoints (sección 7) y el contrato de errores (sección 8).
Onboarding de un cliente final
Lo ejecuta el equipo de Methacortex desde el panel Admin (/admin → Empresas), no el SaaS hermano. Al crear un tenant:
- Se provisiona una base de datos aislada para el cliente (
dvi_<nombre>). - Se genera un API token (o se fija uno manualmente).
- Se crea el usuario dueño con acceso al panel
/appdel cliente. - Se crea el usuario de autenticación de API, con el mismo token, contra el que autentica el endpoint de la sección 4.
- Se le entrega al cliente (o directamente al SaaS hermano) el email del dueño y el API token.
El cliente final debe tener una suscripción activa asignada (panel Admin → Suscripciones) antes de poder emitir ningún documento — ver sección 6.
Autenticación
{
"userName": "<email del usuario del tenant>",
"userPassword": "<API token del tenant>"
}
Respuesta exitosa · 200
{
"success": true,
"message": "Token de Acceso",
"token": "1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
Credenciales inválidas · 401
{ "success": false, "message": "No existe coincidencia de usuario y API Token" }
El token es un Bearer token de Sanctum. Usalo en todos los endpoints siguientes:
Authorization: Bearer <token> Accept: application/json Content-Type: application/json
No expira por tiempo — es válido hasta que se revoque manualmente desde el panel. No hace falta renovarlo cada hora.
Idempotencia
Obligatorio en /invoices: cada request requiere transaccionId, un string hexadecimal de exactamente 50 caracteres, generado por ustedes.
Si reenvían la misma transaccionId (timeout, reintento de red), la API detecta el duplicado:
HTTP 201 { "success": false, "message": "Yo detecté un intento duplicado de inyección para este transaccionId." }
Chequeen el campo success, no asuman que un 2xx significa éxito. El 201 aquí confirma "ya fue procesada" — nunca reintenten con el mismo transaccionId esperando un resultado distinto.
Este mecanismo solo existe en /invoices. Los demás endpoints validan duplicados por número de documento — ver sección 8.
Modelo de folios y billetera
Cada documento emitido consume un folio de la billetera comercial del tenant, que el cliente final recarga pagando con SimplexPay dentro de Valhalla.
Esto es distinto del rango de numeración autorizado por el SENIAT (la "serie" fiscal). Un tenant puede tener un rango SENIAT enorme y aun así quedarse sin saldo pagado — son cosas independientes.
Sin saldo disponible
HTTP 402 Payment Required { "success": false, "message": "Pago requerido: Saldo de folios agotado." }
HTTP 402 Payment Required { "success": false, "message": "Pago requerido: No encontré una suscripción activa." }
No es un error de su lado — es una señal de negocio. Muestren al cliente un mensaje claro y un enlace a su panel de Valhalla (/app → Billetera) para recargar.
Por qué no se puede recargar desde la API
El checkout con SimplexPay vive exclusivamente dentro del panel Filament de Valhalla, autenticado por sesión web. No existe, ni existirá por diseño, un endpoint de API para disparar una recarga en nombre del cliente.
Emisión por lote
/invoices, /dispatch-guides y /delivery-orders aceptan varios documentos en un solo request. El chequeo de saldo se hace contra el tamaño total del lote antes de procesar nada: si el tenant tiene 3 folios y el lote pide 5, la API rechaza todo con 402 sin crear ningún documento. Nunca reciben un resultado parcial.
Catálogo de endpoints
Base URL: https://valhallaimprenta.com/api/v1. Todos requieren Authorization: Bearer <token> salvo donde se indique lo contrario.
{
"transaccionId": "<50 caracteres hex>",
"numeroSerie": "A001",
"facturas": [
{
"numeroDocumento": "F-0001",
"documentoIdentidadCliente": "J-12345678-9",
"tipoDocumentoCliente": "J",
"nombreCliente": "Cliente Demo, C.A.",
"fechaEmision": "2026-08-29",
"montoGravadoTotal": 100.0,
"tipoCambioBCV": 1.16,
"impuestosSubtotal": [
{ "codigoTotalImp": "G", "alicuotaImp": 16.0, "baseImponibleImp": 100.0, "valorTotalImp": 16.0 }
],
"productos": [
{ "codigoProducto": "P001", "nombreProducto": "Servicio", "cantidad": 1, "precio": 100.0, "tipoImpuesto": "general" }
],
"pagos": [
{ "payment_method_id": 1, "amount": 116.0, "reference": "REF-001", "bank_id": 1 }
]
}
]
}
tipoDocumentoCliente: V, J, E, P, G, C (catálogo 7 SENIAT). codigoTotalImp: G 16% · R 8% · A 31% · E exento · P · X (catálogo 9).
Respuesta exitosa · 200
{
"success": true,
"message": "Facturas guardadas satisfactoriamente.",
"invoice_list_success": [
{ "invoice_number": "FACT-000123", "control_number": "A001-00000045", "invoice_pdf": "https://...", "fiscal_exchange_xml": "<?xml ...?>" }
]
}
nombreProducto no puede contener la palabra "total" aislada, sin importar mayúsculas — es una validación fiscal, no un bug.
credit-notes anula el 100% de una factura. credit-notes/with-products permite devolución parcial por producto.
Campos comunes: serieFacturaAfectada, numeroFacturaAfectada, fechaFacturaAfectada, montoFacturaAfectada, numeroNotaCredito, razonEmision — deben coincidir exactamente con la factura original almacenada, o la API rechaza con 203.
Con productos, además: productos[].codigoProducto, cantidad, precio, tipoImpuesto, nombreProducto — no pueden exceder lo facturado originalmente por ese código.
Número de nota duplicado → HTTP 201 con success: false (mismo patrón que idempotencia, sección 5).
Mismos campos "afectados" que nota de crédito, más productos[] obligatorio con nombreProducto. Incrementa el balance del cliente en la factura afectada.
POST acepta alias en español o inglés por campo (numeroComprobante/voucher_number, rifAgente/provider_doc). porcentajeRetencion solo admite 75 o 100.
Igual que IVA, más tipoPersona (PNR, PJD, PNNR) y codigoConceptoISLR, validados contra el catálogo interno de conceptos ISLR.
Aceptan lotes (guias[] / entregas[]). Cada ítem requiere cliente, productos, vehículo (placa, modelo, color) y empleados (nombre, cedula, rol).
Devuelven 404 si el tenant no tiene bancos/métodos de pago configurados.
Parámetros month (1-12), year (2000-2100). Devuelve un archivo .xlsx binario, no JSON.
Para integraciones que solo necesitan un número de control SENIAT válido, sin que Valhalla genere la factura completa. Body opcional: external_invoice_number, amount.
{ "success": true, "control_number": "00-0001234", "remaining_balance": 87 }
Para integradores sin backend propio: un script de precarga (precarga.js) redirige al usuario al panel Admin con los datos prellenados. Autenticación por headers X-Username / X-Password, no Bearer token.
No emite el documento directamente — crea una "factura pendiente" y devuelve una URL al panel Admin para confirmación manual. Úsenlo solo si no pueden implementar el flujo completo de arriba.
Catálogo de errores
| HTTP | Significado | Qué hacer |
|---|---|---|
| 200 | Procesado correctamente | Persistir el/los PDF y XML devueltos |
201 + success:false | Duplicado — idempotencia | No reintentar; ya fue procesado |
| 203 | Rechazo de regla fiscal | Corregir el payload y reintentar con transaccionId nuevo |
| 400 | Validación o regla de negocio | Revisar message — indica el campo o motivo exacto |
| 401 | Token inválido, ausente o expirado | Re-autenticar con /auth/token |
| 402 | Sin folios / sin suscripción activa | Dirigir al cliente a recargar en su panel Valhalla |
| 403 | Cuota de almacenamiento excedida (solo /external-documents/upload) | — |
| 404 | Catálogo vacío para el tenant | Avisar al equipo Methacortex |
El contrato de errores no es 100% uniforme entre endpoints por razones históricas — la tabla refleja el comportamiento real verificado contra el código, no un ideal. Ante la duda, siempre miren success en el body antes que el status HTTP.
Ejemplo end-to-end
# 1. Autenticar curl -X POST https://valhallaimprenta.com/api/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"userName":"cliente@ejemplo.com","userPassword":"<API_TOKEN>"}' # → { "token": "1|xxxx..." } # 2. Emitir factura curl -X POST https://valhallaimprenta.com/api/v1/invoices \ -H "Authorization: Bearer 1|xxxx..." \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "transaccionId": "...", "numeroSerie": "A001", "facturas": [...] }' # 3. Si responde 402: mostrar al cliente el link a # https://valhallaimprenta.com/app (Billetera) para que recargue folios.
Preguntas frecuentes
No expira por tiempo, pero puede ser revocado manualmente desde el panel. Si reciben 401 en cualquier momento, vuelvan a autenticar.
Ambos usan el mismo token y comparten la misma billetera de folios — cada documento que cualquiera de los dos emita consume del mismo saldo.
El sistema calcula todo en bolívares, con soporte opcional de totalesOtraMoneda + tipoCambioBCV para mostrar el equivalente en moneda extranjera — no lo reemplaza, lo complementa.
Nunca parcial. Todo el lote es una única transacción atómica: o se crean todos los documentos del array, o ninguno.