Insertar facturas
POST /api/v1/facturas/insertar
Es el endpoint central de la integración. Recibe un lote (batch) de facturas, las valida en un orden estricto, las agrupa por manifiesto y las inserta. Una sola llamada puede traer facturas de varios manifiestos distintos.
Solicitud
Sección titulada «Solicitud»| Atributo | Valor |
|---|---|
| Método | POST |
| Ruta | /api/v1/facturas/insertar |
| Autenticación | Requerida (ApiKey) |
| Content-Type | application/json |
| Rate limit | 5 por minuto por IP (ver rate limits) |
El cuerpo es un array JSON de facturas no vacío. Ver el esquema completo de la factura y el esquema de la línea.
- Método POST, URL
https://<dominio-hosana>/api/v1/facturas/insertar - Pestaña Headers:
ApiKeycon tu clave - Pestaña Body → raw → JSON, y pega el array de facturas (ejemplo real):
[ { "Nfactura": "002-001-01-04001001", "NumeroManifiesto": "800002", "FechaFactura": "2026-06-05T00:00:00.000Z", "FechaVencimiento": "2026-06-05T00:00:00.000Z", "Almacen": "OAC", "Vendedorid": "11983", "Vendedor": "WALTER REYNALDO MARADIAGA", "Clienteid": "98065401", "Cliente": "PULPERIA ASLYN", "Rtn": "12181986001440", "Cai": "2F0037-619ACD-2A66E0-63BE03-0909DC-56", "Depto": "LA PAZ", "Municipio": "Santiago de Puringla", "Direccion": "BARRIO LAS BRISAS, SANTIAGO DE PURINGLA, LA PAZ", "Tel": "97689223", "NumeroPedido": "6915760", "NumeroRuta": "230", "NumeroFacturaLX": "4001001", "TipoPago": "CONTADO", "DiasCred": 0, "TipoFactura": "FAC", "EstadoFactura": 1, "ImporteGrabado": 878.29, "ImporteGravado_ISV15": 131.75, "ImporteGravado_Total": 1010.04, "Isv15": 131.75, "Isv18": 0.0, "DescuentosRebajas": 0.0, "Total": 1010.04, "LineasFactura": [ { "Id": 3001001, "InvoiceId": 700101, "NumeroLinea": 1, "ProductoId": "30110205", "ProductoDesc": "ORISOL LIGHT OLIVA 410 mL 1/24 HN", "UniVenta": "UN", "TipoProducto": "A", "CantidadFracciones": 6.0, "CantidadCaja": 0.0, "FactorConversion": 24, "Precio": 748.8, "PrecioUnidadMinVenta": 31.2, "Subtotal": 187.2, "Descuento": 0.0, "Impuesto": 28.08, "PorcentajeImpuesto": 15.0, "Total": 215.28, "Peso": 2.3115 }, { "Id": 3001002, "InvoiceId": 700101, "NumeroLinea": 2, "ProductoId": "81800012", "ProductoDesc": "KETCHUP 8X12X87GR", "UniVenta": "UN", "TipoProducto": "A", "CantidadFracciones": 12.0, "CantidadCaja": 0.0, "FactorConversion": 96, "Precio": 642.11, "PrecioUnidadMinVenta": 6.6883, "Subtotal": 80.26, "Descuento": 0.0, "Impuesto": 12.04, "PorcentajeImpuesto": 15.0, "Total": 92.3, "Peso": 1.195 } ] }]Una factura puede traer todas las líneas que necesite. Ver el catálogo completo de campos en el esquema de la factura.
# El cuerpo (el mismo JSON de la pestaña Postman) se manda desde un archivo.curl -X POST "https://<dominio-hosana>/api/v1/facturas/insertar" \ -H "ApiKey: TU_CLAVE_AQUI" \ -H "Content-Type: application/json" \ -d @manifiesto-800002.json$facturas = [[ 'Nfactura' => '002-001-01-04001001', 'NumeroManifiesto' => '800002', 'FechaFactura' => '2026-06-05T00:00:00.000Z', 'Almacen' => 'OAC', 'Vendedorid' => '11983', 'Vendedor' => 'WALTER REYNALDO MARADIAGA', 'Clienteid' => '98065401', 'Cliente' => 'PULPERIA ASLYN', 'Rtn' => '12181986001440', 'Cai' => '2F0037-619ACD-2A66E0-63BE03-0909DC-56', 'Isv15' => 131.75, 'Total' => 1010.04, 'LineasFactura' => [ [ 'NumeroLinea' => 1, 'ProductoId' => '30110205', 'ProductoDesc' => 'ORISOL LIGHT OLIVA 410 mL 1/24 HN', 'UniVenta' => 'UN', 'CantidadFracciones' => 6.0, 'FactorConversion' => 24, 'Precio' => 748.8, 'Impuesto' => 28.08, 'Total' => 215.28, ], [ 'NumeroLinea' => 2, 'ProductoId' => '81800012', 'ProductoDesc' => 'KETCHUP 8X12X87GR', 'UniVenta' => 'UN', 'CantidadFracciones' => 12.0, 'FactorConversion' => 96, 'Precio' => 642.11, 'Impuesto' => 12.04, 'Total' => 92.30, ], ],]];
$response = $client->post('facturas/insertar', [ 'headers' => ['ApiKey' => 'TU_CLAVE_AQUI'], 'json' => $facturas, 'http_errors' => false, // para leer el cuerpo en respuestas 422]);
$data = json_decode($response->getBody(), true);const facturas = [{ Nfactura: "002-001-01-04001001", NumeroManifiesto: "800002", FechaFactura: "2026-06-05T00:00:00.000Z", Almacen: "OAC", Vendedorid: "11983", Vendedor: "WALTER REYNALDO MARADIAGA", Clienteid: "98065401", Cliente: "PULPERIA ASLYN", Rtn: "12181986001440", Cai: "2F0037-619ACD-2A66E0-63BE03-0909DC-56", Isv15: 131.75, Total: 1010.04, LineasFactura: [ { NumeroLinea: 1, ProductoId: "30110205", ProductoDesc: "ORISOL LIGHT OLIVA 410 mL 1/24 HN", UniVenta: "UN", CantidadFracciones: 6.0, FactorConversion: 24, Precio: 748.8, Impuesto: 28.08, Total: 215.28 }, { NumeroLinea: 2, ProductoId: "81800012", ProductoDesc: "KETCHUP 8X12X87GR", UniVenta: "UN", CantidadFracciones: 12.0, FactorConversion: 96, Precio: 642.11, Impuesto: 12.04, Total: 92.30 }, ],}];
const res = await fetch("https://<dominio-hosana>/api/v1/facturas/insertar", { method: "POST", headers: { "ApiKey": "TU_CLAVE_AQUI", "Content-Type": "application/json" }, body: JSON.stringify(facturas),});const data = await res.json(); // res.status puede ser 200 o 422var facturas = new[] { new { Nfactura = "002-001-01-04001001", NumeroManifiesto = "800002", FechaFactura = "2026-06-05T00:00:00.000Z", Almacen = "OAC", Vendedorid = "11983", Vendedor = "WALTER REYNALDO MARADIAGA", Clienteid = "98065401", Cliente = "PULPERIA ASLYN", Rtn = "12181986001440", Cai = "2F0037-619ACD-2A66E0-63BE03-0909DC-56", Isv15 = 131.75, Total = 1010.04, LineasFactura = new object[] { new { NumeroLinea = 1, ProductoId = "30110205", ProductoDesc = "ORISOL LIGHT OLIVA 410 mL 1/24 HN", UniVenta = "UN", CantidadFracciones = 6.0, FactorConversion = 24, Precio = 748.8, Impuesto = 28.08, Total = 215.28 }, new { NumeroLinea = 2, ProductoId = "81800012", ProductoDesc = "KETCHUP 8X12X87GR", UniVenta = "UN", CantidadFracciones = 12.0, FactorConversion = 96, Precio = 642.11, Impuesto = 12.04, Total = 92.30 } }}};
var res = await http.PostAsJsonAsync("facturas/insertar", facturas);var data = await res.Content.ReadFromJsonAsync<JsonElement>(); // res.StatusCode: 200 o 422Archivos de prueba
Sección titulada «Archivos de prueba»Dos lotes reales para probar el endpoint de inmediato (impórtalos en Postman con Body
→ raw → JSON o úsalos con -d @archivo.json):
- manifiesto-800003-pasa.json — se inserta
correctamente (
200). Trae dos facturas con fechas distintas (2026-06-12y2026-05-20); la mezcla de fechas está permitida y ambas están dentro del rango de 30 días. - manifiesto-800004-rechaza.json — se
rechaza (
422,FECHA_FACTURA_DEMASIADO_ANTIGUA). Una de sus facturas tiene fecha2026-04-01, más de 30 días de antigüedad, y eso rechaza el manifiesto completo.
El detalle de cada caso, con su respuesta exacta, está en casos resueltos.
El pipeline de validación
Sección titulada «El pipeline de validación»El endpoint procesa el batch en un orden estricto. Cada etapa es un filtro: si falla,
detiene el proceso y devuelve un 422. Entender este orden es clave para diagnosticar
rechazos.
flowchart TD
Start([POST /facturas/insertar]) --> Auth{ApiKey válido?}
Auth -- no --> E401[401]
Auth -- sí --> P1{1· body es array no vacío?}
P1 -- no --> E422a[422 body inválido]
P1 -- sí --> P2{2· campos obligatorios?}
P2 -- no --> E422b[422 errores·]
P2 -- sí --> P3{3· fechas V1/V2/V3?}
P3 -- no --> EF[422 FECHAS_INVALIDAS<br/>rechaza TODO el batch]
P3 -- sí --> P4{4· manifiestos de hoy/nuevos?}
P4 -- no --> EM[422 MANIFIESTOS_FECHA_INVALIDA<br/>rechaza TODO el batch]
P4 -- sí --> P5{5· batch duplicado hoy?}
P5 -- sí --> Dup[200 resumen original]
P5 -- no --> P6[6· persistir payload]
P6 --> P7[7· procesar por manifiesto]
P7 --> P8{hubo rechazos?}
P8 -- sí --> R422[422 manifiestos_rechazados·<br/>+ inserta los válidos]
P8 -- no --> OK[200 éxito]
style EF fill:#fde3e3,stroke:#c0392b,color:#7a271a
style EM fill:#fde3e3,stroke:#c0392b,color:#7a271a
style OK fill:#dcf3e3,stroke:#1f8a4c,color:#14532d
style Dup fill:#dcf3e3,stroke:#1f8a4c,color:#14532d
El detalle paso a paso está en la guía del pipeline de inserción. A continuación, las respuestas posibles.
Respuesta exitosa
Sección titulada «Respuesta exitosa»HTTP 200 — todas las facturas se procesaron sin rechazos por manifiesto.
{ "success": true, "message": "Facturas procesadas correctamente.", "batch_uuid": "9f1c2a7e-3b4d-4f1a-8c2e-1a2b3c4d5e6f", "manifiestos": ["800002"], "resumen": { "recibidas": 42, "insertadas": 40, "actualizadas": 0, "sin_cambios": 0, "pendientes_revision": 2, "rechazadas": 0 }}| Campo | Tipo | Descripción |
|---|---|---|
batch_uuid | string | Identificador único del lote procesado. Guárdalo para soporte y trazabilidad. |
manifiestos | array | Números de manifiesto presentes en el batch. |
resumen.recibidas | integer | Total de facturas recibidas en el batch. |
resumen.insertadas | integer | Facturas nuevas creadas. |
resumen.actualizadas | integer | Facturas existentes actualizadas. |
resumen.sin_cambios | integer | Facturas idénticas a las ya existentes (no se tocaron). |
resumen.pendientes_revision | integer | Facturas que llegaron con diferencias respecto a la versión existente y quedaron en revisión manual por Hosana. |
resumen.rechazadas | integer | Facturas rechazadas (0 en una respuesta 200). |
Advertencias y conflictos (pendientes_revision)
Sección titulada «Advertencias y conflictos (pendientes_revision)»Si una factura ya existía en el mismo manifiesto pero llega con campos distintos,
no se sobrescribe automáticamente: se registra un conflicto para revisión manual de
Hosana y se cuenta en pendientes_revision. La respuesta incluye un arreglo
advertencias:
{ "advertencias": [ { "factura": "002-001-01-04001001", "manifiesto": "800002", "campos_con_cambio": ["total", "client_name"], "mensaje": "Factura recibida con diferencias respecto a la versión existente. Pendiente de revisión por Hosana." } ]}Respuesta con rechazos por manifiesto
Sección titulada «Respuesta con rechazos por manifiesto»HTTP 422 — el batch se procesó pero uno o más manifiestos fueron rechazados durante
el procesamiento (paso 7). Las facturas de manifiestos válidos sí se insertan; las
de manifiestos rechazados, no.
{ "success": false, "motivo": "ALMACENES_DESCONOCIDOS", "message": "Uno o más manifiestos fueron rechazados por contener almacenes no registrados en el sistema.", "batch_uuid": "9f1c2a7e-...", "manifiestos": ["800002", "800003"], "resumen": { "recibidas": 240, "insertadas": 118, "actualizadas": 0, "sin_cambios": 0, "pendientes_revision": 0, "rechazadas": 122 }, "manifiestos_rechazados": [ { "manifiesto": "800003", "total_facturas": 122, "motivo": "ALMACENES_DESCONOCIDOS", "almacenes_desconocidos": [ { "almacen": "OAX", "facturas": ["002-001-01-04002050", "002-001-01-04002051"], "cantidad": 2 } ] } ]}El campo motivo a nivel raíz
Sección titulada «El campo motivo a nivel raíz»- Si todos los manifiestos rechazados comparten el mismo motivo,
motivotrae ese código único ymessagees específico. - Si hay motivos distintos,
motivoes"MOTIVOS_MIXTOS"y debes leer el campomotivode cada entrada dentro demanifiestos_rechazados[].
Los tres motivos posibles en esta etapa son ALMACENES_DESCONOCIDOS,
MANIFIESTO_CERRADO y FACTURAS_DUPLICADAS_EN_OTRO_MANIFIESTO. Cada uno tiene una
forma de payload propia, documentada en el catálogo de
errores.
Rechazos tempranos (todo el batch)
Sección titulada «Rechazos tempranos (todo el batch)»A diferencia del caso anterior, estos rechazos ocurren antes de insertar nada: ninguna factura entra. Son las validaciones de los pasos 3 y 4.
Fechas inválidas (paso 3)
Sección titulada «Fechas inválidas (paso 3)»HTTP 422 con motivo: "FECHAS_INVALIDAS". Las validaciones de fecha son por
factura y atómicas por manifiesto: una sola factura futura o demasiado antigua
rechaza el manifiesto completo. Un manifiesto puede mezclar fechas (eso ya no se rechaza
por defecto).
Este ejemplo corresponde al archivo de prueba manifiesto-800004-rechaza.json: trae una
factura de hace más de 30 días.
{ "success": false, "motivo": "FECHAS_INVALIDAS", "message": "Batch rechazado por errores de fecha en uno o más manifiestos.", "accion_requerida": "Revise los manifiestos rechazados. Ninguna FechaFactura puede ser futura ni superar el rango configurado de antigüedad; una sola factura inválida rechaza el manifiesto completo.", "resumen": { "total_recibidas": 2, "total_rechazadas": 2, "total_validas": 0, "insertadas": 0 }, "manifiestos_rechazados": [ { "manifiesto": "800004", "motivo": "FECHA_FACTURA_DEMASIADO_ANTIGUA", "detalle": { "hoy_servidor": "2026-06-13", "limite_dias": 30, "facturas_antiguas": { "002-001-01-04004002": { "fecha": "2026-04-01", "dias": 73 } }, "instruccion": "Una o más facturas superan el límite de 30 días de antigüedad. El manifiesto se rechaza completo; corrija el origen o cargue desde el panel administrativo." }, "total_facturas": 2, "facturas": ["002-001-01-04004001", "002-001-01-04004002"] } ], "manifiestos_validos": []}Cada manifiesto rechazado trae su propio motivo y un detalle con la lista de
facturas afectadas:
| Sub-motivo | Campo en detalle | Forma |
|---|---|---|
FECHA_FACTURA_FUTURA | facturas_futuras | { "Nfactura": "YYYY-MM-DD" } |
FECHA_FACTURA_DEMASIADO_ANTIGUA | facturas_antiguas | { "Nfactura": { "fecha": "...", "dias": N } } |
FECHA_FACTURA_INVALIDA | facturas_afectadas | ["Nfactura", ...] |
FECHAS_MEZCLADAS (solo si Hosana activa el modo estricto) | fechas_encontradas, facturas_por_fecha | listas por fecha |
Ver el detalle de cada regla en reglas de fecha.
Manifiestos de días anteriores (paso 4)
Sección titulada «Manifiestos de días anteriores (paso 4)»HTTP 422 con motivo: "MANIFIESTOS_FECHA_INVALIDA". Ocurre cuando se intenta agregar
facturas a un manifiesto que ya existe en Hosana y fue creado en un día anterior:
{ "success": false, "motivo": "MANIFIESTOS_FECHA_INVALIDA", "message": "Batch rechazado completamente. Contiene facturas de manifiestos creados en días anteriores que ya no aceptan nuevas facturas.", "accion_requerida": "Corrija el batch separando los manifiestos afectados. Los manifiestos válidos deben reenviarse en un batch independiente sin mezclar con los del día anterior.", "resumen": { "total_recibidas": 240, "total_rechazadas": 122, "total_validas": 118, "insertadas": 0 }, "manifiestos_rechazados": [ { "manifiesto": "800003", "fecha_original": "2026-06-05", "fecha_intento": "2026-06-06", "total_facturas": 122, "facturas_afectadas": ["002-001-01-04002050", "002-001-01-04002051"], "instruccion": "El manifiesto #800003 fue creado el 2026-06-05 y ya no acepta facturas nuevas. Reenvíe estas facturas en un nuevo número de manifiesto." } ], "manifiestos_no_afectados": [ { "manifiesto": "800002", "total_facturas": 118, "facturas": ["002-001-01-04001001", "..."], "nota": "Este manifiesto es válido pero fue rechazado por venir en el mismo batch que manifiestos de días anteriores. Reenvíelo en un batch independiente." } ]}Batch duplicado
Sección titulada «Batch duplicado»HTTP 200 con success: true. Si reenvías un batch idéntico (mismo contenido exacto)
el mismo día, Hosana lo detecta por hash y no reprocesa — devuelve el resumen del
proceso original:
{ "success": true, "message": "Este batch ya fue procesado anteriormente el día de hoy.", "batch_uuid": "9f1c2a7e-...", "resumen": { "recibidas": 240, "insertadas": 235, "actualizadas": 0, "sin_cambios": 3, "pendientes_revision": 2, "rechazadas": 0 }}Esto hace seguro reintentar ante un timeout sin riesgo de duplicar facturas.
Otros errores
Sección titulada «Otros errores»Error de validación de estructura
Sección titulada «Error de validación de estructura»HTTP 422 con un arreglo errores:
{ "success": false, "message": "El payload contiene errores de validación.", "errores": [ "Factura #3: falta el campo obligatorio 'Almacen'.", "Factura 002-001-01-04001099: 'LineasFactura' no puede estar vacío." ]}Body inválido
Sección titulada «Body inválido»HTTP 422:
{ "success": false, "message": "El body debe ser un array JSON de facturas no vacío." }Error interno
Sección titulada «Error interno»HTTP 500. Hosana notifica automáticamente a su equipo técnico. El batch_uuid
permite rastrear el caso:
{ "success": false, "message": "Error interno al procesar las facturas. El equipo técnico ha sido notificado.", "batch_uuid": "9f1c2a7e-..."}Resumen de respuestas
Sección titulada «Resumen de respuestas»| HTTP | success | Situación |
|---|---|---|
200 | true | Procesado sin rechazos, o batch duplicado ya procesado. |
422 | false | Body inválido, estructura inválida, fechas inválidas, manifiestos viejos, o rechazos por manifiesto. |
401 | false | ApiKey ausente o inválido. |
429 | — | Rate limit superado (5/min). |
500 | false | Error interno en Hosana. |
Los casos resueltos recogen escenarios completos (batch mixto, almacén inválido, fechas mezcladas) con el request y el response exactos.