Documentation Index Fetch the complete documentation index at: https://docs.humanly.io/llms.txt
Use this file to discover all available pages before exploring further.
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
Webhooks API Complete webhook API reference
Error Handling Learn about error handling best practices
Best Practices Review API best practices