# We Playtest Games API — Complete Reference > Marketplace connecting indie game developers with playtesters. > Cost varies by duration and player count: 30min=$15, 1hr=$20, 2hr=$35, 3hr=$50 base (singleplayer). > Multiplayer adds per-extra-player surcharge. Playtesters earn $10/hr per player. Max 8 players per session. ## Base URL https://app.weplaytestgames.com/api/v1/ All endpoints served over HTTPS only. ## Authentication Bearer token API keys in the Authorization header: Authorization: Bearer wpg_sk_a1b2c3d4e5f67890abcdef1234567890 Key format: wpg_sk_{32 random hex characters} Create keys at: https://app.weplaytestgames.com/dashboard/settings/api-keys ### Scopes Keys have scoped permissions: - game_owner: Games, playtests, submissions, slots, dashboard stats - billing: Read-only financial data (balance, payments, invoices) - chat: Conversations and messages - notifications: Notification listing - webhooks: Webhook management (requires game_owner scope too) ## Response Format ### Success { "data": { ... }, "meta": { "requestId": "req_abc123", "timestamp": "2026-03-02T12:00:00Z" } } ### List Responses { "data": [...], "meta": { "requestId": "req_abc123", "timestamp": "2026-03-02T12:00:00Z", "cursor": "eyJpZCI6ImFiYzEyMyJ9", "hasMore": true, "totalCount": 42 } } ### Errors { "error": { "code": "VALIDATION_ERROR", "message": "Invalid email format", "details": [ { "field": "email", "message": "Must be a valid email address" } ] }, "meta": { "requestId": "req_abc123", "timestamp": "2026-03-02T12:00:00Z" } } ## Error Codes UNAUTHORIZED (401) - Missing or invalid API key FORBIDDEN (403) - Valid key but insufficient scope EMAIL_NOT_VERIFIED (403) - Account email not verified NOT_FOUND (404) - Resource not found VALIDATION_ERROR (422) - Request body/params failed validation RATE_LIMITED (429) - Rate limit exceeded CONFLICT (409) - Duplicate resource or state conflict PAYMENT_REQUIRED (402) - Insufficient credit IDEMPOTENCY_MISMATCH (422) - Idempotency key reused with different request INTERNAL_ERROR (500) - Server error SERVICE_UNAVAILABLE (503) - Temporary outage ## Rate Limits Authenticated (per API key): - Global: 100 req/min (standard), 500 req/min (elevated) - Write endpoints: 30 req/min (standard), 150 req/min (elevated) - Billing endpoints: 10 req/min (standard), 30 req/min (elevated) Unauthenticated: - Registration: 5 req/15min per IP, 3 req/hour per email - Public endpoints: 60 req/min per IP Headers on every response: - X-RateLimit-Limit: max requests in window - X-RateLimit-Remaining: remaining requests - X-RateLimit-Reset: unix timestamp when window resets - Retry-After: seconds to wait (only on 429) ## Pagination Cursor-based. Query params: ?limit=20&cursor=... Default limit: 20. Maximum: 100. ## Idempotency POST/PATCH/DELETE accept Idempotency-Key header. Keys stored 24 hours. Same key + same request = cached response. Same key + different request = 422 IDEMPOTENCY_MISMATCH. --- ## ENDPOINTS ### Auth / Account POST /api/v1/auth/register Auth: none Body: { email, password, role: "game_owner", companyName? } Response: { data: { message, userId } } POST /api/v1/auth/register/api-key Auth: none Body: { email, password, role: "game_owner", companyName?, keyName? } Response: { data: { user: {...}, apiKey: "wpg_sk_..." } } Note: Key inactive until email verified. GET /api/v1/auth/me Auth: any scope Response: { data: { id, email, role, emailVerified, createdAt, profile: { displayName, companyName, websiteUrl, ... } } } PATCH /api/v1/auth/profile Auth: any scope Body: { displayName?, companyName?, websiteUrl? } (game_owner fields) Response: { data: { message: "Profile updated" } } POST /api/v1/auth/change-password Auth: any scope Body: { currentPassword, newPassword } ### Info GET /api/v1/info/categories Auth: any scope Response: { data: { categories: [{ id, name, slug }] } } GET /api/v1/info/devices Auth: any scope Response: { data: { devices: [{ id, name }] } } GET /api/v1/info/platforms Auth: any scope Response: { data: { platforms: [{ id, name }] } } ### Games GET /api/v1/games Auth: game_owner Query: limit?, cursor? Response: { data: [{ id, name, description, buildUrl, tags, createdAt }] } POST /api/v1/games Auth: game_owner Body: { name, description, buildUrl, tags: string[] } Response: { data: { id, name, description, buildUrl, tags, createdAt } } GET /api/v1/games/:id Auth: game_owner PATCH /api/v1/games/:id Auth: game_owner Body: { name?, description?, buildUrl?, tags? } GET /api/v1/games/:id/builds Auth: game_owner ### Playtests GET /api/v1/games/:gameId/playtests Auth: game_owner Query: limit?, cursor? POST /api/v1/games/:gameId/playtests Auth: game_owner Body: { visibility: "public"|"private", quantity: 1-100, durationMinutes: 30|60|120|180, playerCount?: 1-8, notesForTesters?, keysForTesters?: string[], isFreePublicTrial?: boolean, targetingType?: "new"|"past", pastPlaytesterScope?: "any"|"specific", targetedPlaytesterId? } Response: { data: { id, gameId, visibility, quantity, durationMinutes, playerCount, costPerSlotCents, payoutCents, notesForTesters, slotsOpen, status, requiresPayment, totalCents, createdAt } } Note: Cost per slot: 30min=$15, 1hr=$20, 2hr=$35, 3hr=$50 base + multiplayer surcharge per extra player (30min=$10, 1hr=$15, 2hr=$25, 3hr=$40). Payout: $10/hr per player. Max 8 players. Free trials: singleplayer 30min only. GET /api/v1/playtests/:id Auth: game_owner PATCH /api/v1/playtests/:id Auth: game_owner GET /api/v1/playtests/:id/slots Auth: game_owner Response: { data: [{ id, playtestId, status, reservedBy, reservedAt, deadlineAt, submittedAt }] } GET /api/v1/slots/:id Auth: game_owner POST /api/v1/slots/:id/accept Auth: game_owner Body: { rating?: 1-5, reason?: string } POST /api/v1/slots/:id/reject Auth: game_owner Body: { reason (required), rating?: 1-5, allowRetry?: boolean } GET /api/v1/slots/:id/download-url Auth: game_owner Response: { data: { downloadUrl, expiresAt } } GET /api/v1/slots/:id/transcript Auth: game_owner GET /api/v1/submissions Auth: game_owner Note: All submissions pending review across all games. GET /api/v1/dashboard/stats Auth: game_owner Response: { data: { totalGames, activePlaytests, pendingReviews, totalAccepted, totalRejected, creditBalanceCents } } ### Billing GET /api/v1/billing/balance Auth: billing Response: { data: { balanceCents, currency: "USD" } } GET /api/v1/billing/payments Auth: billing Query: limit?, cursor? GET /api/v1/billing/payments/:id Auth: billing GET /api/v1/billing/payments/:id/invoice Auth: billing Response: PDF file POST /api/v1/billing/credit Auth: game_owner Body: { amountCents: 2000-1000000, returnUrl? } Response: { data: { checkoutUrl, paymentId, amountCents } } POST /api/v1/billing/credit/x402 Auth: game_owner Body: { amountCents: 500-1000000 } Headers: X-Payment-Method: x402 (required for initial request) X-Payment-Chain: base|ethereum|solana (optional, default: base) X-Payment-Currency: USDC|ETH|SOL (optional, default: USDC) X-Payment: {"txHash":"0x...","chain":"base","currency":"USDC"} (for retry with tx) Valid combos: base(USDC,ETH), ethereum(USDC,ETH), solana(USDC,SOL) Initial: returns 402 with X-Payment header containing payment instructions Retry: verifies on-chain tx and credits account ### Chat GET /api/v1/chat/contacts Auth: chat GET /api/v1/chat/conversations/:id Auth: chat Query: limit? (default 50), cursor? POST /api/v1/chat/conversations/:id/messages Auth: chat Body: { content } GET /api/v1/chat/unread-count Auth: chat ### Notifications GET /api/v1/notifications Auth: notifications Query: limit?, cursor? POST /api/v1/notifications/mark-all-read Auth: notifications ### Webhooks GET /api/v1/webhooks Auth: webhooks + game_owner POST /api/v1/webhooks Auth: webhooks + game_owner Body: { url, events: string[] } Response includes secret (shown once). PATCH /api/v1/webhooks/:id Auth: webhooks + game_owner Body: { url?, events? } DELETE /api/v1/webhooks/:id Auth: webhooks + game_owner GET /api/v1/webhooks/:id/deliveries Auth: webhooks + game_owner POST /api/v1/webhooks/:id/test Auth: webhooks + game_owner POST /api/v1/webhooks/:id/enable Auth: webhooks + game_owner Note: Re-enable after auto-disable (10 consecutive failures). POST /api/v1/webhooks/:id/rotate-secret Auth: webhooks + game_owner Response: { data: { secret } } ### Public (no auth) GET /api/v1/public/transcripts/:id Note: Only approved transcripts. GET /api/v1/public/playtester/:id --- ## Webhook Events slot.reserved - Playtester claimed a slot slot.submitted - Video submitted slot.accepted - Submission accepted slot.rejected - Submission rejected slot.expired - Deadline expired payout.completed - Payout sent payout.failed - Payout failed payment.completed - Payment succeeded payment.failed - Payment failed chat.message - New message Signature: HMAC-SHA256 of "{timestamp}.{body}" with webhook secret. Headers: X-WPG-Signature, X-WPG-Timestamp, X-WPG-Signature-Version: v1 Retry: exponential backoff (1m, 5m, 30m, 2h, 12h). Auto-disable after 10 failures. ## x402 Crypto Payments Supported chains and currencies: base: USDC, ETH (default chain — cheapest gas, fastest confirmation) ethereum: USDC, ETH solana: USDC, SOL Headers: X-Payment-Method: x402 — opt into x402 payment (initial request) X-Payment-Chain: base|ethereum|solana — preferred chain (optional, default: base) X-Payment-Currency: USDC|ETH|SOL — preferred currency (optional, default: USDC) X-Payment: {"txHash":"0x...","chain":"base","currency":"USDC"} — retry with tx proof Flow: 1. Send request with X-Payment-Method: x402 (+ optional chain/currency headers) 2. Server returns 402 with X-Payment header containing payment instructions (JSON) 3. Client pays on-chain, gets tx hash 4. Client retries same request with X-Payment header containing tx hash JSON 5. Server verifies on-chain and processes (activates playtest or credits account) Applies to: - POST /api/v1/games/:gameId/playtests (per-playtest payment) - POST /api/v1/billing/credit/x402 (credit top-up) Overpayment: excess is added as account credit. Reconciliation: background job checks every 5 minutes for pending tx verification. --- Resources: - Docs: https://weplaytestgames.com/docs/api - OpenAPI: https://weplaytestgames.com/openapi/spec.yaml - MCP Server: npx @weplaytestgames/mcp