API reference · v1.0.0

Build on waca.ai

Add customers, send WhatsApp messages, have an AI agent call someone and hear back by webhook — from your own website, CRM or backend.

https://waca.ai/api/v1

Overview

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.

Your first call
curl https://waca.ai/api/v1/me \
  -H "Authorization: Bearer waca_live_XXXXXXXXXXXXXXXXXXXXXXXX"

Authentication & keys

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.
ScopeAllows
contacts:readList and look up customers.
contacts:writeCreate, update, import and erase customers, and record consent.
messages:readRead message status and list templates.
messages:sendSend WhatsApp templates and replies, SMS and Instagram replies; read message status; list templates.
calls:readRead a call’s status and its call report (transcript + recording link).
calls:createAsk an AI agent to call a customer and read the call status.
journeys:readList journeys.
journeys:startList journeys and start one for a customer.
reports:readCall reports (transcript + recording link), wallet balance, journeys and templates.
webhooks:manageAdd, test and remove webhook endpoints.

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"
    }
  }
}
StatuscodeWhen
400invalid_requestThe request is missing a field or a value isn’t valid. message says which.
401unauthorizedNo API key, a wrong key, or a revoked key. Session cookies never work on /api/v1.
402insufficient_balanceYour wallet can’t cover this (live mode). details.rechargeUrl links to Billing.
403forbiddenThe key lacks the scope this endpoint needs (details.required), or the request came from an IP outside the key’s allow-list.
404not_foundNo such resource in your workspace.
409conflictThe resource already exists (e.g. a contact with this phone — pass upsert: true).
409idempotency_conflictThis Idempotency-Key was already used with a different request body.
422not_contactableCompliance stopped it: no consent, opted out, DND for promotional traffic, outside calling hours or outside the 24-hour WhatsApp window. details.reason explains.
429rate_limitedToo many requests for this key. Wait for Retry-After seconds.
500server_errorSomething 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.

bash
curl -X POST https://waca.ai/api/v1/messages \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Idempotency-Key: order-A1023-out-for-delivery" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+919876543210","templateName":"order_out_for_delivery","variables":["Priya","A1023","₹2,499"]}'

Rate limits

Each key can make 600 requests per minute (your plan may differ). Every response tells you where you stand:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute.
X-RateLimit-RemainingLeft in the current minute.
X-RateLimit-ResetUnix time (seconds) when the minute resets.
Retry-AfterOn 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.

bash
curl "https://waca.ai/api/v1/contacts?tag=Diwali%20sale&limit=100" -H "Authorization: Bearer $WACA_KEY"
# → { "data": [...], "hasMore": true, "nextCursor": "eyJvIjoxMDB9" }
curl "https://waca.ai/api/v1/contacts?tag=Diwali%20sale&limit=100&cursor=eyJvIjoxMDB9" -H "Authorization: Bearer $WACA_KEY"

Webhooks

Add an HTTPS endpoint in Settings → Developers (or POST /webhooks) and pick events. We POST JSON to it within seconds of the event:

json
{
  "id": "evt_3a0f9c1d",
  "event": "call.completed",
  "createdAt": "2026-10-16T06:01:12.000Z",
  "businessId": "biz_1f0c",
  "data": {
    "callId": "call_51e0c9b2",
    "status": "completed",
    "direction": "outbound",
    "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.",
    "recordingUrl": "https://waca.ai/api/public/telephony/call-report/r1.eyJiIjoi…/audio",
    "reportUrl": "https://waca.ai/api/public/telephony/call-report/r1.eyJiIjoi…",
    "contact": {
      "id": "contact_8f2c0d4e",
      "name": "Priya Sharma",
      "phone": "+919876543210"
    },
    "agentId": "agent_3b7d",
    "endedAt": "2026-10-16T06:01:10.000Z"
  }
}
HeaderValue
X-Waca-EventThe event, e.g. call.completed
X-Waca-DeliveryUnique per delivery — use it to ignore duplicates
X-Waca-TimestampMilliseconds since 1970 when this attempt was signed
X-Waca-Signaturesha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>
EventWhen
contact.createdA customer was added — by your team, an import, a webhook journey or the API.
contact.updatedA customer’s details, tags, custom fields or consent changed.
contact.stage_changedA lead moved to another pipeline stage — by your team, a journey or the API. Lost includes the reason.
task.dueA follow-up task reached its due time (the assignee is reminded in the app, and on WhatsApp if chosen).
message.receivedA customer sent you a message on WhatsApp, SMS, Instagram or website chat.
message.statusA message you sent changed status: sent → delivered → read, or failed.
call.completedAn AI call ended. Includes the outcome, a short summary and a signed 7-day recording link.
journey.completedA customer reached the end of a journey.
journey.handoffA journey handed the customer to your team (live transfer, callback or chat assignment).
wallet.lowYour 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
});
PHP
<?php
$secret    = getenv('WACA_WEBHOOK_SECRET'); // whsec_…
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WACA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WACA_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$fresh    = abs(round(microtime(true) * 1000) - (int) $timestamp) < 300000;

