Skip to content

Authentication

Every protected endpoint of the API expects proof of identity in the request headers. It always relies on a credential obtained beforehand: a session (possibly an OpenID Connect token) or an API key.

One family of endpoints is deliberately outside all of this: the rail-station referential is open data attached to no group, and takes no credential at all.

Obtaining a session

1. Discover the group's provider

GET /api/v4/auth/provider?group_id=<group_id> tells you how a group authenticates: {"provider": "legacy"} for an email / password login, or {"provider": "auth0", "auth0": {"domain": "...", "connection": "..."}} when the group is delegated to an OpenID Connect provider.

curl --fail --location \
  "https://api.pysae.com/api/v4/auth/provider?group_id=${GROUP_ID}"

2. Open the session

legacy group — POST /api/v4/login. An application/x-www-form-urlencoded body with email and password. The response returns {"session_id": "...", "expires_days": ...}.

curl --fail --location \
  --data-urlencode "email=${EMAIL}" \
  --data-urlencode "password=${PASSWORD}" \
  "https://api.pysae.com/api/v4/login"

auth0 group — OpenID Connect token. Obtain a JWT from the OpenID Connect provider returned by auth/provider, then use it as Authorization: Bearer <jwt>. Email / password login is rejected for these groups.

The session_id (or the JWT) is then used on protected requests, see the "Accepted mechanisms" section. GET /api/v4/logout closes the session (a 204 response):

curl --request GET \
  --header "Authorization: Bearer <session_id>" \
  "https://api.pysae.com/api/v4/logout"

Obtaining an API key

An API key is a persistent, revocable credential bound to a group and to a principal (a user, a device or a role). It avoids handling a password in an automated system.

Creating one goes through an API call authenticated by a session: you must first open a session (above) holding the admin role. POST /api/v4/groups/{group_id}/api-keys creates the key and returns it in the value field. The body declares exactly one principal: user_id, device_id or role (the latter together with group_ids); scopes and expire restrict it.

body="$(printf '{"user_id": "%s"}' "${USER_ID}")"

curl --fail --location \
  --request POST \
  --header "Authorization: Bearer ${SESSION_ID}" \
  --header "Content-Type: application/json" \
  --data "${body}" \
  "https://api.pysae.com/api/v4/groups/${GROUP_ID}/api-keys"

A group's keys are listed via GET /api/v4/groups/{group_id}/api-keys and deleted via DELETE /api/v4/groups/{group_id}/api-keys/{api_key_id}. A key spanning several groups, or a superuser key, is created via POST /api/v4/api-keys.

Accepted mechanisms

Mechanism Header Usage
Session as bearer Authorization: Bearer <session_id> Programmatic client reusing a session
API key Authorization: Api-Key <api_key> or x-api-key: <api_key> Machine-to-machine (recommended)
JWT (OpenID Connect) Authorization: Bearer <jwt> Groups delegated to an OpenID Connect provider

Authorization header

The mechanisms based on the Authorization header — session as bearer, API key and OpenID Connect token — are used identically: you place the exact header value in the request. In the example below, PYSAE_AUTH holds, depending on the case, Bearer <session_id>, Api-Key <api_key> or Bearer <jwt>.

curl --fail --location \
  --header "Authorization: ${PYSAE_AUTH}" \
  "https://api.pysae.com/api/v4/groups/${GROUP_ID}/export/trips?date=20260131" \
  --output trips.csv

The API key also accepts the dedicated header x-api-key: <api_key>, equivalent to Authorization: Api-Key <api_key>:

curl --fail --location \
  --header "x-api-key: ${PYSAE_API_KEY}" \
  "https://api.pysae.com/api/v4/groups/${GROUP_ID}/export/trips?date=20260131" \
  --output trips.csv

Machine-to-machine usage

For an automated integration:

  • API key (recommended) — persistent, revocable, scoped by group and by role, with no password in the calling code.

The POST /api/v4/login flow targets human usage (the web interface) and is not recommended for automation.

Versioning (v2v5, v4 by default) and the public / internal / datahub split are described on the home page.