Saltar al contenido principal
Versión: 1.21.1

🔧 Solución de Problemas

Aquí encontrarás soluciones a los problemas más comunes al usar MATICERTS.

Los ejemplos usan la URL de Sandbox (https://sandbox-api.maticerts.com/api/v1). En Producción usa https://api.maticerts.com/api/v1.


🔐 Problemas de Autenticación​

Error 401: "No autenticado."​

Síntomas (HTTP 401):

{
"success": false,
"message": "No autenticado."
}

La API devuelve la misma respuesta si el token Bearer falta, está mal formado, expiró o fue revocado.

Causas posibles:

  • Falta la cabecera Authorization o el prefijo Bearer
  • Token mal copiado (incompleto o con espacios)
  • Token expirado: los tokens emitidos por POST /api/v1/auth/login y los Personal Access Tokens (PAT) vencen a los 90 días de su emisión. En el token de login, el campo expires_at de la respuesta indica la fecha exacta; en un PAT creado con expires_in_days distinto de 90, no (ver la advertencia al final de esta sección).
  • Token revocado con GET /api/v1/auth/logout, DELETE /api/v1/tokens/{id} o POST /api/v1/tokens/revoke-all

Soluciones:

  1. Verifica el formato del token:

    # ❌ INCORRECTO
    curl -H "Authorization: eyJhbGciOiJIUzI1NiIs..."

    # ✅ CORRECTO
    curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
  2. Comprueba si el token sigue vigente:

    curl -X GET "https://sandbox-api.maticerts.com/api/v1/auth/user" \
    -H "Authorization: Bearer $TOKEN"
    # 200 = vigente · 401 = expirado, revocado o inválido
  3. Obtén un token nuevo con login (dura 90 días por defecto):

    curl -X POST "https://sandbox-api.maticerts.com/api/v1/auth/login" \
    -H "Content-Type: application/json" \
    -d '{
    "email": "tu-email@empresa.com",
    "password": "tu-contraseña"
    }'
  4. Si usas un Personal Access Token (PAT):

    • Un token expirado o revocado no sirve para consultarse ni renovarse a sí mismo. Autentícate primero con un token válido: el de sesión que obtienes con login (paso 3) u otro PAT vigente.
    • Lista tus tokens vigentes con GET /api/v1/tokens (o en la aplicación web: Ajustes → Tokens de API). Los tokens expirados o revocados no aparecen en ese listado, así que guarda el id que devuelve la creación. Con GET /api/v1/tokens/{id} consultas el estado de un token (revoked, expires_at).
    • Si expiró o fue revocado, renuévalo con POST /api/v1/tokens/{id}/renew. Se emite un token nuevo de 90 días con los mismos alcances y el anterior queda revocado. Esta llamada no cuenta para el límite de creación de tokens.
    • El valor del token (data.token) solo se muestra en la respuesta de creación o de renovación. Guárdalo en ese momento.
    • Crear tokens con POST /api/v1/tokens está limitado a 10 intentos por usuario en 24 horas, contando también los rechazados por validación. Al superarlo, la API responde 429.
📅 Vigencia de los PAT

Todo PAT es válido 90 días desde su creación, sin importar el expires_in_days con que se creó: ese valor define la fecha expires_at que muestran la respuesta y GET /api/v1/tokens. Si el PAT tiene más de 90 días, responde 401 aunque su expires_at sea posterior. Si se creó con menos de 90 días, desaparece de GET /api/v1/tokens al pasar su expires_at, pero sigue autenticando hasta el día 90. Planifica la rotación a los 90 días de la creación.


Error: "Credenciales inválidas." y otros rechazos del login​

Síntomas (POST /api/v1/auth/login). Las respuestas de error del login incluyen "success": false y un message, salvo el 429 del límite general de peticiones, que responde {"message": "Too Many Attempts."} (ver la sección Error 429 más abajo), y el 500 excepcional {"message": "Server Error"} cuando la cuenta no tiene una empresa asociada válida (en ese caso, contacta al soporte):

HTTPmessageQué hacer
404Credenciales inválidas.Revisa el email y la contraseña: mayúsculas y espacios al inicio o al final. Si no la recuerdas, restablécela (ver abajo).
404El usuario se encuentra inactivo. Comuníquese con el administrador.Contacta al soporte.
400El correo electrónico no ha sido verificado. Por favor, revise su bandeja de entrada y siga las instrucciones para verificar su cuenta.Abre el enlace del correo de verificación. Para pedir uno nuevo usa POST /api/v1/email/verification-notification con {"email": "tu-email"}.
404La empresa se encuentra inactiva. Comuníquese con el administrador.Contacta al soporte.
422Los datos proporcionados no son válidos. (con errors)Envía email con un formato válido y password. remember_me, si lo envías, debe ser booleano.
{
"success": false,
"message": "Credenciales inválidas."
}