if (!hash_equals($expected, $signature) || !$fresh) {
    http_response_code(401);
    exit('bad signature');
}
$event = json_decode($body, true);
// e.g. $event['event'] === 'message.received'
http_response_code(200);
Python (Flask)
import hashlib, hmac, json, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["WACA_WEBHOOK_SECRET"].encode()  # whsec_…

@app.post("/hooks/waca")
def waca():
    body = request.get_data()  # raw bytes
    timestamp = request.headers.get("X-Waca-Timestamp", "")
    signature = request.headers.get("X-Waca-Signature", "")
    expected = "sha256=" + hmac.new(SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() * 1000 - int(timestamp or 0)) < 300000
    if not hmac.compare_digest(expected, signature) or not fresh:
        abort(401)
    event = json.loads(body)
    return "", 200

Testing in demo mode

A demo workspace gives you waca_test_ keys with exactly the same API. Nothing real happens:

  • Messages are recorded as sent (status sent) — no WhatsApp leaves.
  • Calls are simulated: the call goes queued → in_progress → completed in about 10 seconds with a sample transcript, then call.completed fires.
  • Your wallet is never charged or blocked.
  • Webhooks are really delivered, so you can test your receiver. In demo mode http://localhost URLs are allowed too; live endpoints must be https.
  • Use POST /webhooks/{id}/test to send a signed sample event.

When you go live, create a waca_live_ key in your live workspace. No code changes are needed.

Your key

Check that your key works and see what it can do — a good first call.

Who am I

GET/api/v1/me

Any key

Returns the workspace and the key’s name, scopes and mode. Needs no scope.

Request
curl https://waca.ai/api/v1/me \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "business": {
    "id": "biz_1f0c",
    "name": "Sunrise Homes"
  },
  "key": {
    "id": "key_9a1b",
    "name": "CRM sync",
    "scopes": [
      "contacts:read",
      "contacts:write"
    ],
    "mode": "live"
  },
  "rateLimit": {
    "limit": 600,
    "windowSec": 60
  }
}

Contacts

Your customers. Phone numbers are stored in E.164 (+91…). Consent changes are written to the consent log with the source you give.

The Contact object

A customer in your workspace.

FieldTypeDescription
idstringContact id, e.g. contact_8f2c0d4e…
namestringFull name.
phonestringE.164 phone number, e.g. +919876543210.
emailstringEmail, or empty.
tagsstring[]Up to 20 tags.
consentobject{ granted, at, optedOutAt } — WhatsApp/marketing consent.
fieldsobjectYour custom fields (Settings → Customer fields), by key.
addressobject{ line1, line2, city, state, pincode, country }.
languagestringPreferred language code, e.g. hi-IN.
sourcestringWhere the lead came from: whatsapp, instagram, web, call, csv_import, api, shopify, gmail, indiamart, linkedin, exotel_missed_call, manual…
stagestringPipeline stage id (GET /crm/stages), e.g. new, contacted, qualified, won, lost.
lostReasonstringWhy the lead was lost (only in the Lost stage), or null.
ownerIdstringTeammate who owns this customer (member id, or "owner"), or null.
valuenumberExpected deal value in ₹, or null.
wonValuenumberFinal deal value in ₹ once won, or null.
warmthintegerEngagement score 0–100 (replies, calls, orders, recency), or null before the first score.
firstSeenAtdate-timeFirst time this customer reached you.
lastActivityAtdate-timeLatest message, call, order or team note, or null.
createdAtdate-timeWhen the contact was created (UTC).
updatedAtdate-timeLast change (UTC), or null.
Contact example
{
  "id": "contact_8f2c0d4e",
  "name": "Priya Sharma",
  "phone": "+919876543210",
  "email": "priya@example.in",
  "tags": [
    "Diwali sale"
  ],
  "consent": {
    "granted": true,
    "at": "2026-10-16T04:30:00.000Z",
    "optedOutAt": null
  },
  "fields": {
    "city_tier": "Tier 1",
    "last_order_value": "2499"
  },
  "address": {
    "city": "Pune",
    "state": "Maharashtra",
    "pincode": "411057"
  },
  "language": "hi-IN",
  "source": "api",
  "stage": "qualified",
  "lostReason": null,
  "ownerId": "member_2c1a",
  "value": 85000,
  "wonValue": null,
  "warmth": 64,
  "firstSeenAt": "2026-10-16T04:30:00.000Z",
  "lastActivityAt": "2026-10-17T06:10:00.000Z",
  "createdAt": "2026-10-16T04:30:00.000Z",
  "updatedAt": null
}

