Saltar al contenido principal
Versión: 1.21.1

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

¿Qué hace la API y qué hace tu sistema?

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ón https://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_flowQué significaCómo se tramita
BIOMETRICBiométrica (con verificación de identidad)El representante legal valida su identidad con un enlace. La emisión arranca sola al crear la solicitud
DOCUMENTARYDocumental (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: solo BIOMETRIC.
  • mail: solo DOCUMENTARY.
  • dual: los dos. Se elige en cada solicitud con issuance_flow; si no se envía, la solicitud es DOCUMENTARY.

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.

  1. Se usa primero el cupo pospago vigente de ese tipo, que sirve para cualquier vigencia.
  2. 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).
  3. Si no hay ninguno, la creación responde 402 con error_code = QUOTA_EXHAUSTED, un message como 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. y available_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.BIOMETRIC y by_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 con selectable: 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_at es el vencimiento más próximo), o null si no hay.
    • prepaid_1_year / prepaid_2_year / prepaid_items_available: certificados prepago disponibles.
    • has_quota y selectable.
  • has_quota_any_flow: true si hay cupo en algún tipo que tu empresa puede usar.
  • issuance_provider: el valor vigente de tu empresa (viafirma, mail o dual).
  • has_agreement: true si tu empresa tiene convenio (pospago) y false si es prepago. Es el valor vigente y tiene precedencia sobre el company.has_agreement del 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 (con dual, DOCUMENTARY): coinciden con lo que consumirá una solicitud creada sin issuance_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 (o DOCUMENTARY) devuelve los rangos de precio de ese tipo; sin el parámetro, los del tipo por defecto de tu empresa. Cada rango trae su issuance_flow. Con quantity y vigencia (por ejemplo GET /pricing?issuance_flow=DOCUMENTARY&quantity=5&vigencia=1) obtienes la cotización, con tier_name e issuance_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 (quantity mayor o igual a 1, vigencia 1 o 2 e issuance_flow opcional; sin él, el tipo por defecto). Los certificados comprados solo se usan en solicitudes de ese tipo. La respuesta, dentro de data, trae order_id, los datos para pagar con el widget de WOMPI, issuance_flow y tier. Si no hay un rango de precios para esa cantidad, responde 422 con error_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 webhooks payment.approved y payment.failed (ver Caso 2) o consulta GET /orders/{uuid} hasta que el estado sea PAID (aprobado) o FAILED (rechazado). Una orden FAILED no se reintenta, pero todavía puede pasar a PAID si 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 en PENDING y latest_transaction.status es VOIDED o ERROR, vuelve a pagarla o elimínala. Solo con PAID los certificados quedan disponibles. GET /orders y GET /orders/{uuid} también traen el issuance_flow de cada orden.
  • Errores de tipo: en /pricing y /orders, un issuance_flow que no es BIOMETRIC ni DOCUMENTARY responde 422 INVALID_ISSUANCE_FLOW, y uno que tu empresa no tiene habilitado, 422 ISSUANCE_FLOW_NOT_ALLOWED.

Ver Cupos, Órdenes y Precios.


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

  1. 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 en issuance_flow. La respuesta trae el id numérico de la solicitud y su issuance_flow.
  2. 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 con POST /certificate-request/{id}/issue.
  3. 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 = accreditation en GET /certificate-request/{id}/issuance), MATICERTS lo envía al correo de tu empresa; también puedes obtenerlo con GET /certificate-request/{id}/kyc-link y enviarlo desde tu propio sistema.
  4. Esperar la emisión. Es asíncrona: recibe el webhook certificate_request.status_changed (ver Caso 2) o consulta GET /certificate-request/{id}/issuance cada cierto tiempo.
  5. 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 };
}
Campos de la solicitud

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) y terms_version_id (la versión vigente de GET /terms/current; si no es la vigente, responde 400).
  • Persona Jurídica (type_organization_id = 1): también entity_document_type_id.
  • Representante legal: legal_rep_first_name y legal_rep_last_name (o legal_representative con el nombre completo).
  • Tipo de emisión (issuance_flow, opcional): BIOMETRIC o DOCUMENTARY. Sin él, el tipo por defecto de tu empresa (viafirma → BIOMETRIC; mail o dual → DOCUMENTARY).
  • Solicitud BIOMETRIC: legal_rep_email, legal_rep_first_name y legal_rep_last_name son obligatorios; los adjuntos son opcionales.
  • Solicitud DOCUMENTARY: attachments es 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 es dual y no enviaste issuance_flow ni adjuntos, el mensaje de errors.attachments te recuerda que envíes issuance_flow=BIOMETRIC para una emisión biométrica.
  • Opcionales: phone, mobile e info.

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.

