API v2 (beta)
Reminders
A reminder sends the same message to one recipient again and again on a schedule: rent every month, a class every Monday, a report every evening. The schedule is a 5-part cron expression in a time zone you choose. Reminders made with the API are the same as those on the Reminders page of the dashboard.
| Operation | Endpoint | Scope |
|---|---|---|
| Create a reminder | POST /api/v2/reminders | reminders |
| List reminders | GET /api/v2/reminders | reminders |
| Get a reminder | GET /api/v2/reminders/{id} | reminders |
| Change, pause or resume | PATCH /api/v2/reminders/{id} | reminders |
| Delete a reminder | DELETE /api/v2/reminders/{id} | reminders |
Creating and changing reminders needs a plan that includes reminders; otherwise it answers 403 plan_required. Listing, reading, pausing and deleting work on any plan. For a message that goes out once at a set time, use send_at in Send a message instead.
Create a reminder#
https://wbiztool.com/api/v2/remindersBody: JSON (application/json)
curl -X POST https://wbiztool.com/api/v2/reminders \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rent-flat-4b" \
-d '{
"name": "Weekly rent",
"from": 101,
"to": { "phone": "+919876543210" },
"text": "Hi, a reminder that the rent is due on Friday.",
"schedule": { "cron": "0 9 * * 1", "timezone": "Asia/Kolkata" }
}'import os
import requests
response = requests.post(
"https://wbiztool.com/api/v2/reminders",
headers={
"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}",
"Idempotency-Key": "rent-flat-4b",
},
json={
"name": "Weekly rent",
"from": 101,
"to": {"phone": "+919876543210"},
"text": "Hi, a reminder that the rent is due on Friday.",
"schedule": {"cron": "0 9 * * 1", "timezone": "Asia/Kolkata"},
},
timeout=30,
)
reminder = response.json()
print(response.status_code, reminder.get("id"), reminder.get("next_run_at"))// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const response = await fetch("https://wbiztool.com/api/v2/reminders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "rent-flat-4b",
},
body: JSON.stringify({
name: "Weekly rent",
from: 101,
to: { phone: "+919876543210" },
text: "Hi, a reminder that the rent is due on Friday.",
schedule: { cron: "0 9 * * 1", timezone: "Asia/Kolkata" },
}),
});
const reminder = await response.json();
console.log(response.status, reminder.id, reminder.next_run_at);<?php
$ch = curl_init('https://wbiztool.com/api/v2/reminders');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: rent-flat-4b',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Weekly rent',
'from' => 101,
'to' => ['phone' => '+919876543210'],
'text' => 'Hi, a reminder that the rent is due on Friday.',
'schedule' => ['cron' => '0 9 * * 1', 'timezone' => 'Asia/Kolkata'],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$reminder = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $reminder['id'] . ' next: ' . $reminder['next_run_at'];Answer (201):
{
"id": 812,
"name": "Weekly rent",
"active": true,
"to": { "phone": "+919876543210" },
"from": 101,
"text": "Hi, a reminder that the rent is due on Friday.",
"media": null,
"schedule": { "cron": "0 9 * * 1", "timezone": "Asia/Kolkata" },
"next_run_at": "2026-10-19T09:00:00+05:30",
"created_at": "2026-10-18T10:20:11.004Z"
}
Body
namestringrequiredA name for the reminder, up to 100 characters, as shown on the Reminders page.
toobjectrequiredExactly one of
phone(international format with+),group(a WhatsApp group's exact name),contact_idorusername(Telegram), as in Messages → Recipients.frominteger or nullRequired for a key limited to some numbersThe
idof the number that sends it. It must be connected when you set it. Left out ornull: any connected number of the workspace sends it.textstringrequiredThe message, up to 3,000 characters. It's also the caption when
mediais sent.mediaobject or nulloptionalAn image or file to send each time:
urlormedia_id, and optionallytype, as in Messages → Media.scheduleobjectrequiredcron(required) andtimezone. See Schedule.activebooleanoptionaltrue(default) runs it;falsecreates it paused.
Schedule#
schedule.cron has five parts separated by spaces: minute, hour, day of the month, month, day of the week. * means "every". It must be a schedule that runs at some point (not 31 February).
cron | Runs |
|---|---|
0 9 * * 1 | Mondays at 09:00 |
30 18 * * * | Every day at 18:30 |
0 10 1 * * | The 1st of every month at 10:00 |
0 9 * * 1-5 | Weekdays at 09:00 |
0 8,20 * * * | Every day at 08:00 and 20:00 |
0 9 15 1 * | 15 January at 09:00, every year |
schedule.timezone is an IANA time zone name, such as Asia/Kolkata, Europe/London or America/New_York (default UTC). The cron times are in that zone, and next_run_at is given in it too, with its offset. See the time zone reference.
Each run sends a message like any other: it needs messages left on the plan and shows up in List messages.
Errors when creating#
| HTTP | Code | When |
|---|---|---|
400 | validation_failed | A required field is missing or empty, a field is unknown or too long (name, to.group), or media isn't valid |
400 | text_required | text is empty |
400 | text_too_long | text is over 3,000 characters |
400 | invalid_cron | schedule.cron isn't 5 parts, or never runs |
400 | invalid_timezone | schedule.timezone isn't an IANA time zone |
403 | plan_required | The plan doesn't include reminders |
403 | number_not_allowed | The key is limited to some numbers and from isn't one of them, or is missing |
422 | invalid_phone | to.phone isn't a valid number in international format with + |
422 | invalid_username | to.username isn't a Telegram username, link or tg: id |
422 | username_needs_telegram | A Telegram username, but from isn't a Telegram account (or, without from, no Telegram account is connected) |
422 | contact_not_found | No contact with that to.contact_id |
422 | media_not_found | No file with that media.media_id |
422 | invalid_media_url | media.url isn't a public http(s) link |
422 | number_not_connected | from isn't a connected number of this workspace |
List reminders#
https://wbiztool.com/api/v2/remindersThe workspace's reminders, newest first, in pages (limit 1 to 100, default 25, and cursor). Paused ones are included, with "active": false. A key limited to some numbers sees only the reminders that send from them.
curl "https://wbiztool.com/api/v2/reminders?limit=50" \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
The answer is {"data": [Reminder, …], "next_cursor": "…" or null}.
Get a reminder#
https://wbiztool.com/api/v2/reminders/{id}curl https://wbiztool.com/api/v2/reminders/812 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
A reminder that doesn't exist, was deleted, or sends from a number the key may not use answers 404 reminder_not_found.
Change, pause or resume a reminder#
https://wbiztool.com/api/v2/reminders/{id}Body: JSON (application/json)
Send only the fields that change; the others keep their values. The fields are those of Create a reminder. The answer is 200 with the reminder as it is now.
- Pause:
{"active": false}. It stops running andnext_run_atbecomesnull. Pausing works on any plan. - Resume:
{"active": true}. It runs again from its next time. Resuming needs a plan with reminders. scheduleis replaced as a whole: sendcronwith it. Withouttimezone, the reminder keeps its current one.
# Pause
curl -X PATCH https://wbiztool.com/api/v2/reminders/812 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active": false}'
# Move it to Fridays at 10:00 and change the text
curl -X PATCH https://wbiztool.com/api/v2/reminders/812 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"schedule": {"cron": "0 10 * * 5"}, "text": "Hi, the rent is due today."}'import os
import requests
headers = {"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"}
url = "https://wbiztool.com/api/v2/reminders/812"
paused = requests.patch(url, headers=headers, json={"active": False}, timeout=30).json()
print(paused["active"], paused["next_run_at"]) # False None
resumed = requests.patch(url, headers=headers, json={"active": True}, timeout=30).json()
print(resumed["active"], resumed["next_run_at"])// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const response = await fetch("https://wbiztool.com/api/v2/reminders/812", {
method: "PATCH",
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ active: false }),
});
const reminder = await response.json();
console.log(reminder.active, reminder.next_run_at); // false null<?php
$ch = curl_init('https://wbiztool.com/api/v2/reminders/812');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['active' => false]),
CURLOPT_RETURNTRANSFER => true,
]);
$reminder = json_decode(curl_exec($ch), true);
curl_close($ch);
var_dump($reminder['active']);A change answers with the same errors as creating, and 404 reminder_not_found. If one field is wrong, nothing is changed.
Delete a reminder#
https://wbiztool.com/api/v2/reminders/{id}Deletes the reminder: it never runs again. Messages it already sent stay in the history. To stop it for a while instead, pause it.
curl -X DELETE https://wbiztool.com/api/v2/reminders/812 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
{ "id": 812, "deleted": true }
The Reminder object#
| Field | Type | Description |
|---|---|---|
id | integer | The reminder's id. |
name | string | Its name. |
active | boolean | true while it runs, false while it's paused. |
to | object | The recipient: phone (with +), group or username. A reminder made with contact_id shows the contact's number or group. |
from | integer or null | The sending number's id, or null for any connected number. |
text | string | The message. |
media | object or null | type (image or file), url and file_name. |
schedule | object | cron and timezone. |
next_run_at | string or null | When it runs next, in its time zone with the offset, such as 2026-10-19T09:00:00+05:30. null while it's paused. |
created_at | string | When it was made (UTC). |
Related#
- Reminders in the dashboard
- Time zone reference
- Messages: one message at a set time with
send_at - Errors