List contacts

GET/api/v1/contacts

Scope contacts:read

Newest first. Filter by phone, email, tag or change time. Erased customers are never returned.

Parameters

NameInTypeDescription
phonequerystringExact phone (any Indian format).
emailquerystringExact email (case-insensitive).
tagquerystringHas this tag.
stagequerystringIn this pipeline stage (id from GET /crm/stages).
owner_idquerystringOwned by this teammate (member id or owner).
updated_sincequerydate-timeCreated or changed at/after this time, e.g. 2026-10-01T00:00:00+05:30.
limitqueryinteger1–100, default 25.
cursorquerystringnextCursor from the previous page.
Request
curl https://waca.ai/api/v1/contacts \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "data": [
    {
      "id": "contact_8f2c0d4e",
      "name": "Priya Sharma",
      "phone": "+919876543210",
      "email": "priya@example.in",
      "tags": [
        "Diwali sale"
      ],
      "consent": {
        "granted": true,
        "at": "2026-10-16T04:30:00.000Z",
        "optedOutAt": null
      },
      "fields": {
        "city_tier": "Tier 1",
        "last_order_value": "2499"
      },
      "address": {
        "city": "Pune",
        "state": "Maharashtra",
        "pincode": "411057"
      },
      "language": "hi-IN",
      "source": "api",
      "stage": "qualified",
      "lostReason": null,
      "ownerId": "member_2c1a",
      "value": 85000,
      "wonValue": null,
      "warmth": 64,
      "firstSeenAt": "2026-10-16T04:30:00.000Z",
      "lastActivityAt": "2026-10-17T06:10:00.000Z",
      "createdAt": "2026-10-16T04:30:00.000Z",
      "updatedAt": null
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Get a contact

GET/api/v1/contacts/{id}

Scope contacts:read

Parameters

NameInTypeDescription
idpathstringRequired. The contact id.
Request
curl https://waca.ai/api/v1/contacts/contact_8f2c0d4e \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "id": "contact_8f2c0d4e",
  "name": "Priya Sharma",
  "phone": "+919876543210",
  "email": "priya@example.in",
  "tags": [
    "Diwali sale"
  ],
  "consent": {
    "granted": true,
    "at": "2026-10-16T04:30:00.000Z",
    "optedOutAt": null
  },
  "fields": {
    "city_tier": "Tier 1",
    "last_order_value": "2499"
  },
  "address": {
    "city": "Pune",
    "state": "Maharashtra",
    "pincode": "411057"
  },
  "language": "hi-IN",
  "source": "api",
  "stage": "qualified",
  "lostReason": null,
  "ownerId": "member_2c1a",
  "value": 85000,
  "wonValue": null,
  "warmth": 64,
  "firstSeenAt": "2026-10-16T04:30:00.000Z",
  "lastActivityAt": "2026-10-17T06:10:00.000Z",
  "createdAt": "2026-10-16T04:30:00.000Z",
  "updatedAt": null
}

Create (or upsert) a contact

POST/api/v1/contacts

Scope contacts:writeSupports Idempotency-Key

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

FieldTypeDescription
namestringrequiredFull name (max 100).
phonestringrequiredPhone number; Indian numbers without +91 are fine.
emailstringEmail.
tagsstring[]Tags (max 20, 40 chars each).
fieldsobjectCustom field values by key.
addressobject{ line1, line2, city, state, pincode, country }.
languagestringe.g. hi-IN, en-IN, mr-IN.
consentobject{ granted: true, source: "website_form", note } — or just true/false. Recorded in the consent log.
stagestringPipeline stage id (GET /crm/stages). Moving to Lost needs lostReason.
lostReasonstringOne of your lost reasons (Lost stage only).
ownerIdstringTeammate who owns the customer (member id or owner); null removes.
valuenumberExpected deal value in ₹.
sourcestringWhere the lead came from, e.g. indiamart, website, referral.
upsertbooleanUpdate 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}'
Response 201
{
  "id": "contact_8f2c0d4e",
  "name": "Priya Sharma",
  "phone": "+919876543210",
  "email": "priya@example.in",
  "tags": [
    "Diwali sale"
  ],
  "consent": {
    "granted": true,
    "at": "2026-10-16T04:30:00.000Z",
    "optedOutAt": null
  },
  "fields": {
    "city_tier": "Tier 1",
    "last_order_value": "2499"
  },
  "address": {
    "city": "Pune",
    "state": "Maharashtra",
    "pincode": "411057"
  },
  "language": "hi-IN",
  "source": "api",
  "stage": "qualified",
  "lostReason": null,
  "ownerId": "member_2c1a",
  "value": 85000,
  "wonValue": null,
  "warmth": 64,
  "firstSeenAt": "2026-10-16T04:30:00.000Z",
  "lastActivityAt": "2026-10-17T06:10:00.000Z",
  "createdAt": "2026-10-16T04:30:00.000Z",
  "updatedAt": null
}

