Campaigns and statements
A campaign fans one approved template out to an audience. The flagship pattern is the monthly statement run: every customer receives the same template with their own variables and their own PDF.
Send one document message
single.ts
await bt.messages.send({
channel: "whatsapp",
to: { phone: "+27821234567" },
template_id: "<document-header template id>",
template_variables: { "1": "Sam", "2": "May 2026" },
document: {
url: "https://files.example.com/statements/acct-1001.pdf",
filename: "Statement May 2026.pdf",
},
});document.urlmust be a publicly fetchablehttps://URL (signed URLs from your storage work well).- Omitting
documenton a document-header template returnsdocument_required; supplying it on a plain template returnsdocument_not_applicable.
Bulk: documents from a contact field
Store each customer's statement URL on their contact record (a custom field like statement_url), then create the campaign with a document_source:
campaign.ts
await bt.campaigns.create({
name: "May statements",
channel: "whatsapp",
audience: { type: "list", list_id: "<contact list id>" },
template_id: "<document-header template id>",
template_variables: { "1": { source: "first_name" } },
document_source: {
mode: "url_field",
field_key: "statement_url",
filename_template: "Statement {{first_name}}.pdf",
missing_policy: "skip",
},
mode: "now",
});Missing-document policy
| Policy | Behaviour |
|---|---|
skip | Recipients without a document are recorded as failed with document_missing; everyone else sends. |
block | Nothing sends if anyone is missing a document — the API returns document_missing_blocked with a count, and no campaign is created. |
The library mode
Operators can also bulk-upload PDFs in the dashboard (filenames are matched to contacts by phone or account number); campaigns then use document_source.mode: "library"to attach each recipient's newest uploaded document. Uploaded documents are retained for 90 days.
Scheduling and validation
mode: "schedule"withscheduled_atqueues the run (up to 90 days ahead).test_mode: truevalidates the audience and counts without queueing anything.- Every send is deduplicated, suppression-checked, and rendered per recipient before it reaches WhatsApp.