Skip to content

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:

http
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.

ScopeWho can get itWhat it opens
readEvery organisation in the portal; through OAuth anyone (Read only).The public agenda, your own events and your token. (16 endpoints)
scanOrganisations in the portal; through OAuth the roles at the door.Validating tickets and marking them used at the door. (1 endpoint)
attendanceOrganisations 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)
ticketsOrganisations 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)
adminOnly 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, S256 only. 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/mcp and works nowhere else, the REST API included.
  • Metadata per RFC 8414 and RFC 9728, and iss in 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:

/.well-known/oauth-authorization-server
{
  "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:

  1. Call /mcp without a token. The answer is 401 with a WWW-Authenticate header pointing at the resource metadata.
  2. Read /.well-known/oauth-protected-resource, then /.well-known/oauth-authorization-server.
  3. Register your client with POST /oauth/register and keep the client_id.
  4. Make a code_verifier and its code_challenge (SHA-256, base64url), and send the person to /oauth/authorize with response_type=code, client_id, redirect_uri, code_challenge, code_challenge_method=S256, state and resource.
  5. After yes the person comes back to your redirect_uri with code, state and iss. Check state and iss.
  6. Exchange the code with POST /oauth/token (grant_type=authorization_code, code, redirect_uri, client_id, code_verifier), as application/x-www-form-urlencoded.
  7. Send the access token to /mcp. Expired? Get a new one with grant_type=refresh_token.
bash
# 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