Odoo’s External API looks, at first glance, like it should work the way a typical web API does: log in once, get a token, send the token on later requests. It doesn’t — and building an integration around that assumption is the most common way people over-engineer (or under-secure) a script talking to Odoo from outside.
The short version: authenticate() doesn't create a session at all — it's a stateless credential check that returns a plain user id (uid), and every single subsequent call resends your full password or API key again, every time, alongside that uid. There's no token, no session cookie, and nothing to refresh — which is simpler than it looks, but only safe at all over HTTPS.
Two endpoints, and what each one is for
Odoo’s External API exposes two unauthenticated meta-endpoints and one authenticated one, whether you call them via XML-RPC or the JSON-RPC equivalent:
common.authenticate(db, login, password, {})— validates credentials and returns auid(an integer) on success, orfalseon failure. It requires no prior authentication — that’s the point.object.execute_kw(db, uid, password, model, method, args, kwargs)— every actual data operation (search_read,create,write,fields_get, anything) goes through this one call shape, always passing the model and method names as plain strings.
The part that surprises people: there’s no session
authenticate() returning a uid looks like a login step, and it’s tempting to treat that uid the way you’d treat a session token or a JWT — get it once, then use it to avoid resending credentials. That’s not what happens: every execute_kw call requires the full password (or API key) again, in plain alongside the uid. The uid on its own authenticates nothing; it’s the plain identifier of which user’s permissions apply to that call, not a secret.
# Every single call — not just the first one — needs the real credential.
uid = common.authenticate(db, login, password, {})
models.execute_kw(db, uid, password, 'res.partner', 'search_read', [[]], {'fields': ['name']})
models.execute_kw(db, uid, password, 'sale.order', 'search_read', [[]], {'fields': ['name']})
# ^ 'password' here is the real credential again, not a token derived from authenticate()
This is precisely why the External API is stateless in the way it is: there’s no server-side session to expire, no token refresh flow to implement, nothing to invalidate on logout — but it also means the real credential is on the wire for every call, which is exactly why Odoo’s own documentation is explicit that this only belongs over HTTPS, never plain HTTP.
API keys exist so that credential isn’t your login password
Odoo’s own account settings let a user generate an API key instead of using their actual login password for this purpose. Functionally, an API key is a password as far as execute_kw is concerned — same call shape, same full-resend-every-time behavior — with two differences that matter operationally: it can’t be used to log in through the web UI at all, and if a specific integration’s key is compromised or simply retired, it can be deleted individually without touching the user’s actual login password or breaking anything else using a different key.
Watch out: a lost or leaked API key can't be recovered or rotated in place — it can only be deleted and a new one generated. Any integration storing a key should treat regenerating it as a routine, low-friction operation (a config value to swap), not an emergency procedure to design for after the fact.
Why this matters for anything calling Odoo from outside
Nothing about execute_kw’s call shape changes based on how many times you’ve called it before — there’s no rate-limit-friendly session to hold open, and no benefit to trying to cache or reuse anything beyond the uid integer itself (which never expires or rotates on its own, since it’s just the user’s database id). Every call is independently authenticated and independently authorized — the same access rights and record rules from the three-layers article apply to execute_kw exactly as they would to that same user clicking around the web UI, since it’s still the real ORM underneath, not a separate API-specific permission model.
Quick reference
| Question | Answer |
|---|---|
| Does authenticate() start a session? | No — it’s a stateless credential check returning a plain user id |
| Do I need to resend the password on every call? | Yes, every single execute_kw call, in full |
| Is an API key different in kind from a password? | No — functionally equivalent, just can’t log in via the UI and can be revoked independently |
| Is this safe over plain HTTP? | No — the real credential is on the wire every call; HTTPS only |
| Do access rights/record rules still apply? | Yes — identical to what that same user gets through the web UI |
Frequently asked questions
Can I skip authenticate() and just call execute_kw directly?
You still need the uid that authenticate() returns as one of execute_kw's parameters, so no — but since it's a plain credential check with no session created, there's nothing wrong with calling authenticate() once at the start of a script and reusing the returned integer for the rest of that script's run.
Does the uid ever expire or need refreshing?
No — it's simply that user's database id, not a token with a lifetime. It stays valid for as long as the user account itself exists and the credential you pass alongside it on each call remains correct.
What actually happens if the password is wrong on an execute_kw call?
The call fails with an authentication error at that point — execute_kw independently verifies the credential on every single call, it's not that authenticate() unlocked anything upstream of it.
Is JSON-RPC meaningfully different in security terms from XML-RPC here?
No — both are transport/encoding choices for the exact same underlying call shape and the exact same stateless, resend-every-time credential model. Neither is more or less secure than the other on its own; HTTPS is what actually matters for both.
Should a server-side integration store the plain password/API key to make these calls repeatedly?
It has to store it somewhere to keep making calls, since there's no token to hold instead — which is exactly why treating that stored credential with the same care as any other production secret (not committed to a repo, not logged, rotatable without downtime) matters as much for an Odoo integration as for any other system.
Further reading
- External API — official Odoo 18.0 developer documentation, the primary source for the authentication model and API key behavior described above.
And on this site: Why Can’t This User See a Record They Should? for what actually governs what an authenticated execute_kw call can see and do — identical to the web UI, not a separate API permission layer. The Report Builder’s Connect panel is a live, working example of this exact authentication flow, proxied server-to-server since browsers can’t call it directly (CORS).