zavudev/zavu-skills

send-message

Send messages via SMS, WhatsApp, Email, Telegram, Instagram, Messenger, or Voice with channel selection logic.

First seen Mar 10, 2026

Installation

$ npx skills add zavudev/zavu-skills --skill send-message

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from zavudev/zavu-skills · top by installs.

npx skills add zavudev/zavu-skills

Browse all from zavudev/zavu-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1
License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 16,253 B
  • docs SUMMARY.md 130 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 322 installs

SKILL.md

Send Message

When to Use

Use this skill when building code to send messages through the Zavu API. Covers channel selection, the recipient (to) formats, message types, and error handling.

Channels

auto, sms, sms_oneway, whatsapp, telegram, email, instagram, messenger, voice.

Senders vs accounts (one paragraph)

A Sender is the API handle you pass as Zavu-Sender; accounts (a WhatsApp Business Account, a Facebook Page, a Telegram bot, a phone number) are the connections it routes — and what bills. Senders are free. Connecting an account in the dashboard auto-creates its sender; find it with GET /v1/senders and trust its channels array for what it can send. See the channel-setup skill for the full model.

Channel Selection Decision Tree

Is recipient an email address?
  -> YES: channel = "email" (the sender needs an email channel: a verified domain)
Is message type non-text (image, video, buttons, list, template, etc.)?
  -> YES: channel = "whatsapp" (auto-selected)
Need voice call / TTS?
  -> YES: channel = "voice"
Recipient is a numeric chat ID (Instagram / Messenger / Telegram)?
  -> YES: channel = "instagram" | "messenger" | "telegram"
Need one-way SMS (no inbound replies)?
  -> YES: channel = "sms_oneway"
Need guaranteed delivery to a specific channel?
  -> YES: channel = "sms" | "whatsapp" | "telegram" | "instagram" | "messenger"
Want cost-optimized routing?
  -> YES: channel = "auto" (ML-powered smart routing)
Default?
  -> channel = "sms" (or omit for default)

Recipient (to) formats

The universal to field accepts several identifier formats. Routing follows channel (or is auto-selected from the identifier when channel is omitted).

Format Example Notes
E.164 phone +14155551234 SMS, WhatsApp, Voice, Telegram.
Email address [email protected] Defaults to email.
WhatsApp BSUID US.13491208655302741918 Business-scoped user ID. Routed to WhatsApp; use to message a contact who adopted a username and hid their phone number.
Numeric chat ID 123456789 Telegram, Instagram, or Messenger chat/user ID.

Basic Messages

SMS (default)

TypeScript:

const result = await zavu.messages.send({
  to: "+14155551234",
  text: "Your verification code is 123456",
});
console.log(result.message.id);

Python:

result = zavu.messages.send(
    to="+14155551234",
    text="Your verification code is 123456",
)
print(result.message.id)

Go:

result, err := client.Messages.Send(context.TODO(), zavudev.MessageSendParams{
    To:   zavudev.String("+14155551234"),
    Text: zavudev.String("Your verification code is 123456"),
})
fmt.Println(result.Message.ID)

Ruby:

result = client.messages.send(to: "+14155551234", text: "Your verification code is 123456")
puts result.message.id

PHP:

$result = $client->messages->send([
    'to' => '+14155551234',
    'text' => 'Your verification code is 123456',
]);
echo $result->message->id;

WhatsApp Text

const result = await zavu.messages.send({
  to: "+14155551234",
  channel: "whatsapp",
  text: "Hello from Zavu!",
});

Email

const result = await zavu.messages.send({
  to: "[email protected]",
  channel: "email",
  subject: "Your order has shipped",
  text: "Hi John, your order #12345 has shipped.",
  htmlBody: "<h1>Order Shipped</h1><p>Your order #12345 has shipped.</p>",
  replyTo: "[email protected]",
});

Voice (Text-to-Speech)

const result = await zavu.messages.send({
  to: "+14155551234",
  channel: "voice",
  text: "Your verification code is 1 2 3 4 5 6",
  voiceLanguage: "en-US", // optional, auto-detected from country code
});

One-Way SMS

Use sms_oneway when replies are not expected (no inbound path back to you).

await zavu.messages.send({
  to: "+14155551234",
  channel: "sms_oneway",
  text: "Your appointment is confirmed for 3pm.",
});

Instagram / Messenger

Target a numeric chat/user ID (from an inbound conversation).

// Instagram Direct
await zavu.messages.send({
  to: "17841400000000000",
  channel: "instagram",
  text: "Hello from Zavu via Instagram!",
});

// Messenger (Facebook Page / Marketplace chat)
await zavu.messages.send({
  to: "24025631120151183",
  channel: "messenger",
  text: "Hello from Zavu via Messenger!",
});

WhatsApp Rich Messages

Image

await zavu.messages.send({
  to: "+14155551234",
  messageType: "image",
  text: "Check out this product!", // caption
  content: { mediaUrl: "https://example.com/image.jpg" },
});

Document