Para restablecer tu contraseña:

  • Abre la pantalla de recuperación de la aplicación web: Producción · Sandbox. También puedes entrar desde el inicio de sesión con «¿Olvidaste tu contraseña?». Por API, envía POST /api/v1/forgot-password con {"email": "tu-email@empresa.com"}; este endpoint no requiere autenticación.
  • Recibirás un correo con un enlace válido durante 60 minutos. Solo se permite una solicitud cada 60 segundos; si repites antes, la respuesta es 422 con el mensaje en errors.email.
  • Define la nueva contraseña desde el enlace del correo (o con POST /api/v1/reset-password) e inicia sesión de nuevo.

📝 Problemas con Solicitudes​

Error: "Solicitud no encontrada"​

Síntomas:

  • GET /api/v1/certificate-request/{id} responde 200, pero con "dataRecords": { "data": [null] }
  • Los endpoints de emisión responden 404 La solicitud de certificado {id} no existe.
  • Los de descarga o revocación responden 404 Solicitud de certificado no encontrada.

Soluciones:

  1. Usa el identificador correcto: los endpoints con {id} esperan el ID numérico (p. ej. 636). Los de descarga y revocación (/{uuid}/issuance/download, /{uuid}/issuance/download/base64, /{uuid}/revoke) esperan el uuid de la solicitud. La respuesta de creación no incluye el uuid: obténlo con GET /api/v1/certificate-request/{id}.

    curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request/636" \
    -H "Authorization: Bearer $TOKEN"
  2. Comprueba que la solicitud pertenece a tu empresa: solo ves las solicitudes de la empresa del usuario del token. Puedes buscarlas por NIT:

    curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request?query=900455420" \
    -H "Authorization: Bearer $TOKEN"
  3. Verifica que no haya sido eliminada:

    • Las solicitudes no caducan ni se archivan automáticamente.
    • Una solicitud solo desaparece si alguien de tu empresa la elimina con DELETE /api/v1/certificate-request/{id}. Ese borrado es definitivo, devuelve el cupo y solo se permite en estado DRAFT o REJECTED; en cualquier otro estado la API responde 409.

Error: "Los datos de la solicitud son inválidos."​

Síntomas (HTTP 400 en POST /api/v1/certificate-request):

{
"message": "Los datos de la solicitud son inválidos.",
"errors": {
"legal_rep_email": ["El correo del representante legal es requerido para la emisión con verificación biométrica"]
}
}

En los demás endpoints los errores de validación responden 422 con {"success": false, "message": "Los datos proporcionados no son válidos.", "errors": {...}}. En ambos casos errors es un mapa de campo a lista de mensajes.

Soluciones:

  1. Envía todos los campos obligatorios:

    const solicitud = {
    country_id: 45, // ✅ Requerido (GET /api/v1/countries)
    city_id: 149, // ✅ Requerido (GET /api/v1/cities)
    identity_document_id: 1, // ✅ Requerido (GET /api/v1/identity-documents)
    type_organization_id: 1, // ✅ Requerido (GET /api/v1/organization-type)
    entity_document_type_id: 1, // ✅ Requerido si type_organization_id = 1 (GET /api/v1/entity-document-types)
    document_number: "1234567890", // ✅ Requerido: documento del representante legal
    address: "Calle 123 # 45-67", // ✅ Requerido
    company_name: "MI EMPRESA S.A.S.", // ✅ Requerido
    dni: "900455420", // ✅ Requerido: NIT sin dígito de verificación
    legal_rep_first_name: "JUAN PABLO", // Requerido con issuance_flow BIOMETRIC (en DOCUMENTARY: estos dos o legal_representative)
    legal_rep_last_name: "PÉREZ GÓMEZ", // Requerido con issuance_flow BIOMETRIC
    legal_rep_email: "juan.perez@empresa.com", // Requerido con issuance_flow BIOMETRIC (dominio con DNS válido)
    life: 1, // ✅ Requerido: 1 o 2 años
    issuance_flow: "BIOMETRIC", // Opcional: BIOMETRIC o DOCUMENTARY (sin él, el tipo por defecto de tu empresa)
    accept_terms: true, // ✅ Requerido
    terms_version_id: 3 // ✅ Requerido: id de GET /api/v1/terms/current
    };

    Los IDs del ejemplo son ilustrativos: tómalos siempre de los catálogos.

  2. Usa los catálogos, no textos libres: los tipos de documento, organización, país y ciudad son IDs numéricos de los endpoints de Datos Maestros. terms_version_id debe ser la versión vigente que devuelve GET /api/v1/terms/current.

  3. Adjuntos: viajan en Base64 dentro del JSON (attachments[].base64), no como multipart. Dependen del tipo de emisión de la solicitud (issuance_flow): en DOCUMENTARY son obligatorios, de 2 a 3 archivos, 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); en BIOMETRIC son opcionales.

  4. Te piden adjuntos y querías una emisión biométrica: si tu empresa es dual y no envías issuance_flow, la solicitud se crea DOCUMENTARY. En ese caso errors.attachments dice Los adjuntos son requeridos para la emisión documental. La empresa admite ambos flujos; envíe issuance_flow=BIOMETRIC para emisión biométrica. Envía "issuance_flow": "BIOMETRIC" con el correo y los nombres del representante legal.


