← Blog
odooapiwebhooksjson-rpcautomation

Odoo Webhooks and the API: Send, Receive, and Configure

How Odoo webhooks actually work: native outgoing webhooks in Odoo 17+, the pre-17 pattern, and how to configure and trigger them through the JSON-RPC API.

ODXProxy Team · Jul 6, 2026 · 8 min read

Odoo Webhooks and the API: Send, Receive, and Configure — ODXProxy blog cover

"Does Odoo have a webhook API?" is one of those questions with a yes-and-no answer, and the confusion costs people real time. Odoo has outgoing webhooks — it can POST to your URL when a record changes — but it isn't a webhook receiver the way Stripe or GitHub are, and the JSON-RPC API is request/response, not event-driven. Once you see which direction each piece points, the whole picture gets simple. This guide covers native outgoing webhooks in Odoo 17 and later, the older pattern for earlier versions, how to configure webhooks through the API instead of clicking around the UI, and how the API and webhooks fit together for a real integration.

The two directions, kept straight

Almost every webhook mistake comes from mixing up who calls whom. There are two independent flows:

  • Outgoing (Odoo → you): something happens in Odoo (an order is confirmed, a partner is created) and Odoo sends an HTTP POST to a URL you control. This is what Odoo's built-in webhooks do.
  • Inbound requests (you → Odoo): your app calls Odoo to read or change data. This is the JSON-RPC API — a synchronous request that returns a response. It is not a webhook; nothing is "pushed."

The API and outgoing webhooks are complementary, not alternatives. Webhooks tell you when something happened; the API lets you act on it. A typical integration uses both: Odoo fires a webhook, and your handler turns around and calls the API to fetch the full record or write something back.

Odoo's API speaks JSON-RPC 2.0, not REST, and a call can return HTTP 200 while still carrying an error. If you haven't made a first call yet, start with authenticating to the Odoo API — the rest of this article assumes that request/response shape.

Outgoing webhooks in Odoo 17+ (native)

Since Odoo 17, outgoing webhooks are a first-class feature of Automation Rules — no custom code required. In the UI (developer mode on) you go to Settings → Technical → Automation Rules, create a rule on a model, pick a trigger (e.g. On Creation, On Update), and add an action of type Send Webhook Notification with a target URL. When the trigger fires, Odoo POSTs a JSON payload to that URL.

Under the hood that's two records: a base.automation rule bound to a server action (ir.actions.server) whose type is a webhook. The action carries the destination URL and the set of fields to include in the payload. Knowing the model names matters because — as we'll see — you can create the exact same thing through the API.

Odoo's outgoing webhook payload is intentionally minimal — typically the record id and the fields you selected, not the whole record. Treat the webhook as a notification: "record 42 on sale.order changed." Then call the API to fetch whatever detail you actually need. Don't assume the payload contains everything.

Before Odoo 17: the automated-action pattern

On Odoo 16 and earlier there's no "Send Webhook" button, but you can get the same outgoing behavior with an Automated Action that runs Python. Create the action on your model with a trigger, set its type to Execute Python Code, and POST from the server:

# Server action code (runs inside Odoo). `record` and `env` are in scope.
import requests

payload = {
    "model": record._name,
    "id": record.id,
    "name": record.display_name,
}
requests.post(
    "https://your-app.example.com/hooks/odoo",
    json=payload,
    timeout=5,
)

This works back through many Odoo versions, but it has sharp edges: the POST runs inside the transaction, so a slow or failing endpoint can hold a database lock or roll back the user's action. Keep the timeout short, and if you can, hand off to a queued job rather than calling out inline. The native Odoo 17 webhook handles this dispatch more gracefully, which is one good reason to prefer it where available.

Configuring webhooks through the API

Here's the part people miss: because Odoo webhooks are just records (base.automation + ir.actions.server), you can create and manage them through the JSON-RPC API instead of clicking through the UI on every instance. That's exactly what you want when you're provisioning webhooks across many client databases.

The one caveat is that the field names on these technical models vary by Odoo version, so don't hard-code them from a blog post — ask the instance. That's what the fields_get action is for. Through ODXProxy, inspect the automation model first:

{
  "id": "fields-1",
  "action": "fields_get",
  "model_id": "base.automation",
  "params": [],
  "keyword": { "attributes": ["string", "type", "relation"] },
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}

That returns the real field set for this version, so you create the rule against fields you've confirmed exist rather than ones you hoped exist. Then create the webhook rule with the ordinary create action — the same nine-action vocabulary every other call uses:

{
  "id": "create-hook-1",
  "action": "create",
  "model_id": "base.automation",
  "params": [{
    "name": "Notify app on new partner",
    "model_id": 63,
    "trigger": "on_create",
    "webhook_url": "https://your-app.example.com/hooks/odoo"
  }],
  "keyword": {},
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}
The field names above (model_id, trigger, webhook_url) are illustrative — confirm them with fields_get on your target version before you run this. On some versions the webhook lives on a linked ir.actions.server record with state set to a webhook type, and you create that first, then reference it. The mechanism is stable; the exact schema drifts.

Remember the two-layer error check on every one of these calls: proxy-level failures come back non-200, but if the integration user lacks rights to write technical models like base.automation, Odoo returns an AccessError on an HTTP 200. See Odoo API error handling for the full model — configuring automations is exactly the kind of privileged operation that trips the 200 trap.

Triggering Odoo from your side

The mirror image of "Odoo notifies me" is "I make something happen in Odoo." You don't need a special webhook for this — you call the API, and any automation rules on that model fire as a side effect.

Creating or updating a record through the proxy runs Odoo's normal ORM hooks, so an On Creation or On Update automation triggers just as if a user had done it in the UI:

{
  "id": "trigger-1",
  "action": "create",
  "model_id": "res.partner",
  "params": [{ "name": "Acme GmbH", "is_company": true }],
  "keyword": {},
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}

For business events that aren't a plain create/write — confirming an order, posting an invoice — you call the model's own method with call_method and a fn_name, and any automation watching that state change fires too:

{
  "id": "confirm-1",
  "action": "call_method",
  "model_id": "sale.order",
  "fn_name": "action_confirm",
  "params": [[42]],
  "keyword": {},
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user API key>"
  }
}

call_method is the escape hatch for anything outside the eight direct data actions — the method name goes in fn_name, and params stays a JSON array. There's a whole guide on it in calling Odoo model methods through the API.

What Odoo does not give you out of the box

Two honest limitations, because assuming otherwise leads to a bad afternoon:

  • A generic inbound webhook receiver. Odoo won't accept an arbitrary third-party webhook (a Stripe event, a shipping update) at a ready-made URL. For that you either use a purpose-built module (payment providers ship their own controllers) or write a small http.Controller route in a custom module. The JSON-RPC API isn't that receiver — it's for your code calling Odoo, authenticated per request.
  • Delivery guarantees on outgoing webhooks. Odoo's outgoing webhook is a best-effort POST, not a durable queue with retries and dead-lettering. If your endpoint is down when the event fires, treat the webhook as a hint and reconcile with a periodic API poll (search_read for records changed since your last checkpoint) so a missed POST doesn't mean lost data.
A robust integration pairs both directions: outgoing webhooks for low-latency "something changed" signals, and a scheduled search_read sweep on write_date as the backstop that catches anything the webhook missed. Belt and suspenders beats trusting either alone.

Where to go next

Odoo webhooks are real but one-directional: Odoo POSTs out (natively from 17, via automated actions before that), while everything inbound goes through the JSON-RPC API. The lever most people overlook is that webhooks are just automation records — so you can provision and manage them through the same API, across every instance, once you've confirmed the schema with fields_get.