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 401Node.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.