API issues

Common failure patterns seen in dk_customer_portal_api, and how to recognize them from the response.

CORS preflight failures

Symptom: requests fail in the browser before your code even sees a response — dev tools show a failed OPTIONS request or a missing Access-Control-Allow-Origin header.

Common causes:

  • The calling origin isn’t in the controller’s allowed_origins list (see Authentication § CORS). Local dev origins outside localhost:3000/3001/8069 won’t get the header back.

  • A route’s methods list is missing "OPTIONS" — every route needs it explicitly, since Odoo won’t add it implicitly.

  • A response was built without going through the controller’s _json_response() helper, so the CORS headers were never attached.

Fix: add the origin to allowed_origins, add "OPTIONS" to the route’s methods, and make sure every return path uses _json_response() rather than constructing a response manually.

401 Unauthorized on routes that should be public

Symptom: a route that’s supposed to work pre-login (e.g. an invitation accept link) returns 401.

Cause: the route incorrectly calls _get_authenticated_user() and rejects when it returns False, instead of treating that route as token-free. Compare against /api/users/invitation/<token> and /api/users/invitation/accept in Users, which use the invitation’s own token as the credential instead.

401 on routes that should require a session

Symptom: request.env.user._is_public() is used to check authentication on an auth="none" route.

Cause: auth="none" routes never populate request.env.user from a session — there is no session. Use self._get_authenticated_user() (the shared bearer-token helper — see Authentication) instead.

400 on POST /api/subscription/select-plan / saas.configuration not saved

Symptom: the select-plan call returns 200, but the next call in the flow (subscribe or confirm) fails with a 400 because it can’t find a configuration for the tenant.

Cause: the saas.configuration record created by select_subscription_plan wasn’t actually persisted — check that the create()/write() call isn’t being rolled back by an exception earlier in the same request, and that the tenant lookup used to scope the record matches the tenant used later in subscribe/confirm.

500 with a generic message

Every controller wraps its body in try/except Exception as e and returns {"success": false, "message": str(e)} with status 500. The message is whatever Python’s exception str() produces — often not very descriptive on its own. Check the Odoo server log around the request timestamp; controllers log with _logger.exception(...) right before returning, which captures the full traceback even though the HTTP response doesn’t include it.