Create or update up to 1,000 contacts

POST/api/v1/contacts/bulk

Scope contacts:writeSupports Idempotency-Key

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.

Body

FieldTypeDescription
contactsobject[]requiredUp to 1,000 objects shaped like POST /contacts.
upsertbooleanUpdate existing phones (default true).
Request
curl -X POST https://waca.ai/api/v1/contacts/bulk \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"upsert":true,"contacts":[{"name":"Rahul Verma","phone":"9812345678","tags":["Lead"]},{"name":"Ananya Iyer","phone":"+919845012345","consent":true}]}'
Response 200
{
  "created": 1,
  "updated": 1,
  "skipped": 0,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "status": "created",
      "id": "contact_a1"
    },
    {
      "index": 1,
      "status": "updated",
      "id": "contact_b2"
    }
  ]
}

Update a contact

PATCH/api/v1/contacts/{id}

Scope contacts:write

Only the fields you send change. fields and address are merged; tags replaces the list, addTags / removeTags edit it.

Parameters

NameInTypeDescription
idpathstringRequired. The contact id.

Body

FieldTypeDescription
namestringFull name.
phonestringNew phone (must not belong to another contact).
emailstringEmail.
tagsstring[]Replace all tags.
addTagsstring[]Add these tags.
removeTagsstring[]Remove these tags.
fieldsobjectCustom field values to set.
addressobjectAddress parts to set.
languagestringPreferred language.
consentobject{ granted, source, note } — opt in or out.
stagestringPipeline stage id (GET /crm/stages). Moving to Lost needs lostReason.
lostReasonstringOne of your lost reasons (Lost stage only).
ownerIdstringTeammate who owns the customer (member id or owner); null removes.
valuenumberExpected deal value in ₹.
wonValuenumberFinal deal value in ₹ (with stage won).
sourcestringWhere the lead came from, e.g. indiamart, website, referral.
Request
curl -X PATCH https://waca.ai/api/v1/contacts/contact_8f2c0d4e \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"addTags":["VIP"],"fields":{"last_order_value":"4999"},"stage":"qualified","value":85000,"consent":{"granted":false,"source":"call_center","note":"Asked not to get offers"}}'
Response 200
{
  "id": "contact_8f2c0d4e",
  "name": "Priya Sharma",
  "phone": "+919876543210",
  "email": "priya@example.in",
  "tags": [
    "Diwali sale"
  ],
  "consent": {
    "granted": true,
    "at": "2026-10-16T04:30:00.000Z",
    "optedOutAt": null
  },
  "fields": {
    "city_tier": "Tier 1",
    "last_order_value": "2499"
  },
  "address": {
    "city": "Pune",
    "state": "Maharashtra",
    "pincode": "411057"
  },
  "language": "hi-IN",
  "source": "api",
  "stage": "qualified",
  "lostReason": null,
  "ownerId": "member_2c1a",
  "value": 85000,
  "wonValue": null,
  "warmth": 64,
  "firstSeenAt": "2026-10-16T04:30:00.000Z",
  "lastActivityAt": "2026-10-17T06:10:00.000Z",
  "createdAt": "2026-10-16T04:30:00.000Z",
  "updatedAt": null
}

Erase a contact (DPDP)

DELETE/api/v1/contacts/{id}

Scope contacts:write

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.

Parameters

NameInTypeDescription
idpathstringRequired. The contact id.
Request
curl -X DELETE https://waca.ai/api/v1/contacts/contact_8f2c0d4e \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "id": "contact_8f2c0d4e",
  "erased": true,
  "messages": 12,
  "calls": 2,
  "runs": 1
}

Pipeline & tasks

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).

