| title | Server events |
|---|---|
| subtitle | Learn about different events that can be sent to a Server URL. |
| slug | server-url/events |
All messages sent to your Server URL are POST requests with this body shape:
{
"message": {
"type": "<server-message-type>",
"call": { /* Call Object */ },
/* other fields depending on type */
}
}Common metadata included on most events:
phoneNumber,timestampartifact(recording, transcript, messages, etc.)assistant,customer,call,chat
Most events are informational and do not require a response. Responses are only expected for these types sent to your Server URL:
- "assistant-request"
- "tool-calls"
- "transfer-destination-request"
- "knowledge-base-request"
Note: Some specialized messages like "voice-request" and "call.endpointing.request" are sent to their dedicated servers if configured (e.g. assistant.voice.server.url, assistant.startSpeakingPlan.smartEndpointingPlan.server.url).
Example assistant configuration (excerpt):
{
"model": {
"provider": "openai",
"model": "gpt-4o",
"functions": [
{
"name": "sendEmail",
"description": "Used to send an email to a client.",
"parameters": {
"type": "object",
"properties": {
"emailAddress": { "type": "string" },
"message": { "type": "string" }
},
"required": ["emailAddress", "message"]
}
}
]
}
}When tools are triggered, your Server URL receives a tool-calls message:
{
"message": {
"type": "tool-calls",
"call": { /* Call Object */ },
"toolWithToolCallList": [
{
"name": "sendEmail",
"toolCall": { "id": "abc123", "parameters": { "emailAddress": "john@example.com", "message": "Hi!" } }
}
],
"toolCallList": [
{ "id": "abc123", "name": "sendEmail", "parameters": { "emailAddress": "john@example.com", "message": "Hi!" } }
]
}
}Respond with results for each tool call:
{
"results": [
{
"name": "sendEmail",
"toolCallId": "abc123",
"result": "{ \"status\": \"sent\" }"
}
]
}Optionally include a message to speak to the user while or after running the tool.
If a tool does not need a response immediately, you can design it to be asynchronous.For inbound phone calls, you can specify the assistant dynamically. If a PhoneNumber doesn't have an assistantId, Vapi may request one from your server:
{
"message": {
"type": "assistant-request",
"call": { /* Call Object */ }
}
}To avoid timeouts:
- Return quickly with an existing
assistantIdor a minimal assistant, then enrich context asynchronously after the call starts using Live Call Control. - Host your webhook close to
us-west-2to reduce latency, and target < ~6s to allow for network jitter.
Respond with either an existing assistant ID, a transient assistant, or transfer destination:
{ "assistantId": "your-saved-assistant-id" }{
"assistant": {
"firstMessage": "Hey Ryan, how are you?",
"model": {
"provider": "openai",
"model": "gpt-4o",
"messages": [
{ "role": "system", "content": "You're Ryan's assistant..." }
]
}
}
}{ "destination": { "type": "number", "number": "+11234567890" } }If you want to immediately transfer the call without using an assistant, return a destination in your assistant-request response. This bypasses AI handling.
{
"destination": {
"type": "number",
"number": "+14155552671",
"callerId": "{{phoneNumber.number}}",
"extension": "101",
"message": "Connecting you to support."
}
}{
"destination": {
"type": "sip",
"sipUri": "sip:support@example.com",
"sipHeaders": { "X-Account": "gold" },
"message": "Transferring you now."
}
}assistantId, assistant, squadId, and squad are ignored.
You must still respond within 7.5 seconds.
To transfer silently, set destination.message to an empty string.
For caller ID behavior, see Call features.
Or return an error message to be spoken to the caller:
{ "error": "Sorry, not enough credits on your account, please refill." }{
"message": {
"type": "status-update",
"call": { /* Call Object */ },
"status": "ended"
}
}{
"message": {
"type": "end-of-call-report",
"endedReason": "hangup",
"call": { /* Call Object */ },
"artifact": {
"recording": { /* Recording object with URLs */ },
"transcript": "AI: How can I help? User: What's the weather? ...",
"messages": [
{ "role": "assistant", "message": "How can I help?" },
{ "role": "user", "message": "What's the weather?" }
]
}
}
}{
"message": {
"type": "hang",
"call": { /* Call Object */ }
}
}Use this to surface delays or notify your team.
Sent when an update is committed to the conversation history.
{
"message": {
"type": "conversation-update",
"messages": [ /* current conversation messages */ ],
"messagesOpenAIFormatted": [ /* openai-formatted messages */ ]
}
}Partial and final transcripts from the transcriber.
{
"message": {
"type": "transcript",
"role": "user",
"transcriptType": "partial",
"transcript": "I'd like to book...",
"isFiltered": false,
"detectedThreats": [],
"originalTranscript": "I'd like to book..."
}
}For final-only events, you may receive type: "transcript[transcriptType=\"final\"]".
{
"message": {
"type": "speech-update",
"status": "started",
"role": "assistant",
"turn": 2
}
}Tokens or tool-call outputs as the model generates. The optional turnId groups all tokens from the same LLM response, so you can correlate output with a specific turn.
{
"message": {
"type": "model-output",
"output": { /* token or tool call */ },
"turnId": "abc-123"
}
}Requested when the model wants to transfer but the destination is not yet known and must be provided by your server.
{
"message": {
"type": "transfer-destination-request",
"call": { /* Call Object */ }
}
}This event is emitted only if the assistant did not supply a destination when calling a transferCall tool (for example, it did not include a custom parameter like phoneNumber). If the assistant includes the destination directly, Vapi will transfer immediately and will not send this webhook.
Respond with a destination and optionally a message:
{
"destination": { "type": "number", "number": "+11234567890" },
"message": { "type": "request-start", "message": "Transferring you now" }
}Fires whenever a transfer occurs.
{
"message": {
"type": "transfer-update",
"destination": { /* assistant | number | sip */ }
}
}Sent when the user interrupts the assistant. The optional turnId identifies the LLM turn that was interrupted, matching the turnId on model-output messages so you can discard that turn's tokens.
{
"message": {
"type": "user-interrupted",
"turnId": "abc-123"
}
}Sent when the transcriber switches based on detected language.
{
"message": {
"type": "language-change-detected",
"language": "es"
}
}When requested in assistant.serverMessages, hangup and forwarding are delegated to your server.
{
"message": {
"type": "phone-call-control",
"request": "forward",
"destination": { "type": "sip", "sipUri": "sip:agent@example.com" }
}
}{
"message": {
"type": "phone-call-control",
"request": "hang-up"
}
}If using assistant.knowledgeBase.provider = "custom-knowledge-base".
{
"message": {
"type": "knowledge-base-request",
"messages": [ /* conversation so far */ ],
"messagesOpenAIFormatted": [ /* openai-formatted messages */ ]
}
}Respond with documents (and optionally a custom message to speak):
{
"documents": [
{ "content": "Return policy is 30 days...", "similarity": 0.92, "uuid": "doc-1" }
]
}{
"message": {
"type": "voice-input",
"input": "Hello, world!"
}
}Sent to assistant.voice.server.url. Respond with raw 1-channel 16-bit PCM audio at the requested sample rate (not JSON).
{
"message": {
"type": "voice-request",
"text": "Hello, world!",
"sampleRate": 24000
}
}Sent to assistant.startSpeakingPlan.smartEndpointingPlan.server.url.
{
"message": {
"type": "call.endpointing.request",
"messagesOpenAIFormatted": [ /* openai-formatted messages */ ]
}
}Respond with the timeout before considering the user's speech finished:
{ "timeoutSeconds": 0.5 }chat.created: Sent when a new chat is created.chat.deleted: Sent when a chat is deleted.
{ "message": { "type": "chat.created", "chat": { /* Chat */ } } }session.created: Sent when a session is created.session.updated: Sent when a session is updated.session.deleted: Sent when a session is deleted.
{ "message": { "type": "session.created", "session": { /* Session */ } } }