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 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:
| HTTP | Código | Qué hacer |
|---|---|---|
| 401 | MISSING_API_KEY, INVALID_API_KEY | Revisar la configuración, no reintentar |
| 400 | INVALID_REQUEST | Corregir el cuerpo (por ejemplo, verificar sin destinationId) |
| 400 | DESTINATION_NOT_FOUND | El destino no existe o no es de tu cuenta |
| 402 | INSUFFICIENT_TOKENS | Recargar saldo |
| 409 | IDEMPOTENCY_KEY_REUSED | Usaste 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
| Evento | Cuándo | Qué hacer |
|---|---|---|
curp.verification.completed | El registro respondió | Leer estatus y los datos registrados |
curp.verification.retrying | Falló un intento y hay otro programado | Nada: es un aviso de avance, sin la CURP |
curp.verification.manual_review | Se agotaron los reintentos; una persona la resuelve | Esperar; el resultado llega después |
curp.verification.failed | Excepcionalmente, no se pudo resolver | Decidir 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