Skip to main content
Webhooks send your server real-time updates when something important happens in Attendee, so that you don’t need to poll the API. They can alert your server when a bot joins a meeting, starts recording, when a recording is available, when a chat message is sent, when the transcript is updated, when participants join or leave meetings, or when calendar events are updated. Attendee supports two types of webhooks:
  • Project-level webhooks: Apply to all bots in a project (created via UI)
  • Bot-level webhooks: Apply to specific bots only (created via API)

Creating Project-Level Webhooks

1

Navigate to webhooks

Click on “Settings → Webhooks” in the sidebar
2

Create webhook

Click “Create Webhook”
3

Configure webhook

Provide an HTTPS URL that will receive webhook events
4

Select triggers

Select the triggers you want to receive notifications for (we currently have eight triggers: bot.state_change, transcript.update, chat_messages.update, participant_events.join_leave, participant_events.speech_start_stop, calendar.events_update, calendar.state_change, and bot_logs.update)
5

Save

Click “Create” to save your subscription

Creating Bot-Level Webhooks

Bot-level webhooks are created via the API when creating a bot. Include a webhooks field in your bot creation request:
{
  "meeting_url": "https://zoom.us/j/123456789",
  "bot_name": "My Bot with Webhooks",
  "webhooks": [
    {
      "url": "https://my-app.com/bot-webhook",
      "triggers": ["bot.state_change", "transcript.update"]
    },
    {
      "url": "https://backup-webhook.com/events",
      "triggers": ["bot.state_change", "chat_messages.update"]
    }
  ]
}

Available Webhook Triggers

TriggerDescription
bot.state_changeBot changes state (joins, leaves, starts recording, etc.)
transcript.updateReal-time transcript updates during meeting
chat_messages.updateChat message updates in the meeting
participant_events.join_leaveA participant joins or leaves the meeting
participant_events.speech_start_stopA participant starts or stops speaking
calendar.events_updateCalendar events have been synced and updated
calendar.state_changeCalendar connection state has changed (connected/disconnected)
bot_logs.updateA log entry associated with a bot has been created

Webhook Delivery Priority

When a bot has both project-level and bot-level webhooks configured, the bot-level webhooks will be used instead of the project-level webhooks.

Webhook Limits and Validation

  • Maximum: 2 webhooks per project or per bot
  • URL Format: Must start with https://
  • Uniqueness: Same URL cannot be used multiple times for the same bot/project

Webhook Payload

When a webhook is delivered, Attendee will send an HTTP POST request to your webhook URL with the following structure:
{
  "idempotency_key": "< UUID that uniquely identifies this webhook delivery >",
  "bot_id": "< Id of the bot associated with the webhook delivery >",
  "bot_metadata": "< Any metadata associated with the bot >",
  "trigger": "< Trigger for the webhook >",
  "data": "< Trigger-specific data >"
}
{
  "idempotency_key": "< UUID that uniquely identifies this webhook delivery >",
  "calendar_id": "< Id of the calendar associated with the webhook delivery >",
  "calendar_deduplication_key": "< Deduplication key of the calendar (if set) >",
  "calendar_metadata": "< Any metadata associated with the calendar >",
  "trigger": "< Trigger for the webhook >",
  "data": "< Trigger-specific data >"
}

Payload for bot.state_change trigger

For webhooks triggered by bot.state_change, the data field contains:
{
  "new_state": "< The current state of the bot >",
  "old_state": "< The previous state of the bot >",
  "created_at": "< The timestamp when the state change occurred >",
  "event_type": "< The type of event that triggered the state change >",
  "event_sub_type": "< The sub-type of event that triggered the state change >"
}

Using webhooks to know when the recording is available

The most common use case for webhooks is to be notified when the meeting has ended and the recording is available. You can do this by listening for the post_processing_completed event type. The data field will look like this:
{
  "new_state": "ended",
  "old_state": "post_processing",
  "created_at": "2023-07-15T14:30:45.123456Z",
  "event_type": "post_processing_completed",
  "event_sub_type": null
}

Payload for transcript.update trigger

For webhooks triggered by transcript.update, the data field contains a single utterance:
{
  "speaker_name": "<The name of the speaker>",
  "speaker_uuid": "<The UUID of the speaker within the meeting>",
  "speaker_user_uuid": "<The UUID of the speaker's user account within the meeting platform>",
  "speaker_is_host": "<Whether the speaker is the host of the meeting>",
  "timestamp_ms": "<The timestamp of the utterance in milliseconds>",
  "duration_ms": "<The duration of the utterance in milliseconds>",
  "transcription": {
    "transcript": "<The utterance text>",
    "words": "<The word-level timestamps of the utterance if they exist>"
  }
}

Payload for chat_messages.update trigger

For webhooks triggered by chat_messages.update, the data field contains a single chat message:
{
  "id": "<The ID of the chat message>",
  "to": "<Whether the message was sent to the bot or to everyone>",
  "text": "<The text of the chat message>",
  "timestamp": "<The timestamp of the chat message>",
  "sender_name": "<The name of the participant who sent the chat message>",
  "sender_uuid": "<The UUID of the participant who sent the chat message>",
  "timestamp_ms": "<The timestamp of the chat message in milliseconds>",
  "additional_data": "<Any additional data associated with the chat message>",
  "sender_user_uuid": "<The UUID of the participant's user account within the meeting platform>"
}

Payload for participant_events.join_leave and participant_events.speech_start_stop triggers

