006 — API and Controller Architecture¶
Controller Inheritance¶
classDiagram
class OdooHttpController {
<<Odoo framework>>
}
class LpBaseController {
<<lp_base>>
api_response()
handle_api_error()
_set_cors_headers()
handle_options()
lp_base_request
logger
user_model
}
class EcommerceBaseController {
<<ecommerce>>
api_response() overrides messages
handle_api_error()
lp_ecommerce_request
brand_model / banner_model / sale_order_model / ...
}
class AccessMgmtBaseController {
<<access_management>>
}
class ClientPortalControllers {
14 route files
}
class AdminPortalControllers {
15 route files
}
OdooHttpController <|-- LpBaseController
LpBaseController <|-- EcommerceBaseController
LpBaseController <|-- AccessMgmtBaseController
EcommerceBaseController <|-- ClientPortalControllers
EcommerceBaseController <|-- AdminPortalControllers
lp_base owns the envelope, CORS and authentication. The ecommerce base controller adds
only two things: its own error-message dictionary and lazy sudo() model accessors so route
methods never touch request.env directly.
Naming caution. Odoo registers routes by method name across the whole controller inheritance chain. Two sibling controllers defining the same method name silently drop one route — the symptom looks like a CORS failure but is really a 404. Client and admin counterparts are therefore named distinctly (
list_bannersvslist_banners_admin).
Request Handling Contract¶
Every route follows the same shape:
flowchart LR
R["@http.route<br/>auth='public'<br/>csrf=False<br/>methods incl. OPTIONS"] --> D["@http_auth_validated<br/>or @http_public_cors"]
D --> P["parse params / JSON body"]
P --> M["one model call<br/>api_*() or *_payload()"]
M --> E["api_response(**result)"]
E --> X["exception → handle_api_error()"]
| Element | Why |
|---|---|
auth='public' |
The decorator, not Odoo, decides authentication — so failures return the JSON envelope instead of an HTML login redirect |
csrf=False |
Cross-origin JSON API; protection is the session cookie plus CORS |
OPTIONS in methods |
Browser preflight is answered by the decorator with 204 + CORS headers |
Response envelope¶
| Field | Meaning |
|---|---|
response_code |
"0" = success. "100" = generic failure. "401" = session expired (the only case that also returns HTTP 401). 5xxx/6xxx = business errors. |
response_message |
English message, resolved from a central message dictionary or overridden per call |
Business failures deliberately return HTTP 200. Clients must branch on response_code,
never on the HTTP status.
A few legacy endpoints still emit an extra localised message key alongside
response_message. The portals ignore it — English is the only rendered language, and new endpoints should not add it.
Error code families¶
| Range | Family |
|---|---|
| 5101–5134 | User / authentication |
| 5201–5212 | Orders and order lines |
| 5401–5407 | Products, product attributes, product variants |
| 5501–5509 | Attachments and chatter |
| 5601–5612 | Product requests |
| 5701–5703 | Portal user tags |
| 6xxx | Additional admin-side families (banners, brands, customers, pricelists, chat) |
All codes and their messages are centralised in one module-level file, so a code never has two meanings.
Endpoint Catalog¶
Route prefixes in use:
| Prefix | Consumer |
|---|---|
/ecommerce/api/* |
Customer Portal |
/ecommerce/api/admin/* |
AMP — most admin resources |
/api/admin/* |
AMP — customers, pricelists, pricelist items, currencies |
/ecommerce/admin/api/chat/* |
AMP — chat |
/amm/api/* |
Both portals — permission lookup |
Three admin prefixes coexist for historical reasons. New admin endpoints should use
/ecommerce/api/admin/*.
Customer Portal API¶
| Area | Endpoints | Purpose |
|---|---|---|
| Authentication | authenticate, logout |
Session login by login or National ID / Iqama; blocks users who are not activated on both flags. Password is the only sign-in method |
| Password reset | otp/send, otp/verify, change-password |
Three-step forgotten-password flow: e-mail a one-time code, verify it for a secret token, set the new password. Verification does not create a session |
| Password change | reset-password |
Lets an already authenticated user change their own password |
| Catalog | brands, products, product, public_categories |
Paginated, filterable catalog with pricelist-resolved prices, product variants and product attribute exclusions |
| Marketing | banners |
Active, schedule-aware banners for the user's company |
| Orders | orders, orders/create, orders/drafts, orders/details, orders/update, orders/delete |
Basket/RFQ/quotation/order lifecycle; create supports copying an existing order |
| Order lines | order_lines/create, update, delete |
Line editing including customer target price |
| Product requests | product_requests (+ create, update, delete, details) |
Requests for items not in the catalog |
| Chatter | chatter/messages, message/post, message/update, attachment/upload, attachment/delete |
Per-order comment thread with attachments |
| Chat | chat/init, chat/send, chat/mark-read |
Live conversation with the account manager |
| Presence | presence/ping, presence/offline |
HTTP heartbeat backing online status |
| Notifications | notifications, mark-seen, seen-all |
In-app activity feed |
| Portal users | portal/users, portal/user, activate, deactivate, delete |
Company admin manages their own users |
| User tags | portal/user/tags (+ create, update, delete) |
Per-company tagging |
| Media | /web/content, image proxy route |
Serves product and attachment media with CORS |
| Diagnostics | build-info |
Deployed build identification |
Account Management Portal API¶
| Area | Endpoints | Purpose |
|---|---|---|
| Products | products (GET/POST/PUT/DELETE), product, products/import |
Full product CRUD with media, plus JSON bulk import |
| Product attributes & product variants | attributes, attribute/values, product/variants (GET/POST/DELETE), product/variant (PUT), link-attribute, unlink-attribute, remove-attribute-value, generate-product-variants |
Product variant matrix management |
| Product metadata | ribbons, alert-messages |
Lookup lists for the product form |
| Product categories | product-categories (list / create / update / delete) |
Public category tree |
| Brands | brands, details, create, update, delete |
Brand CRUD with duplicate-name protection |
| Banners | banners, details, create, update, delete |
Banner CRUD with scheduling and targeting |
| Orders | orders, orders/details, orders/update |
Server-side filtered order queues; state transitions |
| Order lines | order_lines/create, update, delete |
Account manager pricing of lines |
| Customers | /api/admin/clients (+ get, create, update, delete, restore) |
Customer company CRUD with archive/restore |
| Customer segments | client-categories (+ details, create, update, delete) |
Category (grade) CRUD |
| Pricelists | /api/admin/pricelists (+ details, create, update, delete), currencies |
Pricelist CRUD |
| Pricelist items | /api/admin/pricelists/items (+ details, create, update, delete) |
Price rule CRUD |
| Product requests | product_requests, product_requests/update |
Triage of catalog requests |
| Chat | chat/threads, chat/init, chat/send, chat/mark_read |
Multi-customer inbox |
| Users | users/send-invitation |
Sends the QR invitation e-mail |
| Address lookups | countries, states |
Form dropdowns |
| Attachments | attachments/list |
Documents on a record |
Access Management API¶
| Endpoint | Purpose |
|---|---|
GET /amm/api/permissions?app=&user= |
Returns roles, permission codes and the visible screen tree for one user in one portal |
Conventions Worth Knowing Before Adding an Endpoint¶
- Thin controller. The route body parses input, makes one model call, returns
api_response(**result). Business logic in a controller is a code-review failure. - Model methods return plain dicts containing
response_codeplus payload keys — never HTTP responses. - Model access is
sudo(); the tenancy filter is applied explicitly in the domain, not by record rules. - Serialization lives in
_prepare_*_dict()helpers on the model, so the same shape is reused by list, detail and update responses. - New error codes go in the central error file, never inline as literals.
- Method names must be unique across the whole controller inheritance chain.
- Pagination is
page+limit, and the response returns a pagination block.