Webhooks

Webhooks notify your backend when a call starts, ends, or fails, so you don't have to poll. Use them to update a CRM record, store transcripts in your data warehouse, alert on failures, or track call duration for billing. Webhooks are the server-side record of each call. To react while the conversation runs, use the session events instead.

How delivery works

How delivery works

You register an endpoint URL, and Mistral sends it a signed JSON payload each time an event happens. Every delivery is an HTTP POST with content-type: application/json, and the same envelope:

{
  "event": "<event_type>",
  "created_at": "2026-09-08T14:32:10Z",
  "data": { ... }
}
FieldTypeDescription
eventstringThe event type, see Event types and payloads.
created_atstringWhen the event was emitted, as an ISO 8601 UTC timestamp.
dataobjectThe payload of the event.
Set up an endpoint and a subscription

Set up an endpoint and a subscription

You configure webhooks per agent, in the Mistral console or through the API:

  1. Create an endpoint: give the URL that receives the events and a name. The platform generates a signing secret.
  2. Create a subscription: attach the endpoint to an agent, and choose the event types to receive.

One endpoint can serve several agents, and one agent can have several subscriptions.

Warning

Store the signing secret securely: you need it to verify signatures. It's generated once and can't be rotated. If it leaks, delete the endpoint, create a new one, and update your verification code.

Verify signatures and handle deliveries

Verify signatures and handle deliveries

Every delivery has an x-mistral-signature header: an HMAC-SHA256 digest of the raw request body, computed with your endpoint secret.

x-mistral-signature: hmac-sha256=<hex_digest>

To verify a delivery:

  1. Read the raw request body, before any JSON parsing.
  2. Compute HMAC-SHA256(secret, raw_body).
  3. Compare your digest with the value after hmac-sha256=, with a constant-time comparison.
import hashlib
import hmac

def verify_signature(raw_body: bytes, secret: str, header: str) -> bool:
    expected = hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    received = header.removeprefix("hmac-sha256=")
    return hmac.compare_digest(expected, received)
Warning

Always verify the raw bytes of the body, not a JSON string you serialized again: that can change whitespace or key order, and the digest no longer matches.

A safe receiver verifies the signature, returns a 200 right away, then processes the event. This minimal server dispatches by event type:

import hashlib
import hmac
import os

from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
SECRET = os.environ["WEBHOOK_SECRET"]

def verify_signature(raw_body: bytes, header: str) -> None:
    expected = hmac.new(
        SECRET.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    received = header.removeprefix("hmac-sha256=")
    if not hmac.compare_digest(expected, received):
        raise HTTPException(status_code=401, detail="Invalid signature")

@app.post("/webhooks")
async def webhook_handler(
    request: Request,
    x_mistral_signature: str = Header(...),
):
    raw_body = await request.body()
    verify_signature(raw_body, x_mistral_signature)

    event = await request.json()

    match event["event"]:
        case "call_started":
            print(f"Call started: {event['data']['session_id']}")
        case "call_ended":
            print(f"Call ended: {event['data']['session_id']}")
        case "call_failed":
            print(f"Call failed: {event['data']['session_id']}")

    return {"status": "ok"}

Because deliveries can be retried, the same event can arrive more than once: use session_id and event together to skip duplicates.

Event types and payloads

Event types and payloads

EventSent when
call_startedA session is created and the voice pipeline is ready.
call_endedA session ends normally.
call_failedA session fails to start, or hits a fatal error during the call.
call_started

call_started

{
  "event": "call_started",
  "created_at": "2026-09-08T14:32:10Z",
  "data": {
    "session_id": "RM_a1b2c3d4e5f6",
    "agent_id": "ag_01234567-89ab-cdef-0123-456789abcdef",
    "model": "mistral-small-latest",
    "started_at": 1725802330
  }
}
FieldTypeDescription
session_idstringUnique ID of the session.
agent_idstringAgent that handled the call.
modelstringModel used for the answers.
started_atintegerWhen the session was created, as a Unix timestamp in seconds.
call_ended

call_ended

Sent when a session ends normally. It adds the duration and, when available, the full transcript.

{
  "event": "call_ended",
  "created_at": "2026-09-08T14:35:42Z",
  "data": {
    "session_id": "RM_a1b2c3d4e5f6",
    "agent_id": "ag_01234567-89ab-cdef-0123-456789abcdef",
    "model": "mistral-small-latest",
    "duration_seconds": 212,
    "started_at": 1725802330,
    "ended_at": 1725802542,
    "transcript": [
      { "role": "assistant", "content": "Hello! How can I help you today?" },
      { "role": "user", "content": "I'd like to check my account balance." },
      { "role": "assistant", "content": "Sure, let me look that up for you." }
    ]
  }
}
FieldTypeDescription
session_idstringUnique ID of the session.
agent_idstringAgent that handled the call.
modelstringModel used for the answers.
duration_secondsintegerDuration of the call, in seconds.
started_atintegerWhen the session started, as a Unix timestamp in seconds.
ended_atintegerWhen the session ended, as a Unix timestamp in seconds.
transcriptarray or nullThe turns of the conversation, in order. Each turn has a role (user or assistant) and a content (text, or null if empty).
Note

The transcript is best effort: if it isn't ready in time for the delivery, transcript is null. The rest of the payload is complete either way.

call_failed

call_failed

Sent when a session fails to start, for example because of an infrastructure error, or hits a fatal error during the call.

{
  "event": "call_failed",
  "created_at": "2026-09-08T14:32:11Z",
  "data": {
    "session_id": "RM_a1b2c3d4e5f6",
    "agent_id": "ag_01234567-89ab-cdef-0123-456789abcdef",
    "model": "mistral-small-latest",
    "error": "livekit_error"
  }
}
FieldTypeDescription
session_idstringUnique ID of the session.
agent_idstringAgent that handled the call.
modelstringModel used for the answers.
errorstringMachine-readable reason, for example token_error, db_error, livekit_error, or a disconnection reason for failures during the call.
Delivery and retries

Delivery and retries

BehaviorDetail
TimeoutYour endpoint must respond within 10 seconds.
SuccessAny 2xx status code counts as delivered.
RetriesOn 5xx or 429 responses, Mistral tries up to 3 times in total, with exponential backoff (2 s, then 4 s).
Not retried4xx responses other than 429. Check your endpoint logic.
Transport errorsNetwork failures (DNS, TLS, connection reset) are retried on the same schedule.

Follow these practices:

  • Return a 200 right away. Acknowledge the delivery, then process it asynchronously. A slow handler risks timeouts and duplicate deliveries.
  • Make your handler idempotent. Retries can deliver the same event twice: deduplicate with session_id and event.
  • Use HTTPS. The API accepts plain HTTP URLs, but HTTPS protects the payload and your secret in transit.
  • Keep the secret safe. It can't be rotated: if it leaks, replace the endpoint.
Test locally

Test locally

To receive webhooks on your machine during development, expose your local server with an HTTP tunneling tool, and register the URL it gives you as the endpoint.

Going further

Going further