AI · Convex component
convex-vapi
Sync Vapi voice AI calls into Convex reactively, and place outbound calls from Convex functions.
npm install convex-vapiDownloads
Download history
Build this
Build with convex-vapi
The install and setup steps from the package documentation.
- 1
Install
shnpm install convex-vapi - 2
Quick Start
1. Add the component
ts// convex/convex.config.ts import { defineApp } from "convex/server"; import convexVapi from "convex-vapi/convex.config"; const app = defineApp(); app.use(convexVapi); export default app;2. Set environment variables
shnpx convex env set VAPI_API_KEY your-private-api-key npx convex env set VAPI_WEBHOOK_SECRET whsec_...VAPI_API_KEYis your private key from the Vapi dashboard (Settings → API Keys).VAPI_WEBHOOK_SECRETis a secret string you choose yourself — you'll configure the same value on the assistant in step 4.3. Mount the webhook handler
ts// convex/http.ts import { httpRouter } from "convex/server"; import { components } from "./_generated/api"; import { Vapi } from "convex-vapi"; const vapi = new Vapi(components.convexVapi, { apiKey: process.env.VAPI_API_KEY!, webhookSecret: process.env.VAPI_WEBHOOK_SECRET!, }); const http = httpRouter(); http.route({ path: "/webhooks/vapi", method: "POST", handler: vapi.webhookHandler, }); export default http;4. Configure the webhook on your assistant
Set the assistant's
serverUrltohttps://<your-deployment>.convex.site/webhooks/vapiand itsserver.secretto the same value asVAPI_WEBHOOK_SECRET, either in the Vapi dashboard or via the API:shcurl -X PATCH https://api.vapi.ai/assistant/<assistant-id> \ -H "Authorization: Bearer $VAPI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"serverUrl": "https://<your-deployment>.convex.site/webhooks/vapi", "server": {"secret": "whsec_..."}}'5. Initialize the client
ts// convex/example.ts import { action, query } from "./_generated/server"; import { components } from "./_generated/api"; import { Vapi } from "convex-vapi"; import { v } from "convex/values"; const vapi = new Vapi(components.convexVapi, { apiKey: process.env.VAPI_API_KEY!, webhookSecret: process.env.VAPI_WEBHOOK_SECRET!, }); export const listCallsByAssistant = query({ args: { assistantId: v.string() }, handler: async (ctx, args) => { return await vapi.listCallsByAssistant(ctx, args); }, }); - 3
Setup
The component needs no schema changes in your app — its tables (
calls,webhookEvents) live entirely inside the component's own isolated schema. All you need is the webhook mounted (step 3) and secret configured (step 4), plus aVapiclient instance wherever you call its methods.Assistants and phone numbers are managed entirely in the Vapi dashboard or API — this component tracks calls, not the assistants that make them.
- 4
Usage
Place an outbound call
tsexport const dial = action({ args: { assistantId: v.string(), phoneNumberId: v.string(), customerNumber: v.string(), }, handler: async (ctx, args) => { return await vapi.createCall(ctx, args); }, });Returns
{ callId, status }and immediately records the call in Convex — you don't have to wait for the first webhook to see it in a query.Refresh a call on demand
tsexport const sync = action({ args: { callId: v.string() }, handler: async (ctx, args) => { await vapi.refreshCall(ctx, args); return null; }, });Fetches the call directly from the Vapi API and re-records it — useful as a fallback if a webhook delivery was missed, or to pull in the transcript/recording immediately after a call ends without waiting on the webhook queue.
Read calls reactively
tsxconst calls = useQuery(api.example.listCallsByAssistant, { assistantId: "assistant_...", });Every
status-updateandend-of-call-reportwebhook event patches a row, so this query re-renders live as a call progresses fromqueuedthroughringing,in-progress, andended— no polling.