Error al crear una solicitud: sin cupo, duplicada o empresa inactiva​

Sin cupo disponible (HTTP 402):

{
"success": false,
"error_code": "QUOTA_EXHAUSTED",
"message": "No tiene certificados disponibles de tipo Biométrica (con verificación de identidad) para vigencia de 1 año(s). Debe adquirir un paquete de certificados para continuar.",
"issuance_flow": "BIOMETRIC",
"vigencia": 1,
"available_in_other_flows": [{ "issuance_flow": "DOCUMENTARY", "available": 3 }]
}
  • Consulta tu disponibilidad por tipo de emisión con GET /api/v1/quota/status (by_flow.<TIPO>.available_1_year y available_2_year).
  • available_in_other_flows lista los otros tipos habilitados para tu empresa que tienen cupo para esa vigencia (puede venir vacío). Si te sirve, crea la solicitud con ese issuance_flow.
  • Revisa los precios del tipo que necesitas en GET /api/v1/pricing?issuance_flow=<TIPO>, compra con POST /api/v1/orders (con el mismo issuance_flow) y paga con POST /api/v1/orders/{uuid}/pay.
  • Los certificados prepago son por vigencia y por tipo de emisión: un paquete de 2 años no habilita solicitudes con life: 1, y uno documental no habilita solicitudes biométricas. Compra el tipo y la vigencia que vas a solicitar. Un cupo pospago vigente sirve para cualquier vigencia, pero solo para su tipo de emisión.

Solicitud duplicada (HTTP 400):

{
"success": false,
"message": "Ya existe una solicitud de certificado, en proceso, con el mismo NIT y DV. Por favor verifique el estado de la solicitud."
}
  • Tu empresa no puede tener dos solicitudes con el mismo NIT y DV en estado DRAFT, SENT, PENDING, ACCEPTED o PROCESSING.
  • Búscala con GET /api/v1/certificate-request?query=900455420 (tu NIT) y continúa con la existente.
  • Si está en DRAFT y no la vas a usar, elimínala con DELETE /api/v1/certificate-request/{id} (se devuelve el cupo) y después crea la nueva. Si es una solicitud BIOMETRIC ya en trámite y sus datos son incorrectos, recházala (ver «Quiero anular una solicitud con datos erróneos» más abajo).

Empresa inactiva (HTTP 403): la empresa asociada a tu usuario está inactiva y no puede operar. Contacta al soporte.


Errores del tipo de emisión (issuance_flow)​

HTTPerror_codeDóndeQué hacer
400INVALID_ISSUANCE_FLOWPOST /api/v1/certificate-requestEnvía BIOMETRIC o DOCUMENTARY, o no envíes el campo para usar el tipo por defecto de tu empresa.
400ISSUANCE_FLOW_NOT_ALLOWEDPOST /api/v1/certificate-requestTu empresa no tiene habilitado ese tipo. Revisa issuance_provider en GET /api/v1/quota/status: viafirma solo admite BIOMETRIC, mail solo DOCUMENTARY y dual los dos.
400ISSUANCE_FLOW_UNAVAILABLEPOST /api/v1/certificate-requestLa 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, usa DOCUMENTARY.
400ISSUANCE_FLOW_IMMUTABLEPUT /api/v1/certificate-request/{id}El tipo de una solicitud no se puede cambiar. No envíes issuance_flow al editar; para usar otro tipo, elimina la solicitud (solo en DRAFT o REJECTED) y crea una nueva.
402QUOTA_EXHAUSTEDPOST /api/v1/certificate-requestSin cupo para ese tipo y esa vigencia (ver «Sin cupo disponible» arriba).
409ISSUANCE_FLOW_ACTION_NOT_ALLOWEDPOST /api/v1/certificate-request/{id}/issueEnviaste request_status o user_of_change en el cuerpo de /issue sobre una solicitud BIOMETRIC. No envíes esos campos: las solicitudes biométricas avanzan solas con la verificación de identidad del titular.
422ISSUANCE_PROVIDER_MISMATCHPOST /api/v1/certificate-request/{id}/issueEl provider del cuerpo no corresponde al tipo de la solicitud. No envíes provider.
422INVALID_ISSUANCE_FLOWGET /api/v1/pricing, POST /api/v1/orders, filtros ?issuance_flow= de listados y estadísticasUsa BIOMETRIC o DOCUMENTARY. En los filtros, un valor vacío cuenta como si no lo enviaras.
422ISSUANCE_FLOW_NOT_ALLOWEDGET /api/v1/pricing, POST /api/v1/ordersTu empresa no tiene habilitado ese tipo: no puedes cotizarlo ni comprarlo.
422PRICING_TIER_NOT_FOUNDGET /api/v1/pricing (cotización), POST /api/v1/ordersNo hay un rango de precios para esa cantidad en ese tipo de emisión. Revisa los rangos con GET /api/v1/pricing?issuance_flow=<TIPO>.

