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

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/executeYou 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'sx-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, underodoo_instance.
Create a Postman environment (e.g. "Odoo dev") with these variables:
| Variable | Example value |
|---|---|
proxy_url | https://your-proxy.example.com |
proxy_key | <the proxy x-api-key> |
odoo_url | https://erp.example.com |
odoo_db | prod |
odoo_uid | 2 |
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.
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:
paramsis a JSON array (positional args, default[]). Forsearch_readthe 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.keywordis a JSON object (keyword args, default{}) — herefieldsandlimit.user_idis 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:
| HTTP | code | Meaning |
|---|---|---|
| 401 | -32000 | Missing or wrong x-api-key |
| 400 | -32001 | action not in the allowlist |
| 400 | -32002 | call_method with no fn_name |
| 504 | -32003 | Upstream Odoo call timed out |
| 502 | -32004 | Could not reach the Odoo instance |
| 500 | -32005 | Internal 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
- Turn your working Postman calls into code with the Python SDK or the JavaScript SDK, and read the full contract in the API reference.
- Go deeper on the reads and writes you tested: Odoo search_read, in depth and create, write, unlink.
- Make your error handling bulletproof with Odoo API error handling.