Ir al contenido

Planes y cupo de documentos

Un emisor factura contra un paquete: una cantidad de documentos y una fecha de corte. Cada documento que emites gasta una unidad. Cuando se acaban las unidades, o llega la fecha, el API deja de aceptar documentos nuevos y responde 402.

Eso es todo el modelo. El resto de esta página es dónde exactamente se descuenta, qué no se descuenta, y cómo enterarte antes de quedarte sin cupo.

Un documento emitido = una unidad. No importa el tipo: los cinco tipos que emite el API salen del mismo cupo.

Tipo Gasta cupo
Factura electrónica de venta Sí
Nota crédito Sí
Nota débito Sí
Documento soporte Sí
Documento equivalente POS Sí

No gasta cupo:

  • Reintentar con el mismo reference_code. La idempotencia se resuelve antes de tocar el paquete: el API encuentra el documento que ya existe y lo devuelve sin volver a cobrar. Un timeout tuyo no te cuesta un documento.
  • Consultar. GET /v1/documents/..., /v1/subscription y el resto de lecturas no descuentan nada.

Al crear el documento, antes de guardarlo y antes de encolar el trabajo en segundo plano. Es a propósito: si el paquete ya se agotó, mejor no haber consumido un consecutivo de la DIAN que después nadie va a poder usar.

El orden real dentro de un POST de documento:

  1. Se busca el reference_code. Si ya existe, se devuelve ese documento y aquí se acaba: no se cobra nada.
  2. Se validan los datos y el servidor calcula los montos.
  3. Se descuenta una unidad. Si el paquete no da, responde 402 y no se crea ningún documento.
  4. Se guarda el documento y se encola para armar el XML, firmarlo y transmitirlo.

Si el paso 4 falla al guardar, la unidad se devuelve. Es la única devolución automática que existe, y solo cubre el caso de que el documento no llegara a existir.

El descuento es un UPDATE condicional atómico —la condición y la escritura son la misma operación—, no un «leer, sumar, guardar». Dos documentos simultáneos con una sola unidad libre no se pueden pasar de la cuota: uno pasa y el otro recibe PLAN_EXHAUSTED.

Depende de una variable del despliegue, PLANS_ENFORCE_ALL:

Valor A quién se le exige
Sin poner (por defecto) Solo a las empresas a las que alguna vez se les vendió un paquete
true A todas

Una empresa que nunca tuvo paquete y corre con la configuración por defecto emite sin bloqueo, y GET /v1/subscription le devuelve null.

El vigente es la suscripción más reciente por fecha de creación cuyo estado sea ACTIVE o EXHAUSTED. Los otros dos estados no cuentan como vigentes.

status Qué significa
ACTIVE Vigente y con unidades
EXHAUSTED Se acabaron las unidades. Sigue siendo el paquete vigente: una recarga lo reabre
EXPIRED Venció. Deja de ser el vigente
CLOSED Cancelado a mano, o reemplazado por un paquete nuevo estando en ACTIVE

El campo booleano active de la respuesta es más estricto que el status: es true solo si el estado es ACTIVE y la fecha de fin todavía no pasó.

Un paquete puede venderse con unlimited: true. Cuenta lo que emites pero nunca bloquea, y esa comprobación va antes que la del vencimiento a propósito: a un cliente con contrato plano no se le corta la facturación en mitad del día porque se pasó la fecha de corte; eso lo resuelve comercial.

En un contrato plano, documentsRemaining llega en null y usedPercent en 0.

Los tres traen el cuerpo de error estándar del API, con errors vacío.

code HTTP Cuándo aparece
PLAN_REQUIRED 402 No hay paquete vigente y a esa empresa sí se le exige uno
PLAN_EXPIRED 402 El paquete vigente ya pasó su fecha de fin, y no es plano
PLAN_EXHAUSTED 402 Se acabaron las unidades
{
"status": 402,
"code": "PLAN_EXHAUSTED",
"message": "Se agotaron los 1600 documentos del paquete \"Paquete 1.600 documentos\". Contrata una recarga para seguir emitiendo.",
"errors": []
}

