Rangos de numeración
Un rango de numeración es, casi siempre, una resolución de la DIAN guardada en el API: autoriza a tu emisor a numerar un tipo de documento, con un prefijo, entre dos números y entre dos fechas. Las notas crédito y débito son la excepción, porque ahí no hay resolución que copiar — lo explica Las notas no llevan resolución.
Cada documento que emites toma un consecutivo de un rango. Por eso
numbering_range_id va en el cuerpo de toda factura, nota o documento
soporte: sin rango no hay número, y sin número no hay documento.
Un rango por tipo de documento
Sección titulada «Un rango por tipo de documento»La DIAN no deja numerar dos tipos distintos en la misma serie. Cada tipo
lleva su serie aparte, y el API refleja eso: un rango tiene un
documentType, y solo sirve para ese.
documentType |
Para qué documento | Código único |
|---|---|---|
INVOICE |
Factura electrónica de venta (01) |
CUFE |
CREDIT_NOTE |
Nota crédito (91) |
CUDE |
DEBIT_NOTE |
Nota débito (92) |
CUDE |
SUPPORT_DOCUMENT |
Documento soporte (05) |
CUDS |
EQUIVALENT_POS |
Documento equivalente POS (20) |
CUDE |
Si mandas el id de un rango que no corresponde, el documento se rechaza al crearlo, antes de tocar la cola:
{ "status": 422, "code": "VALIDATION", "message": "El rango 9a76de2a-71c6-4020-aa54-255c58219283 es de tipo INVOICE, no aplica para este documento", "errors": []}Las notas no llevan resolución
Sección titulada «Las notas no llevan resolución»La DIAN autoriza la numeración de la factura, del documento soporte y del equivalente POS: entrega un número de resolución, un prefijo, un tramo y una vigencia. De las notas crédito y débito no autoriza nada. Una nota va con el consecutivo del propio emisor, así que no hay resolución ni fechas de validez que guardar.
documentType |
resolutionNumber, validFrom y validUntil |
|---|---|
INVOICE, SUPPORT_DOCUMENT, EQUIVALENT_POS |
Obligatorios. Son la copia del acto administrativo que ampara esa serie. |
CREDIT_NOTE, DEBIT_NOTE |
Opcionales. Si no los mandas, se guardan y se devuelven en null. |
Se ve en el XML: el bloque sts:InvoiceControl —la resolución, su vigencia y
el tramo autorizado— viaja firmado dentro de la factura, del documento
soporte y del tiquete POS, y no viaja dentro de una nota. Un número de
resolución inventado para llenar el campo no llegaba a ningún documento: solo
ocupaba la llave única del rango y le hacía creer al emisor que tenía un
papel que nadie le dio.
Un rango así se comporta distinto en dos sitios, y los dos están más abajo: no vence nunca, y su prefijo sirve otra vez para el rango siguiente cuando este se agote.
Listar los rangos
Sección titulada «Listar los rangos»GET/v1/numbering-ranges
Sin cuerpo. Devuelve un array de rangos, del más reciente al más antiguo por fecha de creación. No hay paginación.
curl https://sandbox.facturacion.misofk.com/v1/numbering-ranges \ -H "Authorization: Bearer $TOKEN"[ { "id": "9a76de2a-71c6-4020-aa54-255c58219283", "companyId": "3f2b1c9d-8a41-4e77-9b03-1d6e5a2c7f80", "documentType": "INVOICE", "resolutionNumber": "18764092807123", "resolutionDate": "2026-01-15", "prefix": "SETP", "rangeFrom": "990000000", "rangeTo": "990005000", "currentNumber": "990000123", "validFrom": "2026-01-15", "validUntil": "2027-01-15", "isActive": true, "source": "DIAN_SYNC", "createdAt": "2026-01-15T14:02:11.884Z", "updatedAt": "2026-09-03T18:41:07.221Z", "hasTechnicalKey": true, "usagePercent": 2.5, "proximoNumero": "990000124" }]El objeto rango
Sección titulada «El objeto rango»| Campo | Tipo | Descripción |
|---|---|---|
id |
uuid | El que mandas en numbering_range_id. |
companyId |
uuid | Emisor dueño de la resolución. |
documentType |
enum | Uno de los cinco de arriba. |
resolutionNumber |
string | null | Número de la resolución DIAN. Máximo 30 caracteres. null en un rango de notas sin resolución. |
resolutionDate |
string | null | yyyy-MM-dd. Puede venir en null. |
prefix |
string | Prefijo autorizado. Máximo 10 caracteres, y puede ser cadena vacía. |
rangeFrom |
string | Primer número autorizado. |
rangeTo |
string | Último número autorizado. |
currentNumber |
string | Último consecutivo ya consumido. "0" significa que no se ha usado ninguno. |
validFrom |
string | null | yyyy-MM-dd. Antes de esta fecha no se puede emitir. null = el rango no espera ninguna fecha para empezar. |
validUntil |
string | null | yyyy-MM-dd. Después de esta fecha tampoco. null = no vence. |
isActive |
boolean | Un rango inactivo no entrega números. |
source |
enum | MANUAL si lo cargaste tú, DIAN_SYNC si lo trajo la sincronización. |
hasTechnicalKey |
boolean | Si el rango tiene clave técnica guardada. |
usagePercent |
number | Porcentaje consumido, con un decimal. |
proximoNumero |
string | null | Con qué número sale el siguiente documento. null = agotado. |
createdAt |
fecha-hora | |
updatedAt |
fecha-hora |
La clave técnica nunca sale por el API, ni cifrada ni en claro. Lo
único que puedes saber es si está: hasTechnicalKey.
Consultar un rango
Sección titulada «Consultar un rango»GET/v1/numbering-ranges/:id
Mismo objeto que en la lista.
| Código | code |
Cuándo |
|---|---|---|
400 |
VALIDATION |
El :id no es un UUID. Falla antes de consultar la base. |
404 |
NOT_FOUND |
Rango de numeración no encontrado — no existe, o no es de tu emisor. |
Crear un rango
Sección titulada «Crear un rango»POST/v1/numbering-ranges
Para registrar a mano lo que dice el PDF de la resolución — o, si es un rango de notas, el tramo de consecutivos con el que el emisor va a numerarlas.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
documentType |
enum | Sí | INVOICE, CREDIT_NOTE, DEBIT_NOTE, SUPPORT_DOCUMENT o EQUIVALENT_POS. |
resolutionNumber |
string | Sí, salvo en notas | No puede ir vacío. Máximo 30 caracteres, el largo de la columna. Omitido en una nota, se guarda null. |
resolutionDate |
string | No | yyyy-MM-dd, y el día tiene que existir. |
prefix |
string | No | Por defecto, cadena vacía. Máximo 10 caracteres. |
rangeFrom |
entero | Sí | Mínimo 1. |
rangeTo |
entero | Sí | Mínimo 1, y no menor que rangeFrom. |
validFrom |
string | Sí, salvo en notas | yyyy-MM-dd, y el día tiene que existir: 2026-02-30 devuelve 400. Omitido, el rango puede emitir desde ya. |
validUntil |
string | Sí, salvo en notas | yyyy-MM-dd, con la misma comprobación. Omitido, el rango no vence. |
technicalKey |
string | No en el contrato, sí para INVOICE |
La de la resolución, copiada tal cual. |
«Salvo en notas» son CREDIT_NOTE y DEBIT_NOTE, y solo esos dos: en
INVOICE, SUPPORT_DOCUMENT y EQUIVALENT_POS los tres siguen siendo
obligatorios.
curl -X POST https://facturacion.misofk.com/v1/numbering-ranges \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "documentType": "INVOICE", "resolutionNumber": "18764092807123", "resolutionDate": "2026-01-15", "prefix": "SETP", "rangeFrom": 990000000, "rangeTo": 990005000, "validFrom": "2026-01-15", "validUntil": "2027-01-15", "technicalKey": "fc8eac422eba16e22ffd8c6f94b3f40a6e38162c" }'curl -X POST https://facturacion.misofk.com/v1/numbering-ranges \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "documentType": "CREDIT_NOTE", "prefix": "NC", "rangeFrom": 1, "rangeTo": 2000 }'Cuatro campos, y ninguno es una resolución: la nota lleva el consecutivo
del emisor, no una numeración autorizada. resolutionNumber, validFrom
y validUntil vuelven en null, y el rango no vence.
Las notas tampoco llevan clave técnica: su CUDE se calcula con el PIN del software, no con la clave del rango.
La respuesta es el objeto rango completo, con currentNumber: "0" y
source: "MANUAL". En un rango de notas, los tres campos que omitiste salen
en null, no en cadena vacía, y de esa diferencia depende poder registrar
el rango siguiente con el mismo prefijo.
Errores al crear
Sección titulada «Errores al crear»Los tres son 422 con code: VALIDATION, y traen el campo en errors[].
errors[].field |
message |
Cuándo |
|---|---|---|
rangeTo |
El número final no puede ser menor que el inicial |
rangeTo < rangeFrom |
validUntil |
La resolución no puede vencer antes de empezar a regir |
validUntil < validFrom, cuando llegan las dos fechas |
technicalKey |
Una resolución de facturas necesita su clave técnica: sin ella no se puede calcular el CUFE |
documentType: INVOICE sin technicalKey |
{ "status": 422, "code": "VALIDATION", "message": "Una resolución de facturas necesita su clave técnica: sin ella no se puede calcular el CUFE", "errors": [ { "field": "technicalKey", "message": "Viene en la resolución de la DIAN" } ]}Omitir resolutionNumber, validFrom o validUntil en un INVOICE, un
SUPPORT_DOCUMENT o un EQUIVALENT_POS no llega a esa tabla: lo ataja la
validación del cuerpo y responde 400, también con code: VALIDATION,
pero con un errors[] que trae solo message y no field. Y el de las
fechas te habla del formato aunque el campo venga vacío: cuando no mandas
validFrom, lo que recibes es validFrom debe ser una fecha real en formato YYYY-MM-DD. Dice «está mal escrita» donde querría decir «falta», así que si
lo ves y estás seguro del formato, mira si el campo está llegando.
Un emisor no puede tener dos rangos con el mismo prefix y el mismo
resolutionNumber: la base lo impide.
Corregir un rango mal registrado
Sección titulada «Corregir un rango mal registrado»PATCH/v1/numbering-ranges/:id
Mandas solo lo que cambia. Todos los campos son opcionales y tienen los
mismos topes que en el alta: documentType, resolutionNumber,
resolutionDate, prefix, rangeFrom, rangeTo, validFrom, validUntil,
technicalKey y ultimoEmitido.
Un campo ausente significa «no lo toques». Un campo en null
explícito es otra cosa: lo borra. Esa diferencia importa en cuatro
campos —resolutionNumber, resolutionDate, validFrom y validUntil— y
es la salida del emisor que ya registró su serie de notas con una resolución
inventada, cuando el API todavía se la exigía:
curl -X PATCH https://facturacion.misofk.com/v1/numbering-ranges/$ID \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resolutionNumber": null, "validFrom": null, "validUntil": null }'La respuesta es el objeto rango completo, ya con los tres en null. A partir
de ahí la serie se comporta como debe: no vence, y su prefijo queda libre
para el tramo siguiente.
Hasta cuándo se puede
Sección titulada «Hasta cuándo se puede»Mientras de este rango no haya salido nada que no se pueda desmentir. La
condición exacta es que no exista ningún documento suyo que la DIAN ya tenga
o esté a punto de tener: firmado, enviado, validado o en contingencia, más
los que están esperando número en la cola. Un borrador, un descartado y un
FAILED no cuentan — de esos no hay nada declarado ahí fuera.
La pregunta no es si currentNumber está en cero. Haber declarado por
/ultimo-emitido hasta dónde
llegaste en otro sistema mueve el contador, pero no emite nada desde aquí:
ese rango se sigue pudiendo corregir. Es justo el caso que antes dejaba
encerrado a quien migraba —declaraba su número y con eso perdía el derecho a
arreglar la fecha mal tecleada—.
Si ya emitió, la corrección se rechaza con 422:
{ "status": 422, "code": "VALIDATION", "message": "De esta resolución ya salió un documento que la DIAN tiene o está a punto de tener, así que sus datos autorizados no se pueden cambiar: viajan firmados dentro de él. Registra la resolución correcta y retira esta.", "errors": [ { "field": "id", "message": "Va por el 1253" } ]}Dos cosas se corrigen siempre, hayan emitido o no: resolutionDate y
technicalKey. Ninguna de las dos viaja dentro de un documento, así que
cambiarlas no desmiente nada de lo que ya salió.
Lo que no deja hacer
Sección titulada «Lo que no deja hacer»El API revisa cómo quedaría el rango después del cambio, no cómo estaba. Así que no puedes dejar sin resolución uno que la necesita, ni por borrarla ni por cambiarle el tipo:
| Qué intentas | Respuesta |
|---|---|
validUntil: null en un INVOICE, SUPPORT_DOCUMENT o EQUIVALENT_POS |
422 — Solo las notas de crédito y débito van sin resolución. Este rango tiene que citar la suya y su vigencia: viajan dentro de cada documento que ampare. |
documentType: "INVOICE" en una serie de notas que no tiene resolución |
El mismo 422: quedaría una resolución de facturación sin resolución. |
| Corregir mientras otro cambia el mismo rango | 422 — Alguien cambió esta resolución mientras la corregías. Vuelve a abrirla y comprueba cómo quedó. |
ultimoEmitido también viaja por aquí, y esta es la única ruta que lo
deja bajar — /ultimo-emitido solo sube. Para bajarlo la exigencia es
aún más cerrada, y con su propio 422: que de este rango no haya salido
ningún número, cuente el documento como cuente hoy. Un FAILED o un
descartado que alcanzó a llevar número también lo impiden, porque ese número
ya se gastó y bajar el contador lo volvería a entregar —
De esta resolución ya salieron números, así que el contador no puede bajar: se volverían a entregar. Registra la resolución correcta y retira esta.
En sandbox está cerrado, igual que el POST: la numeración es compartida y
no se administra desde el API.
Retirar un rango de circulación
Sección titulada «Retirar un rango de circulación»PATCH /v1/numbering-ranges/{id}/actividad{ "isActive": false }Un rango retirado deja de entregar consecutivos: POST /v1/documents
responde 422 RANGE_INACTIVE y el rango desaparece de las listas de
resoluciones utilizables. No se pierde nada — el consecutivo se queda
donde está, y reactivarlo ("isActive": true) lo continúa desde ahí, no
desde el principio.
Es la salida de una resolución mal cargada que ya emitió, y lo que hace un emisor con la resolución del proveedor anterior una vez ha migrado.
Continuar la numeración de otro sistema
Sección titulada «Continuar la numeración de otro sistema»PATCH /v1/numbering-ranges/{id}/ultimo-emitido{ "ultimoEmitido": 1253 }Un rango recién registrado arranca en rangeFrom. Si el emisor ya emitió
con esa misma resolución en otro sistema, hay que decirlo o los primeros
documentos saldrían con números que ya usó.
ultimoEmitido es el último gastado, no el próximo: es el mismo
significado que currentNumber, para que no haya dos formas de decir lo
mismo. Con 1253 en un rango que empieza en 1000, el siguiente documento
sale con el 1254.
Sandbox
Sección titulada «Sandbox»En sandbox POST /v1/numbering-ranges está cerrado:
{ "status": 422, "code": "VALIDATION", "message": "En sandbox la numeración es compartida y no se administra desde aquí. Usa el rango que ya viene listado.", "errors": []}El rango de sandbox es uno solo para todos los integradores. GET sigue
funcionando y te lo devuelve: ese es el id que mandas en tus pruebas.
Sincronizar con la DIAN
Sección titulada «Sincronizar con la DIAN»POST/v1/numbering-ranges/sync-dian
Sin cuerpo. Le pregunta a la DIAN por las resoluciones autorizadas de tu emisor y las guarda con su clave técnica.
Es la forma recomendada de cargar numeración de facturación. Registrarla a mano es copiar ocho datos de un PDF, y el que más duele equivocarse es la clave técnica: un carácter cambiado no se nota al guardar —el rango se ve perfecto— sino cuando la DIAN rechaza la primera factura.
Antes de llamar a la DIAN, el API exige tres cosas del emisor: credenciales
de software (softwareId y PIN), certificado digital activo y proveedor
configurado.
curl -X POST https://facturacion.misofk.com/v1/numbering-ranges/sync-dian \ -H "Authorization: Bearer $TOKEN"{ "encontradas": 2, "creadas": 1, "actualizadas": 1, "sinCambios": 0, "conflictos": 0, "sinClave": 0, "rangos": [ { "prefix": "SETP", "resolutionNumber": "18764092807123", "rangeFrom": "990000000", "rangeTo": "990005000", "validUntil": "2027-01-15", "resultado": "clave-actualizada", "tieneClaveTecnica": true }, { "prefix": "FE", "resolutionNumber": "18764092811987", "rangeFrom": "1", "rangeTo": "5000", "validUntil": "2027-06-30", "resultado": "creado", "tieneClaveTecnica": true } ], "motivo": null}| Campo | Tipo | Descripción |
|---|---|---|
encontradas |
number | Cuántas resoluciones devolvió la DIAN. |
creadas |
number | Rangos nuevos dados de alta. |
actualizadas |
number | Rangos que ya existían y a los que se les corrigió la clave técnica. |
sinCambios |
number | Rangos que ya existían con la misma clave. |
conflictos |
number | Las que no se pudieron guardar. Cada una dice por qué en su fila. |
sinClave |
number | Las que la DIAN devolvió sin clave técnica en esta respuesta. Las nuevas no sirven para facturar; las que ya existían conservan la suya. |
rangos |
array | Una entrada por resolución. |
motivo |
string | null | Lo que dijo la DIAN cuando no mandó ninguna. null si vinieron rangos. |
Cada entrada de rangos trae prefix, resolutionNumber, rangeFrom,
rangeTo, validUntil, tieneClaveTecnica y un resultado que vale
creado, clave-actualizada, sin-cambios o conflicto.
conflicto es la fila que hay que mirar. El sync recorre las
resoluciones una por una y no se detiene cuando una falla, así que la
respuesta puede venir con rangos creados y una resolución que se quedó
fuera. Esa trae además un motivo propio, con la razón concreta:
{ "prefix": "FE", "resolutionNumber": "18764092811987", "rangeFrom": "1", "rangeTo": "5000", "validUntil": "2027-06-30", "resultado": "conflicto", "motivo": "Ese rango se pisa con la resolución 18764092800001, que va del 1 al 3000 con el mismo prefijo y ya tiene números comprometidos. Dos documentos no pueden llevar el mismo número.", "tieneClaveTecnica": true}No lo confundas con el motivo de arriba, que es del nivel de la respuesta
y solo habla de cuando la DIAN no devolvió ninguna.
Qué crea y qué no toca
Sección titulada «Qué crea y qué no toca»Crea siempre rangos INVOICE. La operación que se consulta responde por
las resoluciones de facturación; las notas, el documento soporte y el
equivalente POS se registran aparte, a mano, con POST /v1/numbering-ranges.
De un rango que ya existe solo corrige la clave técnica, y solo si cambió. El consecutivo actual, los números y las fechas se quedan como están. Es a propósito: una ampliación de rango llega de la DIAN como una resolución nueva, con su propia clave técnica, no como un cambio de la anterior.
Cuando la DIAN no devuelve nada
Sección titulada «Cuando la DIAN no devuelve nada»encontradas: 0 con rangos: [] no siempre significa «no tienes
resoluciones». El API te pasa la explicación de la DIAN, literal, en
motivo:
{ "encontradas": 0, "creadas": 0, "actualizadas": 0, "sinCambios": 0, "conflictos": 0, "sinClave": 0, "rangos": [], "motivo": "No registra prefijos asociados al código de software: beec7513-..."}Ese caso concreto no se arregla aquí: hay que asociarle los prefijos al software en el portal de la DIAN.
Errores
Sección titulada «Errores»| Código | code |
Cuándo |
|---|---|---|
422 |
VALIDATION |
Falta registrar el software (softwareId y PIN) antes de consultar la numeración |
422 |
CERT_MISSING |
El emisor no tiene certificado activo |
422 |
CERT_EXPIRED |
El certificado está vencido |
422 |
VALIDATION |
En sandbox: la numeración es compartida y no se sincroniza |
Los dos de certificado aparecen porque la consulta a la DIAN va firmada.
La clave técnica
Sección titulada «La clave técnica»Es el dato de la resolución que entra en el cálculo del CUFE:
CUFE = SHA-384(NumFac + FecFac + HorFac + ValFac + '01' + ValIVA + '04' + ValINC + '03' + ValICA + ValTot + NitOFE + NumAdq + ClTec + TipoAmbiente)ClTec es la clave técnica del rango. No hay forma de calcular un CUFE sin
ella, y sin CUFE no hay factura: es el identificador con el que la DIAN
recibe el documento y con el que se arma el QR.
Solo la factura la usa. El CUDE de las notas y del equivalente POS, y el
CUDS del documento soporte, se calculan con el PIN del software en su
lugar. Por eso un rango CREDIT_NOTE sin clave técnica es perfectamente
normal, y uno INVOICE sin clave técnica es una bomba de tiempo.
Cómo se consume el consecutivo
Sección titulada «Cómo se consume el consecutivo»El número se toma con una operación atómica: dos facturas simultáneas nunca
reciben el mismo. currentNumber es el último consumido, y "0" significa
que no se ha usado ninguno — el primero que sale es rangeFrom.
Cuando el rango no puede entregar número
Sección titulada «Cuando el rango no puede entregar número»Son cuatro cosas, y no salen todas por el mismo sitio. Tres se
comprueban al crear el documento —con el cajero todavía delante de la
pantalla— y vuelven en el 422 de la respuesta del POST:
code |
message |
Cuándo |
|---|---|---|
RANGE_INACTIVE |
Esa resolución está retirada y no se puede facturar con ella. |
isActive: false |
RANGE_EXPIRED |
La resolución de numeración está vencida |
validUntil anterior a hoy |
VALIDATION |
La resolución es válida desde {validFrom} |
Todavía no empieza a regir |
Los dos primeros traen el detalle en errors[0] con
field: "numbering_range_id": el rango citado —por su prefijo cuando no
tiene resolución— y Venció el {validUntil}. El tercero lo dice todo en el
message.
Los dos de la vigencia no le pueden pasar a un rango de notas con las fechas
en null: sin fechas no hay nada que comparar y la comprobación se salta.
Un rango así solo deja de entregar números si lo retiras o si se agota.
La cuarta, el agotamiento, no se sabe hasta pedir el número, y eso pasa
ya dentro de la cola. El documento se acepta con 201, queda en QUEUED, y
el fallo aparece en segundo plano — el mismo camino que la clave técnica que
falta:
code |
message |
Cuándo |
|---|---|---|
RANGE_EXHAUSTED |
El rango de numeración está agotado |
No queda número dentro de rangeTo |
Ahí el documento pasa a FAILED, el mensaje queda en
dian_errors[0].message, y sale el webhook document.failed:
{ "event": "document.failed", "data": { "id": "1c4f8e0b-6d52-4a19-8f77-2b90cae41d33", "reference_code": "FV-1042", "error": "El rango de numeración está agotado" }, "sent_at": "2026-09-04T14:22:31.006Z"}Por ahí solo viaja la frase. No hay code, no hay errors[] y no hay
field: "numbering_range_id": el detalle que acompaña al error —«Último
número autorizado: 990005000»— se pierde al marcar el documento.
A cambio, estos cuatro no se comen el consecutivo: el número no llega a tomarse. El reintento del documento vuelve a pedirlo, y volverá a fallar mientras el rango siga como está.
Además del rango, el POST rechaza en el momento el del tipo equivocado
que viste al principio de esta página. Ese se comprueba antes que los tres de
arriba, así que un rango retirado y del tipo que no toca se queja primero
del tipo.
De los cuatro, los dos que no se arreglan tocando el rango son vencido y agotado: hay que pedirle numeración nueva a la DIAN, y eso no es instantáneo. Son los únicos fallos del API cuya solución vive fuera del software.
Vencido y agotado son independientes. Una resolución puede vencer con 4.000 números sin usar, y puede agotarse un año antes de su fecha de vencimiento.
Un rango de notas no vence, y cuando se agota la salida no pasa por la DIAN: registras el siguiente tramo con el mismo prefijo y sigues. Es el único agotamiento que se resuelve el mismo día.
Cuánto queda
Sección titulada «Cuánto queda»Tienes tres formas de saberlo, y las tres vienen en el mismo objeto.
usagePercent — el porcentaje consumido, con un decimal. Lo más rápido
de graficar.
proximoNumero — con qué número sale el siguiente documento, o null
si ya no sale ninguno. Es la respuesta a «¿en qué voy?», que no es lo mismo
que currentNumber: un rango recién cargado muestra currentNumber: "0",
que se lee como si el consecutivo fuera cero cuando en realidad la próxima
factura va a salir con 990000000.
Los números que faltan, si los quieres en unidades:
quedan = rangeTo − proximoNumero + 1Con el rango del ejemplo: 990005000 − 990000124 + 1 = 4877. Cuando
proximoNumero es null, quedan cero.
El API te avisa por su cuenta en dos momentos, pero los dos son avisos para el dueño del emisor, no para tu integración: se deja una advertencia en el registro del servidor al pasar del 90 % consumido, y sale un correo cuando al emisor le quedan 500 consecutivos o menos sumando todos sus rangos activos y vigentes.
Vigilancia
Sección titulada «Vigilancia»Un rango se agota en mitad de una venta, no en mitad de un despliegue. Y una resolución nueva de la DIAN no se consigue el mismo día. Lo que hay que mirar, en orden de qué tan tarde te enteras si no lo miras:
hasTechnicalKey de todo rango INVOICE. Revísalo el día que cargas el
rango, no el día que facturas con él. Es un booleano; si viene en false,
esa numeración no puede emitir facturas aunque todo lo demás se vea bien.
usagePercent de los rangos vigentes, a diario. Pon tu alerta en el
80 %, no en el 90 %: el 90 % ya es tarde para tramitar una resolución. Con
un rango de 5.000, el 80 % te deja 1.000 facturas de margen.
validUntil contra hoy más 30 días. Una resolución vencida no entrega
números aunque le sobren miles. Es el caso que más sorprende, porque
usagePercent se ve tranquilo hasta el último día. En un rango de notas
viene en null y no hay nada que vigilar: ahí lo único que se acaba es el
tramo, y eso lo ves en usagePercent.
isActive. Un rango inactivo tampoco entrega números, y no da ninguna
señal previa: la primera noticia es un documento en FAILED.
El webhook document.failed. Es donde aparecen los fallos que no
alcanzas a ver en la respuesta del POST: el rango agotado y la clave
técnica que falta. Los otros tres del rango —retirado, vencido y «válida
desde»— te los dice el POST en su 422, y solo caen aquí si el rango
cambia entre aceptar el documento y numerarlo. La diferencia entre el
agotamiento y la clave está en el consecutivo: el agotado se levanta al pedir
el número, así que no llega a gastarlo; la clave técnica falla cuando el
número ya está guardado, y ese sí se consumió.
sinClave después de cada sync-dian. Si es mayor que cero, la DIAN
devolvió resoluciones sin clave técnica en esa respuesta. Las que
además salen con resultado: "creado" son las que no sirven para
facturar: se dieron de alta sin clave. Las que ya existían no pierden
nada —una respuesta sin TechnicalKey nunca borra la clave guardada—, así
que pueden seguir emitiendo aunque cuenten aquí. Para saber si un rango
puede emitir, mira hasTechnicalKey en GET /v1/numbering-ranges, que es
el que refleja la clave realmente guardada.
Una regla que ahorra el susto: cuando pidas una resolución nueva, cárgala
antes de que se agote la anterior. Nada impide que un emisor tenga dos
rangos INVOICE vigentes a la vez; el que se usa es el del
numbering_range_id que mandes, así que el cambio de uno a otro lo decides
tú y puedes hacerlo un martes.
Después de esto
Sección titulada «Después de esto»- Errores — todos los códigos, y cuáles se reintentan
- Primera factura — el rango en su contexto
- Ambientes y URLs — sandbox, habilitación y producción