Repository navigation
Message Flows
Manaa-Soft edited this page Oct 22, 2026
·
5 revisions
User creates WhatsApp Message in Desk
│
▼
frappe.get_doc(new_doc).insert()
│
▼
after_insert → outbox entry (if OpenWA + outbox needed)
│
▼
OverrideWhatsAppMessage.notify(data)
│
├─ account.openwa_enabled = false?
│ └─ YES → super().notify(data) → Meta Cloud API
│
├─ content_type in (interactive, flow)?
│ └─ YES → super().notify(data) → Meta Cloud API (fallback)
│
└─ OpenWA path → _send_via_openwa(account, data)
│
▼
POST /api/sessions/:id/messages/send-text
Headers: Idempotency-Key: frappe-<WhatsApp Message name> (optional, v0.25)
{ chatId, text }
│
▼
resp.json() → messageId → update WhatsApp Message status to "Sent"
── Non-2xx? → openwa_error_message() parses JSON body → raises requests.HTTPError ──
User creates WhatsApp Message with template set
│
▼
OverrideWhatsAppMessage.notify(data)
│
▼
_send_via_openwa(account, data)
│
├─ self.use_template AND self.template is set?
│ │
│ ▼
│ openwa_tid = frappe.db.get_value("WhatsApp Templates", self.template, "openwa_template_id")
│ │
│ ├─ openwa_tid exists?
│ │ │
│ │ ▼
│ │ Build vars from body_param or template_parameters:
│ │ body_param format: { "1": "val1", "2": "val2" } → { "param1": "val1", "param2": "val2" }
│ │ template_parameters: ["val1", "val2"] → { "param1": "val1", "param2": "val2" }
│ │ │
│ │ ▼
│ │ POST /messages/send-template
│ │ Headers: Idempotency-Key: frappe-<WhatsApp Message name> (optional, v0.25)
│ │ { chatId, templateId, vars: { param1: "val1", ... } }
│ │ │
│ │ ▼
│ │ OpenWA: renderTemplate(header+body+footer, vars) → sendText()
│ │
│ └─ openwa_tid is empty?
│ │
│ ▼
│ Fallback: _translate_template_payload() → manual substitution
│ POST /messages/send-text
│ Headers: Idempotency-Key: frappe-<WhatsApp Message name> (optional, v0.25)
│ { chatId, text: "Dear Faissal, your invoice..." }
│
└─ self.template set but self.use_template NOT set?
└─ Jinja path: falls through to content_type check below
│
▼
Update WhatsApp Message: message_id + status="Sent"
DocType event triggers (e.g., Sales Invoice submit)
│
▼
WhatsAppNotification.send_template_message(doc)
│
▼
OverrideWhatsAppNotification.send_template_message(doc)
│
├─ not OpenWA account? → super().send_template_message() → Meta API
│
├─ openwa_send_type not in ("Template", "Jinja")? → super().send_template_message() → Meta API
│
└─ OpenWA path: builds data dict, calls self.notify(data, doc_data)
│
▼
openwa_send_type routing:
│
├─ "Template" → _send_openwa_template(account, data, doc_data)
│ │
│ ▼
│ Create WhatsApp Message doc:
│ { type: "Outgoing", message_type: "Template",
│ use_template: 1, template: ...,
│ template_parameters: '["val1","val2",...]' }
│ │
│ ▼
│ frappe.get_doc(new_doc).insert()
│ │
│ ▼
│ after_insert → outbox entry → process_outbox_entry
│ │
│ ▼
│ _send_outbox_message():
│ ├─ _send_dynamic_header_for_outbox(msg, account, caption=msg.message)
│ │ ├─ Template has openwa_dynamic_header? → render doc as PNG → send image
│ │ └─ Returns True if sent (done, no separate text send)
│ └─ If no image: _send_via_openwa()
│ ├─ use_template=1 AND template set? → POST /messages/send-template
│ └─ Otherwise → POST /messages/send-text
│
├─ "Jinja" → notify() with rendered message
│ │
│ ▼
│ 1. Render self.code via frappe.render_template(self.code, {"doc": doc})
│ 2. _send_openwa_text(account, data, rendered_message, doc_data)
│ │
│ ▼
│ after_insert → outbox entry → process_outbox_entry
│ │
│ ▼
│ _send_outbox_message():
│ ├─ _send_dynamic_header_for_outbox(msg, account, caption=msg.message)
│ │ ├─ Template has openwa_dynamic_header? → render doc as PNG → send image with caption
│ │ └─ Returns True if sent (done)
│ └─ If no image: _send_via_openwa()
│ └─ POST /messages/send-text
│
└─ (empty) → fallback to _send_openwa_template()
User saves WhatsApp Templates doc
│
▼
OverrideWhatsAppTemplates.before_save()
│
├─ not OpenWA account? → super().before_save() → Meta API sync
│
└─ OpenWA path → _sync_to_openwa()
│
▼
Build payload: { name, body, header, footer }
│
├─ openwa_template_id exists?
│ │
│ ├─ Try PUT /templates/:id
│ │ │
│ │ ├─ 200 OK → done
│ │ │
│ │ └─ 404 Not Found (stale ID)?
│ │ │
│ │ ▼
│ │ Clear openwa_template_id
│ │ GET /templates → find by name
│ │ │
│ │ ├─ Found → PUT with correct ID
│ │ └─ Not found → POST (create fresh)
│ │
│ └─ Other error → log and continue
│
└─ No openwa_template_id?
│
▼
POST /templates (create)
│
▼
Save openwa_template_id = response.id
Set status = "APPROVED"
OpenWA receives message from WhatsApp
│
▼
OpenWA webhook → POST /api/method/openwa_bridge.inbound.receive_openwa_message
│
Headers: X-OpenWA-Signature: sha256=<hmac-hex> (optional)
Headers: X-OpenWA-Idempotency-Key: <unique-id> (optional)
Body: { event, sessionId, data: { from, to, body, type, ... } }
│
▼
receive_openwa_message()
│
1. Parse request body
2. Validate event field present
3. Rate limiting: 60 req/min per IP
4. Resolve WhatsApp Account by session ID
5. HMAC verification (lenient mode)
6. Idempotency check
7. Route by event type:
│
├─ "message.received" → _handle_inbound_message()
│ ├─ Skip if fromMe
│ ├─ Extract sender JID → strip_jid_suffix()
│ ├─ Create WhatsApp Message doc
│ ├─ Download and attach media if present
│ ├─ Create WhatsApp Profile
│ ├─ _create_communication() → create Communication + auto-create Lead/Contact
│ └─ Log success
│
├─ "message.sent" / "message.ack" / "message.failed" → _handle_status_update()
│
├─ "message.revoked" → _handle_message_revoked()
│
├─ "message.reaction" → _handle_message_reaction()
│
├─ "status.received" → _handle_status_received()
│
├─ "message.receipt" → _handle_message_receipt()
│ └─ Opt-in per-recipient delivery/read/played receipt (group + status broadcasts)
│
├─ "session.status" / "session.disconnected" → _handle_session_status()
│
├─ "session.qr" → _handle_session_qr()
│
└─ "session.authenticated" → _handle_session_authenticated()
│
▼
Return {"status": "ok"} (200)
User opens WhatsApp Account form
│
▼
refresh() → get_openwa_session_status()
│
├─ openwa_session_id empty? → Show "Setup OpenWA" button
│ │
│ ▼
│ setup_openwa_session(account_name)
│ │
│ ├─ Connectivity check (10s timeout)
│ ├─ Sanitize account name → session name
│ ├─ GET /api/sessions → find existing or POST to create
│ ├─ Save openwa_session_id
│ ├─ Check status → POST /start if needed (60s timeout)
│ └─ GET /qr → return QR data URL
│
│ ▼
│ JS renders QR in openwa_qr_html field
│ Auto-refreshes every 55 seconds
│ User scans QR → status changes to "ready"
│
└─ openwa_session_id exists? → Check live status → show QR / Connected / Disconnected
User deletes WhatsApp Account in Frappe Desk
│
▼
doc_events["on_trash"] → on_account_trash(doc, method)
│
├─ openwa_enabled = 0? → return
├─ openwa_session_id empty? → return
│
└─ DELETE /api/sessions/:session_id
│
├─ 204 No Content → session deleted
└─ Error → log error, don't block Frappe delete
Frappe Scheduler (hourly/daily)
│
▼
_run_health_check()
│
├─ Query: openwa_enabled=1 AND openwa_session_id IS SET
│
└─ For each account:
│
▼
_check_session_status(base_url, session_id, api_key)
│
├─ Connection error → set status to "disconnected"
├─ status = "ready" → update status
└─ status != "ready" → POST /start → wait → re-check
_send_via_openwa(account, meta_payload)
│
▼
_ensure_session_ready(account)
│
├─ 1. GET /api/sessions/:id → check status
│ ├─ ready → return (all good)
│ └─ not ready → continue
│
├─ 2. POST /start (60s timeout)
│
├─ 3. Wait 5s
│
├─ 4. GET /api/sessions/:id → re-check
│ ├─ ready → return (recovered!)
│ ├─ qr_ready → throw "requires QR re-scan"
│ └─ other → continue
│
└─ 5. throw "session could not be recovered"
WhatsApp Message created (type="Outgoing")
│
▼
notify() → set _openwa_outbox_needed = True
│
▼
after_insert() → create OpenWA Outbox doc → enqueue process_outbox_entry
│
▼ [background worker]
process_outbox_entry()
│
1. Load outbox + message + account
2. Guard checks (status, retry time, attempts, openwa_enabled)
3. Circuit breaker check
4. _send_dynamic_header_for_outbox() → render + send image if configured
5. _send_via_openwa() → send-template or send-text
6. Success → record_success(), status=Sent
7. Failure → record_failure(), exponential backoff
→ after max_attempts → status=Failed
User saves WhatsApp Account
│
▼
on_account_update(doc, method)
│
├─ openwa_enabled = 0? → return
├─ openwa_session_id empty? → return
│
└─ GET /webhooks → list all webhooks
│
├─ Found matching URL → PUT /webhooks/:id { secret }
└─ Not found → POST /webhooks { url, events, secret }
Whitelisted method called from Desk
│
▼
Check frappe.has_permission()
│
└─ Route by method:
│
├─ check_whatsapp_number → GET /contacts/check/:number
├─ block_contact → POST /contacts/:jid/block
├─ unblock_contact → DELETE /contacts/:jid/block
├─ send_typing_indicator → POST /chats/typing { chatId, state }
├─ send_bulk_openwa → POST /messages/send-bulk { chatIds, text }
├─ forward_message → POST /messages/forward { messageId, chatId }
├─ delete_message → DELETE /messages/:id (?revoke=true)
├─ request_pairing_code → POST /pairing-code { phoneNumber }
└─ send_sticker → POST /messages/send-sticker { chatId, url/base64 }
Frappe Scheduler (daily)
│
▼
cleanup_old_outbox()
│
├─ DELETE WHERE status='Sent' AND creation < (now - 7 days)
└─ DELETE WHERE status='Failed' AND creation < (now - 30 days)
Whitelisted openwa_bridge.whatsapp_account methods for the newer OpenWA API surface. All follow the same pattern: permission check → _get_account → openwa_api(account, METHOD, <relative path>, json_data). Relative paths are joined onto /api/sessions/{session_id} by openwa_api() (handles the uniform {sessionId} routing in v0.19+).
messages/
vote_poll(account, chat_id, poll_message_id, options)
└─ POST /messages/vote-poll { chatId, pollMessageId, options[] }
click_button(account, chat_id, message_id, button_id, text=None)
└─ POST /messages/click-button { chatId, messageId, buttonId, text? }
(structured prompt reply, Baileys; wweb.js → 501)
pin_message(account, chat_id, message_id, duration_seconds=604800)
└─ POST /messages/pin { chatId, messageId, durationSeconds }
unpin_message(account, chat_id, message_id)
└─ POST /messages/unpin { chatId, messageId }
star_message(account, chat_id, message_id, star=1)
└─ POST /messages/star { chatId, messageId, star }
get_chat_media(account, chat_id, message_id)
└─ GET /messages/:chatId/:messageId/media → { status: "ok", media }
chats/
archive_chat(account, chat_id, archive=1)
└─ POST /chats/archive { chatId, archive }
mute_chat(account, chat_id, mute_until=0)
└─ POST /chats/mute { chatId, muteUntil }
pin_chat(account, chat_id, pin=1)
└─ POST /chats/pin { chatId, pin }
clear_chat_messages(account, chat_id)
└─ DELETE /chats/:chatId/messages
get_session_proxy(account) / set_session_proxy(account, proxy_url)
└─ GET /proxy / PATCH /proxy { proxyUrl }
groups/
get_group_membership_requests(account, group_id)
└─ GET /groups/:groupId/membership-requests
approve_group_membership_requests(account, group_id, participants="")
└─ POST /groups/:groupId/membership-requests/approve (empty = approve all)
reject_group_membership_requests(account, group_id, participants="")
└─ POST /groups/:groupId/membership-requests/reject (empty = reject all)
statuses/
post_status_voice(account, url="" | base64="", caption="")
└─ POST /status/send-voice
channels/
create_channel(account, name, description="")
└─ POST /channels { name, description }
mute_channel(account, channel_id, mute=1)
└─ POST /channels/:channelId/mute { mute }
demote_channel_admin(account, channel_id, user_id)
└─ POST /channels/:channelId/admins/demote { userId }
transfer_channel_ownership(account, channel_id, new_owner_id)
└─ POST /channels/:channelId/owner/transfer { newOwnerId }
-
send_catalog_messageis deprecated since OpenWA 0.19 (endpoint removed, 501) — the method returns a clear error and never calls OpenWA; usesend_product_message(product card, Baileys) orsend_product_to_chat/send_catalog_to_chat(text+image / text-summary fallbacks). -
mark_chat_readacceptsmessage_ids— comma-separated list forwarded as v0.23messageIds(max 100, Baileys) for per-message read receipts. -
Session proxy —
set_session_proxywith an emptyproxy_urlclears the proxy (proxyUrl: null).