Los mensajes de PLAN_EXPIRED y PLAN_EXHAUSTED traen el nombre del paquete y —según el caso— la fecha de vencimiento o la cantidad contratada. Están escritos para mostrárselos al usuario tal cual. Aun así, programa contra code: el texto puede cambiar.

El documento que alcanza exactamente la cuota sí se emite. Si tienes 1.600 documentos y emites el número 1.600, ese sale bien. Lo que pasa es que el paquete queda en EXHAUSTED. El 402 llega en el siguiente.

PLAN_EXPIRED normalmente lo ves una sola vez. Al detectar el vencimiento, el API marca la suscripción como EXPIRED, y ese estado ya no cuenta como paquete vigente. La petición siguiente no vuelve a dar PLAN_EXPIRED: da PLAN_REQUIRED. Y desde ese momento GET /v1/subscription devuelve null —el paquete solo se ve en el histórico—. Por eso conviene tratar los tres códigos igual: no hay nada que tu código pueda arreglar, hay que renovar.

PLAN_EXHAUSTED sí se repite en cada intento, porque un paquete agotado sigue siendo el vigente.

Ninguno de los tres es reintentable. Reintentar sin cambiar nada devuelve el mismo error; míralos junto al resto en Errores.

runningLow se pone en true cuando:

  • Paquete normal: queda el 10% o menos de los documentos (con un umbral mínimo de 1), o faltan 30 días o menos para la fecha de corte.
  • Contrato plano: solo por fecha, 30 días o menos.

Consúltalo una vez al día, no en cada venta: cuenta contra el límite de 300 peticiones por minuto y el número no cambia tan rápido.

Los tres endpoints exigen Authorization: Bearer con un token válido. El emisor sale del token; un usuario ADMIN, que no tiene emisor propio, indica sobre cuál trabaja con la cabecera X-Company-Id. Si falta, responde 400 con Falta indicar sobre qué emisor trabajar (cabecera X-Company-Id).

GET/v1/subscription

Devuelve el paquete vigente. Si la empresa no tiene ninguno vigente —porque nunca tuvo uno, o porque el suyo quedó en EXPIRED o CLOSED—, responde 200 sin cuerpo: cadena vacía, Content-Length: 0. No es un 404, y tampoco llega el texto null. Comprueba que el cuerpo no venga vacío antes de parsearlo: un await res.json() directo revienta con «Unexpected end of JSON input».

{
"id": "3f0c9e21-5b8a-4d77-9a13-6c2e84fbb410",
"plan": {
"id": "b7d41e88-2c05-4f6a-8e39-1d5a7c930bb2",
"name": "Paquete 1.600 documentos"
},
"documentTypes": ["INVOICE", "CREDIT_NOTE", "DEBIT_NOTE"],
"unlimited": false,
"documentsIncluded": 1600,
"documentsUsed": 1520,
"documentsRemaining": 80,
"usedPercent": 95,
"startsAt": "2025-11-20T00:00:00.000Z",
"endsAt": "2026-11-20T00:00:00.000Z",
"daysRemaining": 77,
"status": "ACTIVE",
"active": true,
"runningLow": true,
"priceCop": 220000
}
Campo Tipo Descripción
id uuid Identificador de la suscripción
plan.id uuid | null null en paquetes hechos a la medida
plan.name string Copia congelada del nombre del paquete al venderlo
documentTypes string[] Valor fijo. Ver el aviso de arriba
unlimited booleano Contrato plano: cuenta pero no bloquea
documentsIncluded número Cuota, ya con las recargas sumadas
documentsUsed número Documentos emitidos contra este paquete
documentsRemaining número | null null si unlimited. Nunca baja de 0
usedPercent número Entero redondeado. 0 si unlimited
startsAt fecha Inicio del periodo
endsAt fecha Fecha de corte
daysRemaining número Redondeado hacia arriba. Nunca baja de 0
status enum ACTIVE, EXHAUSTED, EXPIRED o CLOSED
active booleano ACTIVE y sin vencer
runningLow booleano Hay que renovar pronto
priceCop número Precio pagado, en pesos y sin decimales

