Skip to content

API Reference

Manaa-Soft edited this page Oct 22, 2026 · 17 revisions

API Reference

OpenWA REST Endpoints

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-Key header (OpenWA 0.24+). The bridge sends it on every supported send — see Outbound Idempotency.


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() in utils.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: true response 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 4xx or 501 refusal 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.


Messages

POST /messages/send-text

Send a plain text message.

{
  "chatId": "967777715787@c.us",
  "text": "Hello world"
}

POST /messages/send-image

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

POST /messages/send-video

Same structure as send-image but for video.

POST /messages/send-audio

{
  "chatId": "967777715787@c.us",
  "url": "https://example.com/audio.mp3"
}

POST /messages/send-document

{
  "chatId": "967777715787@c.us",
  "url": "https://example.com/file.pdf",
  "filename": "invoice.pdf"
}

POST /messages/send-template

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).

POST /messages/reply

Reply to a specific message with a quote.

{
  "chatId": "967777715787@c.us",
  "quotedMessageId": "true_967777715787@c.us_3EB0...",
  "text": "This is my reply"
}

POST /messages/react

React to a message with an emoji.

{
  "chatId": "967777715787@c.us",
  "messageId": "true_967777715787@c.us_3EB0...",
  "emoji": "👍"
}

POST /messages/send-location

{
  "chatId": "967777715787@c.us",
  "latitude": -6.2088,
  "longitude": 106.8456,
  "description": "Office location",
  "address": "Jakarta, Indonesia"
}

POST /messages/send-contact

{
  "chatId": "967777715787@c.us",
  "contactName": "John Doe",
  "contactNumber": "+1234567890"
}

POST /messages/send-poll

{
  "chatId": "967777715787@c.us",
  "name": "What's your favorite color?",
  "options": ["Red", "Blue", "Green"],
  "allowMultipleAnswers": false
}

POST /messages/send-sticker

Send a sticker message. Supports URL or base64.

{
  "chatId": "967777715787@c.us",
  "url": "https://example.com/sticker.webp"
}

POST /messages/send-bulk

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 /messages/batch/:batchId

Get batch processing status and progress.

POST /messages/batch/:batchId/cancel

Cancel a running batch.

POST /messages/react

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 /messages/:chatId/:messageId/reactions

Get reactions for a specific message.

GET /api/sessions/:sessionId/messages/967777715787@c.us/true_.../reactions

POST /messages/forward

Forward an existing message to another chat.

{
  "messageId": "true_967777715787@c.us_3EB0...",
  "chatId": "967777711111@c.us"
}

DELETE /messages/:messageId

Delete a message. Add ?revoke=true to delete for everyone (revoke).

DELETE /api/sessions/:sessionId/messages/true_967777715787@c.us_3EB0...?revoke=true

POST /messages/edit

Edit a sent message's text body.

{
  "chatId": "967777715787@c.us",
  "messageId": "true_967777715787@c.us_3EB0...",
  "body": "Updated message text"
}

Chat Management

POST /chats/typing

Send a typing indicator to a chat.

{
  "chatId": "967777715787@c.us",
  "state": "typing"
}

States: typing, recording, paused

POST /chats/:chatId/read

Mark a chat as read/seen.

POST /api/sessions/:sessionId/chats/967777715787@c.us/read

POST /chats/:chatId/unread

Mark a chat as unread.

POST /api/sessions/:sessionId/chats/967777715787@c.us/unread

GET /chats/:chatId/history

Get live chat history from the WhatsApp client.

GET /api/sessions/:sessionId/chats/967777715787@c.us/history?limit=50

Status/Stories

POST /status/send-text

Post a text status/story.

{
  "text": "Hello World!",
  "backgroundColor": "#FF5722",
  "font": "serif"
}

POST /status/send-image

Post an image status/story.

{
  "image": { "url": "https://example.com/image.jpg" },
  "caption": "Check this out!"
}

POST /status/send-video

Post a video status/story.

{
  "video": { "url": "https://example.com/video.mp4" },
  "caption": "New video!"
}

GET /status

List all visible status/story updates (grouped by contact).

GET /status/:contactId

Get status updates from a specific contact.

GET /api/sessions/:sessionId/status/1234567890@c.us

GET /status/:statusId/media

Stream a stored status media file (image/video). Returns 404 for text statuses or expired items.

GET /api/sessions/:sessionId/status/:statusId/media

DELETE /status/:statusId

Delete your own status update.


Groups

GET /groups

List all groups the session belongs to.

POST /groups

Create a new WhatsApp group.

{
  "name": "My Group",
  "participants": ["967777715787@c.us", "967777711111@c.us"]
}

POST /groups/:groupId/participants/add

Add participants to a group.

{
  "participants": ["967777715787@c.us"]
}

POST /groups/:groupId/participants/remove

Remove participants from a group.

POST /groups/:groupId/participants/promote

Promote participants to group admin.

POST /groups/:groupId/participants/demote

Demote group admins to regular participants.

PUT /groups/:groupId/subject

Set/update group name.

{
  "subject": "New Group Name"
}

POST /groups/:groupId/leave

Leave a group.


Labels (WhatsApp Business)

GET /labels

List all WhatsApp Business labels.

POST /labels/:labelId/chats/:chatId

Add a label to a chat.

DELETE /labels/:labelId/chats/:chatId

Remove a label from a chat.


Calls

POST /calls/:callId/reject

Reject an incoming call.


Search

GET /search

Full-text message search across sessions.

GET /api/search?q=hello&limit=50

