Webhooks
Subscribe a URL to domain events. Growth.Talent POSTs JSON-encoded events to your endpoint with an HMAC-SHA256 signature. Replaces polling for ATSes, sourcing bots, lifecycle systems, and any agent that needs to react in real time.
Available events
| Event | When it fires | Visible to |
|---|---|---|
application.received | A candidate submitted an application to one of your company's jobs. | Company |
application.created | You (the candidate) submitted an application — useful for personal pipeline trackers. | Candidate |
job.approved | Your job posting passed AI moderation and is live. | Company |
job.expired | Your job posting was closed or hit its expiry date. | Company |
candidate.verified | Admin verified your candidate profile. | Candidate |
boost.purchased | Your company purchased a Boost (featured placement). | Company |
Subscribe
Mint an API key with the webhooks:write scope at /settings/api-keys, then:
curl -X POST https://www.growthtalent.org/api/v1/webhooks \
-H "Authorization: Bearer gt_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/growthtalent",
"events": ["application.received", "job.approved"],
"description": "ATS prod"
}'The response includes a secret — store it. You won't see it again.
https. Keep the secret out of logs and source control.Verify signatures
Every delivery sets X-GrowthTalent-Signature and X-GrowthTalent-Timestamp headers. The signature has the form t=<timestamp>,v1=<hex hmac> where the HMAC-SHA256 covers `${timestamp}.${rawBody}`.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(
rawBody: string,
header: string,
secret: string,
toleranceSec = 300,
): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (!parts.t || !parts.v1) return false;
const ts = parseInt(parts.t, 10);
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSec) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}import hmac, hashlib, time
def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:
parts = dict(p.split("=") for p in header.split(","))
if "t" not in parts or "v1" not in parts:
return False
ts = int(parts["t"])
if abs(int(time.time()) - ts) > tolerance_sec:
return False
expected = hmac.new(
secret.encode(),
f"{ts}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Retry policy
Non-2xx responses (or timeouts > 10s) get retried with exponential backoff: 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours. After 6 failed attempts the delivery is marked DEAD and dropped. The subscription stays active.
Inspect recent deliveries:
curl https://www.growthtalent.org/api/v1/webhooks/<id>/deliveries \
-H "Authorization: Bearer gt_live_..."Manage subscriptions
# List your subscriptions
curl https://www.growthtalent.org/api/v1/webhooks \
-H "Authorization: Bearer gt_live_..."
# Revoke
curl -X DELETE https://www.growthtalent.org/api/v1/webhooks/<id> \
-H "Authorization: Bearer gt_live_..."Example payload
{
"event": "application.received",
"eventId": "a3f1c8d29e6b1042",
"application": {
"id": "clw3xyz...",
"appliedVia": "magic",
"message": "Hi — built growth at Spendesk #4 to unicorn.",
"hadMessage": true,
"source": "api",
"createdAt": "2026-05-06T22:13:39.123Z"
},
"job": {
"id": "clw...",
"slug": "head-of-growth-the-mobile-first-company",
"title": "Head of Growth",
"category": "growth-marketing",
"market": "USA"
},
"candidate": {
"id": "clw...",
"slug": "jane-doe",
"name": "Jane Doe",
"currentTitle": "Growth Lead",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe"
},
"company": {
"id": "clw...",
"slug": "the-mobile-first-company",
"name": "The Mobile First Company"
}
}