Volver al blog

Integrar una API para Verificar CURP: Webhook, Reintentos e Idempotencia

Cómo integrar la verificación de CURP en RENAPO sin sorpresas: respuesta inmediata, resultado en tu webhook, firma HMAC, idempotencia y reintentos.

Verificar CURP
API CURPAPI verificar CURPwebhook CURPintegrar verificación CURPCURP RENAPOdesarrolladores

Verificar una CURP en el registro de RENAPO no se comporta como una consulta a tu propia base de datos. El registro puede tardar, puede no responder, y a veces responde minutos después. Una integración que espera la respuesta en la misma petición HTTP termina con timeouts, reintentos a ciegas y cobros duplicados.

Por eso nuestra API separa el trabajo en dos pasos, y esta guía explica cómo integrarlos para que la parte difícil no te sorprenda en producción.

Dos pasos, una sola llamada

  • Paso 1: validación y cotejo. Se calculan sobre la propia CURP y llegan en la respuesta HTTP, en milisegundos. (La diferencia entre validar y verificar está en esta guía.)
  • Paso 2: verificación en el registro. Si la pides, la respuesta HTTP solo confirma que quedó en curso. El resultado llega después a un destino que configuras en el panel: un webhook, un correo o un chat de Telegram. Normalmente tarda segundos.

La petición:

curl -X POST https://verificarcurp.com/api/v1/curp \
  -H "X-API-Key: tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "curp": "PEGJ850101HDFRRL04",
    "persona": {
      "nombres": "Julio",
      "primerApellido": "Pérez",
      "segundoApellido": "García",
      "fechaNacimiento": "1985-01-01"
    },
    "verificar": true,
    "destinationId": "dst_...",
    "idempotencyKey": "alta-83412"
  }'

persona es opcional y cada campo también: solo se cotejan los que envías, y no se guardan. destinationId es obligatorio cuando verificar es true.

La respuesta inmediata trae el paso 1 en data y el estado del paso 2 en verificacion:

{
  "success": true,
  "data": { "curp": "PEGJ850101HDFRRL04", "valida": true, "cotejo": "coincide", "...": "..." },
  "tokens_remaining": 431,
  "verificacion": {
    "aceptada": true,
    "job_id": "clz...",
    "estado": "pendiente",
    "expires_at": "2026-10-10T16:35:00.000Z"
  }
}

Decide con data lo que se puede decidir ya. Una CURP mal formada o un cotejo no_coincide en la fecha no necesitan esperar al registro. Guarda el job_id y sigue tu flujo cuando llegue el resultado.

Los códigos de error, antes que el camino feliz

Hay dos familias, y se tratan distinto.

Errores de la petición (HTTP distinto de 200), que no se cobran:

HTTPCódigoQué hacer
401MISSING_API_KEY, INVALID_API_KEYRevisar la configuración, no reintentar
400INVALID_REQUESTCorregir el cuerpo (por ejemplo, verificar sin destinationId)
400DESTINATION_NOT_FOUNDEl destino no existe o no es de tu cuenta
402INSUFFICIENT_TOKENSRecargar saldo
409IDEMPOTENCY_KEY_REUSEDUsaste la misma clave para otra CURP

Verificación no aceptada (HTTP 200): el paso 1 ya está en data y es válido, pero el paso 2 no quedó en curso. verificacion.aceptada es false y trae un código:

  • REGISTRY_UNAVAILABLE: el registro no está disponible ahora.
  • REGISTRY_CEILING_REACHED: se alcanzó el límite diario de consultas; se reinicia a medianoche UTC.
  • TOO_MANY_VERIFICATIONS: tu cuenta tiene demasiadas verificaciones en curso o sin entregar.

Cuando esperar sirve de algo, retry_after trae los segundos y la respuesta lleva el mismo valor en el encabezado Retry-After. Cuando es null, esperar no ayudaría: no programes un reintento.

Idempotencia: la conexión que se corta

El caso clásico: envías la petición, se corta la conexión y no sabes si llegó. Si repites la llamada sin protección, pagas dos veces y creas dos verificaciones.

