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.
const groupId = process.env.GROUP_ID
const url = new URL("https://api.pysae.com/api/v4/auth/provider")
url.searchParams.set("group_id", groupId)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Provider lookup failed: ${response.status}`)
}
// { provider: "legacy" } or { provider: "auth0", auth0: { domain: ... } }
console.log(await response.json())
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": ...}.
const email = process.env.EMAIL
const password = process.env.PASSWORD
const response = await fetch("https://api.pysae.com/api/v4/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ email, password }),
})
if (!response.ok) {
throw new Error(`Login failed: ${response.status}`)
}
const { session_id: sessionId } = await response.json()
Automation — POST /api/v4/login-scoped. Intended for automation: the scopes and expire query parameters bound the session (scope and expiry date). Same body (email, password) and same response.
const email = process.env.EMAIL
const password = process.env.PASSWORD
const url = new URL("https://api.pysae.com/api/v4/login-scoped")
url.searchParams.append("scopes", "read")
url.searchParams.set("expire", "2026-12-31")
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ email, password }),
})
if (!response.ok) {
throw new Error(`Scoped login failed: ${response.status}`)
}
const { session_id: sessionId } = await response.json()
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.
const groupId = process.env.GROUP_ID
const sessionId = process.env.SESSION_ID
const userId = process.env.USER_ID
const response = await fetch(
`https://api.pysae.com/api/v4/groups/${groupId}/api-keys`,
{
method: "POST",
headers: {
Authorization: `Bearer ${sessionId}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ user_id: userId }),
},
)
if (!response.ok) {
throw new Error(`API key creation failed: ${response.status}`)
}
const { value: apiKey } = await response.json()
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 |
| Session cookie | Cookie: session_id=<session_id> | Browser — set automatically after login |
| WebSocket | Authorization/Cookie header, or first message {"session_id": "<session_id>"} | Real-time streams |
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>.
const groupId = process.env.GROUP_ID
const authorization = process.env.PYSAE_AUTH
const url = new URL(
`https://api.pysae.com/api/v4/groups/${groupId}/export/trips`,
)
url.searchParams.set("date", "20260131")
const response = await fetch(url, { headers: { Authorization: authorization } })
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`)
}
The API key also accepts the dedicated header x-api-key: <api_key>, equivalent to Authorization: Api-Key <api_key>:
const groupId = process.env.GROUP_ID
const apiKey = process.env.PYSAE_API_KEY
const url = new URL(
`https://api.pysae.com/api/v4/groups/${groupId}/export/trips`,
)
url.searchParams.set("date", "20260131")
const response = await fetch(url, { headers: { "x-api-key": apiKey } })
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`)
}
Session cookie¶
After a successful login, the API returns the session_id in a Set-Cookie cookie (httponly, secure, samesite=lax). A browser sends it back on every request; a manual client attaches it explicitly:
curl --cookie "session_id=<session_id>" \
"https://api.pysae.com/api/v4/groups/<group_id>/export/trips"
WebSocket¶
A WebSocket connection reuses the session_id. A client that can set headers sends it via Authorization: Bearer <session_id> or the Cookie, as over HTTP. A client that cannot (browser WebSocket) sends it in the first JSON message of the connection:
const groupId = process.env.GROUP_ID
const sessionId = process.env.SESSION_ID
const socket = new WebSocket(
`wss://api.pysae.com/api/v4/groups/${groupId}/events/op/subscribe`,
)
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ session_id: sessionId }))
})
socket.addEventListener("message", (event) => {
console.log(event.data)
})
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.
-
login-scoped— a session bounded byscopesandexpire, when a short-lived credential is preferable to a permanent key.
The POST /api/v4/login flow targets human usage (the web interface) and is not recommended for automation.
Versioning (v2–v5, v4 by default) and the public / internal / datahub split are described on the home page.