Espera y descarga
  • dataRecords.status en GET /certificate-request/{id}/issuance vale submitted, processing, ready o failed. Si es failed, 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 ver ready, 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 (.p12 y 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);
}
Entregas sin reintento
  • 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 con PUT /webhooks/{id}). Concilia periódicamente con GET /certificate-request/{id}/issuance y revisa el historial en GET /webhooks/{id}/deliveries.
  • Generan entregas los eventos certificate_request.* y, desde la 1.21.1, payment.approved y payment.failed. certificate.expiring se 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:

EventoCuándo llegaCampo propio
payment.approvedLa 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.failedLa orden pasa a FAILED porque la entidad financiera rechazó el pagoreason (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 devuelve POST /orders y que acepta GET /orders/{uuid}. Úsalo para relacionar la entrega con la compra.
  • order_type: QUOTA_PURCHASE (compra de certificados) o CERTIFICATE_RENEWAL (renovación de un certificado). Una renovación llega con issuance_flow = BIOMETRIC, quantity = 1 y vigencia = 1, igual que la compra de un certificado biométrico, pero no agrega cupos: extiende la vigencia del certificado. Si sumas cupos al recibir payment.approved, hazlo solo con QUOTA_PURCHASE.
  • company_id, issuance_flow (BIOMETRIC o DOCUMENTARY; null solo en órdenes antiguas sin tipo), quantity y vigencia de la compra.
  • amount (texto con 2 decimales), currency, payment_provider, provider_reference, provider_transaction_id y payment_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.failed todavía puede recibir payment.approved si después se aprueba otro intento de pago de esa misma orden (por ejemplo, dos pagos con POST /orders/{uuid}/pay en 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 un payment.failed, consulta GET /orders/{uuid} y hazlo solo si sigue en FAILED, para no comprar dos veces.
  • Si la transacción queda en VOIDED o ERROR, la orden sigue en PENDING y no llega ningún evento: consulta GET /orders/{uuid} y vuelve a pagarla o elimínala.
  • Una orden de renovación que pasa a FAILED porque el certificado dejó de ser renovable no genera payment.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 en PENDING sin 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.

  1. Detecta los certificados por vencer con GET /certificates/expiring?days=30 (añade &issuance_flow=BIOMETRIC para ver solo los de emisión biométrica, que son los renovables). Cada elemento trae el id de la solicitud, days_remaining, urgency_level e issuance_flow. Con days negativo (por ejemplo -15) obtienes los vencidos en ese periodo que no se renovaron.
  2. Consulta la elegibilidad y el precio con GET /certificate-request/{id}/issuance/renewal. Si eligible es false, reason explica el motivo (NOT_RENEWABLE_PROVIDER, NOT_ACTIVE, MAX_TERM o GRACE_EXPIRED) y message trae un texto listo para mostrar. Si es renovable, quote trae el precio con IVA incluido (siempre con las tarifas de la emisión biométrica) y pending_order la orden ya generada, si existe.
  3. Genera la orden con POST /certificate-request/{id}/issuance/renew (sin cuerpo). La orden viene en data. 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 con reason.
  4. 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 ese acceptance_token para pagarla con POST /orders/{uuid}/pay, y espera el webhook payment.approved (ver Webhooks de pagos) o consulta GET /orders/{uuid} hasta que quede en PAID. 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." }
  • reason debe tener de 5 a 400 caracteres y queda en el historial de la solicitud.
  • La solicitud queda CANCELLED y 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_reject en GET /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.

Irreversible

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:

ConsultaQué 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_flow sigue presente, con el otro tipo en 0.
  • Las respuestas de estadísticas indican en issuance_flow el filtro que se aplicó (null si no hubo filtro). Un issuance_flow vacío cuenta como si no lo enviaras.
  • Un valor distinto de BIOMETRIC o DOCUMENTARY responde 422 con error_code = INVALID_ISSUANCE_FLOW y el detalle en errors.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​