Skip to content

Message Flows

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

Message Flows

Flow 1: Outbound Plain Text (Desk → WhatsApp)

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 ──

Flow 2: Outbound Template (Desk → WhatsApp)

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"

Flow 3: Notification (After Submit → WhatsApp)

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

Flow 4: Template Sync (Save on WhatsApp Templates)

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"

Flow 5: Inbound Message (WhatsApp → Desk)

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)

Flow 6: One-Click Session Setup

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

Flow 7: Account Deletion

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

Flow 8: Scheduled Health Check

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

Flow 9: Pre-Send Session 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"

Flow 10: Outbox Processing

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

Flow 11: Webhook Auto-Sync

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 }

Flow 12: Contact Management & Advanced Messaging

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 }

Flow 13: Outbox Cleanup (Daily)

Frappe Scheduler (daily)
  │
  ▼
cleanup_old_outbox()
  │
  ├─ DELETE WHERE status='Sent' AND creation < (now - 7 days)
  └─ DELETE WHERE status='Failed' AND creation < (now - 30 days)

Flow 15: OpenWA v0.19-v0.25 Whitelisted Methods

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 }

Key Points

  • send_catalog_message is deprecated since OpenWA 0.19 (endpoint removed, 501) — the method returns a clear error and never calls OpenWA; use send_product_message (product card, Baileys) or send_product_to_chat / send_catalog_to_chat (text+image / text-summary fallbacks).
  • mark_chat_read accepts message_ids — comma-separated list forwarded as v0.23 messageIds (max 100, Baileys) for per-message read receipts.
  • Session proxy — set_session_proxy with an empty proxy_url clears the proxy (proxyUrl: null).