FieldTypeDescription
idstringTask id (task_…).
contactIdstringThe customer.
titlestringWhat to do, e.g. Call back about the quote.
dueAtdate-timeWhen it is due (UTC).
assigneeIdstringTeammate (member id, or "owner").
assigneeNamestringTheir name.
statusstring · open | doneopen or done.
remindViastring · inapp | whatsappIn-app reminder only, or also WhatsApp to the assignee.
overduebooleanOpen and past its due time.
createdAtdate-timeWhen it was added.
doneAtdate-timeWhen 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.

Request
curl https://waca.ai/api/v1/crm/stages \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "data": [
    {
      "id": "new",
      "label": "New",
      "kind": "open",
      "position": 1,
      "contacts": 42
    },
    {
      "id": "qualified",
      "label": "Qualified",
      "kind": "open",
      "position": 3,
      "contacts": 9
    },
    {
      "id": "won",
      "label": "Won",
      "kind": "won",
      "position": 4,
      "contacts": 5
    },
    {
      "id": "lost",
      "label": "Lost",
      "kind": "lost",
      "position": 5,
      "contacts": 7
    }
  ],
  "lostReasons": [
    "Price too high",
    "Not interested"
  ]
}

List tasks

GET/api/v1/crm/tasks

Scope contacts:read

Soonest due first.

Parameters

NameInTypeDescription
statusquerystringopen or done.
contact_idquerystringOnly this customer’s tasks.
assignee_idquerystringOnly this teammate’s tasks (member id or owner).
limitqueryinteger1–100, default 25.
cursorquerystringnextCursor from the previous page.
Request
curl https://waca.ai/api/v1/crm/tasks \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "data": [
    {
      "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
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Add a follow-up task

POST/api/v1/crm/tasks

Scope contacts:writeSupports Idempotency-Key

The assignee is reminded in the app when it is due (and on WhatsApp with remindVia: whatsapp, outside their quiet hours). Fires the task.due webhook.

Body

FieldTypeDescription
contactIdstringrequiredThe customer.
titlestringrequiredWhat to do (max 200).
dueAtdate-timerequiredWhen it is due, e.g. 2026-10-18T17:00:00+05:30.
assigneeIdstringTeammate (member id or owner). Default owner.
remindViastring · inapp | whatsappDefault inapp.
Request
curl -X POST https://waca.ai/api/v1/crm/tasks \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"contactId":"contact_8f2c0d4e","title":"Call back about the 2BHK site visit","dueAt":"2026-10-18T17:00:00+05:30","assigneeId":"member_2c1a","remindVia":"whatsapp"}'
Response 201
{
  "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
}

Update or complete a task

PATCH/api/v1/crm/tasks/{id}

Scope contacts:write

Send status: done to complete it. A new dueAt reminds again.

Parameters

NameInTypeDescription
idpathstringRequired. The task id.

Body

FieldTypeDescription
titlestringWhat to do.
dueAtdate-timeNew due time.
assigneeIdstringTeammate (member id or owner).
statusstring · open | donedone completes it.
remindViastring · inapp | whatsappReminder channel.
Request
curl -X PATCH https://waca.ai/api/v1/crm/tasks/contact_8f2c0d4e \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"done"}'
Response 200
{
  "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.

The Message object

One outbound message and its delivery status.

FieldTypeDescription
idstringMessage id (msg_…).
conversationIdstringThe conversation it belongs to.
contactIdstringThe customer.
channelstring · whatsapp | sms | instagramChannel used.
statusstring · queued | sent | delivered | read | failedLatest delivery status.
textstringWhat the customer sees (template filled in).
templateIdstringTemplate used, or null for free text.
errorstringWhy it failed, or null.
demobooleanTrue when sent in demo mode (nothing really left).
createdAtdate-timeWhen it was sent.
statusAtdate-timeWhen 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

FieldTypeDescription
contactIdstringExisting contact id. Give this or phone.
phonestringCustomer phone — +91 98765 43210, 9876543210 or E.164. Give this or contactId.
channelstring · whatsapp | sms | instagramDefault whatsapp.
templateIdstringApproved template id (GET /templates).
templateNamestringOr the template’s Meta name.
variablesobjectBody 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.textstringHeader value: the text header’s variable (max 60 characters).
header.mediaUrlstringHeader value: a public https image, video or PDF link for a media header. Without it the approved sample media is used.
buttonsobjectButton 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" }].
textstringFree text (max 4,000) when not using a template.
headerMediaUrlstringOlder name for header.mediaUrl (still accepted).
Request
curl -X POST https://waca.ai/api/v1/messages \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+919876543210","templateName":"order_out_for_delivery","variables":["Priya","A1023","₹2,499"],"header":{"text":"A1023"},"buttons":{"0":"A1023"}}'
Response 201
{
  "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"
}

Get a message’s status

GET/api/v1/messages/{id}

Scope messages:read

Parameters

NameInTypeDescription
idpathstringRequired. The message id.
Request
curl https://waca.ai/api/v1/messages/msg_7d01b3aa \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "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"
}

Calls

Ask one of your AI agents to call a customer. Consent, DND (for promotional calls), TRAI calling hours and your wallet are checked first.

The Call object

An AI phone call.

FieldTypeDescription
idstringCall id (call_…).
statusstring · queued | ringing | in_progress | completed | failed | no_answer | busy | canceled | transferred | skippedWhere the call is.
directionstring · outbound | inboundWho called whom.
contactIdstringThe customer.
agentIdstringThe AI agent that spoke.
durationSecintegerTalk time in seconds.
outcomeobject{ label, reason } once the call ended, else null.
summarystringShort summary once the call ended, else null.
skipReasonstringWhy the call didn’t go out (status skipped), else null.
demobooleanTrue for simulated demo-mode calls.
startedAtdate-timeWhen the call was created.
endedAtdate-timeWhen 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

FieldTypeDescription
contactIdstringExisting contact id. Give this or phone.
phonestringCustomer phone — +91 98765 43210, 9876543210 or E.164. Give this or contactId.
namestringName for a new contact.
agentIdstringWhich agent calls (default: your default calling agent).
goalstringWhy you’re calling, in plain words — the agent opens with it (max 500).
variablesobjectFacts the agent can use, e.g. { orderId, amount }.
purposestring · transactional | promotionalDefault 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"}}'
Response 201
{
  "callId": "call_51e0c9b2",
  "status": "queued",
  "contactId": "contact_8f2c0d4e",
  "demo": false
}

Get a call

GET/api/v1/calls/{id}

Scope calls:read

Parameters

NameInTypeDescription
idpathstringRequired. The call id.
Request
curl https://waca.ai/api/v1/calls/call_51e0c9b2 \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "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"
}

