Skip to content

Playtests Endpoints

These endpoints are for game owners managing playtests for their games. All require the game_owner scope.

GET /api/v1/games/:gameId/playtests

ParameterTypeDefaultDescription
limitinteger20Max 100
cursorstringPagination cursor
{
"data": {
"playtests": [
{
"id": "pt-uuid",
"gameId": "game-uuid",
"visibility": "private",
"quantity": 3,
"durationMinutes": 60,
"playerCount": 1,
"costPerSlotCents": 2000,
"payoutCents": 1000,
"notesForTesters": "Focus on the tutorial flow",
"status": "active",
"isFreePublicTrial": false,
"targetingType": null,
"createdAt": "2026-02-01T10:00:00.000Z",
"slots": {
"open": 1,
"reserved": 1,
"submitted": 1,
"accepted": 0,
"rejected": 0,
"expired": 0
}
}
],
"eligibleForFreeTrial": true
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z",
"cursor": "eyJpZCI6ImFiYzEyMyJ9",
"hasMore": false
}
}

POST /api/v1/games/:gameId/playtests

Order playtests for a game.

FieldTypeRequiredDescription
visibilitystringNopublic or private (default: private)
quantityintegerNoNumber of slots to create, 1-100 (default: 1)
durationMinutesintegerNo30, 60, 120, or 180 (default: 60)
playerCountintegerNoPlayers per session, 1-8 (default: 1)
notesForTestersstringNoInstructions for playtesters (max 5000 chars). Only visible after claiming a slot.
keysForTestersstring[]NoGame keys distributed one per slot. Each playtester sees only their assigned key after claiming. Keys are cleared from the request after distribution.
isFreePublicTrialbooleanNoUse free public trial (one per game, singleplayer 30min only)
targetingTypestringNonew (new playtesters only) or past (returning playtesters)
pastPlaytesterScopestringNoRequired when targetingType is past: any or specific
targetedPlaytesterIdstringNoRequired when pastPlaytesterScope is specific
selectionModestringNoautomatic (default — first eligible playtester claims instantly) or manual (see below)
qualificationsstringNoFree-form requirements shown to applicants in both selection modes (max 2000 chars). Example: "Playtester must be over 55 and own a Quest 3 headset."
  • automatic (default) — The first eligible playtester to claim a slot gets it instantly. Slots are created in open status. Approved playtesters have 24h (singleplayer) or 72h (multiplayer) to submit.
  • manual — Playtesters apply with a qualificationsResponse instead of claiming directly. Slots are created in open status (manual mode is a request-level setting, not a slot status). The game owner reviews the pool of applications and approves up to quantity of them. Approval atomically flips one open slot to reserved for the approved playtester. Approved applicants get 72h to submit regardless of player count. Unfilled slots after a 7-day review window expire and the cost is credited back to the owner’s account. Manual cannot be combined with targetingType: past + pastPlaytesterScope: specific (only one possible applicant) or isFreePublicTrial: true.
Terminal window
curl -X POST https://app.weplaytestgames.com/api/v1/games/game-uuid/playtests \
-H "Authorization: Bearer wpg_sk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"visibility": "private",
"quantity": 3,
"notesForTesters": "Focus on the tutorial and first boss fight"
}'

Paid playtests return with requiresPayment: true and pricing info:

{
"data": {
"playtest": {
"id": "pt-uuid",
"gameId": "game-uuid",
"visibility": "private",
"quantity": 3,
"durationMinutes": 60,
"playerCount": 1,
"costPerSlotCents": 2000,
"payoutCents": 1000,
"notesForTesters": "Focus on the tutorial and first boss fight",
"status": "pending_payment",
"isFreePublicTrial": false,
"selectionMode": "automatic",
"qualifications": null,
"applicationsExpireAt": null,
"createdAt": "2026-03-02T12:00:00.000Z"
},
"requiresPayment": true,
"costPerSlotCents": 2000,
"totalCents": 6000
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

Free trials are immediately active and require admin approval:

{
"data": {
"playtest": { "..." },
"slots": [{ "id": "slot-uuid", "status": "open", "..." }],
"needsAdminApproval": true
},
"meta": { "..." }
}

GET /api/v1/playtests/:id

Returns details for a specific playtest request with slot statistics.

{
"data": {
"playtest": {
"id": "pt-uuid",
"gameId": "game-uuid",
"gameName": "Dungeon Crawlers",
"visibility": "private",
"quantity": 3,
"durationMinutes": 60,
"playerCount": 1,
"costPerSlotCents": 2000,
"payoutCents": 1000,
"notesForTesters": "Focus on the tutorial",
"status": "active",
"isFreePublicTrial": false,
"targetingType": null,
"createdAt": "2026-02-01T10:00:00.000Z",
"slots": {
"open": 1,
"reserved": 1,
"submitted": 1,
"accepted": 0,
"rejected": 0,
"expired": 0
}
}
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

PATCH /api/v1/playtests/:id

Update an active playtest request.

FieldTypeDescription
notesForTestersstringUpdated instructions for playtesters (max 5000 chars). Only visible after claiming a slot.

Returns the updated playtest request object.


GET /api/v1/playtests/:id/slots

Returns all slots for a playtest request with their current status (cursor-paginated).

ParameterTypeDefaultDescription
limitinteger20Max 100
cursorstringPagination cursor
{
"data": {
"slots": [
{
"id": "slot-uuid",
"requestId": "pt-uuid",
"status": "submitted",
"deadlineAt": "2026-02-16T14:00:00.000Z",
"createdAt": "2026-02-15T14:00:00.000Z",
"playtester": {
"id": "playtester-uuid",
"displayName": "TestGamer42"
},
"submission": {
"id": "sub-uuid",
"videoFileSize": 524288000,
"notes": "Found a bug in level 3",
"createdAt": "2026-02-15T18:30:00.000Z"
},
"rating": null,
"rejection": null,
"transcription": {
"id": "trans-uuid",
"status": "approved",
"selectedVersion": "claude"
}
}
]
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z",
"hasMore": false
}
}

When present, rating and rejection have these structures:

  • rating: { "score": 5, "comment": "Great feedback!" }score is an integer, comment is a string or null
  • rejection: { "reason": "Video was too short", "createdAt": "2026-02-16T10:00:00.000Z" }

GET /api/v1/playtests/slots/:id

Returns full details for a specific slot including game info, submission, rating, rejection, and transcription.

{
"data": {
"slot": {
"id": "slot-uuid",
"requestId": "pt-uuid",
"status": "submitted",
"deadlineAt": "2026-02-16T14:00:00.000Z",
"createdAt": "2026-02-15T14:00:00.000Z",
"game": {
"id": "game-uuid",
"name": "Dungeon Crawlers"
},
"playtest": {
"id": "pt-uuid",
"visibility": "private",
"durationMinutes": 60,
"playerCount": 1
},
"playtester": {
"id": "playtester-uuid",
"displayName": "TestGamer42"
},
"submission": {
"id": "sub-uuid",
"videoFileSize": 524288000,
"notes": "Found a bug in level 3",
"createdAt": "2026-02-15T18:30:00.000Z"
},
"rating": null,
"rejection": null,
"transcription": null
}
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

The transcription object in slot detail responses includes an additional keyTakeaways field compared to the list view:

"transcription": {
"id": "trans-uuid",
"status": "approved",
"selectedVersion": "claude",
"keyTakeaways": ["Bug found in level 3", "Tutorial was confusing"]
}

POST /api/v1/playtests/slots/:id/accept

Accept a submitted playtest. The playtester earns their payout.

No request body required.

{
"data": {
"message": "Submission accepted"
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

POST /api/v1/playtests/slots/:id/reject

Reject a submitted playtest with an optional reason.

FieldTypeRequiredDescription
reasonstringNoExplanation for the rejection (max 5000 chars)
allowRetrybooleanNoAllow the playtester to resubmit (default: false)
{
"data": {
"message": "Submission rejected"
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

When allowRetry is true, a new slot is created in reserved status for the same playtester with a fresh 24-hour deadline. The response message will be "Submission rejected - playtester can try again".


For playtests created with selectionMode: "manual", playtesters apply instead of claiming slots directly. Applications live at the playtest-request level (a pool), not per-slot — this lets you approve any 5 applicants from a pool of 30 without being constrained by slot assignment.

Slots on a manual-selection playtest start (and stay) in open status until you approve an applicant — manual mode is a property of the request, not a slot status. When you approve an application, the next available open slot atomically flips to reserved and the applicant gets 72 hours to submit. When all slots are filled, any remaining pending applications are auto-rejected.

Unfilled slots after the 7-day review window expire automatically. The cost of those slots is credited back to your account.

GET /api/v1/playtests/:id/applications

Returns the pool of applications for a manual-selection playtest plus slot counts.

{
"data": {
"request": {
"id": "pt-uuid",
"gameId": "game-uuid",
"gameName": "Dungeon Crawlers",
"quantity": 3,
"qualifications": "Playtester must be over 55 and own a Quest 3.",
"selectionMode": "manual",
"applicationsExpireAt": "2026-03-09T12:00:00.000Z",
"scheduledAt": null
},
"counts": {
"approvedCount": 1,
"pendingCount": 12,
"slotsRemaining": 2
},
"applications": [
{
"id": "app-uuid",
"status": "pending",
"qualificationsResponse": "I'm 58 and have a Quest 3 — I've played 200 hours of similar titles.",
"createdAt": "2026-03-02T14:30:00.000Z",
"reviewedAt": null,
"assignedSlotId": null,
"playtesterId": "playtester-uuid",
"displayName": "TestGamer42",
"gamingExperience": "VR enthusiast, big into roguelites",
"validatedAt": "2026-01-12T00:00:00.000Z",
"ratingAverage": "4.80",
"acceptedCount": "47"
}
]
},
"meta": { "requestId": "req_...", "timestamp": "..." }
}

Application status values:

  • pending — waiting for your review.
  • approved — assigned to the slot in assignedSlotId (now reserved).
  • rejected — you rejected them directly.
  • auto_rejected — rejected automatically because another applicant filled the last slot.
  • withdrawn — the playtester withdrew before review.

POST /api/v1/playtests/playtest-applications/:id/approve

Atomically flips the next open slot on the request to reserved for this applicant and starts their 72-hour submission deadline. No request body.

{
"data": {
"application": { "id": "app-uuid", "status": "approved", "assignedSlotId": "slot-uuid", "...": "..." },
"assignedSlotId": "slot-uuid",
"slotsRemaining": 1
},
"meta": { "...": "..." }
}

When slotsRemaining reaches 0, all remaining pending applications on the same playtest are marked auto_rejected and those playtesters are notified. The slot referenced by assignedSlotId is now reserved and the playtester can submit like any other reserved playtest.

Errors:

  • 409 Conflict — all slots are already filled, or the application is no longer pending.
  • 400 Bad Request — the playtest is not manual selection.

POST /api/v1/playtests/playtest-applications/:id/reject

Rejects a single pending application without touching slots or other applicants. The rejected playtester is notified and cannot re-apply to this playtest (the uniqueness constraint on (requestId, playtesterId) blocks it).

FieldTypeRequiredDescription
customMessagestringNoOptional personal note included verbatim in the rejection email (max 1500 chars). Use it to leave feedback like “you were close, we’ll consider you next round”.
{
"data": {
"application": { "id": "app-uuid", "status": "rejected", "reviewedAt": "...", "...": "..." }
},
"meta": { "...": "..." }
}

GET /api/v1/playtests/slots/:id/download-url

Returns a pre-signed URL to download the playtest video. URL expires in 1 hour.

{
"data": {
"downloadUrl": "https://storage.example.com/videos/...",
"expiresAt": "2026-03-02T13:00:00.000Z"
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

GET /api/v1/playtests/slots/:id/transcript

Returns the AI-generated transcript for a submission. Returns a pre-signed download URL for the transcript file.

{
"data": {
"downloadUrl": "https://storage.example.com/transcripts/...",
"filename": "transcript-claude.txt",
"version": "claude",
"keyTakeaways": ["Bug found in level 3", "Tutorial was confusing"]
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

Returns 404 if the transcription is not yet available or still processing.


GET /api/v1/playtests/submissions

List submissions across all your games (cursor-paginated). Defaults to showing submitted status.

ParameterTypeDefaultDescription
limitinteger20Max 100
cursorstringPagination cursor
statusstringsubmittedFilter by slot status: open, reserved, submitted, accepted, rejected, blocked, expired, cancelled, publishing
{
"data": {
"submissions": [
{
"slot": {
"id": "slot-uuid",
"requestId": "pt-uuid",
"status": "submitted",
"reservedBy": "playtester-uuid",
"deadlineAt": "2026-02-16T14:00:00.000Z",
"createdAt": "2026-02-15T14:00:00.000Z"
},
"submission": {
"id": "sub-uuid",
"videoStorageKey": "submissions/...",
"videoFileSize": 524288000,
"notes": "Found a bug in level 3",
"createdAt": "2026-02-15T18:30:00.000Z"
},
"game": {
"id": "game-uuid",
"name": "Dungeon Crawlers"
},
"playtest": {
"id": "pt-uuid",
"visibility": "private",
"durationMinutes": 60,
"playerCount": 1,
"isFreePublicTrial": false
},
"playtester": {
"id": "playtester-uuid",
"displayName": "TestGamer42"
},
"awaitingAdminApproval": false,
"blockedReport": null,
"transcription": null
}
]
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z",
"hasMore": false
}
}

GET /api/v1/playtests/dashboard/stats

Get summary statistics for the game owner dashboard.

{
"data": {
"totalGames": 3,
"activePlaytests": 5,
"pendingReviews": 2
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}