# waca.ai API reference (v1.0.0)

Base URL: `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**

```text
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**

```bash
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.

| 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. |
| journeys:read | List journeys. |
| journeys:start | List journeys and start one for a customer. |
| reports:read | Call reports (transcript + recording link), wallet balance, journeys and templates. |
| webhooks:manage | Add, 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"
    }
  }
}
```

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

```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:

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

```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"
  }
}
```

| Header | Value |
| --- | --- |
| X-Waca-Event | The event, e.g. call.completed |
| X-Waca-Delivery | Unique per delivery — use it to ignore duplicates |
| X-Waca-Timestamp | Milliseconds since 1970 when this attempt was signed |
| X-Waca-Signature | sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)> |

| Event | When |
| --- | --- |
| contact.created | 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)**

```javascript
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
<?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)**

```python
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 /me` · any key

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

```bash
curl https://waca.ai/api/v1/me \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `id` | string |  | Contact id, e.g. contact_8f2c0d4e… |
| `name` | string |  | Full name. |
| `phone` | string |  | E.164 phone number, e.g. +919876543210. |
| `email` | string |  | Email, or empty. |
| `tags` | string[] |  | Up to 20 tags. |
| `consent` | object |  | { granted, at, optedOutAt } — WhatsApp/marketing consent. |
| `fields` | object |  | Your custom fields (Settings → Customer fields), by key. |
| `address` | object |  | { line1, line2, city, state, pincode, country }. |
| `language` | string |  | Preferred language code, e.g. hi-IN. |
| `source` | string |  | Where the lead came from: whatsapp, instagram, web, call, csv_import, api, shopify, gmail, indiamart, linkedin, exotel_missed_call, manual… |
| `stage` | string |  | Pipeline stage id (GET /crm/stages), e.g. new, contacted, qualified, won, lost. |
| `lostReason` | string |  | Why the lead was lost (only in the Lost stage), or null. |
| `ownerId` | string |  | Teammate who owns this customer (member id, or "owner"), or null. |
| `value` | number |  | Expected deal value in ₹, or null. |
| `wonValue` | number |  | Final deal value in ₹ once won, or null. |
| `warmth` | integer |  | Engagement score 0–100 (replies, calls, orders, recency), or null before the first score. |
| `firstSeenAt` | date-time |  | First time this customer reached you. |
| `lastActivityAt` | date-time |  | Latest message, call, order or team note, or null. |
| `createdAt` | date-time |  | When the contact was created (UTC). |
| `updatedAt` | date-time |  | Last change (UTC), or null. |

### List contacts

`GET /contacts` · scope `contacts:read`

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `phone` | query | string |  | Exact phone (any Indian format). |
| `email` | query | string |  | Exact email (case-insensitive). |
| `tag` | query | string |  | Has this tag. |
| `stage` | query | string |  | In this pipeline stage (id from GET /crm/stages). |
| `owner_id` | query | string |  | Owned by this teammate (member id or owner). |
| `updated_since` | query | date-time |  | Created or changed at/after this time, e.g. 2026-10-01T00:00:00+05:30. |
| `limit` | query | integer |  | 1–100, default 25. |
| `cursor` | query | string |  | `nextCursor` from the previous page. |

```bash
curl https://waca.ai/api/v1/contacts \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /contacts/{id}` · scope `contacts:read`

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The contact id. |

```bash
curl https://waca.ai/api/v1/contacts/contact_8f2c0d4e \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /contacts` · scope `contacts:write` · supports 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:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Full name (max 100). |
| `phone` | string | yes | 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. |

```bash
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`:

