---
title: "WooCommerce Odoo Integration: Sales Orders via JSON-2 API"
description: "WooCommerce to Odoo 19 without a connector: the JSON-2 API, find_or_create customers, sales order lines in one call, and the Custom plan you need."
url: "https://wpwebhooks.org/blog/woocommerce-odoo-integration/"
date: "2026-11-03"
---

# WooCommerce Odoo Integration: Sales Orders via JSON-2 API

**TL;DR:** Odoo 19 has a plain HTTP API, `/json/2/<model>/<method>` with a bearer API key, so WordPress can create the customer and the sales order with no middleware. Three calls, chained.

-   External API access needs Odoo's **Custom** plan. It is not available on One App Free or Standard.
-   XML-RPC and JSON-RPC are deprecated since Odoo 19, with removal scheduled for **Odoo Online 21.1 (winter 2027)** and Odoo 22 (fall 2028).
-   API keys cannot last longer than **three months**. Rotation is a calendar task, not an option.
-   Each JSON-2 call is its own transaction. Use `res.partner/find_or_create`, which finds or creates in one call, then `sale.order/create` with the lines inside it.
-   Put the WooCommerce order ID in `client_order_ref`. Odoo flags draft orders for the same customer with the same reference as possible duplicates.

/ Ready-made

## Is there an official Odoo connector for WooCommerce?

