← Blog
odooapipostmanjson-rpctesting

Testing the Odoo API in Postman (JSON-RPC)

Test the Odoo API in Postman step by step: build the JSON-RPC request, store both keys as variables, run a credential-free smoke test, then read responses correctly.

ODXProxy Team · Jul 14, 2026 · 9 min read

Testing the Odoo API in Postman (JSON-RPC) — ODXProxy blog cover

Before you write a line of integration code, it pays to poke the Odoo API by hand — see the exact request that works, the exact shape that comes back, and where the sharp edges are. Postman is the fastest way to do that. This guide shows you how to test the Odoo API in Postman over JSON-RPC 2.0: building the request, storing your keys as environment variables instead of pasting them into every call, running a credential-free smoke test first, making a real search_read, and — the part most people get wrong — reading the response correctly, because an Odoo error can arrive with an HTTP 200.

Why Postman for the Odoo API

The Odoo External API isn't a REST service with tidy GET /partners routes; it's an RPC interface where every call is the same JSON envelope with a different model and method inside. That's actually ideal for Postman: you set up one request, then change the body to do anything. Reading partners, creating an invoice, and confirming a sale order are all the same request with different fields. Once you can make one call in Postman, you can make all of them.

This guide sends requests through ODXProxy, which exposes Odoo over a single JSON-RPC endpoint and a clean, uniform request shape. If you want the background on that shape versus Odoo's older XML-RPC transport, see the Odoo External API guide.

The one endpoint you'll hit

Every data call goes to a single route:

POST https://your-proxy.example.com/api/odoo/execute

You don't learn a URL per model. The model, the action, and the arguments all travel in the JSON body, and the proxy forwards the call to the Odoo instance named inside that body. In Postman, that means one saved request does the whole surface — you edit the body, not the URL.

Step 1 — store your secrets as Postman variables

The Odoo API involves two distinct secrets, and the single most common mistake is conflating them. Set them up as Postman environment variables so they never get pasted into a request (and never leak into a shared collection):

  • proxy_key — the proxy's x-api-key. It authenticates your client to the proxy and travels as an HTTP header. It's the same no matter which Odoo instance you target.
  • odoo_api_key — the Odoo user's API key. It authenticates the actual ERP call and is sent per request inside the body, under odoo_instance.

Create a Postman environment (e.g. "Odoo dev") with these variables:

VariableExample value
proxy_urlhttps://your-proxy.example.com
proxy_key<the proxy x-api-key>
odoo_urlhttps://erp.example.com
odoo_dbprod
odoo_uid2
odoo_api_key<the Odoo user API key>

Mark proxy_key and odoo_api_key as secret type so Postman masks them. Now you reference them in requests as {{proxy_key}}, {{odoo_url}}, and so on — change instances by switching environments, never by editing the request.

Swapping the two keys is the number-one cause of a mysterious 401. x-api-key is the proxy key; odoo_instance.api_key is the Odoo user key. They are never interchangeable — see how to authenticate to the Odoo API for the full breakdown.

Step 2 — a credential-free smoke test first

Before you involve Odoo credentials at all, confirm Postman can reach the proxy and the proxy can reach Odoo. There's an endpoint for exactly this: POST /api/odoo/version asks the target Odoo for its public version banner. It needs the proxy key but no Odoo credentials, so it isolates connectivity from authentication.

In Postman, create a POST request to {{proxy_url}}/api/odoo/version. Under Headers, add:

Content-Type: application/json
x-api-key: {{proxy_key}}

Under Body, select raw and JSON, and paste:

{ "id": "ping-1", "url": "{{odoo_url}}" }

Send it. A healthy setup returns HTTP 200 with Odoo's version info in result:

{
  "jsonrpc": "2.0",
  "id": "ping-1",
  "result": { "server_version": "17.0", "server_version_info": [17, 0, 0, "final", 0, ""] }
}

If this fails with 401, your proxy_key is wrong. If it fails with 502 (code -32004), the proxy can't reach odoo_url. Getting a green result here means the only thing left to prove is your Odoo user key — a much smaller problem to debug.

Step 3 — your first real call (search_read)

Now the real thing. Duplicate the request, point it at {{proxy_url}}/api/odoo/execute, keep the same two headers, and use this body to read a page of companies:

{
  "id": "read-1",
  "action": "search_read",
  "model_id": "res.partner",
  "params": [[["is_company", "=", true]]],
  "keyword": { "fields": ["name", "email", "country_id"], "limit": 10 },
  "odoo_instance": {
    "url": "{{odoo_url}}",
    "db": "{{odoo_db}}",
    "user_id": {{odoo_uid}},
    "api_key": "{{odoo_api_key}}"
  }
}

