API v2 (beta)
Opt-outs
An opt-out is a person who doesn't want your workspace's messages. Nothing is ever sent to an opted-out number: a new message to it is refused, and messages queued earlier fail with the error Opted out instead of going out. It works for every way you send: the API (v1 and v2), the dashboard, campaigns, template messages, reminders, Google Sheets and Shopify.
The list is the same as the Opted out tab of Contacts in the dashboard.
| Operation | Endpoint | Scope |
|---|---|---|
| Opt a number out | POST /api/v2/opt-outs | contacts |
| List opt-outs | GET /api/v2/opt-outs | contacts |
| Get an opt-out | GET /api/v2/opt-outs/{phone} | contacts |
| Opt a number back in | DELETE /api/v2/opt-outs/{phone} | contacts |
STOP replies are added for you#
When someone replies to one of your inbox numbers with a message that is only an opt-out word, they're opted out by themselves, with source reply and their message as keyword. A message that is only START (or Subscribe) opts them back in.
- The words, in any capitals and with or without punctuation:
STOP,Stop all,Unsubscribe,Opt out,Baja,Parar,Sair,Stopp,Abmelden,Arrêt,Désabonner,Basta,Disiscrivi,Стоп,Отписаться,Berhenti,Dur,توقف,إلغاء الاشتراك,עצור,הסר. - Only a message that is just the word counts: "please stop sending me the price list" doesn't opt anyone out.
- It needs the inbox on the number they reply to, because only inbox numbers read incoming messages. Replies older than 7 days, such as those read when an inbox number is added, don't count.
- Nothing is sent back to them, and no auto-reply answers the STOP.
Opt a number out#
https://wbiztool.com/api/v2/opt-outsBody: JSON (application/json)
Body
phonestringrequiredThe number in international format with
+(+919876543210). Spaces, dashes and brackets are ignored. A Telegram@usernameworks too.notestringoptionalWhy, for your team, up to 200 characters. Shown on the Opted out tab.
curl -X POST https://wbiztool.com/api/v2/opt-outs \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "+919876543210", "note": "Asked by email"}'import os
import requests
r = requests.post("https://wbiztool.com/api/v2/opt-outs", timeout=30,
headers={"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"},
json={"phone": "+919876543210", "note": "Asked by email"})
print(r.status_code, r.json())// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const r = await fetch("https://wbiztool.com/api/v2/opt-outs", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ phone: "+919876543210", note: "Asked by email" }),
});
console.log(r.status, await r.json());<?php
$ch = curl_init('https://wbiztool.com/api/v2/opt-outs');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['phone' => '+919876543210', 'note' => 'Asked by email']),
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);
curl_close($ch);Answer: 201 with the opt-out. A number that had opted out already answers 200 with its existing entry, unchanged.
{
"recipient": "+919876543210",
"source": "api",
"keyword": null,
"note": "Asked by email",
"created_at": "2026-10-26T09:12:03.120Z"
}
Errors#
| HTTP | Code | When |
|---|---|---|
400 | validation_failed | note isn't text or is longer than 200 characters |
422 | invalid_phone | phone is missing or isn't a phone number |
List opt-outs#
https://wbiztool.com/api/v2/opt-outsEveryone who opted out, newest first, in pages (limit 1 to 100, default 25, and cursor). Add phone to look up one number.
curl "https://wbiztool.com/api/v2/opt-outs?limit=100" \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
{
"data": [
{ "recipient": "+919876543210", "source": "reply", "keyword": "STOP", "note": null, "created_at": "2026-10-26T09:12:03.120Z" }
],
"next_cursor": null
}
Get an opt-out#
https://wbiztool.com/api/v2/opt-outs/{phone}The number in the address in international format without + (919876543210). Answers 404 opt_out_not_found when the number hasn't opted out, so it's a quick check before you send.
curl https://wbiztool.com/api/v2/opt-outs/919876543210 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
Opt a number back in#
https://wbiztool.com/api/v2/opt-outs/{phone}Messages to the number go out again from now on. Messages that already failed as Opted out stay failed. Only do this when the person asked for your messages again.
curl -X DELETE https://wbiztool.com/api/v2/opt-outs/919876543210 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
{ "recipient": "+919876543210", "deleted": true }
A number that hadn't opted out answers 404 opt_out_not_found.
Sending to an opted-out number#
POST /messagesanswers422recipient_opted_out, and nothing is queued. In a batch, the whole batch is refused and the error'sfieldnames the entry.- A message queued before the person opted out fails with
errorOpted outwhen its turn comes, and a webhook getsmessage.failed. - API v1 queues the message and it fails with
Opted out, as you'll see in message status. - Campaigns' Retry failed skips these messages.
The opt-out object#
| Field | Type | What it is |
|---|---|---|
recipient | string | + and the number, or a Telegram @username |
source | string | reply: they replied STOP to an inbox number. dashboard: added on Contacts. import: from a CSV on Contacts. api: this API |
keyword | string or null | The reply that opted them out, as they wrote it |
note | string or null | Your team's note |
created_at | string | When they opted out (ISO 8601, UTC) |
