Odoo API CORS: Calling It From a Browser or App
Why calling the Odoo API from a browser or mobile app hits CORS errors and leaks your key — and the server-side gateway pattern that fixes both the right way.
ODXProxy Team · Jul 14, 2026 · 7 min read

You're building a single-page app, an Ionic mobile app, or any browser front-end, and you try to call Odoo directly from client-side JavaScript. It fails — a CORS error in the console, the request blocked before it even reaches Odoo. The instinct is to make the CORS error go away by allowing the origin. Don't: for the Odoo API, the CORS wall is a symptom of a deeper problem, and the "fix" that silences it exposes your credentials to everyone who opens the app. This guide explains why the browser can't call the Odoo API directly, why adding CORS headers is the wrong solution, and the server-side gateway pattern that solves both the CORS error and the security hole at once.
Two problems, not one
When a browser refuses your call to Odoo, there are actually two distinct problems stacked on top of each other, and only one of them is CORS:
- CORS blocks the request. Browsers enforce the same-origin policy: JavaScript on
https://app.example.comcannot read a response fromhttps://erp.example.comunless Odoo explicitly returns headers permitting that origin. Odoo doesn't, so the browser blocks it. - The credentials can't live in the browser. To call the Odoo API you need an Odoo user's API key (and the database name and uid). Anything your JavaScript can read, so can anyone with the browser's dev tools. Shipping an Odoo API key to the client hands every visitor a working ERP credential.
CORS is the one you see first, but the second is the one that matters. Even if you made CORS disappear, you'd be left broadcasting a key that can read and write your ERP.
Why "just enable CORS on Odoo" is the wrong fix
The top search result for an Odoo CORS error is always someone adding permissive Access-Control-Allow-Origin
headers — via a reverse proxy, a custom controller, or a wildcard. It makes the console error go away,
and that's exactly why it's dangerous. It solves problem #1 while making problem #2 worse:
- To actually use the API from the browser after enabling CORS, you still need the Odoo API key in client-side code. Now it's shipped in your JS bundle for anyone to extract.
- A wildcard
Access-Control-Allow-Origin: *invites any website to script against your Odoo instance using a key they can lift from your app. - You've turned a browser safety mechanism off instead of respecting what it's telling you: secrets don't belong on the client.
The right pattern: keep the secret server-side
The durable answer is the same one every production front-end uses to talk to a privileged backend: don't call the ERP from the client at all. Put a server-side hop in between. Your browser or mobile app talks to your own backend over an origin you control; that backend holds the secrets and makes the Odoo call server-to-server, where CORS doesn't apply.
Browser / mobile app → your backend (same origin / CORS you control) → ODXProxy → OdooThis collapses both problems:
- CORS disappears because the browser now talks to your own origin (or a backend where you set a
narrow, correct
Access-Control-Allow-Originfor your app — not for Odoo). Server-to-server calls from your backend onward have no CORS at all; CORS is a browser mechanism. - The Odoo key never ships to the client. It lives on your server. The browser sends only a session cookie or a short-lived token your backend already trusts.
This is often called a backend-for-frontend (BFF): a thin server layer whose whole job is to be the trusted caller the browser can't be.
Where the gateway fits
You could have your backend hold the Odoo database name, uid, and user key and speak Odoo's raw RPC directly. But a cleaner split is to put a gateway — ODXProxy — between your backend and Odoo. That gives you the same two-hop safety with a uniform request shape and a strict action allowlist:
x-api-key— the proxy's key. Your backend holds this and sends it as a header to the gateway. It never touches the browser.odoo_instance.api_key— the Odoo user's key, sent by the gateway (or your backend, per request) to Odoo. Also server-side only.
The browser holds neither. It holds your app's own session, and nothing that works against Odoo or the gateway on its own. Why put a gateway in front rather than calling Odoo straight from your backend is covered in Odoo API gateway: why and how.
Concrete: a browser call that's actually safe
Here's the shape end-to-end. The browser calls your endpoint — same origin, a normal fetch, no Odoo
anything:
// Runs in the browser. No Odoo URL, no db, no api key — just your own API.
const res = await fetch("/api/partners", {
headers: { "Accept": "application/json" },
credentials: "same-origin",
});
const partners = await res.json();Your backend receives that, checks the user's session, and then makes the Odoo call through the gateway with the secrets it holds:
{
"id": "bff-1",
"action": "search_read",
"model_id": "res.partner",
"params": [[["is_company", "=", true]]],
"keyword": { "fields": ["name", "email"], "limit": 20 },
"odoo_instance": {
"url": "https://erp.example.com",
"db": "prod",
"user_id": 2,
"api_key": "<the Odoo user API key — server-side only>"
}
}That request goes to POST /api/odoo/execute with the x-api-key header — from your server, never from
the page. The browser only ever sees the filtered result your backend chooses to return. You can even
narrow what the front-end is allowed to ask for, so a compromised client still can't read models you
never exposed.
200 from the gateway can still carry an Odoo error object (an access or validation error). Have your backend check the HTTP status first, then the body's error field, before returning anything to the browser — see Odoo API error handling.Mobile apps have the same problem
A native iOS or Android app doesn't hit CORS — there's no browser same-origin policy in the way — but it has problem #2 in full. An API key compiled into an app binary can be extracted from the package in minutes. So the pattern is identical: the app talks to your backend with a user token; your backend holds the Odoo/gateway keys and makes the call. The Swift SDK and other clients are built to run in that trusted server tier, not inside the shipped app.
What if I really want the browser to call the gateway?
Sometimes you want to skip the extra backend and let the client call the gateway directly. Be honest
about the trade: to do that, the gateway's x-api-key has to be present in client code, which reintroduces
problem #2. There's no way around the rule that a secret in the browser is not a secret. If you go
this route, treat that key as public, scope it to the absolute minimum, and put it behind your own
authenticated session anyway — at which point you've rebuilt the backend hop. For anything touching real
ERP data, keep the keys server-side.
Note also that ODXProxy is early (v0.1.0); its job is the authenticated server-side entry point — one uniform JSON-RPC envelope, an action allowlist, per-request instance routing. Browser-facing concerns like per-origin CORS policies and public client tokens are things you handle in your own backend tier, which is exactly where they belong.
Where to go next
- Understand the layer this pattern sits on: Odoo API gateway: why and how and putting Odoo behind an nginx reverse proxy.
- Get the two-key model exactly right in how to authenticate to the Odoo API.
- Serve a front-end from Odoo data the same safe way in Odoo as a headless CMS.
- Read the full request/response contract in the API reference, or use a server-side SDK so your backend never hand-rolls the envelope.