The Bulk email API lets your system create and monitor bulk customer email campaigns in PracBill, using the same recipient selection rules and queued delivery pipeline as the platform's Customer module. Base URL: https://billing.pracbill.com.au/api/:tokenid/customer/bulk-email....
Key concepts. Nothing is sent while your API call runs. Creating a campaign only queues it, and PracBill's background sender delivers the mail on its next run. Recipient lists are always resolved server-side from filters: you never upload email addresses, and only active customers with a valid email address in your own department can ever be selected.
This API sends real email to real customers. Always use the preview endpoint to check the recipient count and rendered message before creating a campaign, and pass
confirm_counton creation so the campaign fails safely if the customer base changed in between. Cancellation stops future delivery only: it cannot recall messages already sent.
All endpoints use your standard PracBill API token in the URL path (see Getting started with the API):
https://{your-pracbill-host}/api/{token}/customer/bulk-email...
In addition, your API key must be explicitly granted the bulk email capability by a PracBill administrator: the key's data must contain "bulk_email": true (boolean). Keys without it receive capability_denied on every bulk-email endpoint.
Your API key belongs to exactly one department, and every operation is locked to it. Do not send a deptid field in any request; it will be rejected with deptid_not_allowed.
Every response is JSON. Successes carry "success": true; failures carry a stable machine-readable error_code plus a human-readable description (which may change, so branch on error_code, not the text):
{ "success": true, "description": "ok", ... }
{ "success": false, "error_code": "no_recipients", "description": "the filter selection matched no recipients" }
confirm_count set to the confirmed count.GET /:tokenid/customer/bulk-email/filtersReturns the valid filter fields and your department's option values.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenid |
string | Yes | Your API access token. |
Request
curl 'https://billing.pracbill.com.au/api/892f86d5-da86-4e98-9343-1976a375/customer/bulk-email/filters'
Response
{
"success": true,
"description": "ok",
"fields": [
{ "key": "suburb", "label": "Primary Address Suburb", "type": "text", "options": {} },
{ "key": "street", "label": "Primary Address Street", "type": "text", "options": {} },
{ "key": "billinggroup", "label": "Billing Group", "type": "select", "options": { "wholesale": "Wholesale" } },
{ "key": "account_state", "label": "Account Status", "type": "select", "options": { "": "Normal", "Suspended": "Suspended", "Disconnected": "Disconnected" } },
{ "key": "billingcycle", "label": "Billing Cycle", "type": "select", "options": { "monthly": "Monthly" } },
{ "key": "business_type", "label": "Business Type", "type": "text", "options": {} },
{ "key": "account_manager", "label": "Account Manager", "type": "select", "options": { "5": "Jane Smith" } },
{ "key": "reseller_cid", "label": "Reseller (Customer ID)", "type": "number", "options": {} },
{ "key": "paymethod", "label": "Payment Method", "type": "select", "options": { "invoice": "Invoice" } }
],
"joins": ["AND", "OR"]
}
For select fields, send one of the option keys as the value. text fields match as a substring; number fields match exactly. Option lists for lookup-backed fields are specific to your department, so always read them from this endpoint rather than hard-coding them. Note that account_state with a blank value is meaningful: it means the "Normal" account status.
POST /:tokenid/customer/bulk-email/previewResolves your filter selection and returns the full recipient count plus a capped sample. Optionally renders the message for the first recipient exactly as delivery will render it. Never creates anything. Send a JSON body with Content-Type: application/json.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenid |
string | Yes | Your API access token. |
filters |
array | No | Rows combine left-to-right with each row's join (AND default, OR allowed). Omit it to select all eligible customers in your department. Unknown fields and blank values are silently ignored. |
customer_ids |
array | No | Only ever narrows the filtered set; it can never add a customer the filters or eligibility rules would exclude. An explicitly supplied empty array means "nobody". |
sample_limit |
integer | No | Sample size returned. Default 25, maximum 1000, 0 allowed for a count-only response. |
subject |
string | No | With content: when both are non-empty the response includes a preview rendered for the first recipient. |
content |
string | No | See subject. |
Request
{
"filters": [
{ "field": "suburb", "value": "Carlton", "join": "AND" },
{ "field": "account_state", "value": "", "join": "AND" }
],
"customer_ids": [1234, 5678],
"sample_limit": 25,
"subject": "Hello {{name}}",
"content": "<p>Dear {{name}} {{surname}} of {{company}}, ...</p>"
}
Response
{
"success": true,
"description": "ok",
"count": 342,
"sample_limit": 25,
"truncated": true,
"recipients": [
{ "cid": 1234, "company": "Acme Pty Ltd", "name": "Jane", "surname": "Smith", "email": "jane@acme.example" }
],
"preview": {
"cid": 1234,
"subject": "Hello Jane",
"content": "<p>Dear Jane Smith of Acme Pty Ltd, ...</p>"
}
}
POST /:tokenid/customer/bulk-emailValidates the request, re-resolves the recipients server-side, and queues the campaign. The background sender delivers it; the API never sends mail inline. Send a JSON body with Content-Type: application/json.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenid |
string | Yes | Your API access token. |
subject |
string | Yes | Non-empty. Send it raw; PracBill HTML-encodes the subject at delivery time. |
content |
string | Yes | Non-empty. The message body; HTML is supported. |
filters |
array | No | Same semantics as the preview endpoint. |
customer_ids |
array | No | Same semantics as the preview endpoint. |
confirm_count |
integer | No | Strongly recommended. If present it must equal the recipient count resolved at creation time, otherwise the request fails with recipient_count_changed and nothing is created. |
scheduled_at |
string | No | Local Australia/Melbourne time, formats Y-m-d, Y-m-d H:i or Y-m-d H:i:s (a T may replace the space). Maximum 90 days ahead; invalid calendar dates are rejected. Omitted or past values mean "send on the sender's next run"; a bare date means midnight. |
attachments |
No | Not supported. Any value fails with attachments_not_supported. |
Merge fields: {{field}} placeholders in the subject and content are replaced per recipient from the customer record. Common ones are {{name}}, {{surname}}, {{company}} and {{email}}. A placeholder whose field is empty for a customer is left unreplaced, so preview with real data first.
Request
{
"subject": "Planned maintenance on {{company}} services",
"content": "<p>Dear {{name}},</p><p>...</p>",
"filters": [ { "field": "billinggroup", "value": "wholesale", "join": "AND" } ],
"customer_ids": [1234, 5678],
"confirm_count": 342,
"scheduled_at": "2026-09-01 09:00"
}
Response
{
"success": true,
"description": "ok",
"id": 9876,
"status": "queued",
"recipient_count": 342,
"scheduled_at": "2026-09-01 09:00:00"
}
status is pending when the campaign is scheduled for the future, otherwise queued.
| Safeguard | Default | Error code |
|---|---|---|
| Maximum recipients per campaign | 5000 | too_many_recipients |
| Maximum campaigns per rolling 24 hours | 10 | daily_campaign_limit_reached |
| Identical subject within 60 seconds | always on | duplicate_campaign |
The recipient and daily limits are configurable per API key by your PracBill administrator (bulk_email_max_recipients, bulk_email_max_daily_campaigns). The daily cap and duplicate check are counted per department, so campaigns created in the PracBill UI count toward them too.
GET /:tokenid/customer/bulk-email/:idReturns the current state and delivery counts of a campaign.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenid |
string | Yes | Your API access token. |
id |
integer | Yes | Campaign ID returned by the create endpoint. |
Response
{
"success": true,
"description": "ok",
"id": 9876,
"subject": "Planned maintenance",
"status": "sending",
"scheduled_at": "2026-09-01 09:00:00",
"started_at": "2026-09-01 09:02:11",
"ended_at": null,
"created_at": "2026-08-23 14:00:00",
"counts": { "recipients": 342, "sent": 120, "pending": 222, "failed": 0, "cancelled": 0 }
}
| Status | Meaning |
|---|---|
queued |
Accepted; ready for the sender's next run |
pending |
Accepted; scheduled for a future time |
sending |
The background sender is working through it |
completed |
The sender finished the campaign |
cancelled |
Ended early; unsent recipients were withdrawn |
In counts, recipients is the original total (withdrawn rows included), sent were delivered to the mail system, failed were attempted unsuccessfully, cancelled were withdrawn by a cancel call and pending is the remainder. All datetimes are PracBill server-local strings, not UTC.
A campaign id that does not exist, or that belongs to another department, returns not_found; the two cases are deliberately indistinguishable.
GET /:tokenid/customer/bulk-email/page/:pageNumberReturns pages of 20 campaigns, newest first. Each record has the same shape as the status endpoint's response.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenid |
string | Yes | Your API access token. |
pageNumber |
integer | Yes | Page number, starting at 1. |
Response
{
"success": true,
"description": "ok",
"page_number": 1,
"page_size": 20,
"total": 57,
"records": [ { "id": 9876, "subject": "...", "status": "completed", "counts": { } } ]
}
POST /:tokenid/customer/bulk-email/:id/cancelWithdraws all unsent recipients, then ends the campaign. No request body is required.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenid |
string | Yes | Your API access token. |
id |
integer | Yes | Campaign ID. |
Response
{
"success": true,
"description": "ok",
"id": 9876,
"status": "cancelled",
"sent_count": 120,
"cancelled_count": 222,
"recipient_count": 342,
"note": "Cancellation prevents further delivery only. Messages already sent cannot be recalled, and a message the sender was transmitting when this request arrived may still be delivered."
}
A completed campaign cannot be cancelled (campaign_completed); cancelling twice returns already_cancelled. If the campaign was actively sending, treat sent_count as a floor: one in-flight message may still be delivered.
| error_code | Endpoint(s) | Meaning |
|---|---|---|
invalid_key |
all | API token unknown or disabled |
capability_denied |
all | Key lacks "bulk_email": true; ask your PracBill administrator to grant it |
deptid_not_allowed |
preview, create | A deptid was supplied; the key's department is always used |
invalid_request |
create | Request body was not valid JSON |
attachments_not_supported |
create | Attachments are not supported |
missing_subject |
create | Subject is required |
missing_content |
create | Content is required |
invalid_schedule |
create | Bad scheduled_at format/date, or more than 90 days ahead |
no_recipients |
create | The filter selection matched no recipients |
recipient_count_changed |
create | Recipient count differs from confirm_count; re-preview and confirm again |
too_many_recipients |
create | Selection exceeds the per-campaign recipient limit |
daily_campaign_limit_reached |
create | Rolling 24-hour campaign cap reached; do not retry in a loop |
duplicate_campaign |
create | Identical subject queued under 60 seconds ago (likely a double submit; check history before retrying) |
create_failed |
create | Server-side insert failed; safe to retry once |
not_found |
status, cancel | Campaign missing or not owned by your department |
already_cancelled |
cancel | Campaign is already cancelled |
campaign_completed |
cancel | Campaign already finished; nothing left to cancel |
If you are wiring this API into an AI agent or LLM-based automation, a self-contained machine-readable guide is available: bulk-email-api-agent-guide.md. Download it and give it to your agent as context. It contains the full endpoint schemas, the complete error-code table with recommended agent actions, and the required safe workflow (preview, human confirmation, create with confirm_count, poll, cancel).
Related: Getting started with the API, API overview, Customer API.