Human-in-the-loop
Some tool calls, like sending an email or modifying data, are safer with human approval. The requires_confirmation parameter lets you intercept these calls before they run, so a user or your system can approve or deny each action.
This works for all tool types: Connectors, built-in tools (such as web_search_premium), and local functions (Python SDK only).
You can implement the confirmation flow in two ways:
- REST API: manage conversation IDs and tool call IDs yourself. Works with any language or HTTP client.
- Python SDK: use
RunContextandDeferredToolCallsExceptionfor a higher-level flow that handles the plumbing for you.
Configuration
Add requires_confirmation to the tool_configuration of any Connector or built-in tool, and list the tool names that require approval:
[
{
"type": "connector",
"connector_id": "gmail",
"tool_configuration": {
"requires_confirmation": ["gmail_search"]
}
},
{
"type": "web_search_premium",
"tool_configuration": {
"requires_confirmation": ["web_search", "news_search"]
}
}
]REST API flow
The confirmation flow has two steps:
- Start the conversation and get a pending
function.call. - Approve or deny the call to resume the conversation.
Start the conversation
The API returns a pending function.call entry instead of running the tool.
RESPONSE=$(curl -s -X POST "https://api.mistral.ai/v1/conversations" \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mistral-medium-latest",
"inputs": "What is the title of my latest email?",
"tools": [
{
"type": "connector",
"connector_id": "gmail",
"tool_configuration": {
"requires_confirmation": ["gmail_search"]
}
}
]
}')
CONVERSATION_ID=$(echo "$RESPONSE" | jq -r '.conversation_id')
TOOL_CALL_ID=$(echo "$RESPONSE" | jq -r '.outputs[0].tool_call_id')The response contains a pending function.call:
{
"conversation_id": "conv_abc123...",
"outputs": [
{
"type": "function.call",
"tool_call_id": "WJfo42Ow3",
"name": "gmail_search",
"arguments": "{\"limit\": 1}",
"confirmation_status": "pending"
}
]
}Approve the tool call
Send "allow" to run the tool and get the model response.
curl -X POST "https://api.mistral.ai/v1/conversations/$CONVERSATION_ID" \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tool_confirmations": [
{"tool_call_id": "'$TOOL_CALL_ID'", "confirmation": "allow"}
]
}'Deny the tool call
Send "deny" to reject the call. The model handles the rejection gracefully.
curl -X POST "https://api.mistral.ai/v1/conversations/$CONVERSATION_ID" \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tool_confirmations": [
{"tool_call_id": "'$TOOL_CALL_ID'", "confirmation": "deny"}
]
}'Multiple tool calls can be pending at once. You can approve or deny them individually or as a batch in a single request.
Python SDK flow
DeferredToolCallsException requires mistralai v2.4+ and the mcp extra (pip install mistralai[mcp]). If you're on an earlier version, use the REST API flow.
The Python SDK provides RunContext and DeferredToolCallsException to handle confirmation flows without managing conversation IDs and tool call IDs yourself.
run_async runs the conversation loop and raises DeferredToolCallsException when a tool requires confirmation. Call dc.confirm() or dc.reject() on each deferred call, then loop back to resume.
Local functions
Register local Python functions with register_func and set requires_confirmation=True for functions that need approval.
import asyncio
import os
import random
from mistralai.client import Mistral
from mistralai.extra.run.context import RunContext
from mistralai.extra.exceptions import DeferredToolCallsException
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
temp = random.randint(10, 30)
return f"The weather in {city} is {random.choice(['sunny', 'cloudy'])}, {temp}C"
def book_flight(destination: str, date: str) -> str:
"""Book a flight to a destination."""
return f"Flight booked to {destination} on {date}. Confirmation: FL-{random.randint(10000, 99999)}"
async def main():
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
conversation_id = None
pending_inputs = [
{"role": "user", "content": "I need a vacation somewhere warm next Friday. Can you help?"}
]
while True:
async with RunContext(model="mistral-large-latest") as run_ctx:
run_ctx.conversation_id = conversation_id
run_ctx.register_func(get_weather, requires_confirmation=False)
run_ctx.register_func(book_flight, requires_confirmation=True)
try:
result = await client.beta.conversations.run_async(
run_ctx=run_ctx,
inputs=pending_inputs,
instructions="You are a travel assistant. Check the weather and book a flight to the warmest destination.",
)
print(result.output_entries)
break
except DeferredToolCallsException as deferred:
conversation_id = deferred.conversation_id
pending_inputs = []
for dc in deferred.deferred_calls:
print(f"[APPROVAL REQUIRED] {dc.tool_name}({dc.arguments})")
approved = input("Approve? (y/n): ").strip().lower() == "y"
pending_inputs.append(dc.confirm() if approved else dc.reject("Denied by user"))
asyncio.run(main())Connector tools
Connector tools work the same way. run_async raises DeferredToolCallsException when the server returns a function.call with confirmation_status: "pending".
import asyncio
import os
from mistralai.client import Mistral
from mistralai.extra.exceptions import DeferredToolCallsException
from mistralai.extra.run.context import RunContext
async def main():
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
conversation_id = None
pending_inputs = [{"role": "user", "content": "Summarize my latest emails from Gmail."}]
while True:
async with RunContext(model="mistral-large-latest") as run_ctx:
run_ctx.conversation_id = conversation_id
try:
result = await client.beta.conversations.run_async(
run_ctx=run_ctx,
inputs=pending_inputs,
instructions="You are a helpful assistant. Use Gmail to access the user's emails.",
tools=[
{
"type": "connector",
"connector_id": "gmail",
"tool_configuration": {
"requires_confirmation": ["gmail_search"],
},
},
],
)
for entry in result.output_entries:
if hasattr(entry, "content"):
print(entry.content)
break
except DeferredToolCallsException as deferred:
conversation_id = deferred.conversation_id
pending_inputs = []
for dc in deferred.deferred_calls:
print(f"[APPROVAL REQUIRED] {dc.tool_name}({dc.arguments})")
approved = input("Approve? (y/n): ").strip().lower() == "y"
pending_inputs.append(dc.confirm() if approved else dc.reject("Denied by user"))
asyncio.run(main())Stateless (serialize and resume)
For web APIs where the confirmation happens in a separate request (for example, the frontend sends back the approval), serialize the deferred state and reconstruct it later.
import asyncio, json, os
from mistralai.client import Mistral
from mistralai.extra.run.context import RunContext
from mistralai.extra.exceptions import DeferredToolCallsException
def book_flight(destination: str, date: str) -> str:
"""Book a flight to a destination."""
return f"Flight booked to {destination} on {date}"
async def main():
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
async with RunContext(model="mistral-large-latest") as run_ctx:
run_ctx.register_func(book_flight, requires_confirmation=True)
try:
result = await client.beta.conversations.run_async(
run_ctx=run_ctx,
inputs=[{"role": "user", "content": "Book me a flight to Paris next Friday."}],
instructions="You are a travel assistant. Book the flight directly.",
)
print("No confirmation needed:", result.output_as_text)
except DeferredToolCallsException as deferred:
# Serialize and send to frontend / store in DB
print(json.dumps(deferred.to_dict()))
asyncio.run(main())deferred.executed_results contains results from tools that ran before the deferral point. Always prepend them when resuming so the model has full context.