All skills

Claude Code skill

WhatsApp Skill for Claude Code

Agent skill that lets Claude Code send WhatsApp messages, manage templates, run broadcasts, and register webhooks via the Gavi WhatsApp API.

Install

  1. 1. Save the skill content below as~/.claude/skills/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 Claude Code. The agent will now know how to use the Gavi WhatsApp API when you ask it to send messages.

GaviVentures WhatsApp API — Claude Code Instructions

When the user asks to send WhatsApp messages, add WhatsApp notifications, or integrate WhatsApp into their application, follow these instructions.

Canonical documentation

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

Setup

Install the SDK and configure the API key:

npm install @gaviwhatsapp/whatsapp

Environment variable required:

GAVIVENTURES_API_KEY=gv_...

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

SDK Usage

import { WhatsApp } from '@gaviwhatsapp/whatsapp'

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

Send text

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

Send template

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

Send media

await wa.sendMedia({
  to: '+919876543210',
  type: 'image', // or 'document', 'video', 'audio'
  url: 'https://example.com/photo.jpg',
  caption: 'Check this out',
})

List templates

const { templates } = await wa.getTemplates()

Send broadcast

await wa.broadcast({
  recipients: ['+919876543210', '+919876543211'],
  template: 'promo_offer',
  language: 'en',
})

Register webhook

const webhook = await wa.registerWebhook({
  url: 'https://myapp.com/api/whatsapp-webhook',
  events: ['message.received', 'message.delivered'],
})
// Save webhook.secret as-is. Treat it as opaque — do NOT strip prefixes
// (e.g. `whsec_...`) and do NOT base64-decode before using as HMAC key.

Receiving Webhooks (handler contract)

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

Headers Gavi sends:

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

Event payload shape — top-level fields, NOT nested under data:

{
  "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 +. Prepend + when replying.
  • text is a plain string — NOT { body: "..." } (that's the Meta Cloud API shape; Gavi normalizes it).
  • event values: message.received, message.sent, message.delivered, message.read, message.failed.

HMAC verification rules:

  1. Use the secret exactly as Gavi gave it to you. If it's whsec_abc123..., pass the entire string as the HMAC key. Do NOT strip the whsec_ prefix. Do NOT base64-decode.
  2. Hash the raw bytes of the request body, not a re-stringified parsed JSON. Different byte sequences produce different HMACs.
  3. Payload is the raw body only. Do NOT prefix with the timestamp. (Stripe uses ${timestamp}.${body}; Gavi does NOT.)

Express handler:

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

const app = express()

app.use(express.json({
  verify: (req, _res, buf) => {
    (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
    const text = event.text
    console.log(`Received from ${from}: ${text}`)
  }
  return res.sendStatus(200)
})

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()
  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
    console.log(`Received from ${from}: ${event.text}`)
  }
  return NextResponse.json({ ok: true })
}

SDK helper:

import { verifyWebhookSignature } from '@gaviwhatsapp/whatsapp'
const ok = verifyWebhookSignature(rawBody, signature, secret)

REST API (alternative to SDK)

Base URL: https://www.gaviventures.com Auth: Authorization: Bearer <api_key>

MethodEndpointBody
POST/api/v1/messages/send{ to, text }
POST/api/v1/messages/template{ to, template, language?, variables? }
POST/api/v1/messages/media{ to, type, url, caption?, filename? }
GET/api/v1/messages?phone=+91...&limit=20
GET/api/v1/templates?sync=true (optional, pulls from Meta)
POST/api/v1/broadcasts{ recipients, template, language?, variables_per_recipient? }
POST/api/v1/webhooks{ url, events? }

Python

pip install gaviwhatsapp
from gaviwhatsapp import WhatsApp
import os

wa = WhatsApp(api_key=os.environ["GAVIVENTURES_API_KEY"])
wa.send(to="+919876543210", text="Hello from Python!")

Important Rules

  • Phone numbers always include country code: +919876543210
  • Template messages must be pre-approved by Meta
  • User needs a connected WhatsApp Business Account in the dashboard
  • Meta bills message costs directly to the user's Meta Business Account. Meta's pricing changes frequently — see current rates at https://business.whatsapp.com/products/platform-pricing
  • The API key starts with gv_

Other skills

Ready to send your first message?

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