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:

  1. Is reachable over HTTPS (Svix only delivers to HTTPS URLs)
  2. Returns a 2xx response promptly (process payloads asynchronously to avoid timeouts)
  3. Can verify the Svix signature to ensure the request came from Serveka

Register a webhook endpoint

bash
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 endpoint
  • filter_types: Subset of event types to receive. Omit to receive all events.

Response 201:

json
{
  "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 typeWhen it firesVolume
bot.status_changedEvery time a bot changes statusLow–medium
bot.completedBot reaches completed state (meeting ended)Low
transcription.readyFull transcript is available after the meetingLow
recording.readyRecording file has finished uploading to storageLow
transcription.segmentEach live transcript segment during the meetingHigh

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).

json
{
  "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"
  }
}
FieldTypeDescription
bot_idUUID stringThe bot's unique identifier (from MeetingResponse.bot_id)
workspace_idUUID stringYour workspace identifier
old_statusstringPrevious status value
new_statusstringNew status value
timestampISO 8601When the status transition occurred (UTC)

bot.completed

Fires once when the bot finishes the meeting and reaches the completed state.

json
{
  "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 naturally
  • stopped_by_user - You called DELETE /api/v1/bots/{bot_id}
  • left_alone_timeout - All participants left and timeout elapsed
  • startup_alone_timeout - No one joined during the startup window
  • max_duration_reached - Hit the configured maximum duration
  • removed_by_admin - A meeting admin removed the bot

transcription.ready

Fires when the full transcript is ready to fetch via the REST API.

json
{
  "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.

json
{
  "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).

json
{
  "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).

Svix provides official SDKs that handle verification automatically:

Node.js:

javascript
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:

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:

  1. Extract the svix-id, svix-timestamp, and svix-signature from the request headers
  2. Create the signed payload string: svix-id + "." + svix-timestamp + "." + raw_body
  3. Compute the HMAC-SHA256 of the signed payload using your webhook secret as the key
  4. Compare the result to the svix-signature (which should be in the format v1,<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:

AttemptDelay
1st retry5 seconds
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry5 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:

bash
curl https://api.serveka.com/api/v1/workspaces/YOUR_WORKSPACE_ID/webhooks/ep_2abc1234.../logs \
  -H "X-API-Key: srvk_your_key_here"

Response:

json
[
  {
    "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 returned
  • timestamp: When the attempt was made (UTC)

Testing your endpoint

Before going to production, send a synthetic test event to verify your endpoint is working:

bash
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:

json
{
  "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

bash
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:

bash
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:

bash
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}/subscribe and 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.completed and transcription.ready webhooks
  • 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_segment events as they occur
  • Display the text in your UI with minimal latency
  • Optionally, also subscribe to bot.status_changed to show connection status

3. Meeting analytics dashboard

  • Use webhooks to track meeting lifecycle: bot.status_changed for join/leave times
  • Combine with transcription.ready to analyze transcript content after the meeting
  • Build metrics like talking time ratios, sentiment, and engagement scores

4. Compliance and archiving

  • Subscribe to recording.ready to automatically archive recordings to your long-term storage
  • Use transcription.ready to 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 owner role
  • 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.segment events, consider:
    • Using SSE instead for real-time transcript segments
    • Using webhooks only for lower-volume events like bot.completed and transcription.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
Updated May 2026Edit on GitHub