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.url must be a publicly fetchable https:// URL (signed URLs from your storage work well).
  • Omitting document on a document-header template returns document_required; supplying it on a plain template returns document_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

PolicyBehaviour
skipRecipients without a document are recorded as failed with document_missing; everyone else sends.
blockNothing 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" with scheduled_at queues the run (up to 90 days ahead).
  • test_mode: true validates the audience and counts without queueing anything.
  • Every send is deduplicated, suppression-checked, and rendered per recipient before it reaches WhatsApp.