API v2 (beta)
API v2
API v2 is the new WbizTool REST API: one key per app with only the access it needs, resource-style URLs, real HTTP status codes, signed JSON webhooks and safe retries. This page covers what every request has in common. The resource pages cover each endpoint.
Base URL#
https://wbiztool.com/api/v2
Paths have no trailing slash (/api/v2/messages), but one is accepted too. An address that doesn't exist answers 404 with the code not_found.
Quick example#
Check your key with GET /me. It works with any scope and sends nothing:
curl https://wbiztool.com/api/v2/me \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"import os
import requests
response = requests.get(
"https://wbiztool.com/api/v2/me",
headers={"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"},
timeout=30,
)
print(response.status_code, response.json())// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const response = await fetch("https://wbiztool.com/api/v2/me", {
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}` },
});
console.log(response.status, await response.json());<?php
$ch = curl_init('https://wbiztool.com/api/v2/me');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$body = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n";
print_r($body);
curl_close($ch);{
"workspace": { "id": 5012, "name": "Acme Bakery" },
"key": {
"id": 7,
"name": "Order system",
"mode": "live",
"scopes": ["messages:send", "messages:read"],
"limited_to_numbers": false,
"number_ids": [],
"expires_at": null
},
"plan": { "name": "Starter" },
"limits": {
"requests_per_minute": 60,
"requests_per_day": 10000,
"messages_per_number_per_day": 1000,
"messages_remaining": null
}
}
messages_remaining is null when the plan sends unlimited messages. plan is null when the workspace has no active plan.
Keys#
Send your key in the Authorization header on every request:
Authorization: Bearer wbz_live_…
There's no client_id: the key names its workspace. A v2 key starts with wbz_live_ or wbz_test_, followed by 38 letters and digits. v1 keys don't work on v2 (they get v1_key_on_v2), and v2 keys don't work on v1.
Make a key#
Owners make keys on Settings → API keys, in the API v2 keys card: click Create v2 key, then choose:
- Name: the app that will use it, up to 50 characters.
- Kind of key: Live sends real messages; Test checks everything and sends nothing (see Test mode).
- What it can do: the scopes, or Everything.
- Numbers it can use: all numbers, including ones added later, or only some (see Keys limited to some numbers).
- Expires: never, or in 30 days, 90 days or 1 year.
The key is shown once. Copy it straight away and store it on your server, for example in an environment variable. WbizTool keeps only a hash of it and its last four characters, so a lost key can't be shown again: revoke it and make a new one.
A workspace can have up to 10 v2 keys that aren't revoked. The card shows each key's last four characters, scopes, numbers, expiry, and when and from which IP address it was last used. Revoke stops a key at once, and with it the webhook endpoints it made. A key past its expiry date gets api_key_expired.
Scopes#
Each key can do only what its scopes allow. A request outside them answers 403 with missing_scope, and required_scope in the error says which one it needs.
| Scope | What it allows |
|---|---|
messages:send | Send messages and batches, and cancel waiting messages: POST /messages, POST /messages/batch, DELETE /messages/{id} |
messages:read | Read messages and their status (GET /messages, GET /messages/{id}), and manage webhook endpoints |
reminders | Create, list, change, pause and delete reminders |
numbers | List numbers, see their connection, and connect WhatsApp numbers |
media | Upload, list and delete files in the Media directory |
contacts | Number checks (contact endpoints come later) |
GET /me needs no particular scope. Sending to a saved contact with to.contact_id needs only messages:send.
Keys limited to some numbers#
A key limited to some numbers works only with them:
- It sends only from those numbers, and lists, reads and cancels only their messages. Another number in
fromgetsnumber_not_allowed. GET /numberslists only those numbers. It can reconnect them but can't add a new number.- It sees and changes only the reminders that send from those numbers, and a new reminder must name one of them as
from. - It can't manage webhook endpoints at all, because they receive the whole workspace's events.
GET /me shows limited_to_numbers and the number_ids the key may use.
Requests#
- JSON in and out. Send bodies as
Content-Type: application/json. The one exception is uploading a file, which ismultipart/form-data. Every answer is JSON, errors included. - Field names are
snake_case, and unknown fields are refused: a typo such astxtanswersvalidation_failedinstead of being ignored. - Phone numbers are in international format with
+(E.164), for example+919876543210. Spaces and dashes are allowed; a number without+is refused withinvalid_phone. - Times are ISO 8601 with an offset, for example
2026-10-20T09:30:00+05:30or2026-10-20T04:00:00Z. A time without an offset is refused withinvalid_datetime. Times in answers are in UTC and end inZ, except a reminder'snext_run_at, which is in the reminder's time zone. - IDs are integers, such as a number's
id(fromwhen you send) or a message'sid. - Status codes mean what they say:
200OK,201created,202accepted (queued),400the request is wrong,401the key is wrong,402no messages left,403not allowed,404not found,409conflict,413too large,415wrong content type,422a value can't be used,429too many requests,5xxour side.
Answers carry Cache-Control: no-store.
Errors#
Every error has the same shape:
{
"error": {
"code": "invalid_phone",
"message": "This is not a valid phone number. Write it in international format with +, e.g. +919876543210.",
"field": "to.phone",
"docs": "https://wbiztool.com/docs/api/v2/errors/#invalid_phone",
"request_id": "req_4f1c2a9b8e7d6c5b4a392817"
}
}
codeis stable: branch on it in your code.messageis English text for the developer and may change.fieldnames the field the error is about (to.phone,to[3].phone,schedule.cron), or isnull.docslinks to the code on the errors page.request_idis the same as theX-Request-Idheader. Quote it if you contact us.- Some errors add details, for example
statusonnot_cancellableorusedandlimitonstorage_full. When a request has several problems,errorslists the others, each withfieldandmessage.
Every code, its HTTP status and what to do are on the errors page.
Idempotency-Key#
A network error or a timeout leaves you unsure whether a request worked. Send an Idempotency-Key header with a POST that creates something, and you can safely send it again:
curl -X POST https://wbiztool.com/api/v2/messages \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-ready" \
-d '{"to": {"phone": "+919876543210"}, "text": "Hi Asha, your order #1042 is ready."}'
- The value is yours: any unique string of 1 to 255 printable characters, such as a UUID or your own order id. Make a new one for each new request, and reuse it only for a retry of the same request.
- A retry gets the first answer back with the header
Idempotent-Replayed: true. Nothing is done twice: no second message, reminder, file or endpoint. - Keys last 24 hours from the first request, and belong to your API key: another API key of the workspace can use the same value without clashing.
- Same key, different request: a request with a key already used for a different method, path or body answers
422idempotency_key_reused. - Still running: while the first request with the key is still being handled, a retry answers
409idempotency_in_progresswithRetry-After: 1. Wait a moment and send it again. - Errors aren't kept. If the first request answered with an error, nothing is stored: fix the request and send it again with the same key.
- Secrets aren't replayed. A replay of
POST /webhooksleaves out the endpoint'ssecret, which is shown only in the first answer.
It works on POST /messages, POST /messages/batch, POST /numbers, POST /reminders, POST /media, POST /number-checks and POST /webhooks, and is optional on each. Other requests ignore the header. A value that's empty or too long answers 400 invalid_idempotency_key.
Rate limits#
Each key can make 60 requests a minute and 10,000 requests a day. These are fixed windows: the minute window starts on the clock minute, the day window at 00:00 UTC. Over a limit, the request answers 429 rate_limited.
Answers to requests with a valid key carry these headers:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the current window (60, or 10000 once the day limit is reached) |
RateLimit-Remaining | Requests left in that window |
RateLimit-Reset | Seconds until that window starts again |
RateLimit-Policy | Both limits: 60;w=60, 10000;w=86400 |
Retry-After | On a 429: seconds to wait before you try again |
Separately from these:
- Wrong keys: more than 30 requests a minute with a wrong key from one IP address answer
429too_many_auth_failureswithRetry-After: 60. - Messages per number: a number sends at most 1,000 messages in 24 hours (fair use). A send over it answers
429number_daily_cap_reached.
Pagination#
Lists that can grow (GET /messages, /reminders, /media, /number-checks and a check's /results) come in pages, newest first:
{
"data": [ … ],
"next_cursor": "eyJpZCI6OTgxNzI2MywidCI6IjIwMjYtMTAtMThUMTA6MTU6MDIuMzExWiIsImYiOiI0NGIxMzZmYTM1NWIifQ"
}
limitsets the page size, from 1 to 100 (default 25).- To get the next page, send the same request with
cursorset to the last page'snext_cursor.next_cursorisnullon the last page. - Keep the other filters the same: a cursor used with different filters answers
400invalid_cursor. Start again without a cursor. - Treat the cursor as an opaque string. Items added while you page don't shift the pages.
curl "https://wbiztool.com/api/v2/messages?limit=100&cursor=eyJpZCI6OTgxNzI2MywidCI6..." \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
GET /numbers and GET /webhooks return everything in one data list.
Request ids#
Every answer, errors included, has an X-Request-Id header such as req_4f1c2a9b8e7d6c5b4a392817. Errors carry the same value as request_id. Log it, and quote it if you contact us: it lets us find the request in our logs, which record your key's id but never the key.
If you send your own X-Request-Id (up to 100 printable characters), it comes back as X-Client-Request-Id, so you can match answers to your own logs.
Test mode#
A test key (wbz_test_…) lets you build and test an integration without sending anything. Make one on Settings → API keys with Kind of key: Test. GET /me shows "mode": "test".
What a test key does like a live key:
POST /messagesandPOST /messages/batchcheck everything a live send checks: the recipient, the text, media,send_atandexpires_at, the sending number, and the key's scopes and numbers. The same mistakes get the same errors.- Scopes, numbers, rate limits and
Idempotency-Keywork the same. - Every read of the rest of the workspace (
GET /me,/numbers,/reminders,/media,/number-checks,/webhooks) shows its real data, read-only.
What's different:
- Nothing is sent. A test send is kept as a test message instead, and doesn't use the plan's messages or the number's daily 1,000, so it never gets
no_creditsornumber_daily_cap_reached. - Its status moves on by itself:
queued, thensent5 seconds later anddelivered15 seconds after it was made (counted fromsend_atfor a scheduled one). A message whoseexpires_atcomes before it would be sent becomesexpired. Test messages never becomereadorfailed. - A test key sees only its own test messages.
GET /messagesandGET /messages/{id}show them and nothing else, andDELETE /messages/{id}cancels one while it'squeuedorscheduled. Live keys never see test messages. Thestatusfilter ofGET /messagesisn't supported for test keys and answerstest_mode_unsupported;number,sinceanduntilwork. - Other writes are refused with
403test_mode_unsupported: connecting a number, creating, changing or deleting reminders, uploading or deleting media, starting a number check, and adding, removing, rotating or testing webhook endpoints. - No webhook events are sent for test messages.
- Test messages are kept for 30 days.
Resources#
- Messages: send now or later, send to up to 1,000 recipients, read the history, cancel.
- Numbers: list your WhatsApp numbers and Telegram accounts, and connect a WhatsApp number by QR code or pairing code.
- Reminders: repeating messages on a cron schedule.
- Media: upload files once and send them by
media_id. - Number checks: find out which numbers are on WhatsApp.
- Webhooks: signed events when a message is sent, fails, is cancelled or expires.
- Errors: every error code.
- Moving from v1 to v2: each v1 endpoint and field, and its v2 equivalent.
