📖 Guía Paso a Paso: Tu Primera Emisión de Certificado
En esta guía aprenderás a emitir tu primer certificado digital usando MATICERTS. Seguiremos un flujo completo desde la autenticación hasta la descarga del certificado (.zip con el .p12 y su PIN).
Todos los ejemplos usan Sandbox (https://sandbox-api.maticerts.com/api/v1). En Producción la base es https://api.maticerts.com/api/v1.
📋 Requisitos Previos
Antes de comenzar, asegúrate de tener:
- ✅ Una cuenta en MATICERTS activa, con el email verificado y la empresa activa (si no, el login se rechaza)
- ✅ Un Personal Access Token (PAT) o credenciales de usuario
- ✅ Saber qué tipo de emisión vas a usar:
BIOMETRIC(con verificación biométrica de identidad) oDOCUMENTARY(documental, sin biometría). Los tipos que puede usar tu empresa dependen decompany.issuance_provider(ver el Paso 1) - ✅ Cupo disponible para ese tipo de emisión y para la vigencia que vas a pedir, de 1 o 2 años. Consúltalo con
GET /api/v1/quota/status: enby_flow.BIOMETRICyby_flow.DOCUMENTARY,available_1_yearyavailable_2_yearson los certificados que puedes pedir para cada vigencia (pospago más prepago). Si no tienes cupo para ese tipo y esa vigencia, la creación de la solicitud responde402; puedes comprar certificados del mismo tipo conPOST /api/v1/orders - ✅ Una herramienta para hacer peticiones HTTP (cURL, Postman o tu lenguaje favorito)
- ✅ Datos del titular: NIT o cédula sin dígito de verificación (
dni), razón social, dirección, país y ciudad, tipo de organización y, si es Persona Jurídica, tipo de documento constitutivo - ✅ Datos del representante legal: tipo y número de documento y nombre. En una solicitud
BIOMETRIC, también nombres y apellidos por separado y email - ✅ Aceptar la versión vigente de los Términos y Condiciones (
accept_terms+terms_version_id) - ✅ En una solicitud
DOCUMENTARY: 2 a 3 soportes enviados en Base64, por defecto de máximo 7 MB cada uno y 10 MB en total (se recomiendan PDF, JPG o PNG; el servidor no valida el tipo de archivo)
Paso 1️⃣: Obtén tu Token de Acceso
Opción A: Usando Personal Access Token (PAT)
Si ya tienes un PAT, puedes usarlo directamente; es el método recomendado para integraciones backend.
Ten en cuenta que los PAT caducan a los 90 días de su creación. Pasado ese plazo, toda petición con el token responde 401 con "message": "No autenticado.".
Al crear un PAT puedes enviar expires_in_days (de 1 a 365): ese valor define la fecha expires_at que muestran la respuesta y GET /api/v1/tokens. El token es válido 90 días desde su creación en todos los casos: si pediste más, responde 401 desde el día 90; si pediste menos, sigue autenticando hasta el día 90 aunque ya no aparezca en GET /api/v1/tokens. Planifica la rotación a los 90 días.
- Créalos desde la aplicación web o con
POST /api/v1/tokens(máximo 10 intentos de creación por usuario en 24 horas, contando también los rechazados por validación; si superas el límite, la API responde429). El valor del token solo aparece en esa respuesta: guárdalo en un lugar seguro. - Consulta sus datos (
id,expires_at) conGET /api/v1/tokens; recuerda que la vigencia real es de 90 días desde la creación. - Antes de que venza, renuévalo con
POST /api/v1/tokens/{id}/renew: se emite un token nuevo y el anterior queda revocado, así que actualiza el valor en tu integración. El token renovado vale 90 días.
# Simplemente guarda tu PAT en una variable
export ACCESS_TOKEN="tu-pat-aqui"
Opción B: Autenticarse como Usuario
Si prefieres autenticarte con credenciales de usuario:
- cURL
- Node.js
- Python
curl -X POST "https://sandbox-api.maticerts.com/api/v1/auth/login" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "tu-email@empresa.com",
"password": "tu-contraseña"
}'
Respuesta esperada (200):
{
"success": true,
"message": "Inicio de sesión exitoso.",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
"expires_at": "2026-12-26 10:00:00",
"user": { "id": 40, "email": "tu-email@empresa.com", "name": "JUAN PÉREZ", "...": "..." },
"company": {
"has_agreement": false,
"company_type_id": 1,
"issuance_provider": "viafirma",
"company_name": "MI EMPRESA S.A.S.",
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
}
const response = await fetch('https://sandbox-api.maticerts.com/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
email: 'tu-email@empresa.com',
password: 'tu-contraseña'
})
});
const data = await response.json();
const accessToken = data.access_token;
console.log('Token válido hasta:', data.expires_at);
console.log('Tipos de emisión de la empresa:', data.company.issuance_provider); // viafirma | mail | dual
import requests
response = requests.post(
'https://sandbox-api.maticerts.com/api/v1/auth/login',
json={
'email': 'tu-email@empresa.com',
'password': 'tu-contraseña'
},
headers={'Accept': 'application/json'}
)
data = response.json()
access_token = data['access_token']
print('Token válido hasta:', data['expires_at'])
print('Tipos de emisión de la empresa:', data['company']['issuance_provider']) # viafirma | mail | dual
-
El token se envía en la cabecera
Authorization: Bearer <access_token>. Vence en la fecha deexpires_at(formatoY-m-d H:i:s, 90 días por defecto) y aparece enGET /api/v1/tokenscon tu email como nombre. -
company.issuance_providerindica qué tipos de emisión puede usar tu empresa y nunca esnull:viafirma: soloBIOMETRIC.mail: soloDOCUMENTARY.dual: los dos. Eliges el tipo en cada solicitud conissuance_flow; si no lo envías, la solicitud esDOCUMENTARY.
Si tu integración recibe un valor que no reconoce, no crees solicitudes y contacta a soporte.
companyes una foto del inicio de sesión: los valores vigentes deissuance_provideryhas_agreementestán enGET /api/v1/quota/status. -
Errores:
404conCredenciales inválidas., y también404cuando el usuario o la empresa están inactivos (elmessageindica cuál);400si el email no está verificado;422siemailopasswordfaltan o no son válidos.
Almacena el token en una variable de entorno para usarlo en los siguientes pasos:
export ACCESS_TOKEN="tu-token-aqui"
Paso 2️⃣: Verifica tu Token
Antes de continuar, verifica que tu token funciona correctamente consultando el usuario dueño del token:
- cURL
- Node.js
- Python
curl -X GET "https://sandbox-api.maticerts.com/api/v1/auth/user" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json"
Respuesta esperada (200):
{
"success": true,
"dataRecords": {
"data": [
{ "id": 40, "email": "tu-email@empresa.com", "name": "JUAN PÉREZ", "...": "..." }
]
}
}
const response = await fetch('https://sandbox-api.maticerts.com/api/v1/auth/user', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
});
const data = await response.json();
console.log('Usuario autenticado:', data.dataRecords.data[0]);
import requests
headers = {
'Authorization': f'Bearer {access_token}',
'Accept': 'application/json'
}
response = requests.get(
'https://sandbox-api.maticerts.com/api/v1/auth/user',
headers=headers
)
print('Usuario autenticado:', response.json()['dataRecords']['data'][0])
✅ Si recibes 200 con "success": true y tus datos en dataRecords.data[0] (un arreglo con un único elemento), tu token funciona. Si recibes 401 con "message": "No autenticado.", el token falta, es inválido, expiró o fue revocado.
Paso 3️⃣: Crea una Solicitud de Certificado
Antes de crear la solicitud:
- Consulta
GET /api/v1/terms/current(público) y guardadataRecords.id: lo enviarás comoterms_version_id. - Obtén los IDs de los catálogos con
GET /api/v1/countries,/cities,/identity-documents,/organization-typey/entity-document-types(ver Datos Maestros). - Verifica que tienes cupo para el tipo de emisión y la vigencia (
life) conGET /api/v1/quota/status:by_flow.<TIPO>.available_1_yearoby_flow.<TIPO>.available_2_year.
El ejemplo crea una solicitud BIOMETRIC (con verificación biométrica). issuance_flow es opcional: si tu empresa es viafirma puedes omitirlo; si es dual, envíalo, porque sin él la solicitud se crea DOCUMENTARY y te pedirá adjuntos.
- cURL
- Node.js
- Python
curl -X POST "https://sandbox-api.maticerts.com/api/v1/certificate-request" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"country_id": 45,
"city_id": 149,
"identity_document_id": 1,
"type_organization_id": 1,
"entity_document_type_id": 1,
"document_number": "1234567890",
"address": "Calle 123 # 45-67",
"legal_rep_first_name": "JUAN PABLO",
"legal_rep_last_name": "PÉREZ GÓMEZ",
"legal_rep_email": "juan.perez@empresa.com",
"company_name": "MI EMPRESA S.A.S.",
"dni": "900455420",
"mobile": "3001234567",
"life": 1,
"issuance_flow": "BIOMETRIC",
"accept_terms": true,
"terms_version_id": 3
}'
Respuesta esperada (200):
{
"success": true,
"message": "Solicitud de certificado creada exitosamente",
"dataRecords": {
"id": 636,
"company_id": 1,
"company_name": "MI EMPRESA S.A.S.",
"dni": "900455420",
"dv": 8,
"country_id": 45,
"city_id": 149,
"identity_document_id": 1,
"type_organization_id": 1,
"entity_document_type_id": 1,
"document_number": "1234567890",
"address": "Calle 123 # 45-67",
"legal_representative": null,
"legal_rep_first_name": "JUAN PABLO",
"legal_rep_last_name": "PÉREZ GÓMEZ",
"legal_rep_email": "juan.perez@empresa.com",
"info": "",
"mobile": "3001234567",
"phone": null,
"life": 1,
"issuance_flow": "BIOMETRIC",
"request_status": "PROCESSING",
"created_at": "08-10-2026 10:15:00 am",
"updated_at": "08-10-2026 10:15:00 am",
"created_at_formatted": "08-10-2026 10:15:00 am",
"updated_at_formatted": "08-10-2026 10:15:00 am",
"expiration_date_formatted": null,
"files": []
}
}
const requestData = {
country_id: 45,
city_id: 149,
identity_document_id: 1,
type_organization_id: 1,
entity_document_type_id: 1,
document_number: '1234567890',
address: 'Calle 123 # 45-67',
legal_rep_first_name: 'JUAN PABLO',
legal_rep_last_name: 'PÉREZ GÓMEZ',
legal_rep_email: 'juan.perez@empresa.com',
company_name: 'MI EMPRESA S.A.S.',
dni: '900455420',
mobile: '3001234567',
life: 1,
issuance_flow: 'BIOMETRIC',
accept_terms: true,
terms_version_id: 3
};
const response = await fetch(
'https://sandbox-api.maticerts.com/api/v1/certificate-request',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(requestData)
}
);
const data = await response.json();
console.log('Solicitud creada:', data);
const requestId = data.dataRecords.id;
import requests
request_data = {
'country_id': 45,
'city_id': 149,
'identity_document_id': 1,
'type_organization_id': 1,
'entity_document_type_id': 1,
'document_number': '1234567890',
'address': 'Calle 123 # 45-67',
'legal_rep_first_name': 'JUAN PABLO',
'legal_rep_last_name': 'PÉREZ GÓMEZ',
'legal_rep_email': 'juan.perez@empresa.com',
'company_name': 'MI EMPRESA S.A.S.',
'dni': '900455420',
'mobile': '3001234567',
'life': 1,
'issuance_flow': 'BIOMETRIC',
'accept_terms': True,
'terms_version_id': 3
}
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.post(
'https://sandbox-api.maticerts.com/api/v1/certificate-request',
json=request_data,
headers=headers
)
data = response.json()
print('Solicitud creada:', data)
request_id = data['dataRecords']['id']
dnies el NIT o la cédula del titular sin dígito de verificación (lo calcula el servidor).entity_document_type_ides obligatorio cuandotype_organization_ides1(Persona Jurídica).issuance_flow(opcional):BIOMETRICoDOCUMENTARY. Sin él se usa el tipo por defecto de tu empresa:BIOMETRICconviafirmayDOCUMENTARYconmailodual. Debe ser un tipo habilitado para tu empresa. Queda guardado en la solicitud (dataRecords.issuance_flow) y no cambia después.- Solicitud
BIOMETRIC:legal_rep_email,legal_rep_first_nameylegal_rep_last_nameson obligatorios; los adjuntos son opcionales. - Solicitud
DOCUMENTARY:attachmentses obligatorio, con 2 a 3 archivos en Base64 ([{"name": "rut.pdf", "base64": "data:application/pdf;base64,..."}]), máximo 7 MB por archivo y 10 MB en total. - La solicitud consume un certificado del mismo tipo de emisión y de la vigencia pedida (
life): primero del cupo pospago de ese tipo, si tienes, y si no, de un certificado prepago de ese tipo y esa vigencia. - Límite: 10 solicitudes por minuto por usuario.
Para los siguientes pasos necesitarás el id numérico (dataRecords.id, que en el ejemplo es 636).
Una solicitud BIOMETRIC nace en PROCESSING y la emisión arranca sola. Una DOCUMENTARY nace en DRAFT.
La respuesta no incluye el uuid. Obtenlo con GET /api/v1/certificate-request/{id} (en dataRecords.data[0].uuid); lo necesitarás para descargar o revocar el certificado.
Posibles errores:
400con"message": "Los datos de la solicitud son inválidos."y el detalle por campo enerrors. Siterms_version_idno es la versión vigente, vuelve a consultarGET /api/v1/terms/current.400si ya existe una solicitud en proceso con el mismo NIT y DV.400conerror_codepor el tipo de emisión (messagetrae el motivo):INVALID_ISSUANCE_FLOW: el valor no esBIOMETRICniDOCUMENTARY.ISSUANCE_FLOW_NOT_ALLOWED: tu empresa no tiene habilitado ese tipo.ISSUANCE_FLOW_UNAVAILABLE: la emisión biométrica no está disponible en este momento. La solicitud no se crea ni consume cupo; reintenta más tarde o, si tu empresa lo permite, usaDOCUMENTARY.
402conerror_code=QUOTA_EXHAUSTEDsi no tienes cupo para ese tipo de emisión y esa vigencia.available_in_other_flowslista los otros tipos habilitados para tu empresa que sí tienen cupo para esa vigencia (puede venir vacío):
{
"success": false,
"error_code": "QUOTA_EXHAUSTED",
"message": "No tiene certificados disponibles de tipo Documental (sin biometría) para vigencia de 1 año(s). Debe adquirir un paquete de certificados para continuar.",
"issuance_flow": "DOCUMENTARY",
"vigencia": 1,
"available_in_other_flows": [{ "issuance_flow": "BIOMETRIC", "available": 41 }]
}
Paso 4️⃣: Inicia la Emisión y la Verificación de Identidad
El flujo depende del tipo de emisión de la solicitud (issuance_flow, en la respuesta de creación y en GET /api/v1/certificate-request/{id}), no de la empresa: una empresa dual tiene solicitudes de los dos tipos.
Solicitud BIOMETRIC (con verificación biométrica)
No tienes que disparar la emisión: se encola automáticamente al crear la solicitud, usando el legal_rep_email que informaste. Consulta el avance con GET /api/v1/certificate-request/{id}/issuance (Paso 5).
El representante legal debe completar la verificación de identidad biométrica (KYC):
- Cuando
dataRecords.data.remote_statusseaaccreditation, obtén el enlace conGET /api/v1/certificate-request/{id}/kyc-link. Viene endata.link(no endataRecords) y también se expone endataRecords.data.kyc_accreditation_linkdeGET /api/v1/certificate-request/{id}/issuance. Si lo pides antes de ese estado, responde422(elmessageindica el estado remoto actual). Justo después de crear la solicitud, mientras el trámite de Viafirma aún no existe, responde404(Recurso no encontrado.). En ambos casos vuelve a consultar más tarde. No lo confundas con el404Solicitud de certificado no encontrada., que indica que elidno existe o pertenece a otra empresa. - Envíaselo al suscriptor. Tiene hasta
expires_at(96 horas desde el envío del trámite) para completarlo. 24 horas antes del vencimiento se envía un aviso de último llamado al correo de la empresa. - Si no completa la verificación a tiempo, el trámite vence, la solicitud se cancela y el cupo se reintegra.
curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request/636/kyc-link" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json"
En una solicitud BIOMETRIC, POST /api/v1/certificate-request/{id}/issue solo se usa para reintentar cuando el trámite vigente quedó en FAILED, FAILED_RECOVERABLE o EXPIRED. Debe incluir email_certificate y, solo para persona jurídica (FE-PJ), organization_type (RM, PROP, RUNEOL, RNT, ESAL, ESOL, JUEGOS o EXTRANJERAS); en persona natural (FE-PN) está prohibido enviarlo. Responde 201 con dataRecords.status = "submitted". Si ya hay una emisión en curso, responde 409 o 422: no lo llames justo después de crear la solicitud.
No envíes provider en el cuerpo: el proveedor lo decide el tipo de emisión de la solicitud. Un provider que no corresponde a ese tipo responde 422 con error_code = ISSUANCE_PROVIDER_MISMATCH.
Solicitud DOCUMENTARY (documental)
Dispara la emisión. La solicitud necesita al menos un adjunto de tipo ATTACHED: los attachments que enviaste al crearla ya cuentan como tales (puedes agregar más con POST /api/v1/certificate-request/{id}/files).
curl -X POST "https://sandbox-api.maticerts.com/api/v1/certificate-request/636/issue" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{}'
Responde 200 con dataRecords.status = "sent" y la solicitud pasa a PROCESSING. Sin adjuntos ATTACHED responde 400 o 422.
Límite de /issue: 5 llamadas por minuto por usuario (429 si lo superas).
- En una solicitud
DOCUMENTARY, unos 30 segundos después de unPOST /api/v1/certificate-request/{id}/issuecorrecto (el tiempo depende de la cola de trabajos) se genera un certificado de prueba autofirmado: la solicitud pasa dePROCESSINGaPROCESSEDy se emite el eventocertificate_request.status_changed. Así puedes probar el monitoreo y la descarga sin esperar. - Ese certificado se entrega en un
.zipprotegido con contraseña: el NIT del titular, o el NIT seguido del DV si es persona jurídica. Dentro está el.p12, que se abre con su PIN. - Los certificados de Sandbox son de prueba y no tienen validez legal; no los uses para firmar documentos reales.
Paso 5️⃣: Monitorea el Estado de la Emisión
Consulta el avance de la emisión con el id numérico de la solicitud:
- cURL
- Node.js
- Python
curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request/636/issuance" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json"
Respuesta (solicitud BIOMETRIC, ejemplo):
{
"success": true,
"message": "Estado del trámite Viafirma.",
"dataRecords": {
"provider": "viafirma",
"status": "processing",
"message": "Estado del trámite Viafirma.",
"external_id": "D4AZEQQG6",
"resource_id": 5,
"data": {
"public_id": "fab8d88e0c5a4cab8bfc2b72be05a098",
"profile_type": "FE-PN",
"identity_type": "IDC",
"internal_state": "POLLING",
"remote_status": "accreditation",
"revocation_code": null,
"revoked_at": null,
"submitted_at": "2026-10-08T15:15:30.000000Z",
"expires_at": "2026-10-12T15:15:30.000000Z",
"history_count": 3,
"kyc_accreditation_link": "https://signup.metamap.com/?merchantToken=...",
"kyc_flow_completed_at": null,
"poll_attempts": 12,
"last_error_code": null,
"last_error_message": null,
"accredited_status": null,
"last_public_note": null,
"public_notes": [],
"can_reject": true
}
}
}
const response = await fetch(
`https://sandbox-api.maticerts.com/api/v1/certificate-request/${requestId}/issuance`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
}
);
const data = await response.json();
console.log('Estado de la emisión:', data.dataRecords.status);
console.log('Estado interno:', data.dataRecords.data.internal_state);
response = requests.get(
f'https://sandbox-api.maticerts.com/api/v1/certificate-request/{request_id}/issuance',
headers=headers
)
data = response.json()
print('Estado de la emisión:', data['dataRecords']['status'])
print('Estado interno:', data['dataRecords']['data'].get('internal_state'))
dataRecords.statusvalesubmitted,processing,ready(certificado listo) ofailed(revocado, fallido, vencido o cancelado). En los fallidos, vencidos y cancelados el motivo está endataRecords.data.last_error_message; en los revocados (internal_state=REVOKED) ese campo puede venir vacío o con un error anterior: la revocación se identifica porinternal_stateyrevoked_at.- En una solicitud
DOCUMENTARY,dataRecords.datasolo traerequest_status, ystatusesreadycuando la solicitud estáPROCESSEDyfailedcuando estáREJECTED. - Mientras el titular no complete la verificación de identidad, el trámite no avanza; el enlace está en
dataRecords.data.kyc_accreditation_link(ver Paso 4). - Si el
idno existe o es de otra empresa, este endpoint responde404.
El estado de la solicitud está en request_status de GET /api/v1/certificate-request/{id} (la solicitud viene en dataRecords.data[0], junto con su uuid): DRAFT, SENT, PENDING, ACCEPTED, PROCESSING, PROCESSED (certificado emitido), REJECTED, REVOKED, EXPIRED o CANCELLED. Si el id no existe o pertenece a otra empresa, ese endpoint responde 200 con dataRecords.data = [null].
No consultes más de una vez por minuto: el avance en Viafirma se sincroniza cada 60 segundos y la verificación de identidad puede tardar hasta 96 horas. El límite general es de 60 peticiones por minuto por usuario, compartido por todas tus llamadas; si lo superas recibes 429. Deja de consultar cuando dataRecords.status sea ready o failed, y antes de descargar confirma que request_status es PROCESSED. Mejor aún, registra un webhook con el evento certificate_request.status_changed y espera new_status = "PROCESSED" (ver Webhooks).
Paso 6️⃣: Descarga el Certificado
Cuando request_status sea PROCESSED, toma el uuid de la solicitud (GET /api/v1/certificate-request/{id} → dataRecords.data[0].uuid) y descarga el certificado en Base64. La descarga usa el UUID, no el ID numérico.
- cURL
- Node.js
- Python
UUID="550e8400-e29b-41d4-a716-446655440000"
curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request/$UUID/issuance/download/base64" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json" \
-o resp.json
# Decodifica el .zip y extrae el .p12
jq -r .dataRecords.data.p12_base64 resp.json | base64 -d > certificado.zip
unzip certificado.zip
jq -r .dataRecords.data.p12_pin resp.json # PIN para abrir el .p12
Respuesta esperada (200):
{
"success": true,
"dataRecords": {
"data": {
"p12_pin": "X3kP9aQ1mZv7nR2sYwQe8BcZ4uVhKpTf",
"p12_filename": "empresa-sas_636.zip",
"p12_base64": "UEsDBBQAAAAI..."
}
}
}
const fs = require('fs');
const response = await fetch(
`https://sandbox-api.maticerts.com/api/v1/certificate-request/${uuid}/issuance/download/base64`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
}
);
const { dataRecords: { data } } = await response.json();
fs.writeFileSync(data.p12_filename, Buffer.from(data.p12_base64, 'base64'));
console.log('Certificado descargado:', data.p12_filename);
// Guarda data.p12_pin en un almacén seguro: es la contraseña del .p12
import base64
response = requests.get(
f'https://sandbox-api.maticerts.com/api/v1/certificate-request/{uuid}/issuance/download/base64',
headers=headers
)
data = response.json()['dataRecords']['data']
with open(data['p12_filename'], 'wb') as f:
f.write(base64.b64decode(data['p12_base64']))
print('Certificado descargado:', data['p12_filename'])
# Guarda data['p12_pin'] en un almacén seguro: es la contraseña del .p12
p12_base64 es un .zip que contiene el .p12: decodifícalo, descomprímelo y abre el .p12 con p12_pin. Si p12_pin vale NO_PIN, la solicitud no tiene PIN registrado.
En una solicitud DOCUMENTARY (también en Sandbox) el .zip viene protegido con contraseña: el NIT del titular, o el NIT seguido del DV si es persona jurídica. Si unzip no admite el cifrado del archivo, ábrelo con 7-Zip (7z x certificado.zip) u otra herramienta compatible con ZIP AES.
Alternativa: GET /api/v1/certificate-request/{uuid}/issuance/download devuelve download_url (URL firmada válida 24 horas, hasta expires_at; puede venir en null si el almacenamiento no admite URLs firmadas), p12_filename y p12_pin.
Posibles errores:
409: la solicitud no está enPROCESSED(revocada, vencida, cancelada, rechazada o aún en trámite); el estado actual viene enstatus.404: la solicitud no existe o es de otra empresa, todavía no hay certificado emitido, o el archivo ya no está disponible.410(solo en/download/base64): el archivo no se pudo leer.
✅ ¡Tu certificado está listo para usar!
Paso 7️⃣: Verifica el Certificado Localmente
La API no tiene un endpoint para validar certificados. Puedes revisar el .p12 descargado con OpenSSL, usando el PIN de p12_pin:
unzip empresa-sas_636.zip
P12_PIN="<valor de p12_pin>"
openssl pkcs12 -in empresa-sas_636.p12 -nokeys -passin pass:"$P12_PIN" \
| openssl x509 -noout -subject -issuer -serial -dates
Comprueba que el sujeto corresponde al titular y que las fechas notBefore/notAfter son las que esperas. Si OpenSSL 3 rechaza el archivo por un algoritmo no soportado, repite el comando con la opción -legacy.
🎉 ¡Felicidades!
Has completado el flujo de emisión de certificados. Ahora puedes:
- 📝 Firmar documentos electrónicos con el certificado descargado
- 📊 Consultar tus solicitudes con
GET /api/v1/certificate-requesty los certificados próximos a vencer conGET /api/v1/certificates/expiring(los dos aceptan?issuance_flow=BIOMETRICo?issuance_flow=DOCUMENTARYpara filtrar por tipo de emisión) - 🔄 Renovar por 1 año más los certificados biométricos de 1 año, desde la emisión hasta el fin del periodo de gracia
- ⛔ Revocar un certificado biométrico emitido (irreversible)
- 🔔 Recibir los cambios de estado en tu sistema mediante webhooks
🔄 Después de la Emisión
Renovación (certificados biométricos de 1 año)
Puedes extender el certificado 1 año más sin reemitirlo. La renovación solo existe para certificados emitidos en solicitudes BIOMETRIC y se cotiza siempre con las tarifas de la emisión biométrica. Se puede renovar desde que se emite y hasta el fin del periodo de gracia (revocation_date), que es la fecha de vencimiento más 15 días por defecto.
- Consulta si se puede renovar y cuánto cuesta con
GET /api/v1/certificate-request/{id}/issuance/renewal({id}es el ID numérico). La respuesta traeeligible,reason(NOT_RENEWABLE_PROVIDER,NOT_ACTIVE,MAX_TERMoGRACE_EXPIRED),commercial_expiration_date,revocation_date,in_grace_period,quoteypending_order. - Genera la orden con
POST /api/v1/certificate-request/{id}/issuance/renew(sin cuerpo). Si ya hay una orden de renovación pendiente, se devuelve esa misma; si el certificado no se puede renovar, responde422conreason. - Esta llamada no cobra y la orden no trae los datos de pago. Pídelos con
POST /api/v1/orders/{uuid}/retry(devuelveacceptance_tokeneintegrity_hash) y paga con el widget de WOMPI, o envía eseacceptance_tokenjunto conpayment_source_id(fuente de pago tokenizada) ypayment_methodaPOST /api/v1/orders/{uuid}/pay. Eluuidde la orden viene endata.uuidde la respuesta de renew, no endataRecords.
Si no se renueva dentro del periodo de gracia, el certificado se revoca automáticamente.
Revocación de un certificado emitido (irreversible)
Usa POST /api/v1/certificate-request/{uuid}/revoke, con el UUID de la solicitud. El cuerpo lleva:
revoking_code: el código dedataRecords.data.revocation_codeenGET /api/v1/certificate-request/{id}/issuance.revocation_reason:0,1,2,3,4,5,9o10, según RFC 5280.
Solo funciona con solicitudes BIOMETRIC en estado PROCESSED.
Rechazo de una solicitud en curso (irreversible)
Si los datos quedaron mal y el certificado todavía no se ha emitido, usa POST /api/v1/certificate-request/{id}/issuance/reject (con el ID numérico) y un reason de 5 a 400 caracteres. La solicitud BIOMETRIC debe estar en PROCESSING y sin certificado. El rechazo la cancela y te devuelve el cupo (dataRecords.quota_released); después, crea una solicitud nueva.
Antes de rechazar, revisa can_reject en GET /api/v1/certificate-request/{id}/issuance: es solo orientativo, porque la decisión final la toma Viafirma (409 si no lo admite). Para anular un certificado ya emitido, usa la revocación.
Más detalle en Emisión de Certificados y Viafirma.
🔔 Recomendado: Webhooks en lugar de Polling
Registra una URL con POST /api/v1/webhooks (name, url, events; description es opcional; máximo 5 webhooks por empresa). El secret de firma no se devuelve al crear el webhook: obtenlo con POST /api/v1/webhooks/{id}/rotate-secret y guárdalo. Es la única respuesta que lo expone, y al rotarlo el anterior deja de valer de inmediato.
Generan entregas los eventos certificate_request.created, certificate_request.status_changed, certificate_request.ai_processed, certificate_request.file_uploaded y certificate_request.deleted y, desde la 1.21.1, payment.approved (la orden pasa a PAID) y payment.failed (la orden pasa a FAILED porque se rechazó el pago). El catálogo está en GET /api/v1/webhooks/events. certificate.expiring se acepta en la suscripción, pero todavía no genera entregas.
Los eventos de pago llegan como máximo una vez por cada cambio de estado de la orden (una orden rechazada puede recibir después payment.approved si otro intento de pago de esa orden se aprueba) y traen en data el order_id (el UUID de la orden, el que acepta GET /api/v1/orders/{uuid}), el order_type (QUOTA_PURCHASE = compra de certificados; CERTIFICATE_RENEWAL = renovación, que no agrega cupos), issuance_flow, quantity y vigencia de la compra, los datos de la transacción y paid_at (aprobado) o reason (rechazado). Son una alternativa a consultar GET /api/v1/orders/{uuid} una y otra vez, pero mantén esa consulta como respaldo: las entregas no se reintentan. Ejemplo completo en Webhooks de pagos.
certificate_request.created y certificate_request.status_changed traen en data.issuance_flow el tipo de emisión de la solicitud (BIOMETRIC o DOCUMENTARY).
Cada entrega es un POST JSON con las cabeceras X-Webhook-Event, X-Webhook-Source y X-Webhook-Sig: t=<timestamp>,v1=<firma>, donde v1 es el HMAC-SHA256 (hexadecimal) del cuerpo crudo calculado con tu secret. Verifica la firma con el cuerpo tal como lo recibiste, sin volver a serializarlo.
Cada evento se envía una sola vez y el receptor tiene 10 segundos para responder. Solo cuentan como fallo los errores de conexión y los timeouts; las respuestas HTTP 4xx/5xx se registran como entregadas, con su código de estado, y no se reenvían. Tras 10 fallos consecutivos el webhook se desactiva (is_active = false); puedes reactivarlo con PUT /api/v1/webhooks/{id} enviando {"is_active": true}. Consulta el historial con GET /api/v1/webhooks/{id}/deliveries. Como complemento, usa polling (Paso 5) para conciliar los eventos que se puedan perder.
Más detalle en la referencia de Webhooks.
🆘 Solución de Problemas
401 — "No autenticado."
- Verifica que el token no haya expirado (los PAT vencen a los 90 días de su creación, aunque
expires_atindique otra fecha) ni haya sido revocado - Comprueba que envías la cabecera
Authorization: Bearer <token> - Crea un PAT nuevo, o renueva el anterior con
POST /api/v1/tokens/{id}/renewautenticándote con otra credencial válida (un token vencido no sirve para renovarse a sí mismo)
403
- La empresa de tu usuario está inactiva: contacta a soporte
La solicitud no aparece (dataRecords.data = [null]) o recibes 404 al descargar
GET /api/v1/certificate-request/{id}usa elidnumérico; las descargas usan eluuidde la solicitud- Verifica que la solicitud pertenece a la empresa de tu usuario
404"Archivo de certificado no encontrado.": la solicitud está enPROCESSED, pero no hay un archivo de certificado disponible para descargar por la API (si aún no está enPROCESSEDrecibes409, no404). Contacta a soporte con el UUID de la solicitud
409 al descargar
- El certificado solo se entrega cuando
request_status = PROCESSED; si está revocado, vencido, cancelado, rechazado o aún en trámite, no se puede descargar. El campostatusde la respuesta trae el estado actual; si esDRAFT, la solicitud aún no se ha enviado a emisión: envíala conPOST /api/v1/certificate-request/{id}/issue
400 — "Los datos de la solicitud son inválidos." / 422 — "Los datos proporcionados no son válidos."
- El
400corresponde a la creación de solicitudes y a la carga de archivos (POST /api/v1/certificate-request/{id}/files); los demás endpoints responden422ante datos inválidos - Revisa el detalle en
errors: campos obligatorios, valores de catálogo,terms_version_idvigente y adjuntos (tamaño y tipo)
402 — sin cupo / 429 — demasiadas peticiones
402(QUOTA_EXHAUSTED): no tienes cupo para ese tipo de emisión y esa vigencia. Compra certificados del mismo tipo y la misma vigencia conPOST /api/v1/orders(issuance_flow,vigenciayquantity), o, siavailable_in_other_flowstrae otro tipo habilitado con cupo, crea la solicitud con ese tipo429: espera un minuto. Límites: 60 peticiones por minuto en general, 10 creaciones de solicitud por minuto, 20 cargas de archivos por minuto y 5 emisiones por minuto
Para más ayuda, consulta la guía de Solución de Problemas y la referencia de la API: Solicitudes de Certificado, Emisión de Certificados, Viafirma, Webhooks y Tokens.