For webhooks triggered by participant_events.join_leave and participant_events.speech_start_stop, the data field contains a single participant event:
{
  "id": "<The ID of the participant event>",
  "participant_name": "<The name of the participant who joined or left the meeting>",
  "participant_uuid": "<The UUID of the participant who joined or left the meeting>",
  "participant_user_uuid": "<The UUID of the participant's user account within the meeting platform>",
  "participant_is_host": "<Whether the participant is the host of the meeting>",
  "event_type": "<The type of event that occurred. Either 'join', 'leave', 'speech_start', or 'speech_stop'>",
  "event_data": "<Any additional data associated with the event. This is empty for join and leave events>",
  "timestamp_ms": "<The timestamp of the event in milliseconds>"
}

Payload for calendar.events_update trigger

For webhooks triggered by calendar.events_update, the data field contains calendar sync information:
{
  "state": "<The current state of the calendar connection ('connected' or 'disconnected')>",
  "connection_failure_data": "<Any error data if the calendar is disconnected, null if connected>",
  "last_successful_sync_at": "<The timestamp of the last successful calendar sync>",
  "last_attempted_sync_at": "<The timestamp of the last sync attempt>"
}
This webhook is triggered after each successful calendar sync operation, which occurs automatically to keep your calendar events up to date with the remote calendar (Google Calendar or Microsoft Calendar). After receiving this webhook, you can fetch the calendar events from the Attendee API to get the latest events.

Payload for calendar.state_change trigger

For webhooks triggered by calendar.state_change, the data field contains the same structure as calendar.events_update:
{
  "state": "<The current state of the calendar connection ('connected' or 'disconnected')>",
  "connection_failure_data": "<Error details when the calendar becomes disconnected>",
  "last_successful_sync_at": "<The timestamp of the last successful calendar sync>",
  "last_attempted_sync_at": "<The timestamp of the last sync attempt>"
}
This webhook is triggered when a calendar’s connection state changes, typically when authentication fails and the calendar becomes disconnected. The connection_failure_data field will contain error details such as:
{
  "error": "Google Authentication error: {'error': 'invalid_grant'}",
  "timestamp": "2023-07-15T14:30:45.123456Z"
}

Payload for bot_logs.update trigger

For webhooks triggered by bot_logs.update, the data field contains information about a log entry associated with a bot:
{
  "id": "<The ID of the log entry>",
  "level": "<The severity level of the log entry (e.g., 'debug', 'info', 'warning', 'error')>",
  "entry_type": "<The type of the log entry (e.g., 'uncategorized', 'could_not_enable_closed_captions')>",
  "message": "<The log message>",
  "created_at": "<The timestamp when the log entry was created>"
}
This webhook fires every time a new log entry is recorded for a bot. Use these events to keep track of errors, warnings, or other relevant activities occurring on your bots — especially issues that matter but don’t rise to the level of a state change. Monitoring these log webhooks helps you catch and investigate noteworthy bot events in real time.

Debugging Webhook Deliveries

Go to the ‘Bots’ page and navigate to a Bot which was created after you created your webhook. You should see a ‘Webhooks’ tab on the page. Clicking it will show a list of all the webhook deliveries for that bot, whether they succeeded and the response from your server.

Verifying Webhooks

To ensure the webhook requests are coming from Attendee, we sign each request with a secret. You can verify this signature to confirm the authenticity of the request.
  • Each project has a single webhook secret used for both project and bot-level webhooks. You can get the secret in the Settings → Webhooks page.
  • The signature is included in the X-Webhook-Signature header of each webhook request

Webhook Retry Policy

If your endpoint returns a non-2xx status code or fails to respond within 10 seconds, Attendee will retry the webhook delivery up to 3 times with exponential backoff.

Code examples for processing webhooks

Here are some code examples for processing webhooks in different languages.
import json
import logging
import hmac
import hashlib
import base64

from flask import Flask, request

app = Flask(__name__)
port = 5005

# Add your secret you got from the dashboard here
webhook_secret = "<YOUR_SECRET>"

def sign_payload(payload, secret):
    """
    Sign a webhook payload using HMAC-SHA256. Returns a base64-encoded HMAC-SHA256 signature
    """
    # Convert the payload to a canonical JSON string
    payload_json = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))

    # Decode the secret
    secret_decoded = base64.b64decode(secret)

    # Create the signature
    signature = hmac.new(secret_decoded, payload_json.encode("utf-8"), hashlib.sha256).digest()

    # Return base64 encoded signature
    return base64.b64encode(signature).decode("utf-8")

@app.route("/", methods=["POST"])
def webhook():
    # Try to parse as JSON
    payload = json.loads(request.data)
    print("Received payload =", payload)
    signature_from_header = request.headers.get("X-Webhook-Signature")
    signature_from_payload = sign_payload(payload, webhook_secret)
    print("signature_from_header =", signature_from_header)
    print("signature_from_payload =", signature_from_payload)
    if signature_from_header != signature_from_payload:
        return "Invalid signature", 400
    print("Signature is valid")

    # Respond with 200 OK
    return "Webhook received successfully", 200


if __name__ == "__main__":
    print(f"Webhook server running at http://localhost:{port}")
    print("Ready to receive webhook requests")
    log = logging.getLogger("werkzeug")
    log.setLevel(logging.ERROR)  # Only show errors, not request info
    app.run(host="0.0.0.0", port=port, debug=False)

Build docs developers (and LLMs) love