💼 Casos de Uso Reales
Escenarios de integración típicos para casas de software, proveedores de arrendamiento en servidor y partners que usan MATICERTS para aprovisionar certificados de firma de factura electrónica (perfiles FE-PJ, Persona Jurídica, y FE-PN, Persona Natural) con vigencia de 1 o 2 años.
La API de MATICERTS gestiona el ciclo de vida del certificado: crear la solicitud, verificar la identidad del titular, emitir, consultar el estado (o recibirlo por webhook), descargar el certificado (un .zip con el .p12 y su PIN), renovar, rechazar y revocar.
No firma documentos, no genera enlaces de firma y no valida certificados ni firmas. La firma de tus facturas y documentos la hace tu sistema con el .p12 descargado y su PIN.
Convenciones de los ejemplos
- No existe un SDK oficial: los ejemplos llaman directamente a la API REST v1 con
fetch(Node.js 18 o superior). Puedes traducirlos a cualquier lenguaje. - URL base: Sandbox
https://sandbox-api.maticerts.com/api/v1· Producciónhttps://api.maticerts.com/api/v1. - Autenticación: cabecera
Authorization: Bearer <tu token>(un Personal Access Token; ver Tokens). - Las peticiones y respuestas son JSON. El límite general es de 60 peticiones por minuto por usuario.
// cliente.js — utilidades compartidas por todos los casos
const API = process.env.MATICERTS_API_URL; // p. ej. https://sandbox-api.maticerts.com/api/v1
const headers = {
Authorization: `Bearer ${process.env.MATICERTS_TOKEN}`, // tu Personal Access Token
Accept: 'application/json',
'Content-Type': 'application/json',
};
async function api(path, options = {}) {
const res = await fetch(`${API}${path}`, { headers, ...options });
const body = await res.json();
if (!res.ok) {
const error = new Error(body.message ?? `HTTP ${res.status}`);
error.status = res.status;
error.body = body; // en errores de validación trae `errors` con los mensajes por campo
throw error;
}
return body;
}
🎟️ Antes de empezar: tipo de emisión, cupo y compras
Tipo de emisión de cada solicitud
Cada solicitud tiene un tipo de emisión en issuance_flow:
issuance_flow | Qué significa | Cómo se tramita |
|---|---|---|
BIOMETRIC | Biométrica (con verificación de identidad) | El representante legal valida su identidad con un enlace. La emisión arranca sola al crear la solicitud |
DOCUMENTARY | Documental (sin biometría) | Se adjuntan 2 a 3 documentos de soporte y envías la solicitud a emisión con /issue |
Los tipos que puede usar tu empresa dependen de issuance_provider (en el login, en GET /company y, con el valor vigente, en GET /quota/status):
viafirma: soloBIOMETRIC.mail: soloDOCUMENTARY.dual: los dos. Se elige en cada solicitud conissuance_flow; si no se envía, la solicitud esDOCUMENTARY.
El tipo se fija al crear la solicitud y no cambia. Para saber cómo continuar con una solicitud, usa siempre su issuance_flow, nunca el issuance_provider de la empresa: una empresa dual tiene solicitudes de los dos tipos.
Cupo por tipo de emisión
Cada solicitud creada con POST /certificate-request consume un certificado de su mismo tipo de emisión. Un certificado comprado o asignado para un tipo no sirve para el otro.
- Se usa primero el cupo pospago vigente de ese tipo, que sirve para cualquier vigencia.
- Si no hay, se usa un certificado prepago de ese tipo y de la misma vigencia que pidas en
life(1 o 2 años). - Si no hay ninguno, la creación responde 402 con
error_code=QUOTA_EXHAUSTED, unmessagecomoNo tiene certificados disponibles de tipo Documental (sin biometría) para vigencia de 1 año(s). Debe adquirir un paquete de certificados para continuar.yavailable_in_other_flows, con los otros tipos habilitados que sí tienen cupo para esa vigencia.
Si eliminas la solicitud (en DRAFT o REJECTED), la rechazas con POST /certificate-request/{id}/issuance/reject o se cancela porque venció la verificación de identidad, el certificado vuelve al mismo saldo del que salió. Pasarla a CANCELLED con PUT /certificate-request/{id}/status no devuelve el cupo. Si el certificado salió de un cupo pospago que ya venció, la unidad devuelta no vuelve a quedar disponible.
Consulta tu disponibilidad con GET /quota/status. Lo más práctico es leer by_flow, que trae un bloque por tipo de emisión:
by_flow.BIOMETRICyby_flow.DOCUMENTARY: aparecen los tipos que tu empresa puede usar y, además, cualquier tipo en el que conserves saldo aunque ya no esté habilitado (llega conselectable: false: ves el saldo, pero no puedes crear solicitudes de ese tipo). Cada bloque trae:available_1_year/available_2_year: certificados que puedes pedir para cada vigencia (pospago más prepago). Es el número que conviene mostrar.postpaid:{ allocated, used, remaining, expires_at, status }con la suma de todos los cupos pospago vigentes de ese tipo (expires_ates el vencimiento más próximo), onullsi no hay.prepaid_1_year/prepaid_2_year/prepaid_items_available: certificados prepago disponibles.has_quotayselectable.
has_quota_any_flow:truesi hay cupo en algún tipo que tu empresa puede usar.issuance_provider: el valor vigente de tu empresa (viafirma,mailodual).has_agreement:truesi tu empresa tiene convenio (pospago) yfalsesi es prepago. Es el valor vigente y tiene precedencia sobre elcompany.has_agreementdel login, que es una foto del inicio de sesión. Con convenio, emites con el cupo pospago que te asigna MATICERTS; sin convenio, compras tus certificados. El convenio no cambia cómo se consumen los cupos: los certificados pospago y prepago que tengas se siguen usando igual.- Las claves de primer nivel (
has_quota,postpaid,prepaid_1_year,prepaid_2_year,prepaid_items_available) describen el tipo por defecto de tu empresa (condual,DOCUMENTARY): coinciden con lo que consumirá una solicitud creada sinissuance_flow.
const { dataRecords: cupo } = await api('/quota/status');
const tipo = 'BIOMETRIC'; // o 'DOCUMENTARY'
const saldo = cupo.by_flow?.[tipo];
const puedeCrear = Boolean(saldo?.selectable); // ¿tu empresa puede usar este tipo?
const disponibles1Anio = saldo?.available_1_year ?? 0;
const esPospago = cupo.has_agreement; // convenio vigente
Tarifas y compras por tipo de emisión
- Tarifas:
GET /pricing?issuance_flow=BIOMETRIC(oDOCUMENTARY) devuelve los rangos de precio de ese tipo; sin el parámetro, los del tipo por defecto de tu empresa. Cada rango trae suissuance_flow. Conquantityyvigencia(por ejemploGET /pricing?issuance_flow=DOCUMENTARY&quantity=5&vigencia=1) obtienes la cotización, contier_nameeissuance_flow. Si la lista llega vacía ([]), ese tipo no está disponible para compra en tu plan: no es un error, simplemente no ofrezcas esa compra. - Compra prepago: crea una orden con
POST /orders(quantitymayor o igual a 1,vigencia1 o 2 eissuance_flowopcional; sin él, el tipo por defecto). Los certificados comprados solo se usan en solicitudes de ese tipo. La respuesta, dentro dedata, traeorder_id, los datos para pagar con el widget de WOMPI,issuance_flowytier. Si no hay un rango de precios para esa cantidad, responde 422 conerror_code=PRICING_TIER_NOT_FOUND. - Pago: como alternativa al widget, puedes pagar la orden con
POST /orders/{uuid}/pay(payment_source_id,acceptance_token,payment_method). El resultado llega de forma asíncrona: recíbelo con los webhookspayment.approvedypayment.failed(ver Caso 2) o consultaGET /orders/{uuid}hasta que el estado seaPAID(aprobado) oFAILED(rechazado). Una ordenFAILEDno se reintenta, pero todavía puede pasar aPAIDsi después se aprueba otro intento de pago de esa misma orden: antes de crear una orden nueva, vuelve a consultarla (ver Webhooks de pagos). Si la orden sigue enPENDINGylatest_transaction.statusesVOIDEDoERROR, vuelve a pagarla o elimínala. Solo conPAIDlos certificados quedan disponibles.GET /ordersyGET /orders/{uuid}también traen elissuance_flowde cada orden. - Errores de tipo: en
/pricingy/orders, unissuance_flowque no esBIOMETRICniDOCUMENTARYresponde 422INVALID_ISSUANCE_FLOW, y uno que tu empresa no tiene habilitado, 422ISSUANCE_FLOW_NOT_ALLOWED.
🏢 Caso 1: Software de facturación electrónica — aprovisionar el certificado de un cliente
El desafío
Una casa de software de facturación electrónica necesita que cada cliente nuevo (empresa o persona natural) tenga su certificado de firma para firmar sus documentos ante la DIAN, sin procesos manuales.
La solución
El software crea la solicitud desde su propio formulario de alta, hace llegar el link de verificación de identidad al representante legal y descarga el certificado (.p12 y PIN) en cuanto queda emitido.
- Crear la solicitud con
POST /certificate-request, con los datos reales del titular y, si tu empresa puede elegir (dual), el tipo de emisión enissuance_flow. La respuesta trae elidnumérico de la solicitud y suissuance_flow. - Iniciar la emisión. Depende del tipo de emisión de la solicitud (
issuance_flow, que viene en la respuesta de creación):- Biométrica (
BIOMETRIC): la emisión arranca sola al crear la solicitud; no hace falta llamar a/issue. - Documental (
DOCUMENTARY): la solicitud nace en borrador (DRAFT) y debes enviarla conPOST /certificate-request/{id}/issue.
- Biométrica (
- Verificación de identidad (solo
BIOMETRIC). El representante legal completa una verificación biométrica con su documento de identidad. Cuando el link está listo (dataRecords.data.remote_status=accreditationenGET /certificate-request/{id}/issuance), MATICERTS lo envía al correo de tu empresa; también puedes obtenerlo conGET /certificate-request/{id}/kyc-linky enviarlo desde tu propio sistema. - Esperar la emisión. Es asíncrona: recibe el webhook
certificate_request.status_changed(ver Caso 2) o consultaGET /certificate-request/{id}/issuancecada cierto tiempo. - Descargar el certificado con el UUID de la solicitud (no con su ID numérico) en
GET /certificate-request/{uuid}/issuance/download/base64.
Implementación
// 1. Crear la solicitud con los datos del titular del certificado
async function crearSolicitud(cliente) {
// Versión vigente de los Términos y Condiciones (endpoint público)
const terms = await api('/terms/current');
const creada = await api('/certificate-request', {
method: 'POST',
body: JSON.stringify({
country_id: 45, // 45 = Colombia (GET /countries)
city_id: cliente.ciudadId, // GET /cities
identity_document_id: 1, // tipo de documento del representante (GET /identity-documents)
type_organization_id: 1, // 1 = Persona Jurídica (GET /organization-type)
entity_document_type_id: 1, // obligatorio para Persona Jurídica (GET /entity-document-types)
document_number: cliente.representante.documento,
address: cliente.direccion,
company_name: cliente.razonSocial,
dni: cliente.nit, // NIT o cédula SIN dígito de verificación (el DV lo calcula MATICERTS)
legal_rep_first_name: cliente.representante.nombres, // obligatorios en solicitudes BIOMETRIC
legal_rep_last_name: cliente.representante.apellidos,
legal_rep_email: cliente.representante.email,
life: 1, // vigencia en años: 1 o 2
issuance_flow: 'BIOMETRIC', // opcional: BIOMETRIC o DOCUMENTARY (sin él, el tipo por defecto de tu empresa)
accept_terms: true,
terms_version_id: terms.dataRecords.id,
// Solo en solicitudes DOCUMENTARY: 2 a 3 adjuntos PDF/JPG/PNG en Base64
// attachments: [{ name: 'rut.pdf', base64: 'data:application/pdf;base64,...' }, ...],
}),
});
// Errores: 400 datos inválidos o ya hay una solicitud en curso con el mismo NIT y DV
// 400 con error_code INVALID_ISSUANCE_FLOW, ISSUANCE_FLOW_NOT_ALLOWED o ISSUANCE_FLOW_UNAVAILABLE
// 402 QUOTA_EXHAUSTED: sin cupo para ese tipo y esa vigencia (mira e.body.available_in_other_flows)
// 403 empresa inactiva · 429 más de 10 solicitudes por minuto
const { id, issuance_flow } = creada.dataRecords; // ID numérico. La respuesta de creación NO incluye el uuid.
return { id, issuanceFlow: issuance_flow }; // decide los pasos siguientes por issuance_flow
}
// 2. Solo en solicitudes DOCUMENTARY: enviar la solicitud a emisión (sin `provider` en el cuerpo)
async function enviarAEmision(id) {
await api(`/certificate-request/${id}/issue`, { method: 'POST', body: JSON.stringify({}) });
}
// 3. Solo en solicitudes BIOMETRIC: obtener el link de verificación de identidad
async function obtenerLinkVerificacion(id) {
try {
const kyc = await api(`/certificate-request/${id}/kyc-link`);
return kyc.data.link; // el link viene en data.link
} catch (e) {
// 422: el enlace no se puede obtener ahora (lee e.message). Si el estado remoto aún no llega a
// 'accreditation', reintenta más tarde; si ya lo pasó (p. ej. 'fail') o Viafirma respondió
// con error, reintentar no lo resuelve: revisa GET /certificate-request/{id}/issuance.
// 404 'Recurso no encontrado.' justo después de crear la solicitud: el trámite de Viafirma aún no
// existe; reintenta más tarde. (404 'Solicitud de certificado no encontrada.' = id inexistente
// o de otra empresa: no reintentes.)
if (e.status === 422 || (e.status === 404 && e.message === 'Recurso no encontrado.')) return null;
throw e;
}
}
// 4. Consultar el estado de la emisión (si no usas webhooks)
async function estadoEmision(id) {
const r = await api(`/certificate-request/${id}/issuance`);
return r.dataRecords.status; // 'submitted' | 'processing' | 'ready' | 'failed'
}
// 5. Descargar el certificado emitido
async function descargarCertificado(id) {
const detalle = await api(`/certificate-request/${id}`);
const uuid = detalle.dataRecords.data[0].uuid; // la descarga usa el UUID, no el ID numérico
const cert = await api(`/certificate-request/${uuid}/issuance/download/base64`);
const { p12_base64, p12_pin, p12_filename } = cert.dataRecords.data;
// p12_base64 es un .zip que CONTIENE el .p12: decodifica, descomprime y abre el .p12 con p12_pin.
// p12_pin vale 'NO_PIN' si la solicitud no tiene PIN registrado.
return { zip: Buffer.from(p12_base64, 'base64'), pin: p12_pin, nombre: p12_filename };
}
La petición es POST /certificate-request con token Bearer y cuerpo JSON: los adjuntos van en Base64 dentro del JSON, no en multipart.
- Obligatorios:
country_id,city_id,identity_document_id,type_organization_id,document_number,address,company_name,dni(NIT o cédula sin dígito de verificación),life(1 o 2 años),accept_terms(true) yterms_version_id(la versión vigente deGET /terms/current; si no es la vigente, responde 400). - Persona Jurídica (
type_organization_id= 1): tambiénentity_document_type_id. - Representante legal:
legal_rep_first_nameylegal_rep_last_name(olegal_representativecon el nombre completo). - Tipo de emisión (
issuance_flow, opcional):BIOMETRICoDOCUMENTARY. Sin él, el tipo por defecto de tu empresa (viafirma→BIOMETRIC;mailodual→DOCUMENTARY). - Solicitud
BIOMETRIC:legal_rep_email,legal_rep_first_nameylegal_rep_last_nameson obligatorios; los adjuntos son opcionales. - Solicitud
DOCUMENTARY:attachmentses obligatorio: 2 a 3 archivos en Base64, máximo 7 MB por archivo y 10 MB en total. Se recomiendan PDF, JPG o PNG; el servidor no valida el tipo de archivo de los adjuntos. Si tu empresa esdualy no enviasteissuance_flowni adjuntos, el mensaje deerrors.attachmentste recuerda que envíesissuance_flow=BIOMETRICpara una emisión biométrica. - Opcionales:
phone,mobileeinfo.
Un error de validación responde 400 con { message, errors }, donde errors trae los mensajes por campo. Los errores del tipo de emisión añaden error_code (INVALID_ISSUANCE_FLOW, ISSUANCE_FLOW_NOT_ALLOWED o ISSUANCE_FLOW_UNAVAILABLE) y message trae el motivo. Para cambiar el tipo de una solicitud ya creada, elimínala (solo en DRAFT o REJECTED) y crea otra: PUT /certificate-request/{id} con un issuance_flow distinto responde 400 ISSUANCE_FLOW_IMMUTABLE. Detalle completo en Solicitudes de Certificado.
dataRecords.statusenGET /certificate-request/{id}/issuancevalesubmitted,processing,readyofailed. Si esfailed, deja de consultar y revisa la solicitud.- La verificación de identidad depende de una persona y puede tardar horas o días: consulta con intervalos razonables (por ejemplo, cada pocos minutos) o, mejor, usa webhooks.
- La descarga solo funciona cuando la solicitud está en
PROCESSED; en cualquier otro estado responde 409 con el estado actual. Si recibes 409 justo después de verready, reintenta en un minuto. - Guarda el PIN de forma segura (cifrado): sin él no se puede abrir el
.p12.
Resultado
- ✅ Alta de certificados sin intervención manual desde tu propio software.
- ✅ El certificado (
.p12y PIN) llega a tu sistema en cuanto se emite. - ✅ La firma de las facturas sigue en tu sistema, con el certificado del cliente.
🔔 Caso 2: Enterarte de la emisión sin hacer polling
En lugar de consultar el estado una y otra vez, registra un webhook con POST /webhooks (name, url y events; description es opcional) suscrito a certificate_request.status_changed. Los eventos disponibles se listan en GET /webhooks/events. Cada empresa puede tener hasta 5 webhooks.
El secret para verificar la firma no viene en la respuesta de creación: genéralo con POST /webhooks/{id}/rotate-secret y guárdalo en ese momento (viene en dataRecords.secret y no se vuelve a mostrar).
Cada entrega es un POST JSON a tu URL con las cabeceras X-Webhook-Event, X-Webhook-Source y X-Webhook-Sig, y un cuerpo como este:
{
"id": "wh_01J9Z3K8Q4V7X2M5N6P8R0S1T2",
"event": "certificate_request.status_changed",
"created_at": "2026-10-08T11:41:33-05:00",
"data": {
"certificate_request_id": 636,
"company_id": 12,
"previous_status": "PROCESSING",
"new_status": "PROCESSED",
"changed_by_user_id": null,
"comment": null,
"issuance_flow": "BIOMETRIC"
}
}
Cuando recibas data.new_status = "PROCESSED", el certificado de la solicitud data.certificate_request_id ya está emitido: descárgalo como en el paso 5 del Caso 1. data.issuance_flow es el tipo de emisión de la solicitud (BIOMETRIC o DOCUMENTARY); solo llega null si la solicitud se eliminó antes de la entrega. certificate_request.created también lo trae.
Pagos. Desde la 1.21.1 también puedes recibir el resultado del pago de tus órdenes con payment.approved y payment.failed (ver Webhooks de pagos).
Verifica cada entrega. X-Webhook-Sig tiene el formato t={timestamp},v1={firma}, donde v1 es el HMAC-SHA256 (hexadecimal) del cuerpo crudo firmado con el secret del webhook. Rechaza las entregas cuyo t tenga más de 5 minutos de antigüedad.
const crypto = require('crypto');
function firmaValida(rawBody, header, secret) {
const partes = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (!partes.t || Math.abs(Date.now() / 1000 - Number(partes.t)) > 300) return false;
const esperada = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const recibida = Buffer.from(partes.v1 || '');
return recibida.length === esperada.length &&
crypto.timingSafeEqual(Buffer.from(esperada), recibida);
}
- Responde con un 2xx en menos de 10 segundos y procesa el evento después.
- Una entrega falla cuando no se puede conectar con tu URL o esta no responde en ese plazo. Las entregas fallidas no se reintentan, y tras 10 fallos consecutivos el webhook se desactiva (
is_active=false; se reactiva conPUT /webhooks/{id}). Concilia periódicamente conGET /certificate-request/{id}/issuancey revisa el historial enGET /webhooks/{id}/deliveries. - Generan entregas los eventos
certificate_request.*y, desde la 1.21.1,payment.approvedypayment.failed.certificate.expiringse acepta en la suscripción, pero todavía no genera entregas.
Webhooks de pagos: payment.approved y payment.failed
Desde la 1.21.1, si suscribes tu webhook a estos eventos, recibes el resultado del pago de tus órdenes sin consultar su estado una y otra vez:
| Evento | Cuándo llega | Campo propio |
|---|---|---|
payment.approved | La orden pasa a PAID: una compra de certificados (order_type = QUOTA_PURCHASE; sus cupos ya están disponibles) o una renovación (order_type = CERTIFICATE_RENEWAL) | paid_at (fecha del pago, ISO 8601) |
payment.failed | La orden pasa a FAILED porque la entidad financiera rechazó el pago | reason (motivo en texto plano, máx. 200 caracteres) |
Ambos traen en data los datos de la orden y de la transacción:
order_id: el UUID de la orden, el mismo que devuelvePOST /ordersy que aceptaGET /orders/{uuid}. Úsalo para relacionar la entrega con la compra.order_type:QUOTA_PURCHASE(compra de certificados) oCERTIFICATE_RENEWAL(renovación de un certificado). Una renovación llega conissuance_flow=BIOMETRIC,quantity= 1 yvigencia= 1, igual que la compra de un certificado biométrico, pero no agrega cupos: extiende la vigencia del certificado. Si sumas cupos al recibirpayment.approved, hazlo solo conQUOTA_PURCHASE.company_id,issuance_flow(BIOMETRICoDOCUMENTARY;nullsolo en órdenes antiguas sin tipo),quantityyvigenciade la compra.amount(texto con 2 decimales),currency,payment_provider,provider_reference,provider_transaction_idypayment_method_type.
{
"id": "wh_01JA2B3C4D5E6F7G8H9J0K1M2N",
"event": "payment.approved",
"created_at": "2026-10-17T10:15:02-05:00",
"data": {
"order_id": "9b1d2f7e-3c4a-4e8b-9f1a-2d3c4b5a6e7f",
"order_type": "QUOTA_PURCHASE",
"company_id": 7,
"issuance_flow": "DOCUMENTARY",
"quantity": 5,
"vigencia": 1,
"amount": "725000.00",
"currency": "COP",
"payment_provider": "WOMPI",
"provider_reference": "APP3-CERTS-20261017101458-ABC123",
"provider_transaction_id": "12345-1700000000-12345",
"payment_method_type": "CARD",
"paid_at": "2026-10-17T10:14:58-05:00"
}
}
En payment.failed, data trae los mismos campos con reason en lugar de paid_at (por ejemplo "reason": "Fondos insuficientes"; si la pasarela no informa el motivo, La entidad financiera no aprobó el pago.).
- Cada tipo de evento sale como máximo una vez por orden: los reenvíos de la pasarela, un rechazo tardío de una orden ya pagada o un segundo cobro aprobado no generan otra entrega.
- Una orden que recibió
payment.failedtodavía puede recibirpayment.approvedsi después se aprueba otro intento de pago de esa misma orden (por ejemplo, dos pagos conPOST /orders/{uuid}/payen curso, o una aprobación tardía). Manda el último evento recibido, o el estado de la orden: antes de crear una orden nueva tras unpayment.failed, consultaGET /orders/{uuid}y hazlo solo si sigue enFAILED, para no comprar dos veces. - Si la transacción queda en
VOIDEDoERROR, la orden sigue enPENDINGy no llega ningún evento: consultaGET /orders/{uuid}y vuelve a pagarla o elimínala. - Una orden de renovación que pasa a
FAILEDporque el certificado dejó de ser renovable no generapayment.failed: no es un pago rechazado. - Mantén
GET /orders/{uuid}como respaldo: las entregas no se reintentan, así que si tu servidor no estuvo disponible, el estado de la orden es la fuente de verdad. Por ejemplo, consulta las órdenes que lleven un rato enPENDINGsin que te haya llegado ningún evento. - Además del webhook, el usuario que creó la orden recibe un correo cuando el pago se aprueba o se rechaza.
Ver Webhooks.
🔄 Caso 3: Renovar certificados antes de que se revoquen
Los certificados de 1 año emitidos en solicitudes BIOMETRIC (con verificación de identidad) se pueden renovar por 1 año más sin reemitirlos: el cliente sigue usando el mismo certificado. Se puede renovar en cualquier momento desde la emisión y hasta 15 días después del vencimiento comercial (periodo de gracia, revocation_date). Si no se renueva a tiempo, el certificado se revoca automáticamente y deja de poder descargarse.
- Detecta los certificados por vencer con
GET /certificates/expiring?days=30(añade&issuance_flow=BIOMETRICpara ver solo los de emisión biométrica, que son los renovables). Cada elemento trae elidde la solicitud,days_remaining,urgency_leveleissuance_flow. Condaysnegativo (por ejemplo-15) obtienes los vencidos en ese periodo que no se renovaron. - Consulta la elegibilidad y el precio con
GET /certificate-request/{id}/issuance/renewal. Sieligibleesfalse,reasonexplica el motivo (NOT_RENEWABLE_PROVIDER,NOT_ACTIVE,MAX_TERMoGRACE_EXPIRED) ymessagetrae un texto listo para mostrar. Si es renovable,quotetrae el precio con IVA incluido (siempre con las tarifas de la emisión biométrica) ypending_orderla orden ya generada, si existe. - Genera la orden con
POST /certificate-request/{id}/issuance/renew(sin cuerpo). La orden viene endata. Si ya hay una orden de renovación pendiente, se devuelve esa misma y no se crea un cobro duplicado. Si el certificado no es renovable, responde 422 conreason. - Paga la orden. La orden de renovación no trae los datos de pago: pídelos con
POST /orders/{uuid}/retry(token de aceptación y firma de integridad) y abre el widget de WOMPI, o usa eseacceptance_tokenpara pagarla conPOST /orders/{uuid}/pay, y espera el webhookpayment.approved(ver Webhooks de pagos) o consultaGET /orders/{uuid}hasta que quede enPAID. Al aprobarse el pago se extienden la vigencia y la fecha de vencimiento del mismo certificado.
async function renovar(id) {
const { dataRecords } = await api(`/certificate-request/${id}/issuance/renewal`);
const estado = dataRecords.data;
if (!estado.eligible) return { renovable: false, motivo: estado.reason, mensaje: estado.message };
const { data: orden } = await api(`/certificate-request/${id}/issuance/renew`, { method: 'POST' });
return { renovable: true, ordenUuid: orden.uuid, total: orden.total_amount }; // págala con los endpoints de Órdenes
}
Ver Emisión de Certificados, Notificaciones y Órdenes.
🛑 Caso 4: Corregir o anular un certificado
La solicitud tiene datos erróneos y aún no se emitió el certificado
Recházala con POST /certificate-request/{id}/issuance/reject (ID numérico). Solo aplica a solicitudes BIOMETRIC en estado PROCESSING (con una solicitud DOCUMENTARY responde 422). El cuerpo es obligatorio:
{ "reason": "Los datos del suscriptor se registraron con errores." }
reasondebe tener de 5 a 400 caracteres y queda en el historial de la solicitud.- La solicitud queda
CANCELLEDy se reintegra el cupo (dataRecords.quota_released). - El rechazo es definitivo: la misma solicitud no puede volver a emitirse, así que debes crear una solicitud nueva con los datos corregidos.
- Antes de rechazar, consulta
dataRecords.data.can_rejectenGET /certificate-request/{id}/issuance. Es orientativo: la decisión final la toma la autoridad de registro. - Si el certificado ya fue emitido, la API responde 409: en ese caso corresponde revocarlo.
El certificado ya fue emitido y debe anularse
Por ejemplo, por clave comprometida o cese de operaciones. Revócalo con POST /certificate-request/{uuid}/revoke, usando el UUID de la solicitud (no su ID numérico). Solo aplica a certificados emitidos en solicitudes BIOMETRIC y a solicitudes en estado PROCESSED:
{
"revoking_code": "<dataRecords.data.revocation_code de GET /certificate-request/{id}/issuance>",
"revocation_reason": 1
}
Motivos (revocation_reason): 0 Sin especificar, 1 Clave comprometida, 2 Autoridad de certificación comprometida, 3 Ha cambiado la afiliación, 4 Sustitución, 5 Cese de operaciones, 9 Permisos retirados, 10 AA comprometida.
La revocación no se puede deshacer: la solicitud pasa a REVOKED y el certificado deja de poder descargarse.
Ver Viafirma (verificación de identidad, rechazo y revocación).
📊 Caso 5: Reportes de consumo por tipo de emisión
Si tu empresa usa los dos tipos de emisión (o cambió de tipo con el tiempo), puedes separar tus reportes entre biométrica y documental. Estas consultas aceptan el filtro opcional ?issuance_flow=BIOMETRIC o ?issuance_flow=DOCUMENTARY y, con o sin filtro, traen el desglose por tipo:
| Consulta | Qué devuelve por tipo de emisión |
|---|---|
GET /certificate-request (listado) | Cada solicitud trae issuance_flow; el filtro limita el listado a ese tipo |
GET /consume/{year} y GET /consume/{year}/{month} | Cada fila trae by_flow ({ "BIOMETRIC": n, "DOCUMENTARY": m }) |
GET /certificate-request/stats/{companyId} | Cada año trae by_flow con total y statuses por tipo; además grand_total_by_flow y quota.by_flow |
GET /certificates/expiring?days= | Cada certificado trae issuance_flow; además total_by_flow |
- Sin filtro, las respuestas traen los mismos totales que antes y suman el desglose. Con filtro, los totales son solo de ese tipo y
by_flowsigue presente, con el otro tipo en 0. - Las respuestas de estadísticas indican en
issuance_flowel filtro que se aplicó (nullsi no hubo filtro). Unissuance_flowvacío cuenta como si no lo enviaras. - Un valor distinto de
BIOMETRICoDOCUMENTARYresponde 422 conerror_code=INVALID_ISSUANCE_FLOWy el detalle enerrors.issuance_flow.
// Certificados emitidos este año, separados por tipo de emisión
const { data: filas } = await api(`/consume/${new Date().getFullYear()}`);
const porTipo = filas.reduce((acc, fila) => {
acc.BIOMETRIC += fila.by_flow.BIOMETRIC;
acc.DOCUMENTARY += fila.by_flow.DOCUMENTARY;
return acc;
}, { BIOMETRIC: 0, DOCUMENTARY: 0 });
Ver Consumo, Solicitudes de Certificado y Notificaciones.
🚀 Siguientes pasos
- Crea tu cuenta y tu token en Sandbox siguiendo la Guía Paso a Paso.
- Consulta los endpoints usados en esta página: Solicitudes de Certificado, Emisión de Certificados, Viafirma, Webhooks, Cupos, Precios, Órdenes y Consumo.
- Si algo falla, revisa Solución de Problemas.