Billing Endpoints
Get Credit Balance
Section titled “Get Credit Balance”GET /api/v1/billing/balance
Scope: billing
Returns the account credit balance (dollars) and, separately, the subscription playtest hours balance in minutes (1 playtest hour = 60 minutes). See Subscriptions below.
Response
Section titled “Response”{ "data": { "balanceCents": 15000, "currency": "USD", "playtestMinutes": 270 }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z" }}List Payment History
Section titled “List Payment History”GET /api/v1/billing/payments
Scope: billing
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max 100 |
cursor | string | — | Pagination cursor |
Response
Section titled “Response”{ "data": { "payments": [ { "id": "pay-uuid", "amountCents": 10000, "status": "completed", "provider": "stripe", "externalId": "cs_abc123", "createdAt": "2026-02-15T10:00:00.000Z" } ] }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z", "hasMore": false }}Get Payment Details
Section titled “Get Payment Details”GET /api/v1/billing/payments/:id
Scope: billing
Response
Section titled “Response”{ "data": { "payment": { "id": "pay-uuid", "amountCents": 10000, "status": "completed", "provider": "stripe", "externalId": "cs_abc123", "createdAt": "2026-02-15T10:00:00.000Z" } }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z" }}Download Invoice
Section titled “Download Invoice”GET /api/v1/billing/payments/:id/invoice
Scope: billing
Returns a PDF invoice for the given payment. The response is a binary PDF file.
Purchase Credit (Stripe)
Section titled “Purchase Credit (Stripe)”POST /api/v1/billing/credit
Scope: game_owner
Creates a Stripe Checkout session to top up account credit. The response includes a checkoutUrl to redirect the user to for payment.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
amountCents | integer | Yes | Amount in cents. Min 2000 ($20), max 1000000 ($10,000) |
returnUrl | string | No | URL to redirect to after checkout. Appends ?payment_id=...&status=success or ?status=cancelled |
Response
Section titled “Response”{ "data": { "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_...", "paymentId": "pay-uuid", "amountCents": 10000 }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z" }}Redirect the user to checkoutUrl to complete payment. On success, credit is added automatically to their account.
Purchase Credit (x402 Crypto)
Section titled “Purchase Credit (x402 Crypto)”POST /api/v1/billing/credit/x402
Scope: game_owner
Top up account credit using crypto via the x402 payment protocol (USDC, ETH, or SOL). This is a two-step flow.
See the x402 Payments guide for full details on the x402 protocol.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
amountCents | integer | Yes | Amount in USD cents. Min 500 ($5), max 1000000 ($10,000) |
Step 1 — Get payment instructions (402 response)
Section titled “Step 1 — Get payment instructions (402 response)”Send the request with the X-Payment-Method: x402 header and your preferred chain/currency:
curl -X POST https://app.weplaytestgames.com/api/v1/billing/credit/x402 \ -H "Authorization: Bearer wpg_sk_..." \ -H "Content-Type: application/json" \ -H "X-Payment-Method: x402" \ -H "X-Payment-Chain: base" \ -H "X-Payment-Currency: USDC" \ -d '{ "amountCents": 5000 }'Returns 402 Payment Required with payment details:
{ "error": { "code": "PAYMENT_REQUIRED", "message": "Send 50.00 USDC on base to complete this purchase", "payment": { "id": "x402-payment-uuid", "chain": "base", "currency": "USDC", "cryptoAmount": "50.00", "receiver": "0xABC...", "network": "mainnet", "expiresAt": "2026-03-02T12:15:00.000Z" } }}The X-Payment response header contains the full payment instructions in x402 format.
Step 2 — Submit transaction hash (confirmation)
Section titled “Step 2 — Submit transaction hash (confirmation)”After broadcasting the transaction, re-send the request with an X-Payment header containing a JSON object with your transaction hash, chain, and currency. No request body is needed — the amount is looked up from the pending payment created in Step 1:
curl -X POST https://app.weplaytestgames.com/api/v1/billing/credit/x402 \ -H "Authorization: Bearer wpg_sk_..." \ -H 'X-Payment: {"txHash":"0xabc123...","chain":"base","currency":"USDC"}'Returns 200 on success:
{ "data": { "message": "Payment confirmed", "paymentId": "pay-uuid", "creditedCents": 5000 }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-03-02T12:00:00.000Z" }}Subscriptions
Section titled “Subscriptions”The Indie ($79/month or $790/year) and Studio ($379/month or $3,790/year) plans add playtest hours to the account on every successful payment: 5 / 25 hours a month, or 60 / 300 hours up front on the yearly plans. Hours are stored in minutes (1 playtest hour = 60 minutes), roll over and never expire.
A playtest costs durationMinutes × playerCount × quantity minutes — a 2-player 2-hour playtest is 4 playtest hours. When the balance covers a whole order, POST /api/v1/games/:id/playtests pays with hours automatically and responds { "paid": true, "paidWithPlaytestHours": true, ... } (see Playtests). Otherwise the order is paid with money as usual and the hours stay untouched; the response carries playtestMinutesNeeded and playtestMinutesAvailable so you can explain why. Hours are never combined with money, account credit or discount codes on one order.
Subscriptions are billed by Stripe. Checkout, plan changes and the Customer Portal need a human in a browser; everything else is available here.
List Plans
Section titled “List Plans”GET /api/v1/billing/plans
Scope: billing
{ "data": { "plans": [ { "key": "indie_monthly", "name": "Indie", "displayName": "Indie", "tier": "indie", "interval": "month", "priceCents": 7900, "minutesPerCycle": 300, "hoursPerCycle": 5, "pricePerHourCents": 1580 }, { "key": "indie_yearly", "name": "Indie", "displayName": "Indie Yearly", "tier": "indie", "interval": "year", "priceCents": 79000, "minutesPerCycle": 3600, "hoursPerCycle": 60, "pricePerHourCents": 1317 }, { "key": "studio_monthly", "name": "Studio", "displayName": "Studio", "tier": "studio", "interval": "month", "priceCents": 37900, "minutesPerCycle": 1500, "hoursPerCycle": 25, "pricePerHourCents": 1516 }, { "key": "studio_yearly", "name": "Studio", "displayName": "Studio Yearly", "tier": "studio", "interval": "year", "priceCents": 379000, "minutesPerCycle": 18000, "hoursPerCycle": 300, "pricePerHourCents": 1263 } ], "configured": true }}Get Subscription
Section titled “Get Subscription”GET /api/v1/billing/subscription
Scope: billing
subscription is null when the account is not subscribed. nextRenewalAt is set while the plan renews; endsAt is set once a cancellation is scheduled.
{ "data": { "subscription": { "id": "sub-uuid", "planKey": "indie_monthly", "planName": "Indie", "tier": "indie", "interval": "month", "priceCents": 7900, "minutesPerCycle": 300, "hoursPerCycle": 5, "status": "active", "currentPeriodStart": "2026-09-01T00:00:00.000Z", "currentPeriodEnd": "2026-10-01T00:00:00.000Z", "cancelAtPeriodEnd": false, "cancelledAt": null, "endedAt": null, "endedReason": null, "createdAt": "2026-09-01T00:00:00.000Z" }, "plan": { "key": "indie_monthly", "name": "Indie", "displayName": "Indie", "tier": "indie", "interval": "month", "priceCents": 7900, "minutesPerCycle": 300, "hoursPerCycle": 5, "pricePerHourCents": 1580 }, "playtestMinutes": 270, "playtestHours": 4.5, "nextRenewalAt": "2026-10-01T00:00:00.000Z", "endsAt": null, "cancelAtPeriodEnd": false }}status is one of incomplete (checkout started, not paid), active, past_due (a renewal failed and Stripe is retrying) or cancelled.
Start Subscription Checkout
Section titled “Start Subscription Checkout”POST /api/v1/billing/subscription/checkout
Scope: game_owner
Creates a Stripe Checkout session for a plan. Open checkoutUrl in a browser to pay; the hours are added when the payment succeeds. With an active subscription on a different plan this is a plan change: the current plan ends the moment the new plan’s payment succeeds, the new plan’s full hours are added, and there is no proration. Subscribing to the plan you are already on returns 400. Returns 503 when subscriptions are not configured on the server.
| Field | Type | Required | Description |
|---|---|---|---|
planKey | string | Yes | indie_monthly, indie_yearly, studio_monthly or studio_yearly |
{ "data": { "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_...", "sessionId": "cs_...", "subscriptionId": "sub-uuid", "isPlanChange": false }}Cancel Subscription
Section titled “Cancel Subscription”POST /api/v1/billing/subscription/cancel
Scope: game_owner
Cancels at the end of the current billing period: no further charges and no further hours after that. Playtest hours already granted stay spendable. Returns the updated subscription (cancelAtPeriodEnd: true). 400 when there is no active subscription.
Resume Subscription
Section titled “Resume Subscription”POST /api/v1/billing/subscription/resume
Scope: game_owner
Undoes a pending cancellation. 400 when the subscription is not scheduled to cancel.
Get Playtest Hours
Section titled “Get Playtest Hours”GET /api/v1/billing/playtest-hours
Scope: billing
{ "data": { "balanceMinutes": 270, "balanceHours": 4.5 }}Playtest Hours History
Section titled “Playtest Hours History”GET /api/v1/billing/playtest-hours/history
Scope: billing
The ledger, newest first. Positive entries are grants (invoice:…), refunds (slot_refund:…, manual_slot_expired:…) and adjustments (admin_grant:…); negative entries are spends (playtest:…) and clawbacks (chargeback:…).
{ "data": { "entries": [ { "id": "entry-uuid", "amountMinutes": -120, "source": "playtest:pt-uuid", "note": "1x 120min playtest — My Game", "subscriptionId": null, "paymentId": null, "playtestRequestId": "pt-uuid", "createdAt": "2026-09-03T10:00:00.000Z" }, { "id": "entry-uuid-2", "amountMinutes": 300, "source": "invoice:in_...", "note": "Indie plan · subscription started", "subscriptionId": "sub-uuid", "paymentId": "pay-uuid", "playtestRequestId": null, "createdAt": "2026-09-01T00:00:00.000Z" } ] }}Webhook events
Section titled “Webhook events”Subscriptions emit subscription.created, subscription.renewed, subscription.cancelled and subscription.payment_failed — see Webhooks.