Skip to content

Webhooks

Receive real-time HTTP callbacks when events happen in your Kayse account — no polling required.

Overview

Webhooks push event data to your server the moment something happens: a call starts, a message is delivered, and more. Each delivery is signed, retried on failure, and logged for inspection.

Use Cases

  • Sync call outcomes to your CRM in real-time
  • Trigger follow-up workflows when messages are delivered
  • Build audit logs of all campaign activity
  • Send notifications to Slack, Teams, or other channels

Setting Up Webhooks

From Company Integrations

  1. Open Company → Integrations
  2. Select Webhooks to open the dedicated detail screen
  3. Click New
  4. Enter your endpoint URL
  5. Select events to subscribe to (or leave empty for all events)
  6. Optionally attach an App Key
  7. Save

From a Campaign

  1. Open a campaign and go to the Workflows tab
  2. Scroll to the Webhooks section and click New
  3. The campaign filter is applied automatically — the webhook only fires for events in that campaign

Configuration Fields

FieldRequiredDescription
NameYesA label for this webhook (minimum 3 characters)
Destination URLYesThe HTTPS endpoint that receives events. Must resolve to a public IP address (no localhost, private, or link-local IPs).
EventsNoWhich event types trigger this webhook. Leave empty to receive all event types.
Case Type FilterNoLimit deliveries to events matching specific case types. Leave empty for all.
Campaign FilterNoLimit deliveries to events from specific campaigns. Cannot be combined with message events or the all-events option.
App KeyNoAttach an API key — it will be sent as the X-Public-Api-Key header on every delivery for extra verification.
ActiveEnable or disable the webhook. Disabled webhooks stop receiving events (except test events).

Event Types

Kayse currently fires the following event types:

Event TypeTrigger
call_startedA campaign call begins
call_endedA campaign call finishes
call_analyzedThe transcript and summary for a call are ready
message_sentAn outbound message (SMS, email, etc.) is sent
message_deliveredThe provider confirms a message was delivered
task_completedA case task is marked as completed
form_submittedA form task is submitted
webhook_testYou clicked Send Test in the UI

All Events

Leave the event selection empty when creating a webhook to automatically receive all event types, including any new types added in the future.

Campaign Filter Restriction

Campaign filters can only be used when specific event types are selected, and cannot be combined with message events (message_sent, message_delivered) because messages are not scoped to a single campaign.

Payload Format

Every webhook delivery is an HTTP POST with a JSON body. All payloads share this envelope:

json
{
  "id": "call_started:12345:a1b2c3d4",
  "type": "call_started",
  "occurred_at": "2025-06-15T14:30:00Z",
  "data": {
    // event-specific fields — see below
  }
}

Envelope Fields

FieldTypeDescription
idstringUnique event identifier. Use this for idempotency — the same id is never delivered twice to the same webhook.
typestringOne of the event types listed above.
occurred_atstringISO 8601 / RFC 3339 timestamp of when the event happened.
dataobjectEvent-specific payload (see sections below).

Call Events

Sent for call_started, call_ended, and call_analyzed. All three share the same data shape; call_analyzed additionally includes a non-empty summary and may include post_call_analysis.

json
{
  "id": "call_ended:42:e8f3a1b2",
  "type": "call_ended",
  "occurred_at": "2025-06-15T14:35:00Z",
  "data": {
    "campaign_call_id": 42,
    "client_id": 100,
    "case_id": 55,
    "campaign_id": 7,
    "case_type_id": 3,
    "client_name": "Jane Smith",
    "client_number": "+15551234567",
    "agent_number": "+15559876543",
    "direction": "outbound",
    "status": "completed",
    "call_status": "completed",
    "duration_ms": 45000,
    "disconnection_reason": "caller_hangup",
    "summary": "",
    "post_call_analysis": {
      "appointment_scheduled": true,
      "preferred_language": "English"
    }
  }
}

Call Data Fields