GET /api/v1/pricing devuelve una lista vacía ([]): no es un error. Ese tipo de emisión no está disponible para compra en tu plan; no ofrezcas esa compra.

En todos los casos, muestra el message de la respuesta: describe el motivo en lenguaje de usuario.


⏳ Problemas de Estado y Progreso​

La solicitud no avanza​

Síntomas:

  • request_status sigue en DRAFT, SENT, PENDING, ACCEPTED o PROCESSING durante horas
  • GET /api/v1/certificate-request/{id}/issuance devuelve dataRecords.status = submitted o processing

Soluciones:

  1. Consulta el estado de la emisión:

    curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request/636/issuance" \
    -H "Authorization: Bearer $TOKEN"

    dataRecords.status puede valer submitted, processing, ready o failed. En una solicitud BIOMETRIC, dataRecords.data incluye estos campos:

    • internal_state y remote_status
    • accredited_status: resultado de la verificación de identidad
    • last_public_note: motivo concreto que informa Viafirma
    • last_error_message
    • expires_at: fecha límite para completar la verificación de identidad
    • can_reject: si hoy se puede rechazar el trámite
  2. Acciones según el caso (solicitud BIOMETRIC):

    • remote_status = accreditation: el suscriptor debe completar la verificación de identidad (KYC). Obtén el enlace con GET /api/v1/certificate-request/{id}/kyc-link y envíaselo. Si responde 422, el enlace no se puede obtener ahora: lee message. Si el estado remoto todavía es anterior a accreditation, vuelve a consultar más tarde; si ya es posterior (por ejemplo fail o accreditation_rejected) o Viafirma respondió con un error, reintentar no lo resolverá: revisa el trámite con GET /api/v1/certificate-request/{id}/issuance. Justo después de crear la solicitud, mientras el trámite de Viafirma aún no existe, responde 404 (Recurso no encontrado.): vuelve a consultar más tarde. Si el 404 dice Solicitud de certificado no encontrada., el id no existe o pertenece a otra empresa.
    • kyc_flow_completed_at informado pero la solicitud no avanza: el suscriptor terminó el flujo, pero la aprobación la confirma Viafirma. El estado se actualiza solo: el sistema consulta a Viafirma cada 60 segundos.
    • accredited_status = NOT_MATCHING o REVIEW, o last_error_message / last_public_note informados: contienen el motivo reportado por el proveedor. NOT_MATCHING significa que los datos del documento no coinciden; REVIEW, que un operador de la autoridad de registro la está revisando.
    • Datos incorrectos: si can_reject es true, rechaza el trámite con POST /api/v1/certificate-request/{id}/issuance/reject (es definitivo) y crea una solicitud nueva.
    • Se acerca expires_at: si el suscriptor no completa el KYC dentro del plazo, la solicitud se cancela (ver la sección siguiente).
  3. Acciones según el caso (solicitud DOCUMENTARY):

    • Solicitud en DRAFT: envíala con POST /api/v1/certificate-request/{id}/issue, que necesita al menos un adjunto de tipo ATTACHED. Es la llamada que remite los soportes para la emisión y pasa la solicitud a PROCESSING. PUT /api/v1/certificate-request/{id}/status con SENT solo cambia el estado: la solicitud queda en SENT a la espera de revisión por MATICERTS, sin enviar los soportes a la emisión (en Sandbox tampoco se genera el certificado de prueba).
    • Solicitud en SENT, PENDING, ACCEPTED o PROCESSING: está en revisión. Si nunca llamaste a POST /api/v1/certificate-request/{id}/issue (por ejemplo, la pasaste a SENT con PUT /status), los soportes no se han enviado: llama a /issue. Si ya lo hiciste, solo queda esperar.
  4. POST /issue responde 422 por provider o email_certificate: no envíes provider en el cuerpo: el proveedor lo decide el tipo de emisión de la solicitud, y un provider que no corresponde a ese tipo responde 422 con error_code = ISSUANCE_PROVIDER_MISMATCH. Si envías "provider": "viafirma", email_certificate es obligatorio. En una solicitud BIOMETRIC no necesitas llamar a /issue: la emisión arranca sola al crear la solicitud (solo se usa para reintentar una emisión fallida).


