Browse docs

API reference (v1)

Authenticate with an API key, call the public /v1 endpoints, and receive webhook events.

Create an API key under Developers. The secret is shown once — store it securely. Authenticate every request with a bearer token.

curl https://api.vaanicall.com/api/v1/agents \
  -H "Authorization: Bearer vk_your_key_here"

Endpoints

GET /v1/agents

List your agents.

GET /v1/calls

Paginated call history. Query params: limit (≤100), offset, agent_id, direction (inbound | outbound).

GET /v1/numbers

List your inbound numbers. Returns e164, status, agent, answeringHours, ivrMenu, and source per number.

GET /v1/inbound/settings

Read your workspace-level inbound call settings (fallback action, max concurrency).

POST /v1/calls/test

Place a test call against an agent’s latest published version. Body: { "agent_id": "…", "to_number": "+91…" }.

curl -X POST https://api.vaanicall.com/api/v1/calls/test \
  -H "Authorization: Bearer vk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"agent_id":"<id>","to_number":"+919876543210"}'

Keys are rate-limited. A revoked key returns 401 immediately; a suspended workspace returns 403.

Webhooks

Register up to 5 HTTPS endpoints under Developers › Webhooks. When a call reaches a terminal state Vaanicall posts a signed call.completed event to each active endpoint. The webhook secret is shown once at creation — store it alongside your API key.

Payload schema (v1, stable)

{
  "event": "call.completed",
  "delivery_id": "<uuid>",
  "occurred_at": "<ISO-8601>",
  "call": {
    "id": "<uuid>",
    "direction": "outbound" | "inbound",
    "status": "completed" | "missed_inbound" | ...,
    "base_taxonomy": "answered" | "no_answer" | ...,
    "disposition_code": "<string | null>",
    "miss_reason": "<string | null>",
    "duration_secs": <number | null>,
    "e164": "<E.164 contact number | null>",
    "from_number": "<string | null>",
    "to_number": "<string | null>",
    "agent_version_id": "<uuid | null>",
    "campaign_id": "<uuid | null>",
    "created_at": "<ISO-8601>",
    "ended_at": "<ISO-8601 | null>"
  }
}

Transcripts and recordings are not included in the payload (size and privacy). To review them, use the Reports section of the dashboard — download a CSV export or open the call detail view. A single-call detail endpoint on the /v1 API is not yet available.

Verifying signatures

Every request carries X-Vaani-Signature: sha256=<hex> — an HMAC-SHA256 of the raw request body using your endpoint secret. Always verify with a constant-time comparison to prevent timing attacks.

Python

import hashlib
import hmac

def verify_signature(raw_body: bytes, secret: str, header: str) -> bool:
    """Return True only if the X-Vaani-Signature header is valid.

    Design ref: docs/superpowers/plans/2026-06-07-public-api-p2.md §Locked design.
    Uses hmac.compare_digest (constant-time) to prevent timing side-channel leaks.
    """
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    # compare_digest requires both operands to be the same type (str here).
    return hmac.compare_digest(expected, header)

# Flask / FastAPI usage example:
# raw = request.get_data()          # raw bytes — do NOT parse JSON first
# sig = request.headers["X-Vaani-Signature"]
# if not verify_signature(raw, YOUR_SECRET, sig):
#     return 401

Node.js

import crypto from "node:crypto";

/**
 * Return true only if the X-Vaani-Signature header is valid.
 *
 * Design ref: docs/superpowers/plans/2026-06-07-public-api-p2.md §Locked design.
 * Uses crypto.timingSafeEqual (constant-time) to prevent timing side-channel leaks.
 */
function verifySignature(
  rawBody: Buffer | string,
  secret: string,
  header: string
): boolean {
  const body = typeof rawBody === "string" ? Buffer.from(rawBody) : rawBody;
  const digest = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  // timingSafeEqual requires equal-length Buffers — pad to same length.
  const a = Buffer.from(digest);
  const b = Buffer.from(header);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// Express usage example:
// app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
//   const sig = req.headers["x-vaani-signature"] as string;
//   if (!verifySignature(req.body, process.env.VAANI_WEBHOOK_SECRET!, sig)) {
//     return res.status(401).send("Invalid signature");
//   }
//   const event = JSON.parse(req.body.toString());
//   // handle event …
//   res.sendStatus(200);
// });

Delivery and retries

Vaanicall expects a 2xx response within 10 seconds. Any other outcome triggers exponential back-off retries:

Attempt 1  → wait  1 min  → Attempt 2
Attempt 2  → wait  5 min  → Attempt 3
Attempt 3  → wait 25 min  → Attempt 4
Attempt 4  → wait  2 h    → Attempt 5
Attempt 5  → dead-lettered (DLQ)

After 5 failed attempts the delivery is dead-lettered. Use Redrive on the delivery log to re-enqueue dead deliveries, or call POST /developers/webhooks/{id}/redrive from the API. The Test button sends a synthetic signed event: "test" payload immediately so you can confirm your endpoint is reachable and verifying signatures correctly.

Note: delivery log retention policy is not yet configurable — all delivery rows are retained indefinitely at this time.