Download OpenAPI specification:
OpenAPI description of the Pinquest Backend API. The API is deployed as a set of independent AWS Lambda services (one per bounded context: auth, health, contests, league, tournaments, quests, high-scores, leaderboards, machine-reports, locations, uploads, prizes) fronted by API Gateway v2. This document unifies every route into a single surface.
Protected routes require a Cognito access token supplied as an HTTP Bearer token (Authorization: Bearer <access_token>). A missing or invalid token is rejected by the shared verifier middleware with 401 {"error":"unauthorized"} and a WWW-Authenticate: Bearer realm="pinquest-api" header, before the handler runs. Public routes (auth, health, uploads, leaderboards, and the read-only tournament/high-score lists) do not require a token.
All request and response bodies are application/json unless noted.
Errors share the shape {"error": "<message>"} (the sole exception is
GET /healthz/db, which uses a db key).
List endpoints always emit their array ([], never null).
int64 identifiers and scores serialize as JSON numbers.
Dates are ISO calendar dates (YYYY-MM-DD); timestamps are RFC 3339.
Creates a Cognito user (auto-confirmed in dev) and best-effort captures profile fields into the legacy users row.
| email required | string <email> |
| password required | string <password> |
| username | string |
| first_name | string |
| last_name | string |
| state | string |
| dob | string <date> |
| ifpa_number | string |
{- "email": "user@example.com",
- "password": "pa$$word",
- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "state": "string",
- "dob": "2019-08-24",
- "ifpa_number": "string"
}{- "user_sub": "string",
- "confirmed": true,
- "confirmation_required": true,
- "profile_captured": true
}Authenticates via Cognito USER_PASSWORD_AUTH and returns the token bundle.
| email required | string <email> |
| password required | string <password> |
{- "email": "user@example.com",
- "password": "pa$$word"
}{- "access_token": "string",
- "id_token": "string",
- "refresh_token": "string",
- "token_type": "string",
- "expires_in": 0
}Triggers a Cognito reset code. Anti-enumeration — an unknown user still returns 202 with an empty body.
| email required | string <email> |
{- "email": "user@example.com"
}{- "delivery_medium": "string",
- "destination": "string"
}Completes the reset via Cognito ConfirmForgotPassword (code + new password).
| email required | string <email> |
| code required | string |
| password required | string <password> |
{- "email": "user@example.com",
- "code": "string",
- "password": "pa$$word"
}{- "error": "string"
}The /me claims plus Cognito GetUser attributes and best-effort legacy users-row fields.
{- "sub": "string",
- "username": "string",
- "client_id": "string",
- "scopes": [
- "string"
], - "groups": [
- "string"
], - "email": "string",
- "email_verified": true,
- "first_name": "string",
- "last_name": "string",
- "state": "string",
- "dob": "2019-08-24",
- "ifpa_number": "string"
}Updates legacy users-row fields. All fields optional; an omitted field is left unchanged, a present field (including empty string) sets the value. At least one field must be supplied. Requires the DB store.
| username | string |
| first_name | string |
| last_name | string |
| state | string |
| dob | string <date> Validated as YYYY-MM-DD when non-empty |
| ifpa_number | string |
{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "state": "string",
- "dob": "2019-08-24",
- "ifpa_number": "string"
}{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "state": "string",
- "dob": "2019-08-24",
- "ifpa_number": "string"
}{- "id": 0,
- "title": "string",
- "score": 0,
- "end_on": "2019-08-24",
- "rules": "string",
- "prize": "string",
- "prize_photo": "string",
- "at_contest_limit": true,
- "game": {
- "id": 0,
- "name": "string",
- "manufacturer": "string"
}
}Submits a proof-photo high-score entry. Runs six ordered validations (at-limit, photo freshness, score-vs-target, 24h duplicate, demographics, terms) in one transaction.
| id required | integer <int64> >= 1 |
| score required | integer <int64> |
| photo required | string Object key of the proof photo |
| agreed_to_terms required | boolean |
| latitude | number <double> |
| longitude | number <double> |
| time_offset | integer Client tz offset in whole minutes (JS getTimezoneOffset convention) |
required | object |
{- "score": 0,
- "photo": "string",
- "agreed_to_terms": true,
- "latitude": 0.1,
- "longitude": 0.1,
- "time_offset": 0,
- "demographics": {
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zip": "string",
- "sex": "string",
- "birthdate": "2019-08-24"
}
}{- "id": 0,
- "contest_id": 0,
- "score": 0,
- "remaining_entries": 0,
- "created_at": "2019-08-24T14:15:22Z"
}Resolves the contest's game and queries pinballmap.com for nearby venues. A pinballmap failure degrades to an empty list with source_unavailable: true (never a 500).
| id required | integer <int64> >= 1 |
{- "game": {
- "id": 0,
- "name": "string",
- "manufacturer": "string"
}, - "disclaimer": "string",
- "source_unavailable": true,
- "locations": [
- {
- "id": 0,
- "name": "string",
- "street": "string",
- "city": "string",
- "state": "string",
- "zip": "string"
}
]
}| id required | integer <int64> >= 1 |
{- "location_id": 0,
- "current_seasons": [
- {
- "id": 0,
- "name": "string",
- "virtual": true,
- "max_players": 0,
- "games_per_week": 0,
- "drop_lowest": 0,
- "weeks": 0,
- "meet_time": "string",
- "rules": "string",
- "start_on": "2019-08-24",
- "end_on": "2019-08-24",
- "signed_up": true,
- "meets": [
- {
- "id": 0,
- "at": "2019-08-24T14:15:22Z",
- "checkin_open": true,
- "complete": true,
- "checked_in": true
}
]
}
], - "past_seasons": [
- {
- "id": 0,
- "name": "string",
- "virtual": true,
- "max_players": 0,
- "games_per_week": 0,
- "drop_lowest": 0,
- "weeks": 0,
- "meet_time": "string",
- "rules": "string",
- "start_on": "2019-08-24",
- "end_on": "2019-08-24",
- "signed_up": true,
- "meets": [
- {
- "id": 0,
- "at": "2019-08-24T14:15:22Z",
- "checkin_open": true,
- "complete": true,
- "checked_in": true
}
]
}
]
}{- "season_id": 0,
- "name": "string",
- "drop_lowest": 0,
- "rows": [
- {
- "signup_id": 0,
- "user_id": 0,
- "name": "string",
- "total": 0,
- "adjusted": 0,
- "weeks": [
- {
- "meet_id": 0,
- "points": 0,
- "struck": true
}
]
}
]
}Returns the groups→rounds→players→scores tree for a non-virtual meet, or a standings table for a virtual meet. The shape is discriminated by virtual.
| seasonID required | integer <int64> >= 1 |
| meetID required | integer <int64> >= 1 |
{- "meet_id": 0,
- "season_id": 0,
- "at": "2019-08-24T14:15:22Z",
- "checkin_open": true,
- "complete": true,
- "virtual": false,
- "ungrouped": [
- {
- "checkin_id": 0,
- "signup_id": 0,
- "user_id": 0,
- "name": "string",
- "group_id": 0,
- "position": 0,
- "points": 0,
- "scores": [
- {
- "id": 0,
- "round_id": 0,
- "score": 0,
- "points": 0
}
]
}
], - "groups": [
- {
- "id": 0,
- "number": 0,
- "players": [
- {
- "checkin_id": 0,
- "signup_id": 0,
- "user_id": 0,
- "name": "string",
- "group_id": 0,
- "position": 0,
- "points": 0,
- "scores": [
- {
- "id": 0,
- "round_id": 0,
- "score": 0,
- "points": 0
}
]
}
], - "rounds": [
- {
- "id": 0,
- "number": 0,
- "game_id": 0
}
]
}
]
}Idempotent. Returns 201 on a new signup, 200 if already signed up.
| id required | integer <int64> >= 1 |
| accepted_terms | boolean Must be true to sign up |
{- "accepted_terms": true
}{- "season_id": 0,
- "signup_id": 0,
- "user_id": 0,
- "name": "string",
- "signed_up": true
}Restricted to players signed up for the season. Idempotent — 201 on a new check-in, 200 if already checked in.
| seasonID required | integer <int64> >= 1 |
| meetID required | integer <int64> >= 1 |
{- "meet_id": 0,
- "season_id": 0,
- "checkin_id": 0,
- "signup_id": 0,
- "checked_in": true
}{- "upcoming": [
- {
- "id": 0,
- "name": "string",
- "at": "2019-08-24T14:15:22Z",
- "format": {
- "name": "string",
- "strikes": 0
}, - "full": true,
- "results": [
- {
- "place": 0,
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string"
}
]
}
], - "past": [
- {
- "id": 0,
- "name": "string",
- "at": "2019-08-24T14:15:22Z",
- "format": {
- "name": "string",
- "strikes": 0
}, - "full": true,
- "results": [
- {
- "place": 0,
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string"
}
]
}
]
}{- "upcoming": [
- {
- "id": 0,
- "name": "string",
- "at": "2019-08-24T14:15:22Z",
- "format": {
- "name": "string",
- "strikes": 0
}, - "full": true,
- "results": [
- {
- "place": 0,
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string"
}
]
}
], - "past": [
- {
- "id": 0,
- "name": "string",
- "at": "2019-08-24T14:15:22Z",
- "format": {
- "name": "string",
- "strikes": 0
}, - "full": true,
- "results": [
- {
- "place": 0,
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string"
}
]
}
]
}{- "id": 0,
- "name": "string",
- "at": "2019-08-24T14:15:22Z",
- "format": {
- "name": "string",
- "strikes": 0
}, - "full": true,
- "finished": true,
- "results": [
- {
- "place": 0,
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string"
}
], - "signups": [
- {
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string",
- "place": 0
}
], - "rounds": [
- {
- "id": 0,
- "number": 0,
- "groups": [
- {
- "id": 0,
- "number": 0
}
]
}
], - "ungrouped": [
- {
- "id": 0,
- "number": 0
}
]
}Fields default from the player's profile; the body may override them.
| id required | integer <int64> >= 1 |
| first_name | string |
| last_name | string |
| ifpa_number | string |
{- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string"
}{- "tournament_id": 0,
- "signup_id": 0,
- "user_id": 0,
- "first_name": "string",
- "last_name": "string",
- "ifpa_number": "string",
- "place": 0,
- "signed_up": true
}| id required | integer <int64> >= 1 |
{- "quests": [
- {
- "id": 0,
- "name": "string",
- "global": true,
- "location_id": 0,
- "instructions": "string",
- "end_on": "2019-08-24",
- "foreground_color": "string",
- "background_color": "string",
- "rewards": {
- "questbux": 0,
- "wizard_points": 0,
- "physical_prize": "string",
- "prize_type": "string"
}, - "embarkment_state": "none",
- "goals_with_progress": [
- {
- "id": 0,
- "name": "string",
- "position": 0,
- "target_score": 0,
- "complete": true
}
]
}
]
}{- "id": 0,
- "name": "string",
- "global": true,
- "location_id": 0,
- "instructions": "string",
- "end_on": "2019-08-24",
- "foreground_color": "string",
- "background_color": "string",
- "rewards": {
- "questbux": 0,
- "wizard_points": 0,
- "physical_prize": "string",
- "prize_type": "string"
}, - "embarkment_state": "none",
- "goals_with_progress": [
- {
- "id": 0,
- "name": "string",
- "position": 0,
- "target_score": 0,
- "complete": true
}
]
}Idempotent first-or-create. 201 creates the embarkment, 200 returns the existing one.
| id required | integer <int64> >= 1 |
{- "id": 0,
- "quest_id": 0,
- "state": "none",
- "id_pin": "string",
- "wizard_points": 0
}Scoped to the owning caller — another user's embarkment reports 404.
| id required | integer <int64> >= 1 |
{- "id": 0,
- "quest_id": 0,
- "state": "none",
- "id_pin": "string",
- "rewards": {
- "questbux": 0,
- "wizard_points": 0,
- "physical_prize": "string",
- "prize_type": "string"
}
}Submits a score against a goal with a proof photo. Runs the anti-cheat validation gate (photo freshness, GPS radius, 24h duplicate, score target) and advances the embarkment state machine in one transaction.
| id required | integer <int64> >= 1 |
| photo_key required | string |
| score | integer <int64> |
| latitude | number <double> |
| longitude | number <double> |
| time_offset | integer Whole minutes (JS getTimezoneOffset convention) |
| user_agent | string Accepted for parity; not persisted |
{- "photo_key": "string",
- "score": 0,
- "latitude": 0.1,
- "longitude": 0.1,
- "time_offset": 0,
- "user_agent": "string"
}{- "mission_id": 0,
- "goal_id": 0,
- "goal_complete": true,
- "quest_complete": true,
- "embarkment": {
- "id": 0,
- "quest_id": 0,
- "state": "none",
- "id_pin": "string",
- "wizard_points": 0
}, - "earned_questbux": 0,
- "earned_wizard_points": 0,
- "old_wizard_level": {
- "title": "string",
- "sublevel": 0
}, - "new_wizard_level": {
- "title": "string",
- "sublevel": 0
}
}Lists active venues carrying the goal's game. Optional lat/lng attaches per-venue distance and orders nearest-first.
| id required | integer <int64> >= 1 |
| lat | number <double> [ -90 .. 90 ] Must be supplied together with lng |
| lng | number <double> [ -180 .. 180 ] Must be supplied together with lat |
{- "locations": [
- {
- "id": 0,
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zip": "string",
- "latitude": 0.1,
- "longitude": 0.1,
- "distance_miles": 0.1,
- "quest_count": 0
}
]
}| id required | integer <int64> >= 1 |
| month | string^\d{4}-\d{2}$ YYYY-MM; defaults to the current month |
{- "month": "2026-07",
- "games": [
- {
- "game_id": 0,
- "name": "string",
- "scores": [
- {
- "rank": 0,
- "username": "string",
- "score": 0
}
]
}
]
}Records a monthly high score with anti-cheat validation (proof-photo freshness, 24h duplicate, GPS radius).
| id required | integer <int64> >= 1 |
| game_id required | integer <int64> |
| score required | integer <int64> >= 0 |
| photo required | string Object key from POST /uploads |
| latitude | number <double> |
| longitude | number <double> |
| time_offset | integer |
{- "game_id": 0,
- "score": 0,
- "photo": "string",
- "latitude": 0.1,
- "longitude": 0.1,
- "time_offset": 0
}{- "id": 0,
- "game_id": 0,
- "location_id": 0,
- "score": 0,
- "month": "2026-07",
- "created_at": "2019-08-24T14:15:22Z"
}| page | integer >= 1 Default: 1 |
| state | string |
| location_id | integer <int64> >= 1 |
| timeframe | string Default: "all_time" Enum: "all_time" "month" "week" |
{- "data": [
- {
- "rank": 0,
- "username": "string",
- "wizard_level": {
- "title": "string",
- "sublevel": 0
}, - "points": 0
}
], - "pagination": {
- "page": 0,
- "per_page": 30,
- "total": 0,
- "total_pages": 0
}
}Flags a machine at a location so staff are notified. The reporter is resolved from the token; any client-supplied user id is ignored. Rate-limited to 5 reports per user per location per hour.
| id required | integer <int64> >= 1 |
| machine_type required | string Enum: "pinball" "other" |
| description required | string |
| game_id | integer or null <int64> Required for machine_type=pinball |
| name | string Required for machine_type=other |
{- "machine_type": "pinball",
- "description": "string",
- "game_id": 0,
- "name": "string"
}{- "id": 0,
- "location_id": 0,
- "user_id": 0,
- "machine_type": "string",
- "game_id": 0,
- "name": "string",
- "description": "string",
- "created_at": "2019-08-24T14:15:22Z"
}| lat required | number <double> [ -90 .. 90 ] |
| lng required | number <double> [ -180 .. 180 ] |
| radius | number <double> ( 0 .. 500 ] Default: 25 Miles |
| query | string Case-insensitive name substring filter |
{- "locations": [
- {
- "id": 0,
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zip": "string",
- "latitude": 0.1,
- "longitude": 0.1,
- "distance_miles": 0.1,
- "quest_count": 0
}
]
}{- "id": 0,
- "name": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "zip": "string",
- "latitude": 0.1,
- "longitude": 0.1,
- "distance_miles": 0.1,
- "quest_count": 0,
- "notification": "string",
- "notification_hash": "string",
- "following": true,
- "wallet_balance": 0,
- "features": {
- "national": true,
- "has_tournaments": true,
- "has_league_seasons": true,
- "silverball_prizes_enabled": true,
- "redemption_prizes_enabled": true
}
}Returns a short-lived presigned PUT URL and a server-generated object key for direct-to-storage image uploads.
| content_type | string If set, must start with image/ |
| ext | string File extension appended to the key |
{- "content_type": "string",
- "ext": "string"
}{- "key": "string",
- "method": "PUT",
- "headers": {
- "property1": [
- "string"
], - "property2": [
- "string"
]
}, - "expires_in": 0
}| id required | integer <int64> >= 1 |
{- "wallet_balance": 0,
- "prizes": [
- {
- "id": 0,
- "name": "string",
- "cost": 0,
- "redeem_via": "mail",
- "image": "string",
- "global": true,
- "location_id": 0,
- "variants": [
- {
- "id": 0,
- "label": "string",
- "position": 0
}
]
}
]
}{- "id": 0,
- "name": "string",
- "cost": 0,
- "redeem_via": "mail",
- "image": "string",
- "global": true,
- "location_id": 0,
- "variants": [
- {
- "id": 0,
- "label": "string",
- "position": 0
}
]
}Branches on redeem_via — on_location validates a staff PIN + signature; mail validates the shipping address + variant. The cost is debited from the caller's wallet at the location.
| id required | integer <int64> >= 1 |
| prize_id required | integer <int64> |
| redeem_via required | string Enum: "on_location" "mail" |
| location_pin | string Required for on_location |
| location_signature | string Required for on_location |
| first_name | string Required for mail |
| last_name | string Required for mail |
| address | string Required for mail |
| address2 | string |
| city | string Required for mail |
| state | string Required for mail |
| zip | string Required for mail |
| variant_id | integer <int64> Required when the prize has variants (mail) |
{- "prize_id": 0,
- "redeem_via": "on_location",
- "location_pin": "string",
- "location_signature": "string",
- "first_name": "string",
- "last_name": "string",
- "address": "string",
- "address2": "string",
- "city": "string",
- "state": "string",
- "zip": "string",
- "variant_id": 0
}{- "purchase_id": 0,
- "remaining_balance": 0
}