La solicitud pasó a CANCELLED sin que la cancelaras​

Síntomas:

  • request_status = CANCELLED, y en GET /api/v1/certificate-request/{id}/issuance el trámite aparece con internal_state = EXPIRED, last_error_code = POLL_EXPIRED y dataRecords.status = failed.

Causa: en una solicitud BIOMETRIC, el suscriptor tiene 96 horas (4 días) desde el envío del trámite para completar la verificación de identidad (KYC). La fecha límite está en dataRecords.data.expires_at. 24 horas antes del vencimiento se envía un aviso de último llamado al correo de la empresa. Si el plazo vence sin que el suscriptor haya completado el KYC, un proceso automático que corre cada hora rechaza el trámite en Viafirma, lo pasa a EXPIRED, cancela la solicitud y reintegra a tu empresa el cupo consumido. Si el suscriptor alcanzó a avanzar en Viafirma antes del vencimiento, el trámite no se cancela y sigue su curso.

Soluciones:

  1. Crea una solicitud nueva. El cupo ya está disponible de nuevo.
  2. Cuando el enlace de verificación esté disponible, compártelo de inmediato con el suscriptor (GET /api/v1/certificate-request/{id}/kyc-link) y pídele completarlo antes de expires_at.

No confundas CANCELLED con EXPIRED en request_status: EXPIRED indica un certificado ya emitido cuya vigencia terminó.


📥 Problemas de Descarga​

Error: el certificado no se puede descargar​

Síntomas (GET /api/v1/certificate-request/{uuid}/issuance/download o /issuance/download/base64; el parámetro es el UUID de la solicitud, no su ID numérico):

HTTPmessageSignificado
409El certificado no está vigente y no se puede descargar.La solicitud no está en PROCESSED. El campo status trae su estado actual: DRAFT, SENT, PENDING, ACCEPTED, PROCESSING (aún en trámite; DRAFT en una solicitud DOCUMENTARY significa que falta llamar a POST /api/v1/certificate-request/{id}/issue) o REVOKED, EXPIRED, CANCELLED, REJECTED (ya no se entrega).
404Solicitud de certificado no encontrada.El UUID no existe o pertenece a otra empresa.
404Archivo de certificado no encontrado.La solicitud está en PROCESSED, pero no hay un archivo de certificado disponible para descargar por la API.
404Archivo no disponible en almacenamiento.El archivo ya no existe en el almacenamiento (p. ej., se purgó tras el vencimiento).
410No se pudo leer el archivo del almacenamiento.Solo en /download/base64: el archivo está vacío o no se pudo leer.
{
"success": false,
"message": "El certificado no está vigente y no se puede descargar.",
"status": "PROCESSING"
}

Soluciones:

  1. Comprueba el estado de la emisión: GET /api/v1/certificate-request/{id}/issuance (ID numérico). Puedes descargar cuando dataRecords.status sea ready y la solicitud esté en PROCESSED.
  2. Si status es DRAFT: la solicitud aún no se ha enviado a emisión. Envíala con POST /api/v1/certificate-request/{id}/issue (ver «Solicitud en DRAFT» más arriba).
  3. Si status es SENT, PENDING, ACCEPTED o PROCESSING: espera. El tiempo no es fijo: depende de que el suscriptor complete la verificación de identidad (el trámite vence a las 96 horas) y de la autoridad certificadora. Con Viafirma, si remote_status es accreditation, reenvía al suscriptor el enlace de GET /api/v1/certificate-request/{id}/kyc-link. Suscríbete al webhook certificate_request.status_changed para no tener que consultar periódicamente.
  4. Si status es REVOKED, EXPIRED, CANCELLED o REJECTED: el certificado ya no se entrega. Si se revocó por no renovarse a tiempo, revisa la sección «Renovación, revocación y rechazo» más abajo.
  5. Si recibes un 404 de archivo o un 410 con la solicitud en PROCESSED: contacta al soporte e indica el UUID de la solicitud.

Error: "Archivo corrupto" al abrir el certificado​

Síntomas:

  • El archivo descargado no se puede abrir o importar
  • file muestra Zip archive data o JSON data en lugar de un certificado

Causas frecuentes:

  • Guardar la respuesta JSON del endpoint como si fuera el .p12: ninguno de los dos endpoints de descarga devuelve el binario; ambos responden con JSON.
  • Abrir el .zip como .p12: el archivo entregado es un .zip que contiene el .p12 (p. ej. empresa-sas_636.zip → empresa-sas_636.p12).

