All skills

Cursor skill

WhatsApp Skill for Cursor

Drop-in Cursor rule that teaches the AI to send WhatsApp messages, broadcasts, and webhooks via the Gavi WhatsApp API.

Install

  1. 1. Save the skill content below as.cursor/rules/gaviwhatsapp.mdin your project (or user config) directory.
  2. 2. Install the SDK:
    npm install @gaviwhatsapp/whatsapp
  3. 3. Set the API key environment variable:
    GAVIVENTURES_API_KEY=gv_...
    Get a key →
  4. 4. Restart Cursor. The agent will now know how to use the Gavi WhatsApp API when you ask it to send messages.

GaviVentures WhatsApp API — Cursor Skill

Use this skill whenever the user asks to send WhatsApp messages, add WhatsApp notifications, or integrate WhatsApp into their application.

Canonical documentation

When you need authoritative reference (endpoints, payload shapes, error codes), use these URLs — do NOT guess paths like /docs:

Setup

  1. Install the SDK:
npm install @gaviwhatsapp/whatsapp
  1. Set the environment variable:
GAVIVENTURES_API_KEY=gv_...

Get an API key at https://www.gaviventures.com/whatsapp/settings/api-keys

Send a Text Message

import { WhatsApp } from '@gaviwhatsapp/whatsapp'

const wa = new WhatsApp({ apiKey: process.env.GAVIVENTURES_API_KEY! })

await wa.send({ to: '+919876543210', text: 'Hello from my app!' })

Send a Template Message

Templates are pre-approved message formats required for initiating conversations.

await wa.sendTemplate({
  to: '+919876543210',
  template: 'order_confirmation',
  language: 'en',
  variables: { '1': 'ORD-1234', '2': '$49.99' },
})

Send Media (Image, Document, Video)

await wa.sendMedia({
  to: '+919876543210',
  type: 'image',
  url: 'https://example.com/receipt.png',
  caption: 'Your receipt',
})

await wa.sendMedia({
  to: '+919876543210',
  type: 'document',
  url: 'https://example.com/invoice.pdf',
  filename: 'invoice.pdf',
})

List Templates

const templates = await wa.getTemplates()
// Returns: { templates: [{ name, status, category, language, ... }] }

Send Broadcast

await wa.broadcast({
  recipients: ['+919876543210', '+919876543211'],
  template: 'promo_offer',
  language: 'en',
  variables_per_recipient: [
    { '1': 'Alice', '2': '20%' },
    { '1': 'Bob', '2': '15%' },
  ],
})

Register a Webhook

Receive real-time delivery updates and incoming messages.

const webhook = await wa.registerWebhook({
  url: 'https://myapp.com/api/whatsapp-webhook',
  events: ['message.received', 'message.delivered', 'message.read'],
})
// Save webhook.secret — use it to verify HMAC signatures on every event.
// Treat it as opaque; do NOT strip prefixes or decode it.

Receiving Webhooks (handler contract — read carefully)

Most webhook integration bugs come from violating one of the rules below. Follow them exactly.

Headers Gavi sends with every event

  • X-GaviVentures-Signature — HMAC-SHA256 hex digest of the raw body
  • X-GaviVentures-Timestamp — Unix timestamp (seconds). Advisory only — it is NOT part of the HMAC payload.

Event payload shape (top-level fields, NOT nested under a data key)

{
  "event": "message.received",
  "timestamp": "2026-04-16T22:51:33.273Z",
  "message_id": "wamid.HBgM...",
  "from": "91987654321",
  "type": "text",
  "text": "your plain message body here"
}

Field notes:

  • from is the phone number without the + (e.g. 91987654321). Prepend + when you reply: +91987654321.
  • text is a plain string at the top level — NOT { body: "..." }. (That nested shape is the Meta Cloud API format; Gavi normalizes it for you.)
  • event values: message.received, message.sent, message.delivered, message.read, message.failed.
  • For non-message.received events, fields differ: to, status, message_id are present; from and text may be absent.

