API key authentication
API keys provide a stateless, long-lived authentication mechanism for programmatic access and third-party integrations. Unlike JWT sessions, API keys do not require a login flow or token refresh, and are scoped to specific operations.
Security Notice
API keys are powerful credentials. Treat them like passwords: store them securely and never expose them in client-side code, public repositories, or logs.
Key Format
API keys use the following format:
endurain_<43-character-base64url-random-string>
- Prefix:
endurain_— identifies Endurain API keys in secret scanning tools (e.g., GitHub secret scanning) - Random part: 256 bits of cryptographically secure random data (
secrets.token_urlsafe(32)), encoded as a 43-character base64url string - Total length: ~52 characters
The raw key is shown once at creation time and is never stored by the server (only the SHA-256 hash is stored). If lost, the key must be deleted and a new one created.
Scopes
API keys are granted one or more API-key scopes at creation time. Currently only activities:upload is accepted; JWT-only scopes such as profile, activities:read, or users:write cannot be granted to API keys.
The key-management API rejects any requested scope outside the API-key allow-list. This keeps long-lived API keys limited to the endpoint that currently supports API-key authentication.
| Scope | Description |
|---|---|
activities:upload |
Upload privileges over activities |
How to Authenticate with an API Key
API keys bypass the standard JWT and X-Client-Type requirements. Send the raw key using either option:
Option 1: X-API-Key header (recommended):
POST /api/v1/activities/create/upload
X-API-Key: endurain_abc12345...
Content-Type: multipart/form-data
(file body)
Option 2: api_key query parameter (for tools that cannot set custom headers):
POST /api/v1/activities/create/upload?api_key=endurain_abc12345...
Content-Type: multipart/form-data
(file body)
Query parameter delivery is disabled by default
The ?api_key= query parameter is disabled by default. To enable it, set ALLOW_API_KEY_QUERY_PARAM=true in your environment. This option exists for self-hosted deployments where an integration cannot set custom request headers (e.g. certain webhook senders). When enabled, every request using this method emits a warning in the application log.
Query-string credentials appear in server access logs, reverse-proxy logs, and browser history. Use the X-API-Key header whenever possible.
If both the X-API-Key header and the ?api_key= query parameter are present, the header takes precedence.
Endpoints That Accept API Keys
| Method | Endpoint | Required Scope | Description |
|---|---|---|---|
POST |
/activities/create/upload |
activities:upload |
Upload a .gpx, .tcx, .fit, or .gz activity file |
The upload endpoint also accepts JWT authentication through the same unified-auth dependency. JWT upload requests still require Authorization: Bearer <access token> and the activities:upload scope, but this endpoint does not require X-Client-Type.
Managing API Keys
API keys are managed through the Endurain web UI (Settings → Security → API Keys) or via the REST API. All management endpoints require a valid JWT access token.
Creating an API key also requires step-up verification because the key is a long-lived credential:
- Local-password accounts must provide
current_password - Accounts with MFA enabled must also provide
mfa_code - SSO-only accounts may omit
current_passwordbecause there is no local password to verify
Create API Key Request:
POST /api/v1/profile/api_keys
Authorization: Bearer {access_token}
X-Client-Type: web
X-CSRF-Token: {csrf_token}
Content-Type: application/json
{
"name": "Home Server Integration",
"scopes": ["activities:upload"],
"expires_at": "2027-01-01T00:00:00Z",
"current_password": "current-password",
"mfa_code": "123456"
}
| Field | Required | Description |
|---|---|---|
name |
Yes | Human-readable label (max 100 characters) |
scopes |
Yes | Array of API-key scopes to grant. Currently only activities:upload is accepted |
expires_at |
No | ISO 8601 expiry datetime. Omit or null for no expiry |
current_password |
Conditionally | Required for accounts with a local password. Omit for SSO-only accounts |
mfa_code |
Conditionally | Required when MFA is enabled on the account |
Create API Key Response (HTTP 201):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": 1,
"name": "Home Server Integration",
"key_prefix": "abc12345",
"scopes": "[\"activities:upload\"]",
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": null,
"created_at": "2026-03-02T10:00:00Z",
"is_active": true,
"key": "endurain_abc12345..."
}
Save the key immediately
The key field is returned only in this response. It is not stored by the server and cannot be retrieved later. Store it in a secure location (e.g., a password manager or secrets vault) before dismissing the response.
List API Keys Response (GET /profile/api_keys):
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": 1,
"name": "Home Server Integration",
"key_prefix": "abc12345",
"scopes": "[\"activities:upload\"]",
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": "2026-03-01T08:30:00Z",
"created_at": "2026-03-02T10:00:00Z",
"is_active": true
}
]
The key field is never returned in list or subsequent get responses — only key_prefix is shown for identification.
Revoke API Key (PATCH /profile/api_keys/{id}/revoke):
Revocation soft-deletes the key by setting is_active = false. Revoked keys are rejected immediately but remain visible in the list (useful for audit purposes). Returns HTTP 204 on success.
Delete API Key (DELETE /profile/api_keys/{id}):
Permanently removes the key from the database. Returns HTTP 204 on success.
Security Properties
| Property | Detail |
|---|---|
| Storage | SHA-256 hex digest stored server-side; raw key never persisted |
| Comparison | Constant-time (hmac.compare_digest) prevents timing attacks |
| Revocation | Immediate — revoked keys are rejected at validation time |
| Expiry | Optional; expired keys are rejected at validation time with timezone-aware comparison |
| Audit logging | Every successful authentication is logged with key prefix, user ID, endpoint, and client IP |
| No self-escalation | An API key cannot create, list, or revoke other API keys (JWT required) |
| Minimum privilege | Keys carry only the scopes explicitly granted at creation time |