Skip to content

Commit 4506f11

Browse files
committed
[IMP] developer/api/external_api: document programmatic API key management
Document the RPC methods introduced by odoo/odoo#246118 to generate and revoke API keys programmatically, and add recommendations about API key rotation best practices.
1 parent ed03991 commit 4506f11

1 file changed

Lines changed: 211 additions & 2 deletions

File tree

content/developer/reference/external_api.rst

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

142145
An 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>`_
172175
for 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

176385
Access Rights

0 commit comments

Comments
 (0)