Developers

API quickstart

Send messages, queue campaigns and receive replies from your own software. Everything is JSON over HTTPS. The full machine-readable spec is at /openapi.json.

Authentication

Create an API key in your dashboard under Developer → API keys. Keys start with vr_ and are shown once. Send it on every request:

Authorization: Bearer vr_your_key_here

Replace https://your-domain below with your panel address and SESSION with the name of a linked number, which you’ll find on the Numbers page (for example u3-main).

Phone numbers: a 10-digit Indian number like 9876543210 gets 91 added automatically. You can also send to a group (…@g.us), a channel (…@newsletter) or a saved contact (contact:12).

Send a text message

POST /api/sessions/{session}/send

curl -X POST https://your-domain/api/sessions/SESSION/send \
  -H "Authorization: Bearer $VR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "9876543210",
    "text": "Namaste {{name}}, your OTP is {{code}}",
    "vars": { "name": "Asha", "code": "4821" }
  }'

Response:

{ "ok": true, "message": { "id": "3EB0…", "to": "919876543210@s.whatsapp.net", "timestamp": 1792214875, "new_contact": false } }

Send an image, video or document by URL

The file is fetched into memory, sent, and forgotten. Use type image, video, audio or document; add filename for documents.

curl -X POST https://your-domain/api/sessions/SESSION/send \
  -H "Authorization: Bearer $VR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "9876543210",
    "text": "Your invoice for October",
    "media": { "type": "document", "url": "https://example.com/inv-2041.pdf", "filename": "Invoice-2041.pdf" }
  }'

Queue a campaign

Campaigns go through the safe queue: random gaps, typing, breaks, daily caps, sending hours and STOP handling all apply. Without schedule.at the campaign starts now.

POST /api/campaigns

curl -X POST https://your-domain/api/campaigns \
  -H "Authorization: Bearer $VR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Diwali offer",
    "sessions": ["SESSION"],
    "message": { "text": "{Namaste|Hello} {{name}} ji, 20% off till Friday. Reply YES to book. Reply STOP to opt out." },
    "audience": { "tags": ["vip"], "exclude_tags": ["unpaid"] },
    "schedule": { "at": 1792310400, "repeat": "none" },
    "speed": "safe"
  }'

Track it with GET /api/campaigns/{id}, and pause, resume or cancel with POST /api/campaigns/{id}/pause, /resume or /cancel. A quick one-off list without saved contacts still works through the older POST /api/sessions/{session}/queue endpoint.

Webhooks

Add a webhook URL under Developer → Webhooks and choose events. Each event is a POST like this:

{ "event": "message", "session": "u3-main", "owner_id": 3, "timestamp": 1792214875,
  "data": { "id": "3EB0…", "chat_phone": "919876543210", "push_name": "Asha", "type": "text", "text": "YES" } }

Events include message, message.receipt, poll.vote, queue.status, campaign.status, session.status, session.health, contact.optout and contact.optin. If your endpoint does not return 2xx, delivery is retried a few times with increasing delays.

Verify webhook signatures

Each request carries X-Timestamp and X-Signature: sha256=<hex>, an HMAC-SHA256 of timestamp + "." + raw body using your webhook secret. Reject anything older than five minutes.

PHP

<?php
$raw    = file_get_contents('php://input');
$ts     = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$sig    = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = getenv('VR_WEBHOOK_SECRET');

$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int)$ts) > 300) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// handle $event['event'] …
http_response_code(200);

Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/vr-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-Timestamp') || '';
  const sig = req.get('X-Signature') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.VR_WEBHOOK_SECRET)
    .update(ts + '.' + req.body)
    .digest('hex');
  const ok = sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // handle event.event …
  res.sendStatus(200);
});

Errors

Errors look like {"ok": false, "code": "…", "error": "…"} with a matching HTTP status. Codes worth handling:

  • not_connected (503): the number is offline or reconnecting.
  • not_on_whatsapp (422): the recipient doesn’t use WhatsApp.
  • daily_limit, new_contact_limit, timelocked (429): a safety limit was reached; try later.
  • limit_reached (429), feature_locked (403): your plan doesn’t allow this.
  • unauthorized (401): missing or wrong API key.

Need help integrating? Contact us or grab the OpenAPI spec for Postman or code generators.