Soluciones:

  1. Obtén la URL y el PIN (usa el uuid de la solicitud, no su ID numérico):

    curl -X GET "https://sandbox-api.maticerts.com/api/v1/certificate-request/$UUID/issuance/download" \
    -H "Authorization: Bearer $TOKEN"
    # dataRecords.data: download_url (válida 24 h), p12_pin, p12_filename, expires_at
  2. Descarga el .zip desde download_url, descomprímelo y abre el .p12:

    curl -L "$DOWNLOAD_URL" -o certificado.zip
    file certificado.zip # Debería mostrar: "Zip archive data"
    unzip certificado.zip # Extrae el .p12
    openssl pkcs12 -in empresa-sas_636.p12 -info -noout -passin pass:$P12_PIN

    Usa como contraseña del .p12 el valor de p12_pin. Si p12_pin vale NO_PIN, contacta al soporte. En una solicitud DOCUMENTARY, el .zip puede venir protegido con contraseña: el NIT del titular, o el NIT seguido del DV si es persona jurídica.

  3. Integraciones desatendidas (ERP, scripts) o download_url en null: usa GET /api/v1/certificate-request/{uuid}/issuance/download/base64. p12_base64 es el Base64 del .zip:

    curl -s -H "Authorization: Bearer $TOKEN" \
    "https://sandbox-api.maticerts.com/api/v1/certificate-request/$UUID/issuance/download/base64" > resp.json
    jq -r .dataRecords.data.p12_base64 resp.json | base64 -d > certificado.zip
    unzip certificado.zip
  4. Si el problema persiste: contacta al soporte indicando el UUID de la solicitud.


🔁 Renovación, revocación y rechazo​

No puedo renovar un certificado​

Consulta primero GET /api/v1/certificate-request/{id}/issuance/renewal. Si eligible es false, reason explica el motivo:

reasonSignificadoQué hacer
NOT_RENEWABLE_PROVIDEREl certificado no se emitió en una solicitud BIOMETRIC o aún no está emitido.Solicita un certificado nuevo.
NOT_ACTIVELa solicitud no está en PROCESSED (revocada, vencida…).Solicita un certificado nuevo.
MAX_TERMYa tiene la vigencia máxima de 2 años.No requiere renovación.
GRACE_EXPIREDPasó revocation_date (vencimiento + 15 días de gracia).Solicita un certificado nuevo.

Si es renovable, POST /api/v1/certificate-request/{id}/issuance/renew genera la orden de pago (o devuelve la orden pendiente que ya existía, sin duplicarla); si no lo es, responde 422 con reason. La renovación se aplica cuando pagas esa orden con los endpoints de Órdenes.

Una orden de renovación pendiente pasa a FAILED si el certificado deja de ser renovable (al intentar pagarla, al revocarse o al vencer). Una orden FAILED no se puede reintentar: si el certificado sigue siendo renovable, genera otra con POST /api/v1/certificate-request/{id}/issuance/renew.

Mi certificado aparece como REVOKED sin haberlo revocado​

Los certificados de 1 año que no se renuevan antes de revocation_date se revocan automáticamente; desde ese momento la descarga responde 409 con status: "REVOKED". Para evitarlo, renueva y paga antes de que termine el periodo de gracia.

Si hay una orden de renovación pendiente con actividad en las últimas 48 horas (creada, reutilizada o con un pago iniciado), la revocación automática se aplaza, pero como máximo hasta 3 días después de revocation_date.

Error al revocar (POST /api/v1/certificate-request/{uuid}/revoke)​

  • Usa el uuid de la solicitud, no el ID numérico.
  • revoking_code se obtiene de GET /api/v1/certificate-request/{id}/issuance → dataRecords.data.revocation_code.
  • revocation_reason debe ser uno de: 0, 1, 2, 3, 4, 5, 9 o 10.
  • 422 No se puede revocar un certificado que no esté en estado PROCESSED...: solo se revocan certificados emitidos. La revocación es irreversible.

Quiero anular una solicitud con datos erróneos que aún no se emitió​

Usa POST /api/v1/certificate-request/{id}/issuance/reject con un reason de 5 a 400 caracteres. La solicitud queda CANCELLED y se reintegra el cupo. Es definitivo, así que deberás crear una solicitud nueva. can_reject en GET /api/v1/certificate-request/{id}/issuance anticipa si el rechazo es posible.

  • 409: la solicitud ya no está en PROCESSING o el trámite ya terminó, el certificado ya fue emitido (revócalo con POST /api/v1/certificate-request/{uuid}/revoke), Viafirma no admite el rechazo en el estado actual del trámite (contacta al soporte) o la solicitud se está actualizando en ese momento (reintenta en unos segundos).
  • 502: error de comunicación con Viafirma; no se modificó nada. Reintenta más tarde.
  • La respuesta incluye dataRecords.quota_released: si es false, el cupo no se reintegró, pero el rechazo se mantiene.

