Odoo Two-Factor Authentication and the API: What Breaks
Odoo two-factor authentication protects the web login but blocks password-based API access. Here's why API keys are the fix and how to keep integrations working with 2FA on.
ODXProxy Team · Jul 11, 2026 · 7 min read

Turning on Odoo two-factor authentication is one of the first things a security review asks for — and one of the first things that quietly breaks an integration. The moment 2FA is enabled on an account, any script that logs in with a username and password stops working, because the login now demands a rotating code no automated caller can supply. The good news is that this isn't a conflict you have to resolve with a workaround: Odoo's API keys are designed for exactly this situation. This guide explains what 2FA does to API access, why API keys are the correct fix, and how to keep your integrations running with two-factor authentication switched on.
What two-factor authentication changes about login
Odoo's two-factor authentication (Settings → Users, "Two-factor authentication" on a user, or enforced account-wide) adds a time-based one-time password (TOTP) step to the web session login. After the password check, Odoo asks for a six-digit code from an authenticator app before it issues a session cookie.
That's great for humans and fatal for scripts. Any integration that authenticates by POSTing a
username and password — the classic common/authenticate or web-session login flow — now hits the
TOTP challenge and can't proceed. There's no password you can add to the request to get past it,
because the whole point of the second factor is that a stolen password isn't enough.
Why API keys are the answer, not a workaround
The instinct is to look for a way to bypass 2FA for the integration account. Don't — that reopens exactly the hole 2FA closed. Odoo's intended path is API keys, and they coexist with 2FA by design.
An Odoo API key (Odoo 14 and later) is a per-user credential that replaces the password for API calls only. It's tied to the user and inherits that user's permissions, but it is not subject to the TOTP challenge — a key holder has already proven possession of a secret that only lives on the server. So the model is clean:
- Humans log in through the web with password + their second factor.
- Machines authenticate API calls with an API key and never touch the TOTP flow.
Enabling 2FA and using API keys aren't in tension. In fact, enabling 2FA on human accounts and moving every integration to a dedicated key'd service account is the configuration you want.
Generating an API key that survives 2FA
Create the key on the user the integration will act as:
- In the Odoo web client, open your avatar → My Profile → Account Security.
- Under API Keys, click New API Key, name it (e.g.
nightly-sync), and copy the value.
You only see the key once, so store it in your secrets manager immediately. You'll also need the
user's numeric user id (uid): with developer mode on, open the user record and read the id from
the URL (.../web#id=2&model=res.users).
Making an authenticated call with 2FA enabled
With an API key in hand, the call is identical whether or not the account has 2FA turned on — the key
carries the identity. Through ODXProxy, the request goes to POST /api/odoo/execute, authenticated at
the gateway with the x-api-key header, and at Odoo with the user's API key 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": "auth-2fa-1",
"action": "search_read",
"model_id": "res.users",
"params": [[["id", "=", 2]]],
"keyword": { "fields": ["name", "login"], "limit": 1 },
"odoo_instance": {
"url": "https://erp.example.com",
"db": "prod",
"user_id": 2,
"api_key": "<the Odoo user API key>"
}
}'Note the two different secrets in that request, which you must never conflate:
x-api-key— the proxy's key, an HTTP header that authenticates you to the gateway.odoo_instance.api_key— the Odoo user's API key, sent in the body to authenticate the ERP call. This is the credential that replaces the password-plus-TOTP login.
On success you get HTTP 200 and a JSON-RPC envelope with the data in result:
{
"jsonrpc": "2.0",
"id": "auth-2fa-1",
"result": [
{ "id": 2, "name": "Integration Bot", "login": "svc-integration" }
]
}Telling 2FA-related failures apart from other auth errors
When a call won't authenticate, the failure layer tells you where to look. Odoo speaks JSON-RPC 2.0
(not REST), and a request that "fails" often still returns HTTP 200, so you have to check in order:
- HTTP
401, code-32000— the proxyx-api-keyis missing or wrong. Nothing to do with Odoo or 2FA; fix the gateway header. - HTTP
200with anerrorobject — Odoo itself rejected the call. An invalid or revoked API key, or a user without access, shows up here:
{
"jsonrpc": "2.0",
"id": "auth-2fa-1",
"error": {
"code": 200,
"message": "Access Denied",
"data": { "name": "odoo.exceptions.AccessDenied" }
}
}An AccessDenied on a key'd call usually means the key is wrong, revoked, or belongs to a different
user than the user_id you sent — not that 2FA is blocking you. API keys don't trigger the TOTP
challenge at all, so if you're using a key correctly, 2FA is simply out of the picture. If you're
still seeing a password/OTP-style rejection, you're almost certainly falling back to a
password-login code path somewhere; switch that caller to the key.
error.code of 200 here is Odoo's own server-side error code, not the HTTP status. Always check the HTTP status first, then check the body's error field even on a 200. See Odoo API error handling for the full two-layer model.Doing it properly in code
Putting the identity and the two-step check together, a robust 2FA-safe call in Python is just an ordinary key'd request:
import requests
PROXY_URL = "https://your-proxy.example.com/api/odoo/execute"
PROXY_API_KEY = "<the proxy x-api-key>"
payload = {
"id": "auth-2fa-1",
"action": "search_read",
"model_id": "res.partner",
"params": [[["is_company", "=", True]]],
"keyword": {"fields": ["name", "email"], "limit": 5},
"odoo_instance": {
"url": "https://erp.example.com",
"db": "prod",
"user_id": 2,
"api_key": "<the Odoo user API key>", # replaces password+TOTP
},
}
resp = requests.post(PROXY_URL, headers={"x-api-key": PROXY_API_KEY}, json=payload, timeout=20)
resp.raise_for_status() # step 1: proxy-level failures are non-200
body = resp.json()
if body.get("error"): # step 2: a 200 can still carry an Odoo error
err = body["error"]
raise RuntimeError(f"Odoo error {err['code']}: {err['message']}")
print(body["result"])Nothing in that code is 2FA-specific — which is the point. Once the account uses an API key, the second factor stays where it belongs (human web logins) and never touches your integration.
A checklist for 2FA + API access
- Enable 2FA on human accounts, especially admins.
- Give every integration its own service user with least-privilege access — never share a human's account or key.
- Authenticate integrations with API keys, not usernames and passwords.
- Store keys in a secrets manager; they're shown once and inherit the owner's full permissions.
- Rotate keys on a schedule and immediately if one leaks — deleting a key in Odoo revokes it at once.
- Keep the two keys straight — the proxy
x-api-keyand the Odoo user'sapi_keyare different secrets.
Where to go next
- Start from the basics in how to authenticate to the Odoo API.
- Verify connectivity before credentials with the Odoo API connection quickstart.
- Read the full request/response contract in the API reference, or let the Python SDK wrap the envelope and error checks for you.