Con idempotencyKey, repetir la llamada con la misma clave desde la misma cuenta no crea otra verificación ni cobra otro token: devuelve el mismo job_id, su estado actual y repetida: true. Tres reglas:

  • Una clave por consulta. Si reusas una clave para otra CURP, la respuesta es 409.
  • Nunca pongas la CURP en la clave. Usa tu propio identificador: el id del alta, del expediente o de la solicitud.
  • La clave viaja de vuelta en cada aviso a tu destino, así que te sirve para relacionar el resultado con tu registro.

El webhook: verifica la firma

Cada aviso a un webhook llega firmado con el secreto del destino, que ves una sola vez al crearlo:

  • X-Signature: sha256= seguido del HMAC-SHA256 en hexadecimal.
  • X-Signature-Timestamp: segundos Unix.
  • La cadena firmada es timestamp + "." + cuerpo, con el cuerpo crudo, tal como llegó.
import { createHmac, timingSafeEqual } from 'node:crypto'

function firmaValida(secreto, timestamp, cuerpoCrudo, firma) {
  const esperada = Buffer.from(
    'sha256=' + createHmac('sha256', secreto).update(`${timestamp}.${cuerpoCrudo}`).digest('hex')
  )
  const recibida = Buffer.from(firma ?? '')
  return esperada.length === recibida.length && timingSafeEqual(esperada, recibida)
}

El error más común es verificar sobre el JSON ya interpretado y vuelto a serializar: cambia el orden o los espacios y la firma no coincide. Lee el cuerpo como texto antes de interpretarlo. Y rechaza timestamps viejos: la firma los cubre justamente para que un aviso capturado no se pueda reenviar después.

Los avisos que llegan a tu destino

EventoCuándoQué hacer
curp.verification.completedEl registro respondióLeer estatus y los datos registrados
curp.verification.retryingFalló un intento y hay otro programadoNada: es un aviso de avance, sin la CURP
curp.verification.manual_reviewSe agotaron los reintentos; una persona la resuelveEsperar; el resultado llega después
curp.verification.failedExcepcionalmente, no se pudo resolverDecidir con el paso 1 o volver a pedirla

completed incluye estatus: "NO_ENCONTRADA": que la CURP no exista es una respuesta del registro, no una falla. Qué significa cada estatus y qué hacer con él está en Estatus de la CURP.

Dos cosas que conviene saber del lado de tu endpoint:

  • Cada aviso se entrega una sola vez. No reintentamos la entrega a tu destino. Responde rápido con un 2xx y procesa después, en una cola, en lugar de hacer trabajo pesado antes de contestar.
  • Deduplica por X-Idempotency-Key. Tu propia infraestructura puede reprocesar un mismo aviso; la clave te permite reconocerlo.

Reintentos: los decides tú

Si el registro no responde, reintentamos la consulta automáticamente. Por defecto son 3 intentos en total, el primer reintento a los 5 minutos y la espera se duplica en cada intento; los reintentos automáticos nunca duran más de 24 horas. Si se agotan, una persona de nuestro equipo resuelve la verificación a mano y el resultado llega igual.

Tu cuenta guarda sus valores en Configuración, y cada petición puede cambiarlos con reintentos:

{
  "curp": "PEGJ850101HDFRRL04",
  "verificar": true,
  "destinationId": "dst_...",
  "reintentos": { "activo": true, "max": 5, "espera_s": 60 }
}

max va de 1 a 5 intentos y espera_s de 60 a 3,600 segundos; un valor fuera de rango se ajusta al límite en lugar de rechazar la petición. La respuesta inmediata repite en verificacion.reintentos los valores que quedaron fijados.

Un alta en línea con una persona esperando puede preferir pocos intentos y cortos; un proceso por lotes nocturno, más intentos y más espaciados.

Lo que cuesta y lo que se guarda

  • 1 token por respuesta, con la validación, el cotejo y la verificación incluidos. Los reintentos y la resolución a mano no se cobran aparte.
  • La CURP se guarda solo mientras se verifica y se borra al terminar; el resultado se borra en cuanto se entrega. Los datos del cotejo nunca se guardan ni se envían al registro.

La referencia completa está en la documentación de la API, y los paquetes de tokens, en precios. No somos RENAPO: consultamos su registro público y te entregamos lo que ahí consta.

¿Necesitas verificar CURP automáticamente?

Valida, coteja y verifica CURP en el registro de RENAPO con una sola petición a la API.

Comenzar gratis