Threadwire by TriTech

← API reference

Sending messages

Everything is sent with one request, POST /v1/messages. It always needs instance_id and who it goes to: to, a phone in international format (like 201012345678), or chat_id for a group or channel. Then add one kind of content below.

The answer is immediate: 202 with status: "queued" (or "scheduled"). The message then goes out at a person's pace, and its status changes arrive as message.status webhooks. A message may wait, or fail with a reason: Waiting, errors and retries says when, for how long, and what to retry.

Text

{"instance_id": "…", "to": "201012345678", "text": "Hi Sara, your order 1042 is on its way."}

Up to 4,096 characters. WhatsApp shows a preview of the first link in the text, as in the app; add "link_preview": false to leave it out. Links are previewed only when they point to public addresses.

Images, documents, video and voice notes

Put the file in media, by address or as base64. text becomes its caption.

{"instance_id": "…", "to": "201012345678", "text": "Your ticket",
 "media": {"url": "https://example.com/ticket.pdf", "mimetype": "application/pdf", "filename": "ticket.pdf"}}
{"instance_id": "…", "to": "201012345678",
 "media": {"data": "JVBERi0xLjQK…", "mimetype": "application/pdf", "filename": "ticket.pdf"}}
  • The mimetype decides how it shows: image/… as a picture, video/… as a video, audio/… as a voice note (converted to the format WhatsApp plays), anything else as a document.
  • By address: the url must be a public http:// or https:// address on the usual port. We fetch the file when the message goes, following up to 3 redirects to public addresses, within 30 seconds; serve it uncompressed. If your server does not answer, or answers with an error, we try twice more over a few minutes, and then the message fails with the reason. An address inside a private network is refused with 422.
  • As base64: put the file's content in data instead of url, for files that are not public.
  • Size: up to 16 MB.
  • A day's files as base64: an account may send 2 GB of files as base64 data in a day, across its messages, broadcasts and statuses; 200 MB on the free trial and on the smallest plan. Past that, a request with data answers 429 with the code file_budget_used, and its Retry-After and message say when the day starts again. Files by url are not counted, so send large or many files that way.
  • The same rules apply to a sticker's, a status file's and a profile picture's url (a sticker up to 1 MB; a profile picture up to 2 MB, fetched at once, within 15 seconds).

Stickers, locations, contact cards and polls

Each goes instead of text:

{"sticker": {"url": "https://example.com/sticker.webp"}}
{"location": {"latitude": 30.0444, "longitude": 31.2357, "title": "Our office"}}
{"contact": {"name": "Sara Ali", "phone": "201012345678", "organization": "Acme"}}
{"poll": {"name": "Which day suits you?", "options": ["Sunday", "Monday"], "multiple_answers": false}}
  • Sticker: a WebP image of up to 1 MB, by url or base64 data. Stickers people send you arrive with type sticker and the image in media.
  • Poll: 2 to 12 options; multiple_answers lets people choose more than one. The message's poll shows results (votes per answer) and votes (each person's latest choice), and every vote, changed vote or vote taken back arrives as a message.poll_vote webhook.
  • A location's title and a contact card's organization are optional.
  • None of these four can start a chat with someone who never wrote to the instance: send a short text first.

Quote, schedule, groups

  • Quote a message: add "reply_to": "<message id>", a message in the same chat. It shows as a reply in WhatsApp, and reply_to is set on the message you sent. If the quoted message never went out, or WhatsApp cannot find it, yours goes without the quote.
  • Send later: add "send_at": "2026-10-01T09:00:00+03:00" (ISO 8601 with its offset; UTC without one; up to 14 days ahead). The message waits as scheduled, then goes like one sent at that moment, with every rule applied then.
  • A group or channel: use "chat_id": "…@g.us" (a group) or "…@newsletter" (a channel the instance runs) instead of to. See Chats, contacts and groups.
  • Someone who wrote without a phone number: WhatsApp sometimes hides a person's number, and their message.received then has "phone": null and a chat_id ending in @lid. Answer them with that chat_id instead of to. It works only as a reply, on the instance they wrote to: a chat_id nobody wrote from is refused (422), since a first message needs a phone number. We first learn whose chat it is, so the message shows their phone and every rule applies to the person: if they wrote "STOP" from their number, it is not sent. When WhatsApp cannot tell us the number at that moment, the request answers 503 with the code person_unknown and a Retry-After, and nothing is sent: try again.

