Quick start
- Go to Company settings → Webhooks and register your HTTPS endpoint.
- Copy the signing secret shown once on creation — store it in your environment.
- Add a route in your app that accepts
POSTrequests. - Verify the
X-TryCrucible-Signatureheader on every request. - Respond with
200 OKwithin 10 seconds.
Events
Currently one event type is delivered. More will be added in future releases.
| Event | When |
|---|---|
| invitation_scored | A 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"
}| Field | Type | Description |
|---|---|---|
| event | string | Always "invitation_scored" for this event type. |
| submission_id | string | Unique ID of the submission. |
| candidate_id | string | TryCrucible user ID of the candidate. Use this to correlate with a profile lookup. |
| challenge_id | string | The challenge that was completed. |
| score | integer | Overall score, 0–100. |
| scored_at | ISO 8601 | UTC 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
| Parameter | Value |
|---|---|
| Request timeout | 10 seconds |
| Success condition | HTTP 2xx within timeout |
| Retry schedule | 3 attempts — immediately, +1 min, +5 min |
| After 3 failures | Endpoint 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.