Access
Authentication
Every endpoint asks for a bearer token, except two: the specification and the terms. A ppl_live_ token works on the REST API and on the MCP server; an OAuth token only on the MCP server.
Sending the token
Send the token in the Authorization header, on every request:
Authorization: Bearer ppl_live_...No header, or an unknown or revoked token, gets 401 unauthorized. A valid token without the scope the endpoint asks for gets 403 forbidden. Never put a token in a URL or in code that runs on your users' devices.
Scopes
A token carries one or more scopes. Every endpoint asks for exactly one; the reference says which for each endpoint.
| Scope | Who can get it | What it opens |
|---|---|---|
read | Every organisation in the portal; through OAuth anyone (Read only). | The public agenda, your own events and your token. (16 endpoints) |
scan | Organisations in the portal; through OAuth the roles at the door. | Validating tickets and marking them used at the door. (1 endpoint) |
attendance | Organisations in the portal; through OAuth the roles that edit events. | Going, interested and ticket counts of your own events, and reporting external sales. (3 endpoints) |
tickets | Organisations in the portal, for a link with their own ticket shop. Never through OAuth. | Delivering the ticket list of your own shop, so the tickets land in the buyers' app. (2 endpoints) |
admin | Only the platform itself, in /admin/api or as an administrator through OAuth. | The agenda team's work. |
OAuth 2.1 with PKCE (for the MCP server)
The MCP server has OAuth 2.1, so an assistant can connect without anybody copying a token. What is implemented:
- Authorization code with PKCE,
S256only. No implicit flow and no passwords. - Dynamic client registration (RFC 7591) at
/oauth/register. A redirect URI is https, or http on a loopback address (localhost,127.0.0.1,[::1]) on any port. - Resource indicators (RFC 8707): a token is issued for
https://partyppl.nl/mcpand works nowhere else, the REST API included. - Metadata per RFC 8414 and RFC 9728, and
issin the response (RFC 9207). - An access token (
ppl_oat_) lives 60 minutes; a refresh token (ppl_ort_) 30 days and is replaced on every use. An authorization code is valid for 60 seconds and once; the request waits 10 minutes for the person to sign in and say yes. - Revocation at
/oauth/revoke(RFC 7009), or by the person under Connected apps in their settings.
What a connection may do is decided by the person on the consent page, by their role: Read only gives read; an organisation gives what that role may do there (read, plus attendance and scan for the roles that edit events or work the door). When the role changes, so does what the token may do, on the next request. The tickets scope is never given through OAuth.
The authorization server metadata, as this server issues it:
{
"issuer": "https://partyppl.nl",
"authorization_endpoint": "https://partyppl.nl/oauth/authorize",
"token_endpoint": "https://partyppl.nl/oauth/token",
"registration_endpoint": "https://partyppl.nl/oauth/register",
"revocation_endpoint": "https://partyppl.nl/oauth/revoke",
"response_types_supported": [
"code"
],
"response_modes_supported": [
"query"
],
"grant_types_supported": [
"authorization_code",
"refresh_token"
],
"token_endpoint_auth_methods_supported": [
"none",
"client_secret_post",
"client_secret_basic"
],
"revocation_endpoint_auth_methods_supported": [
"none",
"client_secret_post",
"client_secret_basic"
],
"code_challenge_methods_supported": [
"S256"
],
"scopes_supported": [
"read",
"attendance",
"scan",
"admin"
],
"authorization_response_iss_parameter_supported": true,
"service_documentation": "https://partyppl.nl/api",
"ui_locales_supported": [
"nl",
"en"
]
}The flow step by step
An MCP client like Claude does this on its own. If you build your own client, these are the steps:
- Call
/mcpwithout a token. The answer is401with aWWW-Authenticateheader pointing at the resource metadata. - Read
/.well-known/oauth-protected-resource, then/.well-known/oauth-authorization-server. - Register your client with
POST /oauth/registerand keep theclient_id. - Make a
code_verifierand itscode_challenge(SHA-256, base64url), and send the person to/oauth/authorizewithresponse_type=code,client_id,redirect_uri,code_challenge,code_challenge_method=S256,stateandresource. - After yes the person comes back to your
redirect_uriwithcode,stateandiss. Checkstateandiss. - Exchange the code with
POST /oauth/token(grant_type=authorization_code,code,redirect_uri,client_id,code_verifier), asapplication/x-www-form-urlencoded. - Send the access token to
/mcp. Expired? Get a new one withgrant_type=refresh_token.
# 1. PKCE
verifier=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)
challenge=$(printf %s "$verifier" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
# 2. Register a client
curl -s -X POST "https://partyppl.nl/oauth/register" \
-H "Content-Type: application/json" \
-d '{"client_name":"My app","redirect_uris":["http://127.0.0.1:8765/callback"]}'
# 3. Send the person to the consent page (in a browser)
# https://partyppl.nl/oauth/authorize?response_type=code&client_id=$CLIENT_ID
# &redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback
# &code_challenge=$challenge&code_challenge_method=S256&state=$STATE
# &resource=https://partyppl.nl/mcp
# 4. Exchange the code
curl -s -X POST "https://partyppl.nl/oauth/token" \
-d grant_type=authorization_code -d code="$CODE" -d client_id="$CLIENT_ID" \
-d redirect_uri=http://127.0.0.1:8765/callback -d code_verifier="$verifier"Updated