🔄 Problemas de Integración​

Error: "CORS - Cross-Origin Request Blocked"​

Síntomas:

Access to fetch at 'https://sandbox-api.maticerts.com/...'
from origin 'http://localhost:3000' has been blocked by CORS policy

Causa: la política CORS la decide el servidor de la API de MATICERTS, que puede restringir los orígenes de navegador permitidos según el entorno. Configurar CORS en tu propio servidor no cambia esa política.

Solución recomendada: llama a la API desde tu backend. Así evitas CORS y el token Bearer (que dura 90 días) nunca queda expuesto en el navegador.

// Backend (Node.js 18+ con Express): tu frontend llama a /api/solicitudes/:id/emision
app.get('/api/solicitudes/:id/emision', async (req, res) => {
const r = await fetch(
`${process.env.MATICERTS_API_URL}/certificate-request/${req.params.id}/issuance`,
{ headers: { Authorization: `Bearer ${process.env.MATICERTS_TOKEN}`, Accept: 'application/json' } }
);
res.status(r.status).json(await r.json());
});

No uses proxies de desarrollo (por ejemplo "proxy" en package.json) como solución de producción: solo funcionan en local y siguen dejando el token en el cliente. Si necesitas llamar a la API directamente desde un navegador, contacta al soporte para solicitar la autorización de tu dominio.


Error: "Timeout - La petición tardó demasiado"​

Síntomas:

Error: Request timeout after 30000ms

Soluciones:

  1. Aumenta el timeout en tu cliente:

    // Node.js 18+ (fetch nativo no admite la opción `timeout`)
    const response = await fetch(url, { signal: AbortSignal.timeout(60000) }); // 60 s
    # Python
    response = requests.get(url, timeout=60)
  2. No esperes la emisión en la misma petición; consulta el estado periódicamente (o usa webhooks):

    const API = 'https://sandbox-api.maticerts.com/api/v1';
    const headers = { Authorization: `Bearer ${process.env.MATICERTS_TOKEN}`, Accept: 'application/json' };

    async function esperarCertificado(id, { intervaloMs = 60000, plazoMs = 96 * 3600 * 1000 } = {}) {
    const limite = Date.now() + plazoMs;
    while (Date.now() < limite) {
    const res = await fetch(`${API}/certificate-request/${id}/issuance`, { headers });
    if (res.status === 429) { await new Promise(r => setTimeout(r, 60000)); continue; }
    const body = await res.json();
    const { status, data } = body.dataRecords;
    if (status === 'ready' || status === 'completed') return body;
    if (status === 'failed' || status === 'unsupported') {
    throw new Error(data?.last_error_message || body.message || 'Emisión fallida');
    }
    // submitted | processing | sent: seguir esperando
    await new Promise(r => setTimeout(r, intervaloMs));
    }
    throw new Error('La emisión no finalizó dentro del plazo');
    }

    El estado se consulta con GET /api/v1/certificate-request/{id}/issuance (dataRecords.status: submitted, processing, ready o failed en solicitudes BIOMETRIC; processing, ready o failed en solicitudes DOCUMENTARY). El estado de Viafirma se actualiza cada 60 segundos, así que consulta como máximo una vez por minuto: la API admite 60 peticiones por minuto y responde 429 si se superan. En una solicitud BIOMETRIC la emisión depende de que el suscriptor complete la verificación de identidad, lo que puede tardar horas o días; el trámite vence a las 96 horas y termina en failed.

    Para evitar el polling, suscríbete al webhook certificate_request.status_changed.

  3. Contacta al soporte si el servidor es lento:

    • Podemos investigar problemas de rendimiento
    • Proporciona el ID de la solicitud

Error 429: "Demasiadas solicitudes"​

Síntomas: la API responde HTTP 429. El cuerpo depende del límite que se superó:

  • Límite general (cualquier endpoint):
{ "message": "Too Many Attempts." }
  • Límites específicos por operación:
{
"success": false,
"message": "Demasiadas solicitudes de certificado. Intente nuevamente en un minuto."
}

Límites vigentes:

OperaciónLímiteMensaje
Cualquier endpoint de la API (límite general)60 peticiones por minuto por usuario (por IP si no hay sesión)Too Many Attempts.
POST /certificate-request10 por minuto por usuarioDemasiadas solicitudes de certificado. Intente nuevamente en un minuto.
POST /certificate-request/{id}/files20 por minuto por usuarioDemasiadas solicitudes de carga de archivos. Intente nuevamente en un minuto.
POST /certificate-request/{id}/issue5 por minuto por usuarioDemasiadas solicitudes de emisión. Intente nuevamente en un minuto.
POST /tokens10 intentos en 24 horas por usuario (cuentan también los fallidos)Límite de creación de tokens alcanzado. Máximo 10 por día.

