Skip to main content

Specifying a webhook URL

When creating an interview flow you can specify a webhook_url. This can be used to receive updates related to interviews.
curl --request POST \
     --url https://app.ribbon.ai/be-api/v1/interview-flows \
     --header 'accept: application/json' \
     --header 'authorization: Bearer <your-api-key-here>' \
     --header 'content-type: application/json' \
     --data '
{
  "webhook_url": "https://example.com",
  "org_name": "Example Org Name",
  "title": "Example Title",
  "questions": [
    "Example Question 1?",
    "Example Question 2?"
  ]
}
'

Webhook Authentication

You can specify a webhook_secret_key to verify that webhook requests are genuinely from Ribbon. When a secret key is configured, Ribbon will include an X-Ribbon-Signature header (also accepted as x-ribbon-signature) with each webhook request. The signature is an HMAC-SHA256 hash of the raw JSON request body, using your webhook secret key, encoded as a 64-character hex string. Ribbon never sends the secret key in the webhook request — you configure it in your interview settings and use the same value on your server to verify signatures.

Example

Given this webhook secret key configured in your interview settings:
myGoodSecret
And this raw request body:
{"interview_flow_id": "f6298c77", "interview_id": "e48f2a8f-e235-4e4c-b5f9-b2114d684bdc", "status": "completed", "interview_flow_name": "Team 1 interview", "event_type": "interview_processed", "interviewee_email_address": "anne@ribbon.ai", "interviewee_first_name": "Anne", "interviewee_last_name": "Anquetin0", "interview_link": "https://staging.ribbon.ai/recruit/interviews/f6298c77/a5bc36d2-c22c-4bae-9d08-0b941738924e"}
Ribbon sends a request with headers like:
Content-Type: application/json
User-Agent: Ribbon-API-Webhook/1.0
x-ribbon-signature: bdae121de5d94dffe936ec3337b0395a4237a6d2433bbd0bc2941883e5667d18
To verify, compute HMAC-SHA256 of the exact raw body string using myGoodSecret and compare the result to the header value. The signature must match byte-for-byte — use the raw request body as received, not a re-serialized or pretty-printed version of the JSON.

Verifying the signature

  1. Read the raw request body as a string (before parsing JSON)
  2. Compute HMAC-SHA256 using your webhook secret key and the raw body
  3. Hex-encode the result and compare it to the X-Ribbon-Signature header value using a constant-time comparison

Code examples

Python
import hmac
import hashlib

def verify_webhook_signature(body: str, secret: str, signature: str) -> bool:
    expected = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

body = '{"interview_flow_id": "f6298c77", "interview_id": "e48f2a8f-e235-4e4c-b5f9-b2114d684bdc", "status": "completed", "interview_flow_name": "Team 1 interview", "event_type": "interview_processed", "interviewee_email_address": "anne@ribbon.ai", "interviewee_first_name": "Anne", "interviewee_last_name": "Anquetin0", "interview_link": "https://staging.ribbon.ai/recruit/interviews/f6298c77/a5bc36d2-c22c-4bae-9d08-0b941738924e"}'
secret = "myGoodSecret"
signature = "bdae121de5d94dffe936ec3337b0395a4237a6d2433bbd0bc2941883e5667d18"

verify_webhook_signature(body, secret, signature)  # True
TypeScript
import crypto from "crypto";

function verifyWebhookSignature(body: string, secret: string, signature: string): boolean {
  const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

const body = '{"interview_flow_id": "f6298c77", "interview_id": "e48f2a8f-e235-4e4c-b5f9-b2114d684bdc", "status": "completed", "interview_flow_name": "Team 1 interview", "event_type": "interview_processed", "interviewee_email_address": "anne@ribbon.ai", "interviewee_first_name": "Anne", "interviewee_last_name": "Anquetin0", "interview_link": "https://staging.ribbon.ai/recruit/interviews/f6298c77/a5bc36d2-c22c-4bae-9d08-0b941738924e"}';
const secret = "myGoodSecret";
const signature = "bdae121de5d94dffe936ec3337b0395a4237a6d2433bbd0bc2941883e5667d18";

verifyWebhookSignature(body, secret, signature); // true

Security best practices

  • Always verify signatures in production to ensure webhooks are from Ribbon
  • Use a strong, random secret key (at least 32 characters recommended)
  • Use constant-time comparison functions (like hmac.compare_digest in Python or crypto.timingSafeEqual in TypeScript) to prevent timing attacks
  • Keep your secret key secure and never expose it in client-side code
  • Verify the raw request body — parsing and re-stringifying JSON will produce a different signature

Static IP allowlisting

By default, webhook requests may originate from dynamic IP addresses. If your server requires IP allowlisting, contact your Ribbon account team to enable static IP webhook delivery for your organization. When static IP delivery is enabled, all webhook requests to your endpoint will originate from a fixed egress IP address that you can add to your firewall allowlist. Ribbon still recommends verifying the X-Ribbon-Signature header on every request — signature verification is the most reliable way to authenticate webhooks.

Webhook payload

The webhook payload returns the interview_flow_id, interview_id, interview_link and status. ⚠️ interviewee_email_address, interviewee_first_name and interviewee_last_name will be null unless the candidate information were provided when creating the interview session (POST interviews) or if the candidate went through the full platform flow where candidate information is requested. We currently support three webhook event types: interview_processed, video_processed, and candidate_status_updated

Webhook event types

interview_processed This event occurs after an interview has been completed and the interview has been successfully processed and analyzed by Ribbon. If video was collected during the interview the video will not be available yet.

Payload example

{
  "event_type": "interview_processed",  
  "interview_flow_id": "d1a104e7",
  "interview_id": "7ada85b2-b8a6-4e4a-84ed-f1c25fa63843",
  "interview_link": "https://app.ribbon.ai/recruit/interviews/d1a104e7/9ae73ba9-1e8c-4d73-8454-478d7674ea97",
  "interview_flow_name": "Manager position role interview",
  "interviewee_email_address": "[email protected]",
  "interviewee_first_name": "John",
  "interviewee_last_name": "Doe",
  "status": "completed",
}
video_processed This event only occurs if you are collecting video during the interview (is_video_enabled is true). The event is sent after video processing is complete and when the video_url is available.

Payload example

{
  "event_type": "video_processed",  
  "interview_flow_id": "d1a104e7",
  "interview_id": "7ada85b2-b8a6-4e4a-84ed-f1c25fa63843",
  "interview_link": "https://app.ribbon.ai/recruit/interviews/d1a104e7/9ae73ba9-1e8c-4d73-8454-478d7674ea97",
  "interview_flow_name": "Manager position role interview",
  "interviewee_email_address": "[email protected]",
  "interviewee_first_name": "John",
  "interviewee_last_name": "Doe",
  "status": "completed",
}
candidate_status_updated This event occurs anytime a candidate’s status changes within the candidate pipeline (for example: shortlisted → accepted). The event includes both the previous status and the new status so you can track transitions precisely.

Payload example

{
  "event_type": "candidate_status_updated",
  "interview_flow_id": "7b896044",
  "interview_flow_name": "Backend Engineer",
  "interviewee_email": "[email protected]",
  "new_status": "accepted",
  "previous_status": "shortlisted"
}

Webhook event ordering

You will explicitly always receive the events in this order:
  • interview_processed
  • video_processed

Supported Voices