Skip to main content

Webhooks

Replay webhooks allow you to receive real-time notifications when important events occur in your account. Configure webhook endpoints in your company settings to automatically receive event data.

Overview

Webhooks are HTTP POST requests sent to your configured endpoint URL when specific events occur. Each webhook includes:
  • Signed payloads - All webhooks are signed using HMAC-SHA256 for security
  • Retry logic - Automatic retries with exponential backoff (up to 3 attempts)
  • Event filtering - Enable or disable specific event types per webhook endpoint
  • Multiple endpoints - Configure multiple webhook URLs per company

Setting Up Webhooks

1. Configure Webhook Endpoint

Navigate to your company settings and add a webhook URL. Each webhook endpoint includes:
  • Webhook URL - The HTTPS endpoint where events will be sent
  • Signing Secret - A secret key used to verify webhook authenticity (format: whsec_...)
  • Event Subscriptions - Toggle which events to receive

2. Verify Webhook Signatures

All webhooks include an X-Replay-Signature header that you should verify to ensure the request came from Replay. Header Format:
X-Replay-Signature: t={timestamp},v1={signature}
Verification Steps:
  1. Extract the timestamp (t) and signature (v1) from the header
  2. Get the raw request body as a string
  3. Create the signed payload: {timestamp}.{raw_body_json}
  4. Compute HMAC-SHA256 using your signing secret
  5. Compare the computed signature with the provided signature using a timing-safe comparison
  6. Verify the timestamp is within 5 minutes (to prevent replay attacks)
Example Verification (Node.js):
const crypto = require('crypto');

function verifyWebhookSignature(payload, signatureHeader, secret) {
  // Parse header: t={timestamp},v1={signature}
  const parts = signatureHeader.split(',');
  const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
  const signature = parts.find(p => p.startsWith('v1='))?.split('=')[1];
  
  if (!timestamp || !signature) {
    return false;
  }
  
  // Check timestamp is within 5 minutes
  const timestampDate = new Date(timestamp);
  const now = new Date();
  const ageInSeconds = (now.getTime() - timestampDate.getTime()) / 1000;
  
  if (Math.abs(ageInSeconds) > 300) { // 5 minutes
    return false;
  }
  
  // Create signed payload
  const payloadString = JSON.stringify(payload);
  const signedPayload = `${timestamp}.${payloadString}`;
  
  // Compute HMAC
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(signedPayload);
  const expectedSignature = hmac.digest('hex');
  
  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

Webhook Events

recording.processing.failed

Sent when processing of an external recording fails. Payload:
{
  "event": "recording.processing.failed",
  "timestamp": "2025-01-24T12:00:00Z",
  "data": {
    "userUniqueIdentifier": "user-123",
    "recordingPresignedUrl": "https://storage.example.com/audio.mp3?signature=...",
    "error": "Failed to download file from presigned URL"
  }
}

roleplay.session.results

Sent when a roleplay session completes and results are available. Payload:
{
  "event": "roleplay.session.results",
  "timestamp": "2025-01-24T12:00:00Z",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "activityType": "premade-roleplay",
    "activityId": "550e8400-e29b-41d4-a716-446655440000",
    "didPass": true,
    "length": 120000,
    "wpm": 150,
    "nameCount": 3,
    "numQuestions": 5,
    "numFillerWords": 2,
    "score": 85.5,
    "createdAt": "2025-01-24T12:00:00Z",
    "updatedAt": "2025-01-24T12:05:00Z",
    "user": {
      "learnerName": "John Doe",
      "originDomain": "example.com",
      "uniqueIdentifier": "user-123"
    },
    "premadeRoleplay": {
      "title": "Customer Service Roleplay",
      "passingScore": 70
    },
    "snippets": [
      {
        "isUser": true,
        "text": "Hello, how can I help you today?"
      },
      {
        "isUser": false,
        "text": "I'm having trouble with my order."
      }
    ],
    "scenario": {
      "title": "Order Issue"
    },
    "customerPersona": {
      "name": "Sarah",
      "imageUrl": "https://example.com/avatar.jpg"
    },
    "customerSituation": {
      "title": "Delayed Delivery"
    },
    "criteriaResponses": [
      {
        "title": "Used customer's name",
        "passed": true,
        "explanation": "The rep used the customer's name appropriately",
        "weight": 10,
        "sectionTitle": "Communication"
      }
    ],
    "metricResponses": [
      {
        "title": "WPM",
        "passed": true,
        "explanation": "Scored 150 (85%)",
        "weight": 15,
        "sectionTitle": "Performance"
      }
    ]
  }
}

