Skip to content
WbizTool

API v2 (beta)

Messages

Send a message now or at a set time, to a phone number, a WhatsApp group, a saved contact or a Telegram username. Send the same message to up to 1,000 recipients in one request, read the history, follow a message's status, and cancel it while it waits.

OperationEndpointScope
Send a messagePOST /api/v2/messagesmessages:send
Send to many recipientsPOST /api/v2/messages/batchmessages:send
List messagesGET /api/v2/messagesmessages:read
Get a messageGET /api/v2/messages/{id}messages:read
Cancel a messageDELETE /api/v2/messages/{id}messages:send

Keys, errors, Idempotency-Key and limits work as described in the API v2 overview. To hear when a message is sent or fails without polling, add a webhook endpoint.

Send a message#

POSThttps://wbiztool.com/api/v2/messages

Body: JSON (application/json)

The message is queued and the answer comes before it goes out, with HTTP 202 and the Message in status queued (or scheduled with a future send_at). Follow it with Get a message or a webhook.

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 '{
    "from": 101,
    "to": { "phone": "+919876543210" },
    "text": "Hi Asha, your order #1042 is ready."
  }'

Answer (202):

{
  "id": 9817263,
  "status": "queued",
  "from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
  "to": { "phone": "+919876543210", "contact_id": 991 },
  "text": "Hi Asha, your order #1042 is ready.",
  "media": null,
  "send_at": null,
  "expires_at": null,
  "created_at": "2026-10-18T10:15:02.311Z",
  "sent_at": null,
  "error": null
}

Body

toobjectrequired

Who gets it: exactly one of phone, group, contact_id or username. See Recipients.

frominteger or nullRequired unless the workspace has one connected number

The id of the number that sends it, from List numbers. Left out, the only connected number sends it: one connected WhatsApp number is used even if a Telegram account is also connected, and a Telegram account only when no WhatsApp number is connected. A number that is logged out is accepted: its messages wait until it reconnects.

textstringRequired unless media is sent

The message, up to 3,000 characters. With media, it's the caption and can be left out. WhatsApp formatting works: *bold*, _italic_, ~strikethrough~. In JSON, write new lines as \n.

mediaobject or nulloptional

An image or file to send. See Media.

send_atstring or nulloptional

Send at this time instead of now: ISO 8601 with its offset, such as 2026-10-20T09:30:00+05:30. It must be in the future. See Scheduling and expiry.

expires_atstring or nulloptional

Don't send after this time: the message becomes expired instead. ISO 8601 with its offset, in the future and after send_at.

Headers

Authorizationheaderrequired

Bearer wbz_live_…, a key with the messages:send scope.

Idempotency-Keyheaderoptional

Any unique string, so a retry never sends a second message. See Idempotency-Key.

Errors when sending#

HTTPCodeWhen
400validation_failedA field is missing, has the wrong type or isn't known; to has none or several of its fields; media has both or neither of url and media_id; expires_at isn't after send_at
400text_requiredNo text and no media
400text_too_longtext is over 3,000 characters
400invalid_datetimesend_at or expires_at isn't ISO 8601 with an offset
402no_creditsThe workspace has no messages left on its plan
403number_not_allowedThe key is limited to other numbers
403account_disabledThe workspace owner's account is disabled
422invalid_phoneto.phone isn't a phone number in international format with +
422invalid_usernameto.username isn't a Telegram username, link or tg: id
422username_needs_telegramto.username with a WhatsApp number as from
422contact_not_foundNo contact with that to.contact_id
422media_not_foundNo file with that media.media_id
422invalid_media_urlmedia.url isn't a public http(s) link
422time_in_pastsend_at or expires_at has already passed
422number_not_foundfrom isn't a number of this workspace
422from_requiredNo from, and the workspace has no connected number or several
422message_rejectedThe message couldn't be queued; message says why
429number_daily_cap_reachedThe number has sent 1,000 messages in the last 24 hours

A refused send leaves nothing behind: no message is stored, so there's nothing to cancel. Errors that can happen on any request (keys, scopes, rate limits, Idempotency-Key) are on the errors page.

Send to many recipients#

POSThttps://wbiztool.com/api/v2/messages/batch

Body: JSON (application/json)