HMAC verification rules

  1. Use the secret exactly as Gavi gave it to you. If your secret looks like whsec_abc123..., pass the whole string as the HMAC key. Do not strip the whsec_ prefix. Do not base64-decode. Do not hex-decode. Pass it as a UTF-8 string, byte-for-byte as Gavi returned it.
  2. Hash the raw bytes of the request body, NOT a re-stringified version of the parsed JSON. In Express, capture the raw body via the verify callback in express.json(). In Next.js, call req.text() before any parsing.
  3. The HMAC payload is the raw body only. Do not prefix it with the timestamp. (Stripe uses ${timestamp}.${body}; Gavi does NOT.)

Express handler (works out of the box)

import express from 'express'
import { createHmac, timingSafeEqual } from 'crypto'

const app = express()

app.use(
  express.json({
    verify: (req, _res, buf) => {
      // Capture the raw body BEFORE JSON parsing — required for HMAC.
      ;(req as any).rawBody = buf.toString('utf8')
    },
  })
)

app.post('/webhook', (req, res) => {
  const received = req.headers['x-gaviventures-signature'] as string
  const computed = createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update((req as any).rawBody)
    .digest('hex')

  const sigOk =
    typeof received === 'string' &&
    received.length === computed.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(computed))

  if (!sigOk) return res.sendStatus(401)

  const event = req.body
  if (event.event === 'message.received') {
    const from = '+' + event.from   // prepend + for replies
    const text = event.text         // plain string
    console.log(`Received from ${from}: ${text}`)
  }

  return res.sendStatus(200)
})

app.listen(3001)

Next.js App Router handler

// app/api/whatsapp-webhook/route.ts
import { createHmac, timingSafeEqual } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
  const rawBody = await req.text() // raw bytes BEFORE parsing
  const received = req.headers.get('x-gaviventures-signature') ?? ''
  const computed = createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex')

  const sigOk =
    received.length === computed.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(computed))

  if (!sigOk) {
    return new NextResponse('invalid signature', { status: 401 })
  }

  const event = JSON.parse(rawBody)
  if (event.event === 'message.received') {
    const from = '+' + event.from
    const text = event.text
    console.log(`Received from ${from}: ${text}`)
  }

  return NextResponse.json({ ok: true })
}

SDK helper

import { verifyWebhookSignature } from '@gaviwhatsapp/whatsapp'

const ok = verifyWebhookSignature(rawBody, signature, secret)

REST API (direct)

If not using the SDK, call the API directly:

MethodEndpointDescription
POST/api/v1/messages/sendSend text message
POST/api/v1/messages/templateSend template message
POST/api/v1/messages/mediaSend media message
GET/api/v1/messages?phone=+91...Get message history
GET/api/v1/templatesList templates
POST/api/v1/broadcastsSend broadcast
GET/api/v1/webhooksList webhooks
POST/api/v1/webhooksRegister webhook

All requests require Authorization: Bearer <api_key> header.

Base URL: https://www.gaviventures.com

Common Patterns

Next.js API Route (order notification)

// app/api/orders/route.ts
import { WhatsApp } from '@gaviwhatsapp/whatsapp'

const wa = new WhatsApp({ apiKey: process.env.GAVIVENTURES_API_KEY! })

export async function POST(req: Request) {
  const order = await req.json()

  await wa.sendTemplate({
    to: order.customerPhone,
    template: 'order_confirmation',
    language: 'en',
    variables: { '1': order.id, '2': order.total },
  })

  return Response.json({ success: true })
}

Note: a complete webhook handler with HMAC verification is in the Receiving Webhooks section above. Always include signature verification in production — never trust unverified webhook bodies.

Important Notes

  • Phone numbers must include country code (e.g., +919876543210)
  • Template messages must be approved by Meta before sending
  • You need a connected WhatsApp Business Account (set up in the dashboard)
  • Message costs are billed directly by Meta to the user's Business Account. Meta's pricing changes frequently — see the current rates at https://business.whatsapp.com/products/platform-pricing

Other skills

Ready to send your first message?

Get an API key and have Cursor send a WhatsApp message in under 5 minutes.