No. Odoo does not publish one. The [Odoo Apps store](https://apps.odoo.com/apps/modules/browse?search=woocommerce) lists third-party connectors, from free modules to paid ones costing several hundred euros, which import orders, sync products and stock, and push shipment status back to WooCommerce. They are Odoo modules, so they need a database where you can install third-party apps. On Odoo Online that rules out most of them.

If you need two-way product and stock sync, buy one of those. Building inventory sync from webhooks means handling every edge case the connector vendor has already found.

Build it yourself when the job is one-directional and specific: paid orders become sales orders, maybe only for one country or one customer type, and the same order also goes to a CRM or a fulfilment service with one delivery log for all of them.

/ API

## Which Odoo API should a WooCommerce integration use?

The [External JSON-2 API](https://www.odoo.com/documentation/19.0/developer/reference/external_api.html), new in Odoo 19. You POST a JSON object to `/json/2/<model>/<method>`, with an `Authorization: bearer <api key>` header, and every method parameter is a named key in the body. A success is a 200 with the method's return value as JSON. An error is a 4xx or 5xx with a JSON error object.

Older tutorials use XML-RPC through `/xmlrpc/2/object` and `execute_kw`. Don't start anything new on it. The [External RPC API page](https://www.odoo.com/documentation/19.0/developer/reference/external_rpc_api.html) says both XML-RPC and JSON-RPC are "scheduled for removal in Odoo 22 (fall 2028) and Online 21.1 (winter 2027)". An integration written on XML-RPC today has about a year on Odoo Online before it has to be rewritten.

Two prerequisites come before any code:

-   **The plan.** The documentation says access via the external API "is only available on Custom Odoo pricing plans" and "is not available on One App Free or Standard plans". Odoo's own apps calling out (bank sync, carriers, payment providers) are part of Standard. Your store calling in is not.
-   **The key.** Create a dedicated integration user with access to Sales and Contacts only, then under _Preferences → Account Security_ create an API key. A key needs a description and a duration, and "it is not possible to create keys that last for more than three months". Every call runs with that user's access rights.

FIG 01 — Three JSON-2 calls, chained: each one is its own transaction, so each one has to be safe to repeat

/ Transactions

## Why can't you create the customer and the order in one Odoo transaction?

Because JSON-2 does not allow it. The documentation is direct: "All calls to the JSON-2 endpoint run in their own SQL transaction," and "it is not possible to chain multiple calls inside a single transaction". Its advice is to call one method that does all the related work, the way `search_read` searches and reads in one step.

Two methods that already exist cover most of this job:

-   **`res.partner/find_or_create`** takes an email address (it also accepts `"Name <email>"`), returns the existing partner with that email, or creates one. One call, one transaction, with no gap where a second request could create the same customer twice. JSON-2 returns the partner as a list of IDs, such as `[42]`.
-   **`sale.order/create`** takes the order and its lines together. The lines are one2many commands inside the same body, so a sales order without lines can't be left behind.

Confirming the quotation, `sale.order/action_confirm`, is a third call. Leave it out if someone reviews web orders before they become sales orders.

JSON — POST /json/2/sale.order/create

```
{
  "vals_list": [
    {
      "partner_id": 42,
      "client_order_ref": "WC-1234",
      "order_line": [
        [0, 0, { "product_id": 17, "product_uom_qty": 2, "price_unit": 25.0 }],
        [0, 0, { "product_id": 9,  "product_uom_qty": 1, "price_unit": 4.9 }]
      ]
    }
  ]
}
```

`[0, 0, {…}]` is the ORM's "create this line" command. `vals_list` is the parameter name of `create` in Odoo 19's ORM, because it creates a list of records. The response is a list of new IDs, such as `[311]`.

`price_unit` and `product_uom_qty` are computed fields that can be written, so the values you send win over Odoo's pricelist. That is what you want: the sales order should show what the customer actually paid in WooCommerce, not what Odoo would have charged.

/ Chain

## How does WordPress send three dependent calls to Odoo?

As a chain of three webhooks, each one fired by the previous one's success. The first posts to `find_or_create`, with a one-line snippet that turns the order's billing name and email into `{"email": "Ana Lee <ana@example.com>"}`. Its 2xx response body, `[42]`, travels to the second webhook as `args.0.response.body`, together with the original order as `args.0.original_payload`. A pre-dispatch snippet on that second webhook builds the `sale.order/create` body from both:

PHP — pre-dispatch snippet on the second link (builds the sale.order body)

```
$up    = $payload['args'][0];
$order = $up['original_payload']['args'][1];   // WC_Order::get_data()

// WooCommerce product id -> Odoo product.product id. Fine for a small, stable catalogue.
$products = [ 101 => 17, 102 => 23 ];
$shipping = 9;                                  // an Odoo service product for delivery

$lines = [];
foreach ( $order['line_items'] as $item ) {
    $lines[] = [ 0, 0, [
        'product_id'      => $products[ $item['product_id'] ] ?? 0,
        'product_uom_qty' => $item['quantity'],
        'price_unit'      => $item['total'] / $item['quantity'],   // after coupons, before tax
    ] ];
}
if ( $order['shipping_total'] > 0 ) {
    $lines[] = [ 0, 0, [ 'product_id' => $shipping, 'product_uom_qty' => 1,
                         'price_unit' => (float) $order['shipping_total'] ] ];
}

return [ 'vals_list' => [ [
    'partner_id'       => $up['response']['body'][0],
    'client_order_ref' => 'WC-' . $order['id'],
    'order_line'       => $lines,
] ] ];
```

The authorization header comes from a stored Bearer credential holding the API key, not from the snippet. Odoo matches `bearer` without caring about case, so a `Bearer` prefix works. The key never appears in code, and the delivery log stores the header as `[redacted]`.

The product map is the honest weak spot. WooCommerce's order data carries each line's `product_id` but not its SKU, so the map is a hard-coded list of IDs, and an unmapped product sends `product_id: 0`, which Odoo rejects with a 4xx. That is the right failure: the delivery shows up in the log as failed instead of silently creating an order without the line. For a catalogue that changes every week, a connector that syncs products is the better tool.

> Every JSON-2 call commits on its own. Design each step so that running it twice does no harm, because sooner or later a retry will run it twice. — the JSON-2 transaction rule

| Concern | Odoo Apps connector | Webhook Actions chain |
| --- | --- | --- |
| Product and stock sync | Built in, both directions | Not built. Orders flow one way; products are mapped by hand |
| Where it runs | Inside Odoo, so a database that allows third-party modules | In WordPress, calling the JSON-2 API. Needs the Custom plan |
| Which orders go to Odoo | Its import settings | Any condition on the order payload |
| Customer creation | Its own matching rules | Odoo's find\_or\_create: an exact email match, in one transaction |
| Odoo down or slow | Next scheduled import | Each link is retried with backoff, and logged per hop with request and response |
| API end of life | The vendor's job | Already on JSON-2, the API that replaces XML-RPC |

try\_it

Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.

[Try the live preview →](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) [Install plugin](https://downloads.wordpress.org/plugin/flowsystems-webhook-actions.zip)

/ Retries

## What happens when an Odoo call is retried?

Step by step, because each step is its own transaction:

-   **`find_or_create`** is safe to repeat. The second call finds the partner the first one created.
-   **`create`** is not. If Odoo created the order but the response timed out on the way back, a retry creates a second one. The chain only continues from the attempt that succeeded, so the first order stays a draft, and its form shows _"Warning: this order might be a duplicate of"_ with a link to the other one. Odoo raises that for draft orders with the same customer and the same `client_order_ref`. It warns, it does not block, so somebody still has to cancel the leftover draft.
-   **`action_confirm`** on an order that is already confirmed fails, and the failure is logged. Nothing is confirmed twice.

The most likely cause of a timeout is the sender's own clock. A 5-second HTTP timeout is a common default for webhook senders, and creating a sales order with several lines on a busy Odoo Online database can take longer than that. Raise the timeout for these webhooks so a slow success is not mistaken for a failure and retried.

/ Exposure

## What does Odoo not protect you from?

**An expired key.** At most three months, and nothing reminds you. On the day it expires, every call returns 401 and every order fails. Put the rotation in a calendar, and watch the failure notifications.

**Too much access.** The key acts as its user. An integration running as an administrator can rewrite anything in the database, so give it a user with Sales rights and nothing else.

**Customers merged by email.** `find_or_create` matches on email only. Two people sharing an office address become one partner. For B2B stores, match on the VAT number in a separate step.

**The price you sent.** Odoo accepts the `price_unit` you send and computes taxes from the product's own tax settings. If those differ from WooCommerce's, the order total in Odoo will not match what the customer paid. Test one order per tax rate before going live.

The same delivery pattern feeds other systems from one order. [The WooCommerce to n8n article](https://wpwebhooks.org/blog/woocommerce-n8n-webhook/) covers routing through n8n instead, and [the Xero article](https://wpwebhooks.org/blog/woocommerce-xero-integration/) covers an accounting API where WordPress cannot hold the token.

/Footnotes

¹ Endpoint, headers, key duration, plan availability and transaction rules from the [Odoo 19 External JSON-2 API](https://www.odoo.com/documentation/19.0/developer/reference/external_api.html) page, read 2026-11-05.

² `find_or_create`, `create(vals_list)`, the duplicate check on `client_order_ref` and the case-insensitive bearer check are from the [Odoo 19.0 source](https://github.com/odoo/odoo/tree/19.0).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WooCommerce Odoo Integration: Sales Orders via JSON-2 API","description":"WooCommerce to Odoo 19 without a connector: the JSON-2 API, find_or_create customers, sales order lines in one call, and the Custom plan you need.","datePublished":"2026-11-03","dateModified":"2026-11-03","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/woocommerce-odoo-integration/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["woocommerce odoo integration","odoo woocommerce connector","odoo json 2 api","odoo external api","odoo xmlrpc deprecated","odoo sale order api"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WooCommerce Odoo Integration: Sales Orders via JSON-2 API","item":"https://wpwebhooks.org/blog/woocommerce-odoo-integration/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Does Odoo have an official WooCommerce connector?","acceptedAnswer":{"@type":"Answer","text":"No. Odoo does not publish one. The Odoo Apps store lists third-party WooCommerce connectors, free and paid, which run as Odoo modules and handle product and stock sync. Most of them need a database where third-party modules can be installed."}},{"@type":"Question","name":"Which Odoo API should I use for a new integration?","acceptedAnswer":{"@type":"Answer","text":"The External JSON-2 API, new in Odoo 19: POST to /json/2/<model>/<method> with an Authorization: bearer <api key> header and named parameters in the JSON body. XML-RPC and JSON-RPC are deprecated since Odoo 19 and scheduled for removal in Odoo Online 21.1 (winter 2027) and Odoo 22 (fall 2028)."}},{"@type":"Question","name":"Can I use the Odoo API on the Standard plan?","acceptedAnswer":{"@type":"Answer","text":"No. Odoo documents that access via the external API is only available on Custom pricing plans and is not available on One App Free or Standard. Integrations that Odoo itself initiates, such as bank sync or carriers, are part of Standard."}},{"@type":"Question","name":"How long does an Odoo API key last?","acceptedAnswer":{"@type":"Answer","text":"You choose a duration when you create it under Preferences, Account Security, but Odoo does not allow keys that last more than three months. Long-running integrations must rotate their key at least every three months. The key acts with its user’s access rights."}},{"@type":"Question","name":"How do I stop duplicate sales orders when an Odoo call is retried?","acceptedAnswer":{"@type":"Answer","text":"Each JSON-2 call is its own transaction, so a create that succeeded but timed out will create a second order when retried. Put the WooCommerce order ID in client_order_ref: Odoo warns on a draft order that another order for the same customer has the same reference. Raise the sender timeout so slow successes are not retried."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/woocommerce-odoo-integration.png","caption":"FIG 01 — Three JSON-2 calls, chained: each one is its own transaction, so each one has to be safe to repeat","description":"A paid WooCommerce order fires a queued webhook to Odoo res.partner find_or_create with the customer email; Odoo returns the partner id. That success fires a second webhook to sale.order create, built from the partner id and the original order, with client_order_ref set to the WooCommerce order id and the lines as one2many create commands. Its success fires a third call to action_confirm. Every call uses a bearer API key and runs in its own transaction; a failed call is retried on its own.","encodingFormat":"image/png","creator":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org/"},"copyrightHolder":{"@type":"Organization","name":"Flow Systems","url":"https://flowsystems.pl/"},"copyrightNotice":"© Flow Systems","creditText":"WP Webhooks","license":"https://creativecommons.org/licenses/by/4.0/","acquireLicensePage":"https://wpwebhooks.org/image-license/"}
```
