The waca.ai API lets your website, CRM, Shopify app or backend work with your waca.ai workspace: add customers, send WhatsApp messages, have an AI agent call someone, start journeys and get told when things happen.
It is a JSON REST API over HTTPS. Every request uses your API key. Everything you do goes through the same rules as the app — consent, DND, TRAI calling hours, the 24-hour WhatsApp window and your wallet — and shows up in your Audit log as “API key <name>”.
Base URL
https://waca.ai/api/v1
Times are ISO 8601 in UTC (e.g. 2026-10-16T05:30:00.000Z = 11:00 am IST). Amounts are rupees (₹) as numbers. Phone numbers come back in E.164 (+919876543210); you can send Indian numbers as 98765 43210, 09876543210 or +91-98765-43210.
Create keys in Settings → Developers (owners and admins only). Give each key a name (“Shopify sync”), only the scopes it needs, and — for servers with fixed addresses — an IP allow-list.
The full key is shown once. We store only a salted SHA-256 hash, so we can’t show it again: copy it into your server’s secrets. Lost it? Rotate the key (the old value stops working at once) or create a new one.
Live keys look like waca_live_… and work in live workspaces.
Demo-mode workspaces issue waca_test_… keys — everything is simulated, nothing is sent or charged.
Send the key as Authorization: Bearer <key> on every request.
Keys work only on /api/v1. Your login session (cookie) never works on /api/v1, and API keys never work on the app’s own endpoints.
Revoke a key any time; requests with it then get 401 unauthorized.
Scope
Allows
contacts:read
List and look up customers.
contacts:write
Create, update, import and erase customers, and record consent.
messages:read
Read message status and list templates.
messages:send
Send WhatsApp templates and replies, SMS and Instagram replies; read message status; list templates.
calls:read
Read a call’s status and its call report (transcript + recording link).
calls:create
Ask an AI agent to call a customer and read the call status.
Never put a key in a mobile app, browser JavaScript or a public Git repo. Call the API from your server.
Errors
Errors use normal HTTP status codes and always have the same JSON shape:
json
{
"error": {
"code": "forbidden",
"message": "This API key doesn’t have the contacts:write scope.",
"details": {
"required": "contacts:write"
}
}
}
Status
code
When
400
invalid_request
The request is missing a field or a value isn’t valid. message says which.
401
unauthorized
No API key, a wrong key, or a revoked key. Session cookies never work on /api/v1.
402
insufficient_balance
Your wallet can’t cover this (live mode). details.rechargeUrl links to Billing.
403
forbidden
The key lacks the scope this endpoint needs (details.required), or the request came from an IP outside the key’s allow-list.
404
not_found
No such resource in your workspace.
409
conflict
The resource already exists (e.g. a contact with this phone — pass upsert: true).
409
idempotency_conflict
This Idempotency-Key was already used with a different request body.
422
not_contactable
Compliance stopped it: no consent, opted out, DND for promotional traffic, outside calling hours or outside the 24-hour WhatsApp window. details.reason explains.
429
rate_limited
Too many requests for this key. Wait for Retry-After seconds.
500
server_error
Something broke on our side. Retry with the same Idempotency-Key.
Idempotency
Networks fail. To retry a POST safely, send an Idempotency-Key header (any unique string up to 255 characters, e.g. your order id or a UUID). For 24 hours, the same key with the same body returns the first response again (with Idempotent-Replayed: true) instead of sending a second WhatsApp or placing a second call.
Re-using a key with a different body returns 409 idempotency_conflict. Keys are per API key.
Each key can make 600 requests per minute (your plan may differ). Every response tells you where you stand:
Header
Meaning
X-RateLimit-Limit
Requests allowed per minute.
X-RateLimit-Remaining
Left in the current minute.
X-RateLimit-Reset
Unix time (seconds) when the minute resets.
Retry-After
On 429 only: seconds to wait.
Need to add thousands of customers? Use POST /contacts/bulk (1,000 per request) instead of one request each.
Pagination
List endpoints return { data, hasMore, nextCursor }. Pass limit (1–100, default 25) and, for the next page, cursor=<nextCursor>. Stop when hasMore is false.
A customer was added — by your team, an import, a webhook journey or the API.
contact.updated
A customer’s details, tags, custom fields or consent changed.
contact.stage_changed
A lead moved to another pipeline stage — by your team, a journey or the API. Lost includes the reason.
task.due
A follow-up task reached its due time (the assignee is reminded in the app, and on WhatsApp if chosen).
message.received
A customer sent you a message on WhatsApp, SMS, Instagram or website chat.
message.status
A message you sent changed status: sent → delivered → read, or failed.
call.completed
An AI call ended. Includes the outcome, a short summary and a signed 7-day recording link.
journey.completed
A customer reached the end of a journey.
journey.handoff
A journey handed the customer to your team (live transfer, callback or chat assignment).
wallet.low
Your wallet dropped below your alert level (or to ₹0). Recharge to keep calls and campaigns running.
Answer with any 2xx within 10 seconds. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then mark the delivery failed. The last 50 deliveries per endpoint are in Settings → Developers, where you can resend any of them. Events can arrive out of order or more than once — use id / X-Waca-Delivery.
Recording links in call.completed are signed and valid for 7 days. Download the audio if you need to keep it.
Verifying signatures
Always check the signature before trusting a webhook. Compute HMAC-SHA256 of timestamp + "." + rawBody with your endpoint’s secret, compare it to X-Waca-Signature in constant time, and reject timestamps older than 5 minutes (stops replays).
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WACA_WEBHOOK_SECRET; // whsec_…
// Keep the raw body: the signature is over the exact bytes we sent.
app.post("/hooks/waca", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Waca-Timestamp");
const signature = req.get("X-Waca-Signature") || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET)
.update(timestamp + "." + req.body.toString("utf8"))
.digest("hex");
const fresh = Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok || !fresh) return res.status(401).send("bad signature");
const event = JSON.parse(req.body);
if (event.event === "call.completed") console.log(event.data.summary);
res.sendStatus(200); // answer fast; do slow work in a queue
});
Creates a customer and starts any “Customer created” journeys and automations. With upsert: true an existing contact with the same phone is updated instead (200) — otherwise you get 409 conflict.
Body
Field
Type
Description
name
string
required
Full name (max 100).
phone
string
required
Phone number; Indian numbers without +91 are fine.
email
string
Email.
tags
string[]
Tags (max 20, 40 chars each).
fields
object
Custom field values by key.
address
object
{ line1, line2, city, state, pincode, country }.
language
string
e.g. hi-IN, en-IN, mr-IN.
consent
object
{ granted: true, source: "website_form", note } — or just true/false. Recorded in the consent log.
stage
string
Pipeline stage id (GET /crm/stages). Moving to Lost needs lostReason.
lostReason
string
One of your lost reasons (Lost stage only).
ownerId
string
Teammate who owns the customer (member id or owner); null removes.
value
number
Expected deal value in ₹.
source
string
Where the lead came from, e.g. indiamart, website, referral.
upsert
boolean
Update the contact with this phone if it exists.
Request
curl -X POST https://waca.ai/api/v1/contacts \
-H "Authorization: Bearer $WACA_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Priya Sharma","phone":"+91 98765 43210","email":"priya@example.in","tags":["Diwali sale"],"fields":{"city_tier":"Tier 1"},"consent":{"granted":true,"source":"website_form","note":"Ticked the WhatsApp box on checkout"},"upsert":true}'
One row failing doesn’t stop the others. Rows are matched by phone; with upsert: false existing phones are skipped. Journeys are not started for bulk rows.
Irreversibly anonymises the customer under the Digital Personal Data Protection Act, 2023: name, phone, messages, transcripts and recordings are removed; counts are kept. Running journeys stop.
Your sales pipeline: the stages leads move through (set `stage` on a contact) and follow-up tasks for your team. Scopes contacts:read / contacts:write.
The Task object
A follow-up task on a customer (CRM).
Field
Type
Description
id
string
Task id (task_…).
contactId
string
The customer.
title
string
What to do, e.g. Call back about the quote.
dueAt
date-time
When it is due (UTC).
assigneeId
string
Teammate (member id, or "owner").
assigneeName
string
Their name.
status
string · open | done
open or done.
remindVia
string · inapp | whatsapp
In-app reminder only, or also WhatsApp to the assignee.
overdue
boolean
Open and past its due time.
createdAt
date-time
When it was added.
doneAt
date-time
When it was completed, or null.
Task example
{
"id": "task_51c0",
"contactId": "contact_8f2c0d4e",
"title": "Call back about the 2BHK site visit",
"dueAt": "2026-10-17T11:30:00.000Z",
"assigneeId": "member_2c1a",
"assigneeName": "Ravi",
"status": "open",
"remindVia": "whatsapp",
"overdue": false,
"createdAt": "2026-10-17T06:12:00.000Z",
"doneAt": null
}
List pipeline stages
GET/api/v1/crm/stages
Scope contacts:read
In board order. kind is open, won or lost; also returns your lost reasons.
{
"id": "task_51c0",
"contactId": "contact_8f2c0d4e",
"title": "Call back about the 2BHK site visit",
"dueAt": "2026-10-17T11:30:00.000Z",
"assigneeId": "member_2c1a",
"assigneeName": "Ravi",
"status": "open",
"remindVia": "whatsapp",
"overdue": false,
"createdAt": "2026-10-17T06:12:00.000Z",
"doneAt": null
}
Messages
Send WhatsApp templates (any time, to opted-in customers) or free text inside the 24-hour window after the customer’s last message. Same compliance and billing rules as the app.
True when sent in demo mode (nothing really left).
createdAt
date-time
When it was sent.
statusAt
date-time
When the status last changed, or null.
Message example
{
"id": "msg_7d01b3aa",
"conversationId": "chat_42b1",
"contactId": "contact_8f2c0d4e",
"channel": "whatsapp",
"status": "delivered",
"text": "Hi Priya, your order A1023 is out for delivery today. Total ₹2,499 (COD).",
"templateId": "template_5a9c",
"error": null,
"demo": false,
"createdAt": "2026-10-16T05:10:00.000Z",
"statusAt": "2026-10-16T05:10:03.000Z"
}
Send a message
POST/api/v1/messages
Scope messages:sendSupports Idempotency-Key
Give templateId (or templateName) with variables for a WhatsApp template, or text for a free-text reply. Free text needs an open 24-hour window; a template to a customer outside the window needs consent (marketing templates also respect opt-outs).
Body
Field
Type
Description
contactId
string
Existing contact id. Give this or phone.
phone
string
Customer phone — +91 98765 43210, 9876543210 or E.164. Give this or contactId.
channel
string · whatsapp | sms | instagram
Default whatsapp.
templateId
string
Approved template id (GET /templates).
templateName
string
Or the template’s Meta name.
variables
object
Body values. Numbered templates: an array in order (["Priya", "A1023"] fills {{1}}, {{2}}) or an object ({"1": "Priya"}). Named templates: an object by name ({"customer_name": "Priya", "order_id": "A1023"}). A value you leave out comes from the variable mapping / the customer’s profile ({{1}} or {{customer_name}} → the customer’s name); if a required value is still empty nothing is sent and you get 400 invalid_request with missing.
header.text
string
Header value: the text header’s variable (max 60 characters).
header.mediaUrl
string
Header value: a public https image, video or PDF link for a media header. Without it the approved sample media is used.
buttons
object
Button values by button index (0-based, as GET /templates lists them): the dynamic end of a website button’s link, or a coupon code. {"1": "A1023"} or [{ "index": 1, "value": "A1023" }].
Why the call didn’t go out (status skipped), else null.
demo
boolean
True for simulated demo-mode calls.
startedAt
date-time
When the call was created.
endedAt
date-time
When it ended, or null.
Call example
{
"id": "call_51e0c9b2",
"status": "completed",
"direction": "outbound",
"contactId": "contact_8f2c0d4e",
"agentId": "agent_3b7d",
"durationSec": 94,
"outcome": {
"label": "Interested",
"reason": "Customer asked for a site visit on Saturday"
},
"summary": "Wants a 2BHK site visit in Wakad this Saturday, 11 am.",
"skipReason": null,
"demo": false,
"startedAt": "2026-10-16T05:59:30.000Z",
"endedAt": "2026-10-16T06:01:10.000Z"
}
Place an AI call
POST/api/v1/calls
Scope calls:createSupports Idempotency-Key
Returns at once with the call id; follow progress with GET /calls/{id} or the call.completed webhook. A new phone number becomes a contact.
Body
Field
Type
Description
contactId
string
Existing contact id. Give this or phone.
phone
string
Customer phone — +91 98765 43210, 9876543210 or E.164. Give this or contactId.
name
string
Name for a new contact.
agentId
string
Which agent calls (default: your default calling agent).
goal
string
Why you’re calling, in plain words — the agent opens with it (max 500).
variables
object
Facts the agent can use, e.g. { orderId, amount }.
purpose
string · transactional | promotional
Default transactional. Promotional calls need consent, skip DND numbers and run 10:00–19:00 IST only.
Request
curl -X POST https://waca.ai/api/v1/calls \
-H "Authorization: Bearer $WACA_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"phone":"9876543210","agentId":"agent_3b7d","goal":"Confirm the COD order A1023 of ₹2,499 and the delivery address in Wakad.","variables":{"orderId":"A1023","amount":"₹2,499"}}'