Descripción general
Los webhooks son la tercera familia de la Developer API de CazVid, junto a Job Builder y Candidate Search. Registras un webhook endpoint, eliges los eventos que debe recibir y CazVid envía cada evento por POST a tu URL. Las entregas van firmadas y solo se envían a endpoints que hayas verificado.
- Entrega al menos una vez. El mismo evento puede llegar más de una vez; deduplica con el
iddel sobre. - Sin garantía de orden. Usa
occurredAtsi necesitas ordenar los eventos. - Solo aditivo dentro de
v1. Pueden aparecer campos nuevos sin aviso, así que ignora los campos desconocidos. - Solo endpoints verificados. El acceso en producción requiere un plan Platinum o superior.
Autenticación y planes
Los webhooks reutilizan tus claves de la Developer API de CazVid: una sola clave funciona en Job Builder, Candidate Search y Webhooks. Envíala con el encabezado x-api-key o como un token Authorization: Bearer. Trata las claves como secretos. Los propietarios y administradores de la organización también pueden gestionar los endpoints desde la pestaña Webhooks en CazVid.
x-api-key: cazvid_jb_...
Authorization: Bearer cazvid_jb_...
Content-Type: application/jsonDos scopes controlan el acceso a los webhooks. Las claves nuevas obtienen ambos por defecto.
webhooks:read- listar endpoints, leer un endpoint y su registro de entregas, leer el catálogo de eventos y el uso.webhooks:manage- crear, actualizar, eliminar, rotar el secreto, verificar, probar y reenviar.
Endpoints de gestión
Todas las rutas parten de /api/v3.0/webhooks/public. La API de gestión se autentica con clave y está limitada por scope. Un endpoint creado con una clave se limita a los workspaces de esa clave, y una organización puede registrar hasta 10 endpoints. Un endpoint nuevo devuelve su secreto de firma una sola vez y comienza en pending_verification.
| Método | Ruta | Scope | Propósito |
|---|---|---|---|
| POST | /public/webhook-endpoints | webhooks:manage | Crea un endpoint. El secreto de firma se devuelve una sola vez. |
| GET | /public/webhook-endpoints | webhooks:read | Lista tus endpoints. Los secretos nunca se devuelven. |
| GET | /public/webhook-endpoints/:id | webhooks:read | Detalle del endpoint más estadísticas de entrega. |
| PATCH | /public/webhook-endpoints/:id | webhooks:manage | Actualiza url, events, description o status (active o disabled). |
| DELETE | /public/webhook-endpoints/:id | webhooks:manage | Elimina (soft delete) un endpoint. |
| POST | /public/webhook-endpoints/:id/rotate-secret | webhooks:manage | Genera un secreto nuevo. El anterior sigue firmando durante 24 horas. |
| POST | /public/webhook-endpoints/:id/verify | webhooks:manage | Reenvía el challenge de verificación. |
| POST | /public/webhook-endpoints/:id/test | webhooks:manage | Envía una entrega de prueba webhook.ping. |
| GET | /public/webhook-endpoints/:id/deliveries | webhooks:read | Registro de entregas. Solo metadatos, nunca payloads. |
| POST | /public/webhook-endpoints/:id/deliveries/:deliveryId/redeliver | webhooks:manage | Reencola una entrega pasada dentro de 30 días. |
| GET | /public/events | webhooks:read | El catálogo de tipos de evento. |
| GET | /public/usage | webhooks:read | Número de endpoints y entregas usadas hoy frente al tope. |
| GET | /public/assets/:token | webhooks:read | Canjea un enlace de recurso. Redirige (302) a una URL prefirmada de corta duración. |
Crear un endpoint
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/webhooks/public/webhook-endpoints \
-H "x-api-key: cazvid_jb_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/cazvid",
"description": "ATS application sync",
"events": ["application.received"],
"workspaceIds": ["WORKSPACE_ID"]
}'Respuesta (el secreto se devuelve una vez)
{
"id": "66a1f0c9e4b0a1d2c3e4f5a6",
"url": "https://hooks.example.com/cazvid",
"description": "ATS application sync",
"events": ["application.received"],
"workspaceIds": ["66936c2f8a1b4e0011aa2b3c"],
"status": "pending_verification",
"apiVersion": "v1",
"secret": "whsec_9tJ0k3mX2pQ7rL5nV8wZ1yB4cD6eF0g",
"createdAt": "2026-07-09T14:20:11.004Z",
"updatedAt": "2026-07-09T14:20:11.004Z"
}Enviar una prueba (ping)
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/webhooks/public/webhook-endpoints/ENDPOINT_ID/test \
-H "x-api-key: cazvid_jb_..."Sobre del evento
Cada entrega, para cualquier tipo de evento, es el mismo sobre JSON. El cuerpo específico del tipo vive en data.
{
"id": "evt_7f3a9c2e1b6d4f80a5c3e9d1f2b4a6c8",
"type": "application.received",
"apiVersion": "v1",
"createdAt": "2026-07-09T14:32:07.812Z",
"occurredAt": "2026-07-09T14:32:06.104Z",
"organizationId": "6462916b41d57a6091c3d18a",
"data": { }
}| Campo | Tipo | Descripción |
|---|---|---|
| id | string | Id único del evento, estable en cada reintento. Úsalo como clave de deduplicación. |
| type | string | El tipo de evento, por ejemplo application.received. |
| apiVersion | "v1" | Versión del contrato del payload. Por ahora siempre v1. |
| createdAt | string (ISO 8601) | Cuándo CazVid emitió el evento. Estable en los reintentos. |
| occurredAt | string (ISO 8601) | Hora de negocio en que ocurrió el evento subyacente (el appliedAt real). |
| organizationId | string | El id de tu organización. |
| data | object | El cuerpo específico del tipo, documentado por evento más abajo. |
Eventos
En la v1 se incluyen tres tipos de evento.
| Evento | Cuándo |
|---|---|
| application.received | Un candidato completa una postulación a uno de tus empleos. |
| webhook.verify | Se envía al crear y al volver a verificar. Devuelve el challenge para activar el endpoint. |
| webhook.ping | Se envía con la acción de prueba. data.test es true; nunca afecta la suspensión. |
application.received
Se envía cuando un candidato completa una postulación a uno de tus empleos, tanto en el flujo de postulación de escritorio como en el móvil. candidate.email es la clave estable de la persona y application.id identifica la postulación; consulta IDs y recursos para saber cómo tratar cada campo. screeningAnswers está vacío cuando el empleo no tiene preguntas de filtro.
{
"id": "evt_7f3a9c2e1b6d4f80a5c3e9d1f2b4a6c8",
"type": "application.received",
"apiVersion": "v1",
"createdAt": "2026-07-09T14:32:07.812Z",
"occurredAt": "2026-07-09T14:32:06.104Z",
"organizationId": "6462916b41d57a6091c3d18a",
"data": {
"application": {
"id": "66a1f0c9e4b0a1d2c3e4f5a6",
"listItemId": "66a1f0c9e4b0a1d2c3e4f5b7",
"url": "https://desktop.cazvid.app/employer/applicants/66a1f0c9e4b0a1d2c3e4f5a6",
"appliedAt": "2026-07-09T14:32:06.104Z",
"source": "one_tap",
"qualificationStatus": "qualified",
"screeningAnswers": [
{
"questionId": "6698c1a2f0b7d90012ab34ce",
"question": "Do you have at least 2 years of customer support experience?",
"answer": "Yes",
"questionType": "boolean",
"answerKind": "boolean",
"required": true,
"disqualifying": true
}
]
},
"job": {
"postId": "6698c1a2f0b7d90012ab34cd",
"url": "https://cazvid.com/job/senior-customer-support-specialist",
"title": "Senior Customer Support Specialist",
"locationName": "Mexico City, Mexico",
"countryCode": "MX",
"isRemote": false,
"workspaceId": "66936c2f8a1b4e0011aa2b3c"
},
"candidate": {
"publicId": "pc1.ZXhhbXBsZS1vcGFxdWUtaGFuZGxlLXRva2Vu",
"name": "Maria Gonzalez",
"email": "maria.gonzalez@example.com",
"phoneNumber": "+52 55 1234 5678",
"locationName": "Mexico City, Mexico",
"countryCode": "MX",
"resumeUrl": "https://aio-backend-prod.cazvid.app/api/v3.0/webhooks/public/assets/eyJhbGciOi...",
"videoUrl": "https://ik.imagekit.io/cazvid/candidate-videos/66a1f0c9.mp4"
}
}
}webhook.verify
Se envía cuando creas o vuelves a verificar un endpoint. Devuelve la cadena exacta de data.challenge en el cuerpo de tu respuesta 2xx para activar el endpoint. Hasta entonces no se entrega ningún evento real.
{
"id": "evt_2b9d4f8a1c6e0d3b5f7a9c1e2d4b6a8c",
"type": "webhook.verify",
"apiVersion": "v1",
"createdAt": "2026-07-09T14:20:12.507Z",
"occurredAt": "2026-07-09T14:20:12.507Z",
"organizationId": "6462916b41d57a6091c3d18a",
"data": {
"challenge": "whc_c1f4a8e2b6d0937f5a1c9e3d7b2408af"
}
}webhook.ping
Se envía con la acción de prueba para que confirmes tu receptor y la verificación de firma de extremo a extremo. data.test siempre es true y los pings nunca cambian el estado de suspensión de un endpoint.
{
"id": "evt_5c1e9a2d7f4b0836a9c3e1d5f7b2408a",
"type": "webhook.ping",
"apiVersion": "v1",
"createdAt": "2026-07-09T14:25:00.113Z",
"occurredAt": "2026-07-09T14:25:00.113Z",
"organizationId": "6462916b41d57a6091c3d18a",
"data": {
"test": true
}
}Verificación del endpoint
Un endpoint no puede recibir eventos reales hasta que demuestres que controlas su URL.
- 1.Al crear (y en cada llamada de verificación) el endpoint queda en
pending_verificationy CazVid envía por POST un eventowebhook.verifya la URL. - 2.Tu endpoint devuelve una respuesta 2xx cuyo cuerpo repite la cadena exacta de
data.challenge. - 3.Con un echo correcto, el endpoint pasa a
activey empieza a recibir los eventos a los que se suscribió. - 4.La reverificación está limitada a 5 intentos por hora. Usa el endpoint de verificación o el botón de verificar en CazVid.
Verificación de firmas
Cada entrega se firma con el esquema Standard Webhooks. Verifica cada solicitud antes de confiar en ella. El POST llega con tres encabezados:
webhook-id: evt_7f3a9c2e1b6d4f80a5c3e9d1f2b4a6c8
webhook-timestamp: 1783002727
webhook-signature: v1,g0X4Hq7m2p9sB1vZ8wYc3dE5fG6hJ0kL2mN4oP6qR8=El contenido firmado es {webhook-id}.{webhook-timestamp}.{body}, con HMAC-SHA256 usando el secreto de tu endpoint, codificado en base64 y con el prefijo v1,. Verifica los bytes crudos exactos que recibiste; no vuelvas a serializar el JSON parseado o los espacios y el orden de las claves romperán la firma. Durante una rotación el encabezado lleva dos firmas separadas por un espacio; acepta la solicitud si alguna coincide. Rechaza un webhook-timestamp que difiera más de 5 minutos de tu hora actual para acotar los replays.
whsec_<base64>. Quita el prefijo whsec_ y decodifica el resto en base64 para obtener la clave de firma cruda. Las librerías de Standard Webhooks lo hacen por ti cuando les pasas el valor completo whsec_....Instala la librería de Standard Webhooks con npm i standardwebhooks (Node) o pip install standardwebhooks (Python). Las librerías de svix son compatibles a nivel de protocolo. standardwebhooks.com
Node
import { Webhook } from "standardwebhooks";
import express from "express";
// The secret is the whsec_... value returned at create / rotate-secret. The
// library strips the prefix and base64-decodes it to the raw key for you.
const wh = new Webhook(process.env.CAZVID_WEBHOOK_SECRET);
const app = express();
app.post(
"/cazvid-webhooks",
express.raw({ type: "application/json" }), // verify the RAW body
(req, res) => {
try {
const event = wh.verify(req.body, {
"webhook-id": req.header("webhook-id"),
"webhook-timestamp": req.header("webhook-timestamp"),
"webhook-signature": req.header("webhook-signature"),
});
// event is the verified envelope. Dedupe on event.id, then ACK fast.
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
},
);Python
from standardwebhooks import Webhook
# The secret is the whsec_... value from create / rotate-secret.
wh = Webhook(secret)
@app.post("/cazvid-webhooks")
async def receive(request):
raw = await request.body() # verify the RAW bytes
headers = {
"webhook-id": request.headers["webhook-id"],
"webhook-timestamp": request.headers["webhook-timestamp"],
"webhook-signature": request.headers["webhook-signature"],
}
event = wh.verify(raw, headers) # raises on an invalid signature
return Response(status_code=200)Verificación manual (sin dependencia)
import { createHmac, timingSafeEqual } from "node:crypto";
// Fallback if you cannot add the standardwebhooks dependency.
function verify(rawBody, headers, whsecret, toleranceSec = 300) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;
// Strip "whsec_" and base64-decode to the raw signing key.
const key = Buffer.from(whsecret.replace(/^whsec_/, ""), "base64");
const signed = id + "." + ts + "." + rawBody;
const expected = createHmac("sha256", key).update(signed).digest("base64");
// The header can carry several space-delimited signatures during rotation.
return headers["webhook-signature"]
.split(" ")
.map((part) => part.split(",")[1])
.some((sig) => {
const a = Buffer.from(sig);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
});
}IDs y recursos
El payload combina varios tipos de identificador. Trátalos de forma distinta.
| Campo | Clase | Notas |
|---|---|---|
| application.id | Crudo, estable | El id de la postulación (qatId). Estable en los reintentos e idéntico en escritorio y móvil. Seguro para guardar como clave de la postulación. |
| job.postId | Crudo | El id del post en la plataforma, el mismo que devuelve la Job Builder API. |
| organizationId | Crudo | El id de tu organización. |
| job.workspaceId | Crudo | El workspace al que pertenece el empleo. |
| candidate.publicId | Token opaco | Un handle pc1., canjeable en el flujo de contacto autenticado. NO es correlacionable entre eventos (aleatorio por payload), así que nunca lo uses como clave de persona. |
| candidate.email | Clave estable de persona | Úsalo para relacionar o unificar a una persona entre varias postulaciones. |
| candidate.resumeUrl | Enlace de corta duración | Un enlace de canje GET /public/assets/<token>. El token dura unos 30 días y redirige (302) a una URL prefirmada nueva de aproximadamente 1 hora. Presente solo si el candidato adjuntó un currículum. |
| candidate.videoUrl | CDN público | Una URL de ImageKit, de acceso público. Presente solo si el candidato subió un video. |
candidate.resumeUrl es un endpoint de canje autenticado, no un enlace directo al archivo. Llama a GET /public/assets/<token> con una clave que tenga webhooks:read y devuelve un 302 a una URL prefirmada nueva de aproximadamente 1 hora; el token también está vinculado a tu organización. Sigue la redirección pronto y no la almacenes; vuelve a canjear el token (válido unos 30 días) cuando necesites el archivo de nuevo.Entrega, reintentos y suspensión
Una entrega tiene éxito cuando tu endpoint devuelve cualquier 2xx dentro de un límite de 10 segundos de reloj (DNS, conexión, TLS y respuesta). Procesa de forma asíncrona y responde (ACK) rápido. Los cuerpos de respuesta se ignoran salvo el echo del challenge de verificación. Las redirecciones cuentan como fallo. Las entregas son solo HTTPS, en el puerto 443 o 8443, y CazVid no entrega a direcciones privadas ni internas.
Los intentos fallidos se reintentan con este calendario: 8 intentos durante aproximadamente 1,8 días y luego la entrega se marca como fallida:
attempt 1 immediate
attempt 2 +1 minute
attempt 3 +5 minutes
attempt 4 +30 minutes
attempt 5 +2 hours
attempt 6 +5 hours
attempt 7 +10 hours
attempt 8 +24 hoursid, que tu consumidor deduplica.Límites y comportamiento
- Hasta 10 endpoints por organización.
- Hasta 5.000 entregas por organización por día. El exceso se aparca como fallido y se puede reenviar al día siguiente.
- Los pings de prueba cuentan para el tope diario y están limitados a 10 por día por endpoint.
- La reverificación está limitada a 5 intentos por hora por endpoint.
- Los registros de entrega se conservan durante 30 días.
¿Construyes más sobre CazVid? Explora el índice de la Developer API para las APIs de Job Builder y Candidate Search.