How Moodle web services really behave
Functions, not resources
Moodle exposes a single REST endpoint. Each request names a function, such as creating users or listing one user's courses, passes a token, and asks for JSON. Parameters travel as form fields, including nested arrays, instead of a JSON body. There are no resource URLs and no meaningful HTTP verbs. Developers used to conventional REST find this odd for a day and then get on with it, provided someone has written down which functions to call and in what order.
Services, users and tokens
A function can only be called if it belongs to a service, the service is enabled, and the token's user has been allowed to use it. That user also needs the capability to use the web service protocol, plus whatever capabilities the function itself checks. We create one service per consumer containing the minimum set of functions, a dedicated user with a purpose-made role, and a token tied to both. If the HR token leaks, it cannot grade assignments, and revoking it does not break the portal.
Writing an external function
Custom functions live in a plugin, usually a local one. Each is a class with three parts: a description of the parameters it accepts, the method that does the work, and a description of what it returns. It is registered in the plugin's services file. Moodle uses those descriptions to validate every call and to generate API documentation in the admin area. Inside the method we validate the context, check capabilities, then use Moodle's own APIs to enroll, grade or read, so events fire and logs are written as if a person had done it. The plugin structure itself is covered under Moodle plugin development.
Errors, load and payload size
Two behaviors catch integrators out. First, a failed call normally still returns HTTP 200 with an exception object in the body, so clients must inspect the response instead of trusting the status code. Second, throttling of API traffic is normally left to the infrastructure in front of Moodle. A consumer that fires thousands of requests competes with learners for the same PHP workers and database. We design for that with batch functions, paging, "changed since" filters and, where needed, limits at the web server or gateway. Large responses are trimmed to the fields the consumer uses, which matters most on mobile connections.
Testing and versioning
External functions are covered by PHPUnit tests that call them as different users and assert both results and refusals. When a function must change shape, we add a new one and keep the old one working until consumers have moved, because a mobile app already in the stores cannot be updated overnight.