← Blog
odooodxproxyapi-gatewayjson-rpcintegration

What ODXProxy Solves: Six Odoo Integration Problems

Credential sprawl, unbounded method access, timeouts, error handling, multi-instance routing, and visibility — what ODXProxy fixes, and what it deliberately doesn't.

ODXProxy Team · Aug 4, 2026 · 10 min read

What ODXProxy Solves: Six Odoo Integration Problems — ODXProxy blog cover

ODXProxy is a reverse proxy that sits in front of one or more Odoo instances and exposes a single authenticated JSON-RPC 2.0 endpoint. That sentence describes the shape of the thing but not the point of it, so this article does the opposite: six concrete problems that show up once real services start calling Odoo, what ODXProxy does about each, and — just as usefully — the problems it does not solve, so you know what you still have to build. It is an early product (v0.1.0), and this page tries to be honest about exactly where that line falls.

1. Credential sprawl across every service

Odoo's External API authenticates each call with a database name, a user id, and that user's API key. Nothing enforces where those live, so the default outcome is that they end up everywhere: the storefront's environment, the mobile backend's secret manager, an automation platform's connection settings, a colleague's .env, a CI job. Rotating the key means finding every copy. Auditing "what touched the ERP" means correlating logs from systems that don't share a format.

ODXProxy replaces that with one credential your services actually hold — the proxy's key, sent as the x-api-key header — and moves the Odoo connection into the request body, where it can be supplied by whatever holds it legitimately:

curl -X POST https://your-proxy.example.com/api/odoo/execute \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ODX_PROXY_KEY" \
  -d '{
    "id": "01J9Z8K3QJ7Y5T2N6V4W8X0ABC",
    "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's API key>"
    }
  }'
These are two different secrets and conflating them is the single most common ODXProxy support question. x-api-key authenticates your caller to the proxy. odoo_instance.api_key authenticates the Odoo call itself. A 401 with code -32000 means the first one is wrong; an Odoo access error returned with HTTP 200 means the second one is. The full breakdown is in how to authenticate to the Odoo API.

2. Any leaked credential can call anything

This is the problem people underestimate. Odoo's execute_kw is not a fixed set of endpoints — it invokes methods on models. A credential that can read customers can also, protocol-wise, attempt to post journal entries, confirm orders, or call any other public method on any model the user can reach. Your webhook receiver only needs search_read, but nothing in the protocol says so.

ODXProxy enforces an action allowlist. Exactly nine actions are directly callable, and anything else is rejected with HTTP 400 and code -32001 before a request is ever made to Odoo:

ActionMeaning
search_countCount records matching a domain.
searchReturn IDs matching a domain.
readRead fields for a list of IDs.
fields_getDescribe a model's fields.
search_readCombined search + read.
createCreate record(s).
writeUpdate record(s) by ID.
unlinkDelete record(s) by ID.
call_methodInvoke a named model method — requires a non-empty fn_name.

Business methods that aren't CRUD go through call_method, and the method name has to be stated explicitly in fn_name:

{
  "id": "01J9Z8K3QJ7Y5T2N6V4W8X0DEF",
  "action": "call_method",
  "model_id": "account.move",
  "fn_name": "action_post",
  "params": [[142]],
  "keyword": {},
  "odoo_instance": {
    "url": "https://erp.example.com",
    "db": "prod",
    "user_id": 2,
    "api_key": "<the Odoo user's API key>"
  }
}

Omit fn_name and the request fails with -32002. The proxy will not infer a method name — an important property, because "guess what the caller meant" is not a behaviour you want on the path to your accounting records. More on the design in the Odoo API gateway guide, and the mechanics of calling custom Odoo methods with call_method.

3. A slow Odoo takes your application down with it

An Odoo instance mid-report, mid-import, or mid-cron can take a very long time to answer. A client with no timeout waits, holds its connection, and — under load — exhausts its own pool waiting on someone else's ERP. The failure propagates outward from the slowest query.

The proxy applies a timeout to the upstream Odoo call, defaulting to 15 seconds, overridable per request with an integer x-request-timeout header (missing, non-numeric, or 0 falls back to the default):

curl -X POST https://your-proxy.example.com/api/odoo/execute \
  -H "x-api-key: $ODX_PROXY_KEY" \
  -H "x-request-timeout: 60" \
  -H "Content-Type: application/json" \
  -d @bulk-export.json

Just as importantly, it tells you which failure you hit. "Odoo was too slow" and "Odoo was unreachable" arrive as different codes — -32003 with HTTP 504 and -32004 with HTTP 502 — so a client can retry the first and page someone about the second. Distinguishing those at the gateway is what stops every service from writing its own guesswork; the wider failure taxonomy is in fixing Odoo 502 and 504 gateway errors.

4. Every client reinvents the same fragile error handling

Odoo's error surface is genuinely awkward to consume. The most-hit tripwire: a logic error comes back as HTTP 200. Access denied, a validation constraint, a missing record — all of it arrives with a success status and an error object in the body. A client that checks response.ok and moves on treats an access denial as an empty result set and silently syncs nothing.

ODXProxy does not paper over this, because it can't without lying about what Odoo said. What it does is make the envelope uniform, so one handler covers every call. Every /api/* response is the same JSON-RPC 2.0 shape, with result and error mutually exclusive:

def call(session, payload):
    r = session.post(f"{PROXY_URL}/api/odoo/execute",
                     json=payload,
                     headers={"x-api-key": PROXY_KEY})

    # Step 1 — proxy-layer failure (auth, allowlist, timeout, unreachable Odoo)
    if r.status_code != 200:
        err = r.json().get("error", {})
        raise ProxyError(err.get("code"), err.get("message"))

    body = r.json()

    # Step 2 — a 200 can STILL carry an Odoo logic error
    if "error" in body and body["error"] is not None:
        err = body["error"]
        raise OdooLogicError(err.get("code"), err.get("message"), err.get("data"))

    return body["result"]

The proxy-layer codes are a closed set, which is what makes that first branch worth writing once:

HTTPcodeMeaning
401-32000Missing or wrong x-api-key
400-32001action not in the allowlist
400-32002call_method with no fn_name
504-32003Upstream Odoo call timed out
502-32004Could not reach the Odoo instance
500-32005Internal proxy error
4030Proxy license expired or invalid
The official SDKs implement both steps and map each code to a typed exception, so you write except OdooTimeoutError rather than comparing integers. See the Python SDK — or the Odoo API error handling guide if you're implementing the client yourself.

5. Staging, production, and one instance per client

Anyone running Odoo for more than one entity hits this: an agency with an instance per client, a group with a database per country, or just the ordinary staging/production pair. Point integration code at a different instance and you're editing per-environment configuration in every service that talks to Odoo — and every new tenant multiplies the work.

Because the target instance travels in the request body, one proxy deployment fronts all of them. The proxy URL and x-api-key never change; odoo_instance does. No upstream block per tenant, no redeploy to onboard one.

There's also a credential-free reachability check. POST /api/odoo/version asks a target Odoo for its public version banner — it needs the proxy key but no Odoo credentials at all:

curl -X POST https://your-proxy.example.com/api/odoo/version \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ODX_PROXY_KEY" \
  -d '{ "id": "ping-1", "url": "https://erp.example.com" }'

That separates "can the proxy reach this instance at all" from "are these Odoo credentials right", which are otherwise easy to confuse while onboarding a new tenant. Note that the db you name is a call parameter, not something inferred from the hostname — a distinction that matters on multi-database servers and is covered in Odoo dbfilter: serving multiple databases behind one domain.

6. No idea what your integrations are doing to the ERP

Odoo's own logs tell you a request arrived; they don't tell you that your fulfilment worker's calls started timing out an hour ago. With every integration calling Odoo directly, there is no single place where that question has an answer.

A proxy is that place. ODXProxy exposes Prometheus metrics at GET /_/metrics (no key required), including counters for successful upstream calls and failed ones labelled by JSON-RPC error code, plus standard request/response counters and histograms:

curl -s https://your-proxy.example.com/_/metrics | grep odoo_

Every response also carries an x-server header identifying the running build, and GET /_/about returns the same version and build id as a JSON-RPC envelope — enough to tell which build produced a response you're arguing about.

What ODXProxy does not solve

Being clear about the boundary is more useful than a longer feature list. As of v0.1.0:

  • It is not Odoo's permission system. Record rules, model ACLs, and multi-company rules still apply to every call, and they are what determines which records come back. A gateway allowlist limits which actions run; it does not widen or narrow what a given Odoo user may see. If your API results look short, the cause is usually Odoo-side — see why Odoo returns fewer records over the API.
  • It does not do rate limiting or quotas. Timeouts ship today; per-client rate limits and quotas are roadmap, not current behaviour. If you need to protect Odoo from a runaway client right now, that belongs in the layer in front.
  • It does not cache. Every call reaches Odoo. Repeated identical search_reads are repeated work, and deciding what is safe to cache is application knowledge the proxy doesn't have.
  • It does not push. Odoo does not notify you when a record changes, and a proxy in the request path can't invent that. Event-driven flows still need Odoo-side automation — the options are compared in Odoo webhooks and the API.
  • It does not replace your web reverse proxy. nginx or Traefik in front of the Odoo web client solves a different problem — TLS, forwarded headers, proxy_mode. Run both; they're orthogonal. See the Odoo proxy_mode configuration guide.
  • It does not authenticate your end users. The proxy authenticates services. Mapping a logged-in human to an Odoo user, and everything that comes with it — sessions, SSO, and the fact that API keys bypass Odoo's two-factor authentication — remains your application's job.
  • It will not make a slow Odoo fast. It bounds how long you wait and tells you clearly what happened. Query and worker tuning is still Odoo-side work.

Should you run one or build one?

You can build this. It is a service that compares a key, checks an action against a list, forwards execute_kw to the instance named in the body, and normalizes the response. The parts that are easy to get subtly wrong are the ones above: the two-step error check, per-request timeouts that distinguish 504 from 502, and never leaking to a caller which of the two keys was the wrong one.

ODXProxy is that gateway ready-made, and deliberately narrow: one authenticated entry point, nine allowlisted actions, per-request instance routing, bounded upstream calls, and metrics. If you want the exact request and response contract, it's in the API reference; if you'd rather not write the envelope by hand, start with the Python SDK.