Webhooks
BetaReceive signed `result.completed` events from the NTRVSTA External Integrations API.
Use webhooks to receive completed interview results automatically instead of polling the API.
At a glance
Receive completed interview results automatically
Webhooks are the best fit when another system needs to react as soon as an
interview result is ready. Build your endpoint around signature
verification, idempotency, and a fast 2xx response.
At A Glance
NTRVSTA currently sends one partner-facing webhook event:
result.completed
This event is delivered when a completed final interview result becomes available for an organization with webhook delivery enabled.
Incomplete interview results do not emit result.completed.
Use it when you want to:
- update your ATS or recruiting system as soon as an interview is completed
- ingest result metadata without polling
GET /v1/interview-results - trigger downstream review, scoring, or workflow automation
Before you start
Webhook delivery is configured at the organization level in the NTRVSTA product:
- Go to
Settings -> Integrations - Set one HTTPS webhook endpoint URL for the organization
- Save or rotate the signing secret from the same screen
Important constraints:
- Only one outbound webhook endpoint is configured per organization
- The event list is fixed today and currently contains only
result.completed - The callback URL must be a public HTTPS endpoint
- Private, link-local, and other non-public destinations are rejected during configuration and again before delivery
What you should build
Your webhook endpoint should do four things:
- accept
POSTrequests with a raw JSON body - verify the
X-Ntrvsta-Signatureheader before trusting the payload - de-duplicate on
resultIdorX-Ntrvsta-Idempotency-Key - return
2xxquickly and process downstream work asynchronously
If you only implement one safety check, make it signature verification.
Delivery headers
Each delivery includes these headers:
Content-Type: application/json
X-Ntrvsta-Event: result.completed
X-Ntrvsta-Event-Id: external_result_completed_<resultId>
X-Ntrvsta-Idempotency-Key: <resultId>
X-Ntrvsta-Signature: t=<unixTimestamp>,v1=<hexDigest>Header semantics:
X-Ntrvsta-Eventis the event typeX-Ntrvsta-Event-Idis the stable event id for that resultX-Ntrvsta-Idempotency-Keyis the stableresultIdX-Ntrvsta-Signaturecontains the timestamp and HMAC digest
Verify the signature
Webhook deliveries are signed with HMAC SHA-256 using your organization’s webhook signing secret.
The digest is computed over:
<timestamp>.<raw_json_body>Express example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use('/webhooks/ntrvsta', express.raw({ type: 'application/json' }));
app.post('/webhooks/ntrvsta', (req, res) => {
const signatureHeader = req.headers['x-ntrvsta-signature'];
if (!verifySignature(req.body, signatureHeader, process.env.NTRVSTA_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
if (event.eventType === 'result.completed') {
handleResultCompleted(event);
}
return res.status(200).send('OK');
});
function verifySignature(rawBody, header, secret) {
if (!header || typeof header !== 'string' || !secret) return false;
const parts = Object.fromEntries(
header.split(',').map((part) => {
const [key, value] = part.split('=');
return [key, value];
})
);
const timestamp = parts.t;
const receivedDigest = parts.v1;
if (!timestamp || !receivedDigest) return false;
const timestampMs = Number(timestamp) * 1000;
if (!Number.isFinite(timestampMs)) return false;
if (Math.abs(Date.now() - timestampMs) > 5 * 60 * 1000) return false;
const expectedDigest = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody.toString('utf8')}`)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(receivedDigest, 'hex'),
Buffer.from(expectedDigest, 'hex')
);
} catch {
return false;
}
}Event payload
result.completed deliveries use this payload shape:
{
"eventId": "external_result_completed_result_123",
"eventType": "result.completed",
"occurredAt": "2026-05-11T12:00:00.000Z",
"idempotencyKey": "result_123",
"resultId": "result_123",
"template": {
"id": "organizationInterviewId",
"name": "Senior Backend Engineer"
},
"candidate": {
"id": "candidateUserId",
"email": "candidate@example.com",
"firstName": "Ada",
"lastName": "Lovelace"
},
"interview": {
"id": "userInterviewId",
"completedAt": "2026-05-11T12:00:00.000Z",
"status": "COMPLETED"
},
"resultUrl": "https://app.ntrvsta.com/shared/interview-details/<token>",
"result": {
"resultId": "result_123",
"aiAnalysis": {
"scoring": {
"technical_score": 84,
"technical_feedback": "Strong backend fundamentals with solid tradeoff reasoning.",
"soft_skills_score": 81,
"soft_skills_feedback": "Communicates clearly and structures answers well.",
"language_proficiency": 90,
"language_proficiency_feedback": "Fluent and easy to follow throughout the interview.",
"weighted_score": 83.1,
"feedback": "Strong overall performance with a few areas to deepen.",
"highlights": [
"Clear system-design explanations",
"Strong debugging mindset"
],
"suggestions": [
"Go deeper on scaling tradeoffs",
"Add more concrete production examples"
],
"duration": 1420
}
}
}
}The partner payload intentionally excludes internal-only analysis fields such as transcripts, chapter breakdowns, whiteboard analysis, translation artifacts, and debug-only scoring data.
Key fields
The fields most integrations care about first are:
eventTypeConfirms the event type. Today this is alwaysresult.completed.resultIdStable id for the completed result and the best idempotency key for your side.template.idThe template or assessment identifier associated with the result.candidate.emailThe most common field for mapping the result back to your recruiting system.interview.completedAtTimestamp included in the webhook payload for the completed interview event.resultUrlLink to the shared result view in NTRVSTA.result.aiAnalysis.scoringPublic scoring summary for downstream consumption.
Delivery semantics
NTRVSTA treats resultId as the webhook idempotency key.
Implications:
- repeated deliveries for the same completed result use the same
resultId - a delivery that was already marked as delivered is not emitted again
- consumers should de-duplicate on
resultIdorX-Ntrvsta-Idempotency-Key
When your endpoint is unavailable or returns a non-2xx response:
- NTRVSTA attempts delivery up to 3 times total
- Between retry attempts, NTRVSTA waits 400 ms before the second attempt and 800 ms before the third attempt
- retry attempts happen synchronously within a single delivery invocation
- recent delivery status and health are visible in
Settings -> Integrations
Recommended handler flow
Your webhook endpoint should:
- return
2xxafter successful verification and processing - persist or enqueue work quickly
- avoid long synchronous processing before responding
Recommended pattern:
- verify the signature
- persist or enqueue the payload
- return
200 - process downstream work asynchronously
Webhooks vs Polling
Use webhooks when you want near-real-time result ingestion.
Use GET /v1/interview-results when:
- you need a pull-based recovery path
- you want to reconcile a missed webhook
- your system processes results on a schedule instead of immediately
The two surfaces return different payload shapes. Webhook deliveries include the full event payload above — candidate identity, template, interview timing, resultUrl, and the aiAnalysis.scoring block. GET /v1/interview-results returns only a minimal summary (resultUrl and overallScore), so plan to use webhooks as the primary path for downstream scoring and feedback.