FieldTypeDescription
campaign_call_idintegerInternal Kayse campaign call ID
client_idintegerID of the client on the call
case_idinteger or nullAssociated case ID, if any
campaign_idinteger or nullCampaign the call belongs to, if any
case_type_idinteger or nullCase type ID, if any
client_namestringFull name of the client
client_numberstringClient phone number (E.164)
agent_numberstringKayse agent phone number (E.164)
directionstring"inbound" or "outbound"
statusstringCampaign call status (e.g. "completed", "failed", "no_answer")
call_statusstringTelephony-level status
duration_msintegerCall duration in milliseconds
disconnection_reasonstringWhy the call ended (e.g. "caller_hangup", "agent_hangup", "timeout")
summarystringAI-generated call summary. Empty for call_started and call_ended; populated for call_analyzed.
post_call_analysisobject or absentCustom post-call analysis fields configured on the campaign agent. Only present for call_analyzed events when custom fields are configured. System fields (e.g. call_summary, converted) are excluded — only user-defined fields appear here.

Message Events

Sent for message_sent and message_delivered.

json
{
  "id": "message_sent:01J5EXAMPLE:b7c8d9e0",
  "type": "message_sent",
  "occurred_at": "2025-06-15T14:40:00Z",
  "data": {
    "message_ulid": "01J5EXAMPLE",
    "client_id": 100,
    "admin_id": 12,
    "case_id": 55,
    "case_type_id": 3,
    "channel": "sms",
    "direction": "outbound",
    "delivery_status": "sent",
    "text": "Hi Jane, just following up on your case.",
    "subject": "",
    "client_contact": "+15551234567",
    "company_contact": "+15559876543"
  }
}

Message Data Fields

FieldTypeDescription
message_ulidstringUnique message identifier (ULID format)
client_idintegerID of the client
admin_idinteger or nullID of the admin who sent the message, or null for automated messages
case_idinteger or nullAssociated case ID, if any
case_type_idinteger or nullCase type ID, if any
channelstringDelivery channel: "sms", "email", "whatsapp", etc.
directionstring"inbound" or "outbound"
delivery_statusstringCurrent delivery status (e.g. "sent", "delivered", "failed")
textstringMessage body text
subjectstringEmail subject line (empty for SMS/WhatsApp)
client_contactstringClient's phone number or email
company_contactstringCompany phone number or email used

Task Events

Sent for task_completed and form_submitted. Both events share the same data shape. form_submitted is fired specifically when a form-type task is completed; task_completed is fired for all other case tasks.

json
{
  "id": "task_completed:150:c4d5e6f7",
  "type": "task_completed",
  "occurred_at": "2025-06-15T15:00:00Z",
  "data": {
    "case_task_id": 150,
    "task_id": 25,
    "case_id": 55,
    "case_type_id": 3,
    "form_id": null,
    "task_title": "Upload signed retainer",
    "task_body": "Please upload a signed copy of the retainer agreement.",
    "status": "completed",
    "status_note": "",
    "completed_at": "2025-06-15T15:00:00Z"
  }
}

Task Data Fields

FieldTypeDescription
case_task_idintegerInternal Kayse case task ID
task_idintegerID of the task template this case task was created from
case_idintegerAssociated case ID
case_type_idinteger or nullCase type ID, if any
form_idinteger or nullForm ID if this is a form-type task, otherwise null
task_titlestringTitle of the task
task_bodystringDescription/body of the task
statusstringTask status (e.g. "completed")
status_notestringOptional status note
completed_atstring or nullISO 8601 timestamp of when the task was completed

Test Event

Sent when you click Send Test in the webhook UI.

json
{
  "id": "test:99:a1b2c3d4",
  "type": "webhook_test",
  "occurred_at": "2025-06-15T14:45:00Z",
  "data": {
    "message": "This is a test webhook from Kayse."
  }
}
FieldTypeDescription
messagestringA static test message

HTTP Headers

Every webhook delivery includes these headers:

HeaderExampleDescription
Content-Typeapplication/jsonAlways JSON
User-AgentKayse-Webhooks/1.0Identifies the sender
X-Kayse-Timestamp1718458200Unix timestamp of when the delivery was sent
X-Kayse-Event-Idcall_ended:42:e8f3a1b2The event id from the payload
X-Kayse-Signaturea3f2b8c1...HMAC-SHA256 signature for verification (see below)
X-Public-Api-Keypk_live_...Only present if an App Key is attached to the webhook

