Skip to content

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-AppCheck header is ignored.
  • Faction-rep deltas are a TODO. POST /quest/complete applies points rewards only. Reputation deltas are authored in quest.rewards.reputation[] but the rep tier system is gated on phase 11c-2 (mechanics-designer). The endpoint returns pointsAwarded and records rewardGranted = true but does not touch faction_rep rows 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

FieldValue
MethodPOST
Path/quest/start
AuthInternal session JWT (Bearer)
IdempotentNo — calling twice returns 409
Rate limit30 req/min/walker

Request

Headers

HeaderRequiredNotes
AuthorizationyesBearer <internal-session-JWT>

Body

{
"questId": "quest.001-first-road"
}
FieldTypeConstraintsNotes
questIdstringnon-emptyMust match an id in @walkrpg/data quests[].

Validation order (fail-fast)

  1. Request body valid — Zod schema. → 400 VALIDATION_ERROR
  2. Quest existsquestId present in @walkrpg/data quests[]. → 400 QUEST_NOT_FOUND
  3. Walker existswalkerId resolves in Postgres. → 404 (internal)
  4. Not already started/completed — no QuestProgress row for (walkerId, questId). → 409 QUEST_ALREADY_IN_PROGRESS or QUEST_ALREADY_COMPLETED
  5. Prerequisites met — every id in quest.prerequisites[] has a completed QuestProgress row for this walker. → 422 PREREQUISITES_NOT_MET

Responses

200 OK

{
"questId": "quest.001-first-road",
"status": "in_progress",
"startedAt": "2026-05-19T10:00:00Z"
}
FieldTypeNotes
questIdstringEcho of the requested questId.
status"in_progress"Always in_progress on successful start.
startedAtISO 8601 UTCTimestamp 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

  1. Authenticate JWT → resolve walkerId.
  2. Validate request body via Zod. 400 on failure.
  3. Look up questId in @walkrpg/data quests[]. 400 QUEST_NOT_FOUND if absent.
  4. Load walker row. 404 if missing.
  5. Check for existing QuestProgress row. 409 if present.
  6. Load completed quest ids for the walker. Check prerequisites. 422 if any missing.
  7. Insert QuestProgress row (currentStepId = quest.steps[0].id, progress = {}).
  8. 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

FieldValue
MethodPOST
Path/quest/complete
AuthInternal session JWT (Bearer)
IdempotentNo — calling twice returns 409
Rate limit30 req/min/walker

Request

Headers

HeaderRequiredNotes
AuthorizationyesBearer <internal-session-JWT>

Body

{
"questId": "quest.001-first-road"
}
FieldTypeConstraintsNotes
questIdstringnon-emptyMust match a QuestProgress row in in_progress state.

Validation order (fail-fast)

  1. Request body valid — Zod schema. → 400 VALIDATION_ERROR
  2. Walker existswalkerId resolves in Postgres. → 404 (internal)
  3. In-progress row existsQuestProgress row found for (walkerId, questId) with completedAt = null. → 404 QUEST_NOT_IN_PROGRESS if missing entirely.
  4. Not already completedcompletedAt is null. → 409 QUEST_ALREADY_COMPLETED if 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
}
FieldTypeNotes
questIdstringEcho of the requested questId.
status"completed"Always completed on success.
startedAtISO 8601 UTCOriginal QuestProgress.startedAt.
completedAtISO 8601 UTCTimestamp set by this call.
pointsAwardedintegerquest.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

  1. Authenticate JWT → resolve walkerId.
  2. Validate request body via Zod. 400 on failure.
  3. Load walker row. 404 if missing.
  4. Look up QuestProgress by (walkerId, questId). 404 QUEST_NOT_IN_PROGRESS if absent.
  5. If completedAt is already set → 409 QUEST_ALREADY_COMPLETED.
  6. Resolve quest.rewards.points from @walkrpg/data.
  7. Execute single Prisma transaction:
    • Set QuestProgress.completedAt = now(), rewardGranted = true.
    • If pointsAwarded > 0: increment walker.availablePoints.
    • TODO (11c-2): apply quest.rewards.reputation[] deltas to FactionRep rows.
  8. Return 200 with { questId, status, startedAt, completedAt, pointsAwarded }.

Quest data reference

Both endpoints resolve quest metadata from @walkrpg/data quests[]:

questIdprerequisitesrewards.pointsregionId
quest.001-first-roadnone4region.plenny
quest.002-silent-seal-housequest.001-first-road6region.frostlands

Open follow-ups

  • (11c-2) Wire quest.rewards.reputation[] deltas in POST /quest/complete once the faction-rep tier system is authored by mechanics-designer.
  • (later) Add step-objective tracking (QuestProgress.currentStepId advancement) 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 separate quest_completions log table is the likely shape.