Skip to main content

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 the Gather API’s webhook configuration endpoints (/qsi/gather/webhooks — create, list, get, update, delete). See the Webhooks API reference for events and delivery details.

Configuration Requirements

  1. Webhook URL: Your endpoint URL that will receive webhook events
  2. Event Types: Which events you want to receive
  3. Webhook Secret: Secret key for signature verification

Webhook Endpoint Setup

Your webhook endpoint should:
  1. Accept POST requests
  2. Respond quickly (within 5 seconds)
  3. Return appropriate HTTP status codes
  4. Verify webhook signatures

Basic Endpoint Structure

const express = require('express');
const crypto = require('crypto');
const app = express();

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Use raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf;
  }
}));

function verifySignature({ rawBody, signature }) {
  const computedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(computedSignature),
    Buffer.from(signature)
  );
}

app.post('/webhooks/qualifi', (req, res) => {
  const signature = req.headers['x-qualifi-signature'];
  const event = req.headers['x-qualifi-event'];
  
  if (!verifySignature({ rawBody: req.rawBody, signature })) {
    return res.status(401).send('Invalid signature');
  }
  
  const payload = req.body;
  
  switch (event) {
    case 'candidate_interview.completed':
      handleInterviewCompleted(payload);
      break;
    case 'candidate_interview.status_changed':
      handleStatusChanged(payload);
      break;
    case 'question.audio_generated':
      handleAudioGenerated(payload);
      break;
    // ... other events
  }
  
  res.status(200).send('OK');
});

function handleInterviewCompleted(payload) {
  console.log('Interview completed:', payload.candidateInterviewId);
  // Process the completed interview
}

function handleStatusChanged(payload) {
  console.log('Status changed:', payload.candidateInterviewId, payload.status);
}

app.listen(3000);

Handling Events

Interview Completed

When a candidate completes an interview, the request includes X-Qualifi-Event: candidate_interview.completed and a flat JSON body:
{
  "candidateInterviewId": "uuid",
  "candidateId": "uuid",
  "interviewId": "uuid",
  "completedAt": "2024-01-01T00:00:00.000Z"
}
Action: Fetch interview results and process them:
async function handleInterviewCompleted(payload) {
  const results = await fetch(
    `https://api.prod.qualifi.hr/qsi/gather/candidate-interviews/${payload.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 (X-Qualifi-Event: question.audio_generated):
{
  "aiAudioVersionId": "uuid",
  "batchId": "uuid",
  "narratorId": "uuid",
  "audioUrl": "https://...",
  "text": "Question text",
  "createdAt": "2024-01-01T00:00:00.000Z"
}
Action: Update your system with the audio URL.

Status Changed

When candidate interview status changes (X-Qualifi-Event: candidate_interview.status_changed):
{
  "candidateInterviewId": "uuid",
  "candidateId": "uuid",
  "interviewId": "uuid",
  "status": "new_response",
  "previousStatus": null,
  "updatedAt": "2024-01-01T00:00:00.000Z"
}
Action: Update your system’s status tracking. Use status for the new value; previousStatus may be null if the prior status was not captured.
When status changes to new_response, you may receive both candidate_interview.status_changed and candidate_interview.completed webhooks.

Best Practices

  1. Verify Signatures: Always verify webhook signatures to ensure authenticity
  2. Idempotency: Handle duplicate webhook deliveries gracefully using event IDs or timestamps
  3. Quick Response: Respond to webhooks quickly (within 5 seconds) to avoid retries
  4. Error Handling: Return appropriate HTTP status codes (200 for success, 4xx/5xx for errors)
  5. Logging: Log all webhook events for debugging and auditing
  6. Async Processing: Process webhook data asynchronously if operations take time

Retry Logic

The Gather API retries failed webhook deliveries:
  1. First attempt: Immediate
  2. Second attempt: After 1 minute
  3. 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 the webhook URL in your webhook configuration

Webhooks API

Complete webhook API reference

Error Handling

Learn about error handling best practices

Best Practices

Review API best practices