Skip to main content

Webhook Integration

Receive scoring events in your ATS the moment an invited candidate completes a challenge. TryCrucible signs every request so you can verify it came from us.

Quick start

  1. Go to Company settings → Webhooks and register your HTTPS endpoint.
  2. Copy the signing secret shown once on creation — store it in your environment.
  3. Add a route in your app that accepts POST requests.
  4. Verify the X-TryCrucible-Signature header on every request.
  5. Respond with 200 OK within 10 seconds.

Events

Currently one event type is delivered. More will be added in future releases.

EventWhen
invitation_scoredA candidate you invited has been scored. Fires only for company-invited candidates — not for candidates who picked challenges independently.

Payload

TryCrucible sends a JSON body with Content-Type: application/json.

{
  "event": "invitation_scored",
  "submission_id": "sub_01HZQ2V8KTXYR4BPNFWJM5D9C",
  "candidate_id": "usr_01HZQ2V6XMKEYJ7RSBGQTWZ3F",
  "challenge_id": "chl_01HZQ2V4PNFETG8SRDKWQXV7B",
  "score": 82,
  "scored_at": "2026-05-23T10:34:17.000Z"
}
FieldTypeDescription
eventstringAlways "invitation_scored" for this event type.
submission_idstringUnique ID of the submission.
candidate_idstringTryCrucible user ID of the candidate. Use this to correlate with a profile lookup.
challenge_idstringThe challenge that was completed.
scoreintegerOverall score, 0–100.
scored_atISO 8601UTC timestamp when scoring completed.

Signature verification

Every request includes an X-TryCrucible-Signature header containing sha256=<hex_digest>. The digest is the HMAC-SHA256 of the raw request body, keyed with your signing secret.

Always verify the signature before acting on the payload. Never skip this check.

TypeScript / Node.js

import crypto from "crypto";

export function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected),
  );
}

Python

import hashlib, hmac

def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature_header, expected)

Full Next.js example

// app/api/trycrucible/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyWebhookSignature } from "@/lib/trycrucible-webhook";

export const POST = async (req: NextRequest) => {
  const sig  = req.headers.get("x-trycrucible-signature") ?? "";
  const body = await req.text();

  if (!verifyWebhookSignature(body, sig, process.env.TRYCRUCIBLE_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(body);

  if (event.event === "invitation_scored") {
    // Update your ATS with event.score, event.candidate_id, etc.
    await updateCandidate(event.candidate_id, { score: event.score });
  }

  return NextResponse.json({ received: true });
};

Retries and timeouts

ParameterValue
Request timeout10 seconds
Success conditionHTTP 2xx within timeout
Retry schedule3 attempts — immediately, +1 min, +5 min
After 3 failuresEndpoint marked as errored; no further attempts for that event

Respond with 200 OK immediately and process the payload asynchronously to avoid timeout failures.

Security checklist

  • Use HTTPS only — TryCrucible rejects plain-HTTP endpoints.
  • Verify the signature on every request using timing-safe comparison.
  • Store your signing secret in an environment variable, never in source code.
  • Respond with 200 immediately; do your work asynchronously.
  • Treat the payload as untrusted until the signature is verified.