Repository navigation
[API] User #39058
Copy link
Copy link
Open
Description
Functional Area: User
Endpoints under this functional area:
| Module | API Version | Resource / Endpoint | HTTP Method | Description |
|---|---|---|---|---|
| Account Management | v1 | /user/v1/account/password_reset/ |
GET | Retrieves the password reset form description |
| Account Management | v1 | /user/v1/account/password_reset/token/validate/ |
POST | Validates a password reset token |
| Account Management | v1 | /user/v1/account/registration/ |
GET | Retrieves user registration form description |
| Account Management | v1 | /user/v1/account/registration/ |
POST | Creates a new user account |
| Account Management | v1 | /user/v1/accounts |
GET | Returns a list of user detail objects; supports filtering by usernames, emails, or LMS IDs |
| Account Management | v1 | /user/v1/accounts/cancel_retirement/ |
POST | Cancels the retirement of a user's account |
| Account Management | v1 | /user/v1/accounts/deactivate_logout/ |
POST | Marks user as having no password and logs them out |
| Account Management | v1 | /user/v1/accounts/name_change/ |
POST | Creates a PendingNameChange request for profile name update |
| Account Management | v1 | /user/v1/accounts/name_change/{username}/confirm/ |
POST | Confirms a pending name change request and updates user's profile name |
| Account Management | v1 | /user/v1/accounts/replace_usernames/ |
POST | Replaces current usernames with desired usernames; returns mapping of successful and failed replacements |
| Account Management | v1 | /user/v1/accounts/retire/ |
POST | Retires a user account including username, email, and other PII |
| Account Management | v1 | /user/v1/accounts/retire_misc/ |
POST | Retires a user in the LMS |
| Account Management | v1 | /user/v1/accounts/retirement_cleanup/ |
POST | Deletes a batch of retirement requests by username |
| Account Management | v1 | /user/v1/accounts/retirement_partner_report/ |
POST | Returns list of UserRetirementPartnerReportingStatus users and updates their processing status |
| Account Management | v1 | /user/v1/accounts/retirement_partner_report/ |
PUT | Creates a UserRetirementPartnerReportingStatus object for a user in the retirement pipeline |
| Account Management | v1 | /user/v1/accounts/retirement_partner_report_cleanup/ |
POST | Deletes UserRetirementPartnerReportingStatus objects for reported users |
| Account Management | v1 | /user/v1/accounts/retirement_queue/ |
GET | Returns RetirementStatus users in specified states and cool-off period |
| Account Management | v1 | /user/v1/accounts/retirements_by_status_and_date/ |
GET | Lists UserRetirementStatusSerializer rows filtered by status and date range |
| Account Management | v1 | /user/v1/accounts/search_emails |
POST | Returns user information for a list of emails; staff-only access |
| Account Management | v1 | /user/v1/accounts/update_retirement_status/ |
PATCH | Updates the RetirementStatus for a user and appends messages to the log |
| Account Management | v1 | /user/v1/accounts/{username} |
GET | Retrieves a single detailed user object |
| Account Management | v1 | /user/v1/accounts/{username} |
PATCH | Updates user account/profile info (merge-patch supported) |
| Account Management | v1 | /user/v1/accounts/{username}/deactivate/ |
POST | Marks user as having no password for deactivation purposes |
| Account Management | v1 | /user/v1/accounts/{username}/image |
POST | Upload/update user profile image |
| Account Management | v1 | /user/v1/accounts/{username}/image |
DELETE | Deletes a user's profile image |
| Account Management | v1 | /user/v1/accounts/{username}/retirement_status/ |
GET | Returns the RetirementStatus of a user |
| Account Management | v1 | /user/v1/accounts/{username}/verification_status/ |
GET | Returns ID verification status for a user |
| Account Management | v1 | /user/v1/accounts/{username}/verifications/ |
GET | Retrieves detailed ID verification information for a user |
| Account Management | v1 | /user/v1/me |
GET | Returns the authenticated user's username |
| Forum Management | v1 | /user/v1/forum_roles/{name}/users/ |
GET | Lists users for a given forum role |
| Preferences Management | v1 | /user/v1/preferences/email_opt_in/ |
POST | Update or create user's email opt-in preference |
| Preferences Management | v1 | /user/v1/preferences/time_zones/ |
GET | List all time zones or by country |
| Preferences Management | v1 | /user/v1/preferences/{pref_key}/users/ |
GET | List a user's preferences |
| Preferences Management | v1 | /user/v1/preferences/{username} |
GET | Retrieve all preferences for a user |
| Preferences Management | v1 | /user/v1/preferences/{username} |
PATCH | Update user preferences |
| Preferences Management | v1 | /user/v1/preferences/{username}/{preference_key} |
GET | Retrieve a specific preference for a user |
| Preferences Management | v1 | /user/v1/preferences/{username}/{preference_key} |
PUT | Update a specific preference for a user |
| Preferences Management | v1 | /user/v1/preferences/{username}/{preference_key} |
DELETE | Delete a specific user preference |
| Preferences Management | v1 | /user/v1/user_prefs/ |
GET | List all user preferences via ORM |
| Preferences Management | v1 | /user/v1/user_prefs/{id}/ |
GET | Retrieve a single user preference via ORM |
| User Management | v1 | /user/v1/users/ |
GET | List all users via ORM |
| User Management | v1 | /user/v1/users/{id}/ |
GET | Retrieve a single user object via ORM |
| Validation & Registration | v1 | /user/v1/validation/registration |
POST | Validate registration form inputs before creating a new user |
| Account Registration | v2 | /user/v2/account/registration/ |
GET | Retrieve registration endpoint info |
| Account Registration | v2 | /user/v2/account/registration/ |
POST | Create a new user account |
| Login Session | v1 | /user/{api_version}/account/login_session/ |
GET | Get login session info |
| Login Session | v1 | /user/{api_version}/account/login_session/ |
POST | Authenticate user and create login session |
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
Abdul-Muqadim-Arbisoft commentedon Oct 15, 2026
Standardization PR open: #39237
PR: #39237 — feat: standardize the user directory, preference and time-zone APIs as /api/user/v2/
State: open
What this does
This is phase A of five for the
[API] Userfunctional area(#39058), under the API standardization
umbrella #38137. The area is split A → E
on that issue's plan; this PR carries A only — seven read-only operations — and B, C, D and E
are separate PRs. B (accounts and profile images), C (preferences and email opt-in) and D (the
retirement pipeline and username replacement) follow this one; E (registration, login session,
validation and password reset) is not approved to build and is listed here only so the phase count
is not mistaken for four.
ADRs applied
14 of 17 decisions applied on this pass; every one left open carries its reason.
v2/package, no superseded file is in the diff, and the superseded addresses are flagged in the schema — but they carry no docstring marker, which decision 4 also asks for. The reason (FC-0118 review, OEP-21 timing) and the follow-up form are in the matrix row and in Deprecationstags=["openedx-platform-sdk"], and SDK tagging starts at phase B. The learner-facing account and preference endpoints are the SDK candidates; an admin-only directory is not, and tagging it would put it in a generated client for no callerThe evidence for each tick is the compliance matrix in the PR body; nothing is ticked without a named test or gate line.
Gates
run_gates.sh — feat/api-adr-user-v2-phase-a vs origin/master @ ce074d1 (2026-10-15)
Six gates ran; none skipped; two are red.
==================== summary ====================
versions PASS
hygiene PASS
schema PASS
urls FAIL
old-tests FAIL
tests PASS
artifacts: /tmp/api-adr-gates.pByf8M
GATES FAILED: 2
RESULT: PASS
RESULT: PASS (0 FAIL, 1 WARN)
RESULT: PASS
RESULT: PASS (0 BREAKING, 18 WARN, 12 INFO)
URL gate — head routes: 3042, base routes: 3036, new api routes checked: 6
RESULT: FAIL (2 FAIL, 1 WARN)
==================== orchestrator reconciliation: final, post checkpoint-2 ====================
Run 2026-10-15 after the two checkpoint-2 decisions (active-user requirement; alias removed
and unknown query parameters refused). Committed as 1b30148 + a8eecf2.
versions PASS 14 --protect paths unchanged. The --allow arguments are mechanically
inert ("allowed: 0"); the evidence for the 7 in-place files is the
per-file diff review, not this gate.
hygiene PASS 0 FAIL, 1 WARN -- H3 on v2/paginators.py only. The two H11 warnings on
the former ?user= alias text are gone with the alias.
schema PASS 0 BREAKING, 18 WARN, 12 INFO. No path, operationId or component on
master removed or renamed, in the LMS document or in /api-docs.
tests PASS 302 passed (was 284 before the two decisions).
urls FAIL DELIBERATE. All three findings are the JSON-404 catch-all
api/user/v2/{_}.(R6, R8, R11) -- not an addressable resource. Sixcandidate shapes were measured; the only conforming one cannot answer
the "unknown collection" case the fallback exists for. run_gates.sh
passes no allowlist, so the composite gate cannot go green regardless.
BLIND ON: R6/R8/R11 for a fallback route. The five real addresses pass
every machine-checkable 0038 rule.
old-tests FAIL RED AND BLIND, environment, identical on both refs:
branch 7 failed / 100 passed / 16 errors (378.02s)
base 7 failed / 100 passed / 16 errors (381.02s)
23 FAILED/ERROR lines over 19 distinct tests, both sets diff EMPTY.
Every one a pymongo ServerSelectionTimeoutError; the image has no
modulestore, so these cannot run here on master either.
PROVES: nothing that passes on master fails here.
DOES NOT PROVE: that those 19 pass at all. Only CI has Mongo.
Also: only 2 of the 11 legacy suites in this app were exercised.
NOT a gate, but the same duty: the legacy comparison above was run by the orchestrator in a
clean origin/master worktree, same image and interpreter, not inferred from the branch run.
Deprecations
Exactly seven legacy paths are marked by this PR — the seven Phase-A addresses below, and no
others:
GET /api/user/v1/users/GET /api/user/v2/users/GET /api/user/v1/users/{id}/GET /api/user/v2/users/{username}/GET /api/user/v1/user_prefs/GET /api/user/v2/user_preferences/GET /api/user/v1/user_prefs/{id}/GET /api/user/v2/user_preferences/{username},{key}/GET /api/user/v1/preferences/{pref_key}/users/GET /api/user/v2/users/?preference_key={pref_key}GET /api/user/v1/forum_roles/{name}/users/GET /api/user/v2/users/?forum_role={name}&course_key={course_key}GET /api/user/v1/preferences/time_zones/GET /api/user/v2/time_zones/They are marked only by
deprecated: truein the published schema, set by the LMSpost-processing hook (
lms/lib/spectacular.py) from a tuple of those seven exact paths. There isno response header and no deprecation note in any superseded code, docstring or comment — the
superseded modules are not in the diff at all. That is a deliberate departure from ADR 0037's
decision 4, which marks the superseded version in the schema and in the endpoint's docstring: the
FC-0118 reviewer asked for the depr notes to be dropped from the replacement PRs (#39100, #39102),
and OEP-21's marker for a REST API is a single
Deprecated <issue link>line added after the DEPRticket reaches Plan Accepted, not a notice. If a reviewer wants the marker, that one line in a
follow-up is the form it should take.
The
/user_api/v1/…twins of these addresses are not deprecated here: they are called byfrontend-app-account,frontend-app-profileand the Android app, and this PR mounts no successorat those addresses.
No query parameter is deprecated alongside them.
GET /api/user/v1/user_prefs/accepts?user=<id>; its successor names owners by username and publishes no identifier filter, soGET /api/user/v2/user_preferences/?user=<id>is a 400validationenvelope naming the parametersthat list does read. Nothing in this version is published with
deprecated: trueand no responsecarries a
Deprecationheader. The removal note for the seven addresses carries the replacementfilter name, so a caller reading the ticket has the whole move in one place.
Tracked on the FC-0118 umbrella DEPR ticket,
#39128, which removes them one named
release after the release this ships in. This area's entry is posted on #39128 once the PR is
open.
Five behaviour notes the removal must carry:
GET /forum_roles/{name}/users/runsRole.objects.get_or_createon every call today. The v2filter does not create the role; an unknown role returns an empty page. No receiver is attached
to
Role, so nothing else fans out.Where that write still lives, for an operator who relied on reading the list to bring a role
into existence:
seed_permissions_roles(course_key)(
openedx/core/djangoapps/django_comment_common/utils.py:49) creates the five standard roleswith their permissions, and already runs when a course is created or imported in Studio
(
cms/djangoapps/contentstore/utils.py:126,contentstore/management/commands/import.py:82) andwhen a CCX course is set up (
lms/djangoapps/ccx/utils.py:517); the LMS management commandseed_permissions_roles <course_id>runs it for a course that predates that; andassign_role(course_id, user, rolename)(django_comment_common/models.py:62) creates a rolewhen granting it to a user. A role created by the legacy GET carried no permissions and no
members, so none of these is a like-for-like replacement for it — they are what should have
created the role in the first place.
What has to exist before the v1 address can be retired: every course whose roles were
brought into existence by calling it must have them seeded, because the instructor endpoint that
grants a role (
POST …/instructor/api/update_forum_role_membership) reads the role withRole.objects.getand raisesRole.DoesNotExistwhen it is missing.are_permissions_roles_seeded(course_id)reports whether a course is in that state and themanagement command puts it right; the check belongs in the removal ticket, not in this PR, since
the legacy address keeps its behaviour until then.
GET /api/user/v2/time_zones/validatescountry_codeas two letters. A well-formed but unknowncode still falls back to every time zone, exactly as before; a malformed one (
FRA) is now a 400envelope where v1 returned the full list. An empty value (
?country_code=) narrows nothing andreturns every zone, as it does at v1; that branch has a test on both counts.
/api/user/v2/does not acceptX-Edx-Api-Key. Server-to-server callers of the v1 directoryendpoints must issue a staff JWT before moving.
GET /api/user/v1/users/{id}/raise; the v2 entry returnsname: null. A caller moving to v2 gets a row where it used to get a server fault.username, so an integration holding identifiers has a step to take before it can address an
entry:
GET /api/user/v1/accounts?lms_user_id={id}— staff-gated, untouched by this PR anditself standardized in phase B — answers with the account whose
usernamekeys the v2 entry.The v2 directory publishes no identifier filter, and this version refuses a parameter it does
not read, so
GET /api/user/v2/users/?id={id}is a 400validationenvelope namingidratherthan a page that looks narrowed, and the resolution step is not optional.
v2/tests/test_parity.py::test_a_database_identifier_is_refused_instead_of_narrowing_nothingrecords the refusal and
::test_an_identifier_held_by_a_caller_resolves_to_the_username_the_entry_is_keyed_bywalks thetwo calls end to end. This step belongs in this area's entry on the DEPR ticket. Whether a v2
idfilter should exist instead is Open decisions, item 3.Review on the PR, please — this issue stays open until it merges.