Checking...
3.1.0
Eval CV Docs
Endpoint Reference

Batch Completion Webhook

When a batch you started reaches a terminal state, we send a single signed batch.completed event to a URL you control via HTTP POST — no polling required. Configure it in Developers → Webhooks(it's optional).
POSThttps://your-server.com/webhooks/evalcv

Request Parameters

NameTypeDescription
eventRequired
stringAlways "batch.completed".
batch_idRequired
stringThe batch that finished.
job_idRequired
stringThe job the batch belongs to.
statusRequired
stringOne of completed, failed, or partial.
total_candidatesRequired
integerCandidates in the batch.
successRequired
integerSuccessfully scored candidates.
failedRequired
integerCandidates that failed scoring.
resumes_scoredRequired
integerResumes scored (equals success).
timestampRequired
stringISO-8601 delivery time.

The three status levels

The payload is the same shape every time — only the status field and the success / failed counts change. Branch on status for a quick decision, and read success, failed and total_candidates for the exact breakdown.

completedsuccess === total_candidates · failed === 0

Every candidate in the batch was scored successfully.

Next: Fetch the full ranking — GET /v1/batch/{batch_id}/rank.

partialfailed > 0 · the rest are in success

The batch finished, but one or more candidates could not be scored (e.g. an unreadable resume). This is also what you receive when every candidate failed but the batch ran to the end.

Next: Read the scored candidates from /rank; use success / failed to see how many were dropped.

failedsuccess === 0

The batch produced no scores at all — it timed out before a single candidate reached a terminal state.

Next: Nothing to rank — check your inputs (resume URLs / parsing) and re-submit the batch.

Verify the signature

Recompute the HMAC-SHA256 of the raw request body with your signing secret (your EVAL_WEBHOOK_SECRET) and compare it (constant-time) to the X-EVAL-Signature header.

verify.js
const crypto = require("crypto");

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-eval-signature"]; // "sha256=<hex>"
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", process.env.EVAL_WEBHOOK_SECRET)
          .update(req.body)            // raw body bytes
          .digest("hex");

  if (signature !== expected) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // handle event.batch_id, event.status …
  res.sendStatus(200);
});
verify.py
import hmac, hashlib, json
from fastapi import Request, HTTPException

@app.post("/webhook")
async def webhook(request: Request):
    raw = await request.body()
    expected = "sha256=" + hmac.new(
        SECRET.encode(), raw, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(request.headers.get("X-EVAL-Signature", ""), expected):
        raise HTTPException(401)
    event = json.loads(raw)
    return {"ok": True}

Delivery & retries

  • Respond with any 2xx status to acknowledge. Non-2xx (or a timeout) is retried a few times with backoff.
  • Each request times out after ~10 seconds — return quickly and process asynchronously.
  • Delivery is not guaranteed exactly-once; de-duplicate on batch_id if needed.
  • If the secret leaks, regenerate it in Developers → Webhooks — the old one stops working immediately.

Pro Tip

Every delivery is signed with X-EVAL-Signature: sha256=… — an HMAC of the raw body using your signing secret. Verify it before trusting the event (see the snippets below).

Payload
{
  "event": "batch.completed",
  "batch_id": "b_3f9c…",
  "job_id": "j_205c…",
  "status": "completed",
  "total_candidates": 6,
  "success": 6,
  "failed": 0,
  "resumes_scored": 6,
  "timestamp": "2026-06-25T00:00:00Z"
}