Three things to notice, because they trip up every first attempt:

  • params is a JSON array (positional args, default []). For search_read the first positional argument is the domain filter, which is why it's nested: [[["is_company", "=", true]]] — an array of args whose first element is the domain, itself a list of conditions.
  • keyword is a JSON object (keyword args, default {}) — here fields and limit.
  • user_id is an integer, so {{odoo_uid}} is not quoted in the body, while the string values are. Quoting the uid is a frequent cause of a rejected call.

Send it, and you get an array of records in result:

{
  "jsonrpc": "2.0",
  "id": "read-1",
  "result": [
    { "id": 14, "name": "Acme Pte. Ltd.", "email": "hello@acme.example", "country_id": [100, "Singapore"] }
  ]
}

The domain syntax in that filter — operators, AND/OR, dotted paths — is covered in Odoo domain filters explained.

Step 4 — read the response correctly (the 200 trap)

Here is the rule that catches everyone testing the Odoo API in Postman: an HTTP 200 does not mean success. Odoo's own logic errors — an access-right denial, a validation constraint — come back with HTTP 200 and a populated error object. Only proxy-layer failures use a non-200 status.

So in Postman, don't trust the green "200 OK" badge alone. Look at the body. A logic error looks like this, still under a 200:

{
  "jsonrpc": "2.0",
  "id": "read-1",
  "error": {
    "code": 200,
    "message": "You are not allowed to access 'Contact' records.",
    "data": { "name": "odoo.exceptions.AccessError" }
  }
}

The discipline is two steps: check the HTTP status; then, even on a 200, check whether the body has an error field before you read result. You can automate the check in Postman's Scripts → Post-response tab so a hidden error fails the request loudly instead of looking like a pass:

pm.test("no JSON-RPC error", function () {
  const body = pm.response.json();
  pm.expect(body.error, JSON.stringify(body.error)).to.be.undefined;
});

Non-200 statuses map to proxy-level problems, each with its own 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 proxy error

The full two-layer error model — and how to retry each kind — is in Odoo API error handling.

Step 5 — write data, and call a method

Testing writes in Postman follows the same envelope. create takes the new record's values as a positional arg and returns the new id in result:

{
  "id": "create-1",
  "action": "create",
  "model_id": "res.partner",
  "params": [{ "name": "Dana Lee", "email": "dana@example.com", "customer_rank": 1 }],
  "odoo_instance": {
    "url": "{{odoo_url}}",
    "db": "{{odoo_db}}",
    "user_id": {{odoo_uid}},
    "api_key": "{{odoo_api_key}}"
  }
}

Only nine actions are directly callable — search_count, search, read, fields_get, search_read, create, write, unlink, and call_method. Anything that isn't plain CRUD (posting an invoice, confirming an order) is a model method you reach through call_method with a non-empty fn_name:

{
  "id": "post-1",
  "action": "call_method",
  "model_id": "account.move",
  "fn_name": "action_post",
  "params": [[142]],
  "keyword": {},
  "odoo_instance": {
    "url": "{{odoo_url}}",
    "db": "{{odoo_db}}",
    "user_id": {{odoo_uid}},
    "api_key": "{{odoo_api_key}}"
  }
}

If you send call_method without fn_name, the proxy refuses it with code -32002 — it won't guess a method name. The mechanics of call_method are in Calling Odoo model methods with call_method.

Step 6 — not sure what fields exist? Ask Odoo

When you don't know a model's field names, fields_get returns the schema — perfect for exploring in Postman before you hardcode field lists:

{
  "id": "fields-1",
  "action": "fields_get",
  "model_id": "res.partner",
  "params": [],
  "keyword": { "attributes": ["string", "type", "required"] },
  "odoo_instance": {
    "url": "{{odoo_url}}",
    "db": "{{odoo_db}}",
    "user_id": {{odoo_uid}},
    "api_key": "{{odoo_api_key}}"
  }
}

The result is an object keyed by field name, each with its label, type, and whether it's required — the fastest way to learn a model's shape interactively.

Save it as a collection

Once these requests work, save them into a Postman collection — "version smoke test", "search_read", "create", "call_method", "fields_get". With the keys in an environment and the bodies parameterized, you have a reusable Odoo API workbench: switch environments to hit staging or another client's instance, and every request just works against the new target. That collection also becomes living documentation for teammates who'd otherwise start from scratch.

What about hitting Odoo directly?

You can point Postman straight at Odoo's own JSON-RPC endpoint (/jsonrpc) or its XML-RPC endpoints, without a proxy. It works, but you'll manage the Odoo database name, uid, and user key in every request, handle Odoo's raw envelope yourself, and get no action allowlist. The side-by-side of the same search_read over both transports is in Odoo search_read over XML-RPC vs JSON-RPC. Going through the gateway is what gives you the single uniform envelope this guide used throughout — see why put an API gateway in front of Odoo.

Where to go next