Sends the same message to up to 1,000 recipients. The body is the same as Send a message, except that to is a list of recipients.

  • All or nothing. Every recipient is checked before anything is queued, and the whole list must fit in the plan's messages left and in the sending number's room for the next 24 hours. If one recipient is wrong, nothing is queued, and field names it, such as to[3].phone.
  • The same recipient twice gets one message.
  • The answer is 202 with data: one Message per recipient, in the order of to.
curl -X POST https://wbiztool.com/api/v2/messages/batch \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: opening-hours-2026-10-20" \
  -d '{
    "from": 101,
    "to": [
      { "phone": "+919876543210" },
      { "phone": "+447700900123" },
      { "group": "Bakery staff" }
    ],
    "text": "We open at 10 tomorrow."
  }'

Answer (202), one Message per recipient; the second and third look like the first, with to set to {"phone": "+447700900123", …} and {"group": "Bakery staff", …}:

{
  "data": [
    {
      "id": 9817264,
      "status": "queued",
      "from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
      "to": { "phone": "+919876543210", "contact_id": 991 },
      "text": "We open at 10 tomorrow.",
      "media": null,
      "send_at": null,
      "expires_at": null,
      "created_at": "2026-10-19T16:02:40.118Z",
      "sent_at": null,
      "error": null
    }
  ]
}

Errors when sending a batch#

The same as when sending one message, with the recipient's place in field (to[0].phone), and also:

HTTPCodeWhen
400too_many_recipientsto has more than 1,000 entries
400validation_failedto is empty or not a list
402no_creditsThe list doesn't fit in the messages left: requested and remaining say by how much
429number_daily_cap_reachedThe list doesn't fit in the number's room for the next 24 hours: requested and room say how many

List messages#

GEThttps://wbiztool.com/api/v2/messages

The workspace's messages of the last 90 days, newest first, in pages. This includes messages sent from the dashboard, campaigns, reminders, integrations and API v1, not only API v2's. A key limited to some numbers sees only their messages.

Query parameters

numberintegeroptional

Only messages from this number id.

statusstringoptional

Only messages with this status: queued, scheduled, sent, delivered, read, failed, cancelled or expired.

sincestringoptional

Only messages created at or after this time (ISO 8601 with offset).

untilstringoptional

Only messages created before this time (ISO 8601 with offset).

limitintegeroptional

Messages per page, 1 to 100 (default 25).

cursorstringoptional

The last page's next_cursor, with the same filters.

Filters combine. In a query string, write the + of an offset as %2B (a plain + arrives as a space, which is read as + too), or use Z.

curl -G https://wbiztool.com/api/v2/messages \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY" \
  --data-urlencode "status=failed" \
  --data-urlencode "since=2026-10-01T00:00:00+05:30" \
  --data-urlencode "limit=100"

The answer is {"data": [Message, …], "next_cursor": "…" or null}.

Errors when listing#

HTTPCodeWhen
400validation_failednumber isn't a number id, status isn't one of the statuses, or limit isn't 1 to 100
400invalid_datetimesince or until isn't ISO 8601 with an offset
400invalid_cursorThe cursor is damaged or was made with other filters

Get a message#

GEThttps://wbiztool.com/api/v2/messages/{id}

One message and its status now.

curl https://wbiztool.com/api/v2/messages/9817263 \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY"

Answer (200), a message that was delivered:

{
  "id": 9817263,
  "status": "delivered",
  "from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
  "to": { "phone": "+919876543210", "contact_id": 991 },
  "text": "Hi Asha, your order #1042 is ready.",
  "media": null,
  "send_at": null,
  "expires_at": null,
  "created_at": "2026-10-18T10:15:02.311Z",
  "sent_at": "2026-10-18T10:15:09.870Z",
  "error": null
}

A message that doesn't exist, belongs to another workspace, or (for a key limited to some numbers) was sent from another number answers 404 message_not_found.

Cancel a message#

DELETEhttps://wbiztool.com/api/v2/messages/{id}

Cancels a message that is still waiting (queued or scheduled), whoever made it. The answer is 200 with the Message, now in status cancelled. A webhook endpoint gets message.cancelled.

A message that has gone out can't be recalled. A message the sending number is picking up at that moment may still go out, so check its status afterwards if it matters.

curl -X DELETE https://wbiztool.com/api/v2/messages/9817263 \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY"

Errors when cancelling#

HTTPCodeWhen
404message_not_foundNo such message for this key
409not_cancellableIt isn't waiting any more; status in the error says what it is now, such as sent

