Certificado, software y habilitación
Un emisor recién creado todavía no puede facturar. Le faltan tres cosas, y van en este orden:
- El certificado digital. Con él se firma el XML de cada documento.
- El software. El
softwareIdy el PIN que la DIAN le asigna al emisor: son insumo del CUDE y del código de seguridad del software. - La habilitación. El set de pruebas ante la DIAN, que es lo que le da efecto fiscal a lo que emitas después.
El orden no es decorativo: el set de pruebas se firma con el certificado y
se identifica con el software. Empezar por el final da errores que parecen
de otra cosa —la DIAN rechaza el ZIP sin explicar por qué— así que primero
el .p12, después el software, y solo entonces el set.
1. El certificado digital
Sección titulada «1. El certificado digital»Qué archivo se sube
Sección titulada «Qué archivo se sube»Un PKCS#12 (.p12) expedido por una entidad de certificación digital
acreditada por la ONAC, a nombre del NIT del emisor. Es el mismo archivo
con el que se firman los XML que van a la DIAN.
El binario y su contraseña se guardan cifrados con AES-256-GCM, y ninguno de los dos vuelve a salir por el API. Los endpoints devuelven los datos del certificado, nunca el archivo ni la clave.
POST/v1/company/certificate
multipart/form-data, dos campos:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
file |
archivo | Sí | El .p12. |
password |
string | Sí | La contraseña con la que se abre el .p12. |
curl -X POST https://sandbox.facturacion.misofk.com/v1/company/certificate \ -H "Authorization: Bearer $TOKEN" \ -F "file=@el-poblado.p12" \ -F "password=la-contrasena-del-p12"Respuesta:
{ "id": "6f0c1d84-2a7b-4e19-9f52-1c8de3b47a06", "subjectDn": "CN=DISTRIBUCIONES EL POBLADO SAS, serialNumber=901456789, O=DISTRIBUCIONES EL POBLADO SAS, L=MEDELLIN, C=CO", "issuerDn": "CN=AC SUB ENTIDAD DE CERTIFICACION DIGITAL, O=ENTIDAD DE CERTIFICACION DIGITAL, C=CO", "serialNumber": "4f2a9c1b8e30", "validFrom": "2026-02-11T00:00:00.000Z", "validUntil": "2027-02-11T23:59:59.000Z"}| Campo | Tipo | Descripción |
|---|---|---|
id |
uuid | Id del certificado guardado. |
subjectDn |
string | Asunto del certificado: quién es el titular. |
issuerDn |
string | Quién lo expidió. |
serialNumber |
string | Serie del certificado, no el NIT. |
validFrom |
fecha | Desde cuándo es válido. |
validUntil |
fecha | Cuándo vence. |
Cargar un certificado es la acción más delicada del emisor —cambia con qué firma— así que queda en la auditoría: quién lo subió, la serie y la fecha de vencimiento. El archivo y la contraseña no se registran en ningún lado.
Cómo se valida contra el NIT
Sección titulada «Cómo se valida contra el NIT»Firmar con el certificado de otra empresa hace que la DIAN rechace todos los documentos, y el rechazo no dice que el problema sea el certificado. Por eso se comprueba al subirlo, con esta regla:
- Se busca
serialNumber=en el asunto. Si está y no coincide con el NIT del emisor, se rechaza conCERT_NIT_MISMATCH. - La comparación es tolerante en los dos sentidos —uno contiene al otro— porque el NIT puede venir con el dígito de verificación pegado o sin él.
- Si no hay
serialNumber=, se busca el NIT entre los números de seis o más dígitos del asunto: algunas autoridades lo meten en elCNo en elOU. - Si no aparece por ningún lado, se deja pasar y queda un aviso en el log.
Ese último punto es a propósito: en un asunto real conviven el teléfono (diez dígitos) y el código postal (seis), y los dos se parecen a un NIT. Bloquear a un cliente por un formato que no reconocemos es peor que el problema que se quiere evitar. Solo se rechaza cuando se puede demostrar que el certificado es de otro.
Consultarlo
Sección titulada «Consultarlo»GET/v1/company/certificate
Devuelve el certificado activo, con dos campos más que el alta:
{ "id": "6f0c1d84-2a7b-4e19-9f52-1c8de3b47a06", "alias": "principal", "subjectDn": "CN=DISTRIBUCIONES EL POBLADO SAS, serialNumber=901456789, O=DISTRIBUCIONES EL POBLADO SAS, L=MEDELLIN, C=CO", "issuerDn": "CN=AC SUB ENTIDAD DE CERTIFICACION DIGITAL, O=ENTIDAD DE CERTIFICACION DIGITAL, C=CO", "serialNumber": "4f2a9c1b8e30", "validFrom": "2026-02-11T00:00:00.000Z", "validUntil": "2027-02-11T23:59:59.000Z", "createdAt": "2026-02-12T15:04:22.318Z"}alias viene siempre principal: no se puede elegir por HTTP.
Sin certificado activo responde 404 NOT_FOUND con
No hay certificado activo. Es la forma rápida de saber si un emisor está
listo, y de ver cuánto le queda antes de vencer.
Errores al cargarlo
Sección titulada «Errores al cargarlo»| HTTP | code |
Cuándo |
|---|---|---|
422 |
VALIDATION |
Falta el archivo .p12 (campo "file") |
422 |
VALIDATION |
Falta la contraseña del certificado (campo "password") |
422 |
VALIDATION |
No se pudo leer el .p12: archivo corrupto o contraseña incorrecta |
422 |
VALIDATION |
El .p12 no contiene clave privada |
422 |
VALIDATION |
El .p12 no contiene certificados |
422 |
CERT_NIT_MISMATCH |
El titular es otro NIT |
422 |
CERT_EXPIRED |
El .p12 ya venía vencido |
CERT_NIT_MISMATCH trae en errors[] el asunto completo del certificado,
para que se vea de quién es sin tener que abrir el archivo:
{ "status": 422, "code": "CERT_NIT_MISMATCH", "message": "El certificado es de 900123456, no del NIT 901456789. Firmar con el certificado de otra empresa hace que la DIAN rechace todos sus documentos.", "errors": [ { "field": "certificate", "message": "Asunto del certificado: CN=COMERCIALIZADORA DEL NORTE SAS, serialNumber=900123456, C=CO" } ]}Qué pasa cuando vence
Sección titulada «Qué pasa cuando vence»El certificado se comprueba cada vez que se usa, no solo al subirlo. Antes de firmar se carga el activo, y ahí salen los dos errores que hay que tener contemplados:
| HTTP | code |
Mensaje |
|---|---|---|
422 |
CERT_MISSING |
La company no tiene certificado activo |
422 |
CERT_EXPIRED |
El certificado está vencido |
Los dos aparecen en más sitios de los que uno espera, porque todo lo que habla con la DIAN carga el certificado: la emisión de documentos, la sincronización de numeración y los tres endpoints de habilitación.
Renovarlo es subir el nuevo .p12 al mismo endpoint. Solo hay un
certificado activo por emisor: al cargar uno, el anterior queda
desactivado en la misma operación. No existe DELETE ni endpoint de
reemplazo — subir otro es todo el procedimiento.
2. El software
Sección titulada «2. El software»Qué son el softwareId y el PIN
Sección titulada «Qué son el softwareId y el PIN»El modelo es software propio: el emisor registra su facturador en el portal de habilitación de la DIAN y de ahí salen tres valores.
| Valor | Qué es |
|---|---|
softwareId |
El identificador que la DIAN le asigna al software. Es un GUID. |
pin |
El PIN que el emisor elige al registrar el software. |
testSetId |
El GUID del set de pruebas. Solo existe en habilitación. |
No los inventa el API ni los expide MISOFK: se copian del portal de la DIAN tal cual. Sirven para tres cosas concretas, todas dentro del documento firmado:
- El CUDE de las notas crédito, las notas débito y el documento equivalente POS. Es el mismo hash del CUFE, pero con el PIN en lugar de la clave técnica del rango.
- El CUDS del documento soporte, que también lleva el PIN.
- El
SoftwareSecurityCode, que esSHA-384(softwareId + PIN + número del documento)y va en el XML de todos los documentos.
Además, el softwareId es el código con el que se le consulta la
numeración a la DIAN.
Registrarlo
Sección titulada «Registrarlo»PUT/v1/company/software
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
softwareId |
string | Sí | El GUID que asignó la DIAN. No puede ir vacío, máximo 50 caracteres. |
pin |
string | Sí | Se guarda cifrado. No puede ir vacío. |
testSetId |
string | No | GUID del set de pruebas, máximo 50 caracteres. Si lo omites, se conserva el que ya hubiera. |
environment |
string | Sí | HABILITACION o PRODUCCION. |
curl -X PUT https://sandbox.facturacion.misofk.com/v1/company/software \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "softwareId": "1f8a2c9e-4b31-4d6a-9c77-5e2f0a3b81d4", "pin": "12345", "testSetId": "8b7d0c31-9f24-4a58-b0e6-3d1c74e9a2f5", "environment": "HABILITACION" }'Es un upsert por emisor y ambiente: la segunda llamada con
HABILITACION pisa la primera, y PRODUCCION es una fila aparte. Cuando
la DIAN habilite al emisor y le entregue las credenciales de producción,
se registran con otra llamada — la de habilitación no se toca.
Respuesta (el PIN cifrado nunca sale):
{ "id": "b3d9f0a1-5c48-4a72-9e10-7d2b6c4f8e35", "companyId": "2c5e7a90-1b34-4d68-8f27-9a0c3e5b7d14", "softwareId": "1f8a2c9e-4b31-4d6a-9c77-5e2f0a3b81d4", "encryptionKeyId": "k1", "testSetId": "8b7d0c31-9f24-4a58-b0e6-3d1c74e9a2f5", "environment": "HABILITACION", "enablementStatus": "NOT_STARTED", "enablementResults": null, "createdAt": "2026-02-12T15:11:03.902Z", "updatedAt": "2026-02-12T15:11:03.902Z"}| Campo | Tipo | Descripción |
|---|---|---|
encryptionKeyId |
string | Con qué clave maestra se cifró el PIN. Sirve para la rotación de claves. |
enablementStatus |
string | En qué va la habilitación de este ambiente. |
enablementResults |
objeto | Lo que ha contestado la DIAN del set de pruebas. null hasta el primer envío. |
Consultarlo
Sección titulada «Consultarlo»GET/v1/company/software
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
environment |
query | No | HABILITACION o PRODUCCION. Por defecto HABILITACION. |
curl "https://sandbox.facturacion.misofk.com/v1/company/software?environment=PRODUCCION" \ -H "Authorization: Bearer $TOKEN"Si ese ambiente no tiene software registrado:
{ "status": 422, "code": "SOFTWARE_NOT_CONFIGURED", "message": "La company no tiene software DIAN configurado para PRODUCCION", "errors": []}Fíjate en el ambiente del mensaje antes de salir a buscar el error: lo más común es tener registrado el de habilitación y estar consultando el de producción, o al revés.
No hay DELETE. Un software registrado se corrige volviéndolo a registrar.
Los estados de enablementStatus
Sección titulada «Los estados de enablementStatus»| Valor | Qué significa |
|---|---|
NOT_STARTED |
Registrado, sin set de pruebas enviado. |
TEST_SET_SENT |
El ZIP se envió y la DIAN devolvió un zipKey. |
IN_PROGRESS |
Se consultó el avance y la DIAN todavía no aprueba el set. |
APPROVED |
La DIAN dijo que el set está aceptado. |
3. La habilitación
Sección titulada «3. La habilitación»El proceso lo define la DIAN (Resolución 165 de 2023, artículo 28) y
consiste en emitir un conjunto de documentos de prueba con tu propio
software hasta que la DIAN los dé por buenos. Con el emisor en
HABILITACION, la mecánica es:
-
Registra el software con su
testSetId, como en el paso anterior. -
Emite los documentos del set con el pipeline normal. Facturas y notas se crean con los mismos endpoints de siempre; el emisor está en
HABILITACION, así que salen firmadas y numeradas pero sin efecto fiscal. -
Mándalos al set con
POST /v1/enablement/send-emitted. -
Consulta el avance con
GET /v1/enablement/statushasta queapprovedseatrue.
La DIAN dice en su portal cuántos documentos pide el set y de qué tipos. El API no lo comprueba: manda lo que le des y te repite lo que la DIAN conteste.
Mandar lo que ya emitiste
Sección titulada «Mandar lo que ya emitiste»POST/v1/enablement/send-emitted
Es la vía recomendada. Arma el ZIP por ti: baja los XML firmados, les pone el nombre que exige la DIAN —con tu NIT y el prefijo de cada familia de documento— y lo envía.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
documentIds |
uuid[] | No | Qué documentos mandar. Sin él van todos los que estén firmados. |
curl -X POST https://sandbox.facturacion.misofk.com/v1/enablement/send-emitted \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "documentIds": [ "48409b5f-337e-468c-88eb-cc828fe09dd7", "7c1e0a63-9d52-4b18-8a37-2f6b45c0e9d1" ] }'{ "zip_key": "8f3c1b7a-4e29-4d06-9b58-1a7e2c0d5f43" }Solo entran los documentos que ya están firmados —en estado SIGNED,
SENT o VALIDATED— y que tengan su XML guardado. Un documento rechazado
no se manda: incluirlo no lo arregla y gasta un intento.
Si no hay ninguno:
{ "status": 422, "code": "VALIDATION", "message": "No hay documentos firmados para mandar al set. Emite primero las facturas y notas que pide la DIAN, con el emisor en HABILITACION.", "errors": []}Mandar un ZIP armado a mano
Sección titulada «Mandar un ZIP armado a mano»POST/v1/enablement/run
multipart/form-data. Sirve cuando el ZIP lo armaste tú por fuera.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
file |
archivo | Sí | ZIP con los XML firmados del set. |
fileName |
string | No | Nombre del ZIP. Si falta se usa el del archivo subido, o testset.zip. |
Responde lo mismo: { "zip_key": "..." }. La diferencia es que aquí los
nombres de archivo dentro del ZIP corren por tu cuenta, y un nombre mal
puesto vuelve como un rechazo que no dice cuál de los treinta venía mal.
Consultar el avance
Sección titulada «Consultar el avance»GET/v1/enablement/status
{ "zip_key": "8f3c1b7a-4e29-4d06-9b58-1a7e2c0d5f43", "approved": false, "set_status": "Set de prueba con identificador 8b7d0c31-9f24-4a58-b0e6-3d1c74e9a2f5 se encuentra en Proceso.", "batch_valid": true, "accepted_count": 1, "documents": [ { "isValid": true, "statusCode": "00", "statusDescription": "Procesado Correctamente.", "documentKey": "a1b2c3d4e5f6...", "errors": [] } ]}| Campo | Tipo | Descripción |
|---|---|---|
zip_key |
string | El identificador del último ZIP enviado. |
approved |
boolean | Si la DIAN dio el set entero por aceptado. |
set_status |
string | null | Lo que dice la DIAN del set, con sus palabras. |
batch_valid |
boolean | Si todos los documentos de esa tanda pasaron. |
accepted_count |
número | Cuántos documentos van aceptados, acumulados entre envíos. |
documents |
array | Resultado por documento. |
Cada entrada de documents trae isValid, statusCode,
statusDescription, documentKey (el CUFE o CUDE del documento) y
errors, que son los de la DIAN, literales.
Los resultados se acumulan entre envíos. Cada consulta trae solo la
tanda que respondió la DIAN, pero documents y accepted_count conservan
lo que ya se sabía: mandar una tanda nueva no hace que el avance parezca
empezar de cero.
Si todavía no has enviado nada:
{ "status": 422, "code": "VALIDATION", "message": "Aún no se ha enviado el set de pruebas", "errors": []}Errores de los tres endpoints
Sección titulada «Errores de los tres endpoints»| HTTP | code |
Cuándo |
|---|---|---|
422 |
VALIDATION |
El set de pruebas solo aplica en ambiente HABILITACION |
422 |
SOFTWARE_NOT_CONFIGURED |
No hay software registrado para HABILITACION |
422 |
SOFTWARE_NOT_CONFIGURED |
Falta el testSetId del set de pruebas (portal de habilitación DIAN) |
422 |
CERT_MISSING / CERT_EXPIRED |
El emisor no tiene certificado activo, o está vencido |
422 |
VALIDATION |
La DIAN no aceptó el set de pruebas, con sus errores en errors[] |
Cuando la DIAN no acepta el ZIP, el intento se guarda antes de que te
llegue el error: el estado vuelve a NOT_STARTED si no hubo zipKey, y el
detalle queda en enablementResults. Así el error no se pierde al cortar
la respuesta.
Lo que la habilitación no hace sola
Sección titulada «Lo que la habilitación no hace sola»Que el set quede APPROVED no mueve al emisor a producción. Lo único que
se escribe al aprobarse es enablementStatus: "APPROVED" en la
configuración de software de HABILITACION.
El paso a producción lo hace la plataforma, cambiando el environment
del emisor. No hay endpoint del inquilino que lo haga, y es deliberado: el
ambiente decide a qué servicio de la DIAN se transmite y con qué valor se
calcula el CUFE. Cuando tengas el set aprobado, escríbenos con las
credenciales de producción del portal DIAN.
Los dos ambientes DIAN
Sección titulada «Los dos ambientes DIAN»El emisor vive en uno de dos ambientes: HABILITACION (el que trae por
defecto) o PRODUCCION. No confundas esto con el ambiente del API —sandbox,
QA o producción—: son dos ejes distintos, y está explicado en
Ambientes y URLs.
Esto es lo que cambia entre uno y otro:
| Qué | HABILITACION |
PRODUCCION |
|---|---|---|
cbc:ProfileExecutionID del XML |
2 |
1 |
TipoAmbiente dentro del CUFE, el CUDE y el CUDS |
2 |
1 |
| Servicio de la DIAN | https://vpfe-hab.dian.gov.co/WcfDianCustomerServices.svc |
https://vpfe.dian.gov.co/WcfDianCustomerServices.svc |
| Base de la URL del QR | https://catalogo-vpfe-hab.dian.gov.co |
https://catalogo-vpfe.dian.gov.co |
Software (softwareId, PIN, testSetId) |
Su propia fila | Su propia fila |
| Set de pruebas | Permitido | 422 |
Proveedor simulado (MOCK) |
Permitido | Prohibido |
Las dos direcciones de la DIAN son las de fábrica y se pueden apuntar a otro sitio desde la configuración del despliegue.
El ambiente entra en el CUFE
Sección titulada «El ambiente entra en el CUFE»Esta es la consecuencia que sorprende: el ambiente es uno de los ingredientes del hash. La misma factura, con el mismo número, el mismo NIT y los mismos montos, da un CUFE distinto en habilitación y en producción. Un documento emitido en habilitación no se «pasa» a producción: se emite otra vez.
Y en la URL del QR
Sección titulada «Y en la URL del QR»La URL del QR se arma con la base del ambiente y el código único del documento:
https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey={cufe}https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey={cufe}El ambiente lo fija la plataforma y el emisor no lo puede cambiar con
PUT /v1/company. Además queda grabado en cada documento, así que un
emisor que pasó a producción conserva sus documentos de habilitación
marcados como lo que son.
Resumen: los cuatro errores de esta página
Sección titulada «Resumen: los cuatro errores de esta página»code |
HTTP | Se arregla |
|---|---|---|
CERT_MISSING |
422 |
Subiendo el .p12 |
CERT_EXPIRED |
422 |
Subiendo el .p12 renovado |
CERT_NIT_MISMATCH |
422 |
Con el .p12 del NIT correcto |
SOFTWARE_NOT_CONFIGURED |
422 |
Registrando softwareId, PIN y —para el set— testSetId |
Los cuatro son de configuración del emisor, no de tu código: reintentar la misma petición da exactamente el mismo error. La lista completa de códigos está en Errores.