Gareth Wilson Gareth Wilson

Guide to Mistral Webhooks: Features and Best Practices

Published


Mistral webhooks tell your backend what happened on a call handled by a Mistral AI Studio voice agent: the call started, it ended normally, or it failed. They are the server-side record of each call, so they are where you update a CRM record, store a transcript, alert on failures or track call duration for billing, without polling Mistral's API.

This guide covers how Mistral webhooks are configured and signed, the three call events and their payloads, the short retry window that catches out new integrations, and the best practices for running a Mistral receiver in production.

What are Mistral webhooks?

Mistral's webhooks belong to its voice agents. You set them up per agent in two parts: an endpoint, which is a URL and a name with a signing secret that Mistral generates, and a subscription, which attaches that endpoint to an agent and chooses the event types to send. One endpoint can serve several agents, and one agent can have several subscriptions.

Every delivery is an HTTP POST with content-type: application/json, and every body uses the same envelope: event names the event type, created_at says when it was emitted, and data carries the call details. Each delivery is signed with an x-mistral-signature header that holds a hex HMAC-SHA256 of the raw body.

Mistral webhooks are not the way to react while a conversation is still running. Mistral points to its session events for that, and describes webhooks as the record of each call.

Mistral webhook features

FeatureDetails
ConfigurationPer agent, in the Mistral console or through the API
Subscription modelAn endpoint (URL, name, secret) attached to agents by subscriptions that pick event types
Method and formatPOST, application/json
Signature headerx-mistral-signature
Header formathmac-sha256=<hex_digest>
SchemeHMAC-SHA256, lowercase hex
Signed contentThe raw request body, nothing else
SecretGenerated per endpoint, shown once, used as a UTF-8 string. It can't be rotated
Replay windowNone. No timestamp is signed
Response window10 seconds
SuccessAny 2xx
Retries5xx, 429 and transport errors, up to 3 attempts in total, with 2 s then 4 s backoff
Not retriedAny other 4xx
Event idNone. Deduplicate on session_id plus event

Mistral's API accepts plain HTTP endpoint URLs, but its docs recommend HTTPS to protect the payload and the secret. It publishes no source IP list for webhooks.

Common events

Mistral documents three events.

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

Every data object carries session_id, agent_id and model. call_started adds started_at. call_ended adds duration_seconds, started_at, ended_at and the conversation transcript. call_failed adds a machine-readable error, for example token_error, db_error, livekit_error, or a disconnection reason when the call failed part-way through. Here is Mistral's call_ended example:

{
  "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." }
    ]
  }
}

The transcript is best effort. If it isn't ready in time for the delivery, transcript is null, though the rest of the payload is complete. Each turn has a role of user or assistant and a content that can itself be null.

Watch the time formats: created_at is an ISO 8601 UTC string, while started_at and ended_at inside data are Unix timestamps in seconds.

Setting up Mistral webhooks

  1. In the Mistral console, or through the API, create an endpoint with your receiver's URL and a name.
  2. Copy the signing secret that Mistral generates and store it server-side, for example as MISTRAL_WEBHOOK_SECRET. It is generated once and can't be rotated, so keep it somewhere you won't lose it.
  3. Create a subscription that attaches the endpoint to an agent, and choose the events: call_started, call_ended, call_failed, or a subset.
  4. Repeat the subscription step for any other agent that should send to the same endpoint.

If you use Hookdeck, create a Mistral source, set the endpoint's signing secret on it, and register the Hookdeck source URL as the endpoint URL in Mistral.

Mistral documents no URL validation or challenge request, so the endpoint does not need to be running when you create it. To test locally, expose your server through a tunnel and register the tunnel's URL. With the Hookdeck CLI:

npx hookdeck-cli listen 3000 mistral --path /webhooks/mistral

Securing Mistral webhooks

Read the raw body, compute an HMAC-SHA256 of it keyed with the endpoint secret, encode it as hex, and compare it with the value after hmac-sha256= in constant time.

const crypto = require('crypto');
const express = require('express');

const PREFIX = 'hmac-sha256=';

function verifyMistralSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !signatureHeader.startsWith(PREFIX)) return false;

  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  const received = Buffer.from(signatureHeader.slice(PREFIX.length), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/mistral', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.MISTRAL_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyMistralSignature(req.body, req.get('x-mistral-signature'), secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.event. Persist the event, then process it asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac

PREFIX = "hmac-sha256="


def verify_mistral_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not signature_header.startswith(PREFIX):
        return False
    received = signature_header[len(PREFIX):]
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_mistral_signature(raw_body, request.headers.get("x-mistral-signature"),
#                          os.environ["MISTRAL_WEBHOOK_SECRET"])

Mistral's own TypeScript sample calls timingSafeEqual without checking lengths first. That function throws when the two buffers differ in length, so a malformed header turns into an exception rather than a clean rejection. The length check above avoids that.

To check your verifier, use the secret mistral-test-secret and this exact body:

{"event":"call_failed","created_at":"2026-09-08T14:32:11Z","data":{"session_id":"RM_a1b2c3d4e5f6","error":"livekit_error"}}

The header should be:

hmac-sha256=c17d4915d2b75cec3b5c0e54a102b0fdf1fabbe8e9b5b33540222ed13aae2b4a

Make Mistral webhooks production-ready. Hookdeck Event Gateway verifies the x-mistral-signature header, accepts every call event as soon as it arrives, and durably queues it, so a few seconds of downtime on your side doesn't use up Mistral's three attempts.

Mistral webhook limitations and pain points

Three attempts in about six seconds

The Problem: An event is lost if your endpoint is down for more than a few seconds, for example during a deploy or a brief database outage.

Why It Happens: Mistral makes at most three attempts in total, with backoff of 2 seconds and then 4 seconds. After the third failure the event is not sent again, and Mistral documents no way to list or redeliver past events.

Workarounds:

  • Keep the receiver's only job to verify, store and return 200, so it stays up through deploys.
  • Run the receiver separately from the services that process call data.
  • Alert on deliveries that fail on your side, so you know which calls may be missing.

How Hookdeck Can Help: Hookdeck accepts each delivery and stores it before your service is involved, then retries delivery to your service on a schedule you control and lets you replay any event, so your downtime no longer has to fit inside Mistral's retry window.

A 4xx stops retries

The Problem: A delivery that fails because of a bug in your handler is never retried.

Why It Happens: Mistral retries 5xx, 429 and transport errors only. Any other 4xx is treated as final, so a handler that returns 400 on a payload it doesn't expect drops that event for good.

Workarounds:

  • Return 4xx only for requests that really are invalid, such as a failed signature.
  • Return 5xx for errors on your side that you want Mistral to retry.
  • Acknowledge unknown event types with 200 and log them.

How Hookdeck Can Help: Hookdeck answers Mistral itself, so how your service responds no longer decides whether the event survives. Failed deliveries to your service are retried by Hookdeck and stay available to replay.

A secret that can't be rotated

The Problem: If the signing secret leaks, you can't swap it for a new one.

Why It Happens: Mistral generates the secret once when you create the endpoint, and its docs state that it can't be rotated. The only fix is to delete the endpoint, create a new one, update your verification code with the new secret and recreate the subscriptions.

Workarounds:

  • Store the secret in a secrets manager the moment you create the endpoint, and keep it out of logs.
  • Script endpoint and subscription creation through the API, so replacing them is quick.

How Hookdeck Can Help: With Hookdeck in front, the secret lives on the Hookdeck source rather than in each service. Replacing a leaked endpoint means updating one source.

No event id and no replay protection

The Problem: You can't tell a retry from a new event by an id, and a captured request still verifies if it is sent again.

Why It Happens: Mistral's envelope has no event or delivery id, and the signature covers only the body, with no timestamp.

Workarounds:

  • Deduplicate on session_id plus event, as Mistral recommends.
  • Keep the endpoint on HTTPS so deliveries can't be captured in transit.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on data.session_id and event before they reach your service.

Best practices

Verify against the raw body

The signature covers the exact bytes Mistral sent. In Express, mount express.raw() on the webhook route and parse only after verification. In a Next.js route handler, read await request.text(). In FastAPI, read await request.body(). Re-serialised JSON can change whitespace or key order, and the digest no longer matches.

Treat the transcript as optional

transcript on call_ended is null when it wasn't ready in time, and a turn's content can be null too. Code that loops over the transcript or reads content as a string should handle both cases, and anything that depends on having a transcript should record when it was missing.

Acknowledge fast and process asynchronously

Mistral waits 10 seconds for a response. Verify, persist the event, return 200, and do the real work afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.

Keep one endpoint per environment

Because one endpoint can serve several agents, it is easy to send test agents and production agents to the same URL. Use separate endpoints for each environment, and record agent_id with every event so you can tell them apart.

Conclusion

Mistral signs each voice agent webhook with a hex HMAC-SHA256 of the raw body in x-mistral-signature, prefixed with hmac-sha256= and keyed with an endpoint secret that can't be rotated. Strip the prefix, verify against the raw bytes, deduplicate on session_id plus event, expect a null transcript now and then, and keep the receiver available, because Mistral gives up after three attempts.

Hookdeck Event Gateway sits in front of your receiver, checks every x-mistral-signature, and holds each call event until your service has processed it.

Get started with Hookdeck for free and handle Mistral webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.