Profile

PUT /profile/name

Set display name.

{
  "name": "My Business Name"
}

PUT /profile/status

Set about text.

{
  "status": "Available on WhatsApp"
}

PUT /profile/picture

Set profile picture.

{
  "url": "https://example.com/photo.jpg"
}

Channels/Newsletters

GET /channels

List WhatsApp Channels the session is subscribed to.

GET /channels/:channelId

Get a specific channel by ID.

GET /channels/:channelId/messages

Get messages from a WhatsApp Channel.

GET /api/sessions/:sessionId/channels/:channelId/messages?limit=50

POST /channels/subscribe

Subscribe to a channel using an invite code.

{
  "inviteCode": "ABC123xyz"
}

DELETE /channels/:channelId

Unsubscribe from a WhatsApp Channel.


Contact Management

GET /contacts/check/:number

Check if a phone number is registered on WhatsApp.

GET /api/sessions/:sessionId/contacts/check/967777715787

Response:

{
  "isRegistered": true
}

POST /contacts/:jid/block

Block a contact.

POST /api/sessions/:sessionId/contacts/967777715787@c.us/block

DELETE /contacts/:jid/block

Unblock a contact.

DELETE /api/sessions/:sessionId/contacts/967777715787@c.us/block

Pairing Code

POST /pairing-code

Request an 8-character pairing code as an alternative to QR scanning.

{
  "phoneNumber": "967777715787"
}

Response:

{
  "pairingCode": "ABCD1234"
}

Templates

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

Template Variable Rendering

OpenWA uses this regex for variable substitution:

/\{\{\s*([\w.-]+)\s*\}\}|\{(\w+)\}/g

Conversion from Frappe format:

  • Frappe: {{1}}, {{2}}, {{3}}
  • OpenWA: {{param1}}, {{param2}}, {{param3}}
  • Conversion function: frappe_to_openwa_vars() in utils.py

Inbound Webhooks

Supported Events

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.receipt is 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.

Webhook Payload Structure

{
  "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"
  }
}

message.receipt Payload

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 to id) and a participant; if either is missing the event is dropped without logging.
  • Logs an OpenWA Event Log entry of type message.receipt with contact=participant, so the recipient is filterable in the Event Log.
  • Receipts from the account's own linked devices are dropped upstream by OpenWA, so participant is always another user.

Enabling it: message.receipt must be listed explicitly in the account's webhook event subscription. A * subscription does not include it.

HMAC Verification

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.

Rate Limiting

  • 60 requests per minute per IP (configurable per account)
  • Exceeding the limit returns {"status": "error", "message": "Rate limit exceeded"} (HTTP 200)

Idempotency

OpenWA sends X-OpenWA-Idempotency-Key header. Duplicate keys within the configured TTL (default 1 hour) are rejected with {"status": "duplicate"} (HTTP 200).


Frappe Whitelisted Methods

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 }

Internal Methods (Not Whitelisted)

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

Error Handling

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)

Structured Error Responses

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.

SSRF Blocked

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)

8. WhatsApp Catalog (Bridge DocType)

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.

DocType: WhatsApp Catalog Product

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

get_catalog_products

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_catalog_product

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_direct

⚠️ Deprecated — delegates to 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_product_to_chat

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_product_to_customer

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_catalog_to_chat

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 }


Tier 2 Improvements (bridge-side)

Scheduled send

OpenWA has no delayed send, so scheduling is implemented in the bridge:

  • Set openwa_scheduled_at (Datetime) on a WhatsApp Message.
  • after_insert propagates it to the OpenWA Outbox entry's scheduled_at and does not enqueue it for immediate send.
  • The outbox processor (_process_outbox_entry_inner) skips Pending entries with scheduled_at > now without bumping attempts.
  • The scheduler safety-net (process_pending_outbox) only picks up Pending entries whose scheduled_at is null or <= now.

Base64 media outbound

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 as url (existing behavior).
  • base64 → sent as base64 + mimetype (mirrors the sticker pattern).
  • filename optional (used by send-document).
  • Caption still sent for non-audio media.

Send-as-PDF

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 (default Standard).
  • 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.

Media reply workaround

OpenWA POST /messages/reply is text-only. When a media message is sent with is_reply + reply_to_message_id:

  1. Media is sent unquoted via send-{type}.
  2. A text reply is sent quoting the returned media messageId.

Inbound event persistence (OpenWA Event Log)

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.

Reactions persistence

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).

Webhook DLQ replay

daily() now calls check_webhook_delivery_failures() which:

  1. Reads GET /api/webhooks/delivery-failures (limit 50 per session).
  2. Writes webhook.delivery_failure rows to the Event Log (deduped by idempotency key).
  3. 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.


OpenWA v0.19-v0.25 whitelisted methods

New openwa_bridge.whatsapp_account whitelisted methods added for the OpenWA 0.19–0.25 API surface.

vote_poll

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}.

click_button

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 answers 404).
  • 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_message

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}.

unpin_message

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_message

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}.

get_chat_media

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_chat

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_chat

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_chat

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}.

clear_chat_messages

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.

get_session_proxy / set_session_proxy

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).

get_group_membership_requests

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_group_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_group_membership_requests

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_status_voice

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_channel

Create a new WhatsApp channel.

Method: openwa_bridge.whatsapp_account.create_channel

Args: { account_name: string, name: string, description?: string }

Endpoint: POST /channels.

mute_channel

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_channel_admin

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

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_catalog_message (deprecated)

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.

Clone this wiki locally