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, thensale.order/createwith 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 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, 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 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.
/ 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_createtakes 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/createtakes 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 <[email protected]>"}. 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 |
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.
/ Retries
What happens when an Odoo call is retried?
Step by step, because each step is its own transaction:
find_or_createis safe to repeat. The second call finds the partner the first one created.createis 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 sameclient_order_ref. It warns, it does not block, so somebody still has to cancel the leftover draft.action_confirmon 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 covers routing through n8n instead, and the Xero article covers an accounting API where WordPress cannot hold the token.
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.