memorization.results

Sent when a script memorization attempt completes. Payload:
{
  "event": "memorization.results",
  "timestamp": "2025-01-24T12:00:00Z",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "score": 92,
    "createdAt": "2025-01-24T12:00:00Z",
    "activityId": "550e8400-e29b-41d4-a716-446655440000",
    "activityType": "script",
    "didPassLevel": true,
    "didPassAllLevels": false,
    "user": {
      "learnerName": "John Doe",
      "originDomain": "example.com",
      "uniqueIdentifier": "user-123"
    },
    "script": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "Sales Script",
      "passingScore": 85,
      "numTotalLevels": 5,
      "numPassedLevels": 3,
      "numTotalAttempts": 8
    },
    "mostRecentAttempt": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "score": 92,
      "createdAt": "2025-01-24T12:00:00Z",
      "scriptSegmentAttempts": [
        {
          "order": 1,
          "ogText": "Hello, how can I help you today?",
          "speaker": "User",
          "spokenWords": "Hi there, how may I assist you today?"
        }
      ]
    },
    "allAttempts": [
      {
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "score": 92,
        "createdAt": "2025-01-24T12:00:00Z",
        "scriptSegmentAttempts": []
      }
    ]
  }
}

roleplay.created

Sent when a LiveKit room is successfully created for a roleplay session. Payload:
{
  "event": "roleplay.created",
  "timestamp": "2025-01-24T12:00:00Z",
  "data": {
    "roleplayId": "123e4567-e89b-12d3-a456-426614174000",
    "roomName": "John Doe::123e4567-e89b-12d3-a456-426614174000",
    "premadeRoleplayId": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2025-01-24T12:00:00Z"
  }
}

roleplay.session.audio.completed

Sent when roleplay audio recording processing completes. Payload:
{
  "event": "roleplay.session.audio.completed",
  "timestamp": "2025-01-24T12:00:00Z",
  "data": {
    "roleplaySessionId": "123e4567-e89b-12d3-a456-426614174000",
    "audioUrl": "https://storage.googleapis.com/replay-storage/recording.mp3?X-Goog-Algorithm=..."
  }
}
Notes:
  • The audioUrl is a pre-signed Google Cloud Storage URL with a 15-minute expiry
  • Download or process the audio file within 15 minutes of receiving the webhook
  • This event is only for audio recordings (not video)

Retry Logic

Webhooks include automatic retry logic:
  • Max Retries: 3 attempts
  • Retry Delays: Exponential backoff (1s, 2s, 4s)
  • Timeout: 30 seconds per attempt
  • Retry Conditions: Network errors, timeouts, or 5xx status codes
Your endpoint should return a 2xx status code to indicate successful processing. Any other status code will trigger a retry.

Security Best Practices

  1. Always verify signatures - Never process webhooks without verifying the X-Replay-Signature header
  2. Use HTTPS - Webhook URLs must use HTTPS
  3. Check timestamps - Reject webhooks older than 5 minutes to prevent replay attacks
  4. Store secrets securely - Keep your webhook signing secrets secure and never expose them in client-side code
  5. Idempotency - Design your webhook handlers to be idempotent (safe to process multiple times)

Testing Webhooks

You can test your webhook endpoint using tools like:

Troubleshooting

Webhook Not Received

  • Verify your webhook URL is accessible and returns a 2xx status code
  • Check that the event type is enabled in your webhook settings
  • Review server logs for delivery attempts
  • Ensure your endpoint can handle POST requests with JSON payloads

Invalid Signature

  • Verify you’re using the correct signing secret from your webhook settings
  • Ensure you’re parsing the signature header correctly
  • Check that you’re using the raw request body (not parsed JSON) for verification
  • Verify timestamp is within the 5-minute tolerance window

Retry Failures

  • Check your endpoint’s response time (must be under 30 seconds)
  • Ensure your endpoint returns proper HTTP status codes
  • Review error logs for specific failure reasons