Webhooks

This page will help you get started with Classet webhooks. You can modify your webhooks in our dashboard once you have been given access to integrate with us.

Webhook URL Management

You can add, update, and manage your webhook URLs directly through your Classet Developer Settings page:

Manage Webhooks →

From this page, you can:

  • Register new webhook URLs
  • Edit or delete existing webhook URLs
  • View recent webhook delivery logs and retry attempts

Verifying Webhook Requests

Classet supports two methods for verifying webhook requests:

  1. Webhook Signature (Recommended) - Uses HMAC-SHA256 signatures for secure verification
  2. API Key (Legacy) - Validates using your organization's API key

Security Note: Always validate webhook requests on your server-side code. Never trust client-side validation alone.


Method 1: Webhook Signature Verification (Recommended)

Each webhook request includes an x-classet-webhook-signature header containing an HMAC-SHA256 signature of the request body.

Step 1: Generate a Webhook Signature

  1. Navigate to your Developer Settings page in the Classet dashboard
  2. Scroll to the Interview Events Webhook section
  3. Click Generate Signature to create a new webhook signature secret
  4. Important: Copy and store the signature securely - you won't be able to view it again after closing the dialog

Step 2: Verify the Signature

When you receive a webhook request:

  1. Extract the signature from the x-classet-webhook-signature header
  2. Compute the HMAC-SHA256 signature of the raw request body using your stored secret
  3. Compare the computed signature with the header value
  4. Reject requests where signatures don't match

Example verification (Node.js/Express):

const crypto = require('crypto');

function verifyWebhookSignature(req, webhookSecret) {
  const signature = req.headers['x-classet-webhook-signature'];
  
  if (!signature) {
    return false;
  }

  // Get the raw request body as a string
  const payload = JSON.stringify(req.body);
  
  // Compute the expected signature
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload, 'utf8')
    .digest('base64url');
  
  // Use constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Usage in your webhook handler
