Webhooks
Receive real-time HTTP POST notifications when interviews are completed. Build powerful integrations and automate your hiring workflow.
Quick Start
Configure your webhook URL
Add a global URL in Settings → Global Interview Settings or set a per-interview override in the Settings tab.
Handle incoming requests
Your endpoint should return a 2xx status code within 10 seconds.
Events
interview.completedinterview.abandonedWebhooks are sent for all session types. Preview sessions include test: true so you can easily filter them: if (payload.test) return;
Payload
{
"event": "interview.completed",
"apiVersion": "2026-01-30",
"timestamp": "2024-12-27T00:25:50.431Z",
"data": {
"interview": {
"id": "c04f53d2",
"title": "Senior Software Engineer",
"companyName": "Acme Corp",
"dashboardUrl": "https://talentsprout.ai/dashboard/interviews/c04f53d2"
},
"candidate": {
"id": "6907d2602dab93276cdedb2a",
"firstName": "John",
"lastName": "Doe",
"fullName": "John Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"externalUserId": "your_user_12345",
"profileUrl": "https://talentsprout.ai/dashboard/interviews/c04f53d2/candidates/6907d2602dab93276cdedb2a",
"shareUrl": "https://talentsprout.ai/share/interviews/c04f53d2/candidates/6907d2602dab93276cdedb2a"
},
"session": {
"id": "694f268cd70bb7447a4e4d4e",
"token": "3f54f82d...",
"type": "candidate",
"status": "completed",
"startedAt": "2024-12-27T00:21:36.836Z",
"completedAt": "2024-12-27T00:25:46.490Z",
"durationMinutes": 4.16,
"recordingUrl": "https://talentsprout.ai/media/recording/3f54f82d...",
"thumbnailUrl": "https://talentsprout.ai/media/thumbnail/3f54f82d...",
"resumeUrl": null
},
"validation": {
"passed": true,
"reason": "completed",
"checkedAt": "2024-12-27T00:25:50.431Z"
},
"scores": {
"overall": 85,
"interview": 85,
"resume": null
},
"evaluation": {
"performanceSummary": "Strong candidate with excellent communication...",
"performance": {
"communicationSkills": { "name": "Communication Skills", "score": 88, "notes": "..." },
"domainExpertise": { "name": "Domain Expertise", "score": 85, "notes": "..." },
"problemSolving": { "name": "Problem Solving", "score": 82, "notes": "..." },
"culturalFit": { "name": "Cultural Fit", "score": 90, "notes": "..." },
"professionalism": { "name": "Professionalism", "score": 87, "notes": "..." }
},
"summary": {
"overview": "Candidate brings strong technical background...",
"background": ["5 years experience", "Previous tech lead role"],
"strengths": ["Strong communication", "Technical expertise"],
"concerns": ["Limited experience with specific stack"],
"tags": ["senior", "leadership"]
}
},
"resumeEvaluation": null,
"transcript": [
{ "timestamp": 1735263696836, "isAgent": true, "text": "Hello! Welcome..." },
{ "timestamp": 1735263702000, "isAgent": false, "text": "Thank you..." }
]
}
}Field Reference
| Field | Description |
|---|---|
event | Event type: interview.completed or interview.abandoned |
apiVersion | API version |
timestamp | ISO 8601 timestamp |
test | Present (true) for preview sessions; omitted for production |
data.interview | Interview details & dashboard URL |
data.candidate | Candidate info, profile URL & share URL |
data.candidate.externalUserId | Your external user ID (from Smart Links) |
data.candidate.shareUrl | Public shareable link (no auth required) |
data.session | Session timing, recording & thumbnail URLs |
data.session.id | Unique session identifier |
data.session.token | Session token (truncated in examples) |
data.session.type | Session type: candidate or preview |
data.session.status | Session status: completed or abandoned |
data.session.startedAt | ISO 8601 timestamp when interview started |
data.session.completedAt | ISO 8601 timestamp when interview ended |
data.session.durationMinutes | Interview duration in minutes |
data.session.recordingUrl | Interview recording (null for preview sessions) |
data.session.thumbnailUrl | Video thumbnail (null for preview sessions) |
data.session.resumeUrl | Uploaded resume URL (when provided) |
data.validation | Completion validation details |
data.validation.passed | Whether interview completed successfully |
data.validation.reason | Reason: completed, too_short, no_evaluation, etc. |
data.validation.checkedAt | ISO 8601 timestamp of validation check |
data.scores | Overall, interview & resume scores |
data.evaluation | Performance metrics & summaries |
data.resumeEvaluation | Resume evaluation (when resume uploaded) |
data.transcript | Full conversation transcript |
data.transcript[].timestamp | Unix timestamp in milliseconds |
data.transcript[].isAgent | true = AI interviewer, false = candidate |
data.transcript[].text | Message content |
Note: Optional fields like resumeEvaluation, language, and customScores are included only when applicable.
Request Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | TalentSprout-Webhooks/1.0 |
X-TalentSprout-Event | interview.completed or interview.abandoned |
X-TalentSprout-Signature | t=1672531200,v1=... (when enabled) |
Security
Optionally verify that webhooks are sent from TalentSprout using HMAC-SHA256 signatures. Enable webhook authentication in Settings → Global Interview Settings. When disabled, webhooks are still sent but without the signature header.
Getting Your Signing Secret
- • Your secret (starting with
whsec_) is shown once when you enable authentication - • Copy it immediately and store it securely (e.g., environment variable)
- • If lost, regenerate a new one — this invalidates the previous secret
Signature Format
When enabled, webhooks include the X-TalentSprout-Signature header:
t— Unix timestamp when the webhook was generatedv1— HMAC-SHA256 signature of{timestamp}.{payload}
Verification Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, header, secret) {
const parts = header.split(',');
const timestamp = parts[0].split('=')[1];
const signature = parts[1].split('=')[1];
// Reject timestamps older than 5 minutes (replay protection)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
throw new Error('Timestamp too old');
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures (timing-safe)
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid signature');
}
return true;
}
// Usage in Express
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-talentsprout-signature'];
if (signature) {
try {
verifyWebhookSignature(
req.body.toString(),
signature,
process.env.TALENTSPROUT_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(err.message);
}
}
// Process the webhook
const event = JSON.parse(req.body);
console.log('Received:', event.event);
res.status(200).send('OK');
});Important: You must use the raw request body for signature verification. Parsing the JSON before verification will change the payload and cause signature mismatch.
Best Practices
Respond quickly
Return a 2xx status code within 10 seconds
Process async
Handle heavy operations asynchronously
Deduplicate
Use session.id to handle potential retries
Use HTTPS
Only secure endpoints are supported
Testing
Use the Send Test button in Settings → Global Interview Settings to verify your endpoint is reachable. Test events include test: true in the payload so you can identify and filter them.
To inspect the full payload structure, use webhook.site to generate a temporary endpoint.
Preview mode interviews trigger webhooks with test: true. Use these to test your integration without affecting billing. Filter production events with if (payload.test) return;
Note: Preview mode interviews are not recorded. recordingUrl and thumbnailUrl will be null for these sessions.
Need help with your integration?
Our team is here to help you get set up.