Botzo Developers
Guide sections: Contacts, templates and campaigns

Contacts, templates and campaigns

These requests connect Botzo to your system (store, CRM, booking system): add customers and organize them in groups, list your templates, and launch a campaign to a whole group.

Required permission: Full access (reading also works with "Read only").

Lists and pages

All list requests (GET) are paginated:

Parameter Default Description
page 1 Page number
per_page 10 Items per page, up to 100

They return:

{
  "data": [ { "...": "..." } ],
  "links": { "first": "…?page=1", "last": "…?page=3", "prev": null, "next": "…?page=2" },
  "meta": { "current_page": 1, "last_page": 3, "per_page": 10, "total": 27 }
}

To walk through everything: start at page=1 and continue while links.next is not null.

Contacts

Request Description
GET /api/contacts List contacts
POST /api/contacts Add a contact
PUT /api/contacts/{uuid} Update a contact
DELETE /api/contacts/{uuid} Delete a contact

Fields (add and update)

Field Type Required Description
first_name string yes when adding First name
last_name string no Last name
phone string yes Valid international number, unique in your organization
email string no Email
group array no Group uuids to join: ["6f1c…", "a92b…"]
metadata object no Your own fields, such as {"city": "Riyadh", "tier": "gold"}. Usable in campaign placeholders
curl -X POST https://botzo.net/api/contacts \
  -H "Authorization: Bearer $BOTZO_API_KEY" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{
    "first_name": "Sara",
    "last_name": "Ali",
    "phone": "+966501234567",
    "email": "sara@example.com",
    "metadata": {"city": "Riyadh", "tier": "gold"}
  }'
