Skip to main content

Auth/session contracts

  • Native auth uses Clerk after a session exists.
  • Protected feature calls require authenticated user context and must handle session expiry gracefully.
  • Mobile OAuth can return a one-time 128-char hex MobileSession token. Prefer the URL fragment (#token=) over the query string.
  • Native stashes the token in module memory (not MMKV), then:
    1. POST /api/mobile/exchange with { token } (or Authorization: Bearer <token>). No Clerk session required.
    2. Receive { ticket, expiresAt }.
    3. signIn.create({ strategy: 'ticket', ticket }) then setActive({ session }).
  • A second exchange of the same token fails. If exchange returns 404/401, leave the user on login.
  • After a Clerk session exists, login can also use Google, Apple, email/password, and MFA. Those are Clerk strategies, not extra API routes.
  • GET /api/user/check-username?username= is the public handle-availability check.

Quest and reward API contracts

  • Quest progression is backend-authoritative for eligibility and completion.
  • Daily quiz sessions use /api/v1/engage/quests/{questId}/session, /session/{sessionId}/submit, and /session/{sessionId}/forfeit.
  • Arena check-in is POST /api/v1/engage/quests/arena-check-in (once per team per UTC day).
  • Reward claim/redeem behavior must use server responses as truth for user-facing state.
  • Pass-related gating (global/event) is evaluated server-side and reflected in client rendering.

Fan Rep contracts

  • GET /api/v1/engage/rep/breakdown returns Fan/Club/Player/Direct totals plus clubRepByTeam. dsportsDirectRep sums repType = Direct rows.
  • GET /api/v1/engage/rep/ledger is keyset-paginated. source is fanAmp | club | playerAmp | dsportsDirect.
  • POST /api/v1/engage/rep/earn accepts only reaction, message, pollVote, and share. Amount and repType are server-authoritative. Direct kinds return 403.
  • Do not call /earn for arena check-in, daily-quest completion, or admin Direct grants.
  • Direct grants use POST /api/admin/users/{userId}/rep (isAdmin only). Native Coins UI for this is follow-up.

Team and league API contracts

  • Team lists and team metadata come from /api/teams. Use ?slug= for a single team, or public GET /api/teams/{id} when you have an id or slug (id is tried first). Archived teams return 404.
  • GET /api/teams/search accepts q, sport, league, cursor, limit, and sort (rank | name | fans).
  • Sports chips come from public GET /api/config/sports. On 500, fall back to a local cache. Do not hardcode a second catalog.
  • Team sport and leagueInfo.sport are never null. Unknown values are "other".
  • POST /api/teams/{id}/follow is social only (TeamFollow).
  • POST/DELETE /api/teams/{id}/join is leaderboard membership only. Both are idempotent.
  • HQ stars are bookmarks, not follows. GET /api/me/starred-teams lists them. PUT and DELETE /api/me/starred-teams/{teamId} star and unstar. Optional PUT body { slug?, name?, logo? } stores a display snapshot. A new star past 50 clubs returns 409 STARRED_TEAMS_LIMIT. POST /api/me/starred-teams is the one-time additive merge (skipped and rejected, not a hard failure).
  • GET /api/user/teams returns { joinedTeamIds, followedTeamIds }.
  • GET /api/teams/{id}/roster is public and returns { players } for the formation builder. Read optional photo and availability on each player. availability is omitted when the player is available.
  • League metadata and branding come from /api/leagues.
  • GET /api/leagues/{leagueId}/standings is public HQ standings. leagueId is the Prisma id or slug. The response is { rows, tiebreakRules, tables }. rows is one team each. tables lists every remaining scope. Empty rows mean the league cannot be scored honestly.
  • Call GET /api/teams/{id}/head-to-head?opponentId= for the public series. You get { record, meetings } for the five most recent decided meetings. Omit opponentId, or send the same id as {id}, and you get 400.
  • Synced canonical data changes can propagate asynchronously; client should tolerate short-lived staleness.

Binder PIN contract

  • Binder PIN is not the wallet PIN and not POST /api/v1/user/verify-pin.
  • POST /api/binders/{id}/verify-pin returns { granted, token, expiresIn: 300 }.
  • Send token as x-binder-pin-token on GET /api/binders/{id}.
  • Missing or expired proof on a PIN binder returns 403 PIN_REQUIRED with metadata only (no shelves).

Locker room authoring

  • teamId on POST /api/locker-room is the about-tag.
  • authorTeamId / postAsTeamId is who the post is authored as. Unauthorized callers get 403.
  • lineup on create is persisted and echoed. Fetch players from GET /api/teams/{id}/roster instead of fabricating names.
  • Composer drafts use GET/PUT/DELETE /api/locker-room/posts/draft.
  • POST /api/locker-room/{id}/like returns { liked, likeCount } and does not replace /react.
  • Comment create may send mentionedUserIds. Responses include mentions[]. Edit must send mentionedUserIds: [] to clear mentions; omitting the field leaves existing mentions in place.
  • For mention typeahead, call GET /api/user/search?q=...&scope=mention&limit=6. Read profileUrl for the avatar. handle is a non-empty string on that scope.
  • Tapping @handle opens that fan’s profile.

Marketplace contracts

  • Prefer GET /api/v1/marketplace/me/listings. GET /api/v1/marketplace/listings/mine is a native alias for the same handler. Create listings with x-listing-pin-token.
  • Listing a processing pack returns 409 PACK_NOT_LISTABLE.
  • GET /api/v1/cards/{id}/provenance is public ownership / sale / mint history.
  • Treat GET /api/user/packs status as the per-purchase source of truth.

Games catalog contract

  • GET /api/games/catalog?teamId=... (or ?teamSlug=...) is public and returns the team’s enabled mini-games grouped by cadence: { daily: Game[], weekly: Game[] }.
  • Each game entry carries id, key, title, description, icon, and badge; key is the stable slug the client maps to a playable game route (for example daily-spin).
  • The catalog is admin-managed and team-selected server-side; the client must render only the games returned and must not assume a fixed set.
  • Responses are cacheable (Cache-Control: public, max-age=60, stale-while-revalidate=300); short-lived staleness after a team changes its selection is expected.
  • Unknown teams return 404; a missing teamId/teamSlug returns 400.
  • Native also calls GET /api/v1/engage/games/catalog and joins the two lists. Engine session verbs, HUD rules, and update-required behavior live in Game Engine. Do not ignore unknown engine keys.
  • locker-singularity does not use session verbs. Read and mutate /api/v1/engage/games/locker-singularity/ledger. Send X-Client-Version. Mutations need a UUID idempotencyKey the client already holds. Send tap counts, never buzz values. Boost claim stays 410 BOOST_DISABLED.

Game Center contracts

  • GET /api/events/schedule accepts repeatable teamId query params.
  • GET /api/events/realtime is public Broadcast discovery. Event detail includes a realtime pointer without the anon key.
  • Use the public Game Center reads under /api/events/{gameId} for detail, box score, player-stats, plays, lineup, top-performer, and win-probability. Top-performer sides include playerKey (same key space as LineupSlot.playerKey) and playerId (linked roster id, or null).
  • Chat mute is POST/DELETE /api/events/{gameId}/chat/mute with either userId or thread: true, not both.
  • Ratings submit is POST /api/events/{gameId}/ratings. Do not also call /api/v1/engage/rep/earn.
  • Call POST/DELETE /api/events/{gameId}/live-activity to register or end an iOS Live Activity. You must have UserSettings.liveScoreUpdates consent first.

User mute and block

  • Global mute is GET/POST/DELETE /api/user/mutes. One-way hide; the muted user is not notified.
  • Global block is GET/POST/DELETE /api/user/blocks. Stronger than mute; excluded from the blocker’s social surfaces.
  • POST body uses mutedUserId / blockedUserId (or handle). DELETE accepts the same id as a query param or in a JSON body.
  • Do not confuse these with Game Center thread mute (/api/events/{gameId}/chat/mute).

Wallet contracts

  • Custodial create: POST /api/wallets with { pin, legalTermsVersion } only.
  • Import/link: same route with pin, address, and encryptedPrivateKey or privateKey. pin is required on every create path.
  • Custodial send: PIN gate, then POST /api/wallets/{address}/sign-transaction.
  • Seed reveal: PIN-gated POST /api/wallets/{address}/export.
  • Do not call deprecated POST /api/wallets/private-key for new work.
  • Attestation is GET /api/wallets/{address}/attestation. The path segment is the wallet database id, not the on-chain address.
  • DELETE /api/wallets needs { address, pin } for custodial wallets. External links need { address } only. Missing or wrong PIN returns 400 VALIDATION_ERROR.
  • GET /api/user/featured-collectibles is public.
  • PUT /api/user/featured-collectibles replaces the authenticated user’s showcase (max 12).

User interests contract

  • User.interests is a string[] of slug IDs stored on the User row in Postgres (via Prisma).
  • Allowed slugs: hockey, basketball, football, soccer, baseball, esports, gamification, stats, fan_engagement, collectibles, gambling, crypto, blockchain, fantasy_sports, live_events, merch_fashion, streaming.
  • Native onboarding step 3 and the retroactive /(onboarding)/interests screen require at least one interest before continue.
  • PATCH /api/user accepts interests: string[]; server validates slugs and dedupes (max 20).
  • GET /api/user returns interests on the authenticated profile payload.
  • Retroactive gate: users with onboardingComplete=true and empty interests are redirected to /(onboarding)/interests until at least one interest is saved.

Settings and notifications

  • GET/PATCH /api/user/settings is the settings SoT. privateProfile mirrors isPrivate.
  • Persist notifyTeams, notifyMilestones, and notifyCommunity as separate booleans. Do not collapse them.
  • Inbox types include GAME_ALERT and RATING.

Error handling and fallback UX

  • Show user-actionable errors for purchase/open/claim/sign failures.
  • Avoid silent failures for mutation endpoints.
  • Use retry/backoff for transient network failures; avoid retry storms on authorization or validation errors.