Threadwire by TriTech

← API reference

Broadcasts

A broadcast sends one message, such as an offer or news, to many people who opted in. That means people who wrote to the instance. Each person gets their own message, one at a time, the way a person would send them. A broadcast pauses itself if the instance is at risk. That is why it is safe to use, where a loop of POST /v1/messages to a list is not.

Who gets it

Only people who opted in to you on that instance:

  • recent: everyone who wrote to it in the last days (30 by default, which is also as far back as messages are kept), oldest first. Sending a code to verify a phone does not count as writing.
  • label: the chats with one of your chat labels (label_id), as your team puts them on the panel's Chats page.
  • phones: your own recipients list, filtered to the people on it who wrote to the instance. With include_saved_contacts: true, saved contacts on the phone who never wrote may stay in too. Each is checked on the phone when their turn comes. They count as new chats, with every limit on new chats. The text must have a placeholder other than {phone}, and a saved contact with no name or field for it is left out (nothing_personal).

Nobody else is ever messaged:

  • Never written: a phone that never wrote to the instance is left out, and is never looked up on WhatsApp.
  • Opted out: someone who wrote "STOP", before the broadcast or during it, is left out. They stay out of every broadcast, even after they write to you again, until they send "START" (or "subscribe", "اشترك", "ابدأ").
  • Groups and channels: a broadcast never goes to one.
  • Your own instances: never.

Narrowing. On the panel's New broadcast page you can narrow any of these to the chats with a label, or to people whose contact field holds a value (say, City is Giza). Narrowing only ever takes people out: everyone left is someone the broadcast would go to anyway. The broadcast then says what it was narrowed to in narrowed_to, and counts the people it took out as not_matching. A broadcast takes at most 5,000 people, and narrowing chooses among those: with more, only the first 5,000 (those who wrote longest ago, or the first on your list) are narrowed. The API does not narrow yet.

The answer says how many people were left out, and why, under left_out: never_wrote, opted_out, not_on_whatsapp, invalid, duplicate, own_number, no_phone and over_limit. A broadcast takes at most 5,000 people.

What it says

text is up to 1,000 characters, with these placeholders:

Placeholder Becomes
{name} The person's name from your list, or else the name they show on WhatsApp
{phone} Their number, like +201012345678
{city} Any of the fields you give a recipient, by its name
{customer_number} Any of your contact fields, by its key: what your team wrote about that person. A field of your list with the same name wins

Add words for when a value is empty after a |: Hi {name|there}. A contact field with nothing for someone is empty too, exactly as a missing name is. Every message ends with an opt-out line in the language you choose:

  • en: "Reply STOP to stop these messages."
  • ar: "أرسل «توقف» لإيقاف هذه الرسائل."

With the opt-out line included, each message may be at most 600 characters. A broadcast may not carry an http:// link (use https://), or be only a link.

Make it personal. When the text has no placeholder, everyone gets the same words. WhatsApp's spam detection looks for exactly that, so the answer carries a warning in warnings.

One image, video or document may go with it in media (url, or base64 data, with mimetype and an optional filename), up to 16 MB, with the text as its caption. As data it counts toward the account's files as base64 for the day. A voice note cannot be sent, because it has no room for the opt-out line.

How it goes

curl https://threadwire.net/api/v1/broadcasts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"instance_id": "YOUR_INSTANCE_ID", "audience": "recent", "days": 14,
       "text": "Hi {name}, our autumn menu starts today. Show this message for a free coffee."}'

It answers 201 with the broadcast, running (or scheduled with start_at). Then:

  • One at a time. The next message is made only once the last has gone, and interval_seconds after it. The interval is 60 by default, at most 600, and never under 20. Each gap is varied up to half again, so it does not tick like a machine.
  • Daytime only. Nothing is sent in the instance's quiet hours (22:00 to 08:00 its time by default).
  • A daily cap. It sends at most per_day messages a day (200 by default, at most 1,000). A new instance also has its own daily cap while it warms up. The rest continue the next day, so a long list takes several days.
  • Every rule of a normal send still applies. Each message is a normal message, sent with every rule in How sending protects your number. It sends the usual message.status events, with origin broadcast.
  • One broadcast at a time per instance. A second one gets a 409 until the first finishes or is paused.
  • Your plan. Each message counts toward your plan's messages like any other.

It pauses itself

A broadcast pauses by itself, with the reason in reason (code and text), when:

reason.code Why
whatsapp WhatsApp warned the instance (even its first warning), is limiting it, or refused a new chat
reply_score Almost none of the people the instance messaged first answered, or few in a week with enough of them to judge
safety The instance's safety meter dropped to caution
operator We paused new chats from the instance
opt_outs More than 2% of the people it reached asked to stop (at least 2 of them, once 20 messages went)
failures More than 10% of its messages failed (at least 3 of them, once 20 messages went)
file Its file could not be fetched from media.url
number_changed A different phone was linked to the instance. It cannot resume
not_included Broadcasts are not open on your account now. It can resume once they are

You get a broadcast.paused event, and your team gets an email. A message already on its way is held back: it fails with an error that says so, and goes again on resume.

Resuming: POST /v1/broadcasts/{id}/resume works only once the reason has cleared. Until then it answers 409 with what still stops it. After opt_outs or failures, it waits a day, and the counting starts afresh; meanwhile no other broadcast starts from that instance, even if you cancel this one. Check who it goes to and what it says first.

Follow and control it

  • GET /v1/broadcasts/{id}: status is draft, scheduled, running, paused, completed or cancelled. progress has recipients, pending and skipped (by why: opted_out, not_saved, cancelled, and no_longer_known for someone whose messages were deleted after 30 days, before their turn: write to them once they write again). It also counts messages queued, sent, delivered, read (each counting the messages that got at least that far), failed, replied and opted_out.
  • GET /v1/broadcasts lists them, newest first. Filter with instance_id and status.
  • POST /v1/broadcasts/{id}/pause pauses it.
  • DELETE /v1/broadcasts/{id} cancels it. Nobody else gets it, and a message still waiting is cancelled too.
  • broadcast.completed arrives when everyone has had theirs.

Neither event carries the text or anyone's number. Send an Idempotency-Key header with the start, so a retry never starts a second broadcast: it answers 200 with the first one and the header Idempotent-Replayed: true, whatever the retry's body. A key is its instance's own, compared exactly (case counts). Broadcasts are deleted 30 days after they finish, and so are their messages.