← Blog
odooapi-gatewayjson-rpcproxy

Odoo API Gateway: Why and How to Put One in Front

An Odoo API gateway centralizes auth, an action allowlist, timeouts, and multi-instance routing in front of Odoo's JSON-RPC. Here's what it does and how to build one.

ODXProxy Team · Jul 11, 2026 · 9 min read

Odoo API Gateway: Why and How to Put One in Front — ODXProxy blog cover

Once more than one app starts talking to Odoo, the same questions keep coming back: where do the credentials live, which methods is a client allowed to call, what happens when Odoo is slow, and how do you point the same integration code at staging and production? An Odoo API gateway answers all four in one place. It's a thin service that sits between your clients and Odoo's JSON-RPC endpoint, authenticates every request, restricts which model methods can run, enforces timeouts, and routes to the right Odoo instance — so your application code stops carrying that responsibility. This guide explains what an Odoo API gateway does, how it differs from a plain reverse proxy, and how ODXProxy implements one.

Reverse proxy vs. API gateway (they're not the same)

If you've already put Odoo behind an nginx reverse proxy, you have the transport layer solved: TLS termination, proxy_mode, forwarding the real client IP. A reverse proxy moves bytes; it doesn't understand that the body is an Odoo RPC call.

An API gateway works one layer up. It reads the request payload and makes decisions based on what the call is trying to do:

  • Authentication — reject anything without a valid gateway credential before it reaches Odoo.
  • Authorization / allowlisting — permit only a known set of actions, so a leaked key can't call arbitrary model methods.
  • Request shaping — enforce timeouts, set response compression, normalize the request/response envelope.
  • Routing — send the call to the correct Odoo instance, chosen per request rather than baked into one upstream block.

You want both. The reverse proxy handles the network; the gateway handles the API contract. ODXProxy is the gateway half — you can run it behind nginx or expose it directly.

Request flow through the Odoo API gateway: your app sends a JSON-RPC call with the proxy x-api-key, ODXProxy authenticates and checks the action allowlist, then forwards to Odoo with the Odoo user api_key

What an Odoo API gateway centralizes

Without a gateway, every client that talks to Odoo has to hold the Odoo database name, a user id, and that user's API key, and each one re-implements error handling and retries. That's a lot of copies of your most sensitive credentials and a lot of duplicated logic. A gateway pulls the cross-cutting concerns into one hop:

  • One authentication point. Clients present a single gateway key; the gateway holds (or is handed) the Odoo credentials. You rotate and audit in one place.
  • One protocol shape. Every call is the same JSON-RPC 2.0 envelope regardless of the Odoo model or method, so clients don't learn a new URL per resource.
  • One allowlist. The gateway decides which actions are legal. Everything else is refused up front.
  • One place for operational limits. Timeouts, and (on the roadmap) rate limiting and quotas, live at the gateway instead of scattered across clients.

The gateway request shape

ODXProxy exposes a single main entry point, POST /api/odoo/execute, and every data call goes through it. The gateway authenticates the caller with the x-api-key header, then forwards the call to the Odoo instance named inside the body:

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": "gw-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>"
    }
  }'

A few things the gateway is doing for you in that one call:

  • action must be one of nine allowlisted actions (more on that below). Anything else is rejected before it reaches Odoo.
  • params is a JSON array of positional arguments (default []); for search_read the first positional argument is the domain filter, which is why it's nested: [[["is_company", "=", true]]].
  • keyword is a JSON object of keyword arguments (default {}) — here fields and limit.
  • odoo_instance carries the target connection, chosen per request. That's what makes the same gateway serve many instances.

The two-key model the gateway enforces

The single most important thing to keep straight is that a gateway introduces two distinct secrets, and they are never interchangeable:

  • x-api-key — the gateway's key. It authenticates the caller to the gateway and travels as an HTTP header. It's the same regardless of which Odoo instance you target.
  • odoo_instance.api_key — the Odoo user's key. It authenticates the actual ERP call and is sent per request in the body.
Swapping these two keys is the most common cause of a mysterious 401. A 401 with code -32000 is a gateway-key problem; an Odoo AccessError returned with HTTP 200 is an Odoo user-key problem. See how to authenticate to the Odoo API for the full breakdown.

Allowlisting: the gateway's security core

The reason an API gateway is a security upgrade over exposing Odoo directly is the action allowlist. The proxy will only forward these nine actions, and rejects anything else with HTTP 400 and JSON-RPC code -32001:

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).

The first eight cover ordinary CRUD. When you need a business method that isn't one of them — posting an invoice, confirming a sale order — you use call_method and name the method in fn_name:

{
  "id": "gw-post-1",
  "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 API key>"
  }
}

If action is call_method but fn_name is missing or empty, the gateway refuses it with code -32002 — it will not guess a method name for you.

Routing to the right Odoo instance

Because the target instance is specified per request in odoo_instance, one gateway fronts many Odoo servers. The same client code targets staging or production by changing the url, db, user_id, and api_key — nothing about the gateway URL or your x-api-key changes. For an agency running one instance per client, that means a single gateway deployment serves every tenant, with no per-instance upstream configuration to maintain.

There's also a credential-free health check. POST /api/odoo/version asks the target Odoo for its public version banner — it needs the gateway key but no Odoo credentials — so you can confirm the gateway can reach an instance before you debug per-instance keys:

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" }'

Timeouts and the errors a gateway surfaces

A gateway is also where you handle Odoo being slow or unreachable without every client re-inventing it. The upstream call uses a default timeout (15 seconds), overridable per request with an integer x-request-timeout header. When something goes wrong, the gateway maps it to a clear code:

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 gateway error

The one rule that catches everyone: an HTTP 200 can still carry an error. Only gateway-layer failures use a non-200 status. When Odoo itself rejects a call — an access error, a validation constraint — that comes back with HTTP 200 and a populated error object, passed straight through:

{
  "jsonrpc": "2.0",
  "id": "gw-1",
  "error": {
    "code": 200,
    "message": "You are not allowed to access 'Contact' records.",
    "data": { "name": "odoo.exceptions.AccessError" }
  }
}
Always check the HTTP status first, then — even on a 200 — check whether the body has an error field before reading result. The Odoo API error handling guide has a reusable two-step handler.

Should you build one or run one?

You can build a minimal gateway yourself: a small service that checks a shared key, validates the action against an allowlist, forwards execute_kw to the right Odoo instance, and normalizes errors. The tricky parts are the ones that are easy to get subtly wrong — the two-step error check, per-request timeouts, multi-instance routing, and never leaking the difference between "wrong gateway key" and "wrong Odoo key" back to a caller.

ODXProxy is that gateway as a ready-made service. It's early (v0.1.0), so treat features beyond the shipped request path — the allowlist, per-request routing, timeouts, the version health check — as the current surface, and things like rate limiting and quotas as roadmap. What ships today is the core an Odoo API gateway is for: one authenticated entry point, a strict action allowlist, and per-request instance routing.

Where to go next