CazVid
API docs

Webhooks API

Webhooks API v1

Register an HTTPS endpoint, subscribe to events, and CazVid sends a signed POST whenever one of those events happens - so your integration never polls. The first event is application.received.

Base URL

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

Overview

Webhooks are the third family of the CazVid Developer API, alongside Job Builder and Candidate Search. You register a webhook endpoint, choose the events it should receive, and CazVid POSTs each event to your URL. Deliveries are signed and only sent to endpoints you have verified.

  • At-least-once delivery. The same event can arrive more than once - dedupe on the envelope id.
  • No ordering guarantee. Use occurredAt if you need to order events.
  • Additive-only within v1. New fields can appear without notice, so ignore unknown fields.
  • Verified endpoints only. Production access requires a Platinum plan or higher.

Authentication and plans

Webhooks reuse your existing CazVid Developer API keys - one key works across Job Builder, Candidate Search, and Webhooks. Send it with the x-api-key header or as an Authorization: Bearer token. Treat keys as secrets. Org owners and admins can also manage endpoints from the Webhooks tab in CazVid.

x-api-key: cazvid_jb_...
Authorization: Bearer cazvid_jb_...
Content-Type: application/json

Two scopes gate webhook access. New keys get both by default.

  • webhooks:read - list endpoints, read an endpoint and its delivery log, read the event catalog and usage.
  • webhooks:manage - create, update, delete, rotate the secret, verify, test, and redeliver.
Production API access requires a Platinum plan or higher. Org owners and admins manage keys and endpoints from the Developer API section in CazVid.

Management endpoints

All routes are rooted at /api/v3.0/webhooks/public. The management API is key-authenticated and scope-gated. An endpoint created with a key is limited to that key's workspaces, and an organization can register up to 10 endpoints. A new endpoint returns its signing secret once and starts in pending_verification.

MethodPathScopePurpose
POST/public/webhook-endpointswebhooks:manageCreate an endpoint. The signing secret is returned once.
GET/public/webhook-endpointswebhooks:readList your endpoints. Secrets are never returned.
GET/public/webhook-endpoints/:idwebhooks:readEndpoint detail plus delivery stats.
PATCH/public/webhook-endpoints/:idwebhooks:manageUpdate url, events, description, or status (active or disabled).
DELETE/public/webhook-endpoints/:idwebhooks:manageSoft-delete an endpoint.
POST/public/webhook-endpoints/:id/rotate-secretwebhooks:manageMint a new secret. The old one keeps signing for 24 hours.
POST/public/webhook-endpoints/:id/verifywebhooks:manageResend the verification challenge.
POST/public/webhook-endpoints/:id/testwebhooks:manageSend a webhook.ping test delivery.
GET/public/webhook-endpoints/:id/deliverieswebhooks:readDelivery log. Metadata only, never payloads.
POST/public/webhook-endpoints/:id/deliveries/:deliveryId/redeliverwebhooks:manageRe-enqueue a past delivery within 30 days.
GET/public/eventswebhooks:readThe event-type catalog.
GET/public/usagewebhooks:readEndpoint count and deliveries used today against the cap.
GET/public/assets/:tokenwebhooks:readRedeem an asset link. 302-redirects to a short-lived presigned URL.

Create an 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"]
  }'

Response (secret returned once)

{
  "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"
}

Send a test 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_..."

Event envelope

Every delivery, for every event type, is the same JSON envelope. The type-specific body lives in 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": { }
}
FieldTypeDescription
idstringUnique event id, stable across every retry. Use it as your dedup key.
typestringThe event type, such as application.received.
apiVersion"v1"Payload contract version. Always v1 for now.
createdAtstring (ISO 8601)When CazVid emitted the event. Stable across retries.
occurredAtstring (ISO 8601)Business time the underlying event happened (the true appliedAt).
organizationIdstringYour organization id.
dataobjectThe type-specific body, documented per event below.

Events

Three event types ship in v1.

EventWhen
application.receivedA candidate completes an application to one of your jobs.
webhook.verifySent on create and re-verify. Echo the challenge to activate the endpoint.
webhook.pingSent by the test action. data.test is true; never affects suspension.

application.received

Sent when a candidate completes an application to one of your jobs, on both the desktop and mobile apply flows. candidate.email is the stable person key and application.id identifies the application - see IDs and assets for how to treat each field. screeningAnswers is empty when the job has no screening questions.

