← All packages

AI · Convex component

convex-vapi

Sync Vapi voice AI calls into Convex reactively, and place outbound calls from Convex functions.

npm install convex-vapi
54downloads last week
745downloads last 12 months
v0.0.5latest, Sep 18, 2026
5releases since Sep 13, 2026

Downloads

Download history

Loading download history…

Build this

Build with convex-vapi

The install and setup steps from the package documentation.

  1. 1

    Install

    sh
    npm install convex-vapi
  2. 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

    sh
    npx convex env set VAPI_API_KEY your-private-api-key
    npx convex env set VAPI_WEBHOOK_SECRET whsec_...

    VAPI_API_KEY is your private key from the Vapi dashboard (Settings → API Keys). VAPI_WEBHOOK_SECRET is 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 serverUrl to https://<your-deployment>.convex.site/webhooks/vapi and its server.secret to the same value as VAPI_WEBHOOK_SECRET, either in the Vapi dashboard or via the API:

    sh
    curl -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. 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 a Vapi client 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. 4

    Usage

    Place an outbound call

    ts
    export 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

    ts
    export 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

    tsx
    const calls = useQuery(api.example.listCallsByAssistant, {
      assistantId: "assistant_...",
    });

    Every status-update and end-of-call-report webhook event patches a row, so this query re-renders live as a call progresses from queued through ringing, in-progress, and ended — no polling.