await zavu.messages.send({
  to: "+14155551234",
  messageType: "document",
  content: {
    mediaUrl: "https://example.com/invoice.pdf",
    filename: "invoice.pdf",
  },
});

Video / Audio

// Video
await zavu.messages.send({
  to: "+14155551234",
  messageType: "video",
  text: "Watch this!",
  content: { mediaUrl: "https://example.com/video.mp4" },
});

// Audio
await zavu.messages.send({
  to: "+14155551234",
  messageType: "audio",
  content: { mediaUrl: "https://example.com/audio.mp3" },
});

Sticker

await zavu.messages.send({
  to: "+14155551234",
  messageType: "sticker",
  content: { mediaUrl: "https://example.com/sticker.webp" },
});

Location

await zavu.messages.send({
  to: "+14155551234",
  messageType: "location",
  content: {
    latitude: 37.7749,
    longitude: -122.4194,
    locationName: "San Francisco",
    locationAddress: "123 Main St, San Francisco, CA",
  },
});

Contact Card

await zavu.messages.send({
  to: "+14155551234",
  messageType: "contact",
  content: {
    contacts: [
      { name: "John Doe", phones: ["+14155551234", "+14155555678"] },
    ],
  },
});

Interactive Buttons (max 3)

await zavu.messages.send({
  to: "+14155551234",
  messageType: "buttons",
  text: "How would you rate your experience?",
  content: {
    buttons: [
      { id: "great", title: "Great!" },
      { id: "okay", title: "It was okay" },
      { id: "poor", title: "Not good" },
    ],
  },
});

List Message

await zavu.messages.send({
  to: "+14155551234",
  messageType: "list",
  text: "Select an option:",
  content: {
    listButton: "View Options",
    sections: [{
      title: "Products",
      rows: [
        { id: "prod_1", title: "Product A", description: "$10.00" },
        { id: "prod_2", title: "Product B", description: "$20.00" },
      ],
    }],
  },
});

Template Message

templateVariables fill body placeholders (keyed by position 1, 2, ... for positional templates, or by name for named ones — do not mix). templateButtonVariables fill dynamic URL/OTP button placeholders (keyed by button index 0, 1, 2). templateHeaderVariables set a text-header variable (keyed by 1).

await zavu.messages.send({
  to: "+14155551234",
  messageType: "template",
  content: {
    templateId: "tpl_abc123",
    templateVariables: { "1": "John", "2": "ORD-12345" },
    templateButtonVariables: { "0": "abc-report-token" }, // optional: dynamic URL/OTP buttons
  },
});

CTA URL (Call-to-Action button)

await zavu.messages.send({
  to: "+14155551234",
  messageType: "cta_url",
  text: "Check out our latest collection",
  content: {
    ctaDisplayText: "View Products",          // max 20 chars
    ctaUrl: "https://example.com/products",
    ctaHeaderType: "image",                    // optional: text | image | video | document
    ctaHeaderMediaUrl: "https://example.com/header.jpg",
    footerText: "Limited time offer",          // optional, max 60 chars
  },
});

Location Request (ask the contact to share their location)

Sends a message with a fixed "Send location" button. WhatsApp-only. Takes no content object — the prompt goes in text (max 1024 chars).

Not yet generated in the SDK — use REST:

curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer $ZAVU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "channel": "whatsapp",
    "messageType": "location_request",
    "text": "To finish your order, share the delivery address."
  }'

The answer arrives as a normal inbound location message (not a new type) with content.replyToMessageId set to the ID of the request. Match on that field to correlate the coordinates with the order/ticket you asked about:

if (
  event.type === "message.inbound" &&
  event.data.messageType === "location" &&
  event.data.content?.replyToMessageId === pendingRequestId
) {
  const { latitude, longitude } = event.data.content;
}

content.name and content.address are optional — present only when the contact picks a saved place instead of dropping a pin. Always rely on lat/lng.

Contact Info Request (ask the contact to share their phone number)

Sends a message with a fixed "Share Contact Info" button. WhatsApp-only. Takes no content object — the prompt goes in text (max 1024 chars). Essential for contacts who adopted a WhatsApp username and are only known by BSUID.

Not yet generated in the SDK — use REST:

curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer $ZAVU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "US.13491208655302741918",
    "channel": "whatsapp",
    "messageType": "request_contact_info",
    "text": "Share your phone number so our team can call you back."
  }'

The answer arrives as a normal inbound contact message with the shared number in content.contacts[0].phones. When the sender was only known by BSUID, Zavu links the shared phone to that contact automatically — no manual update needed.

Note: authentication templates with one-tap/zero-tap/copy-code buttons cannot be sent to a BSUID (WhatsApp error 131062) — recover the phone number first. Broadcasts also reject BSUID/@username recipients.

Reaction

await zavu.messages.react({
  messageId: "msg_abc123",
  emoji: "\ud83d\udc4d",
});

Typing Indicator

Mark an inbound WhatsApp message as read and show a typing indicator while you prepare a reply (POST /v1/messages/{messageId}/typing). It clears automatically when you send a reply or after 25 seconds. Only valid for inbound WhatsApp messages. Use it when a reply takes more than a couple of seconds (LLM agent, tool call, lookup).