Las peticiones a estos endpoints también cuentan para el límite general de 60 por minuto.

Soluciones:

  1. Si superas el límite general, la respuesta trae las cabeceras Retry-After (segundos que debes esperar), X-RateLimit-Limit y X-RateLimit-Remaining. Espera lo que indique Retry-After antes de reintentar. Las respuestas exitosas también traen X-RateLimit-Remaining, así que puedes vigilar cuántas peticiones te quedan.
  2. Las respuestas 429 de los límites específicos (solicitudes, archivos, emisión, tokens) no traen Retry-After. Espera al menos 60 segundos antes de reintentar; en POST /tokens, espera a que se cumplan 24 horas desde el primer intento (el contador no se reinicia a medianoche).
  3. Reduce el polling: consulta el estado de la emisión como máximo cada 60 segundos.
  4. Reutiliza tus tokens de acceso en lugar de crear uno nuevo en cada proceso.

No recibo webhooks o la firma no coincide​

Soluciones:

  1. Revisa que el webhook esté activo: GET /api/v1/webhooks/{id}. Tras 10 fallos consecutivos de conexión o timeout se desactiva automáticamente (is_active: false). Reactívalo con PUT /api/v1/webhooks/{id} enviando {"is_active": true}. Corrige antes la causa: el contador de fallos solo se reinicia con una entrega exitosa, así que si la siguiente entrega vuelve a fallar se desactiva otra vez.
  2. Consulta el historial de entregas: GET /api/v1/webhooks/{id}/deliveries. status puede ser delivered (tu servidor respondió; se guardan http_status y la respuesta), failed (error de conexión o timeout; el error queda en response_body) o pending (el envío está en curso o se interrumpió antes de completarse; también queda así una entrega marcada por soporte para reintento manual). Una entrega no se reenvía automáticamente. Ojo: si tu servidor responde 4xx o 5xx, la entrega queda como delivered y el evento no se reintenta.
  3. Responde rápido y con 2xx: tu endpoint debe contestar en menos de 10 segundos. Procesa el evento en segundo plano.
  4. Obtén el secret: al crear el webhook la respuesta no incluye el secret. Para obtenerlo llama a POST /api/v1/webhooks/{id}/rotate-secret; es el único momento en que se muestra. Cada rotación invalida el secret anterior de inmediato.
  5. Verifica la firma correctamente: la cabecera X-Webhook-Sig tiene el formato t=1735689600,v1=<hmac>. Calcula el HMAC-SHA256 (hexadecimal) del cuerpo crudo recibido, sin volver a serializar el JSON, con el secret, y compáralo con v1. Opcionalmente, rechaza firmas cuyo t tenga más de 5 minutos de antigüedad.
    const crypto = require('crypto');
    app.post('/webhooks/maticerts', express.raw({ type: 'application/json' }), (req, res) => {
    const firma = Object.fromEntries((req.get('X-Webhook-Sig') || '').split(',').map(p => p.split('=')));
    const esperado = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(req.body).digest('hex');
    const a = Buffer.from(esperado);
    const b = Buffer.from(firma.v1 || '');
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
    res.sendStatus(200);
    // procesa JSON.parse(req.body) en segundo plano
    });
  6. Revisa los eventos suscritos: generan entregas 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 y payment.failed. certificate.expiring se puede suscribir, pero todavía no envía notificaciones. La cabecera X-Webhook-Event indica el tipo de cada entrega.
  7. No me llegó el evento de un pago: payment.approved llega solo cuando la orden pasa a PAID, y payment.failed solo cuando pasa a FAILED porque se rechazó el pago; cada uno, como máximo una vez por orden (una orden que recibió payment.failed puede recibir después payment.approved si otro intento de pago de esa orden se aprueba). No llega ninguno si la transacción quedó en VOIDED o ERROR (la orden sigue en PENDING), si la pasarela reenvía un pago ya notificado ni si una orden de renovación se anuló porque el certificado dejó de ser renovable. En cualquier caso, el estado de la orden en GET /api/v1/orders/{uuid} es la fuente de verdad: consúltalo como respaldo, sobre todo para las órdenes que lleven tiempo en PENDING.

📞 Contacto y Soporte​

Si ninguna solución funciona:

  1. Reúne información:

    • ID (y UUID, si aplica) de la solicitud
    • Timestamp del error
    • Pasos que realizaste
    • Código HTTP y mensaje de error exactos
  2. Contacta al soporte:

  3. Proporciona logs:

    • Incluye las peticiones y respuestas relevantes de tu aplicación; ayudan a diagnosticar el problema más rápido.
    • Nunca compartas tokens, contraseñas, secrets de webhooks ni el PIN del certificado.

📚 Recursos Adicionales​