Skip to content

Campaigns

A campaign = message + audience + schedule. Compose in the dashboard or via POST /v1/apps/:appId/campaigns.

Scheduling options

  • Send now / schedule — one-shot delivery.
  • Recurrencedaily or weekly; each occurrence gets its own send ledger, so the same audience is safely re-targeted.
  • Subscriber timezone (deliverInSubscriberTz) — everyone receives the campaign at the scheduled wall-clock time in their timezone, swept over a 26-hour world-clock window.
  • Optimal time (optimalTime) — each subscriber receives it at their personally best engagement hour, learned from 90 days of opens/clicks in their timezone (circularly smoothed hourly histogram, minimum-sample gated, refreshed hourly). Subscribers without enough history send at campaign start; the 25-hour window closes with a catch-up pass.
  • Throttled rollout (sendRatePerMinute) — cap devices enqueued per minute for gradual releases.

Timezone, optimal-time and throttling are mutually exclusive.

A/B testing

Attach up to four variant messages with percentage splits; subscribers are bucketed deterministically at fan-out. GET /campaigns/:id/variants returns the per-variant funnel and the winner by open rate.

Personalization

Liquid templating from subscriber tags in title and body:

{{ tags.first_name | default: "there" }}, your {{ tags.plan }} renews soon

Multi-language

Messages accept per-language overrides, chosen by the subscriber's language tag with region → base-language → default fallback:

json
{ "title": "Big Sale", "body": "50% off", "localizations": { "ms": { "title": "Jualan Besar", "body": "Diskaun 50%" } } }

Measuring

  • Funnel: GET /campaigns/:id/stats — sent, delivered, opened, clicked, bounced + rates. Several side by side: GET /campaigns/stats?ids=a,b,c (Campaigns page → tick rows → "Compare ticked"); all of them as CSV: GET /campaigns/export (Campaigns page → "Export CSV").
  • Why sends did not land: GET /campaigns/:id/delivery-issues — bounced, suppressed and deferred counts per reason (the worker's reasons: suppression list, frequency cap, quiet hours, org daily cap, channel preference, a bounce's provider reason) plus failed per reason from the dead-letter ledger, largest first. Campaign page → "Delivery issues".
  • Conversions: GET /campaigns/:id/outcomes?event=purchase&window=24 — attributed within a window after open/click.
  • Completion notifications: Slack / webhooks via integrations.

App inbox (message center)

Tick Also deliver to app inbox in the composer (or mirrorToInbox: true on POST /campaigns) and every recipient also gets a persistent inbox item — snapshotted at fan-out, so the A/B variant a subscriber received is exactly what their inbox shows, and later message edits never rewrite history. One item per subscriber regardless of device count; scheduler retries never duplicate.

For inbox-only messages (order updates, account notices) post directly: POST /v1/apps/:appId/subscribers/:id/inbox {title, body, imageUrl?, deepLink?}.

Clients read the feed with the SDK: WingBlaze.getInbox() returns {items, unread} newest-first and WingBlaze.markInboxRead(itemId) clears the badge. Journey sends don't mirror yet.

Teknokrat Niaga Sdn Bhd · No. 28-1, Jalan SP 5/2, BSP Business Center, 42610 Bandar Saujana Putra, Selangor, Malaysia · +60 3-5614 3169 · Enquiries: info@teknoniaga.com