curl -X POST https://api.zavu.dev/v1/messages/MESSAGE_ID/typing \
  -H "Authorization: Bearer $ZAVU_API_KEY" \
  -H "Zavu-Sender: sender_12345"

Sender Override

await zavu.messages.send({
  to: "+14155551234",
  text: "Hello!",
  'Zavu-Sender': "snd_abc123",
});

Idempotency

await zavu.messages.send({
  to: "+14155551234",
  text: "Payment confirmed",
  idempotencyKey: "payment_confirm_order_123",
});

Disable Automatic Fallback

By default, WhatsApp messages auto-fallback to SMS on failure. To disable:

await zavu.messages.send({
  to: "+14155551234",
  channel: "whatsapp",
  text: "WhatsApp only — no SMS fallback",
  fallbackEnabled: false,
});

Get Status & List Messages

// Get single message
const msg = await zavu.messages.get({ messageId: "msg_abc123" });
console.log(msg.message.status); // queued | sending | sent | delivered | read | failed

// List with filters + pagination
let cursor: string | undefined;
do {
  const result = await zavu.messages.list({ status: "delivered", limit: 50, cursor });
  for (const message of result.items) {
    console.log(message.id, message.status);
  }
  cursor = result.nextCursor ?? undefined;
} while (cursor);

Common Errors

Error Code Meaning Fix
whatsappwindowclosed 24h window not open Use template message instead
a2plimitexceeded Free plan monthly allowance reached: WhatsApp, Telegram, Instagram and Messenger share 2,000 messages/month. Separate from the daily ceiling below, which never prevents reaching this monthly figure Upgrade to a paid plan (no caps) or wait for the monthly reset on the 1st
insufficient_balance HTTP 402: prepaid balance cannot cover the send. Email is billed from balance in 1,000-message blocks ($0.40/1k transactional, $0.80/1k marketing); SMS and voice are billed per message Add funds from the dashboard, then retry
urlnotverified Message has unverified URLs Submit URLs via /v1/urls first
urlshortenerblocked URL shortener detected Use full destination URL
destinationnotverified HTTP 403: the account has verified nothing yet, so sms, sms_oneway and voice reach only the phone numbers the project has verified. details.verifiedNumbers lists them. Same code on POST /v1/calls Send to a verified number, verify the number you meant (see below), or verify identity / add a payment method / settle a deposit / subscribe to reach any destination
dailylimitexceeded HTTP 429: the channel group's daily ceiling, details.limit says which. Verified nothing: 25/day across sms + sms_oneway, 5/day voice, 100/day across WhatsApp + Telegram + Instagram + Messenger. Past that floor: 200/day SMS, or 10,000 once identity or business verification is approved; on Free, 50/day voice and 250/day conversational. Paid plans have no voice or conversational ceiling. Email: the plan quota (100/day on Free). Counts reset at 00:00 UTC Wait for the reset, or verify identity / add a payment method to raise it (upgrade the plan for email)
EMAILINVALIDRECIPIENT Malformed email address (async, on the failed message) Fix the address; pre-check lists with POST /v1/introspect/email
EMAILDOMAINNOT_FOUND Recipient domain has no MX or A records (async) Remove the address; the send would hard bounce
EMAILRECIPIENTSUPPRESSED Address bounced or complained before (async) Remove it from your lists

How a number gets verified, and what lifts the restriction. A new account that has proven nothing reaches only its verified numbers on sms, sms_oneway and voice; every other channel is open from the start. The developer verifies a number from the dashboard's Sandbox screen: generate a code, open the WhatsApp link (or scan the QR) on the phone to verify, and send the pre-filled VERIFY- message to Zavu's sandbox number. The number is marked verified automatically; one verification covers WhatsApp, SMS and calls; up to 5 numbers per project; a code expires after 10 minutes. There is no API for this step. Any one of identity verification (KYC), a saved payment method, a settled deposit, or a paid plan opens every destination. Business verification (KYB) never gates sending; it gates 10DLC registration. Email has no verification gate: a sender with a verified domain sends from day one within the plan quota (100/day and 3,000/month on Free). Reference: https://docs.zavu.dev/concepts/sending-limits

Email sends are pre-validated automatically at dispatch: guaranteed hard bounces (bad syntax, dead domain, suppressed address) are failed with the codes above instead of being sent, so they never hurt your bounce rate. These surface asynchronously on the message (status: "failed" + errorCode) and in the message.failed webhook. Advisory signals (role addresses like info@, disposable domains) never block a send — check them upfront with POST /v1/introspect/email.

Constraints

  • Button titles: max 20 chars, max 3 buttons
  • List row titles: max 24 chars, descriptions: max 72 chars, max 10 rows per section
  • Location request body: max 1024 chars, no content object, WhatsApp only
  • Email subject: max 998 chars
  • Voice language codes: en-US, es-ES, pt-BR, etc. (auto-detected if omitted)
  • Media messages auto-select WhatsApp channel