Retries are safe

Send an Idempotency-Key header (any unique string of yours, up to 255 characters, such as your order id). A retry with the same key on the same instance returns the first message instead of sending a second, even when both arrive at once. The retry answers 200 with the header Idempotent-Replayed: true, and the first request wins whatever the retry's body: its body is not read, so use a new key for a different message. Keys are compared exactly (Order-7 and order-7 are two keys), and each instance has its own.

We never send a second copy either: if WhatsApp did not answer a send in time, we check whether it went before trying again, and if that cannot be checked, the message fails saying it may have been delivered. Look at the chat before sending it again. A status WhatsApp did not confirm in time fails the same way, and is not posted again by itself: check the phone first. Such a message keeps its sent_at (when we handed it to WhatsApp) and counts toward the instance's limits like one that went, so a second "first message" does not follow it; it is not counted as sent or delivered in your figures.

The same key keeps returning the first message, also once it has failed. To send a failed message anew, use a new key. Which answers to retry is in Waiting, errors and retries.

After it is sent

  • Follow it: GET /v1/messages/{id}, or the message.status webhooks: queued → sent → delivered → read, or failed with the reason in error and its error_code.
  • List messages: GET /v1/messages, newest first, filtered by instance_id, phone and direction; follow links.next for older ones. Messages are kept 30 days.
  • Edit: PATCH /v1/messages/{id} with text changes a text the instance sent, within 15 minutes of it going out (409 after that); the person sees it marked "Edited". A message that went to someone who had never written to the instance (a first message, or a follow-up to it) cannot be edited (422).
  • Delete for everyone: DELETE /v1/messages/{id} on a sent message deletes it in the chat, within two days of sending (409 after that). Its text and file are removed from Threadwire too. A message that went to someone who had never written to the instance cannot be deleted for everyone either (422): it is what opened the chat.
  • Cancel before it goes: DELETE /v1/messages/{id} on a scheduled message removes it (204). On a queued one it cancels it (200): it stays, as failed with error "Cancelled before it was sent.", and a message.status webhook follows. One already handed to WhatsApp, or sent or failed meanwhile, can no longer be cancelled (409). While the instance is sending another message the answer is 409 with Retry-After: try again in a few seconds. Your team can do the same with "Cancel" in the chat, or in the panel's Message log. A cancelled message no longer counts against your plan's messages for the day.
  • React: POST /v1/messages/{id}/reaction with an emoji reacts to a message someone sent you; an empty emoji takes it back. The message's reaction shows yours and reactions the other people's (phone, emoji), which also arrive as message.reaction webhooks. A reaction notifies the person like a message, so none goes to someone who opted out, and at most 10 an hour go to one person.
  • Mark as read: POST /v1/messages/{id}/read marks the chat of a message someone sent you as read, for when your team reads it in your own system. Replying to someone who wrote in the last day does this by itself.

Reactions and read marks go only to messages someone sent you, edits and deletions only to messages the instance sent; all four need a key that may send, and together they are limited to 20 a minute per instance.

Is a phone on WhatsApp?

POST /v1/instances/{id}/check with a phone answers on_whatsapp: true or false. Use it for people who gave you their number: checking bought or guessed lists is against the Acceptable Use Policy, and WhatsApp treats many lookups of unknown numbers as scraping. So checks follow the instance's protection: a few a day while it is new, more as it warms up (checks_left_today says how many remain), at most 10 a minute, none at night or while its new chats are paused; a refusal is 429 with the reason. A "yes" is remembered for a week and a "no" for a day, so asking again is free, and a message to that person then needs no second lookup.