GET/v1/subscription/history

Todos los paquetes de la empresa, del más nuevo al más viejo. Sin paginación ni límite.

[
{
"id": "3f0c9e21-5b8a-4d77-9a13-6c2e84fbb410",
"companyId": "c1a7f402-98d6-4b21-b0e5-7f3ac6d18e94",
"planId": "b7d41e88-2c05-4f6a-8e39-1d5a7c930bb2",
"planName": "Paquete 1.600 documentos",
"documentsIncluded": 1600,
"documentsUsed": 1520,
"unlimited": false,
"priceCop": "220000",
"startsAt": "2025-11-20T00:00:00.000Z",
"endsAt": "2026-11-20T00:00:00.000Z",
"status": "ACTIVE",
"notes": "Recarga de 400 documentos - transferencia 4471",
"createdAt": "2025-11-20T14:32:07.118Z",
"updatedAt": "2026-06-02T09:14:51.006Z"
}
]

GET/v1/plans

Los paquetes que se venden hoy, ordenados de menor a mayor cuota.

[
{
"id": "b7d41e88-2c05-4f6a-8e39-1d5a7c930bb2",
"name": "Paquete 1.600 documentos",
"code": "PAQ-1600",
"documentQuota": 1600,
"priceCop": "220000",
"periodMonths": 12,
"description": "Incluye facturas, notas crédito, notas débito y documentos soporte. Rangos de numeración ilimitados para sucursales, sin limitación por ventas.",
"unlimitedNumberingRanges": true,
"active": true,
"sortOrder": 1600,
"createdAt": "2025-10-01T00:00:00.000Z",
"updatedAt": "2025-10-01T00:00:00.000Z",
"deletedAt": null
}
]
Campo Tipo Descripción
id uuid
name string Nombre comercial
code string Identificador estable y único. Intégrate contra esto, no contra el nombre
documentQuota número Documentos incluidos en el periodo
priceCop string Pesos sin decimales, serializado como cadena
periodMonths número Duración. 12 es como se vende hoy
description string | null Texto comercial
unlimitedNumberingRanges booleano true: los rangos de numeración no se limitan por paquete
active booleano Los inactivos no salen en esta lista
sortOrder número Orden de presentación

Lee las tarifas de aquí, no las fijes en tu código. Los paquetes se editan desde el panel: cambian de precio y entran y salen del catálogo.

No hay endpoint para que un emisor se contrate o se recargue solo. Esas operaciones las hace el equipo de la plataforma. Lo que te conviene saber del comportamiento:

  • Una recarga suma documentos sin mover la fecha de corte. Es lo que se hace cuando alguien se queda corto a mitad de año. Verás subir documentsIncluded.
  • Recargar reabre un paquete agotado: pasa de EXHAUSTED a ACTIVE otra vez, sin cambiar endsAt.
  • Contratar un paquete nuevo cierra el anterior solo si estaba en ACTIVE: en la misma operación pasa a CLOSED. Uno que ya estaba en EXHAUSTED o EXPIRED conserva ese estado en el histórico —el caso más frecuente, comprar porque el anterior se agotó, deja el viejo en EXHAUSTED para siempre—. Deja de ser el vigente porque el nuevo es más reciente por createdAt, no porque se haya cerrado.
  • Subir la tarifa no cambia lo ya vendido. La suscripción guarda su propia cuota y su propio precio en vez de leerlos del catálogo, así que una lista de precios nueva no toca los paquetes en curso.
  • Maneja los tres 402 desde el primer día, aunque el emisor con el que pruebas todavía no tenga paquete.
  • Trátalos como no reintentables: son un aviso para una persona, no un fallo temporal. Corta la cola de envío y muestra el message.
  • Consulta GET /v1/subscription una vez al día y alerta con runningLow. Es el único aviso que hay.
  • No cuentes los documentos por tu lado. Las recargas cambian la cuota sin previo aviso.
  • Recuerda que un rechazo de la DIAN ya gastó su unidad: si vas a reprocesar algo muchas veces, hazlo en sandbox.