Errors, concurrency and rate limits
Behaviour that applies to every endpoint of the API.
Error responses
Every error the API returns is an RFC 9457 problem document with the media type application/problem+json. This holds whatever raised the error: a domain error, a request validation failure, an authentication failure or a rate-limit rejection. A client needs only one error path.
{
"type": "urn:endurain:error:not-found",
"title": "Not Found",
"status": 404,
"detail": "Activity not found",
"instance": "/api/v1/activities/42",
"request_id": "5f0d3c1e8b7a4c9d"
}
| Member | Meaning |
|---|---|
type |
Stable URN for the kind of error. Branch on this. It is not a URL and it does not change between releases. about:blank means the status code says everything. |
title |
Short summary of the kind. The same for every occurrence of a type. |
status |
The HTTP status code, repeated in the body. |
detail |
Explanation of this occurrence. Safe to show to a user and never contains internal diagnostics. |
instance |
The request path. |
request_id |
The X-Request-ID of the request. Quote it in a bug report or to your server operator, who can find the exact log line with it. |
errors |
Field-level problems. Present only for a validation failure (422). |
Clients that only read detail keep working. Some errors carry extra members, such as code for a machine-readable subtype.
Error types
| Status | type |
Title |
|---|---|---|
404 |
urn:endurain:error:not-found |
Not Found |
403 |
urn:endurain:error:permission-denied |
Forbidden |
400 |
urn:endurain:error:invalid-input |
Bad Request |
409 |
urn:endurain:error:conflict |
Conflict |
412 |
urn:endurain:error:precondition-failed |
Precondition Failed |
406 |
urn:endurain:error:unsupported-format |
Not Acceptable |
415 |
urn:endurain:error:unsupported-media-type |
Unsupported Media Type |
413 |
urn:endurain:error:payload-too-large |
Content Too Large |
503 |
urn:endurain:error:service-unavailable |
Service Unavailable |
502 |
urn:endurain:error:upstream-error |
Bad Gateway |
500 |
urn:endurain:error:internal-error |
Internal Server Error |
A request validation failure is 422 with urn:endurain:error:validation-failed and an errors list. Exceeding a rate limit is 429 with urn:endurain:error:rate-limited. An error raised without a specific type uses a slug of the HTTP status phrase.
Authentication errors
Authentication and authorization failures use the same shape. Typical causes:
| Status | Common causes |
|---|---|
400 |
Missing required fields, invalid client type, no pending MFA login |
401 |
Invalid credentials, expired token, invalid MFA code |
403 |
Insufficient permissions, missing required scope |
404 |
Invalid session ID, user not found |
429 |
Too many login or OAuth attempts |
Concurrent edits with If-Match
Two clients that read the same activity and then save it would silently overwrite each other. To prevent this, reading an activity returns an ETag. Send it back in If-Match when you edit.
GET /api/v1/activities/42
→ 200 ETag: "7"
PATCH /api/v1/activities/42
If-Match: "7"
- If the activity is still at that version, the edit applies and the response carries the new
ETag. - If someone else changed it in between, the API answers
412withurn:endurain:error:precondition-failed. Read the activity again, reapply your change and retry. If-Match: *matches any version. Omitting the header skips the check, so existing clients keep working.
Rate limits
Limits are counted per caller: an authenticated request is counted per access token, so users behind the same network address do not share a bucket, and an unauthenticated request is counted per client address.
| Tier | Limit | Applies to |
|---|---|---|
DEFAULT |
120/minute | Baseline applied globally via SlowAPIMiddleware. |
WRITE |
30/minute | Write operations — creating or mutating resources. |
SENSITIVE |
10/minute | Sensitive operations — login, MFA, password reset, signup, OAuth flows. |
UPLOAD |
20/minute | Activity file uploads / imports — CPU/IO-heavy parsing of user-supplied files; tighter than WRITE to bound resource consumption on the ingestion endpoints. |
PROVIDER_SYNC |
6/minute | Endpoints that trigger OUTBOUND calls to a third-party provider (Strava, Garmin Connect). Tighter than WRITE because each request amplifies into several external HTTP calls: without a cap a caller can burn the server's shared provider quota (or have the server throttled/banned) with almost no local cost. |
When you exceed a limit the API answers 429 and sets these headers:
| Header | Meaning |
|---|---|
Retry-After |
Seconds until you can retry. |
X-RateLimit-Limit |
The limit you hit. |
X-RateLimit-Remaining |
Always 0 on a 429. |
Each endpoint uses one tier. The rate limits listed per endpoint in the authentication pages are these tiers, and the full API documentation shows the rest.