Skip to main content

Overview

Webhooks let you receive real-time notifications when events happen on the Kejue platform. When an event occurs, Kejue sends an HTTP POST request to your configured URL with a JSON payload describing the event.

Event Types

EventDescription
call.startedA call has been answered and connected
call.endedA call has ended (before post-call analysis)
call.analyzedPost-call analysis is complete — full results available
campaign.startedA campaign has started processing
campaign.endedA campaign has completed, been cancelled, or failed
campaign.pausedA campaign has been paused
campaign.resumedA paused campaign has been resumed
campaign.failedA campaign has failed due to an error
Use the wildcard * when creating a subscription to receive all event types.

Payload Format

All webhook payloads follow the same envelope structure:
{
  "event": "call.ended",
  "event_id": "evt_abc123",
  "workspace_id": "ws_xyz",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {},
  "metadata": {}
}

Call Events

call.started

Fired when a call is answered and connected.
{
  "event": "call.started",
  "data": {
    "job_id": "job_123",
    "conversation_id": "conv_456",
    "contact_id": "ct_789",
    "persona_id": "per_abc",
    "campaign_id": null,
    "direction": "outbound",
    "channel": "voice",
    "job_type": "outbound_call",
    "attempt_number": 1,
    "started_at": "2025-01-15T10:30:00Z",
    "contact": {
      "id": "ct_789",
      "name": "John Doe",
      "phone": "+14155551234"
    }
  }
}

call.ended

Fired when a call ends. Includes timing, status, transcript, and recording URL.
This event fires before post-call analysis. Use call.analyzed if you need summary, score, and outcome fields.
{
  "event": "call.ended",
  "data": {
    "job_id": "job_123",
    "contact_id": "ct_789",
    "status": "completed",
    "ended_reason": "agent_hangup",
    "duration_seconds": 295,
    "transcript": "Agent: Hello, this is Sara from...",
    "recording_url": "https://recordings.kejue.co/...",
    "retry": {
      "will_retry": false,
      "next_retry_at": null,
      "attempt_number": 1,
      "max_attempts": 3
    }
  }
}
StatusDescription
completedCall connected and conversation took place
no_answerContact did not answer
busyLine was busy
failedCall failed due to a technical issue
voicemailCall went to voicemail

call.analyzed

Fired after post-call analysis completes. Contains summary, outcome, score, structured data, and tool execution results.
{
  "event": "call.analyzed",
  "data": {
    "call": {
      "id": "job_123",
      "status": "completed",
      "summary": "Discussed pricing plans and scheduled a demo...",
      "outcome_id": "interested",
      "score": 85,
      "structured_data": {
        "interested_product": "Enterprise Plan",
        "budget_range": "$500-1000/mo"
      }
    },
    "contact": {
      "id": "ct_789",
      "name": "John Doe",
      "status": "interested"
    },
    "in_call_tools": [],
    "post_call_actions": []
  }
}

Campaign Events

campaign.ended

Fired when a campaign finishes. Includes full statistics and performance metrics.
{
  "event": "campaign.ended",
  "data": {
    "campaign_id": "camp_abc",
    "campaign_name": "Q1 Outreach",
    "end_reason": "completed",
    "stats": {
      "total_contacts": 500,
      "completed": 420,
      "no_answer": 50,
      "failed": 10,
      "voicemail": 20
    },
    "performance": {
      "connect_rate_percent": 84.0,
      "completion_rate_percent": 86.0,
      "average_call_duration_seconds": 120
    }
  }
}
End ReasonDescription
completedAll contacts have been processed
cancelledCampaign was manually cancelled
failedCampaign failed due to infrastructure issues

Webhook Signing & Verification

Kejue signs webhook payloads so you can verify they’re authentic.
MethodHeaders SentHow to Verify
hmac_sha256 (default)X-Kejue-Signature, X-Kejue-TimestampHMAC-SHA256(secret, "{timestamp}.{body}")
hmac_sha1X-Kejue-Signature, X-Kejue-TimestampHMAC-SHA1(secret, "{timestamp}.{body}")
bearerAuthorization: Bearer {secret}Compare token
basic_authAuthorization: Basic {base64}Decode and compare
noneNo auth headersNo verification

Verification Examples

const crypto = require('crypto');

function verifyWebhook(body, signature, timestamp, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(`sha256=${expected}`),
    Buffer.from(signature)
  );
}
Always use constant-time comparison functions to prevent timing attacks.

Delivery & Retries

SettingDefaultDescription
Timeout5 secondsHow long to wait for your server to respond
Max Retries3Maximum number of retry attempts
Backoff500msInitial delay between retries (doubles each attempt)
Only 5xx server errors and timeouts trigger retries. 4xx responses are treated as permanent failures and are not retried.

Inline Webhooks

You can pass webhooks inline when creating a call. Inline webhooks override subscription-based webhooks for that specific call.
{
  "contact_id": "ct_789",
  "persona_id": "per_abc",
  "config": {
    "webhooks": [
      {
        "url": "https://your-server.com/webhook",
        "events": ["call.ended", "call.analyzed"],
        "signing_method": "hmac_sha256",
        "secret": "your-secret-key"
      }
    ]
  }
}