REST API: errors and rate limits
Every error has the same shape:
{ "ok": false, "error": { "code": "validation_failed", "message": "Score must be a whole number.", "fields": { "score": ["Score must be a whole number."] } } }
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request |
| 401 | unauthorized, invalid_credentials | Missing or invalid token / session |
| 402 | limit_reached | Daily play pass is used up; error.entitlement and error.upgrade_url included |
| 402 | premium_required | Game is for Unlimited members |
| 403 | forbidden, account_suspended, email_unverified | Authenticated but not allowed |
| 404 | not_found, leaderboard_disabled | |
| 409 | email_taken, username_taken, version_conflict, too_many_tokens | Conflicts |
| 410 | session_ended, invalid_session | The play session is over |
| 413 | state_too_large | Save state over 256 KB |
| 419 | CSRF token missing (web forms only) | |
| 422 | validation_failed, invalid_slot | Input problems; see fields |
| 429 | rate_limited | Slow down |
| 500 | server_error | Our fault; it is logged |
Rate limits
- API: logged-in players and developer keys are not rate limited by default (
api.rate_limit_per_minute= 0; set a number to enable a per-account limit). Anonymous calls are limited to 120 per minute per IP. - Login: 20 attempts per 15 minutes per IP, 10 per account.
- Registration: 10 per hour per IP.
- Contact form: 5 per hour per IP.
A 429 answer has no Retry-After header; wait a minute.
Timestamps
All timestamps are UTC in YYYY-MM-DD HH:MM:SS form unless they end in a timezone offset (ISO 8601), in which case they are explicit. Day boundaries for the play pass follow the configured reset timezone, reported as entitlement.day.
Versioning
The API is versioned in the path (/api/v1). Backwards-compatible additions (new fields, new endpoints) happen without a version bump; breaking changes get /api/v2 and a migration period. See the changelog.
Last updated October 11, 2026.