Playtests Endpoints
These endpoints are for game owners managing playtests for their games. All require the game_owner scope.
List Playtests for a Game
Section titled “List Playtests for a Game”GET /api/v1/games/:gameId/playtests
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max 100 |
cursor | string | — | Pagination cursor |
Response
Section titled “Response”{ "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 }}Create Playtest Request
Section titled “Create Playtest Request”POST /api/v1/games/:gameId/playtests
Order playtests for a game.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
visibility | string | No | public or private (default: private) |
quantity | integer | No | Number of slots to create, 1-100 (default: 1) |
durationMinutes | integer | No | 30, 60, 120, or 180 (default: 60) |
playerCount | integer | No | Players per session, 1-8 (default: 1) |
notesForTesters | string | No | Instructions for playtesters (max 5000 chars). Only visible after claiming a slot. |
keysForTesters | string[] | No | Game keys distributed one per slot. Each playtester sees only their assigned key after claiming. Keys are cleared from the request after distribution. |
isFreePublicTrial | boolean | No | Use free public trial (one per game, singleplayer 30min only) |
targetingType | string | No | new (new playtesters only) or past (returning playtesters) |
pastPlaytesterScope | string | No | Required when targetingType is past: any or specific |
targetedPlaytesterId | string | No | Required when pastPlaytesterScope is specific |
selectionMode | string | No | automatic (default — first eligible playtester claims instantly) or manual (see below) |
qualifications | string | No | Free-form requirements shown to applicants in both selection modes (max 2000 chars). Example: "Playtester must be over 55 and own a Quest 3 headset." |
Selection mode
Section titled “Selection mode”automatic(default) — The first eligible playtester to claim a slot gets it instantly. Slots are created inopenstatus. Approved playtesters have 24h (singleplayer) or 72h (multiplayer) to submit.manual— Playtesters apply with aqualificationsResponseinstead of claiming directly. Slots are created inopenstatus (manual mode is a request-level setting, not a slot status). The game owner reviews the pool of applications and approves up toquantityof them. Approval atomically flips one open slot toreservedfor 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 withtargetingType: past+pastPlaytesterScope: specific(only one possible applicant) orisFreePublicTrial: true.
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" }'const response = await fetch( `https://app.weplaytestgames.com/api/v1/games/${gameId}/playtests`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ visibility: 'private', quantity: 3, notesForTesters: 'Focus on the tutorial and first boss fight', }), },);Response (Paid Playtest)
Section titled “Response (Paid Playtest)”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" }}Response (Free Public Trial)
Section titled “Response (Free Public Trial)”Free trials are immediately active and require admin approval:
{ "data": { "playtest": { "..." }, "slots": [{ "id": "slot-uuid", "status": "open", "..." }], "needsAdminApproval": true }, "meta": { "..." }}Get Playtest Details
Section titled “Get Playtest Details”GET /api/v1/playtests/:id
Returns details for a specific playtest request with slot statistics.
Response
Section titled “Response”{ "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" }}Update Playtest
Section titled “Update Playtest”PATCH /api/v1/playtests/:id
Update an active playtest request.
Request Body
Section titled “Request Body”| Field | Type | Description |
|---|---|---|
notesForTesters | string | Updated instructions for playtesters (max 5000 chars). Only visible after claiming a slot. |
Response
Section titled “Response”Returns the updated playtest request object.
List Slots for a Playtest
Section titled “List Slots for a Playtest”GET /api/v1/playtests/:id/slots
Returns all slots for a playtest request with their current status (cursor-paginated).
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max 100 |
cursor | string | — | Pagination cursor |
Response
Section titled “Response”{ "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!" }—scoreis an integer,commentis a string or null - rejection:
{ "reason": "Video was too short", "createdAt": "2026-02-16T10:00:00.000Z" }
Get Slot Details
Section titled “Get Slot Details”GET /api/v1/playtests/slots/:id
Returns full details for a specific slot including game info, submission, rating, rejection, and transcription.
Response
Section titled “Response”{ "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"]}Accept Submission
Section titled “Accept Submission”POST /api/v1/playtests/slots/:id/accept
Accept a submitted playtest. The playtester earns their payout.
No request body required.
Response
Section titled “Response”{ "data": { "message": "Submission accepted" }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z" }}Reject Submission
Section titled “Reject Submission”POST /api/v1/playtests/slots/:id/reject
Reject a submitted playtest with an optional reason.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Explanation for the rejection (max 5000 chars) |
allowRetry | boolean | No | Allow the playtester to resubmit (default: false) |
Response
Section titled “Response”{ "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".
Manual-Selection Applications
Section titled “Manual-Selection Applications”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.
List Applications
Section titled “List Applications”GET /api/v1/playtests/:id/applications
Returns the pool of applications for a manual-selection playtest plus slot counts.
Response
Section titled “Response”{ "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 inassignedSlotId(nowreserved).rejected— you rejected them directly.auto_rejected— rejected automatically because another applicant filled the last slot.withdrawn— the playtester withdrew before review.
Approve Application
Section titled “Approve Application”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.
Response
Section titled “Response”{ "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.
Reject Application
Section titled “Reject Application”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).
Request Body (optional)
Section titled “Request Body (optional)”| Field | Type | Required | Description |
|---|---|---|---|
customMessage | string | No | Optional 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”. |
Response
Section titled “Response”{ "data": { "application": { "id": "app-uuid", "status": "rejected", "reviewedAt": "...", "...": "..." } }, "meta": { "...": "..." }}Get Download URL
Section titled “Get Download URL”GET /api/v1/playtests/slots/:id/download-url
Returns a pre-signed URL to download the playtest video. URL expires in 1 hour.
Response
Section titled “Response”{ "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 Transcript
Section titled “Get Transcript”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.
Response
Section titled “Response”{ "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.
List All Submissions
Section titled “List All Submissions”GET /api/v1/playtests/submissions
List submissions across all your games (cursor-paginated). Defaults to showing submitted status.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max 100 |
cursor | string | — | Pagination cursor |
status | string | submitted | Filter by slot status: open, reserved, submitted, accepted, rejected, blocked, expired, cancelled, publishing |
Response
Section titled “Response”{ "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 }}Dashboard Stats
Section titled “Dashboard Stats”GET /api/v1/playtests/dashboard/stats
Get summary statistics for the game owner dashboard.
Response
Section titled “Response”{ "data": { "totalGames": 3, "activePlaytests": 5, "pendingReviews": 2 }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z" }}