Repository navigation
API Reference
All endpoints are relative to: http://<host>:2785/api/sessions/<sessionId>/
Authentication: X-API-Key: <api_key> header on all requests.
Note: The WhatsApp Catalog endpoints (Section 8) are stubs — both whatsapp-web.js and Baileys engines return 501 Not Implemented. The bridge handles this with a fallback that sends richly formatted text+image messages instead.
Note: Send endpoints accept an optional
Idempotency-Keyheader (OpenWA 0.24+). The bridge sends it on every supported send — see Outbound Idempotency.
OpenWA 0.24+ accepts an optional Idempotency-Key header on send endpoints. Within 24 hours, a
retry with the same key and body replays the first response instead of sending again.
Header: Idempotency-Key: <key>
Key format (bridge-generated, derived from the WhatsApp Message document name):
| Send | Key |
|---|---|
| Main send (text, media, template, poll, reply, forward) | frappe-<WhatsApp Message name> |
| Dynamic header image | frappe-<WhatsApp Message name>-hdr |
The dynamic header image needs its own key: OpenWA keys idempotency on (sessionId, key)
regardless of route, so reusing the main send's key would collide with it.
- Keys must be 1–255 visible ASCII characters. The bridge replaces any other character with
-and truncates to 255 (build_idempotency_key()inutils.py). - The key is unique per session — the same key on a different session is a different key.
Behaviour:
- A replay inside the 24-hour window returns the original result with the
Idempotent-Replayed: trueresponse header. -
409 IDEMPOTENCY_KEY_IN_PROGRESS— a request with the same key is still running. -
409 IDEMPOTENCY_OUTCOME_UNKNOWN— the send may have happened but the outcome is unknown; the key stays taken. -
422 IDEMPOTENCY_KEY_REUSED— the key was already used with a different route or body. - A
4xxor501refusal before the engine call frees the key, so a corrected retry may reuse it. - An error from an engine send keeps the key taken, regardless of HTTP status (including
409).
Endpoints that accept the header (OpenWA @IdempotentSend() routes): send-text,
send-template, send-image, send-video, send-audio, send-document, send-location,
send-contact, send-sticker, send-poll, reply, forward.
Send a plain text message.
{
"chatId": "967777715787@c.us",
"text": "Hello world"
}Send an image. Supports URL or base64.
{
"chatId": "967777715787@c.us",
"url": "https://example.com/image.png",
"caption": "Optional caption"
}| Field | Type | Required | Notes |
|---|---|---|---|
chatId |
string | Yes |
phone@c.us or groupId@g.us
|
url |
string | One of url/base64 | HTTP(S) URL to image |
base64 |
string | One of url/base64 | Raw base64 data (no data: prefix) |
mimetype |
string | When using base64 | e.g. image/png, image/jpeg
|
filename |
string | No | Max 255 chars |
caption |
string | No | Max 1024 chars |
mentions |
string[] | No | WIDs to @mention |
Same structure as send-image but for video.
{
"chatId": "967777715787@c.us",
"url": "https://example.com/audio.mp3"
}{
"chatId": "967777715787@c.us",
"url": "https://example.com/file.pdf",
"filename": "invoice.pdf"
}Send a stored template with variable substitution. Text-only — renders header+body+footer, joins with \n\n, sends as plain text.
{
"chatId": "967777715787@c.us",
"templateId": "85f89390-6054-4d79-a4f0-15b973a38c60",
"vars": {
"param1": "Faissal Mannaa",
"param2": "ACC-SINV-2026-00047",
"param3": "50"
}
}| Field | Type | Required | Notes |
|---|---|---|---|
chatId |
string | Yes | |
templateId |
string | Conditional | Required when templateName absent |
templateName |
string | Conditional | Required when templateId absent |
vars |
Record<string,string> | No | Keys match {{key}} in template |
Important: send-template is always text-only. For image headers, use the two-step approach (send-image first, then send-template).
Reply to a specific message with a quote.
{
"chatId": "967777715787@c.us",
"quotedMessageId": "true_967777715787@c.us_3EB0...",
"text": "This is my reply"
}React to a message with an emoji.
{
"chatId": "967777715787@c.us",
"messageId": "true_967777715787@c.us_3EB0...",
"emoji": "👍"
}{
"chatId": "967777715787@c.us",
"latitude": -6.2088,
"longitude": 106.8456,
"description": "Office location",
"address": "Jakarta, Indonesia"
}{
"chatId": "967777715787@c.us",
"contactName": "John Doe",
"contactNumber": "+1234567890"
}{
"chatId": "967777715787@c.us",
"name": "What's your favorite color?",
"options": ["Red", "Blue", "Green"],
"allowMultipleAnswers": false
}Send a sticker message. Supports URL or base64.
{
"chatId": "967777715787@c.us",
"url": "https://example.com/sticker.webp"
}Send a text message to multiple contacts (async batch).
{
"chatIds": ["967777715787@c.us", "967777711111@c.us"],
"text": "Hello everyone!"
}Response (202 Accepted):
{
"batchId": "batch-uuid",
"status": "processing",
"totalMessages": 2,
"statusUrl": "/api/sessions/:sessionId/messages/batch/batch-uuid"
}Get batch processing status and progress.
Cancel a running batch.
Add or remove a reaction to a message. Send empty emoji to remove.
{
"chatId": "967777715787@c.us",
"messageId": "true_967777715787@c.us_3EB0...",
"emoji": "👍"
}Get reactions for a specific message.
GET /api/sessions/:sessionId/messages/967777715787@c.us/true_.../reactionsForward an existing message to another chat.
{
"messageId": "true_967777715787@c.us_3EB0...",
"chatId": "967777711111@c.us"
}Delete a message. Add ?revoke=true to delete for everyone (revoke).
DELETE /api/sessions/:sessionId/messages/true_967777715787@c.us_3EB0...?revoke=true
Edit a sent message's text body.
{
"chatId": "967777715787@c.us",
"messageId": "true_967777715787@c.us_3EB0...",
"body": "Updated message text"
}Send a typing indicator to a chat.
{
"chatId": "967777715787@c.us",
"state": "typing"
}States: typing, recording, paused
Mark a chat as read/seen.
POST /api/sessions/:sessionId/chats/967777715787@c.us/read
Mark a chat as unread.
POST /api/sessions/:sessionId/chats/967777715787@c.us/unread
Get live chat history from the WhatsApp client.
GET /api/sessions/:sessionId/chats/967777715787@c.us/history?limit=50
Post a text status/story.
{
"text": "Hello World!",
"backgroundColor": "#FF5722",
"font": "serif"
}Post an image status/story.
{
"image": { "url": "https://example.com/image.jpg" },
"caption": "Check this out!"
}Post a video status/story.
{
"video": { "url": "https://example.com/video.mp4" },
"caption": "New video!"
}List all visible status/story updates (grouped by contact).
Get status updates from a specific contact.
GET /api/sessions/:sessionId/status/1234567890@c.us
Stream a stored status media file (image/video). Returns 404 for text statuses or expired items.
GET /api/sessions/:sessionId/status/:statusId/media
Delete your own status update.
List all groups the session belongs to.
Create a new WhatsApp group.
{
"name": "My Group",
"participants": ["967777715787@c.us", "967777711111@c.us"]
}Add participants to a group.
{
"participants": ["967777715787@c.us"]
}Remove participants from a group.
Promote participants to group admin.
Demote group admins to regular participants.
Set/update group name.
{
"subject": "New Group Name"
}Leave a group.
List all WhatsApp Business labels.
Add a label to a chat.
Remove a label from a chat.
Reject an incoming call.
Full-text message search across sessions.
GET /api/search?q=hello&limit=50
Set display name.
{
"name": "My Business Name"
}Set about text.
{
"status": "Available on WhatsApp"
}Set profile picture.
{
"url": "https://example.com/photo.jpg"
}List WhatsApp Channels the session is subscribed to.
Get a specific channel by ID.
Get messages from a WhatsApp Channel.
GET /api/sessions/:sessionId/channels/:channelId/messages?limit=50
Subscribe to a channel using an invite code.
{
"inviteCode": "ABC123xyz"
}Unsubscribe from a WhatsApp Channel.
Check if a phone number is registered on WhatsApp.
GET /api/sessions/:sessionId/contacts/check/967777715787
Response:
{
"isRegistered": true
}Block a contact.
POST /api/sessions/:sessionId/contacts/967777715787@c.us/block
Unblock a contact.
DELETE /api/sessions/:sessionId/contacts/967777715787@c.us/block
Request an 8-character pairing code as an alternative to QR scanning.
{
"phoneNumber": "967777715787"
}Response:
{
"pairingCode": "ABCD1234"
}Templates are scoped per session. Name must be unique within a session.
| Method | Endpoint | Description |
|---|---|---|
| GET | /templates |
List all templates |
| GET | /templates/:templateId |
Get a specific template |
| POST | /templates |
Create a new template |
| PUT | /templates/:templateId |
Update a template |
| DELETE | /templates/:templateId |
Delete a template |
OpenWA uses this regex for variable substitution:
/\{\{\s*([\w.-]+)\s*\}\}|\{(\w+)\}/gConversion from Frappe format:
- Frappe:
{{1}},{{2}},{{3}} - OpenWA:
{{param1}},{{param2}},{{param3}} - Conversion function:
frappe_to_openwa_vars()inutils.py
| Event | Description | Handler |
|---|---|---|
message.received |
Incoming message from a contact |
_handle_inbound_message() — creates WhatsApp Message, Communication, Lead |
message.sent |
Outgoing message acknowledged by server |
_handle_status_update() — updates message status |
message.ack |
Delivery/read receipt |
_handle_status_update() — updates message status |
message.receipt |
Per-recipient delivery/read receipt (opt-in, Baileys only) |
_handle_message_receipt() — logs receipt event with contact=participant
|
message.failed |
Send failure |
_handle_status_update() — marks message as Failed |
message.revoked |
Message deleted by sender |
_handle_message_revoked() — marks message as Revoked |
message.edited |
Sent message edited |
_handle_message_edited() — updates WhatsApp Message body |
message.reaction |
Reaction to a message |
_handle_message_reaction() — logs reaction |
status.received |
Contact posts a status/story |
_handle_status_received() — logs status update |
session.status |
Session status changed |
_handle_session_status() — updates account Active/Inactive |
session.qr |
QR code generated (needs scan) |
_handle_session_qr() — sets account Inactive |
session.authenticated |
Session authenticated successfully |
_handle_session_authenticated() — sets account Active |
session.disconnected |
Session disconnected |
_handle_session_status() — sets account Inactive |
session.reconnect_loop |
Session stuck in reconnect loop |
_handle_session_reconnect_loop() — logs error alert |
group.join |
Participant joined group |
_handle_group_membership() — logs event |
group.leave |
Participant left group |
_handle_group_membership() — logs event |
group.update |
Group metadata changed |
_handle_group_update() — logs changes |
call.received |
Incoming call ringing |
_handle_call_received() — logs call event |
Note:
message.receiptis opt-in at the OpenWA subscription level and is excluded from the*wildcard subscription — a session subscribed to*alone never receives it. It is also Baileys-only; the whatsapp-web.js engine does not emit it. See message.receipt Payload.
{
"event": "message",
"data": {
"from": "967777715787@c.us",
"to": "967777713637@c.us",
"body": "Hello!",
"type": "text",
"id": "true_967777715787@c.us_3EB0...",
"timestamp": 1719312000,
"chatId": "967777715787@c.us",
"session": "manaa",
"author": "967777715787@c.us",
"pushName": "John Doe"
}
}Emitted by OpenWA (Baileys engine only) when a group or status-broadcast message reaches a given recipient. One event is delivered per recipient per status transition.
{
"event": "message.receipt",
"data": {
"id": "true_967777715787@c.us_3EB0...",
"messageId": "true_967777715787@c.us_3EB0...",
"chatId": "967777715787@c.us",
"participant": "967777713637@c.us",
"status": "delivered",
"timestamp": 1719312000
}
}| Field | Type | Description |
|---|---|---|
id |
string | Event identifier; also accepted as a fallback for messageId
|
messageId |
string | ID of the message the receipt refers to |
chatId |
string | Chat the message was sent in |
participant |
string | JID of the recipient the receipt is for |
status |
string |
delivered, read, or played
|
timestamp |
number | Unix timestamp of the receipt |
Status values: delivered, read, played (status broadcasts only).
Handler behaviour — _handle_message_receipt(event_data, session_id):
- Requires both a message identifier (
messageId, falling back toid) and aparticipant; if either is missing the event is dropped without logging. - Logs an
OpenWA Event Logentry of typemessage.receiptwithcontact=participant, so the recipient is filterable in the Event Log. - Receipts from the account's own linked devices are dropped upstream by OpenWA, so
participantis always another user.
Enabling it: message.receipt must be listed explicitly in the account's webhook event
subscription. A * subscription does not include it.
OpenWA signs every webhook with:
- Header:
X-OpenWA-Signature: sha256=<hex-digest> - Computation:
HMAC-SHA256(webhook_secret, raw_body)
Lenient mode (default): When a secret is configured but no signature header is sent, the message is processed with a warning log.
- 60 requests per minute per IP (configurable per account)
- Exceeding the limit returns
{"status": "error", "message": "Rate limit exceeded"}(HTTP 200)
OpenWA sends X-OpenWA-Idempotency-Key header. Duplicate keys within the configured TTL (default 1 hour) are rejected with {"status": "duplicate"} (HTTP 200).
These are called from the client JS on the WhatsApp Account form.
| Method | Purpose | Args |
|---|---|---|
setup_openwa_session |
One-click setup: create session, start, fetch QR | { account_name } |
get_openwa_qr |
Fetch QR code for existing session | { account_name } |
get_openwa_session_status |
Get current session status (returns not_found if deleted from OpenWA) |
{ account_name } |
stop_openwa_session |
Disconnect the session | { account_name } |
reset_openwa_session |
Clear stale session ID so a new session can be created | { account_name } |
check_whatsapp_number |
Check if number is on WhatsApp | { account_name, number } |
block_contact |
Block a contact | { account_name, contact_id } |
unblock_contact |
Unblock a contact | { account_name, contact_id } |
send_typing_indicator |
Send typing state | { account_name, chat_id, state } |
send_bulk_openwa |
Bulk text to multiple contacts | { account_name, contacts, message } |
forward_message |
Forward a message | { account_name, message_id, chat_id } |
delete_message |
Delete or revoke a message | { account_name, message_id, revoke? } |
request_pairing_code |
Get 8-char pairing code | { account_name, phone_number } |
send_sticker |
Send sticker by URL or base64 | { account_name, chat_id, url?, base64? } |
edit_message |
Edit a sent message's text | { account_name, chat_id, message_id, body } |
post_status_text |
Post text status/story | { account_name, text, background_color?, font? } |
post_status_image |
Post image status/story | { account_name, url?, base64?, caption? } |
post_status_video |
Post video status/story | { account_name, url?, base64?, caption? } |
get_statuses |
List visible status updates | { account_name } |
reject_call |
Reject incoming WhatsApp call | { account_name, call_id } |
mark_chat_read |
Mark chat (or specific messages) as read | { account_name, chat_id, message_ids? } |
mark_chat_unread |
Mark chat as unread | { account_name, chat_id } |
get_chat_history |
Get live chat history from WhatsApp | { account_name, chat_id, limit? } |
search_messages |
Full-text search across sessions | { account_name, query, limit? } |
list_groups |
List all groups | { account_name } |
create_group |
Create a new WhatsApp group | { account_name, name, participants } |
add_group_participants |
Add participants to group | { account_name, group_id, participants } |
remove_group_participants |
Remove participants from group | { account_name, group_id, participants } |
promote_group_admins |
Promote to group admin | { account_name, group_id, participants } |
demote_group_admins |
Demote group admin | { account_name, group_id, participants } |
set_group_name |
Set group name | { account_name, group_id, name } |
leave_group |
Leave a group | { account_name, group_id } |
list_labels |
List WhatsApp Business labels | { account_name } |
add_label_to_chat |
Add label to chat | { account_name, label_id, chat_id } |
remove_label_from_chat |
Remove label from chat | { account_name, label_id, chat_id } |
send_bulk_with_progress |
Async batch with progress tracking | { account_name, contacts, message } |
set_profile_name |
Set WhatsApp display name | { account_name, name } |
set_profile_status |
Set WhatsApp about text | { account_name, status } |
set_profile_picture |
Set profile picture | { account_name, url?, base64? } |
get_session_stats |
Get aggregate session statistics | { account_name } |
list_channels |
List WhatsApp Channels | { account_name } |
get_channel_messages |
Get channel messages | { account_name, channel_id, limit? } |
get_contact_statuses |
Get statuses from a specific contact | { account_name, contact_id } |
get_status_media |
Stream stored status media file | { account_name, status_id } |
subscribe_channel |
Subscribe to channel via invite code | { account_name, invite_code } |
unsubscribe_channel |
Unsubscribe from a channel | { account_name, channel_id } |
get_message_reactions |
Get reactions for a message | { account_name, chat_id, message_id } |
cancel_batch |
Cancel a running bulk batch | { account_name, batch_id } |
vote_poll |
Cast a vote on a poll | { account_name, chat_id, poll_message_id, options } |
click_button |
Send a structured prompt reply (Baileys) | { account_name, chat_id, message_id, button_id, text? } |
pin_message |
Pin a message in its chat | { account_name, chat_id, message_id, duration_seconds? } |
unpin_message |
Remove a message's pin | { account_name, chat_id, message_id } |
star_message |
Star or unstar a message | { account_name, chat_id, message_id, star? } |
get_chat_media |
Download stored media for a message | { account_name, chat_id, message_id } |
archive_chat |
Archive or unarchive a chat | { account_name, chat_id, archive? } |
mute_chat |
Mute or unmute a chat | { account_name, chat_id, mute_until? } |
pin_chat |
Pin or unpin a chat in the chat list | { account_name, chat_id, pin? } |
clear_chat_messages |
Delete every message in a chat | { account_name, chat_id } |
get_session_proxy |
Read per-session egress proxy config | { account_name } |
set_session_proxy |
Set or clear per-session egress proxy | { account_name, proxy_url } |
get_group_membership_requests |
List pending group membership requests | { account_name, group_id } |
approve_group_membership_requests |
Approve membership requests | { account_name, group_id, participants? } |
reject_group_membership_requests |
Reject membership requests | { account_name, group_id, participants? } |
post_status_voice |
Post an audio status as a voice note | { account_name, url?, base64?, caption? } |
create_channel |
Create a new WhatsApp channel | { account_name, name, description? } |
mute_channel |
Mute or unmute a channel | { account_name, channel_id, mute? } |
demote_channel_admin |
Demote a channel admin | { account_name, channel_id, user_id } |
transfer_channel_ownership |
Transfer channel ownership (irreversible) | { account_name, channel_id, new_owner_id } |
| Method | Purpose |
|---|---|
_ensure_session_ready() |
Pre-send session health check — auto-restarts if not ready |
on_account_update() |
Auto-sync webhook secret to OpenWA on save |
on_account_trash() |
Delete OpenWA session when account is deleted |
| HTTP Status | Cause |
|---|---|
| 400 | Missing mimetype with base64, missing chatId, session not active |
| 404 | Template UUID not found, session not found |
| 200 (with error body) | All inbound webhook errors (prevents retry loops) |
OpenWA returns machine-readable error bodies. The bridge surfaces the server's own reason instead of
a bare Client Error:
{ "message": "Poll option not found", "code": "POLL_OPTION_NOT_FOUND" }{ "error": { "message": "Session not active", "code": "SESSION_INACTIVE" } }Parsing rules — openwa_error_message(resp, context="") in utils.py:
| Order | Source | Example |
|---|---|---|
| 1 |
error.message (nested object) |
Session not active |
| 2 |
error.code (nested object) |
SESSION_INACTIVE |
| 3 |
message (top level) |
Poll option not found |
| 4 |
error (top-level string) |
Bad Request |
| 5 |
code (top level) |
POLL_OPTION_NOT_FOUND |
| 6 | Raw response text | <html>...</html> |
| 7 |
"(no body)" when the body is empty |
— |
The message is trimmed and truncated to 1000 characters, then formatted as
"<context>: OpenWA HTTP <status> — <body>". The context argument is optional and prefixes the
string (for example "vote-poll").
Upstream response preserved: the raised exception is still
requests.exceptions.HTTPError with the original response=resp attached, so callers can read
err.response.status_code, err.response.json(), or err.response.headers — including the
Idempotent-Replayed header. The enriched text is added to the exception message rather than
replacing the exception type.
Where it applies: openwa_api(), outbound sends (_send_via_openwa), account API calls
(_raw_openwa_call), message fetching (_fetch_messages), and vote_poll — for example a
400 POLL_OPTION_NOT_FOUND now surfaces as
vote-poll: OpenWA HTTP 400 — Poll option not found instead of Client Error: 400.
OpenWA blocks requests to private IPs by default. Fix in OpenWA's .env:
SSRF_ALLOWED_HOSTS=your-site-name,your-server-ip,localhost,127.0.0.1,minio
# your-site-name = your Frappe/ERPNext site name (e.g., my-site)
# your-server-ip = the IP used to connect from VM to host (e.g., 192.168.1.15)
# minio = MinIO service hostname (if used for file storage)The bridge provides a WhatsApp Catalog Product DocType that links ERPNext Items to a WhatsApp product catalog. OpenWA does not support WhatsApp catalog operations, so the bridge only uses the local DocType for reference and sends products as formatted text+image messages.
| Field | Type | Source |
|---|---|---|
product_name |
Data | Auto-fetched from Item, editable |
item_code |
Link → Item | ERPNext Item |
whatsapp_account |
Link → WhatsApp Account | Target account |
is_available |
Check | Default: Yes |
description |
Text Editor | Auto-fetched from Item, editable |
price |
Currency | Auto-fetched from Item rate |
currency |
Link → Currency | Default from system |
image |
Attach Image | Auto-fetched from Item image |
retailer_id |
Data | Optional external ID |
sync_status |
Select | Vestigial – OpenWA has no catalog sync |
openwa_product_id |
Data | Vestigial – OpenWA has no catalog sync |
last_sync_on |
Datetime | Read-only |
List all catalog products for an account.
Method: openwa_bridge.catalog.get_catalog_products
Args: { account_name: string }
Returns:
{
"status": "ok",
"products": [
{ "name": "WCP-0001", "product_name": "Widget", ... }
]
}Get a single catalog product.
Method: openwa_bridge.catalog.get_catalog_product
Args: { account_name: string, product_name: string }
Returns:
{
"status": "ok",
"product": { "name": "WCP-0001", ... }
}send_product_to_chat. Use send_product_to_chat instead.
Method: openwa_bridge.catalog.send_product_to_chat_direct
Args: { account_name: string, chat_id: string, product_name: string }
Send a catalog product to a WhatsApp chat as formatted text+image.
Method: openwa_bridge.catalog.send_product_to_chat
Args: { product_name: string, chat_id: string }
Send a catalog product to a Customer's WhatsApp number. Looks up the Customer's primary Contact, resolves the phone number to a chat ID, and sends the product as a text+image fallback message.
Method: openwa_bridge.catalog.send_product_to_customer
Args: { product_name: string, customer: string }
Raises: If no Contact or no phone number is found for the Customer.
Returns:
{
"status": "ok",
"method": "fallback",
"result": { "messageId": "true_967777715787@c.us_3EB0..." }
}Send the full catalog link to a chat. Falls back to a summary message.
Method: openwa_bridge.catalog.send_catalog_to_chat
Args: { account_name: string, chat_id: string }
OpenWA has no delayed send, so scheduling is implemented in the bridge:
- Set
openwa_scheduled_at(Datetime) on a WhatsApp Message. -
after_insertpropagates it to the OpenWA Outbox entry'sscheduled_atand does not enqueue it for immediate send. - The outbox processor (
_process_outbox_entry_inner) skips Pending entries withscheduled_at > nowwithout bumpingattempts. - The scheduler safety-net (
process_pending_outbox) only picks up Pending entries whosescheduled_atis null or<= now.
Image / video / audio / document sends now accept a base64 payload in addition to a URL link:
{
"image": {
"base64": "<base64-encoded bytes>",
"mimetype": "image/jpeg",
"filename": "photo.jpg"
}
}-
link→ sent asurl(existing behavior). -
base64→ sent asbase64+mimetype(mirrors the sticker pattern). -
filenameoptional (used bysend-document). - Caption still sent for non-audio media.
Send the currently-open document as a PDF via OpenWA send-document.
Whitelisted method: openwa_bridge.whatsapp_message.send_document_pdf
Args: { to: string, reference_doctype: string, reference_name: string, print_format?: string, filename?: string, caption?: string }
-
to: recipient phone number (any format — normalized to<num>@c.us). -
print_format: Print Format name (defaultStandard). -
filename: delivered filename (default<reference_name>.pdf). -
caption: optional caption text (default empty).
Returns: string — the created WhatsApp Message name.
Called from the "Send To Whatsapp" dialog in the vendored frappe_whatsapp app when
"Send as PDF" is checked. Creates a WhatsApp Message (content_type="document",
openwa_send_pdf=1) referencing the document; the outbox worker then renders the
PDF at send time via render_doc_as_pdf() (Chrome, falling back to wkhtmltopdf),
base64-encodes it, and POSTs send-document with mimetype: application/pdf.
Rendering on every attempt keeps retries safe and the delivered PDF always current.
Note: draft documents require Allow Print for Draft in Print Settings.
OpenWA POST /messages/reply is text-only. When a media message is sent with
is_reply + reply_to_message_id:
- Media is sent unquoted via
send-{type}. - A text reply is sent quoting the returned media
messageId.
Non-message webhook events are now persisted instead of only logged:
| event_type | Source webhook |
|---|---|
group.join / group.leave / group.update
|
group webhooks |
call.received |
incoming call |
status.received |
contact status/story |
message.reaction |
reaction on a message |
webhook.delivery_failure |
DLQ check (check_webhook_delivery_failures) |
Each row: whatsapp_account, session_id, timestamp, summary, payload
(Code field), related_group, related_contact. Inserts are best-effort and never
break webhook processing.
message.reaction events are stored on the WhatsApp Message doc in the
openwa_reactions JSON field as [{emoji, sender, timestamp}] (advance-only dedupe
per emoji+sender).
daily() now calls check_webhook_delivery_failures() which:
- Reads
GET /api/webhooks/delivery-failures(limit 50 per session). - Writes
webhook.delivery_failurerows to the Event Log (deduped by idempotency key). - Replays the affected account via
replay_webhooks(account_name)to backfill missed inbound messages.
403 from the DLQ endpoint (non-ADMIN key) is expected and skipped silently.
New openwa_bridge.whatsapp_account whitelisted methods added for the OpenWA 0.19–0.25 API surface.
Cast a vote on a WhatsApp poll.
Method: openwa_bridge.whatsapp_account.vote_poll
Args: { account_name: string, chat_id: string, poll_message_id: string, options: string }
-
options: comma-separated option strings (max 12).
Endpoint: POST /messages/vote-poll with {chatId, pollMessageId, options}.
Send a structured button/list reply against a stored WhatsApp Business prompt (OpenWA 0.23.6+, Baileys only).
Method: openwa_bridge.whatsapp_account.click_button
Args: { account_name: string, chat_id: string, message_id: string, button_id: string, text?: string }
-
message_id: Message ID of the stored prompt (an evicted prompt answers404). -
button_id: Id of the button (or list-row) to tap. -
text: optional visible label; when omitted the engine resolves it from the stored prompt.
Endpoint: POST /messages/click-button with {chatId, messageId, buttonId, text?}. whatsapp-web.js answers 501.
Pin a message in a chat.
Method: openwa_bridge.whatsapp_account.pin_message
Args: { account_name: string, chat_id: string, message_id: string, duration_seconds?: number }
-
duration_seconds: 86400 (24h), 604800 (7d), or 2592000 (30d). Default 7d.
Endpoint: POST /messages/pin with {chatId, messageId, durationSeconds}.
Remove a message's pin.
Method: openwa_bridge.whatsapp_account.unpin_message
Args: { account_name: string, chat_id: string, message_id: string }
Endpoint: POST /messages/unpin with {chatId, messageId}.
Star or unstar a message.
Method: openwa_bridge.whatsapp_account.star_message
Args: { account_name: string, chat_id: string, message_id: string, star?: 0 | 1 }
Endpoint: POST /messages/star with {chatId, messageId, star: boolean}.
Download stored media for a message.
Method: openwa_bridge.whatsapp_account.get_chat_media
Args: { account_name: string, chat_id: string, message_id: string }
Returns: { status: "ok", media: {...} }
Endpoint: GET /messages/:chatId/:messageId/media.
Archive or unarchive a chat.
Method: openwa_bridge.whatsapp_account.archive_chat
Args: { account_name: string, chat_id: string, archive?: 0 | 1 }
Endpoint: POST /chats/archive with {chatId, archive: boolean}.
Mute or unmute a chat.
Method: openwa_bridge.whatsapp_account.mute_chat
Args: { account_name: string, chat_id: string, mute_until?: number }
-
mute_until: epoch-ms until which the chat is muted (0 = indefinite).
Endpoint: POST /chats/mute with {chatId, muteUntil}.
Pin or unpin a chat in the chat list.
Method: openwa_bridge.whatsapp_account.pin_chat
Args: { account_name: string, chat_id: string, pin?: 0 | 1 }
Endpoint: POST /chats/pin with {chatId, pin: boolean}.
Delete every message in a chat (clear history).
Method: openwa_bridge.whatsapp_account.clear_chat_messages
Args: { account_name: string, chat_id: string }
Endpoint: DELETE /chats/:chatId/messages.
Read or update the per-session egress proxy configuration.
Methods: openwa_bridge.whatsapp_account.get_session_proxy, openwa_bridge.whatsapp_account.set_session_proxy
Args (set): { account_name: string, proxy_url: string } — set a proxy URL, or empty string to clear it.
Endpoints: GET /proxy and PATCH /proxy (per-session route).
List pending membership requests for a group.
Method: openwa_bridge.whatsapp_account.get_group_membership_requests
Args: { account_name: string, group_id: string }
Endpoint: GET /groups/:groupId/membership-requests.
Approve pending membership requests.
Method: openwa_bridge.whatsapp_account.approve_group_membership_requests
Args: { account_name: string, group_id: string, participants?: string }
-
participants: comma-separated JIDs to approve; empty = approve all.
Endpoint: POST /groups/:groupId/membership-requests/approve.
Reject pending membership requests.
Method: openwa_bridge.whatsapp_account.reject_group_membership_requests
Args: { account_name: string, group_id: string, participants?: string }
-
participants: comma-separated JIDs to reject; empty = reject all.
Endpoint: POST /groups/:groupId/membership-requests/reject.
Post an audio status as a WhatsApp voice note.
Method: openwa_bridge.whatsapp_account.post_status_voice
Args: { account_name: string, url?: string, base64?: string, caption?: string }
Provide either url or base64 (not both).
Endpoint: POST /status/send-voice.
Create a new WhatsApp channel.
Method: openwa_bridge.whatsapp_account.create_channel
Args: { account_name: string, name: string, description?: string }
Endpoint: POST /channels.
Mute or unmute a channel.
Method: openwa_bridge.whatsapp_account.mute_channel
Args: { account_name: string, channel_id: string, mute?: 0 | 1 }
Endpoint: POST /channels/:channelId/mute with {mute: boolean}.
Demote a channel admin back to a subscriber (requires owning the channel).
Method: openwa_bridge.whatsapp_account.demote_channel_admin
Args: { account_name: string, channel_id: string, user_id: string }
Endpoint: POST /channels/:channelId/admins/demote with {userId}.
Transfer channel ownership to another user. Irreversible.
Method: openwa_bridge.whatsapp_account.transfer_channel_ownership
Args: { account_name: string, channel_id: string, new_owner_id: string }
Endpoint: POST /channels/:channelId/owner/transfer with {newOwnerId}.
Send a catalog listing to a chat.
Method: openwa_bridge.whatsapp_account.send_catalog_message
Args: { account_name: string, chat_id: string, catalog_id: string }
Returns:
{
"status": "error",
"error": "POST /messages/send-catalog was removed in OpenWA 0.19 ..."
}POST /messages/send-catalog was removed in OpenWA 0.19 (answers 501 on every engine). The bridge method no longer calls OpenWA and always returns this error. Use openwa_bridge.whatsapp_account.send_product_message (product card, Baileys engine) or the catalog helpers (send_product_to_chat, send_catalog_to_chat) which render text+image / text-summary fallbacks.