The Message object#

Every operation on this page answers with Message objects, and webhook events carry one in data.message.

FieldTypeDescription
idintegerThe message's id. Use it to get or cancel the message.
statusstringWhere it is now. See Statuses.
fromobjectThe sending number: id, phone (its digits with the country code, no +) and platform (whatsapp or telegram).
toobjectThe recipient: phone (with +), group or username, and contact_id, the saved contact it went to. A phone number or group sent to for the first time is saved as a contact. (contact_id is null only for a test message to a number that isn't saved.)
textstringThe text or caption; "" when there's none.
mediaobject or nulltype (image or file), url and file_name (null for images). The url may be a link that works for a limited time; don't store it.
send_atstring or nullThe time it's scheduled for, or null for "now".
expires_atstring or nullThe time after which it's not sent, or null.
created_atstringWhen it was made.
sent_atstring or nullWhen it went out, once it has (sent, delivered or read).
errorobject or nullFor a failed or expired message, message says why, such as File exceeds WhatsApp size limit (16MB max). null otherwise.

Statuses#

StatusMeaning
queuedWaiting to go out.
scheduledWaiting for its send_at.
sentSent from your number (one tick in WhatsApp).
deliveredDelivered to the recipient's phone (two ticks).
readRead by the recipient (blue ticks).
failedIt couldn't be sent; error.message says why.
cancelledCancelled before it went out: with Cancel a message, from the dashboard, or because its number or campaign was removed.
expiredNot sent before its expires_at.

queued and scheduled can be cancelled. sent can still become delivered and then read. failed, cancelled, expired and read are final. Webhooks report sent, failed, cancelled and expired; to see delivered and read, get the message.

Recipients#

to takes exactly one of these fields:

FieldExampleWho gets it
phone{"phone": "+919876543210"}A phone number in international format with + (E.164). Spaces and dashes are allowed.
group{"group": "Bakery staff"}A WhatsApp group the sending number is in, by its exact name (up to 200 characters).
contact_id{"contact_id": 991}A contact saved in the workspace, by its id (the contact_id of an earlier message, say). It goes to that contact's number or group.
username{"username": "@asha_bakes"}A Telegram user: @name, a t.me/name link, or tg: and a user id. Only from a Telegram account, so give one as from (or have exactly one connected).

Group and Telegram sends need the sending number to be in that group or able to reach that user; problems there show up as a failed message, not as an error when you send.

Media#

media sends an image or a file, with text as its caption. Give exactly one of url or media_id:

media.urlstringEither url or media_id

A public http or https link to the file, up to 1,000 characters. Links to private or local addresses are refused with invalid_media_url.

media.media_idintegerEither url or media_id

A file in the workspace's Media directory, from Upload a file. Upload a file once and send it as often as you like.

media.typestringoptional

image or file. Left out, a link ending in .jpg, .jpeg, .png, .gif or .webp is sent as an image and anything else as a file; a media_id uses the type the file was uploaded with.

media.file_namestringoptional

For a file: the name the recipient sees, such as invoice-1042.pdf (up to 100 characters). Left out, the name comes from the link or the uploaded file. Its extension decides how the file is sent, so include one. Ignored for images.

{
  "to": { "phone": "+919876543210" },
  "text": "Your invoice for order #1042",
  "media": { "url": "https://example.com/invoices/1042.pdf", "type": "file", "file_name": "invoice-1042.pdf" }
}

The file is fetched when the message is sent, not when you call the API, so a broken link or a file that's too big shows up as a failed message with the reason in error.message. The size and type rules are those of v1: see Sending images and files.

Scheduling and expiry#

  • send_at schedules the message. It stays scheduled until then and can be cancelled until it goes out. Write the time with its offset, so 2026-10-20T09:30:00+05:30 means 09:30 in India. For messages that repeat, use reminders.
  • expires_at is a deadline: if the message hasn't gone out by then (the number was logged out, say), it becomes expired instead of going out late. Use it for one-time codes, delivery times and offers. A background job marks it expired at least 30 seconds after the deadline, so don't rely on deadlines shorter than a minute.
  • Both must be in the future, and expires_at must be after send_at.
  • Webhooks: get message.sent and message.failed instead of polling.
  • Media: upload files to send by media_id.
  • Numbers: the id to use as from.
  • Errors