Style Guide
1. Purpose
This document defines the core architectural, semantic, and structural design principles for Moodle's modern REST API layer. It serves as the single source of truth for REST semantics, guiding both human developers and AI engines in creating predictable, scalable, and idiomatic integrations.
Objectives
- standardize REST API behaviour
- improve API consistency
- improve OpenAPI generation quality
- align APIs with Moodle architecture
- support long-term maintainability
- support deterministic AI-assisted specification generation
Scope
This document defines:
- REST resource design
- HTTP semantics
- naming conventions
- URI conventions
- response conventions
- error conventions
- versioning behaviour
- Moodle-specific REST constraints
This document does NOT define:
- implementation details
- database design
- business logic
- frontend behaviour
2. Rule Types
The style guide separates:
| Section | Purpose |
|---|---|
| Normative Rules | mandatory behaviour |
| Canonical Patterns | reusable API patterns |
| Rationale | explanatory guidance |
Normative sections MUST use only the following key words, and they are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals:
- MUST
- MUST NOT
- REQUIRED
- FORBIDDEN
- SHOULD
- SHOULD NOT
- MAY
3. Core REST Principles
All Moodle APIs MUST satisfy Richardson Maturity Model Level 2.
3.1 Resource-Oriented Design
API URIs MUST represent resources (nouns), never actions or operations (verbs).
Standard CRUD processing logic MUST be expressed strictly through HTTP semantics (GET, POST, PUT, PATCH, DELETE), NOT by appending action triggers to the path string.
REQUIRED
Resource-oriented APIs:
GET /courses
GET /courses/{courseId}
POST /courses
PATCH /courses/{courseId}
DELETE /courses/{courseId}
GET /course-categories
GET /users
FORBIDDEN
RPC-style APIs:
POST /core_course_get_courses
POST /createCourse
POST /doAction
POST /course/create
RATIONALE
Verb-based, RPC-style endpoints tie the API surface to Moodle's internal web service function names, leak implementation detail, and prevent HTTP tooling (caching, method-based routing, OpenAPI generators) from reasoning about intent. Resource nouns let HTTP semantics carry the verb, enabling standard caching, consistent authorization checks per method, and easier client generation.
3.2 Statelessness
APIs MUST be stateless.
Each request MUST contain:
- authentication context
- authorization context
- required identifiers
- request metadata
APIs MUST NOT depend on server-side session state.
RATIONALE
Moodle's REST layer must scale horizontally and support mobile, third-party, and integration clients that can't rely on sticky sessions or shared server-side session state. Requiring every request to carry its own authentication, authorization, identifiers, and metadata keeps requests independently routable and cacheable, and avoids coupling the API to Moodle's PHP session handling.
3.3 HTTP Semantics
HTTP methods MUST align with operation intent.
| Method | Usage |
|---|---|
| GET | Retrieve resources |
| POST | Create resources |
| PUT | Replace resources |
| PATCH | Partial updates |
| DELETE | Remove resources |
| HEAD | Detect resource existence |
| OPTIONS | Describe communication options |
Rules:
- GET operations MUST NOT modify state
- POST MUST NOT be used for retrieval
RATIONALE
Aligning methods with their defined HTTP semantics lets intermediaries such as browsers, proxies, and CDNs cache and retry requests safely. If GET could mutate state, caching and pre-fetching would become unsafe, and if POST were used for retrieval, clients could not tell idempotent operations from side-effecting ones.
4. URI Structure
All REST API URIs MUST be composed of a:
- Base URI and
- Resource-specific URI suffix
4.1 Base URI
All REST APIs MUST use:
/api/rest/v{major_version}/
{major_version} MUST represent the current major API version.
REQUIRED
/api/rest/v2/
RATIONALE
A versioned base path lets Moodle evolve the REST API's structural conventions over time, for example changing pagination or error format, without breaking existing integrations, since old and new major versions can be served side by side. It also gives a single, predictable place for API gateways and documentation tooling to key off.
4.2 Component URI
All REST APIs MUST specify the component after the base URL.
/api/rest/v2/<component>
EXAMPLE
/api/rest/v2/mod_forum
/api/rest/v2/course
/api/rest/v2/user
/api/rest/v2/block_timeline
/api/rest/v2/core
RATIONALE
Moodle is a plugin-based platform where core and hundreds of plugins each expose functionality. Namespacing every resource under its owning component avoids naming collisions between plugins and core, mirrors Moodle's existing frankenstyle component model that developers already understand, and makes it obvious which plugin owns a given endpoint for maintenance and permission purposes.
4.3 Resource-specific URI Suffix
Resource paths MUST be appended to the component URI.
REQUIRED
/api/rest/v2/<component>/<resource>
/api/rest/v2/<component>/<resource>/{identifier}
/api/rest/v2/<subplugintype>_<subpluginname>/<resource>/{identifier}
EXAMPLE
/api/rest/v2/course/courses
/api/rest/v2/course/courses/9454
/api/rest/v2/user/users/42
/api/rest/v2/mod_forum/discussions/99
/api/rest/v2/assignsubmission_comments/comments/455
RATIONALE
Appending the resource path after the component keeps a consistent, predictable URI shape across the whole API, so any client or code generator can construct or parse a URI knowing only the pattern component/resource/identifier, without needing per-endpoint special cases.
5. Resource Naming Rules
5.1 General Rules
Resource names MUST:
- use plural nouns for collections
- use lowercase
- use kebab-case
- avoid verbs
REQUIRED
/course-categories
/user-enrolments
/course-modules
FORBIDDEN
/getCourses
/courseModules
/course_modules
/createDiscussion
/coursecategories
Resource path segments MUST use kebab-case even when equivalent JSON fields use concatenated lowercase words.
Genuine singleton sub-resources MAY use singular nouns. A singleton MUST identify one subordinate resource within its parent rather than a collection whose members have separate identifiers.
SINGLETON EXAMPLES
/user/users/{userId}/profile
/user/users/{userId}/password
Collections such as /user/users and /user/users/{userId}/custom-fields MUST retain plural nouns. Singleton names MUST remain nouns, not action commands.
RATIONALE
Plural nouns describe collections consistently regardless of operation, and kebab-case is the de facto web convention for URL path segments because it's case-insensitive-safe, readable, and avoids URL-encoding issues that spaces or underscores can cause in some contexts. Path casing is deliberately decoupled from JSON body casing (see section 9.1) because URIs and JSON serialization follow different ecosystem conventions, and mixing them would force clients to guess which convention applies where.
A user's profile and managed password are single subordinate resources, not artificial collections of profiles or passwords. Singular singleton names express that distinction without weakening plural collection naming or permitting action-based paths.
6. Path Parameters
6.1 Parameters
Path parameters MUST:
- use camelCase
- use concatenated words with NO spacing or special characters
- end with
Idwhen referencing identifiers
Path parameters MUST NOT use:
- kebab-case
- snake_case
- StudlyCaps
REQUIRED
/<component>/<resource>/{courseId}
/<component>/users/{userId}
EXAMPLE
/course/course-categories/42
/course/courses/name:ECON-100-2026
FORBIDDEN
/courses/{course_id}
/users/{userid}
/courses/{course id}
/courses/{course-id}
/users/{user}
RATIONALE
Path parameters are placeholders substituted into JSON-facing code and often map directly to JSON identifier fields, for example courseId, so matching JSON's camelCase convention keeps the two consistent for developers reading the endpoint signature next to the payload. A consistent Id suffix also makes it unambiguous, at a glance, which path segments are identifiers versus literal resource names.
7. Resource Structure Rules
7.1 Nested Resources
Nested resources MUST only be used for direct ownership or containment relationships.
Rules:
- must represent clear hierarchical ownership
- must remain shallow (maximum recommended depth: 2)
- must remain readable
REQUIRED
/courses/{courseId}/sections
/mod_forum/discussions{discussionId}/posts/{postId}
FORBIDDEN
/courses/{course_id}/sections/{sectionid}/activities/{activityid}/submissions/{submissionid}
RATIONALE
Deep nesting couples URI structure tightly to data-model relationships, so any change to how resources relate would break client URLs, and long nested paths become hard to read, cache, and generate correct authorization checks for at every level. Limiting nesting to direct containment, such as a course's sections, keeps URIs stable and lets deeper relationships be expressed through query parameters or the resource's own top-level collection instead.
8. Component Namespacing
8.1 Core APIs
Core APIs MUST include component prefixes.
Core APIs MUST NOT include the core_ prefix.
REQUIRED
/course/courses
/user/users
/course/course-categories
FORBIDDEN
/core_course/courses
/core_user/users
RATIONALE
Core Moodle subsystems, such as course or user, still need a namespace to stay consistent with the plugin URI pattern in section 8.2, but the core_ prefix used in legacy web service function names is redundant in a REST context since the absence of a frankenstyle plugin type already implies core. Dropping it keeps core URIs shorter and cleaner while still being unambiguous.
8.2 Plugin APIs
Plugin APIs MUST include Moodle component namespaces in the frankenstyle format.
REQUIRED
/mod_forum/discussions
/mod_assign/submissions
/local_myplugin/settings
/tool_uploadcourse/jobs
FORBIDDEN
/discussions
/submissions
/settings
RATIONALE
Frankenstyle component names are already the canonical, unique identifier for every Moodle plugin across core and third-party add-ons. Reusing that existing naming scheme in URIs avoids collisions between plugins that expose similarly named resources, and lets developers immediately map an API endpoint to the plugin's codebase without inventing a second naming system.
8.3 Component Naming Rules
Component prefixes MUST:
- match Moodle component names
- use lowercase
- preserve underscores
- remain globally unique
RATIONALE
These rules ensure the URI namespace stays in lockstep with Moodle's existing component registry (core_component), so there is exactly one source of truth for a plugin's identity. Any drift, such as a differently cased or abbreviated name in the API, would create confusion between the codebase and the API surface, and could cause collisions since Moodle already guarantees component name uniqueness.
9. Data Format Standards
9.1 JSON Field Naming
JSON fields SHOULD use concatenated camelCase words with NO spacers or special characters.
JSON fields MUST NOT use:
- snake_case
- StudlyCaps
- kebab-case
- UPPERCASE
REQUIRED
{
"courseId": 42,
"shortName": "math101",
"createdAt": "2026-05-19T12:30:00+01:00",
"startDate": "2026-09-01T00:00:00+01:00"
FORBIDDEN
{
"courseid": 42,
"short_name": "math101",
"created-at": 1779193800,
"start date": 1779193800
}
RATIONALE
camelCase is the prevailing convention in modern JSON APIs and matches JavaScript and TypeScript client code directly, so front-end and mobile developers can consume payloads without a field-name translation layer. It's a deliberate departure from Moodle's internal PHP and database convention of snake_case, chosen to make the public API idiomatic for its consumers rather than mirroring Moodle's internal storage layer.
9.2 Nullability Rules
- Nullable fields MUST be explicit
- Arrays SHOULD NOT contain null values
- Nullable fields SHOULD only exist when semantically required
Response fields MAY use null for values that are unavailable or hidden from the caller, preserving a stable response shape without distinguishing those reasons. This meaning MUST be documented, and OpenAPI response schemas MUST explicitly include null in the permitted types.
Readable empty strings MUST remain distinct from null. Response nullability MUST NOT implicitly permit null in mutation requests; request schemas MUST define their own nullability.
EXAMPLE
{
"type": ["string", "null"],
"description": "The email address, or null if unavailable to this caller."
}
RATIONALE
Explicitly declaring which fields are nullable, rather than leaving it ambiguous, lets OpenAPI schemas and generated client types be accurate, so consumers can rely on strict typing instead of defensive null checks everywhere. Avoiding null inside arrays and only allowing nullable fields where a genuine "no value" state exists in the data model keeps payloads predictable and avoids nulls being used as a stand-in for missing or not-yet-implemented data.
A user response can expose an email address only to authorised callers, or have no recorded last-access time. Declaring these response fields nullable represents the actual contract without revealing why a value is unavailable. It does not imply that sending null in a user update clears the field.
9.3 Date and Time Standards
Dates and Timestamps MUST:
- use the ISO-8601 format
- Include the timezone
Timestamps SHOULD:
- be returned in the current user's timezone
Timestamps MUST NOT
- use Unix timestamp format
REQUIRED
{
"createdAt": "2026-05-19T12:30:00+01:00"
}
PROHIBITED
{
"createdAt": 1779193800
}
RATIONALE
ISO-8601 strings are self-describing and human-readable in logs and API explorers, unlike Unix timestamps which require external context to interpret and can't unambiguously carry timezone information. Returning timestamps in the current user's timezone matches how Moodle already displays dates throughout its UI, so API consumers building user-facing features get behaviour consistent with the rest of the platform without doing their own timezone conversion. This is particularly important because JavaScript's native Date object has poor built-in timezone support and handles Unix timestamps inconsistently across environments, so requiring an explicit, timezone-carrying string avoids ambiguity that JavaScript clients would otherwise introduce.
10. Collection Standards
10.1 Response Format
Collection endpoints MUST return:
{
"items": [
{
"id": 42,
"fullName": "Introduction to Economics"
}
],
"pagination": {
"page": 1,
"pageSize": 25,
"totalItems": 1
}
}
RATIONALE
A consistent envelope for every collection endpoint means clients can write one generic pagination and list-rendering component instead of a bespoke one per endpoint, and it clearly separates the actual data (items) from metadata about the result set (pagination), avoiding ambiguity about which top-level keys are data versus paging info.
10.2 Pagination
Collection endpoints MUST support pagination via query parameters:
?page=1&pagesize=25
The default values of these parameters are:
| Rule | Value |
|---|---|
| Default page | 1 |
| Default pagesize | 25 |
| Maximum pagesize | 100 |
The default and maximum page sizes MAY be respecified within an endpoint.
RATIONALE
Mandatory pagination protects Moodle sites with large datasets, for example a site with 100,000 users, from accidentally returning unbounded result sets that could exhaust server memory or time out. Standardizing the parameter names and defaults across all endpoints means a single API client library can implement pagination once and reuse it everywhere, rather than special-casing each endpoint.
10.3 Sorting
Sorting MUST:
- be specified as query parameters
- be specified in the format:
?sort=createdat,-fullname
Descending order MUST use -.
If no sort order is specified, order is not guaranteed.
RATIONALE
A single comma-separated sort parameter with a - prefix for descending order is a widely recognized convention, used by JSON:API and many other REST APIs, that supports multi-field sort without needing a growing number of bespoke query parameters. Explicitly stating that order isn't guaranteed when unspecified avoids clients accidentally depending on incidental database ordering that could change without notice, for example after an index change. Even when a sort is explicitly specified, rows with equal values on the sorted field or fields don't get a guaranteed relative order unless the client adds further fields as tiebreakers, for example ?sort=firstname,-id, since the guarantee only applies to the fields actually specified.
10.4 Filtering
Filtering of results MUST use query parameters:
/courses?categoryid=3&visible=true
RATIONALE
Query parameters are the standard HTTP mechanism for narrowing a GET request without changing the resource identity in the path, keeping the URI stable and cacheable for a given filter combination. It also keeps filtering orthogonal to pagination and sorting, so the three concerns can be combined freely without special-case syntax.
11. Endpoint Patterns
11.1 Collection Resource Pattern
GET /api/rest/v{version}/<component>/<resource>
REQUIRED Behaviour
- pagination
- filtering
- sorting
- authorization filtering
REQUIRED Response
{
"items": [],
"pagination": {
"page": 1,
"pageSize": 25,
"totalItems": 100
}
}
RATIONALE
This section exists to guarantee every collection endpoint behaves consistently: a developer or AI generator building a new endpoint for any component can follow this single pattern rather than rediscovering pagination, filtering, sorting, and authorization scoping independently each time, reducing both implementation inconsistency and security gaps, such as forgetting to filter results by capability.
11.2 Single Resource Pattern
GET /api/rest/v{version}/<component>/<resource>/<identifier>
REQUIRED Behaviour
- authorization validation
- 404 handling
- capability enforcement
OPTIONAL Expansion
Single-resource endpoints MAY support expansion of related resources where there is a clear use case. Expansion MUST NOT be required solely to satisfy this endpoint pattern.
Endpoints offering expansion MUST document:
- parameter syntax and allowed relationships
- response shape
- permission and field-visibility filtering
- cost and depth limits
Expansion MUST NOT bypass scopes, capabilities, or field-visibility rules. Related resources MAY remain independently addressable without an expansion mechanism.
RATIONALE
Retrieving a single resource by identifier still requires the same authorization and capability checks as a collection, just scoped to one record; making this explicit prevents implementers from assuming that fetching one thing is lower risk than listing many and skipping checks. Standardizing 404 handling also ensures a consistently shaped error response, per section 12, rather than each endpoint inventing its own not-found behaviour.
Separate user profile and custom-field resources already allow clients to retrieve those representations without requiring every user read to support embedding them. Making expansion optional avoids an unnecessary response and authorisation contract while requiring explicit limits and permission filtering wherever embedding is useful.
11.3 Create Resource Pattern
POST /api/rest/v{version}/<component>/<resource>
REQUIRED Behaviour
- input validation
- duplicate handling
- transactional integrity
- capability enforcement
REQUIRED Response
201 Created
The response MUST include:
- Location header
- created resource representation
The created representation MAY be limited to fields the caller is authorised to receive, including identifiers and values supplied by the caller. Creation MUST NOT require an additional read scope solely to return this representation. Credentials and other write-only fields MUST NOT be returned.
Documented processing-result metadata MAY accompany the representation. Such metadata MUST be distinguished from persistent resource attributes, and partial side-effect failures MUST NOT be reported as though resource creation was rolled back.
RATIONALE
Returning 201 with a Location header follows HTTP semantics for resource creation and gives clients the canonical URI of the new resource without them having to construct it themselves. Requiring the created representation in the response saves a follow-up GET, and mandating transactional integrity and duplicate handling protects Moodle's data integrity when, for example, two requests attempt to create the same course simultaneously.
11.4 Complex Resource Transformations Pattern (Cloning, Imports)
Operational processes that generate resources by copying, duplicating, or importing data from another existing asset MUST be modeled as a resource creation operation (POST) targeting the destination resource collection. The source asset identity MUST be passed strictly as a query parameter modifier or request metadata property.
REQUIRED Sourcing Architecture
POST /api/rest/v{version}/<component>/<resource>?clonefrom=<identifier>
POST /api/rest/v{version}/<component>/<resource>/<identifier>/<resource>?sourcecourseid=5
FORBIDDEN Sourcing Architecture
POST /api/rest/v{version}/<component>/<resource>/<identifier>/duplicate
POST /api/rest/v{version}/<component>/<resource>/<identifier>/imports
RATIONALE
Cloning or importing fundamentally creates a new resource, so it belongs under the same POST-to-collection semantics as any other creation (see section 11.3), rather than inventing an action-style sub-path like /duplicate that violates the noun-only rule in section 3.1. Passing the source as a query parameter or metadata property keeps the URI focused on the destination resource being created, which is what the client is actually requesting.
11.5 State Evaluation and Delta Query Pattern
Operational queries that compute states, track timeline deltas, or check for data synchronizations (e.g., checking for content updates since a specific marker) MUST be designed as read-only operations using GET. Developers and generating AI engines MUST NOT append execution command verbs (such as /check, /verify, or /calculate) to path strings.
REQUIRED Pattern
GET /api/rest/v{version}/<component>/<resource>/<identifier>/updates?since=1779193800
GET /api/rest/v{version}/<component>/<resource>/<identifier>/updates?since=2026-05-19T12:30:00+01:00
FORBIDDEN Pattern
POST /api/rest/v{version}/<component>/<resource>/<identifier>/updates/check
RATIONALE
Checking for updates or computing a state delta is a read-only query, not a mutation, even though it involves computation on the server. Modeling it as GET on a sub-resource, such as /updates, keeps it cacheable and safe to retry or prefetch, and avoids the anti-pattern of a POST-based command endpoint that hides a read operation behind a verb.
11.6 Declarative Property Representation Pattern
Payload objects for mutation requests using POST, PUT, and PATCH MUST describe the intended state of the target resource rather than relative transitions or step calculations. Documented processing controls MAY accompany resource attributes under section 11.8. Distinct workflows SHOULD be modelled as resources under section 11.9.
REQUIRED Declarative Architecture
PATCH /api/rest/v{version}/course-categories/5
{
"parent": 2,
"sortOrder": 1
}
FORBIDDEN Declarative Architecture
PATCH /api/rest/v{version}/course/categories/5
{
"move": "up",
"incrementDepth": 1
}
RATIONALE
Declarative payloads describe what the resource should look like afterwards, which is easier to validate against a schema and safer to retry. Imperative payloads like "move": "up" encode a transition rather than a state, so retries or race conditions between two clients could produce different results depending on execution order, making PATCH retries unsafe (see section 15.2).
11.7 Declarative Resource Modification Pattern
State alterations, resource updates, lifecycle suspensions, and value updates MUST target the resource noun directly using standard mutation verbs without appending procedural execution flags or custom action sub-resource extensions to path strings. Processing controls permitted by section 11.8 MUST NOT substitute for directly expressible resource-state changes.
REQUIRED Mutation Architecture
PATCH /api/rest/v{version}/course/activities/40
{
"visible": false
}
FORBIDDEN Mutation Architecture
PATCH /api/rest/v{version}/course/activities/40
{
"action": "hide"
}
RATIONALE
This reinforces section 11.6 specifically for lifecycle state changes such as visibility or suspension: an "action": "hide" flag is really just an RPC verb smuggled into the request body, violating the resource-oriented principle in section 3.1 just as surely as putting a verb in the URI would. Setting the actual field value directly, such as "visible": false, keeps the operation declarative, idempotent, and self-documenting from the payload alone.
11.8 Mutation Processing Controls
Mutation requests MAY include processing controls for ancillary behaviour, such as notification delivery. Creation requests MAY additionally specify one-time initialisation policy, such as generation of initial credentials.
Processing controls:
- MUST be documented separately from persistent resource attributes, including their applicability, defaults, validation, side effects, and failure behaviour.
- MUST have the same meaning wherever they are reused.
- MUST NOT be silently ignored when unsupported or incompatible with other supplied values.
- MUST NOT replace a directly expressible state change with an execution command.
- MUST NOT imply that external side effects, such as email delivery, are atomic with a database transaction.
- MUST have documented duplicate-request behaviour, including whether a retry repeats notifications or generates new credentials.
Domain-specific processing controls SHOULD be explicit properties of the JSON request body. DELETE requests SHOULD NOT require a body; controls for such requests MAY use documented query parameters or request headers. Changing a control's location MUST NOT change its semantics or bypass these requirements.
Ignorable HTTP preferences, such as those expressed with Prefer, MUST NOT be used for requirements whose omission would violate the requested behaviour, such as suppressing a credential email.
EXAMPLE
POST /api/rest/v2/user/users
Content-Type: application/json
{
"username": "new.student",
"firstName": "New",
"lastName": "Student",
"email": "new.student@example.invalid",
"generatePassword": true,
"sendPasswordEmail": false
}
Here, generatePassword and sendPasswordEmail are creation-time controls, not readable user attributes. Suppressing email does not imply that the generated password is returned.
RATIONALE
Notification delivery and initial credential generation are legitimate provisioning concerns, but do not describe persistent user attributes. Explicitly distinguishing these controls avoids pretending that they are resource state or hiding them in transport metadata to evade the declarative rules. The same distinction applies to notifications accompanying updates or deletions.
11.9 Workflow Resources
Mutations that initiate a distinct workflow or repeat-sensitive operation SHOULD use POST to an appropriate resource, rather than encode an execution command in PATCH. Setting an explicit password MAY use a credential-resource mutation; generating a new password for an existing account SHOULD submit to a password-reset resource.
A workflow MAY be represented by a singleton processing resource with a singular noun, such as /user/users/{userId}/password-reset or the public /user/password-reset. A completed synchronous submission that creates no separately addressable resource MUST return 200 with its outcome or 204 without a body, rather than imply resource creation with 201 and Location. Accepted asynchronous processing MAY return 202.
Workflow submissions MUST NOT require historical result records, identifiers, retention periods, or result GET endpoints solely to justify collection naming. Workflows that create independently addressable resources MUST follow section 11.3 and use plural collection names.
Workflow resources:
- MUST define the target resource, processing behaviour, and outcome.
- MUST document result retention and access permissions if the result is retrievable.
- MUST distinguish workflow success from ancillary delivery success.
- MUST NOT expose credentials in their result representations.
- MUST document duplicate-request behaviour under section 15.2.
- MAY complete synchronously; creating a workflow resource does not require a background job.
Public account-recovery requests MUST follow section 11.10 instead of exposing an account-specific workflow receipt.
EXAMPLE
POST /api/rest/v2/user/users/42/password-reset
Content-Type: application/json
{
"sendPasswordEmail": true
}
200 OK
Cache-Control: no-store
Content-Type: application/json
{
"status": "completed",
"credentialEmailStatus": "sent"
}
RATIONALE
Retrying a password-generation command can replace credentials again and send another email. A singleton reset resource makes that processing and its immediate non-secret outcome explicit without promising that POST is safe to retry or inventing a historical reset collection. Ordinary state updates remain declarative; independently addressable workflow results still receive an appropriate collection model.
11.10 Public Account Recovery
An API MAY accept an unauthenticated request to initiate account recovery under the public-endpoint rules in section 14.2. Public recovery requests MUST NOT change credentials before the recipient proves control through the recovery mechanism.
Public recovery endpoints:
- MUST use
POSTto accept a recovery request. - MUST return the same generic
202 Acceptedrepresentation for matching, missing, ambiguous and ineligible accounts, including account-specific delivery failures. - MUST NOT expose account identifiers, recovery secrets, delivery status or account-specific result lookup URLs.
- MUST use a common response path and deferred delivery, or equivalent timing protection, to avoid disclosing account-dependent outcomes through response timing.
- MUST apply abuse controls, including request throttling and per-identifier delivery limits, without revealing whether an identifier belongs to an account.
- MUST log account-specific delivery failures privately rather than include them in the public response.
- MUST use time-limited, single-use recovery credentials and the site's supported recovery eligibility and password-policy rules.
- MUST NOT return a password.
- SHOULD reuse the existing recovery and token-redemption lifecycle rather than introduce a second token system.
- MAY return account-independent request-validation errors, throttling responses or service-wide availability errors.
Public recovery requests MAY return 202 without a Location header or retrievable request resource. Acceptance MUST NOT be described as confirmation of account existence or email delivery.
EXAMPLE
POST /api/rest/v2/user/password-reset
Content-Type: application/json
{
"email": "student@example.invalid"
}
202 Accepted
Cache-Control: no-store
Content-Type: application/json
{
"message": "If an eligible account matches, password-reset instructions will be sent."
}
RATIONALE
People recovering an account may have no usable authentication credential. Requiring an API scope would make the recovery path inaccessible, while reporting ordinary account-specific success or failure would allow account enumeration. A generic acceptance response and protected reset-link redemption preserve recovery access without exposing account details. This is distinct from an authorised administrative operation that immediately replaces credentials and can return a private result.
12. Error Handling
12.1 Standard Error Format
All errors MUST be formatted per RFC-9457:
{
"type": "about:blank",
"title": "Not Found",
"detail": "Course not found",
"instance": "/course/courses/934"
}
RATIONALE
A single, predictable error envelope across every endpoint means client code and error-handling middleware can be written once, rather than needing to parse endpoint-specific error shapes. Separating a stable, machine-readable code from a human-readable message lets clients branch on logic reliably while still surfacing a readable message to end users, and details gives room for structured context without breaking the top-level shape.
12.2 Validation Errors
Validation errors MUST retain the RFC 9457 problem object and include a top-level details array of field errors. Each field error MUST contain:
field: the request-field path, using the request's camelCase property names, such asprofile.countryorcustomFields.fields.studentcode.code: a stable, non-localised lowercase identifier under section 12.3.message: a human-readable explanation that MAY be translated.
Validation errors MUST NOT introduce a separate nested error envelope or echo passwords, recovery tokens, or other secrets. Independent field failures SHOULD be collected where practical; responses MUST NOT imply that every possible error has been found.
REQUIRED
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"detail": "Some fields are invalid.",
"details": [
{
"field": "email",
"code": "invalidemail",
"message": "Enter a valid email address."
}
]
}
RATIONALE
A generic "invalid input" message forces the client, or a human filling out a form, to guess which field failed and why. Field-level details let clients map errors directly back to the offending form field, which is essential for building usable client UIs on top of the API, especially for Moodle's own forms and mobile app.
User creation and profile updates can fail on several independent fields. Keeping their details inside the standard problem object gives clients one error envelope, while stable field codes and request-field paths support both automated handling and translated form messages without exposing credentials.
12.3 Error Code Rules
Documented application-specific problems that clients need to distinguish MUST include a top-level code extension in the RFC 9457 problem object. Generic HTTP or framework failures MAY rely on status and type without a code.
The type member MUST retain its RFC 9457 meaning and MAY identify a documented problem-type URI. Application codes MUST NOT be copies of HTTP status numbers or disclose information deliberately hidden by an endpoint's error contract.
Existing deprecated numeric errorcode members MAY remain until their normal removal. Their meaning MUST NOT be changed in place to introduce the application-code convention.
Error codes MUST:
- remain stable
- be machine-readable
- use concatenated lowercase words with NO spacers or special characters
- avoid localization
Error codes MUST NOT:
- use camelCase
- use kebab-case
- use snake_case
REQUIRED
- "coursenotfound"
EXAMPLE
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "usernameexists",
"detail": "The username is already in use."
}
PROHIBITED
- "course_not_found"
- "courseNotFound"
- "course-not-found"
RATIONALE
Error codes are meant to be matched programmatically by client logic, so they must stay stable across releases and be casing-convention-free to avoid clients hardcoding fragile casing assumptions; this also matches Moodle's existing legacy convention of lowercase exception and string identifiers, such as coursenotfound in language files. Avoiding localization in the code itself keeps it a stable identifier, while the separate message field, see section 12.1, carries the human-readable, localizable text.
A user-creation client needs to distinguish a duplicate username from other conflicts without parsing translated text. A top-level application code provides that distinction without repurposing the problem-type URI or changing an existing numeric compatibility field. Public recovery still conceals account-specific outcomes rather than exposing them through codes.
13. HTTP Status Codes
| Status | Usage |
|---|---|
| 200 | Success |
| 201 | Created |
| 202 | Accepted |
| 204 | No Content |
| 400 | Malformed request |
| 401 | Unauthenticated |
| 403 | Unauthorized |
| 404 | Resource not found |
| 409 | Conflict |
| 412 | Precondition Failed |
| 422 | Validation failure |
| 429 | Rate limited |
| 500 | Internal error |
Requests that cannot be parsed or structurally interpreted, such as malformed JSON, MUST return 400 Bad Request.
Well-formed input that fails field validation MUST return 422 Unprocessable Content. This includes missing required fields, invalid email addresses or time zones, unknown or read-only fields, password-policy failures, and incompatible processing options. Router and handler validation MUST apply the same distinction.
Permission failures and locked-field restrictions MUST return 403 Forbidden. Uniqueness conflicts MUST return 409 Conflict. Failed ETag preconditions MUST return 412 Precondition Failed.
RATIONALE
Using standard HTTP status codes for their documented meaning, rather than always returning 200 with an error object in the body as much legacy Moodle web service code does, lets generic HTTP infrastructure, such as proxies, monitoring, and client libraries, correctly interpret success and failure without inspecting the response body first.
For example, a user update containing malformed JSON cannot be interpreted, whereas a valid JSON object containing an invalid email address can be understood but fails validation. A duplicate username conflicts with existing state, and an authentication-plugin field lock is an authorisation restriction, not invalid field syntax. Keeping these outcomes distinct lets clients choose the appropriate recovery.
14. Authentication and Authorization
14.1 Authentication
Preferred authentication methods:
- OAuth2
- Personal Access Tokens
Session-cookie-only APIs SHOULD be avoided.
RATIONALE
OAuth2 and service tokens are stateless credentials that travel with each request, satisfying the statelessness requirement in section 3.2, and are supported natively by third-party integrations, mobile apps, and server-to-server clients, unlike browser session cookies which assume a same-origin browser context and don't work well for non-browser or cross-domain clients.
14.2 Authorization
Authorization MUST be scope-based for scoped endpoints. Public endpoints, such as fetching templates, fetching language strings, or requesting a password-reset email, MAY omit authentication and required scopes where their access policy permits.
REQUIRED
core_course:course:content:read
core_course:course:content:write
All scoped endpoints MUST define:
- sets of required scopes
Endpoints that do not require scopes MUST explicitly declare #[\core\router\scope\unscoped_resource] on the route handler method in their PHP definition. Omitting scope attributes MUST NOT be treated as an implicit declaration of unscoped access.
An unscoped declaration MUST NOT implicitly disable authentication, capability checks, or other access restrictions. These requirements MUST be defined separately.
Public endpoints MUST explicitly document their access policy and applicable abuse protections. Public account recovery MUST additionally follow section 11.10.
EXAMPLE
#[\core\router\scope\unscoped_resource]
public function fetch_public_resource(): ResponseInterface {
// Return a resource permitted by the endpoint's access policy.
}
RATIONALE
Scope-based authorization decouples "who the caller is", established by authentication in section 14.1, from "what the caller is allowed to do", letting a single OAuth2 or personal access token be issued with only the narrow set of permissions an integration actually needs, following the principle of least privilege.
Naming scopes after their component and resource, for example core_course:course:content:read, keeps them self-describing and consistent with the component-namespacing rules in section 8, and lets tooling and documentation enumerate exactly which endpoints a given scope unlocks. Declaring required scopes per endpoint also allows authorization to be checked and audited consistently, independent of a caller's individual Moodle capabilities, which may change over time.
Explicitly marking unscoped handlers distinguishes intentional public or otherwise unscoped access from accidentally missing scope definitions. Authentication and scope requirements are separate: an unscoped resource is not necessarily anonymous.
15. Concurrency and Idempotency
15.1 Concurrency Control
Mutable resources SHOULD support optimistic concurrency control.
If supported, APIs MUST use:
ETag
If-Match
Conflicting updates MUST return:
412 Precondition Failed
RATIONALE
Optimistic concurrency lets Moodle avoid locking rows for the duration of an edit, which would hurt scalability, while still protecting against two clients overwriting each other's changes unknowingly, such as two teachers editing the same course settings simultaneously. ETag and If-Match are the standard HTTP mechanism for this, so existing HTTP client and proxy support can be reused rather than inventing a bespoke versioning scheme.
15.2 Idempotency
| Method | Idempotent |
|---|---|
| GET | Yes |
| PUT | Yes |
| DELETE | Yes |
| POST | No |
| PATCH | No |
POST operations intended to support retries SHOULD support:
Idempotency-Key
Duplicate request behaviour MUST be explicitly defined.
RATIONALE
Clients on unreliable networks, particularly mobile, need to safely retry requests without accidentally creating duplicate resources; since POST is inherently non-idempotent, an explicit Idempotency-Key lets the server recognize and deduplicate a retried creation request as the same logical operation, rather than the client having to guess whether a prior request actually succeeded before retrying.
More generally, an idempotency key guards against other unreliable client and server behaviour, such as a dropped connection after the server has already committed a change, or a server restarting mid-request, where the client cannot tell whether the original request succeeded.
16. Legacy REST Migration and Backward Compatibility
This section defines mandatory standards for transitioning legacy Moodle RPC external functions into RESTful resource layouts while guaranteeing zero data loss, zero dropped fields, and structural regression protection for existing system consumers.
16.1 The Return Payload Superset Rule
When an RPC function is migrated to a REST endpoint, the new REST JSON response payload MUST be a functional superset of the legacy RPC return structure.
Normative Rules
- Redesigned REST response payloads MUST include every data property returned by the original legacy RPC external function block.
- Old fields may only be fully removed in a future major version release cycle, never in minor updates.
RATIONALE
-TO DO:* this section is under revision; most of it is being removed as incorrect, so its rationale will be drafted once the replacement content is agreed.
17. Prohibited Patterns
The following are FORBIDDEN:
- RPC-style endpoints
- action-based URLs
- deep resource nesting
- generic
"data"wrappers - inconsistent date formats
- undocumented enum values
- POST-based retrieval APIs
- session-dependent APIs
18. Consistency Requirements
APIs MUST maintain consistency across:
- naming
- pagination
- filtering
- sorting
- error structures
- authentication
- authorization
- response formats
The same concept MUST NOT use multiple names.