```json
{
  "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 /contacts/bulk` · scope `contacts:write` · supports 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:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contacts` | object[] | yes | Up to 1,000 objects shaped like POST /contacts. |
| `upsert` | boolean |  | Update existing phones (default true). |

```bash
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`:

```json
{
  "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 /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:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The contact id. |

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string |  | Full name. |
| `phone` | string |  | New phone (must not belong to another contact). |
| `email` | string |  | Email. |
| `tags` | string[] |  | Replace all tags. |
| `addTags` | string[] |  | Add these tags. |
| `removeTags` | string[] |  | Remove these tags. |
| `fields` | object |  | Custom field values to set. |
| `address` | object |  | Address parts to set. |
| `language` | string |  | Preferred language. |
| `consent` | object |  | { granted, source, note } — opt in or out. |
| `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 ₹. |
| `wonValue` | number |  | Final deal value in ₹ (with stage won). |
| `source` | string |  | Where the lead came from, e.g. indiamart, website, referral. |

```bash
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`:

```json
{
  "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 /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:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The contact id. |

```bash
curl -X DELETE https://waca.ai/api/v1/contacts/contact_8f2c0d4e \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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).

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

### List pipeline stages

`GET /crm/stages` · scope `contacts:read`

In board order. `kind` is open, won or lost; also returns your lost reasons.

```bash
curl https://waca.ai/api/v1/crm/stages \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /crm/tasks` · scope `contacts:read`

Soonest due first.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string |  | open or done. |
| `contact_id` | query | string |  | Only this customer’s tasks. |
| `assignee_id` | query | string |  | Only this teammate’s tasks (member id or owner). |
| `limit` | query | integer |  | 1–100, default 25. |
| `cursor` | query | string |  | `nextCursor` from the previous page. |

```bash
curl https://waca.ai/api/v1/crm/tasks \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /crm/tasks` · scope `contacts:write` · supports 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:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contactId` | string | yes | The customer. |
| `title` | string | yes | What to do (max 200). |
| `dueAt` | date-time | yes | When it is due, e.g. 2026-10-18T17:00:00+05:30. |
| `assigneeId` | string |  | Teammate (member id or owner). Default owner. |
| `remindVia` | string (inapp \| whatsapp) |  | Default inapp. |

```bash
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`:

```json
{
  "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 /crm/tasks/{id}` · scope `contacts:write`

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The task id. |

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string |  | What to do. |
| `dueAt` | date-time |  | New due time. |
| `assigneeId` | string |  | Teammate (member id or owner). |
| `status` | string (open \| done) |  | done completes it. |
| `remindVia` | string (inapp \| whatsapp) |  | Reminder channel. |

```bash
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`:

```json
{
  "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.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `id` | string |  | Message id (msg_…). |
| `conversationId` | string |  | The conversation it belongs to. |
| `contactId` | string |  | The customer. |
| `channel` | string (whatsapp \| sms \| instagram) |  | Channel used. |
| `status` | string (queued \| sent \| delivered \| read \| failed) |  | Latest delivery status. |
| `text` | string |  | What the customer sees (template filled in). |
| `templateId` | string |  | Template used, or null for free text. |
| `error` | string |  | Why it failed, or null. |
| `demo` | boolean |  | 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. |

### Send a message

`POST /messages` · scope `messages:send` · supports 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 | Required | 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" }]. |
| `text` | string |  | Free text (max 4,000) when not using a template. |
| `headerMediaUrl` | string |  | Older name for header.mediaUrl (still accepted). |

```bash
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`:

```json
{
  "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 /messages/{id}` · scope `messages:read`

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The message id. |

```bash
curl https://waca.ai/api/v1/messages/msg_7d01b3aa \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `id` | string |  | Call id (call_…). |
| `status` | string (queued \| ringing \| in_progress \| completed \| failed \| no_answer \| busy \| canceled \| transferred \| skipped) |  | Where the call is. |
| `direction` | string (outbound \| inbound) |  | Who called whom. |
| `contactId` | string |  | The customer. |
| `agentId` | string |  | The AI agent that spoke. |
| `durationSec` | integer |  | Talk time in seconds. |
| `outcome` | object |  | { label, reason } once the call ended, else null. |
| `summary` | string |  | Short summary once the call ended, else null. |
| `skipReason` | string |  | 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. |

### Place an AI call

`POST /calls` · scope `calls:create` · supports 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 | Required | 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. |

```bash
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`:

```json
{
  "callId": "call_51e0c9b2",
  "status": "queued",
  "contactId": "contact_8f2c0d4e",
  "demo": false
}
```

### Get a call

`GET /calls/{id}` · scope `calls:read`

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The call id. |

```bash
curl https://waca.ai/api/v1/calls/call_51e0c9b2 \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /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:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The call id. |

```bash
curl https://waca.ai/api/v1/calls/call_51e0c9b2/report \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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).

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `id` | string |  | Journey id (flow_…). |
| `name` | string |  | Journey name. |
| `status` | string (active \| draft) |  | Only active journeys can be started. |
| `channel` | string (whatsapp \| call) |  | How it talks to the customer. |
| `trigger` | object |  | { type, event } — what normally starts it. |
| `variables` | string[] |  | {{variables}} its steps use (pass them when you start it). |
| `createdAt` | date-time |  | When it was created. |

### List journeys

`GET /journeys` · scope `journeys:read`

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string |  | active or draft. |
| `limit` | query | integer |  | 1–100, default 25. |
| `cursor` | query | string |  | `nextCursor` from the previous page. |

```bash
curl https://waca.ai/api/v1/journeys \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /journeys/{id}/start` · scope `journeys:start` · supports Idempotency-Key

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The journey id. |

Body:

| Field | Type | Required | 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. |
| `variables` | object |  | Values for the journey’s {{variables}}, e.g. { orderId: "A1023" }. |

```bash
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`:

```json
{
  "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.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `id` | string |  | Template id (template_…). |
| `name` | string |  | Display name. |
| `metaName` | string |  | Name registered with Meta. |
| `language` | string |  | Language code, e.g. en_US or hi. |
| `category` | string (MARKETING \| UTILITY \| AUTHENTICATION) |  | Meta category (decides price and compliance rules). |
| `status` | string |  | Approved (or Demo approved in demo mode). |
| `parameterFormat` | string (POSITIONAL \| NAMED) |  | POSITIONAL = {{1}}, {{2}}…; NAMED = {{customer_name}}, {{order_id}}… |
| `body` | string |  | Body with {{1}}, {{2}} (or {{customer_name}}) placeholders. |
| `variables` | object[] |  | [{ index, placeholder }] (+ name for named templates) — pass values in this order, or by name. |
| `examples` | object |  | Approved sample values by slot: body.1, header.1, button.0… |
| `header` | object |  | null, or { format: TEXT\|IMAGE\|VIDEO\|DOCUMENT, text, variable }. |
| `buttons` | object[] |  | [{ index, type, text, dynamic }] — dynamic buttons take a value in `buttons` when sending. |

### List approved templates

`GET /templates` · scope `messages:read`

```bash
curl https://waca.ai/api/v1/templates \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `currency` | string |  | Always INR. |
| `balance` | number |  | Wallet balance in rupees. |
| `available` | number |  | Balance minus money reserved for calls in progress. |
| `held` | number |  | Reserved for calls in progress. |
| `lowBalanceThreshold` | number |  | Your alert level in rupees (wallet.low fires below it). |
| `plan` | string |  | Your plan key. |
| `enforced` | boolean |  | False in demo mode — nothing is ever blocked for balance. |

### Get wallet balance

`GET /wallet` · scope `reports:read`

```bash
curl https://waca.ai/api/v1/wallet \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `id` | string |  | Webhook id (whk_…). |
| `url` | string |  | Your HTTPS URL. |
| `events` | string[] |  | Events it receives. |
| `description` | string |  | Your note. |
| `status` | string (active \| disabled) |  | Disabled endpoints get nothing. |
| `secret` | string |  | Signing secret (whsec_…) — returned ONLY when the webhook is created. |
| `lastDelivery` | object |  | { at, event, ok, status } of the latest attempt, or null. |
| `createdAt` | date-time |  | When it was added. |

### List webhook endpoints

`GET /webhooks` · scope `webhooks:manage`

```bash
curl https://waca.ai/api/v1/webhooks \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /webhooks` · scope `webhooks:manage` · supports Idempotency-Key

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

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | https:// URL that accepts POST. |
| `events` | string[] | yes | Events to receive (see Webhooks). |
| `description` | string |  | A note for your team. |

```bash
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`:

```json
{
  "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 /webhooks/{id}` · scope `webhooks:manage`

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The webhook id. |

```bash
curl -X DELETE https://waca.ai/api/v1/webhooks/whk_2f6b \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "id": "whk_2f6b",
  "deleted": true
}
```

### Send a test event

`POST /webhooks/{id}/test` · scope `webhooks:manage`

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The webhook id. |

```bash
curl -X POST https://waca.ai/api/v1/webhooks/whk_2f6b/test \
  -H "Authorization: Bearer $WACA_KEY"
```

Response `200`:

```json
{
  "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 /openapi.json` · no key needed

Public — no key needed.

```bash
curl https://waca.ai/api/v1/openapi.json
```

Response `200`:

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "waca.ai API",
    "version": "1.0.0"
  }
}
```

## Changelog

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