← Blog
odoowhatsappintegrationmessagingjson-rpc

Connect Odoo to WhatsApp with the API

Send Odoo notifications to WhatsApp and turn inbound messages into Odoo records — the models, the JSON-RPC calls, and the webhook patterns that make it reliable.

ODXProxy Team · Jul 13, 2026 · 8 min read

Connect Odoo to WhatsApp with the API — ODXProxy blog cover

"Can I connect Odoo to WhatsApp?" comes up constantly — order confirmations pushed to a customer's phone, a leads bot that files enquiries straight into the CRM, delivery updates that don't need an email. Odoo Enterprise ships a native WhatsApp module, but if you're on Community, or your logic is more custom than templates allow, you build the bridge yourself against two APIs: WhatsApp Business Cloud on one side and the Odoo External API on the other. This guide shows both directions — Odoo → WhatsApp notifications and WhatsApp → Odoo records — with the exact calls and the webhook patterns that keep it reliable. Every Odoo call runs over JSON-RPC through a single endpoint.

The two APIs you're bridging

  • WhatsApp Business Cloud API — Meta-hosted, part of the Graph API. You send messages with a POST to https://graph.facebook.com/<version>/<phone-number-id>/messages, authenticated with a bearer access token, and you receive messages via a webhook you register in the Meta app dashboard. Business-initiated conversations must use pre-approved message templates; free-form replies are only allowed inside the 24-hour customer service window.
  • Odoo External API — the ERP side, reached here through the ODXProxy JSON-RPC endpoint.

Your integration is a small service in the middle: it listens for Odoo events to push messages out, and listens for WhatsApp webhooks to write records in.

Two directions, two flows

FlowTriggerDirection
NotificationsAn Odoo event (order confirmed, invoice posted)Odoo → WhatsApp
Inbound captureA customer sends a WhatsApp messageWhatsApp → Odoo

They share nothing but the customer's phone number, which is the join key between the two systems. Get phone-number matching right and the rest is plumbing.

The Odoo models you'll touch

  • res.partner — contacts. The phone and mobile fields are how you match a WhatsApp sender to an existing customer.
  • crm.lead — leads and opportunities, the natural home for an inbound enquiry.
  • sale.order / account.move — the orders and invoices whose state changes trigger outbound notifications.
  • mail.message — Odoo's chatter, if you want the conversation logged against the contact.

You reach all of them through the nine actions of the External API. If these models are unfamiliar, Odoo search_read, in depth and create, write, unlink cover the read and write mechanics used below.

One endpoint, two secrets

Every Odoo call goes to the same route through the proxy:

POST /api/odoo/execute

Keep the two proxy-side secrets straight — conflating them is the usual cause of a stray 401:

  • x-api-key — the proxy's key, an HTTP header authenticating your integration to the proxy.
  • odoo_instance.api_key — the Odoo user's key, sent per request in the body, authenticating the ERP call itself.

Neither is the WhatsApp access token — that belongs to the Meta side. The full proxy credential setup is in How to authenticate to the Odoo API.

Flow 1 — push an Odoo event to WhatsApp

Say a sale.order is confirmed and you want to text the customer. Two steps: read what you need from Odoo, then hand it to the WhatsApp API.

Read the order and the customer's number in one search_read, following the partner_id relation with a dotted path so you don't need a second call:

{
  "id": "read-order-1",
  "action": "search_read",
  "model_id": "sale.order",
  "params": [[["name", "=", "S00042"]]],
  "keyword": { "fields": ["name", "amount_total", "partner_id"], "limit": 1 },
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}

Then send a template message from your service to the WhatsApp Cloud API, filling the template variables with the order number and total. Because this is business-initiated, it must be a pre-approved template — free text would be rejected outside the 24-hour window.

To trigger it, either poll Odoo for state changes on a short interval, or — better — have Odoo push the event to you with an outgoing webhook, so you only call out when something actually changed. Setting those up is covered in The Odoo webhook API.

Flow 2 — turn an inbound WhatsApp message into an Odoo record

When a customer messages your WhatsApp number, Meta delivers the message to your webhook. Your handler matches the sender to a contact and files the enquiry. Do it idempotently.

Step 1 — match the sender to a contact. WhatsApp gives you the sender's phone number; look for an existing res.partner before creating anything:

{
  "id": "find-partner-1",
  "action": "search_read",
  "model_id": "res.partner",
  "params": [["|", ["mobile", "=", "+15551234567"], ["phone", "=", "+15551234567"]]],
  "keyword": { "fields": ["id", "name"], "limit": 1 },
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}

That domain uses the | prefix operator to match either field — the operator syntax is in Odoo domain filters explained. If the result is empty, create a new partner from the WhatsApp profile name and number; otherwise reuse the id you got back.

Step 2 — file the enquiry as a lead. Create a crm.lead linked to that partner, carrying the message body so a salesperson sees the context:

{
  "id": "create-lead-1",
  "action": "create",
  "model_id": "crm.lead",
  "params": [{
    "name": "WhatsApp enquiry from Dana Lee",
    "partner_id": 57,
    "contact_name": "Dana Lee",
    "description": "Do you ship the walnut desk to Portugal?"
  }],
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}

To avoid a duplicate lead per message in the same conversation, store the WhatsApp conversation or message id on the record (a field like x_wa_conversation_id) and search_count on it before creating — the same external-id idempotency habit every reliable integration leans on. The write mechanics are in create, write, unlink.

Making it a bot, not just a bridge

The moment you want to reply intelligently — answer "where's my order?" or "do you have this in stock?" — the pattern becomes an LLM with tools. The model reads the inbound WhatsApp text, decides which Odoo call to make, and you execute it through the same /api/odoo/execute endpoint. Reads like order status map to search_read; actions like booking a callback map to call_method. That tool-use loop, with the exact function-calling schema, is laid out in Odoo + LLM function calling. The WhatsApp layer just becomes the transport that carries the user's turn in and the model's answer back out.

Webhook reliability

Both directions lean on webhooks, and two rules keep them trustworthy:

  • Verify the signature. WhatsApp signs each webhook body with HMAC-SHA256 and sends it in the X-Hub-Signature-256 header; recompute it with your app secret and compare before you act. (You also answer Meta's one-time hub.challenge verification when you register the webhook.)
  • Treat delivery as at-least-once. Meta retries webhooks, so the same message can arrive twice. Your handler must be idempotent — which is why Step 2 stores a message id and checks it before creating.
A 200 from the proxy does not mean Odoo accepted the write. Odoo logic errors — a missing required field, a blocked access right — come back with HTTP 200 and a populated error object. Check the HTTP status first, then check the body's error field before you acknowledge the WhatsApp webhook.

The full two-layer error model — proxy failures versus Odoo logic errors, and how to retry each — is in Odoo API error handling.

A realistic scope note

If you run Odoo Enterprise and your needs are standard — templated order and invoice notifications — try the native WhatsApp module first; it wires the Cloud API into Odoo without code. Build a custom integration when you're on Community, when you need bot logic beyond templates, or when the conversation has to touch models the native module doesn't. When you do, the External API gives you the whole ERP surface, and the patterns above — phone-number matching, external-id idempotency, and the two-step error check — are what keep it from double-filing leads or dropping messages.

Where to go next