← Blog
odooapicorsbrowsergatewaysecurity

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

Odoo API CORS: Calling It From a Browser or App — ODXProxy blog cover

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:

  1. CORS blocks the request. Browsers enforce the same-origin policy: JavaScript on https://app.example.com cannot read a response from https://erp.example.com unless Odoo explicitly returns headers permitting that origin. Odoo doesn't, so the browser blocks it.
  2. 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.
If a solution involves putting an Odoo API key into JavaScript that runs in a browser or ships inside a mobile app binary, it is not a fix — it's a credential leak. Mobile app bundles are trivially decompiled; an embedded key is a public key.

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  →  Odoo

This 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-Origin for 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.

A 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