Ir al contenido

Certificado, software y habilitación

Un emisor recién creado todavía no puede facturar. Le faltan tres cosas, y van en este orden:

  1. El certificado digital. Con él se firma el XML de cada documento.
  2. El software. El softwareId y el PIN que la DIAN le asigna al emisor: son insumo del CUDE y del código de seguridad del software.
  3. 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.

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.
Ventana de terminal
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.

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:

  1. Se busca serialNumber= en el asunto. Si está y no coincide con el NIT del emisor, se rechaza con CERT_NIT_MISMATCH.
  2. 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.
  3. 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 el CN o en el OU.
  4. 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.

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.

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"
}
]
}

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.

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 es SHA-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.

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.
Ventana de terminal
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.

GET/v1/company/software

Parámetro Tipo Obligatorio Descripción
environment query No HABILITACION o PRODUCCION. Por defecto HABILITACION.
Ventana de terminal
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.

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.

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:

  1. Registra el software con su testSetId, como en el paso anterior.

  2. 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.

  3. Mándalos al set con POST /v1/enablement/send-emitted.

  4. Consulta el avance con GET /v1/enablement/status hasta que approved sea true.

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.

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.
Ventana de terminal
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": []
}

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.

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": []
}
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.

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.

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.

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.

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.

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.