Overview
Webhooks allow you to receive real-time notifications when events occur in the Gather API. This guide walks you through setting up and handling webhooks.
Setting Up Webhooks
Webhooks are configured per organization/team via Eucalyptus (internal admin tool). Contact your Qualifi administrator to set up webhooks.
Configuration Requirements
- Webhook URL: Your endpoint URL that will receive webhook events
- Event Types: Which events you want to receive
- Webhook Secret: Secret key for signature verification
Webhook Endpoint Setup
Your webhook endpoint should:
- Accept POST requests
- Respond quickly (within 5 seconds)
- Return appropriate HTTP status codes
- Verify webhook signatures
Basic Endpoint Structure
JavaScript (Express)
Python (Flask)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(payload, signature) {
const [timestamp, sig] = signature.split(',');
const timestampValue = timestamp.split('=')[1];
const signatureValue = sig.split('=')[1];
const payloadString = `${timestampValue}.${JSON.stringify(payload)}`;
const computedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payloadString)
.digest('hex');
return computedSignature === signatureValue;
}
app.post('/webhooks/qualifi', (req, res) => {
const signature = req.headers['x-qualifi-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
const { event, data, timestamp } = req.body;
// Handle the event
switch (event) {
case 'candidate_interview.completed':
handleInterviewCompleted(data);
break;
case 'question.audio_generated':
handleAudioGenerated(data);
break;
// ... other events
}
res.status(200).send('OK');
});
function handleInterviewCompleted(data) {
console.log('Interview completed:', data.candidateInterviewId);
// Process the completed interview
}
app.listen(3000);
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
def verify_signature(payload, signature):
timestamp, sig = signature.split(',')
timestamp_value = timestamp.split('=')[1]
signature_value = sig.split('=')[1]
payload_string = f"{timestamp_value}.{json.dumps(payload)}"
computed_signature = hmac.new(
WEBHOOK_SECRET.encode(),
payload_string.encode(),
hashlib.sha256
).hexdigest()
return computed_signature == signature_value
@app.route('/webhooks/qualifi', methods=['POST'])
def webhook():
signature = request.headers.get('X-Qualifi-Signature')
if not verify_signature(request.json, signature):
return jsonify({'error': 'Invalid signature'}), 401
event = request.json.get('event')
data = request.json.get('data')
if event == 'candidate_interview.completed':
handle_interview_completed(data)
elif event == 'question.audio_generated':
handle_audio_generated(data)
return jsonify({'status': 'OK'}), 200
def handle_interview_completed(data):
print(f"Interview completed: {data['candidateInterviewId']}")
# Process the completed interview
if __name__ == '__main__':
app.run(port=3000)
Handling Events
Interview Completed
When a candidate completes an interview:
{
"event": "candidate_interview.completed",
"timestamp": "2024-01-01T00:00:00Z",
"data": {
"candidateInterviewId": "uuid",
"candidateId": "uuid",
"interviewId": "uuid",
"status": "new_response",
"takenAt": "2024-01-01T00:00:00Z"
}
}
Action: Fetch interview results and process them:
async function handleInterviewCompleted(data) {
const results = await fetch(
`https://api.prod.qualifi.hr/qsi/gather/candidate-interviews/${data.candidateInterviewId}/results`,
{
headers: {
'x-api-key': apiKey
}
}
);
const interviewResults = await results.json();
// Process results: save to database, send notifications, etc.
}
Audio Generated
When question audio generation completes:
{
"event": "question.audio_generated",
"timestamp": "2024-01-01T00:00:00Z",
"data": {
"questionId": "uuid",
"audioUrl": "https://...",
"status": "completed"
}
}
Action: Update your system with the audio URL.
Status Changed
When candidate interview status changes:
{
"event": "candidate_interview.status_changed",
"timestamp": "2024-01-01T00:00:00Z",
"data": {
"candidateInterviewId": "uuid",
"oldStatus": "invite_sent",
"newStatus": "new_response"
}
}
Action: Update your system’s status tracking.
Best Practices
- Verify Signatures: Always verify webhook signatures to ensure authenticity
- Idempotency: Handle duplicate webhook deliveries gracefully using event IDs or timestamps
- Quick Response: Respond to webhooks quickly (within 5 seconds) to avoid retries
- Error Handling: Return appropriate HTTP status codes (200 for success, 4xx/5xx for errors)
- Logging: Log all webhook events for debugging and auditing
- Async Processing: Process webhook data asynchronously if operations take time
Retry Logic
The Gather API retries failed webhook deliveries:
- First attempt: Immediate
- Second attempt: After 1 minute
- Third attempt: After 5 minutes
If all retry attempts fail, the webhook delivery is marked as failed. Ensure your endpoint is available and handles errors gracefully.
Testing Webhooks
Use a tool like ngrok to test webhooks locally:
# Start your local server
npm start
# In another terminal, expose it via ngrok
ngrok http 3000
# Use the ngrok URL as your webhook URL in Eucalyptus