Envío Masivo de Documentos (Bulk)
Disponible desde v2.10.0 · Base path: /api/ubl2.1/bulk/documents · Referencia rápida de endpoints: 📦 Envío Masivo (Bulk)
¿Ya integrabas el envío masivo? Revisa Cambios y compatibilidad 🆕.
El envío masivo de documentos (Bulk) se encuentra actualmente en Fase BETA. Aunque es completamente funcional, esto conlleva que los límites de rate limit, las estructuras de respuesta asíncrona, políticas de retención o las ventanas temporales de procesamiento podrían recibir ajustes técnicos orientados a estabilizar la infraestructura.
Te sugerimos implementar primero tu flujo de integración apoyándote en el ambiente Sandbox antes de liberar a usuarios finales en producción.
Los lotes y sus items son temporales. Un lote terminado (COMPLETED, PARTIAL o FAILED) se elimina por completo a los 7 días de su creación, sin posibilidad de recuperación. Ver Retención de datos.
Cada item que llegó a emitirse retorna un document_uuid. Extrae y guarda este UUID en tu sistema mientras el lote se procesa o apenas termine. Si el lote ya se purgó y no guardaste el UUID, aún puedes consultar el documento con el endpoint de búsqueda usando el número y el prefijo.
El endpoint de envío masivo permite procesar múltiples documentos electrónicos en un solo request HTTP. Los documentos se encolan para procesamiento asíncrono y puedes consultar el estado de cada uno individualmente.
Flujo General
- 🖥️ Tu sistema envía
POST /bulk/documentscon N documentos. - ⚡ La API valida el request y la cuota, registra el lote y sus items y responde
201 Createdcon elbatch_id. - 📦 Cada documento se procesa por separado: se envía a la 🏛️ DIAN en UBL 2.1 y su item se actualiza con la respuesta.
- Cuando todos los items terminan, el lote se cierra y se dispara el webhook
bulk.batch.completed. - Consultas
GET /bulk/documents/{batch_id}para ver el estado completo del lote.
Autenticación
Todos los endpoints requieren autenticación vía Bearer Token (Personal Access Token):
Authorization: Bearer {tu-token}
Accept: application/json
- Casas de Software: agrega
?client_uuid={{client_uuid}}para crear y consultar lotes en nombre de una empresa cliente. El lote queda asociado a esa empresa y solo se consulta con el mismoclient_uuid. Accept: application/json: envíalo siempre. Sin este header, algunos errores (validación, límites) pueden llegar como redirección302o como página HTML en lugar de JSON.
Todo el grupo /bulk exige una membresía con cupo, no solo el POST. Si la empresa agotó su cuota o su membresía está inactiva, también GET /bulk/documents, GET /bulk/documents/{batch_id} y GET /bulk/documents/{batch_id}/items responden 402:
{
"success": false,
"message": "Límite de documentos de la membresía alcanzado o membresía inactiva."
}
Si el client_uuid no corresponde a una empresa cliente activa vinculada a tu cuenta, la petición se rechaza antes de llegar al lote (hoy con un 500 y {"success": false, "message": "Error interno al verificar la membresía."}). Revisa el client_uuid antes de reintentar.
Por eso conviene guardar el document_uuid de cada item mientras el lote se procesa. Si te quedas sin cupo, consulta tus documentos por GET /documents o GET /status (Consultas y Estados).
Límites por Plan
| Plan | Docs por lote | Lotes por minuto | Descripción |
|---|---|---|---|
| FREE | 5 | 1 | Plan gratuito con límite diario |
| Pago | 50 | 10 | Suscripción con cuota finita |
| Ilimitado | 200 | 60 | Plan Servidor / Enterprise |
- Tope técnico: ningún lote puede superar 200 documentos, sea cual sea el plan.
- Docs por lote: si el lote supera el tope de tu plan, la API responde
413antes de validar el resto del body. - Lotes por minuto: el límite se cuenta por usuario y por empresa: varios usuarios de la misma empresa comparten el cupo.
- Cuota: el lote completo debe caber en la cuota disponible de la suscripción, descontando los documentos que aún estén pendientes de lotes anteriores. Si no cabe, responde
402y no se encola ningún documento. - Documentos en proceso: una empresa puede tener como máximo 1000 documentos en proceso (en cola o procesándose) a la vez. Por encima responde
429conRetry-After.
Ver Códigos de Error HTTP para el formato de cada respuesta.
Tipos de Documento Soportados
Valor (kind) | Descripción | Endpoint unitario equivalente (auto-increment) | Endpoint unitario equivalente (manual) |
|---|---|---|---|
invoice | Factura electrónica | POST /auto-increment/invoices | POST /invoice |
credit_note | Nota crédito | POST /auto-increment/credit-notes | POST /notes/credit |
debit_note | Nota débito | POST /auto-increment/debit-notes | POST /notes/debit |
support_document | Documento soporte | POST /auto-increment/support-documents | POST /ds/document |
adjustment_note | Nota de ajuste | POST /auto-increment/adjustment-notes | POST /ds/adjustment-note |
pos_document | Documento POS | POST /auto-increment/pos-documents | POST /invoice |
Compatibilidad de payload
El payload de cada documento usa el mismo formato JSON que el body del endpoint unitario equivalente. Si ya tienes integración con el endpoint unitario, puedes reutilizar el mismo JSON. Hay diferencias importantes:
- El payload no pasa por las validaciones del endpoint unitario. Al admitir el lote solo se verifica que
payloadsea un objeto JSON. Un dato inválido no produce un422conerrors: el lote se acepta y ese item terminaFAILEDcon el error que devuelva el procesamiento (ver Catálogo deerror_code). Los errores internos (5xx) se reintentan antes de quedarFAILED. - Valida tu JSON antes de enviarlo masivamente, por ejemplo probándolo una vez en el endpoint unitario o en el Sandbox.
- La empresa emisora es siempre la del lote (la del token y el
client_uuiddelPOST /bulk/documents). Unclient_uuidocompanyIddentro delpayloadse descarta. payload.uuidse reemplaza por el UUID anti-duplicados del item (verdocuments[].uuid).
Endpoints
1. Crear Lote
POST /api/ubl2.1/bulk/documents
Acepta un lote de documentos para procesamiento asíncrono. Retorna inmediatamente con 201 Created.
Headers
| Header | Tipo | Requerido | Descripción |
|---|---|---|---|
Authorization | string | ✅ | Bearer {token} |
Content-Type | string | ✅ | application/json |
Accept | string | Recomendado | application/json, para recibir todos los errores en JSON. |
Idempotency-Key | UUID v4 | ❌ | Clave de idempotencia (máx. 36 caracteres, válida 24 h). Ver Idempotencia. |
Query Parameters
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
client_uuid | UUID | ❌ | Empresa cliente en cuyo nombre se emite el lote (Casas de Software). |
Request Body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
mode | string | ✅ | auto-increment o manual. Ver Modos de envío. |
stop_on_error | boolean | ❌ | Si true, el primer documento que no termine en SUCCESS cancela los items que siguen en cola. Default: false. |
default_resolution_number | string | ❌ | Número de resolución por defecto (máx. 50). Se copia en payload.resolution_number de los documentos que no lo traigan. |
default_prefix | string | ❌ | Prefijo por defecto (máx. 10). Se copia en payload.prefix de los documentos que no lo traigan. |
documents | array | ✅ | Documentos del lote. Mínimo 1; máximo el tope de tu plan y nunca más de 200. |
documents[].kind | string | ✅ | Tipo de documento. Ver tipos soportados. |
documents[].payload | object | ✅ | Body del endpoint unitario equivalente. Ver Compatibilidad de payload. |
documents[].uuid | UUID | ❌ | UUID anti-duplicados del documento. Debe ser válido y distinto dentro del lote. Ver UUID anti-duplicados. |
documents[].client_reference | string | ❌ | Tu referencia interna para rastrear el documento (máx. 64 caracteres). |
documents[].attachments | array | ❌ | Adjuntos del documento (máx. 4). Se copian en payload.attachments solo si el payload no trae adjuntos propios: si payload.attachments existe y no está vacío, estos se ignoran. Ver Adjuntos. |
documents[].attachments[].filename | string | ✅ (por adjunto) | Nombre del archivo (máx. 255). |
documents[].attachments[].content | string | ✅ (por adjunto) | Contenido del archivo en base64. |
documents[].attachments[].content_type | string | ❌ | Tipo MIME (máx. 100). |
Adjuntos
- Los adjuntos de
documents[].attachmentsviajan dentro delpayload(payload.attachments) y se guardan y vinculan al documento igual que en el endpoint unitario, con la empresa del lote. - Si el
payloadya traeattachmentscon contenido, manda el delpayloadydocuments[].attachmentsse ignora. Envía los adjuntos en uno solo de los dos lugares. - Si el documento se emite pero sus adjuntos no quedan vinculados, el item lleva el warning
ATTACHMENTS_NOT_LINKED(ver El camporesponse).
kind + payloadLos campos del documento (type_document_id, customer, lines…) van dentro de payload, nunca directamente en documents[]. Un documento sin kind o sin payload hace que todo el lote se rechace con 422 (Cada documento debe especificar su tipo (kind). / Cada documento debe incluir su payload.).
UUID anti-duplicados
Cada item se emite con un UUID que impide emitir dos veces el mismo documento:
documents[].uuid, si lo envías.- Si no,
payload.uuid, solo si es un UUID válido (si no lo es, se ignora). - Si tampoco, el
item_iddel propio item.
documents[].uuiddebe ser un UUID válido y no repetirse dentro del lote (sin distinguir mayúsculas). Si no se cumple, todo el lote se rechaza con422(El UUID del documento debe ser un UUID v4 válido./El UUID del documento está repetido dentro del lote.).- El UUID se registra antes de transmitir. Si la empresa ya tiene otro documento con ese UUID, el item no se emite y termina
FAILEDconerror_codeDUPLICATE_DOCUMENT_UUIDy el mensajeEl uuid … ya se usó en el documento …. No se emitió de nuevo.. Sudocument_uuidapunta al documento existente. - Si el item falla antes de transmitirse, el UUID queda libre y puedes reenviar el documento con el mismo UUID.
Ejemplos de Request
Los payloads de estos ejemplos están abreviados: usa el JSON completo del endpoint unitario (ver, por ejemplo, Factura con varios impuestos).
- Auto-increment
- Tipos mixtos
{
"mode": "auto-increment",
"stop_on_error": false,
"default_resolution_number": "18764002566734",
"default_prefix": "SETT",
"documents": [
{
"kind": "invoice",
"uuid": "6f1c2b9e-3d4a-4e8b-9c71-2a5d8e0f4b13",
"client_reference": "FACT-001",
"payload": {
"type_document_id": 7,
"operation_type_id": 1,
"send_email": 0,
"payments": [
{
"payment_method_id": 1,
"means_payment_id": 10,
"value_paid": "1190.00"
}
],
"customer": {
"dni": "1234567890",
"company_name": "CLIENTE DE PRUEBA",
"email": "facturacion@ejemplo.com"
},
"lines": [
{
"invoiced_quantity": "1",
"quantity_units_id": "70",
"line_extension_amount": "1000.00",
"free_of_charge_indicator": false,
"description": "Servicio de consultoría",
"code": "SRV-001",
"price_amount": "1000.00",
"base_quantity": "1",
"tax_totals": [
{
"tax_id": "1",
"tax_amount": 190.00,
"taxable_amount": 1000.00,
"percent": 19
}
]
}
],
"legal_monetary_totals": {
"line_extension_amount": "1000.00",
"tax_exclusive_amount": "1000.00",
"tax_inclusive_amount": "1190.00",
"payable_amount": "1190.00"
}
}
},
{
"kind": "invoice",
"client_reference": "FACT-002",
"payload": {
"type_document_id": 7,
"...": "resto del payload, igual que en el endpoint unitario"
}
}
]
}
{
"mode": "auto-increment",
"stop_on_error": true,
"documents": [
{
"kind": "invoice",
"client_reference": "FACT-100",
"payload": {
"type_document_id": 7,
"resolution_number": "18764002566734",
"prefix": "SETT",
"customer": { "...": "payload completo de factura" },
"lines": ["..."]
}
},
{
"kind": "credit_note",
"client_reference": "NC-100",
"payload": {
"type_document_id": 5,
"resolution_number": "18764002566735",
"prefix": "NC",
"billing_reference": {
"number": "SETT990000001",
"date": "2026-06-01",
"uuid": "CUFE-de-la-factura-referenciada"
},
"customer": { "...": "payload completo de nota crédito" },
"lines": ["..."]
}
},
{
"kind": "support_document",
"client_reference": "DS-100",
"payload": {
"type_document_id": 11,
"resolution_number": "18764002566999",
"prefix": "SEDS",
"customer": { "...": "payload completo de documento soporte" },
"lines": ["..."]
}
}
]
}
Response 201 Created
{
"message": "Lote aceptado para procesamiento asíncrono.",
"batch_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "PENDING",
"status_url": "/api/ubl2.1/bulk/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479",
"received": 2,
"accepted": 2,
"rejected": 0,
"estimated_completion_seconds": 12,
"items": [
{
"index": 0,
"client_reference": "FACT-001",
"item_id": "c1d2e3f4-a1b2-c3d4-e5f6-a7b8c9d0e1f2",
"status": "QUEUED"
},
{
"index": 1,
"client_reference": "FACT-002",
"item_id": "d4e5f6a7-b8c9-d0e1-f2a3-b4c5d6e7f8a9",
"status": "QUEUED"
}
],
"success": true
}
| Campo | Descripción |
|---|---|
batch_id | UUID del lote. Úsalo para consultar su estado. |
status | Estado del lote (PENDING al crearlo). En un replay idempotente, el estado actual del lote original. |
status_url | Ruta relativa para consultar el lote. |
received / accepted / rejected | Documentos recibidos y aceptados. El lote se acepta completo o se rechaza completo, así que rejected siempre es 0. |
estimated_completion_seconds | Estimación de segundos hasta que termine el lote, según la cola actual. 0 si el lote ya terminó (replay). |
items[] | Un registro por documento con su index (posición en documents[], desde 0), client_reference, item_id y status. |
2. Consultar Lote
GET /api/ubl2.1/bulk/documents/{batch_id}
Retorna el estado completo del lote con todos sus items, ordenados por index.
Parámetros de ruta
| Parámetro | Tipo | Descripción |
|---|---|---|
batch_id | UUID | Identificador del lote (retornado en el POST) |
Response 200 OK
{
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"company_id": 1,
"user_id": 5,
"mode": "auto-increment",
"stop_on_error": false,
"total_items": 2,
"succeeded": 1,
"failed": 1,
"status": "PARTIAL",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
"finished_at": "2026-06-01T19:31:00.000000Z",
"created_at": "2026-06-01T19:30:00.000000Z",
"updated_at": "2026-06-01T19:31:00.000000Z",
"batch_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status_description": "Completado parcialmente",
"received": 2,
"items": [
{
"uuid": "c1d2e3f4-a1b2-c3d4-e5f6-a7b8c9d0e1f2",
"index": 0,
"kind": "invoice",
"client_reference": "FACT-001",
"document_uuid": "3e7a9c51-8b2d-4f6e-a1c4-9d0b2e5f7a38",
"shipping_history_id": 4200027,
"status": "SUCCESS",
"attempts": 1,
"error_code": null,
"error_message": null,
"created_at": "2026-06-01T19:30:00.000000Z",
"updated_at": "2026-06-01T19:30:05.000000Z",
"processed_at": "2026-06-01T19:30:05.000000Z",
"response": "{\"uuid\":\"3e7a9c51-8b2d-4f6e-a1c4-9d0b2e5f7a38\",\"document_number\":\"SETT990000001\",\"XmlDocumentKey\":\"cf9864294501e8a9578235dd2ab3c4fd\",\"cufe\":\"cf9864294501e8a9578235dd2ab3c4fd\",\"track_id\":null,\"is_valid\":true,\"status_code\":200,\"dian_status_code\":\"00\",\"dian_status\":\"ACCEPTED\",\"errors\":[],\"notifications\":[],\"warnings\":[],\"http_status\":200}",
"item_id": "c1d2e3f4-a1b2-c3d4-e5f6-a7b8c9d0e1f2",
"kind_description": "Factura electrónica",
"status_description": "Exitoso",
"document_number": "SETT990000001",
"cufe": "cf9864294501e8a9578235dd2ab3c4fd",
"track_id": null,
"dian_response": {
"is_valid": true,
"status_code": "00",
"status": "ACCEPTED",
"errors": [],
"notifications": []
},
"warnings": []
},
{
"uuid": "d4e5f6a7-b8c9-d0e1-f2a3-b4c5d6e7f8a9",
"index": 1,
"kind": "invoice",
"client_reference": "FACT-002",
"document_uuid": null,
"shipping_history_id": null,
"status": "FAILED",
"attempts": 1,
"error_code": "404",
"error_message": "No se encontró una resolución de facturación activa (resolution_number: 18764002566734, prefix: SETX).",
"created_at": "2026-06-01T19:30:00.000000Z",
"updated_at": "2026-06-01T19:30:08.000000Z",
"processed_at": "2026-06-01T19:30:08.000000Z",
"response": null,
"item_id": "d4e5f6a7-b8c9-d0e1-f2a3-b4c5d6e7f8a9",
"kind_description": "Factura electrónica",
"status_description": "Fallido",
"error": {
"code": "404",
"message": "No se encontró una resolución de facturación activa (resolution_number: 18764002566734, prefix: SETX)."
}
}
],
"success": true
}
Ver Campos de cada item para el significado de cada campo según el estado.
Si el batch_id no existe, ya se purgó o pertenece a otra empresa, la API responde 404 con un único campo message (sin success). Sin el header Accept: application/json la respuesta puede ser una página HTML.
3. Listar Items del Lote
GET /api/ubl2.1/bulk/documents/{batch_id}/items
Lista los items de un lote, ordenados por index, con filtros opcionales y paginación estándar (current_page, data, per_page, total, links…). Cada elemento de data tiene los mismos campos que items[] en Consultar Lote.
Query Parameters
| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
status | string | — | Filtrar: QUEUED, PROCESSING, SUCCESS, FAILED, REJECTED_BY_DIAN, POSSIBLY_SENT, CANCELLED |
kind | string | — | Filtrar: invoice, credit_note, debit_note, support_document, adjustment_note, pos_document |
per_page | integer | 50 | Items por página (de 1 a 200) |
page | integer | 1 | Número de página |
Response 200 OK
{
"current_page": 1,
"data": [
{
"uuid": "e5f6a7b8-c9d0-e1f2-a3b4-c5d6e7f8a9b0",
"index": 2,
"kind": "invoice",
"client_reference": "FACT-003",
"document_uuid": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
"shipping_history_id": 4200029,
"status": "POSSIBLY_SENT",
"attempts": 1,
"error_code": "DOCUMENT_POSSIBLY_SENT",
"error_message": "La respuesta no trae un resultado DIAN reconocible. Consulte el estado del envío antes de reenviar.",
"created_at": "2026-06-01T19:30:00.000000Z",
"updated_at": "2026-06-01T19:30:40.000000Z",
"processed_at": "2026-06-01T19:30:40.000000Z",
"response": "{\"uuid\":\"8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d\",\"document_number\":\"SETT990000003\",\"XmlDocumentKey\":null,\"cufe\":null,\"track_id\":null,\"is_valid\":null,\"status_code\":200,\"dian_status_code\":null,\"dian_status\":\"POSSIBLY_SENT\",\"errors\":[],\"notifications\":[],\"warnings\":[],\"http_status\":200}",
"item_id": "e5f6a7b8-c9d0-e1f2-a3b4-c5d6e7f8a9b0",
"kind_description": "Factura electrónica",
"status_description": "Sin respuesta definitiva de la DIAN",
"document_number": "SETT990000003",
"cufe": null,
"track_id": null,
"dian_response": {
"is_valid": null,
"status_code": null,
"status": "POSSIBLY_SENT",
"errors": [],
"notifications": []
},
"warnings": [],
"error": {
"code": "DOCUMENT_POSSIBLY_SENT",
"message": "El documento pudo llegar a la DIAN sin respuesta definitiva. Consulte su estado con el document_uuid antes de cualquier acción; no lo reenvíe."
},
"next_action": "CHECK_STATUS"
}
],
"first_page_url": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479/items?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479/items?page=1",
"links": [
{ "url": null, "label": "« Previous", "active": false },
{ "url": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479/items?page=1", "label": "1", "active": true },
{ "url": null, "label": "Next »", "active": false }
],
"next_page_url": null,
"path": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479/items",
"per_page": 50,
"prev_page_url": null,
"to": 1,
"total": 1
}
4. Listar Lotes
GET /api/ubl2.1/bulk/documents
Lista los lotes de la compañía, ordenados del más reciente al más antiguo, con paginación estándar. Cada elemento de data trae los datos del lote sin batch_id, status_description ni items (el identificador del lote es uuid).
Query Parameters
| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
status | string | — | Filtrar: PENDING, PROCESSING, COMPLETED, PARTIAL, FAILED |
per_page | integer | 15 | Lotes por página (de 1 a 100) |
page | integer | 1 | Número de página |
Response 200 OK
{
"current_page": 1,
"data": [
{
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"company_id": 1,
"user_id": 5,
"mode": "auto-increment",
"stop_on_error": false,
"total_items": 10,
"succeeded": 10,
"failed": 0,
"status": "COMPLETED",
"idempotency_key": null,
"finished_at": "2026-06-01T19:31:00.000000Z",
"created_at": "2026-06-01T19:30:00.000000Z",
"updated_at": "2026-06-01T19:31:00.000000Z"
},
{
"uuid": "44115613-e378-45e1-a6ea-b93f0319de2a",
"company_id": 1,
"user_id": 5,
"mode": "manual",
"stop_on_error": false,
"total_items": 2,
"succeeded": 0,
"failed": 2,
"status": "FAILED",
"idempotency_key": null,
"finished_at": "2026-06-01T21:04:45.000000Z",
"created_at": "2026-06-01T21:03:38.000000Z",
"updated_at": "2026-06-01T21:04:45.000000Z"
}
],
"first_page_url": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents?page=1",
"links": [
{ "url": null, "label": "« Previous", "active": false },
{ "url": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents?page=1", "label": "1", "active": true },
{ "url": null, "label": "Next »", "active": false }
],
"next_page_url": null,
"path": "https://sandbox-api.matias-api.com/api/ubl2.1/bulk/documents",
"per_page": 15,
"prev_page_url": null,
"to": 2,
"total": 2
}
Campos de cada item
Campos presentes en todos los items:
| Campo | Descripción |
|---|---|
uuid / item_id | Identificador del item (mismo valor). |
index | Posición del documento en documents[] (desde 0). |
kind / kind_description | Tipo de documento y su descripción legible. |
client_reference | Tu referencia interna. |
document_uuid | UUID del envío del documento: guárdalo. Es el que usas para consultarlo (GET /documents/{uuid}) y, en general, no coincide con el documents[].uuid que enviaste. En un item que no llegó a emitirse es el documents[].uuid que enviaste, o null; en DUPLICATE_DOCUMENT_UUID, el del documento que ya existía. |
shipping_history_id | Identificador numérico interno del envío. Para consultar el documento usa document_uuid. |
status / status_description | Estado del item y su descripción legible. Ver Estados del Item. |
attempts | Intentos registrados (1 normalmente; hasta 3 con fallos transitorios). |
error_code / error_message | Código y mensaje del error (mensaje truncado a 500 caracteres). Ver Catálogo de error_code. |
processed_at, created_at, updated_at | Fechas en ISO 8601 (UTC). |
response | String con un JSON de resumen del resultado, o null (ver abajo). |
Campos adicionales según el estado:
| Estado | Campos adicionales |
|---|---|
SUCCESS, REJECTED_BY_DIAN, POSSIBLY_SENT | document_number, cufe (CUFE/CUDE), track_id, dian_response (is_valid, status_code, status, errors, notifications, o null) y warnings. |
FAILED, REJECTED_BY_DIAN | error: { "code": …, "message": … } con los mismos valores de error_code y error_message. |
POSSIBLY_SENT | error con code (el error_code, o POSSIBLY_SENT) y un mensaje fijo que indica no reenviar, y next_action: "CHECK_STATUS". |
El campo response
response ya no trae el contenido crudo del documento (XML, AttachedDocument, QR o PDF en base64). Es un string JSON con este resumen:
| Clave | Descripción |
|---|---|
uuid | UUID del envío del documento (el mismo document_uuid del item). |
document_number | Número del documento (prefijo + consecutivo). |
XmlDocumentKey / cufe | CUFE/CUDE del documento. |
track_id | Identificador de seguimiento de la DIAN (en habilitación, el ZipKey). |
is_valid | true si la DIAN lo aceptó, false si lo rechazó, null si no hay resultado definitivo (también en habilitación, donde solo se confirma la recepción). |
status_code / http_status | Código HTTP entero de la emisión (por ejemplo, 200). |
dian_status_code | StatusCode de la DIAN (por ejemplo, "00" o "99"). |
dian_status | ACCEPTED, RECEIVED, REJECTED o POSSIBLY_SENT. |
errors / notifications | Rechazos y notificaciones de la DIAN, por separado: un mensaje de la DIAN que contiene «Notificaci» va a notifications; los demás, a errors. En un FAILED por error HTTP, errors trae el mensaje del error. |
warnings | Avisos del procesamiento (lista de { "code", "message" }). |
Si el item termina FAILED por un error 4xx de la emisión (salvo 408 y 429), response no es null: trae el resumen con errors: ["<mensaje del error>"], status_code y http_status iguales a ese código HTTP y dian_status: null.
"{\"uuid\":null,\"document_number\":null,\"XmlDocumentKey\":null,\"cufe\":null,\"track_id\":null,\"is_valid\":null,\"status_code\":422,\"dian_status_code\":null,\"dian_status\":null,\"errors\":[\"Mensaje del error devuelto por la emisión.\"],\"notifications\":[],\"warnings\":[],\"http_status\":422}"
response es null si el item no se procesó, si el fallo ocurrió fuera de la emisión (por ejemplo, una resolución inexistente, JOB_FAILED o STUCK), si un error transitorio (408, 429, 5xx) agotó los 3 intentos, o si es un registro antiguo de gran tamaño. En esos casos el detalle está en error_code / error_message. El payload que enviaste nunca se devuelve. Para descargar XML o PDF usa los endpoints del documento con su document_uuid o CUFE (Descarga de Archivos).
Códigos posibles en warnings:
| Código | Significado |
|---|---|
RECONCILED | El item se concilió con un envío ya existente de ese mismo documento (no se volvió a emitir). |
ATTACHMENTS_NOT_LINKED | El documento se emitió pero los adjuntos no quedaron vinculados. |
PDF_NOT_GENERATED | El documento se emitió pero no se generó el PDF. |
EMAIL_NOT_SENT | El documento se emitió pero no se envió el correo. |
ATTACHED_DOCUMENT_NOT_GENERATED | El documento se emitió pero no se generó el AttachedDocument. |
ALREADY_AUTHORIZED_RECOVERED / ALREADY_AUTHORIZED_OTHER_CONTENT | La DIAN ya tenía autorizado ese número; el documento se recuperó o la DIAN tiene autorizado un contenido distinto. |
Referencia de Estados
Estados del Lote (status)
| Estado | Emoji | Descripción |
|---|---|---|
PENDING | ⏳ | Aceptado; ningún item ha terminado todavía. |
PROCESSING | ⚙️ | Al menos un item terminó y quedan items pendientes. |
COMPLETED | ✅ | Todos los items terminaron en SUCCESS. |
PARTIAL | ⚠️ | Todos terminaron; al menos uno SUCCESS y al menos uno FAILED, REJECTED_BY_DIAN, POSSIBLY_SENT o CANCELLED. |
FAILED | ❌ | Todos terminaron y ninguno quedó en SUCCESS. |
- Un lote nunca queda en
CANCELLED: constop_on_errortermina enPARTIALoFAILED, y son sus items en cola los que quedanCANCELLED. - Contadores:
succeeded= itemsSUCCESS;failed= itemsFAILED+REJECTED_BY_DIAN+POSSIBLY_SENT. LosCANCELLEDno suman en ninguno. - Un lote
FAILEDoPARTIALpuede tener documentos que sí llegaron a la DIAN (REJECTED_BY_DIAN,POSSIBLY_SENT): revisa cada item.
Estados del Item (items[].status)
| Estado | Emoji | Descripción |
|---|---|---|
QUEUED | 📋 | En cola. También mientras espera un reintento (attempts mayor que 0, con el error del último intento). |
PROCESSING | ⚙️ | Siendo procesado. |
SUCCESS | ✅ | La DIAN lo aceptó (en habilitación: lo recibió). |
FAILED | ❌ | Falló antes de transmitirse a la DIAN. |
REJECTED_BY_DIAN | 🏛️❌ | Transmitido y rechazado por la DIAN. |
POSSIBLY_SENT 🆕 | ❓ | La transmisión empezó y no hubo respuesta definitiva de la DIAN. No lo reenvíes: consulta su estado. |
CANCELLED | 🚫 | Cancelado por stop_on_error antes de procesarse. |
POSSIBLY_SENT: consultar, nunca reenviarEl documento pudo quedar autorizado en la DIAN. Reenviarlo con otro número o con otro contenido puede generar un duplicado. Consulta su estado con el document_uuid (GET /documents/{document_uuid} o GET /status, ver Consultas y Estados) y decide según el resultado. POSSIBLY_SENT cuenta en failed del lote y del webhook y dispara stop_on_error.
- Si el item trae
cufe(XmlDocumentKey), consulta el estado directamente en la DIAN conPOST /status/document/{CUFE}. Si ese número se está transmitiendo en otra solicitud, el endpoint responde409concodeDOCUMENT_IN_TRANSMISSION: consulta de nuevo en unos instantes. - Si
document_uuidlleganull, consulta con el UUID anti-duplicados del item (ver Consultar el documento cuandodocument_uuidesnull).
Cómo se decide el resultado
El resultado lo decide la respuesta de la DIAN, no el código HTTP: un rechazo de la DIAN llega con HTTP 200.
| Respuesta | Estado del item | dian_response.status |
|---|---|---|
IsValid = true y StatusCode = 00 | SUCCESS | ACCEPTED |
Habilitación: sin IsValid, con ZipKey | SUCCESS | RECEIVED (solo recibido; la validación es asíncrona: consulta POST /status/zip/{track_id}) |
IsValid presente y no válido | REJECTED_BY_DIAN | REJECTED (error_code = StatusCode DIAN) |
| Respuesta sin resultado DIAN reconocible, o error después de iniciar la transmisión | POSSIBLY_SENT | POSSIBLY_SENT |
| Error antes de transmitir | FAILED (o reintento si es transitorio) | — |
En habilitación la respuesta no trae el número ni el UUID del envío: el item SUCCESS llega con document_number en null y su document_uuid es el documents[].uuid que enviaste (o null si no lo enviaste). Aun así el documento se puede ubicar con su UUID anti-duplicados (ver abajo).
Consultar el documento cuando document_uuid es null
Todo item se emite con un UUID anti-duplicados, que se registra y se vincula al envío antes de transmitir. Por eso, aunque document_uuid llegue null (en habilitación, o en un POSSIBLY_SENT cuya respuesta no trajo el UUID), puedes consultar el documento con GET /documents/{uuid} usando, en este orden:
- El
documents[].uuidque enviaste. - Si no lo enviaste, el
payload.uuid(solo si es un UUID válido). - Si no enviaste ninguno, el
item_iddel item.
GET /documents/{uuid} acepta tanto el UUID del envío como el anti-duplicados:
| UUID consultado | dataRecord |
|---|---|
UUID del envío (document_uuid) | El envío del documento (con is_valid y XmlDocumentKey). |
| UUID anti-duplicados | El vínculo anti-duplicados; el envío viene en dataRecord.shipping_history. |
| Ninguno de los dos existe | 200 con dataRecord: null. |
Enviar siempre documents[].uuid simplifica esta búsqueda: es un valor que ya tienes en tu sistema.
Catálogo de error_code
| Estado | error_code | Significado | Qué hacer |
|---|---|---|---|
FAILED | Código HTTP como string: "400", "404", "409", "422"… | Error del documento o de los datos (por ejemplo, "404" cuando no hay resolución activa). | Corregir y reenviar. El UUID anti-duplicados queda libre. |
FAILED | "408", "429", "5xx" | Error transitorio que persistió tras agotar los 3 intentos. | Reenviar. |
FAILED | DUPLICATE_DOCUMENT_UUID | El UUID ya se usó en otro documento de la empresa; no se emitió de nuevo. | Consultar el documento al que apunta document_uuid. Si es otro documento, enviar el tuyo con un UUID nuevo. |
FAILED | JOB_FAILED | El procesamiento del item no terminó y no hay envío del documento. | Reenviar sin riesgo: no existe el vínculo anti-duplicados, así que el documento no se transmitió. |
FAILED | STUCK | El item quedó detenido sin transmisión y se cerró en la conciliación (ver Items detenidos). | Reenviar sin riesgo, por la misma razón que JOB_FAILED. |
FAILED | Otro identificador técnico (código o tipo de la excepción) | Error interno sin código HTTP asociado. | Reenviar; si se repite, reportarlo a soporte con el item_id. |
REJECTED_BY_DIAN | StatusCode de la DIAN (por ejemplo, "99") o DIAN | Rechazo de la DIAN. DIAN aparece cuando la respuesta no trae StatusCode o cuando el item se concilió con un envío rechazado. | Corregir según dian_response.errors. |
POSSIBLY_SENT | DOCUMENT_POSSIBLY_SENT | La transmisión empezó y no hubo respuesta definitiva. | No reenviar. Consultar el estado. |
POSSIBLY_SENT | DOCUMENT_IN_TRANSMISSION | Otro proceso estaba transmitiendo ese mismo número. | No reenviar. Consultar el estado. |
POSSIBLY_SENT | DOCUMENT_IN_VALIDATION | La DIAN aún valida un envío anterior del mismo número (StatusCode 98). | No reenviar. Consultar el estado. |
Los casos DOCUMENT_IN_TRANSMISSION y DOCUMENT_IN_VALIDATION (un 409 del pipeline unitario) antes terminaban FAILED. Ahora terminan POSSIBLY_SENT, porque ese número puede quedar autorizado en la DIAN.
Reintentos automáticos
- Solo se reintentan los resultados transitorios
408,429y5xx: hasta 3 intentos, con 30 segundos de espera entre intentos. Mientras espera, el item vuelve aQUEUEDconattemptsmayor que 0 y elerror_code/error_messagedel último intento. - Los demás errores
4xxno se reintentan: el item quedaFAILEDde inmediato. - Un documento cuya transmisión ya empezó nunca se reemite. Si la API no pudo confirmar en la DIAN el estado de un envío anterior (
503), en el siguiente intento el item se concilia con ese envío: normalmente terminaPOSSIBLY_SENT(DOCUMENT_POSSIBLY_SENT, warningRECONCILED); terminaSUCCESSsi el envío ya quedó válido, oREJECTED_BY_DIAN(error_codeDIAN) si consta como rechazado.
Items detenidos y conciliación automática
Si el procesamiento de un item muere (por ejemplo, se detiene el proceso que lo atendía), la API lo resuelve sola. La revisión corre cada 10 minutos:
- Un item que lleva más de 30 minutos en
PROCESSINGsin cambios se concilia con lo que conste de su envío. TerminaSUCCESS,REJECTED_BY_DIAN(error_codeDIAN) oPOSSIBLY_SENT, con el warningRECONCILED, oFAILEDconSTUCKsi nunca llegó a transmitirse. - Un item que sigue en
QUEUEDmás de 6 horas se vuelve a despachar. No se emite dos veces: el item se reclama de forma atómica. - Nunca se reemite un documento que ya se transmitió.
En la práctica, cuando falla el procesamiento, un lote puede quedarse en PROCESSING unos 40 minutos antes de cerrarse. Si haces polling, no lo des por perdido antes de ese tiempo.
Idempotencia
El endpoint POST soporta idempotencia mediante el header Idempotency-Key (un UUID v4, máximo 36 caracteres).
| Escenario | Resultado |
|---|---|
Primera vez con Idempotency-Key | Se crea el lote normalmente (201). |
| Mismo key + mismo body | Se retorna el lote original con su estado actual, sin reprocesar (201; estimated_completion_seconds es 0 si ya terminó). |
| Mismo key + body distinto | Error 409 Conflict. |
| Key de más de 36 caracteres | Error 422. |
| Sin header | Cada request crea un lote nuevo. |
- La comparación se hace sobre el body exacto (huella SHA-256 de los bytes recibidos): cualquier cambio, incluso de espacios u orden de claves, cuenta como body distinto. Reenvía exactamente los mismos bytes.
- Ante un
429o un error de red, reintenta con el mismoIdempotency-Keyy el mismo body: si el lote ya se había creado, recibes el original en lugar de un duplicado.
El Idempotency-Key tiene una validez de 24 horas desde la creación del lote. Después de ese período, la key se libera y el mismo valor crea un lote nuevo.
Modos de Envío
auto-increment
El sistema asigna automáticamente el número de documento a partir de la resolución.
- La resolución se busca por
resolution_number+prefix(delpayload, o dedefault_resolution_number/default_prefixdel lote) y debe estar activa. Verifica además que esté vigente: en bulk no se valida su rango de fechas. - Si no existe una resolución activa con esos datos, el item termina
FAILEDconerror_code"404"y el mensajeNo se encontró una resolución de facturación activa (resolution_number: …, prefix: …).. - Un
document_numberenviado en elpayloadse sobrescribe con el consecutivo asignado. - Consecutivos en fallos:
- Si el item falla antes de transmitirse, la reserva de su número se libera. El siguiente número se calcula como el mayor reservado + 1, así que el número liberado solo se reutiliza si seguía siendo el último de la resolución; si ya se reservaron números posteriores, queda un hueco en la numeración.
- Si la transmisión ya empezó (
REJECTED_BY_DIAN,POSSIBLY_SENT), el número queda asociado a ese documento.
manual
Cada documento debe traer su propio document_number en el payload. El sistema no asigna consecutivos.
stop_on_error
| Valor | Comportamiento |
|---|---|
false | Todos los documentos se procesan independientemente. Los fallos no afectan a los demás. |
true | El primer documento que termine en un estado distinto de SUCCESS (FAILED, REJECTED_BY_DIAN o POSSIBLY_SENT) cancela los items que siguen en cola (CANCELLED). Los items que ya se estaban procesando terminan normalmente. El lote termina en PARTIAL o FAILED. |
Códigos de Error HTTP
| HTTP | Causa | Descripción |
|---|---|---|
201 | ✅ Éxito | Lote aceptado (también en un replay idempotente). |
402 | Membresía o cuota | Membresía sin cupo o inactiva, o cuota insuficiente para el lote completo. |
404 | Lote no encontrado | En las consultas: el lote no existe, ya se purgó o pertenece a otra empresa. |
409 | Conflicto idempotencia | Idempotency-Key reutilizado con un body distinto. |
413 | Lote muy grande | El número de documentos excede el máximo de tu plan. Se valida antes que el esquema. |
422 | Validación | El request no cumple el esquema, el Idempotency-Key supera 36 caracteres, o documents llega vacío. |
429 | Límite de frecuencia o de carga | Lotes por minuto excedidos, demasiados documentos en proceso u otro lote de la empresa en admisión. |
Orden de los controles en POST /bulk/documents
La API aplica los controles en este orden y responde con el primero que falla:
429: lotes por minuto (por usuario y por empresa). El intento se cuenta aunque la petición falle después.402: membresía sin cupo o inactiva.422Compañía o lote inválido.sidocumentsestá vacío o no es un array; luego413por el tope del plan y402por cuota insuficiente (conrequested).422: esquema del request.422:Idempotency-Keyde más de 36 caracteres.429: otro lote de la empresa en admisión (la API espera hasta 5 segundos antes de responder).- Replay idempotente:
201con el lote original, o409si el body difiere. 429: demasiados documentos en proceso.402: cuota revalidada (solomessage).
Reintentar con el mismo Idempotency-Key consume cupo de lotes por minuto y pasa por membresía, tope del plan, cuota y esquema antes de devolver el lote original. Por ejemplo, si la cuota se agotó entretanto, recibes 402 aunque el lote ya exista. Además, la cuota disponible descuenta los documentos aún pendientes de lotes en curso, incluido el propio lote original: mientras ese lote se procesa, el replay puede recibir 402 si lo que queda de cuota no alcanza para volver a cubrir el lote completo. Guarda el batch_id del primer 201 para no depender del replay.
- Error 413
- Error 402
- Error 422
- Error 409
- Error 429
{
"success": false,
"message": "El lote excede el máximo permitido para tu suscripción.",
"max_items": 50,
"received": 75,
"upgrade_url": "https://sandbox-api.matias-api.com/upgrade"
}
Topes por plan: FREE 5, Pago 50, Ilimitado 200.
La API responde 402 en tres formas:
{
"success": false,
"message": "Límite de documentos de la membresía alcanzado o membresía inactiva."
}
{
"success": false,
"message": "Cuota insuficiente para procesar el lote completo.",
"requested": 25
}
Al revalidar la cuota justo antes de crear el lote (descontando los documentos pendientes de lotes anteriores), la respuesta trae solo message:
{
"message": "Cuota insuficiente para procesar el lote completo."
}
Errores de esquema, con el formato estándar de validación (message con el primer error y errors por campo; sin success):
{
"message": "El campo mode es obligatorio (auto-increment o manual). (and 1 more error)",
"errors": {
"mode": ["El campo mode es obligatorio (auto-increment o manual)."],
"documents.0.kind": [
"Tipo de documento no válido. Tipos permitidos: invoice, credit_note, debit_note, support_document, adjustment_note, pos_document"
]
}
}
Otros mensajes de esquema:
| Regla | Mensaje |
|---|---|
mode con un valor distinto de auto-increment o manual | El campo mode debe ser auto-increment o manual. (el de «obligatorio» solo sale cuando falta) |
payload que no es un objeto JSON | El payload de cada documento debe ser un objeto JSON. |
uuid no válido / repetido en el lote | El UUID del documento debe ser un UUID v4 válido. / El UUID del documento está repetido dentro del lote. |
| Más de 4 adjuntos | Cada documento admite máximo 4 adjuntos. |
Adjunto sin filename / sin content | Cada adjunto debe incluir filename. / Cada adjunto debe incluir content (base64). |
client_reference de más de 64 caracteres | La referencia del cliente no puede exceder 64 caracteres. |
| Más de 200 documentos | El lote no puede contener más de 200 documentos. (solo cuando el tope del plan no lo frena antes con 413) |
content_type de más de 100, filename de más de 255, default_resolution_number de más de 50, default_prefix de más de 10, o stop_on_error no booleano | Mensaje estándar de validación (longitud máxima o tipo). |
Idempotency-Key de más de 36 caracteres (solo message):
{
"message": "Idempotency-Key no puede superar 36 caracteres (use un UUID v4)."
}
documents vacío o ausente (se valida antes que el esquema):
{
"success": false,
"message": "Compañía o lote inválido."
}
Según el entorno, un documents vacío también puede llegar como error de esquema (Debe incluir al menos un documento en el lote.).
{
"message": "Idempotency-Key reutilizado con payload distinto."
}
Lotes por minuto. En el plan FREE la respuesta no trae Retry-After ni X-RateLimit-Reset: espera 60 segundos (la ventana es de 1 minuto). Los X-RateLimit-Limit / X-RateLimit-Remaining que acompañen este 429 corresponden al límite general de la API, no al de lotes por minuto.
{
"success": false,
"message": "Plan FREE: máximo 1 lote bulk por minuto. Actualice su plan.",
"upgrade_url": "https://sandbox-api.matias-api.com/upgrade"
}
En los planes de pago e ilimitado, la respuesta estándar de límite de frecuencia, con los headers Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset. Las respuestas correctas también traen X-RateLimit-Limit y X-RateLimit-Remaining, útiles para dosificar los envíos.
{
"message": "Too Many Attempts."
}
Demasiados documentos en proceso. Retry-After estima en segundos cuánto tarda la cola en drenar los documentos que sobran (excedente × ~6 s ÷ procesos que atienden la cola, mínimo 1).
{
"message": "La empresa ya tiene 980 documentos en proceso y el máximo simultáneo es 1000. Reintente más tarde."
}
Otro lote de la misma empresa en admisión. La API espera hasta 5 segundos a que termine la admisión del otro lote y, si no termina, responde con Retry-After: 5.
{
"message": "Hay otro lote de esta empresa en admisión. Reintente en unos segundos."
}
Espera los segundos de Retry-After (60 en el plan FREE) y reintenta con el mismo Idempotency-Key.
Webhook
Cuando un lote finaliza (todos sus items en estado terminal), se dispara una sola vez el evento bulk.batch.completed. Llega con el sobre estándar de los webhooks:
{
"id": "evt_aB3dE5fG7hJ9kL1mN3pQ5rS7",
"type": "bulk.batch.completed",
"created_at": "2026-06-01T19:31:01+00:00",
"api_version": "3.0",
"data": {
"object": "document_batch",
"batch_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "PARTIAL",
"total_items": 10,
"succeeded": 9,
"failed": 1,
"finished_at": "2026-06-01T19:31:00+00:00"
}
}
- La clave del evento es
type(noevent). failedincluye los itemsREJECTED_BY_DIANyPOSSIBLY_SENT.- No trae datos de los documentos: consulta el lote (
GET /bulk/documents/{batch_id}) para el detalle de cada item. - Para recibirlo, suscribe un webhook al evento
bulk.batch.completed. Es un evento válido aunqueGET /webhooks/eventsno lo liste. - Se firma y se reintenta igual que el resto de eventos: ver Seguridad HMAC y Reintentos automáticos.
Guía Rápida de Integración
Paso 1: Enviar el lote
curl -X POST "{{url}}/bulk/documents?client_uuid={{client_uuid}}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"mode": "auto-increment",
"default_resolution_number": "18764002566734",
"default_prefix": "SETT",
"documents": [
{
"kind": "invoice",
"uuid": "6f1c2b9e-3d4a-4e8b-9c71-2a5d8e0f4b13",
"client_reference": "MI-REF-001",
"payload": { "type_document_id": 7, "...": "payload completo de factura" }
},
{
"kind": "invoice",
"uuid": "0d9e8f7a-6b5c-4d3e-8f2a-1b0c9d8e7f6a",
"client_reference": "MI-REF-002",
"payload": { "type_document_id": 7, "...": "payload completo de factura" }
}
]
}'
Respuesta:
201 Createdconbatch_id. Omite?client_uuid=si emites para tu propia empresa.
Paso 2: Polling del estado
Espera unos segundos (el estimated_completion_seconds te da una estimación) y luego consulta:
curl -X GET "{{url}}/bulk/documents/{batch_id}?client_uuid={{client_uuid}}" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"
Repite hasta que
statusseaCOMPLETED,PARTIALoFAILED, o suscríbete al webhookbulk.batch.completed.
Paso 3: Guardar los document_uuid
Guarda el document_uuid de cada item que lo tenga, no solo de los SUCCESS: los REJECTED_BY_DIAN y POSSIBLY_SENT también corresponden a documentos que llegaron a la DIAN. Este UUID es la referencia permanente del documento.
// Ejemplo: extraer UUIDs del response
const batch = await response.json();
const documents = batch.items
.filter(item => item.document_uuid)
.map(item => ({
document_uuid: item.document_uuid, // ← GUARDAR en su BD
client_reference: item.client_reference, // ← Su referencia interna
status: item.status,
document_number: item.document_number ?? null,
cufe: item.cufe ?? null
}));
// Persistir en su sistema para consultas futuras
await saveToDatabase(documents);
// Documentos sin respuesta definitiva: consultar su estado, nunca reenviar
const toCheck = batch.items.filter(item => item.next_action === 'CHECK_STATUS');
Paso 4: Revisar los que no terminaron en SUCCESS
curl -X GET "{{url}}/bulk/documents/{batch_id}/items?status=FAILED&client_uuid={{client_uuid}}" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"
Repite con status=REJECTED_BY_DIAN y status=POSSIBLY_SENT:
FAILED: corrige el documento segúnerror_code/error_messagey reenvíalo en un lote nuevo. Si no llegó a transmitirse, puedes usar el mismouuid.REJECTED_BY_DIAN: revisadian_response.errorsy corrige el documento.POSSIBLY_SENT: consulta el estado con eldocument_uuidantes de cualquier acción; no lo reenvíes.
Retención de Datos
El lote es solo un mecanismo de carga masiva: cada documento se emite y se conserva individualmente, igual que en el endpoint unitario. Por eso los datos del lote se limpian pronto.
| Qué | Cuándo | Efecto |
|---|---|---|
payload de items SUCCESS | 1 día después de procesado | Se vacía. El resumen del item (document_uuid, document_number, cufe, dian_response, error…) se conserva. |
payload de items FAILED, REJECTED_BY_DIAN y CANCELLED | 2 días después de procesado | Se vacía. El resumen del item se conserva. |
Items POSSIBLY_SENT | — | No se limpian antes de la purga del lote. |
| Lote completo (lote + items) | 7 días desde la creación del lote | Se elimina, solo si el lote terminó (COMPLETED, PARTIAL o FAILED). |
- El
payloadnunca se expone por la API; su limpieza no cambia lo que ves en las consultas. - Hasta la purga del lote, sus items siguen disponibles en
GET /bulk/documents/{batch_id}y en/items. Después, el endpoint responde404: consulta los documentos por sudocument_uuid, o por número y prefijo con Búsqueda y Listado.
Notas Técnicas
- Cada documento se procesa de forma independiente, con hasta 3 intentos (30 segundos entre intentos) solo para fallos transitorios (
408,429,5xx). estimated_completion_secondsse calcula con la cola actual, a razón de ~6 segundos por documento (varía según la latencia con la DIAN).- El webhook
bulk.batch.completedse dispara una sola vez, cuando todos los items alcanzaron un estado terminal. - El lote completo se elimina a los 7 días de su creación: guarda los
document_uuidantes. - Si el procesamiento de un item muere, la API lo concilia sola en unos 40 minutos y nunca reemite un documento ya transmitido. Ver Items detenidos y conciliación automática.
Mantén el body del request por debajo de 5 MB. Ese límite lo aplica el servidor web, no la validación de la API: un body mayor puede rechazarse sin el formato JSON de esta página. Para lotes grandes, divide en múltiples requests.
Cambios y compatibilidad 🆕
Si ya consumías el envío masivo, revisa estos cambios de la versión actual.
Cambios incompatibles
| Cambio | Antes | Ahora | Qué debes hacer |
|---|---|---|---|
items[].response | JSON con la clave raw (XML, AttachedDocument, QR y PDF en base64). | String JSON con el resumen: uuid, document_number, XmlDocumentKey/cufe, track_id, is_valid, status_code, dian_status_code, dian_status, errors, notifications, warnings, http_status. La clave raw ya no existe: los registros antiguos se normalizan al resumen y raw nunca se vuelve a exponer. | No leas response.raw. Descarga XML y PDF con el document_uuid (Descarga de Archivos). Ver El campo response. |
response.status_code y response.dian_response | status_code era el código HTTP de la emisión; el detalle de la DIAN venía en response.dian_response y en raw. | status_code sigue siendo el código HTTP entero (igual a http_status). response ya no trae dian_response: el StatusCode de la DIAN va en dian_status_code y, a nivel de item, en dian_response.status_code. | No busques el código de la DIAN en status_code: usa dian_status_code o items[].dian_response. |
SUCCESS frente a REJECTED_BY_DIAN | Lo decidía el código HTTP. | Lo decide la respuesta de la DIAN (IsValid = true y StatusCode = 00). En habilitación, SUCCESS significa recibido (ZipKey). | Usa dian_response para el detalle. En habilitación, consulta el resultado con el track_id. Ver Cómo se decide el resultado. |
Estado POSSIBLY_SENT | No existía. | Estado nuevo: cuenta en failed, dispara stop_on_error y el documento pudo llegar a la DIAN. | No lo reenvíes: consulta su estado. Ver Estados del Item. |
documents[].uuid | Opcional, sin efecto anti-duplicados. | UUID anti-duplicados: debe ser un UUID válido y distinto dentro del lote. Si ya se usó en otro documento, el item termina FAILED con DUPLICATE_DOCUMENT_UUID. | No reutilices el UUID de otro documento. Ver UUID anti-duplicados. |
items[].document_uuid | UUID del envío solo en los SUCCESS; en los demás, el documents[].uuid que enviaste (o null). | El UUID del envío en todo item que tenga envío (también REJECTED_BY_DIAN y POSSIBLY_SENT); en DUPLICATE_DOCUMENT_UUID, el del documento existente; si no hay envío, el documents[].uuid enviado o null. | Guarda el document_uuid que devuelve la API. Ver Campos de cada item. |
Nuevos 429 con Retry-After | Solo el de lotes por minuto. | Además, demasiados documentos en proceso y otro lote de la empresa en admisión. El límite de lotes por minuto se comparte por empresa. | Respeta Retry-After y reintenta con el mismo Idempotency-Key. |
Nuevos 422 | — | Tope técnico de 200 documentos, UUID repetido en el lote, reglas de adjuntos e Idempotency-Key de más de 36 caracteres. | Ajusta el armado del lote. Ver Códigos de Error HTTP. |
error_code | Códigos sin catálogo. | Catálogo documentado: código HTTP como string, DUPLICATE_DOCUMENT_UUID, JOB_FAILED, STUCK, códigos de la DIAN, los de POSSIBLY_SENT y, para errores internos, un identificador técnico. | Actualiza tu mapeo de errores. Ver Catálogo de error_code. |
Cambios compatibles
- Ahora documentado: el
413incluyeupgrade_url. - Los items exponen
item_id,kind_description,status_description,document_number,cufe,track_id,dian_response,warnings,errorynext_action. - El resumen de
responsese conserva hasta la purga del lote. per_pagese limita a 200 en items y a 100 en lotes.