app.post('/webhooks/classet', express.json(), (req, res) => {
  const webhookSecret = process.env.CLASSET_WEBHOOK_SECRET;
  
  if (!verifyWebhookSignature(req, webhookSecret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process the webhook...
  res.status(200).json({ received: true });
});

Important Notes:

  • The signature is computed from the exact JSON string of the request body
  • Use constant-time comparison functions to prevent timing attacks
  • The signature uses base64url encoding (URL-safe base64)
  • Store your webhook secret securely (environment variables, secrets manager, etc.)

Method 2: API Key Verification (Legacy)

If you haven't configured a webhook signature, Classet will send your API key in the x-classet-key header as a fallback.

Example validation:

const apiKey = request.headers['x-classet-key'];

if (!apiKey || !isValidApiKey(apiKey)) {
  return response.status(401).json({ error: 'Unauthorized' });
}

Note: We recommend migrating to webhook signature verification for improved security. API key verification will continue to be supported for backward compatibility.


Interview Events

The Interview Events webhook triggers when an interview has been updated.

Note: This is the new standard going forward. It uses a lightweight, flexible diff model for changes.

Available Interview Events

  • candidate.interview.updated: Fires when an interview's details are updated.


Webhook Response Body

When an candidate.interview.updated event is triggered, the webhook sends a POST request to your configured endpoint with the following JSON payload.


Response Format

FieldTypeDescription
eventstringThe event type (candidate.interview.updated).
changesobjectKey-value pairs showing what fields were updated, with from and to values.
dataobjectThe updated interview object, including interview completion, outcome, timestamps, and any custom data.

Fields

  • event (string): Always candidate.interview.updated.
  • changes (object): A map of changed fields. Each field contains:
    • from: Previous value.
    • to: Updated value.
  • data (object): The updated interview object fields:
    • interview_completed_at (string | null): ISO8601 timestamp of when the interview was completed. null if not yet completed.
    • interview_sent_at (string | null): ISO8601 timestamp of when the interview was originally sent to the candidate.
    • interview_outcome (string | null): The final outcome of the interview. Supported values:
      • candidate_not_interested
      • ai_opt_out
      • qualified
      • unqualified
      • candidate_not_responsive
      • completed
      • not_sent_unreachable
      • not_sent_duplicate
      • not_sent_already_completed
      • ai_stopped_override
      • null (not yet completed or no outcome determined)
    • interview_summary (string | null): The customizable text summary of the interview, generated by the system or interviewer. null if not available.
    • interview_custom_data (object | null): Any additional custom metadata collected during the interview, structured as a JSON object



Example 200 Response

{
  "event": "candidate.interview.updated",
  "changes": {
    "interview_completed_at": {
      "from": null,
      "to": "2025-04-15T16:00:00Z"
    },
    "interview_outcome": {
      "from": null,
      "to": "completed"
    }
  },
  "data": {
    "candidate_id": "cand_12345678",
    "interview_completed_at": "2025-04-15T16:00:00Z",
    "interview_sent_at": "2025-04-14T12:00:00Z",
    "interview_outcome": "completed",
    "interview_summary": "Example of customizable interview summary text",
    "interview_custom_data": {
      "can_lift_50lbs": true,
      "eligible_to_work": true
    }
  }
}

Delivery and Retries

When a webhook event is triggered, Classet will send a POST request to your configured webhook URL.

You must return a 2xx HTTP status code (e.g., 200 OK) to acknowledge successful receipt.

If your endpoint fails to acknowledge the webhook (due to network errors, timeouts, or non-2xx responses), we will automatically retry the delivery.

Retry Behavior

  • We retry up to 3 times after the initial attempt.
  • Retries use exponential backoff to avoid overwhelming your server.
  • After the 3rd retry fails, the webhook event will be marked as undeliverable and no further attempts will be made.

Important: Always return a 2xx status code as quickly as possible. If processing is slow, consider responding immediately and handling the payload asynchronously.


Custom Variables and Data Extraction

Not seeing data in the response that you will like to be able to retrieve? Let us know!

We have the ability to add custom variables and to extract + format additional information from the call. If you need additional details included in the response that are not currently listed above, reach out to [email protected].




Call Completed (Deprecated)

Deprecated Notice: The call_completed webhook is being deprecated in favor of candidate.interview.updated under Interview Events. Please migrate to using candidate.interview.updated for all future integrations.




The Call Completed webhook triggers when a call (interview) has been completed by a candidate.

This event sends detailed call information, including transcript, call analysis, and interview metadata.


Webhook Response Body

When a call is completed, the webhook sends a POST request to your configured endpoint with the following JSON payload.



Response Format

FieldTypeDescription
candidate_idstringUnique identifier for the candidate.
candidate_phonestringCandidate's phone number.
job_idstringUnique identifier for the associated job.
job_titlestringTitle of the associated job.
call_statusstringStatus of the completed call.
transcriptarrayTranscript of the conversation.
questionsarrayStructured questions and candidate answers collected during the call.
call_detailsobjectMetadata about the call session (duration, analysis, timestamps).
call_idstringUnique identifier for the call session.
recording_urlstringURL to access the call recording.


Fields Inside call_details

  • duration (number): Duration of the call in seconds.
  • start_time (number): Start timestamp of the call (Unix time).
  • end_time (number): End timestamp of the call (Unix time).
  • e2e_latency (number): End-to-end latency of the call in milliseconds.
  • call_analysis (object): AI-based analysis of the call:
    • call_summary (string): Natural language summary of the call.
    • user_sentiment (string): Sentiment of the candidate (Positive, Neutral, Negative).
    • agent_sentiment (string): Sentiment of the voice agent.
    • call_completion_rating (string): Call completion quality rating.
    • agent_task_completion_rating (string): Rating for the agent's task performance.
    • call_completion_rating_reason (string): Reason for call completion rating.
    • agent_task_completion_rating_reason (string): Reason for agent task rating.
  • org_phone_number (string): Phone number used by your organization during the call.


Example 200 Response

{
  "candidate_id": "12345",
  "candidate_phone": "+1234567890",
  "job_id": "67890",
  "job_title": "CDL Truck Driver",
  "call_status": "call_completed",
  "transcript": [
    {
      "role": "user",
      "words": [
        {
          "start": 0,
          "end": 0.5,
          "word": "Hello?"
        }
      ],
      "content": "Hello?"
    }
  ],
  "questions": [
    {
      "question": "Are you older than the age of 18?",
      "answer": "Yes, I am"
    }
  ],
  "call_details": {
    "duration": 300,
    "start_time": 1737073800,
    "end_time": 1737074100,
    "e2e_latency": 120,
    "call_analysis": {
      "call_summary": "The AI voice bot interviewed John for the Warehouse Associate role, providing job details and confirming availability. The interview concluded successfully.",
      "user_sentiment": "Positive",
      "agent_sentiment": "Positive",
      "call_completion_rating": "Complete",
      "agent_task_completion_rating": "Complete",
      "call_completion_rating_reason": "The call reached a natural conclusion and all necessary information was gathered.",
      "agent_task_completion_rating_reason": "The agent successfully gathered information about John's background and provided him with the necessary job details."
    },
    "org_phone_number": "+1234567890"
  },
  "call_id": "abcdef123456",
  "recording_url": "https://example.com/recordings/abcdef123456.mp3"
}