CazVid
Documentación de la API

Webhooks API

Webhooks API v1

Registra un endpoint HTTPS, suscríbete a eventos y CazVid envía un POST firmado cada vez que ocurre uno de esos eventos, así tu integración nunca hace polling. El primer evento es application.received.

URL base

https://aio-backend-prod.cazvid.app/api/v3.0/webhooks

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 id del sobre.
  • Sin garantía de orden. Usa occurredAt si 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/json

Dos 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.
El acceso a la API en producción requiere un plan Platinum o superior. Los propietarios y administradores de la organización gestionan las claves y los endpoints desde la sección Developer API en CazVid.

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étodoRutaScopePropósito
POST/public/webhook-endpointswebhooks:manageCrea un endpoint. El secreto de firma se devuelve una sola vez.
GET/public/webhook-endpointswebhooks:readLista tus endpoints. Los secretos nunca se devuelven.
GET/public/webhook-endpoints/:idwebhooks:readDetalle del endpoint más estadísticas de entrega.
PATCH/public/webhook-endpoints/:idwebhooks:manageActualiza url, events, description o status (active o disabled).
DELETE/public/webhook-endpoints/:idwebhooks:manageElimina (soft delete) un endpoint.
POST/public/webhook-endpoints/:id/rotate-secretwebhooks:manageGenera un secreto nuevo. El anterior sigue firmando durante 24 horas.
POST/public/webhook-endpoints/:id/verifywebhooks:manageReenvía el challenge de verificación.
POST/public/webhook-endpoints/:id/testwebhooks:manageEnvía una entrega de prueba webhook.ping.
GET/public/webhook-endpoints/:id/deliverieswebhooks:readRegistro de entregas. Solo metadatos, nunca payloads.
POST/public/webhook-endpoints/:id/deliveries/:deliveryId/redeliverwebhooks:manageReencola una entrega pasada dentro de 30 días.
GET/public/eventswebhooks:readEl catálogo de tipos de evento.
GET/public/usagewebhooks:readNúmero de endpoints y entregas usadas hoy frente al tope.
GET/public/assets/:tokenwebhooks:readCanjea 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": { }
}
CampoTipoDescripción
idstringId único del evento, estable en cada reintento. Úsalo como clave de deduplicación.
typestringEl tipo de evento, por ejemplo application.received.
apiVersion"v1"Versión del contrato del payload. Por ahora siempre v1.
createdAtstring (ISO 8601)Cuándo CazVid emitió el evento. Estable en los reintentos.
occurredAtstring (ISO 8601)Hora de negocio en que ocurrió el evento subyacente (el appliedAt real).
organizationIdstringEl id de tu organización.
dataobjectEl cuerpo específico del tipo, documentado por evento más abajo.

Eventos

En la v1 se incluyen tres tipos de evento.

EventoCuándo
application.receivedUn candidato completa una postulación a uno de tus empleos.
webhook.verifySe envía al crear y al volver a verificar. Devuelve el challenge para activar el endpoint.
webhook.pingSe 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. 1.Al crear (y en cada llamada de verificación) el endpoint queda en pending_verification y CazVid envía por POST un evento webhook.verify a la URL.
  2. 2.Tu endpoint devuelve una respuesta 2xx cuyo cuerpo repite la cadena exacta de data.challenge.
  3. 3.Con un echo correcto, el endpoint pasa a active y empieza a recibir los eventos a los que se suscribió.
  4. 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.

Tu secreto tiene la forma 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.

CampoClaseNotas
application.idCrudo, estableEl 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.postIdCrudoEl id del post en la plataforma, el mismo que devuelve la Job Builder API.
organizationIdCrudoEl id de tu organización.
job.workspaceIdCrudoEl workspace al que pertenece el empleo.
candidate.publicIdToken opacoUn 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.emailClave estable de personaÚsalo para relacionar o unificar a una persona entre varias postulaciones.
candidate.resumeUrlEnlace de corta duraciónUn 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.videoUrlCDN públicoUna 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 hours
Si un endpoint lleva fallando al menos 24 horas Y acumula al menos 12 intentos fallidos, se suspende y recibes una notificación en la app. Un evento real entregado limpia la racha de fallos; los pings exitosos no. Reactivar un endpoint suspendido es una acción explícita.
Los eventos perdidos no se pierden para siempre. Reenvía cualquier entrega dentro de su ventana de 30 días desde el registro de entregas o el endpoint de reenvío; el payload original se reenvía con el mismo id, 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.