Get a call report

GET/api/v1/calls/{id}/report

Scope reports:read

Transcript, outcome, collected answers and a signed recording link (valid 7 days, works without login — keep it private).

Parameters

NameInTypeDescription
idpathstringRequired. The call id.
Request
curl https://waca.ai/api/v1/calls/call_51e0c9b2/report \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "call": {
    "id": "call_51e0c9b2",
    "status": "completed",
    "durationSec": 94,
    "outcome": {
      "label": "Interested",
      "reason": "Asked for a site visit"
    }
  },
  "transcript": [
    {
      "role": "agent",
      "text": "Namaste Priya ji, main Sunrise Homes se bol rahi hoon.",
      "offsetSec": 0,
      "at": "2026-10-16T05:59:41.000Z"
    },
    {
      "role": "customer",
      "text": "Haan boliye. Wakad wala project?",
      "offsetSec": 4,
      "at": "2026-10-16T05:59:45.000Z"
    }
  ],
  "sentiment": "positive",
  "answers": [
    {
      "label": "Visit day",
      "value": "Saturday 11 am"
    }
  ],
  "recording": {
    "status": "ready",
    "url": "https://waca.ai/api/public/telephony/call-report/r1.eyJiIjoi…/audio"
  },
  "reportUrl": "https://waca.ai/api/public/telephony/call-report/r1.eyJiIjoi…"
}

Journeys

Start any active journey for one customer, with your own variables.

The Journey object

An automated journey (WhatsApp or call).

FieldTypeDescription
idstringJourney id (flow_…).
namestringJourney name.
statusstring · active | draftOnly active journeys can be started.
channelstring · whatsapp | callHow it talks to the customer.
triggerobject{ type, event } — what normally starts it.
variablesstring[]{{variables}} its steps use (pass them when you start it).
createdAtdate-timeWhen it was created.
Journey example
{
  "id": "flow_9c1e",
  "name": "COD confirmation",
  "status": "active",
  "channel": "call",
  "trigger": {
    "type": "event",
    "event": "order.placed"
  },
  "variables": [
    "orderId",
    "amount"
  ],
  "createdAt": "2026-09-02T09:00:00.000Z"
}

List journeys

GET/api/v1/journeys

Scope journeys:read

Parameters

