Skip to content

Sending messages

POST /agent/v1/messages

An agent can send text, image, audio, video, document and sticker. Reactions are receive-only.

result = client.send_text("Hello! How can I help you?")
result.message_id     # "wamid.HBg..." — record this; it identifies the message in receipts
result.wa_id          # "user:50972923564215"

Every send returns a SendResult.

The recipient

An agent has exactly one recipient — its creator — so to is optional on every send:

client.send_text("Hello")                   # to the creator
client.send_text(message.sender, "Hello")   # explicit, same thing
client.send_image(file="cat.jpg")           # every send_* takes to=None

When you leave it out, the client uses client.recipient — the identifier it last saw on a poll or a send. A client that has never polled finds it by itself before the first send, with one GET /updates?offset=0&timeout=0 (auto_discover=True). A client that has polled never does that, since a second poll would replace a running loop; it raises ValidationError if nothing has revealed the recipient yet. client.discover_recipient() does the lookup explicitly.

Passing to is optional. When you do, it must be user:<id> — a WhatsApp user, and the agent's creator. An agent:<id>, a bare phone number or any other shape is rejected (HTTP 400, code 131009); the library catches these before the request leaves. Take it from a recent inbound message and pass it back unchanged — never hardcode it.

Text

client.send_text("Hello", preview_url=True, reply_to="wamid.HBg...")
Argument
body up to 4096 characters
preview_url render a preview for the first URL in the body; it must begin with http:// or https://
reply_to wamid of a message to quote

Media

Each media send takes either a media_id from an earlier upload, or a file that the library uploads first:

# upload once, send many times
media_id = client.upload_media("logo.png")
client.send_image(media_id=media_id, caption="our logo")

# or in one call
client.send_image(file="cat.jpg", caption="look at this")
client.send_image(file=open("cat.jpg", "rb"))
client.send_image(file=photo_bytes, mime_type="image/jpeg")
Method Caption Filename
send_image ✅ ≤1024 chars —
send_video ✅ ≤1024 chars —
send_document ✅ ≤1024 chars ✅
send_audio — —
send_sticker — —

Documents keep a filename, including its extension:

client.send_document(file="invoice.pdf")                      # filename: invoice.pdf
client.send_document(media_id=mid, filename="invoice.pdf")    # set it yourself

Formats, size caps and codec constraints are in Limits and formats. Uploading is covered in Files and media.

Quoting a message

client.send_text("On it", reply_to=message.id)
client.reply(message, "On it")          # same thing, to the message's sender
ctx.reply("On it", quote=True)          # from an Agent handler

Use a wamid from this conversation — an id from GET /updates, or one you got back from a send. A wamid the platform never issued is a 400 (code 131009). A wamid from another agent's chat is accepted, not rejected, so don't count on the API to catch a mix-up.

Sending a type the library doesn't know

send_message takes any payload, so a type the platform adds later needs no library upgrade:

client.send_message(None, "text", {"body": "Hello", "preview_url": True})   # None: the known recipient

Ordering

Do not issue concurrent sends to the same recipient. The order in which they arrive is not guaranteed. Send sequentially, or serialize per recipient — see Recipes.

What can go wrong

Situation Result
Body over 4096, or caption over 1024 ValidationError locally; 400 + code 100 from the API
Payload object missing for the declared type 400
Media id missing, unknown or expired 400 + code 131009
to is malformed, or an agent: id 400 + code 131009
type is reaction ValidationError locally; 400 from the API
Recipient is not the agent's creator 403 + code 131005
to is user:<id> with an id that is not a WhatsApp user (e.g. an agent's id) 500 + code 2 — not retried by default
More than 12 sends per minute 429 — paced for you by default
Not accepted for delivery 503 + code 131016 — retried automatically

Fields in payload objects other than the one being sent are still validated, so a stray over-long caption in an unused object can fail the request.

Retrying a send

This is the one place where "just retry" is wrong. What to do depends on what came back:

Response Meaning Action
2xx Sent. Record message_id. Never retry.
4xx Not sent. Fix the request or the token. A repeat fails the same way.
429 Not sent. Back off and retry — the library does this.
503 code 131016 Not accepted for delivery, so not sent. Back off and retry — the library does this.
500, connection reset, read timeout Unknown whether it was sent. Decide in advance. A retry may deliver twice.

The library therefore does not retry a send after a 500 or a dropped connection. Opt in when a duplicate is cheaper than a loss:

client = Client(token, retry_send_on_server_error=True)

A long read timeout is the cheaper protection — it keeps an ordinary delay from turning into an unknown outcome. Client(timeout=...) sets it.