API: POST /quest/start and POST /quest/complete
Quest Progress Endpoints
Two endpoints manage a walker’s progress through quests: starting and completing. Quest data (ids, prerequisites, rewards, steps) is sourced from @walkrpg/data at boot; Postgres holds per-walker progress rows in quest_progress.
Status: draft contract — implementation in phase 11d-1.
Related: data/src/schemas/quest.ts, data/src/content/quests/, Prisma model QuestProgress, POST /tree/allocate (points from quest completion fund tree node allocation).
Test-phase mock mode
Both endpoints ship against the test-phase infrastructure per ADR-0006. Differences from the production contract:
- No App Check token requirement.
X-Firebase-AppCheckheader is ignored. - Faction-rep deltas are a TODO.
POST /quest/completeappliespointsrewards only. Reputation deltas are authored inquest.rewards.reputation[]but the rep tier system is gated on phase 11c-2 (mechanics-designer). The endpoint returnspointsAwardedand recordsrewardGranted = truebut does not touchfaction_reprows yet. - Quest completion is unconditional. In mock mode, any in-progress quest can be completed without verifying individual step objectives. Step-objective tracking is a production-phase concern.
POST /quest/start
Starts a quest for the authenticated walker. Validates that the quest exists, all prerequisites are satisfied, and the quest has not already been started or completed.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /quest/start |
| Auth | Internal session JWT (Bearer) |
| Idempotent | No — calling twice returns 409 |
| Rate limit | 30 req/min/walker |
Request
Headers
| Header | Required | Notes |
|---|---|---|
Authorization | yes | Bearer <internal-session-JWT> |
Body
{ "questId": "quest.001-first-road"}| Field | Type | Constraints | Notes |
|---|---|---|---|
questId | string | non-empty | Must match an id in @walkrpg/data quests[]. |
Validation order (fail-fast)
- Request body valid — Zod schema. → 400
VALIDATION_ERROR - Quest exists —
questIdpresent in@walkrpg/dataquests[]. → 400QUEST_NOT_FOUND - Walker exists —
walkerIdresolves in Postgres. → 404 (internal) - Not already started/completed — no
QuestProgressrow for(walkerId, questId). → 409QUEST_ALREADY_IN_PROGRESSorQUEST_ALREADY_COMPLETED - Prerequisites met — every id in
quest.prerequisites[]has a completedQuestProgressrow for this walker. → 422PREREQUISITES_NOT_MET
Responses
200 OK
{ "questId": "quest.001-first-road", "status": "in_progress", "startedAt": "2026-05-19T10:00:00Z"}| Field | Type | Notes |
|---|---|---|
questId | string | Echo of the requested questId. |
status | "in_progress" | Always in_progress on successful start. |
startedAt | ISO 8601 UTC | Timestamp of the created QuestProgress row. |
400 Bad Request
VALIDATION_ERROR (body fails schema):
{ "error": "VALIDATION_ERROR", "message": "Request body failed schema validation.", "details": { "fieldErrors": {}, "formErrors": ["Required"] }}QUEST_NOT_FOUND (questId not in data layer):
{ "error": "QUEST_NOT_FOUND", "message": "Quest 'quest.nonexistent-xyz' does not exist in the data layer.", "details": { "questId": "quest.nonexistent-xyz" }}401 Unauthorized
Session JWT missing or invalid.
409 Conflict
QUEST_ALREADY_IN_PROGRESS:
{ "error": "QUEST_ALREADY_IN_PROGRESS", "message": "Quest 'quest.001-first-road' is already in progress for this walker.", "details": { "questId": "quest.001-first-road", "startedAt": "2026-05-19T10:00:00Z" }}QUEST_ALREADY_COMPLETED:
{ "error": "QUEST_ALREADY_COMPLETED", "message": "Quest 'quest.001-first-road' is already completed by this walker.", "details": { "questId": "quest.001-first-road", "completedAt": "2026-05-19T12:00:00Z" }}422 Unprocessable Entity
{ "error": "PREREQUISITES_NOT_MET", "message": "Quest 'quest.002-silent-seal-house' has unmet prerequisite quests.", "details": { "questId": "quest.002-silent-seal-house", "missingPrerequisiteQuestIds": ["quest.001-first-road"] }}Server flow
- Authenticate JWT → resolve
walkerId. - Validate request body via Zod. 400 on failure.
- Look up
questIdin@walkrpg/dataquests[]. 400QUEST_NOT_FOUNDif absent. - Load walker row. 404 if missing.
- Check for existing
QuestProgressrow. 409 if present. - Load completed quest ids for the walker. Check prerequisites. 422 if any missing.
- Insert
QuestProgressrow (currentStepId=quest.steps[0].id,progress = {}). - Return 200 with
{ questId, status: "in_progress", startedAt }.
POST /quest/complete
Marks an in-progress quest as completed and grants the quest’s point reward to the walker’s availablePoints.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /quest/complete |
| Auth | Internal session JWT (Bearer) |
| Idempotent | No — calling twice returns 409 |
| Rate limit | 30 req/min/walker |
Request
Headers
| Header | Required | Notes |
|---|---|---|
Authorization | yes | Bearer <internal-session-JWT> |
Body
{ "questId": "quest.001-first-road"}| Field | Type | Constraints | Notes |
|---|---|---|---|
questId | string | non-empty | Must match a QuestProgress row in in_progress state. |
Validation order (fail-fast)
- Request body valid — Zod schema. → 400
VALIDATION_ERROR - Walker exists —
walkerIdresolves in Postgres. → 404 (internal) - In-progress row exists —
QuestProgressrow found for(walkerId, questId)withcompletedAt = null. → 404QUEST_NOT_IN_PROGRESSif missing entirely. - Not already completed —
completedAtis null. → 409QUEST_ALREADY_COMPLETEDif already set.
Responses
200 OK
{ "questId": "quest.001-first-road", "status": "completed", "startedAt": "2026-05-19T10:00:00Z", "completedAt": "2026-05-19T12:00:00Z", "pointsAwarded": 4}| Field | Type | Notes |
|---|---|---|
questId | string | Echo of the requested questId. |
status | "completed" | Always completed on success. |
startedAt | ISO 8601 UTC | Original QuestProgress.startedAt. |
completedAt | ISO 8601 UTC | Timestamp set by this call. |
pointsAwarded | integer | quest.rewards.points from the data layer (0 if not set). Added to walker.availablePoints. |
400 Bad Request
{ "error": "VALIDATION_ERROR", "message": "Request body failed schema validation.", "details": { "fieldErrors": {}, "formErrors": ["Required"] }}401 Unauthorized
Session JWT missing or invalid.
404 Not Found
{ "error": "QUEST_NOT_IN_PROGRESS", "message": "Quest 'quest.001-first-road' has not been started by this walker.", "details": { "questId": "quest.001-first-road" }}409 Conflict
{ "error": "QUEST_ALREADY_COMPLETED", "message": "Quest 'quest.001-first-road' is already completed by this walker.", "details": { "questId": "quest.001-first-road", "completedAt": "2026-05-19T12:00:00Z" }}Server flow
- Authenticate JWT → resolve
walkerId. - Validate request body via Zod. 400 on failure.
- Load walker row. 404 if missing.
- Look up
QuestProgressby(walkerId, questId). 404QUEST_NOT_IN_PROGRESSif absent. - If
completedAtis already set → 409QUEST_ALREADY_COMPLETED. - Resolve
quest.rewards.pointsfrom@walkrpg/data. - Execute single Prisma transaction:
- Set
QuestProgress.completedAt = now(),rewardGranted = true. - If
pointsAwarded > 0: incrementwalker.availablePoints. - TODO (11c-2): apply
quest.rewards.reputation[]deltas toFactionReprows.
- Set
- Return 200 with
{ questId, status, startedAt, completedAt, pointsAwarded }.
Quest data reference
Both endpoints resolve quest metadata from @walkrpg/data quests[]:
| questId | prerequisites | rewards.points | regionId |
|---|---|---|---|
quest.001-first-road | none | 4 | region.plenny |
quest.002-silent-seal-house | quest.001-first-road | 6 | region.frostlands |
Open follow-ups
- (11c-2) Wire
quest.rewards.reputation[]deltas inPOST /quest/completeonce the faction-rep tier system is authored by mechanics-designer. - (later) Add step-objective tracking (
QuestProgress.currentStepIdadvancement) gated on gameplay objective completion events. - (later)
GET /quest/state— read-only endpoint to list a walker’s quest progress rows. Not implemented in 11d-1. - (later) Repeatable quests (
quest.repeatable = true) need a different model — the current@@unique([walkerId, questId])constraint precludes re-runs. A separatequest_completionslog table is the likely shape.