const res = await fetch('https://botzo.net/api/contacts', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.BOTZO_API_KEY}`, Accept: 'application/json', 'Content-Type': 'application/json' },
  body: JSON.stringify({ first_name: 'Sara', phone: '+966501234567', metadata: { city: 'Riyadh' } }),
});
const { id } = await res.json(); // the contact's uuid
$id = Http::withToken(env('BOTZO_API_KEY'))->acceptJson()
    ->post('https://botzo.net/api/contacts', ['first_name' => 'Sara', 'phone' => '+966501234567'])
    ->json('id');
r = requests.post("https://botzo.net/api/contacts", headers=HEADERS,
                  json={"first_name": "Sara", "phone": "+966501234567"})
contact_uuid = r.json()["id"]

Response (add, update or delete):

{
  "statusCode": 200,
  "id": "dd0d957b-9f2c-4bcb-aa3a-a1a17388ff11",
  "message": "Request processed successfully"
}

An item from GET /api/contacts:

{
  "uuid": "dd0d957b-9f2c-4bcb-aa3a-a1a17388ff11",
  "first_name": "Sara",
  "last_name": "Ali",
  "full_name": "Sara Ali",
  "phone": "+966501234567",
  "formatted_phone_number": "+966 50 123 4567",
  "email": "sara@example.com",
  "channel": "whatsapp",
  "metadata": "{\"city\":\"Riyadh\"}",
  "unread_messages": 0,
  "reply_context": {
    "reply_window_open": false,
    "can_send_text": false,
    "can_send_media": false,
    "can_send_template": true
  },
  "created_at": "2026-10-22 21:23:06"
}

reply_context.reply_window_open tells you whether the 24-hour window is open with this customer right now, so you know in advance whether to send text or a template.

Error Cause
400 validation_error with errors.phone Invalid number, or it already exists
403 contacts_limit_reached Your plan's contact limit is reached
404 not_found The uuid is not in your organization

Groups

Request Description
GET /api/contact-groups List groups
POST /api/contact-groups Create a group: {"name": "VIP customers"} (unique name)
PUT /api/contact-groups/{uuid} Rename
DELETE /api/contact-groups/{uuid} Delete the group (the contacts stay)

The response matches contacts: {"statusCode": 200, "id": "<uuid>", "message": "..."}. To put a customer in a group, send group when adding or updating them.

Auto-replies

Fixed replies Botzo sends when a customer's message contains a keyword.

Request Description
GET /api/canned-replies List
POST /api/canned-replies Add
PUT /api/canned-replies/{uuid} Update
DELETE /api/canned-replies/{uuid} Delete
Field Values Description
name text Internal name
trigger text The keyword, such as hours
match_criteria exact match or contains The message equals the keyword, or contains it
response_type text, image or audio Reply type
response text or file The text, or an uploaded file (multipart/form-data) for image and audio
curl -X POST https://botzo.net/api/canned-replies \
  -H "Authorization: Bearer $BOTZO_API_KEY" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"name": "Opening hours", "trigger": "hours", "match_criteria": "contains", "response_type": "text", "response": "We are open every day from 9am to 11pm."}'

Templates

GET /api/templates

{
  "data": [
    {
      "uuid": "1d3f9a0e-…",
      "name": "order_shipped",
      "metadata": "{\"name\":\"order_shipped\",\"language\":\"ar\",\"status\":\"APPROVED\",\"category\":\"UTILITY\",\"components\":[{\"type\":\"BODY\",\"text\":\"Hi {{1}}, your order {{2}} is on its way.\"}]}",
      "updated_at": "2026-10-22 21:23"
    }
  ],
  "links": { "...": "..." },
  "meta": { "...": "..." }
}

metadata is a JSON string with the full template definition as in Meta: language, status, category, components and placeholders. Use its name and language when sending a template. Templates are created and edited in the dashboard.

Campaigns

POST /api/campaigns

Sends one template to everyone in a group (or all contacts), now or at a set time. Botzo sends in batches within WhatsApp's limits and tracks each customer's result in the dashboard.

Field Type Required Description
template string yes Template uuid from GET /api/templates
contacts string yes Group uuid, or all for all contacts
name string no Campaign name (defaults to "API Campaign" with the date)
skip_schedule boolean no true (default) to send now, false to schedule
time string when scheduling Send time in your organization's timezone, such as 2026-11-01 10:00
header object depends on template Header placeholders
body object depends on template Body placeholders
buttons array depends on template Button placeholders

Campaign placeholders

Because a campaign goes to many customers, each placeholder is either the same for everyone or filled from each customer's data:

{
  "body": {
    "parameters": [
      { "type": "text", "selection": "dynamic", "value": "first name" },
      { "type": "text", "selection": "static", "value": "20%" }
    ]
  }
}
selection value Result
static any text The same text for everyone
dynamic first name, last name, name, phone, email From the customer's data
dynamic field:city (any key in metadata) From your own fields on the customer

Image header: "header": {"parameters": [{"type": "IMAGE", "value": "https://cdn.example.com/offer.jpg"}]} (or VIDEO, DOCUMENT).

URL button with a variable part: "buttons": [{"type": "URL", "parameters": [{"type": "static", "value": "summer"}]}]. Button types: URL, QUICK_REPLY, COPY_CODE; inside parameters, type is static or dynamic.

curl -X POST https://botzo.net/api/campaigns \
  -H "Authorization: Bearer $BOTZO_API_KEY" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{
    "name": "Weekend offer",
    "template": "1d3f9a0e-7c2b-4f3e-9a51-2b8f0c6d4e11",
    "contacts": "6f1c2d3e-…",
    "skip_schedule": false,
    "time": "2026-11-01 10:00",
    "body": {"parameters": [
      {"type": "text", "selection": "dynamic", "value": "first name"},
      {"type": "text", "selection": "static", "value": "20%"}
    ]}
  }'
await fetch('https://botzo.net/api/campaigns', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.BOTZO_API_KEY}`, Accept: 'application/json', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    template: '1d3f9a0e-7c2b-4f3e-9a51-2b8f0c6d4e11',
    contacts: 'all',
    body: { parameters: [{ type: 'text', selection: 'dynamic', value: 'first name' }] },
  }),
});

Response:

{
  "statusCode": 200,
  "id": "c0a8012e-…",
  "message": "Campaign created successfully!",
  "data": {
    "campaign_status": "scheduled",
    "scheduled_at_utc": "2026-11-01 07:00:00",
    "contacts_count": 248
  }
}
Error Cause
404 template_not_found Wrong template uuid
404 contact_group_not_found Wrong group uuid
422 campaign_audience_empty The group is empty
403 campaign_limit_reached Your plan's campaign limit is reached
403 whatsapp_not_connected No WhatsApp number connected yet

Campaign pace: messages go out in batches every few minutes, and a scheduled campaign may start a few minutes after its time. Each customer's result (sent or failed, and why) is in the dashboard under Campaigns.