Esquema de color
Are you an LLM? You can read better optimized documentation at /services/imss/vd_premium.md for this page in Markdown format
🏥 Servicio Premium de Vigencia de Derechos del Seguro Social (asíncrono)
Hay tres consultas de vigencia de derechos. Esta es la que entrega el PDF.
| Consulta | Qué obtienes | Entrada | Forma |
|---|---|---|---|
| Solo validar vigencia | Un JSON que dice si la persona tiene servicio médico vigente. Sin documento. | curp + nss | Síncrona |
| Vigencia al correo | El Seguro Social envía la constancia al correo del titular. Tú no recibes el archivo. | curp + nss + userEmail | Síncrona |
| Esta página | La constancia en PDF, entregada a tu sistema. | curp | Asíncrona |
Endpoint asíncrono para obtener la Constancia de Vigencia de Derechos del Seguro Social, el documento oficial que acredita el derecho a servicio médico. El cliente envía la solicitud y elige cómo recibir el resultado:
- Modo webhook: provee
webhookUrly entregamos el PDF a esa URL cuando la constancia esté lista (firmado con HMAC-SHA256). - Modo polling-only: omite
webhookUrly consulta el resultado porGET /v3/vd_premium/{uuid}cuando lo necesites.
Ambos modos cuestan los mismos 3 créditos; lo único que cambia es cómo te enteras del resultado.
- Base URL:
https://api.consultaunica.mx - Autenticación: header
X-API-Key - Formato: JSON (
Content-Type: application/json) - Compra de paquetes: https://consultaunica.mx/paquetes
Si ya integraste el reporte Premium de Semanas Cotizadas, aquí sólo cambia la ruta
Los dos productos comparten el mismo contrato: mismos errorCode, misma forma del cuerpo del webhook, misma firma HMAC-SHA256, mismos reintentos, mismo replay y mismo modo mock. Fue un requisito de diseño, no una casualidad: quien ya tiene un cliente de semanas cotizadas premium lo apunta a /v3/vd_premium y funciona.
Lo único que este producto no tiene es parsePdf / documentData: la constancia de vigencia todavía no se entrega estructurada en JSON. Tampoco hay carga por lotes.
📑 Contenido
- Submit —
POST /v3/vd_premium - Webhook de entrega —
POSTa tuwebhookUrl - Consultar resultado —
GET /v3/vd_premium/{uuid} - Reintentar entrega —
POST /v3/vd_premium/{uuid}/replay - Flujo completo
- Preguntas frecuentes
- Modo Mock
- Changelog
Formato de las respuestas de error
Todos los endpoints de esta API responden los errores con el mismo objeto de tres campos, sin importar el status HTTP:
json
{
"message": "Cuenta no válida, favor de verificar su clave de consulta",
"description": "La clave no corresponde a una cuenta activa: puede ser incorrecta, o la cuenta está bloqueada, sin confirmar o desactivada.",
"errorCode": "unauthorized_user"
}| Campo | Tipo | Descripción |
|---|---|---|
message | string | Mensaje corto de la causa, en español y apto para mostrar al usuario final. |
description | string | Explicación no técnica de por qué ocurrió, pensada para tu UI. Puede venir vacía (""). |
errorCode | string | Slug estable en inglés. Es el campo contra el que debes programar. Puede venir vacío (""). |
Reglas para integrar:
- Haz match por
errorCode, no pormessage. ElerrorCodees contrato estable; los textos demessageydescriptionpueden reescribirse para mejorar la redacción sin aviso. errorCodepuede llegar vacío en errores no clasificados. Todos los errores documentados aquí traen código; los que no lo traen son fallas genéricas de infraestructura y el404de una ruta inexistente. Trata""como error no clasificado y cae a tu manejo genérico por status HTTP.- Nunca recibes el campo
detailde FastAPI. Un handler central reescribe toda excepción a esta forma. - El único error que trae una llave extra es la validación de payload, que agrega
errors(ver 409).
1. Submit — POST /v3/vd_premium
Envía una consulta para una CURP e indica a dónde entregar el resultado.
Request — modo webhook (push)
http
POST /v3/vd_premium HTTP/1.1
Host: api.consultaunica.mx
X-API-Key: <tu_api_key>
Content-Type: application/json
{
"curp": "AAAA800101HDFBBB01",
"webhookUrl": "https://tu-dominio.com/webhook/vigencia"
}Request — modo polling-only (pull)
Omite webhookUrl. No hay entrega automática: el resultado lo lees tú con GET /v3/vd_premium/{uuid}.
http
POST /v3/vd_premium HTTP/1.1
Host: api.consultaunica.mx
X-API-Key: <tu_api_key>
Content-Type: application/json
{
"curp": "AAAA800101HDFBBB01"
}Campos
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
curp | string | sí | CURP de 18 caracteres. Se normaliza a mayúsculas y se valida contra el patrón oficial. |
webhookUrl | string | no | URL HTTPS pública a la que entregaremos el resultado. Si la omites, la consulta es polling-only. |
A diferencia de las otras dos consultas de vigencia, aquí no se envía el NSS: el Seguro Social lo resuelve a partir de la CURP y te lo devuelve en la respuesta del submit.
Requisitos del webhookUrl
Aplican sólo si envías
webhookUrl. En modo polling-only no hay validación de red ni resolución DNS.
Rechazamos la solicitud si la URL no cumple:
- Esquema HTTPS (no
http://). - Sin credenciales embebidas (
https://user:pass@...no se acepta). - Debe resolver vía DNS a una IP pública. Rechazamos loopback (127.0.0.0/8,
::1), rangos privados (10/8, 172.16/12, 192.168/16, fc00::/7), link-local (169.254/16, fe80::/10), reservados, multicast y0.0.0.0. - Timeout de resolución DNS: 2 segundos. Si tu dominio tarda más, la solicitud se rechaza.
La validación ocurre antes de aceptar la consulta, no al entregar: una URL inalcanzable te costaría créditos si se descubriera hasta el momento de la entrega.
Costo
Cada consulta cuesta 3 créditos, que se aplican en dos momentos:
- Retención — al aceptar el submit apartamos los 3 créditos de tu saldo.
- Cobro — se consuman cuando el Seguro Social nos entrega el PDF de la constancia.
Una consulta que nunca llega a producir un PDF no convierte la retención en cobro. Es la razón por la que pdf_download_failed no te cuesta nada, y por la que tampoco consumen saldo la re-entrega del mismo día ni el 422 recent_query.
Rate limit
Máximo 3 requests cada 2 segundos por X-API-Key en este endpoint. Al rebasarlo devolvemos 429 Too Many Requests con el mensaje de la tabla de errores más abajo.
Es un token bucket, no una ventana fija: acumulas hasta 3 peticiones de ráfaga y se repone una cada ~0.67 s, así que el ritmo sostenido es de 90 req/min.
El cupo es independiente del de semanas cotizadas premium. Agotar uno no bloquea el otro: son dos consultas distintas que puedes querer en paralelo, y comparten el número pero no el contador.
Consultas simultáneas con semanas cotizadas
Puedes tener en vuelo, el mismo día y para la misma CURP, una consulta de vigencia y una de semanas cotizadas. Son documentos distintos y no se estorban: ni comparten reserva de idempotencia ni compiten por el candado de duplicados del proveedor. Una consulta de vigencia nunca te va a provocar un duplicate_in_progress de semanas cotizadas, ni al revés.
Re-entrega del mismo día
Si reenvías un submit de una CURP que ya completaste hoy, en vez de rechazarlo te servimos la constancia que ya obtuviste. No volvemos a consultar al Seguro Social, así que:
- No consume créditos. El header
X-Remaining-Requestsreporta tu saldo sin descuento. - Recibes un
uuidnuevo, no el original. El resultado queda disponible por eseuuiden polling y replay, y si mandastewebhookUrlte llega el push firmado igual que en una consulta fresca. - La respuesta trae
redelivered: true. Elmessagees idéntico al de una consulta nueva, así que ese booleano es el único discriminante: si necesitas distinguir los dos casos —para no volver a cobrarle a tu usuario, por ejemplo— es el campo que debes leer.
Si la constancia de la solicitud original ya no es recuperable, el submit responde 422 recent_query. Es el caso degradado, no el habitual.
Idempotency-Key (opcional)
Para protegerte de cobros duplicados si tu cliente reintenta el submit por un error de red, envía el header Idempotency-Key. Reenviar con la misma key devuelve la respuesta de la consulta original sin volver a consumir créditos.
Es una protección de transporte, distinta de la del producto: aunque no mandes la key, una segunda consulta de la misma CURP el mismo día se re-entrega sin cobrar.
- Formato:
^[A-Za-z0-9_-]{1,64}$(64 chars máx, alfanuméricos +-+_). - TTL: se alinea a la medianoche de la Ciudad de México —la vigencia del dato es diaria— con un piso de 1 hora. Después de eso, la misma key vuelve a tratarse como consulta nueva.
- Scope: por usuario. Dos usuarios distintos pueden usar la misma key sin colisión.
- Respuesta de replay: body idéntico al de la consulta original, con el header
X-Idempotent-Replay: true.
Errores:
| HTTP | Caso | errorCode | message |
|---|---|---|---|
| 400 | Key con formato inválido | invalid_idempotency_key | El encabezado Idempotency-Key no tiene un formato válido |
| 409 | Key ya en uso por otra request concurrente que aún no termina | concurrent_idempotent_request | Ya se está procesando una consulta idéntica, espera un momento antes de reintentar |
Ejemplo curl con idempotency:
bash
curl -X POST https://api.consultaunica.mx/v3/vd_premium \
-H "X-API-Key: $CU_API_KEY" \
-H "Idempotency-Key: order-12345-retry-1" \
-H "Content-Type: application/json" \
-d '{"curp":"AAAA800101HDFBBB01","webhookUrl":"https://tu-dominio.com/hook"}'Genera un valor único por cada submit lógico de tu lado (por ejemplo, el ID de la orden interna + contador de reintento).
✅ Respuesta exitosa — 200 OK
json
{
"uuid": "9f2b7c1a-4d3e-4a5f-8b0c-2e1d0f9a8b77",
"message": "Solicitud recibida, pendiente de resultado",
"curp": "AAAA800101HDFBBB01",
"nss": "12345678901",
"fullname": "Juan Pérez López",
"redelivered": false
}| Campo | Tipo | Descripción |
|---|---|---|
uuid | string | Identificador único de la solicitud. Guárdalo: llegará idéntico en el webhook y te sirve para deduplicar. |
message | string | Mensaje descriptivo del estado. |
curp | string | CURP confirmado. |
nss | string | Número de Seguridad Social del titular, resuelto por el Seguro Social. |
fullname | string | Nombre completo del titular. |
redelivered | boolean | true si es una re-entrega del mismo día (sin cargo); false en una consulta nueva. |
nssyfullnamepueden llegar vacíos. El Seguro Social acepta la consulta sin haberlos resuelto, y en ese caso los datos del titular llegan igual con la constancia. Un valor vacío en el submit no significa que la consulta vaya a fallar.
Headers de respuesta:
X-Remaining-Requests: créditos restantes después de este consumo.X-Request-Id: igual aluuidde la consulta.
Ejemplo curl
bash
curl -X POST https://api.consultaunica.mx/v3/vd_premium \
-H "X-API-Key: $CU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"curp": "AAAA800101HDFBBB01",
"webhookUrl": "https://tu-dominio.com/webhook/vigencia"
}'❌ Respuestas de error
Todas siguen el formato de tres campos. Programa contra errorCode.
400 — Webhook inválido y datos rechazados en origen
http
HTTP/1.1 400 Bad Requestjson
{
"message": "webhookUrl debe ser https y apuntar a un host público accesible",
"description": "La dirección web (URL) que configuraste para recibir el resultado no es válida.",
"errorCode": "invalid_webhook_url"
}errorCode | message | Caso |
|---|---|---|
invalid_webhook_url | webhookUrl debe ser https y apuntar a un host público accesible | La URL no pasó las validaciones de red. |
invalid_curp | La CURP ingresada no es válida, favor de verificar | CURP con formato válido pero rechazada al consultarla. |
imss_data_inconsistency | El Seguro Social reportó inconsistencias en los datos de esta CURP | Datos incongruentes en la fuente (típicamente más de un NSS o más de una CURP registrada). |
invalid_idempotency_key | El encabezado Idempotency-Key no tiene un formato válido | Idempotency-Key fuera del patrón permitido. |
401 — Autenticación
http
HTTP/1.1 401 Unauthorizedjson
{
"message": "Cuenta no válida, favor de verificar su clave de consulta",
"description": "La clave no corresponde a una cuenta activa: puede ser incorrecta, o la cuenta está bloqueada, sin confirmar o desactivada.",
"errorCode": "unauthorized_user"
}errorCode | message | Caso |
|---|---|---|
missing_api_key | No se encontró la Clave para realizar consultas | Header X-API-Key ausente o vacío. |
unauthorized_user | Cuenta no válida, favor de verificar su clave de consulta | La clave no corresponde a una cuenta activa. |
user_not_found | Usuario no encontrado | Cuenta sin registro post-autenticación (caso raro). |
403 — Se requiere una compra previa
El uso de la API por clave está restringido a cuentas con al menos un paquete comprado. Una cuenta que sólo tiene créditos bonificados recibe este error aunque su saldo sea mayor a cero.
http
HTTP/1.1 403 Forbiddenjson
{
"message": "Tu clave de consulta requiere una compra activa para usar la API, adquiere un paquete en https://dash.consultaunica.mx/compras/nueva",
"description": "Tu cuenta necesita al menos un paquete de consultas comprado para usar este servicio.",
"errorCode": "api_key_requires_purchase"
}409 — Validación del payload
Cuando algún campo del cuerpo no pasa la validación, el status es 409 Conflict (no 422) y la respuesta agrega la llave errors con un elemento por campo inválido:
http
HTTP/1.1 409 Conflictjson
{
"message": "Datos inválidos, favor de verificar la información ingresada",
"description": "Alguno de los datos que enviaste tiene un formato incorrecto o falta información obligatoria.",
"errorCode": "bad_request",
"errors": [
{
"field": "body.curp",
"error": "String should match pattern '^[A-Z][AEIOUX][A-Z]{2}[0-9]{2}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])[MHX]([ABCMTZ]S|[BCJMOT]C|[CNPST]L|[GNQ]T|[GQS]R|C[MH]|[MY]N|[DH]G|NE|VZ|DF|SP)[BCDFGHJ-NP-TV-Z]{3}[0-9A-Z][0-9]$'"
}
]
}Campo de errors[] | Descripción |
|---|---|
field | Ruta del campo con puntos, incluyendo el origen (body.curp). |
error | Mensaje del validador, en inglés. |
Casos que disparan bad_request:
curpfaltante, con longitud distinta de 18, o fuera del patrón.webhookUrlcon esquema distinto ahttpso formato inválido (sólo si lo envías; omitirlo es válido y activa modo polling-only).- JSON malformado.
409 — Idempotencia concurrente
json
{
"message": "Ya se está procesando una consulta idéntica, espera un momento antes de reintentar",
"description": "Enviaste dos peticiones idénticas casi al mismo tiempo; una ya se está procesando.",
"errorCode": "concurrent_idempotent_request"
}422 — CURP sin información o consulta duplicada
http
HTTP/1.1 422 Unprocessable EntityerrorCode | message | Caso |
|---|---|---|
no_data_found | No se encontró información en el Seguro Social para la CURP ingresada | CURP válida pero sin registros en el Seguro Social. |
duplicate_in_progress | Ya existe una consulta de vigencia en curso para esta CURP, espere a que termine antes de reintentar | Hay una consulta de vigencia en vuelo para la misma CURP. Una consulta de semanas cotizadas en curso no dispara este error. |
recent_query | Ya cuentas con una consulta de vigencia reciente para esta CURP, espera unos minutos antes de reintentar | Caso degradado de la re-entrega del mismo día: ya consultaste esa CURP hoy y su PDF ya no es recuperable. Lo habitual es el 200 con redelivered: true. No se cobra. |
429 — Sin créditos, rate limit o saturación
http
HTTP/1.1 429 Too Many RequestserrorCode | message | Caso |
|---|---|---|
too_many_requests | Has alcanzado el límite de consultas diarias, visita nuestros paquetes para más: … | Saldo agotado. Compra un paquete. |
rate_limit_exceeded | Debe esperar {N} segundos para volver a consultar, evitemos congestionar el servicio | Rebasaste 3 req / 2 s en este endpoint. Trae header Retry-After. |
system_at_capacity | El sistema se encuentra a máxima capacidad, favor de esperar unos minutos antes de reintentar | Demanda alta en el origen; reintenta en unos minutos. |
El número de segundos de rate_limit_exceeded es dinámico: usa el header Retry-After en vez de parsear el texto.
503 — Servicio no disponible
http
HTTP/1.1 503 Service UnavailableerrorCode | message |
|---|---|
service_unavailable | El servicio no se encuentra disponible, favor de consultar más tarde |
system_paused | El servicio de vigencia de derechos no está disponible, favor de intentar más tarde |
Recomendación: reintenta con backoff exponencial empezando en 60 s.
500 — Error interno
http
HTTP/1.1 500 Internal Server Errorjson
{
"message": "Ocurrió un error inesperado, favor de reintentar",
"description": "Ocurrió un error inesperado al procesar tu consulta.",
"errorCode": "unknown"
}Ciertas fallas de red contra el origen entregan
errorCode: "system_paused"con status500en vez de503. Trata ambos status igual para ese código: es una condición transitoria, reintentable con backoff.
Catálogo de errorCode para armar tu diccionario
Estos son los códigos que puede devolver el submit. El errorCode es el contrato estable; message y description pueden reescribirse sin bump de versión.
| HTTP | errorCode | Reintentable |
|---|---|---|
| 400 | invalid_webhook_url | No — corrige la URL. |
| 400 | invalid_curp | No — corrige la CURP. |
| 400 | imss_data_inconsistency | No — requiere trámite del titular en el Seguro Social. |
| 400 | invalid_idempotency_key | No — corrige el header. |
| 401 | missing_api_key | No. |
| 401 | unauthorized_user | No. |
| 401 | user_not_found | No. |
| 403 | api_key_requires_purchase | No — requiere compra. |
| 409 | bad_request | No — corrige el payload. |
| 409 | concurrent_idempotent_request | Sí, tras unos segundos. |
| 422 | no_data_found | No. |
| 422 | duplicate_in_progress | Sí, cuando termine la consulta en curso. |
| 422 | recent_query | Sí, tras unos minutos. |
| 429 | too_many_requests | Sólo tras comprar créditos. |
| 429 | rate_limit_exceeded | Sí — respeta Retry-After. |
| 429 | system_at_capacity | Sí, con backoff. |
| 503 | service_unavailable | Sí, con backoff. |
| 503 | system_paused | Sí, con backoff. |
| 500 | unknown | Sí, una vez. |
Son los mismos slugs que devuelve semanas cotizadas premium. Si ya tienes un diccionario de códigos para ese producto, sirve tal cual aquí: lo único que cambia son los textos de message, que hablan de vigencia de derechos en vez de semanas cotizadas.
2. Webhook de entrega — POST a tu webhookUrl
Aplica sólo si enviaste
webhookUrlen el submit. En modo polling-only no enviamos webhook — salta a §3 Consultar resultado.
Cuando la constancia está lista (o falla), hacemos POST a la URL que nos diste.
Headers
| Header | Ejemplo | Descripción |
|---|---|---|
Content-Type | application/json | Siempre JSON. |
User-Agent | consultaunica-webhook/1.0 | Identifica nuestro tráfico. |
X-CU-Uuid | 9f2b7c1a-4d3e-4a5f-8b0c-2e1d0f9a8b77 | Mismo uuid que devolvió el submit. |
X-CU-Timestamp | 1745336400 | Unix epoch (segundos) en el que se generó la firma. Nuevo en cada reintento. |
X-CU-Signature | sha256=a1b2c3... | HMAC-SHA256 de "{X-CU-Timestamp}." + <cuerpo bruto>, firmado con tu X-API-Key. |
Body — entrega exitosa
json
{
"version": 1,
"uuid": "9f2b7c1a-4d3e-4a5f-8b0c-2e1d0f9a8b77",
"status": "completed",
"pdfBase64": "JVBERi0xLjQKJ...",
"errorMessage": null
}versiones la versión del contrato del webhook (entero). Ver Versionado del contrato abajo.pdfBase64es la constancia codificada en base64 estándar. Decodifícala directamente a bytes y guárdala / muéstrala.
Body — entrega fallida
json
{
"version": 1,
"uuid": "9f2b7c1a-4d3e-4a5f-8b0c-2e1d0f9a8b77",
"status": "failed",
"pdfBase64": null,
"errorMessage": "No se localizó la CURP en RENAPO, revise que esté bien escrita"
}Una consulta que termina en failed no consume créditos: la retención se libera.
Versionado del contrato
El campo version del body te permite detectar cambios de esquema sin parsear headers.
- Estamos en
version: 1. - Cambios aditivos (campos nuevos opcionales) → mantenemos la misma versión.
- Cambios breaking (renames, removals, cambios de tipo) → bump a
version: 2y damos aviso con anticipación. - La versión va dentro del body firmado, así que un atacante no puede cambiarla sin invalidar la firma.
Recomendación: en tu validador, rechaza versiones distintas a las que conoces en vez de silenciosamente aceptarlas.
Estados posibles
status | Significado | ¿Habrá más callbacks? |
|---|---|---|
completed | Constancia lista, pdfBase64 presente. | No, es terminal. |
failed | No se pudo obtener o entregar la constancia. | No, es terminal. |
pending | Sólo aparece en el polling, nunca en una entrega. | — |
Catálogo de errorMessage en status: "failed"
El body del webhook no lleva
errorCode. El objeto de tres campos aplica a las respuestas HTTP de error de la API, no al payload que te entregamos: aquí sólo viaja el texto enerrorMessage. La columna "código sugerido" de abajo es una recomendación para tu propio diccionario, no un valor que enviemos.
errorMessage exacto | Código sugerido | Caso |
|---|---|---|
No se localizó la CURP en RENAPO, revise que esté bien escrita | renapo | La CURP no existe en el registro nacional. Revisa que esté bien capturada. |
La CURP presenta inconsistencias en los datos del Seguro Social | imss_inconsistency | Datos incongruentes en la fuente. |
Esta CURP no pudo procesarse en el Seguro Social. Es posible que exista otra CURP registrada; verifíquela en curp.gob.mx | curp_not_processable | El Seguro Social no puede procesar esa CURP; suele haber una segunda CURP registrada para la misma persona. |
Esta CURP ya fue consultada hoy. El Seguro Social permite una consulta por día, intente nuevamente mañana | daily_limit | Límite del propio Seguro Social, no nuestro. Distinto de nuestra re-entrega del mismo día. |
No fue posible recuperar la constancia en este intento, favor de reintentar la consulta | pdf_download | La constancia se generó pero no pudimos recuperarla. No se cobró. |
No fue posible recuperar el PDF de la constancia, favor de reintentar la consulta | pdf_download_failed | Variante del anterior, emitida al preparar la entrega. No se cobró. |
La consulta fue cancelada por exceder el tiempo de espera, favor de reintentar | cancelled_by_timeout | La consulta se canceló por tiempo de espera en el origen. |
No fue posible completar la consulta para esta CURP | not_completed | El Seguro Social no completó la consulta, sin causa más específica. |
No fue posible completar la consulta, favor de reintentar más tarde | unknown | Error no clasificado. |
Validación de la firma (obligatorio)
Antes de confiar en un callback debes realizar dos verificaciones:
- Tolerancia de tiempo. El
X-CU-Timestamp(unix epoch en segundos) debe estar dentro de una ventana cercana a tu reloj actual. Recomendamos ±300 segundos (5 min). Esto protege contra replay. - Firma HMAC. Recomputas
HMAC-SHA256sobre los bytes"{X-CU-Timestamp}." + <cuerpo_crudo>usando tuX-API-Keycomo secreto. Comparas contraX-CU-Signatureen tiempo constante.
Importante: firmamos los bytes exactos del cuerpo recibido. No parsees ni re-serialices el JSON antes de validar — los separadores/espacios cambiarían la firma. Y el timestamp forma parte del material firmado: firmar sólo el cuerpo produce una firma que nunca va a coincidir.
Pseudocódigo
secret = TU_API_KEY # misma que usas en X-API-Key
raw_body = bytes_del_request # NO parsear ni re-serializar
timestamp = request.headers["X-CU-Timestamp"] # string de dígitos
signature = request.headers["X-CU-Signature"] # "sha256=<hex>"
if abs(now_unix() - int(timestamp)) > 300:
return 401 # fuera de ventana
signed = (timestamp + ".").encode() + raw_body
expected = "sha256=" + hex(hmac_sha256(secret, signed))
if not constant_time_equals(expected, signature):
return 401Python
python
import hmac, hashlib, time
secret = os.environ["CU_API_KEY"].encode()
raw = request.get_data() # Flask: raw bytes
ts = request.headers.get("X-CU-Timestamp", "")
sig = request.headers.get("X-CU-Signature", "")
if not ts.isdigit() or abs(int(time.time()) - int(ts)) > 300:
abort(401)
signed = f"{ts}.".encode() + raw
digest = hmac.new(secret, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={digest}", sig):
abort(401)El esquema de firma es idéntico en todos nuestros webhooks. Si necesitas los receptores equivalentes en Node.js o el comando de depuración en Bash, están en semanas cotizadas premium §2 y sirven aquí sin cambios.
Usa siempre una comparación en tiempo constante (hmac.compare_digest, crypto.timingSafeEqual). Nunca ==.
Qué debe responder tu webhook
- Respuesta exitosa aceptada: HTTP
200,201,202o204. Cualquier otra cosa (incluido299) se considera fallida y la reintentamos. - Sin cuerpo de respuesta requerido.
- Timeout: 15 segundos por intento. Si no respondemos a tiempo, cuenta como fallo de ese intento.
- Responde lo más rápido posible (idealmente
<1s): encola el trabajo y responde202.
Reintentos
Si tu endpoint no responde con un status aceptado:
| Intento | Delay desde el anterior |
|---|---|
| 1 | 0 s |
| 2 | 30 s |
| 3 | 120 s |
Después del 3er intento dejamos de intentar. No hay cola persistente: si perdiste los 3 intentos tienes que recuperar el resultado con el polling o con el endpoint de replay.
Idempotencia y deduplicación
- Reenviamos el mismo
uuiden cada reintento. Tu endpoint debe ser idempotente: si ves unuuidque ya procesaste, responde 200 sin volver a hacer el trabajo. - Cada reintento trae un
X-CU-Timestampnuevo y firma nueva. La ventana de tolerancia (±5 min) cierra replays viejos; el chequeo deuuidcierra duplicados dentro de la ventana. - Recomendación: persiste los
uuidpor al menos 24 horas.
Ejemplo mínimo de receptor (Flask)
python
import base64, hmac, hashlib, os, json, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["CU_API_KEY"].encode()
PROCESSED = set() # usa redis/db en prod
@app.post("/webhook/vigencia")
def webhook():
raw = request.get_data()
ts = request.headers.get("X-CU-Timestamp", "")
sig = request.headers.get("X-CU-Signature", "")
if not ts.isdigit() or abs(int(time.time()) - int(ts)) > 300:
abort(401)
signed = f"{ts}.".encode() + raw
digest = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={digest}", sig):
abort(401)
payload = json.loads(raw)
uid = payload["uuid"]
if uid in PROCESSED:
return "", 200
PROCESSED.add(uid)
if payload["status"] == "completed":
pdf = base64.b64decode(payload["pdfBase64"])
with open(f"/tmp/{uid}.pdf", "wb") as f:
f.write(pdf)
else:
# log payload["errorMessage"]
pass
return "", 2023. Consultar resultado — GET /v3/vd_premium/{uuid}
Consulta el estado actual de una solicitud por uuid. Autenticado con X-API-Key. No consume créditos y no tiene límite de veces.
Es la única vía para obtener el resultado en modo polling-only (cuando no enviaste webhookUrl), y también funciona como complemento al webhook si necesitas recuperar una constancia que se perdió en el primer intento.
Request
bash
curl https://api.consultaunica.mx/v3/vd_premium/9f2b7c1a-4d3e-4a5f-8b0c-2e1d0f9a8b77 \
-H "X-API-Key: $CU_API_KEY"Este endpoint no acepta
parsePdf. A diferencia de semanas cotizadas premium, la constancia de vigencia todavía no se entrega estructurada en JSON: sólo el PDF. Preferimos no ofrecer el parámetro a ofrecerlo devolviendo siemprenull. Cuando exista, se documentará aquí y la llavedocumentDataaparecerá en los cuerpos.
Respuesta
El shape es idéntico al cuerpo del webhook, con un estado adicional pending mientras la consulta aún se genera:
json
// pending — generación en curso; vuelve a consultar en unos segundos
{ "version": 1, "uuid": "...", "status": "pending", "pdfBase64": null, "errorMessage": null }json
// completed — constancia lista
{ "version": 1, "uuid": "...", "status": "completed", "pdfBase64": "JVBE...", "errorMessage": null }json
// failed — no se pudo obtener
{ "version": 1, "uuid": "...", "status": "failed", "pdfBase64": null, "errorMessage": "..." }Errores
| HTTP | Caso | errorCode | message |
|---|---|---|---|
| 404 | El uuid no existe o no pertenece a tu cuenta | request_not_found | No encontramos la consulta solicitada, favor de verificar el identificador |
| 401 | X-API-Key ausente, inválida o revocada | ver 401 — Autenticación |
request_not_foundno es lo mismo que un 404 de ruta inexistente. Si escribes mal la URL recibirás el 404 genérico de la API ({"message": "Resource not found", "errorCode": ""}). El códigorequest_not_foundconfirma que la ruta era correcta y que eluuidfue el que no se encontró.
Cuándo usarlo
- Modo polling-only: enviaste el submit sin
webhookUrl. Consulta hasta questatusdeje de serpending. - Tu webhook estuvo caído y quieres recuperar la constancia.
- Necesitas verificar el estado antes de tomar una decisión en tu UI.
Frecuencia recomendada: son dos tiempos distintos. Espera 30 s después del submit antes de la primera consulta —el tiempo típico de generación arranca ahí, así que consultar antes sólo devuelve pending— y a partir de entonces consulta cada ~10 segundos. En ningún caso hagas más de 1 request cada 5 segundos por uuid. El resultado típico tarda 30 s – 3 min y hasta 10 min en escenarios degradados.
4. Reintentar entrega — POST /v3/vd_premium/{uuid}/replay
Aplica sólo a solicitudes que se enviaron con
webhookUrl. Solicitudes en modo polling-only no tienen URL a la cual reenviar y devuelven422— usaGET /v3/vd_premium/{uuid}para obtener el resultado.
Si las 3 entregas automáticas a tu webhook fallaron (o quieres una copia adicional), reintenta manualmente. Autenticado con X-API-Key. No consume créditos.
Request
bash
curl -X POST https://api.consultaunica.mx/v3/vd_premium/9f2b7c1a-4d3e-4a5f-8b0c-2e1d0f9a8b77/replay \
-H "X-API-Key: $CU_API_KEY"Respuesta — 202 Accepted
json
{ "uuid": "9f2b7c1a-...", "scheduled": true }Disparamos una nueva entrega al webhookUrl original en background, con los mismos 3 intentos y backoff que una entrega regular. El 202 confirma que se agendó, no que tu endpoint la recibió.
Cooldown
Sólo un replay por uuid cada 60 segundos. El segundo request dentro de esa ventana recibe 429:
http
HTTP/1.1 429 Too Many Requests
Retry-After: 47json
{
"message": "Debes esperar antes de volver a solicitar la entrega de esta consulta",
"description": "Ya solicitaste el reenvío de esta consulta hace menos de un minuto; espera un momento antes de pedir otro.",
"errorCode": "replay_cooldown_active"
}Retry-After indica los segundos restantes del cooldown.
Errores
| HTTP | Caso | errorCode |
|---|---|---|
| 404 | uuid no existe o no pertenece a tu cuenta | request_not_found |
| 409 | La consulta aún está en status=pending (no hay nada que reenviar) | replay_not_available_while_pending |
| 422 | La solicitud original se envió en modo polling-only (sin webhookUrl) | replay_unavailable_polling_only |
| 429 | Cooldown activo por uuid | replay_cooldown_active + Retry-After |
| 500 | No se pudo leer la configuración de entrega de la solicitud | replay_metadata_failed |
| 401 | Auth | ver 401 — Autenticación |
Flujo típico de recuperación
- Consultas
GET /v3/vd_premium/{uuid}→status=completed(ofailed). - Llamas
POST /v3/vd_premium/{uuid}/replay→ 202. - Tu endpoint webhook recibe el mismo body que habría recibido originalmente (mismo
uuid, firma fresca con nuevoX-CU-Timestamp).
5. Flujo completo
Entregamos la constancia a tu URL en cuanto está lista. No tienes que preguntar por ella.
9 pasos
Secuencia del modo Webhook (push):
- De Tu backend a Consulta Única: POST /v3/vd_premium, X-API-Key · curp · webhookUrl.
- De Consulta Única a Seguro Social: Validación de la CURP.
- De Seguro Social a Consulta Única: 200 OK, { nss, fullname }.
- De Consulta Única a Tu backend: 200 OK · retiene 3 créditos, { uuid, nss, fullname }.
- De Consulta Única a Seguro Social: Consulta de vigencia de derechos.
- Espera — generación: 30 s – 3 min.
- De Seguro Social a Consulta Única: Constancia en PDF, aquí se cobran los 3 créditos.
- De Consulta Única a Tu backend: POST <webhookUrl>, firmado · { uuid, status, pdfBase64 }.
- De Tu backend a Consulta Única: 202 Accepted, valida la firma antes de confiar.
Tiempos típicos de generación: 30 s – 3 min entre submit y resultado. Diseña tu UX asumiendo hasta 10 minutos en casos degradados.
Ver los dos flujos en texto
Modo webhook (push)
Tu backend Consulta Única Seguro Social
│ │ │
│ POST /v3/vd_premium │ │
│ X-API-Key · curp · webhookUrl │ │
│────────────────────────────────────────>│ │
│ │ │
│ │ Validación de la CURP │
│ │────────────────────────────────────────>│
│ │ │
│ │ 200 OK │
│ │ { nss, fullname } │
│ │<────────────────────────────────────────│
│ │ │
│ 200 OK · retiene 3 créditos │ │
│ { uuid, nss, fullname } │ │
│<────────────────────────────────────────│ │
│ │ │
│ │ Consulta de vigencia de derechos │
│ │────────────────────────────────────────>│
│ │ │
│ │ ... generación: 30 s – 3 min ... │
│ │ │
│ │ Constancia en PDF │
│ │ aquí se cobran los 3 créditos │
│ │<────────────────────────────────────────│
│ │ │
│ POST <webhookUrl> │ │
│ firmado · { uuid, status, pdfBase64 } │ │
│<────────────────────────────────────────│ │
│ │ │
│ 202 Accepted │ │
│ valida la firma antes de confiar │ │
│────────────────────────────────────────>│ │
│ │ │Modo polling-only (pull)
Tu backend Consulta Única Seguro Social
│ │ │
│ POST /v3/vd_premium │ │
│ X-API-Key · curp — sin webhookUrl │ │
│────────────────────────────────────────>│ │
│ │ │
│ │ Validación de la CURP │
│ │────────────────────────────────────────>│
│ │ │
│ │ 200 OK │
│ │ { nss, fullname } │
│ │<────────────────────────────────────────│
│ │ │
│ 200 OK · retiene 3 créditos │ │
│ { uuid, nss, fullname } │ │
│<────────────────────────────────────────│ │
│ │ │
│ │ Consulta de vigencia de derechos │
│ │────────────────────────────────────────>│
│ │ │
│ │ ... generación: 30 s – 3 min ... │
│ │ │
│ GET /v3/vd_premium/{uuid} │ │
│ cada ~10 s · no consume créditos │ │
│────────────────────────────────────────>│ │
│ │ │
│ 200 OK │ │
│ { status: "pending" } │ │
│<────────────────────────────────────────│ │
│ │ │
│ │ Constancia en PDF │
│ │ aquí se cobran los 3 créditos │
│ │<────────────────────────────────────────│
│ │ │
│ GET /v3/vd_premium/{uuid} │ │
│────────────────────────────────────────>│ │
│ │ │
│ 200 OK │ │
│ { status: "completed", pdfBase64: … } │ │
│<────────────────────────────────────────│ │
│ │ │6. Preguntas frecuentes
¿En qué se diferencia de las otras dos consultas de vigencia de derechos? Esta es la única que te entrega la constancia en PDF a tu sistema. Solo validar vigencia devuelve un JSON con banderas y ningún documento; vigencia al correo hace que el Seguro Social mande el documento al correo del titular, no a ti. Las otras dos son síncronas y piden CURP + NSS; ésta es asíncrona y sólo pide la CURP.
¿Puedo consultar vigencia y semanas cotizadas de la misma CURP el mismo día? Sí, incluso al mismo tiempo. Son productos independientes: no comparten reserva de idempotencia, no compiten por el candado de duplicados del proveedor y cada uno tiene su propio cupo de rate limit.
¿Cuándo conviene usar polling-only y cuándo webhook?
- Webhook (push) es la opción recomendada si tu backend tiene un endpoint HTTPS público: te enteras del resultado en cuanto está listo, sin polling extra.
- Polling-only (pull) es ideal si corres en infraestructura sin endpoint público (jobs cron, máquinas detrás de NAT, scripts locales) o si quieres simplicidad operativa sin manejar firma HMAC ni reintentos en tu lado.
¿Puedo cambiar el webhookUrl después de enviar la solicitud? No. La URL se captura al momento del submit. Si necesitas otra, emite una nueva solicitud. Tampoco puedes pasar de polling-only a webhook (ni viceversa) en una solicitud existente.
¿Qué pasa si mi servidor está caído cuando entregamos? Hacemos 3 intentos (0s / 30s / 120s). Si todos fallan, puedes recuperarte sin pagar de nuevo: consulta GET /v3/vd_premium/{uuid} para ver el estado y descargar el PDF, o llama POST /v3/vd_premium/{uuid}/replay para reintentar la entrega (cooldown 60s).
¿Qué hago si mi cliente pierde la respuesta del submit por un error de red? Reintenta el mismo submit con un header Idempotency-Key estable. Si el primero sí llegó y se procesó, el retry devuelve la misma respuesta sin cobrar de nuevo. Ver Idempotency-Key (opcional).
¿Por qué el nss de la respuesta viene vacío? Porque el Seguro Social aceptó la consulta sin haber resuelto todavía al titular. No es un error ni anticipa un fallo: los datos llegan igual con la constancia.
¿Cómo pruebo en desarrollo? Usa un servicio tipo webhook.site, ngrok o smee.io para exponer tu endpoint local en HTTPS con IP pública. Localhost y túneles HTTP no pasan la validación. Si no quieres montar nada de eso, usa modo polling-only: omite webhookUrl y consulta el resultado por GET. Y para todo lo demás está el modo mock, que no cobra.
¿Cuántos créditos cuesta? 3 por cada consulta que llega a entregar la constancia. Los webhooks de entrega no cobran adicional.
¿Hay carga por lotes? No. Este producto se consulta de una CURP a la vez.
7. Modo Mock
Para pruebas de integración del lado del cliente, hay endpoints mock con prefijo /mock que simulan el contrato completo sin consultar al Seguro Social ni cobrar créditos.
Rutas
| Productivo | Mock equivalente |
|---|---|
POST /v3/vd_premium | POST /v3/mock/vd_premium |
GET /v3/vd_premium/{uuid} | GET /v3/mock/vd_premium/{uuid} |
POST /v3/vd_premium/{uuid}/replay | POST /v3/mock/vd_premium/{uuid}/replay |
Migración: elimina /mock del path. El header X-Mock-Mode será ignorado en producción.
Acceso
Necesitas una API Key válida y una cuenta activa, pero no una compra previa ni saldo: los dos guards de dinero (403 api_key_requires_purchase y 429 too_many_requests) no aplican aquí. Ese es justamente el punto del ambiente de pruebas.
Lo que acota el volumen es una cuota diaria por usuario, propia de este producto: agotar la de vigencia no te impide seguir probando semanas cotizadas ni actas. Al agotarla recibes 429 con errorCode: "mock_daily_quota_exceeded" y un header Retry-After.
Header X-Mock-Mode
| Valor | Comportamiento |
|---|---|
success | Tras ~1 s, webhook con status=completed y un PDF dummy en pdfBase64. |
failed | Tras ~1 s, webhook con status=failed y errorMessage="mock_service_error". |
pending | No dispara webhook. GET siempre regresa status=pending. |
| (ausente) | Default success. |
Valores no permitidos → 400 con errorCode: "invalid_mock_mode".
El mock devuelve los mismos
errorCodeque producción. Unuuidinexistente responderequest_not_founden ambos, el replay bloqueado respondereplay_cooldown_activeen ambos, etc. Puedes construir tu diccionario de códigos contra el mock y migrar a producción sin cambiarlo. Los únicos códigos exclusivos del mock soninvalid_mock_modeymock_daily_quota_exceeded, que en producción no existen.
Polling-only en mock: el mock también soporta omitir
webhookUrl. Si lo omites, no dispararemos webhook (independiente del modo) yreplayregresará422 replay_unavailable_polling_only. ElGETsigue funcionando idéntico a producción.
Comportamiento
- Los uuids generados viven 1 hora en caché; luego expiran y
GET/replayregresanrequest_not_found. - El webhook se dispara al
webhookUrlprovisto, firmado con tu API Key (mismos headersX-CU-Signature,X-CU-TimestampyX-CU-Uuidque producción), tras un retardo de ~1 s para que el submit alcance a responder antes. Un único intento, sin reintentos: un ambiente de pruebas no debe insistirle a un endpoint que todavía estás levantando. Si lo pierdes, recupéralo por polling o replay. Idempotency-Keyse acepta por compatibilidad pero se ignora en mock.- Datos mock fijos en la respuesta del POST:
nss="12345678901",fullname="JUAN MOCK PEREZ". Es la misma persona ficticia de todos los ambientes de pruebas. replaymantiene el cooldown de 60 s y regresa409si el modo espending. El polling no consume cuota diaria; el replay sí, porque también dispara una entrega.- Rate limit: 3 req / 2 s por
X-API-KeyenPOST /mock/vd_premium— la misma cuota que producción, en un bucket independiente. Lo que acota el volumen del mock es su cuota diaria, no este limitador. - La respuesta del POST sí incluye el header
X-Remaining-Requestscon tu saldo actual (sin deducción, ya que el mock no cobra). Útil para que tu cliente ejercite la lógica de alertas igual que en producción.
Ejemplos
Submit success:
bash
curl -X POST https://api.consultaunica.mx/v3/mock/vd_premium \
-H "X-API-Key: $CU_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Mock-Mode: success" \
-d '{"curp":"PEGJ800101HDFRRN09","webhookUrl":"https://tuapp.com/cb"}'Submit que simula error del servicio:
bash
curl -X POST https://api.consultaunica.mx/v3/mock/vd_premium \
-H "X-API-Key: $CU_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Mock-Mode: failed" \
-d '{"curp":"PEGJ800101HDFRRN09","webhookUrl":"https://tuapp.com/cb"}'Submit pending (no dispara webhook), para ejercitar tu polling:
bash
curl -X POST https://api.consultaunica.mx/v3/mock/vd_premium \
-H "X-API-Key: $CU_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Mock-Mode: pending" \
-d '{"curp":"PEGJ800101HDFRRN09","webhookUrl":"https://tuapp.com/cb"}'Polling y replay:
bash
curl https://api.consultaunica.mx/v3/mock/vd_premium/<uuid> \
-H "X-API-Key: $CU_API_KEY"
curl -X POST https://api.consultaunica.mx/v3/mock/vd_premium/<uuid>/replay \
-H "X-API-Key: $CU_API_KEY"8. Changelog
v1.0 — primer release del endpoint público
vd_premium. Incluye desde el inicio: submit conwebhookUrlopcional (modo webhook y modo polling-only), headerIdempotency-Keycon TTL alineado a la medianoche de la Ciudad de México, camporedelivereden la respuesta del submit para distinguir la re-entrega del mismo día, webhook firmado con HMAC-SHA256 yversion: 1, endpoint de consulta poruuid, endpoint de replay con cooldown de 60 s,errorCodeen todos los endpoints y modo mock conX-Mock-Mode.Este changelog no hereda el de semanas cotizadas premium: son productos distintos. Lo que allá se fue agregando hasta la v1.9 aquí ya viene de fábrica.