{
  "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

Sent when you create or re-verify an endpoint. Echo the exact data.challenge string in your 2xx response body to activate the endpoint. Until then, no real event is delivered.

{
  "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

Sent by the test action so you can confirm your receiver and signature checks end to end. data.test is always true and pings never change an endpoint's suspension state.

{
  "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
  }
}

Endpoint verification

An endpoint cannot receive real events until you prove you control its URL.

  1. 1.On create (and on each verify call) the endpoint is pending_verification and CazVid POSTs a webhook.verify event to the URL.
  2. 2.Your endpoint returns a 2xx response whose body echoes the exact data.challenge string.
  3. 3.On a correct echo the endpoint becomes active and starts receiving the events it subscribed to.
  4. 4.Re-verification is limited to 5 attempts per hour. Use the verify endpoint or the verify button in CazVid.

Verifying signatures

Every delivery is signed with the Standard Webhooks scheme. Verify each request before trusting it. Three headers accompany the POST:

webhook-id: evt_7f3a9c2e1b6d4f80a5c3e9d1f2b4a6c8
webhook-timestamp: 1783002727
webhook-signature: v1,g0X4Hq7m2p9sB1vZ8wYc3dE5fG6hJ0kL2mN4oP6qR8=

The signed content is {webhook-id}.{webhook-timestamp}.{body}, HMAC-SHA256 with your endpoint secret, base64-encoded, and prefixed with v1,. Verify the exact raw bytes you received - do not re-serialize the parsed JSON, or whitespace and key order will break the signature. During a rotation the header carries two space-delimited signatures; accept the request if any one matches. Reject a webhook-timestamp more than 5 minutes from your current time to bound replays.

Your secret looks like whsec_<base64>. Strip the whsec_ prefix and base64-decode the rest to get the raw signing key. The Standard Webhooks libraries do this for you when you pass the full whsec_... value.

Install the Standard Webhooks library with npm i standardwebhooks (Node) or pip install standardwebhooks (Python). The svix libraries are wire-compatible. 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)

Manual verification (no dependency)

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 and assets

The payload mixes several kinds of identifier. Treat them differently.

FieldKindNotes
application.idRaw, stableThe application id (qatId). Stable across retries and identical on desktop and mobile. Safe to store as your application key.
job.postIdRawThe platform post id, the same id the Job Builder API returns.
organizationIdRawYour organization id.
job.workspaceIdRawThe workspace the job belongs to.
candidate.publicIdOpaque tokenA pc1. handle, redeemable at the authenticated contact flow. NOT correlatable across events (random per payload), so never use it as a person key.
candidate.emailStable person keyUse this to match or merge a person across applications.
candidate.resumeUrlShort-lived linkA GET /public/assets/<token> redemption link. The token lasts about 30 days and 302-redirects to a fresh, roughly 1-hour presigned URL. Present only when the candidate supplied a resume.
candidate.videoUrlPublic CDNAn ImageKit URL, publicly fetchable. Present only when the candidate uploaded a video.
candidate.resumeUrl is an authenticated redemption endpoint, not a direct file link. Call GET /public/assets/<token> with a key that has webhooks:read and it returns a 302 to a fresh, roughly 1-hour presigned URL; the token is also bound to your organization. Fetch the redirect target promptly and do not store it; re-redeem the token (valid about 30 days) when you need the file again.

Delivery, retries, and suspension

A delivery succeeds when your endpoint returns any 2xx within a 10-second wall-clock bound (DNS, connect, TLS, and response). Process asynchronously and ACK fast. Response bodies are ignored except the verification challenge echo. Redirects count as a failure. Deliveries are HTTPS only, on port 443 or 8443, and CazVid will not deliver to private or internal addresses.

Failed attempts are retried on this schedule - 8 attempts over roughly 1.8 days, then the delivery is marked failed:

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
If an endpoint has been failing for at least 24 hours AND has at least 12 failed attempts, it is suspended and you get an in-app notification. A real delivered event clears the failing streak; ping successes do not. Re-enabling a suspended endpoint is an explicit action.
Missed events are not lost. Redeliver any delivery within its 30-day window from the delivery log or the redeliver endpoint; the original payload is replayed with the same id, which your consumer dedupes.

Limits and behavior

  • Up to 10 endpoints per organization.
  • Up to 5,000 deliveries per organization per day. Excess is parked as failed and can be redelivered the next day.
  • Test pings count toward the daily cap and are limited to 10 per day per endpoint.
  • Re-verification is limited to 5 attempts per hour per endpoint.
  • Delivery records are retained for 30 days.

Building more on CazVid? Browse the Developer API index for the Job Builder and Candidate Search APIs.