skills/vapiai/skills/setup-webhook

setup-webhook

SKILL.md

Vapi Webhook / Server URL Setup

Configure server URLs to receive real-time events from Vapi during calls — transcripts, tool calls, status changes, and end-of-call reports.

Setup: Ensure VAPI_API_KEY is set. See the setup-api-key skill if needed.

Overview

Vapi uses "Server URLs" (webhooks) to communicate with your application. Unlike traditional one-way webhooks, Vapi server URLs support bidirectional communication — your server can respond with data that affects the call.

Where to Set Server URLs

On an Assistant

curl -X PATCH https://api.vapi.ai/assistant/{id} \
  -H "Authorization: Bearer $VAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "serverUrl": "https://your-server.com/vapi/webhook",
    "serverUrlSecret": "your-webhook-secret"
  }'

On a Phone Number

curl -X PATCH https://api.vapi.ai/phone-number/{id} \
  -H "Authorization: Bearer $VAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "serverUrl": "https://your-server.com/vapi/webhook"
  }'

At the Organization Level

Set a default server URL in the Vapi Dashboard under Settings > Server URL.

Priority order: Tool server URL > Assistant server URL > Phone Number server URL > Organization server URL.

Event Types

Event Description Expects Response?
assistant-request Request for dynamic assistant config Yes — return assistant config
tool-calls Assistant is calling a tool Yes — return tool results
status-update Call status changed No
transcript Real-time transcript update No
end-of-call-report Call completed with summary No
hang Assistant failed to respond No
speech-update Speech activity detected No

Webhook Server Example (Express.js)

import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

app.post("/vapi/webhook", (req, res) => {
  const { message } = req.body;

  switch (message.type) {
    case "assistant-request":
      // Dynamically configure the assistant based on the caller
      res.json({
        assistant: {
          name: "Dynamic Assistant",
          firstMessage: `Hello ${message.call.customer?.name || "there"}!`,
          model: {
            provider: "openai",
            model: "gpt-4.1",
            messages: [
              { role: "system", content: "You are a helpful assistant." },
            ],
          },
          voice: { provider: "vapi", voiceId: "Elliot" },
          transcriber: { provider: "deepgram", model: "nova-3", language: "en" },
        },
      });
      break;

    case "tool-calls":
      // Handle tool calls from the assistant
      const results = message.toolCallList.map((toolCall: any) => ({
        toolCallId: toolCall.id,
        result: handleToolCall(toolCall.name, toolCall.arguments),
      }));
      res.json({ results });
      break;

    case "end-of-call-report":
      // Process the call report
      console.log("Call ended:", {
        callId: message.call.id,
        duration: message.durationSeconds,
        cost: message.cost,
        summary: message.summary,
        transcript: message.transcript,
      });
      res.json({});
      break;

    case "status-update":
      console.log("Call status:", message.status);
      res.json({});
      break;

    case "transcript":
      console.log(`[${message.role}]: ${message.transcript}`);
      res.json({});
      break;

    default:
      res.json({});
  }
});

function handleToolCall(name: string, args: any): string {
  // Implement your tool logic here
  return `Result for ${name}`;
}

app.listen(3000, () => console.log("Webhook server running on port 3000"));

Webhook Server Example (Python / Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/vapi/webhook", methods=["POST"])
def vapi_webhook():
    data = request.json
    message = data.get("message", {})
    msg_type = message.get("type")

    if msg_type == "assistant-request":
        return jsonify({
            "assistant": {
                "name": "Dynamic Assistant",
                "firstMessage": "Hello! How can I help?",
                "model": {
                    "provider": "openai",
                    "model": "gpt-4.1",
                    "messages": [
                        {"role": "system", "content": "You are a helpful assistant."}
                    ],
                },
                "voice": {"provider": "vapi", "voiceId": "Elliot"},
                "transcriber": {"provider": "deepgram", "model": "nova-3", "language": "en"},
            }
        })

    elif msg_type == "tool-calls":
        results = []
        for tool_call in message.get("toolCallList", []):
            results.append({
                "toolCallId": tool_call["id"],
                "result": f"Handled {tool_call['name']}",
            })
        return jsonify({"results": results})

    elif msg_type == "end-of-call-report":
        print(f"Call ended: {message['call']['id']}")
        print(f"Summary: {message.get('summary')}")

    return jsonify({})

if __name__ == "__main__":
    app.run(port=3000)

Webhook Authentication

Verify webhook authenticity using the secret:

function verifyWebhook(req: express.Request, secret: string): boolean {
  const signature = req.headers["x-vapi-signature"] as string;
  if (!signature || !secret) return false;

  const payload = JSON.stringify(req.body);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Local Development

Use the Vapi CLI to forward webhooks to your local server:

# Install the CLI
curl -sSL https://vapi.ai/install.sh | bash

# Forward events to local server
vapi listen --forward-to localhost:3000/vapi/webhook

Or use ngrok:

ngrok http 3000
# Copy the ngrok URL and set it as your server URL

End-of-Call Report Fields

The end-of-call-report event includes:

Field Description
call Full call object with metadata
transcript Complete conversation transcript
summary AI-generated call summary
recordingUrl URL to the call recording
durationSeconds Call duration
cost Total call cost
costBreakdown Breakdown by component (STT, LLM, TTS, transport)
messages Array of all conversation messages

References

Additional Resources

This skills repository includes a Vapi documentation MCP server (vapi-docs) that gives your AI agent access to the full Vapi knowledge base. Use the searchDocs tool to look up anything beyond what this skill covers — advanced configuration, troubleshooting, SDK details, and more.

Auto-configured: If you cloned or installed these skills, the MCP server is already configured via .mcp.json (Claude Code), .cursor/mcp.json (Cursor), or .vscode/mcp.json (VS Code Copilot).

Manual setup: If your agent doesn't auto-detect the config, run:

claude mcp add vapi-docs -- npx -y mcp-remote https://docs.vapi.ai/_mcp/server

See the README for full setup instructions across all supported agents.

Weekly Installs
231
Repository
vapiai/skills
GitHub Stars
16
First Seen
Feb 23, 2026
Installed on
cursor210
codex207
kimi-cli207
gemini-cli207
github-copilot207
amp207