# WebSocket Contract

> Auto-generated from `server/src/contracts/websocket-commands.ts`.
> Do not edit manually — run `npm run contract:docs` in `server/`.

Generated: 2026-05-26T18:17:02.778Z

## Client → Server Commands

| Command | Envelope | Payload variants | Responses | Clients | Notes |
|---------|----------|------------------|-----------|---------|-------|
| `acknowledgeAchievement` | message-field | { achievementId: string } | — | ios, android | — |
| `applyForExtracurricular` | message-field | string (Legacy activity ID string)<br>{ activityId: string } | extracurricularApplied, playerObject, error | ios, android | — |
| `applyForJob` | message-field | string (Legacy job ID string)<br>{ jobId: string } | jobApplied, playerObject, error | ios, android | — |
| `breakUp` | message-field | { partnerId: string } | — | ios, android | Android historically sent partnerId at envelope root; server accepts message-field. |
| `characterSetup` | message-field | { name: string, age: number, sex: string } | playerObject, error | ios, android | — |
| `claimDailyReward` | message-field | { day: number } | dailyRewardClaimed | ios, android | — |
| `claimEvent` | message-field | { eventId: string, timestamp: string? } | — | ios, android | — |
| `claimQuestReward` | message-field | { questId: string } | questRewardClaimed | ios, android | — |
| `completeOnboarding` | message-field | empty | onboardingComplete, onboardingAlreadyComplete | ios, android | — |
| `conversation` | message-field | { characterID: string, response: string?, conversationEvent: string, cType: string? } | conversationEvent, conversationError, npcTyping | ios, android | — |
| `dateMiniGameResponse` | message-field | { gameId: string, round: number, responseId: string } | — | ios, android | — |
| `dateNight` | message-field | { ideaName: string } | — | ios, android | — |
| `debugGrant` | message-field | { mode: string?, money: number?, energy: number?, diamonds: number? } | debugGrantComplete, error | ios, android | dev-only |
| `debugSetup` | message-field | { preset: string } | debugSetupComplete, error | ios, android | dev-only |
| `deleteAccount` | message-field | { confirmation: string } | accountDeletionScheduled, accountDeleted | ios, android | — |
| `deviceToken` | message-field | string (Legacy push token string)<br>{ token: string } | — | ios, android | — |
| `divorce` | message-field | { partnerId: string } | — | ios, android | — |
| `eventResponse` | message-field | { eventId: string, choiceId: string } | playerObject, error | ios, android | — |
| `exportData` | top-level | { userId: string? } | dataExportComplete, dataExportReady | ios, android | — |
| `focusUpdate` | message-field | { activityId: string, newFocus: string } | focusUpdated, playerObject, error | ios, android | — |
| `getAchievements` | empty | empty | achievements | ios, android | — |
| `getDailyQuests` | empty | empty | dailyQuests | ios, android | — |
| `getDailyRewards` | empty | empty | dailyRewards | ios, android | — |
| `getDailyRewardState` | empty | empty | — | ios, android | — |
| `getDateIdeas` | empty | empty | dateIdeas | ios, android | — |
| `getEnergyRefillTiers` | empty | empty | energyRefillTiers | ios, android | — |
| `getExtraCurriculars` | message-field | empty<br>string (Legacy empty string) | extraCurriculars | ios, android | — |
| `getPlayerStatistics` | message-field | empty | playerStatistics | ios, android | — |
| `getSwipeCharacter` | message-field | empty<br>string (Preferred sex filter) | swipeCharacter | ios, android | — |
| `getTimeSkipTiers` | empty | empty | timeSkipTiers | ios, android | — |
| `init` | init-only | { userID: string } | — | ios, android | init-only |
| `liveActivityEnded` | message-field | { activityId: string? } | — | ios | — |
| `liveActivityToken` | message-field | { token: string, activityId: string? } | — | ios | — |
| `markConversationAsRead` | message-field | { conversationId: string } | — | ios, android | — |
| `marry` | message-field | { partnerId: string } | — | ios, android | — |
| `partnerGift` | message-field | string (Legacy partner/character ID string)<br>{ partnerId: string } | — | ios, android | — |
| `propose` | message-field | { partnerId: string } | — | ios, android | — |
| `purchaseEnergyRefill` | message-field | { refillType: string } | energyRefillComplete, error | ios, android | — |
| `purchaseInAppItem` | message-field | { productId: string, receiptData: string? } | inAppPurchaseComplete, error | ios, android | — |
| `purchaseItem` | message-field | string (Legacy item ID string)<br>{ itemId: string } | purchaseComplete, playerObject, error | ios, android | — |
| `purchaseTimeSkip` | message-field | { skipType: string } | timeSkipComplete, error | ios, android | — |
| `questionEvent` | message-field | { id: string, response: string } | — | ios, android | — |
| `quitExtracurricular` | message-field | string (Legacy activity ID string)<br>{ activityId: string } | extracurricularQuit, playerObject, error | ios, android | — |
| `quitHabit` | message-field | string (Legacy habit name)<br>{ habitId: string }<br>{ habitId: string?, habit: string? } | habitQuitting, playerObject, error | ios, android | — |
| `quitJob` | message-field | string (Legacy job ID string)<br>{ jobId: string } | jobQuit, playerObject, error | ios, android | — |
| `relationshipEventResponse` | top-level | { eventId: string, choiceId: string } | — | ios, android | Android sends eventId/choiceId at envelope root; iOS uses message-field wrapper. |
| `resetSpeed` | message-field | empty | — | ios, android | — |
| `restart` | command-alias | empty | — | ios, android | — |
| `retrievePerson` | message-field | string (Legacy person ID string)<br>{ personId: string } | personObject, error | ios, android | — |
| `romance` | message-field | { partnerId: string } | — | ios, android | — |
| `speed` | message-field | number (Game speed value)<br>string (Speed delta (+/-) or numeric string) | — | ios, android | — |
| `start` | command-alias | empty | — | ios, android | — |
| `startDate` | message-field | { activityId: string, partnerId: string } | — | ios, android | activityId must be the date activity ID, not display name. |
| `stop` | command-alias | empty | — | ios, android | — |
| `stopQuitHabit` | message-field | string (Legacy habit name)<br>{ habitId: string }<br>{ habitId: string?, habit: string? } | habitQuitStopped, playerObject, error | ios, android | — |
| `swipeMatch` | message-field | string (Matched character ID)<br>empty | — | ios, android | — |
| `tooltipSeen` | message-field | { tooltipId: string } | — | ios, android | — |
| `tutorialStepComplete` | message-field | { step: number } | tutorialStepUpdated | ios, android | — |

## Envelope Types

- **message-field**: `{ type, message: payload }` — canonical
- **command-alias**: `{ type: "command", message: "start"|"stop"|"restart" }`
- **top-level**: extra fields at envelope root (legacy Android)
- **empty**: `{ type }` only, no message body
- **init-only**: `{ type: "init", userID }` handled before registry dispatch
