# 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](../developer/authentication.md)). 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/` and `/api/users/invitation/accept` in [Users](../developer/api/users.md), 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](../developer/authentication.md)) 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.