Signature Verification

Every delivery is signed with the webhook's Signing Secret using HMAC-SHA256. Verify the signature to ensure the request genuinely came from Kayse.

How the Signature Is Computed

The signed message is: {timestamp}.{json_body}

HMAC-SHA256(signing_secret, "{X-Kayse-Timestamp}.{raw_request_body}")

The result is hex-encoded and sent as X-Kayse-Signature.

Node.js Example

javascript
const crypto = require('crypto');

function verifyWebhook(req, signingSecret) {
  const timestamp = req.headers['x-kayse-timestamp'];
  const signature = req.headers['x-kayse-signature'];
  const body = req.rawBody; // must be the raw string, not parsed JSON

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

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

Python Example

python
import hmac
import hashlib

def verify_webhook(timestamp: str, body: bytes, signature: str, signing_secret: str) -> bool:
    message = f"{timestamp}.".encode() + body
    expected = hmac.new(
        signing_secret.encode(),
        message,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Go Example

go
func verifyWebhook(timestamp string, body []byte, signature, signingSecret string) bool {
	mac := hmac.New(sha256.New, []byte(signingSecret))
	mac.Write([]byte(timestamp))
	mac.Write([]byte("."))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expected))
}

Finding Your Signing Secret

Open the webhook detail view and click Show next to the Signing Secret field. You can also click Copy to copy it to your clipboard.

Delivery & Retries

Expected Response

Your endpoint should:

  • Return a 2xx status code (200, 201, 202)
  • Respond within 15 seconds
  • Acknowledge quickly and process asynchronously if needed

A delivery is considered failed if your endpoint returns a non-2xx status, times out, or is unreachable.

Retry Policy

Failed deliveries are retried with exponential backoff over a 24-hour window:

AttemptApproximate Delay
1Immediate
21 minute
32 minutes
44 minutes
58 minutes
6+Continues doubling until the 24-hour window expires

After 24 hours from the original event, no more retries are attempted and the delivery is marked as failed.

Manual Replay

If a delivery failed, you can replay it from the webhook detail view:

  1. Open the webhook
  2. Find the failed delivery in Recent Deliveries
  3. Click Replay

Replayed deliveries create a new delivery attempt with a fresh event key.

Delivery Logs

Every delivery attempt is logged. Click any row in Recent Deliveries to inspect:

FieldDescription
EventThe event type that was sent
ResultDelivered or Failed
AttemptsHow many times this delivery was attempted
HTTP CodeThe status code your endpoint returned
DurationHow long the request took
TimeWhen the attempt was made
Request HeadersHeaders sent with the delivery (signatures redacted)
Request PayloadThe full JSON body
Response BodyThe first 4 KB of your endpoint's response
ErrorError message if the delivery failed

Best Practices

Respond Quickly

Return 200 OK immediately, then process the payload in the background. If your handler takes too long, the delivery will be marked as timed out and retried.

javascript
app.post('/webhooks/kayse', (req, res) => {
  res.status(200).send('OK');
  processEvent(req.body); // async
});

Handle Duplicates

Use the id field from the payload for idempotency. In rare cases (network issues, retries), the same event may be delivered more than once.

Use HTTPS

Always use an https:// endpoint. HTTP endpoints are accepted but strongly discouraged in production.

Verify Signatures

Always verify the X-Kayse-Signature header before trusting the payload. This prevents spoofed requests.

Monitor Failures

Check the delivery logs in the webhook detail view regularly. Repeated failures may indicate an endpoint issue.

Troubleshooting

SymptomLikely CauseFix
Not receiving eventsWebhook is pausedToggle Active on
Not receiving eventsNo matching event types selectedCheck event subscriptions or leave empty for all
Signature mismatchUsing parsed body instead of raw bytesVerify against the raw request body string
Signature mismatchWrong signing secretCopy the secret from the webhook detail view
Timeout errorsHandler takes too longReturn 200 immediately, process async
SSL errorsInvalid or expired certificateEnsure your endpoint has a valid SSL certificate
"URL cannot target private network addresses"Endpoint resolves to a private IPUse a publicly routable address

Turn unreachable clients into paid cases.