@@ -136,8 +136,11 @@ Configuration
136136
137137.. _reference/external_api/api_key :
138138
139- API Key
140- -------
139+ API Keys
140+ --------
141+
142+ Manual Key Generation
143+ ~~~~~~~~~~~~~~~~~~~~~
141144
142145An API key must be set in the ``Authorization `` request header, as a bearer token.
143146
@@ -171,6 +174,212 @@ Please refer to `OWASP's Secrets Management Cheat Sheet
171174<https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html#secrets-management-cheat-sheet> `_
172175for further guidance on the management of API keys.
173176
177+ .. _reference/external_api/api_key_management :
178+
179+ Programmatic Key Management
180+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
181+
182+ Odoo provides RPC methods to generate and revoke API keys programmatically.
183+
184+ Prerequisites
185+ ^^^^^^^^^^^^^
186+
187+ By default, programmatic API key management is restricted to users with the *Settings *
188+ administration access right.
189+
190+ To allow other users to manage API keys programmatically, navigate to
191+ :menuselection: `Settings --> Technical --> System Parameters ` and set the parameter
192+ ``base.enable_programmatic_api_keys `` to ``True ``.
193+
194+ Key Generation
195+ ^^^^^^^^^^^^^^
196+
197+ API keys can be generated with :meth: `res.users.apikeys.generate `.
198+
199+ The method accepts the following parameters:
200+
201+ - ``key `` (string): an existing valid API key
202+ - ``scope `` (string or ``null ``): the scope assigned to the new key
203+ - ``name `` (string): a human-readable label
204+ - ``expiration_date `` (string): the expiration date formatted as ISO 8601
205+ (e.g. ``"2026-05-19" ``)
206+
207+ A scope restricts where an API key can be used.
208+
209+ The ``rpc `` scope is the generic scope used for RPC access through controllers with ``auth='bearer' ``.
210+
211+ The new key should generally use the same scope as the existing key. However, it is possible to create
212+ a scoped key from an unscoped key when needed. Unscoped keys use ``null `` as their scope value
213+ (``None `` in Python).
214+
215+ The expiration date is validated against the maximum API key duration allowed by the roles assigned
216+ to the user.
217+
218+ Use shorter expiration periods for high-privilege or externally exposed integrations, and longer
219+ periods only for tightly controlled internal systems.
220+
221+ .. example ::
222+
223+ .. tabs ::
224+
225+ .. code-tab :: python
226+
227+ import requests
228+
229+ API_KEY = ... # get it from a secure location
230+
231+ res_apikey = requests.post(
232+ f"https://mycompany.example.com/json/2/res.users.apikeys/generate",
233+ headers={"Authorization": f"bearer {API_KEY}"},
234+ json={
235+ "key": API_KEY,
236+ "scope": None,
237+ "name": "Some service",
238+ "expiration_date": "2026-05-19",
239+ },
240+ )
241+ res_apikey.raise_for_status()
242+ new_apikey = res_apikey.json()
243+
244+ # store the new key securely
245+
246+ .. code-tab :: javascript
247+
248+ (async () => {
249+ const API_KEY = ; // get it from a secure location
250+ const headers = {
251+ "Content-Type": "application/json",
252+ "Authorization": "bearer " + API_KEY,
253+ }
254+
255+ const reqSearch = {
256+ method: "POST",
257+ headers: headers,
258+ body: JSON.stringify({
259+ "key": API_KEY,
260+ "scope": null,
261+ "name": "Some service",
262+ "expiration_date": "2026-05-19",
263+ }),
264+ };
265+ const resSearch = await fetch("https://mycompany.example.com/json/2/res.users.apikeys/generate", reqSearch);
266+ if (!resSearch.ok) throw new Error(resSearch.json());
267+ const new_apikey = await resSearch.json();
268+
269+ // store the new key securely
270+ })();
271+
272+ .. code-tab :: bash
273+
274+ set -eu
275+
276+ API_KEY=
277+
278+ curl https://mycompany.example.com/json/2/res.users.apikeys/generate \
279+ -X POST \
280+ --oauth2-bearer $API_KEY \
281+ -H "Content-Type: application/json" \
282+ -d '{
283+ "key": "'"$API_KEY"'",
284+ "scope": null,
285+ "name": "Some service",
286+ "expiration_date": "2026-05-19"
287+ }' \
288+ --silent \
289+ --fail
290+
291+ # store the new key securely
292+
293+ The method returns the newly generated API key as a string that cannot be retrieved otherwise.
294+
295+ By default, a user can generate up to 10 API keys programmatically. This limit can be configured with
296+ the system parameter ``base.programmatic_api_keys_limit ``.
297+
298+ Attempting to exceed the configured limit fails with an HTTP ``422 Unprocessable Content `` status
299+ code.
300+
301+ Key Revocation
302+ ^^^^^^^^^^^^^^
303+
304+ API keys can be revoked with :meth: `res.users.apikeys.revoke `.
305+
306+ The method accepts the following parameter:
307+
308+ - ``key `` (string): the API key to revoke
309+
310+ .. example ::
311+
312+ .. tabs ::
313+
314+ .. code-tab :: python
315+
316+ import requests
317+
318+ API_KEY = ... # get it from a secure location
319+
320+ res_apikey = requests.post(
321+ f"https://mycompany.example.com/json/2/res.users.apikeys/revoke",
322+ headers={"Authorization": f"bearer {API_KEY}"},
323+ json={
324+ "key": API_KEY,
325+ },
326+ )
327+ res_apikey.raise_for_status()
328+
329+ .. code-tab :: javascript
330+
331+ (async () => {
332+ const API_KEY = ; // get it from a secure location
333+ const headers = {
334+ "Content-Type": "application/json",
335+ "Authorization": "bearer " + API_KEY,
336+ }
337+
338+ const reqSearch = {
339+ method: "POST",
340+ headers: headers,
341+ body: JSON.stringify({
342+ "key": API_KEY,
343+ }),
344+ };
345+ const resSearch = await fetch("https://mycompany.example.com/json/2/res.users.apikeys/revoke", reqSearch);
346+ if (!resSearch.ok) throw new Error(resSearch.json());
347+ })();
348+
349+ .. code-tab :: bash
350+
351+ set -eu
352+
353+ API_KEY=
354+
355+ curl https://mycompany.example.com/json/2/res.users.apikeys/revoke \
356+ -X POST \
357+ --oauth2-bearer $API_KEY \
358+ -H "Content-Type: application/json" \
359+ -d "{\" key\" : \" $API_KEY\" }" \
360+ --silent \
361+ --fail
362+
363+ If the key is valid, it is revoked immediately and becomes unusable for subsequent requests.
364+ Otherwise, the request fails with an HTTP ``403 Forbidden `` status code.
365+
366+ Note that the key being revoked and the one given in the Authorization header don't need to match. A
367+ good practice during rotation is to authenticate with the new key to revoke the old key.
368+
369+ Key Rotation Best Practices
370+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
371+
372+ Rotating API keys regularly reduces the risk of unauthorized access if a key is compromised.
373+
374+ When rotating API keys:
375+
376+ #. Generate a new key before revoking the previous one.
377+ #. Store the new key securely before deployment.
378+ #. Verify that all services are using the new key.
379+ #. Revoke the previous key only after the transition is complete.
380+ #. Use expiration dates appropriate for the security requirements of the system.
381+ #. Prefer dedicated API keys per service or integration to simplify auditing and revocation.
382+
174383.. _reference/external_api/access_rights :
175384
176385Access Rights
0 commit comments