Webhooks
Webhooks allow your server to receive push notifications from Serveka whenever something happens with a bot. Instead of polling the API, Serveka POSTs a JSON payload to your configured HTTPS URL the moment an event occurs.
Serveka uses Svix to deliver webhooks with automatic retries, delivery logs, and signature verification for security.
Setting up a webhook endpoint
Before configuring webhooks via the API, ensure your server:
- Is reachable over HTTPS (Svix only delivers to HTTPS URLs)
- Returns a
2xxresponse promptly (process payloads asynchronously to avoid timeouts) - Can verify the Svix signature to ensure the request came from Serveka
Register a webhook endpoint
curl -X POST https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks \
-H "X-API-Key: srvk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/serveka",
"description": "Production event handler",
"filter_types": ["bot.completed", "transcription.ready", "recording.ready"]
}'Required:
url: Your HTTPS endpoint that will receive POST requests
Optional:
description: A human-readable label for this endpointfilter_types: Subset of event types to receive. Omit to receive all events.
Response 201:
{
"id": "ep_2abc1234...",
"url": "https://your-server.com/webhooks/serveka",
"description": "Production event handler",
"disabled": false,
"filter_types": ["bot.completed", "transcription.ready", "recording.ready"],
"created_at": "2026-05-12T10:00:00Z"
}Save the webhook id (Svix endpoint ID) — you'll need it to update or delete the endpoint.
Your workspace ID is available from GET /api/v1/workspace/me. It's a UUID like aaaaaaaa-0000-0000-0000-000000000001.
Permission required: owner role in the workspace. admin and member roles cannot manage webhook endpoints.
Event types
Serveka sends the following webhook events:
| Event type | When it fires | Volume |
|---|---|---|
bot.status_changed | Every time a bot changes status | Low–medium |
bot.completed | Bot reaches completed state (meeting ended) | Low |
transcription.ready | Full transcript is available after the meeting | Low |
recording.ready | Recording file has finished uploading to storage | Low |
transcription.segment | Each live transcript segment during the meeting | High |
Note about transcription.segment: This event fires for every transcription chunk during the meeting — potentially hundreds per meeting. Only subscribe to it if you need low-latency transcript delivery via webhook rather than using the SSE stream directly. For most use cases, SSE is better suited for real-time transcript segments.
Event payload shapes
Each webhook request includes a JSON body with the following structure:
bot.status_changed
Fires every time a bot moves to a new status (e.g., from joining to active).
{
"event_type": "bot.status_changed",
"bot": {
"bot_id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "aaaaaaaa-0000-0000-0000-000000000001",
"old_status": "joining",
"new_status": "active",
"timestamp": "2026-05-12T10:05:00Z"
}
}| Field | Type | Description |
|---|---|---|
bot_id | UUID string | The bot's unique identifier (from MeetingResponse.bot_id) |
workspace_id | UUID string | Your workspace identifier |
old_status | string | Previous status value |
new_status | string | New status value |
timestamp | ISO 8601 | When the status transition occurred (UTC) |
bot.completed
Fires once when the bot finishes the meeting and reaches the completed state.
{
"event_type": "bot.completed",
"bot": {
"bot_id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "aaaaaaaa-0000-0000-0000-000000000001",
"status": "completed",
"completion_reason": "normal_completion",
"meeting_id": 99,
"platform": "google_meet",
"timestamp": "2026-05-12T11:00:00Z"
}
}completion_reason values:
normal_completion- Meeting ended naturallystopped_by_user- You calledDELETE /api/v1/bots/{bot_id}left_alone_timeout- All participants left and timeout elapsedstartup_alone_timeout- No one joined during the startup windowmax_duration_reached- Hit the configured maximum durationremoved_by_admin- A meeting admin removed the bot
transcription.ready
Fires when the full transcript is ready to fetch via the REST API.
{
"event_type": "transcription.ready",
"meeting": {
"bot_id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "aaaaaaaa-0000-0000-0000-000000000001",
"transcript_url": "https://api.serveka.com/api/v1/bots/550e8400-e29b-41d4-a716-446655440000/transcription"
}
}Use the transcript_url to fetch the complete structured transcript with speaker labels and timestamps.
recording.ready
Fires when the recording file has finished uploading to cloud storage.
{
"event_type": "recording.ready",
"meeting": {
"bot_id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "aaaaaaaa-0000-0000-0000-000000000001",
"recording_url": "https://storage.googleapis.com/serveka-recordings/...?X-Goog-Expires=3600"
}
}The recording_url is a presigned download URL, valid for 1 hour. If it expires before you download the file, you can fetch a fresh URL via GET /api/v1/bots/{bot_id} — the recording_url field is regenerated on each request.
transcription.segment
Fires for each live transcript segment during the meeting (high volume).
{
"event_type": "transcription.segment",
"segment": {
"start": 1.23,
"end": 5.67,
"text": "Let's get started with the Q3 review.",
"speaker": "Alice",
"language": "en",
"created_at": "2026-05-12T10:05:01Z",
"completed": true,
"absolute_start_time": "2026-05-12T10:05:01Z",
"absolute_end_time": "2026-05-12T10:05:05Z"
}
}Verifying webhook signatures
Every webhook request from Serveka includes a signature you should verify before processing. This confirms the payload came from Serveka and wasn't tampered with.
Svix signs payloads using HMAC-SHA256. The signature is in the Svix-Signature header (or Webhook-Signature depending on the version).
Using the Svix SDK (recommended)
Svix provides official SDKs that handle verification automatically:
Node.js:
import { Webhook } from 'svix';
import express from 'express';
const webhook = new Webhook(process.env.SVIX_WEBHOOK_SECRET);
const app = express();
// Important: Use express.raw() to get the raw body for verification
app.post('/webhooks/serveka', express.raw({ type: 'application/json' }), (req, res) => {
const svix_id = req.headers['svix-id'];
const svix_timestamp = req.headers['svix-timestamp'];
const svix_signature = req.headers['svix-signature'];
// The headers object should contain:
// {
// 'svix-id': '<message id>',
// 'svix-timestamp': '<unix timestamp>',
// 'svix-signature': '<signature>'
// }
try {
const payload = webhook.verify(req.body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature
});
// payload is the verified, parsed event object
handleWebhookEvent(payload);
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook signature verification failed:', err);
res.status(400).json({ error: 'Invalid signature' });
}
});
app.listen(3000);Python:
import os
import json
from flask import Flask, request, abort
import svix
app = Flask(__name__)
webhook_secret = os.environ["SVIX_WEBHOOK_SECRET"]
@app.route("/webhooks/serveka", methods=["POST"])
def webhook():
# Get the headers and body
svix_id = request.headers.get("svix-id")
svix_timestamp = request.headers.get("svix-timestamp")
svix_signature = request.headers.get("svix-signature")
if not svix_id or not svix_timestamp or not svix_signature:
abort(400, "Missing Svix headers")
# Get the raw request body
payload = request.get_data()
try:
# Verify and parse the payload
svix_webhook = svix.Webhook(webhook_secret)
msg = svix_webhook.verify(payload, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature
})
# Process the verified event
handle_webhook_event(msg)
return json.dumps({"received": True}), 200
except Exception as e:
return json.dumps({"error": str(e)}), 400
if __name__ == "__main__":
app.run(port=3000)Manual verification
If you're not using an SDK, you can manually verify the signature:
- Extract the
svix-id,svix-timestamp, andsvix-signaturefrom the request headers - Create the signed payload string:
svix-id + "." + svix-timestamp + "." + raw_body - Compute the HMAC-SHA256 of the signed payload using your webhook secret as the key
- Compare the result to the
svix-signature(which should be in the formatv1,<hex>)
See the Svix webhook verification documentation for detailed implementation examples in various languages.
Retry schedule
If your endpoint returns a non-2xx response or doesn't respond within 10 seconds, Svix will automatically retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 5 hours |
After the fifth failed attempt, the event is marked as failed and will not be retried further.
Best practice: Return a 200 OK response immediately after receiving the webhook, and process the payload asynchronously (e.g., queue it for background processing). This prevents timeouts and ensures reliable delivery.
Viewing delivery logs
You can check the delivery history for any webhook endpoint to debug failures:
curl https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks/ep_2abc1234.../logs \
-H "X-API-Key: srvk_your_key_here"Response:
[
{
"id": "atmpt_2abc...",
"status": 0,
"response_status_code": 200,
"timestamp": "2026-05-12T10:05:00Z"
},
{
"id": "atmpt_3def...",
"status": 1,
"response_status_code": 500,
"timestamp": "2026-05-12T09:58:00Z"
}
]Each log entry represents a delivery attempt:
status: 0= Success (Svix considers any 2xx response as success)status: 1= Failed (non-2xx response, timeout, or connection error)response_status_code: The HTTP status code your server returnedtimestamp: When the attempt was made (UTC)
Testing your endpoint
Before going to production, send a synthetic test event to verify your endpoint is working:
curl -X POST https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks/ep_2abc1234.../test \
-H "X-API-Key: srvk_your_key_here"This dispatches a bot.status_changed event with _test: true in the payload. Verify your server receives the request, verifies the signature, and returns a 2xx response.
Response 202:
{
"message": "Test event dispatched",
"message_id": "msg_2abc...",
"event_type": "bot.status_changed"
}Check your server's logs to confirm receipt and proper handling.
Managing webhook endpoints
List all endpoints
curl https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks \
-H "X-API-Key: srvk_your_key_here"Update an endpoint
Change the URL, description, or filter types:
curl -X PATCH https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks/ep_2abc1234... \
-H "X-API-Key: srvk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"url": "https://new-server.com/webhooks/serveka",
"filter_types": ["bot.status_changed", "transcription.ready"]
}'Delete an endpoint
Permanently remove a webhook endpoint:
curl -X DELETE https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks/ep_2abc1234... \
-H "X-API-Key: srvk_your_key_here"Response 204: No content
Permission note: All webhook operations (GET, POST, PATCH, DELETE) require the owner role in the workspace.
Webhooks vs. SSE
Serveka offers two ways to receive real-time events:
Server-Sent Events (SSE)
- Best for: Real-time, high-volume data like transcript segments
- How it works: Your client opens a persistent connection to
POST /api/v1/bots/{bot_id}/subscribeand receives events as they occur - Ideal when: You need low-latency access to transcript segments and are okay with maintaining a persistent connection
Webhooks
- Best for: Reliable delivery of infrequent, important events like meeting completion
- How it works: Serveka POSTs events to your HTTPS endpoint as they occur
- Ideal when: You want to decouple your event handling from the meeting lifecycle and need guaranteed delivery (with retries)
Many applications use both: webhooks for critical events (meeting completed, recording ready) and SSE for real-time transcript display during the meeting.
Common use cases
1. Post-meeting processing pipeline
- Subscribe to
bot.completedandtranscription.readywebhooks - When received, fetch the full transcript and kick off your summarization/action item extraction pipeline
- Store results in your database and notify users via email or in-app notification
2. Real-time captions/translation display
- Use SSE to get
transcript_segmentevents as they occur - Display the text in your UI with minimal latency
- Optionally, also subscribe to
bot.status_changedto show connection status
3. Meeting analytics dashboard
- Use webhooks to track meeting lifecycle:
bot.status_changedfor join/leave times - Combine with
transcription.readyto analyze transcript content after the meeting - Build metrics like talking time ratios, sentiment, and engagement scores
4. Compliance and archiving
- Subscribe to
recording.readyto automatically archive recordings to your long-term storage - Use
transcription.readyto save transcripts for search and retrieval - Maintain an audit trail of all bot activities via
bot.status_changed
Troubleshooting
"401 Unauthorized" when testing webhooks
- Verify your workspace ID is correct (from
GET /api/v1/workspace/me) - Ensure you're using an API key with
ownerrole - Check that the key hasn't expired or been revoked
Webhooks not being delivered
- Check the delivery logs with
GET /api/v1/workspaces/{workspace_id}/webhooks/{endpoint_id}/logs - Verify your endpoint is returning a 2xx response quickly
- Ensure your server is reachable over HTTPS from the public internet
- Check firewall rules and security groups
Signature verification failing
- Confirm you're using the correct webhook secret (found in your workspace settings)
- Make sure you're verifying the raw request body, not a parsed JSON object
- Check that you're including all required Svix headers in the verification
Receiving too many events
- If you're getting overwhelmed by
transcription.segmentevents, consider:- Using SSE instead for real-time transcript segments
- Using webhooks only for lower-volume events like
bot.completedandtranscription.ready - Increasing your processing capacity or queuing system
Security considerations
- Always verify signatures - Never process a webhook without verifying it came from Serveka
- Use HTTPS - Svix only delivers to HTTPS endpoints; never use HTTP
- Limit permissions - Create API keys with the minimum required role (owner is required for webhook management)
- Rotate secrets - Periodically rotate your webhook secret in workspace settings
- Monitor logs - Regularly check delivery logs for failed attempts
- Validate payloads - Despite signature verification, always validate the structure of incoming events before processing