NameInTypeDescription
statusquerystringactive or draft.
limitqueryinteger1–100, default 25.
cursorquerystringnextCursor from the previous page.
Request
curl https://waca.ai/api/v1/journeys \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "data": [
    {
      "id": "flow_9c1e",
      "name": "COD confirmation",
      "status": "active",
      "channel": "call",
      "trigger": {
        "type": "event",
        "event": "order.placed"
      },
      "variables": [
        "orderId",
        "amount"
      ],
      "createdAt": "2026-09-02T09:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Start a journey for a customer

POST/api/v1/journeys/{id}/start

Scope journeys:startSupports Idempotency-Key

The customer must have opted in. If they are already in this journey it is skipped (not an error).

Parameters

NameInTypeDescription
idpathstringRequired. The journey id.

Body

FieldTypeDescription
contactIdstringExisting contact id. Give this or phone.
phonestringCustomer phone — +91 98765 43210, 9876543210 or E.164. Give this or contactId.
namestringName for a new contact.
variablesobjectValues for the journey’s {{variables}}, e.g. { orderId: "A1023" }.
Request
curl -X POST https://waca.ai/api/v1/journeys/flow_9c1e/start \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+919876543210","variables":{"orderId":"A1023","amount":"₹2,499","deliveryDate":"Fri, 18 Oct"}}'
Response 200
{
  "started": true,
  "runId": "run_77a2",
  "callId": null,
  "skipped": null
}

Templates

Approved WhatsApp templates and the variables each needs.

The Template object

An approved WhatsApp template you can send.

FieldTypeDescription
idstringTemplate id (template_…).
namestringDisplay name.
metaNamestringName registered with Meta.
languagestringLanguage code, e.g. en_US or hi.
categorystring · MARKETING | UTILITY | AUTHENTICATIONMeta category (decides price and compliance rules).
statusstringApproved (or Demo approved in demo mode).
parameterFormatstring · POSITIONAL | NAMEDPOSITIONAL = {{1}}, {{2}}…; NAMED = {{customer_name}}, {{order_id}}…
bodystringBody with {{1}}, {{2}} (or {{customer_name}}) placeholders.
variablesobject[][{ index, placeholder }] (+ name for named templates) — pass values in this order, or by name.
examplesobjectApproved sample values by slot: body.1, header.1, button.0…
headerobjectnull, or { format: TEXT|IMAGE|VIDEO|DOCUMENT, text, variable }.
buttonsobject[][{ index, type, text, dynamic }] — dynamic buttons take a value in buttons when sending.
Template example
{
  "id": "template_5a9c",
  "name": "Order out for delivery",
  "metaName": "order_out_for_delivery",
  "language": "en_US",
  "category": "UTILITY",
  "status": "Approved",
  "body": "Hi {{1}}, your order {{2}} is out for delivery today. Total {{3}} (COD).",
  "parameterFormat": "POSITIONAL",
  "variables": [
    {
      "index": 1,
      "placeholder": "{{1}}"
    },
    {
      "index": 2,
      "placeholder": "{{2}}"
    },
    {
      "index": 3,
      "placeholder": "{{3}}"
    }
  ],
  "examples": {
    "header.1": "A1023",
    "body.1": "Priya",
    "body.2": "A1023",
    "body.3": "₹2,499",
    "button.0": "A1023"
  },
  "header": {
    "format": "TEXT",
    "text": "Order {{1}}",
    "variable": "1"
  },
  "buttons": [
    {
      "index": 0,
      "type": "URL",
      "text": "Track order",
      "dynamic": true
    }
  ]
}

List approved templates

GET/api/v1/templates

Scope messages:read

Request
curl https://waca.ai/api/v1/templates \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "data": [
    {
      "id": "template_5a9c",
      "name": "Order out for delivery",
      "metaName": "order_out_for_delivery",
      "language": "en_US",
      "category": "UTILITY",
      "status": "Approved",
      "body": "Hi {{1}}, your order {{2}} is out for delivery today. Total {{3}} (COD).",
      "parameterFormat": "POSITIONAL",
      "variables": [
        {
          "index": 1,
          "placeholder": "{{1}}"
        },
        {
          "index": 2,
          "placeholder": "{{2}}"
        },
        {
          "index": 3,
          "placeholder": "{{3}}"
        }
      ],
      "examples": {
        "header.1": "A1023",
        "body.1": "Priya",
        "body.2": "A1023",
        "body.3": "₹2,499",
        "button.0": "A1023"
      },
      "header": {
        "format": "TEXT",
        "text": "Order {{1}}",
        "variable": "1"
      },
      "buttons": [
        {
          "index": 0,
          "type": "URL",
          "text": "Track order",
          "dynamic": true
        }
      ]
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Wallet

Your prepaid balance in rupees (GST-inclusive ledger in Billing).

The Wallet object

Your prepaid wallet.

FieldTypeDescription
currencystringAlways INR.
balancenumberWallet balance in rupees.
availablenumberBalance minus money reserved for calls in progress.
heldnumberReserved for calls in progress.
lowBalanceThresholdnumberYour alert level in rupees (wallet.low fires below it).
planstringYour plan key.
enforcedbooleanFalse in demo mode — nothing is ever blocked for balance.
Wallet example
{
  "currency": "INR",
  "balance": 1840.5,
  "available": 1786.5,
  "held": 54,
  "lowBalanceThreshold": 200,
  "plan": "growth",
  "enforced": true
}

Get wallet balance

GET/api/v1/wallet

Scope reports:read

Request
curl https://waca.ai/api/v1/wallet \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "currency": "INR",
  "balance": 1840.5,
  "available": 1786.5,
  "held": 54,
  "lowBalanceThreshold": 200,
  "plan": "growth",
  "enforced": true
}

Webhook endpoints

Tell waca.ai where to send events. You can also manage these in Settings → Developers.

The Webhook object

An HTTPS endpoint of yours that receives events.

FieldTypeDescription
idstringWebhook id (whk_…).
urlstringYour HTTPS URL.
eventsstring[]Events it receives.
descriptionstringYour note.
statusstring · active | disabledDisabled endpoints get nothing.
secretstringSigning secret (whsec_…) — returned ONLY when the webhook is created.
lastDeliveryobject{ at, event, ok, status } of the latest attempt, or null.
createdAtdate-timeWhen it was added.
Webhook example
{
  "id": "whk_2f6b",
  "url": "https://crm.example.in/hooks/waca",
  "events": [
    "call.completed",
    "message.received"
  ],
  "description": "Our CRM",
  "status": "active",
  "lastDelivery": null,
  "createdAt": "2026-10-16T06:30:00.000Z"
}

List webhook endpoints

GET/api/v1/webhooks

Scope webhooks:manage

Request
curl https://waca.ai/api/v1/webhooks \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "data": [
    {
      "id": "whk_2f6b",
      "url": "https://crm.example.in/hooks/waca",
      "events": [
        "call.completed",
        "message.received"
      ],
      "description": "Our CRM",
      "status": "active",
      "lastDelivery": null,
      "createdAt": "2026-10-16T06:30:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

Add a webhook endpoint

POST/api/v1/webhooks

Scope webhooks:manageSupports Idempotency-Key

The URL must be https. The signing secret is shown only in this response — store it.

Body

FieldTypeDescription
urlstringrequiredhttps:// URL that accepts POST.
eventsstring[]requiredEvents to receive (see Webhooks).
descriptionstringA note for your team.
Request
curl -X POST https://waca.ai/api/v1/webhooks \
  -H "Authorization: Bearer $WACA_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://crm.example.in/hooks/waca","events":["call.completed","message.received"],"description":"Our CRM"}'
Response 201
{
  "id": "whk_2f6b",
  "url": "https://crm.example.in/hooks/waca",
  "events": [
    "call.completed",
    "message.received"
  ],
  "description": "Our CRM",
  "status": "active",
  "secret": "whsec_Xb3…",
  "lastDelivery": null,
  "createdAt": "2026-10-16T06:30:00.000Z"
}

Remove a webhook endpoint

DELETE/api/v1/webhooks/{id}

Scope webhooks:manage

Parameters

NameInTypeDescription
idpathstringRequired. The webhook id.
Request
curl -X DELETE https://waca.ai/api/v1/webhooks/whk_2f6b \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "id": "whk_2f6b",
  "deleted": true
}

Send a test event

POST/api/v1/webhooks/{id}/test

Scope webhooks:manage

Sends a signed webhook.test event right now and returns what your server answered.

Parameters

NameInTypeDescription
idpathstringRequired. The webhook id.
Request
curl -X POST https://waca.ai/api/v1/webhooks/whk_2f6b/test \
  -H "Authorization: Bearer $WACA_KEY"
Response 200
{
  "delivered": true,
  "status": 200,
  "ms": 182,
  "deliveryId": "dlv_6c2e",
  "error": null
}

OpenAPI

This reference as an OpenAPI 3.1 document, for Postman, code generators and AI tools.

OpenAPI 3.1 document

GET/api/v1/openapi.json

No key needed

Public — no key needed.

Request
curl https://waca.ai/api/v1/openapi.json
Response 200
{
  "openapi": "3.1.0",
  "info": {
    "title": "waca.ai API",
    "version": "1.0.0"
  }
}

Changelog

Date (IST)Change
16 Oct 2026v1 released: contacts, messages, calls, call reports, journeys, templates, wallet, webhook endpoints, signed events, OpenAPI 3.1.