Skip to content

Billing Endpoints

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.

{
"data": {
"balanceCents": 15000,
"currency": "USD",
"playtestMinutes": 270
},
"meta": {
"requestId": "req_abc123def456",
"timestamp": "2026-03-02T12:00:00.000Z"
}
}

GET /api/v1/billing/payments

Scope: billing

ParameterTypeDefaultDescription
limitinteger20Max 100
cursorstring—Pagination cursor
{
"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 /api/v1/billing/payments/:id

Scope: billing

{
"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"
}
}

GET /api/v1/billing/payments/:id/invoice

Scope: billing

Returns a PDF invoice for the given payment. The response is a binary PDF file.


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.

FieldTypeRequiredDescription
amountCentsintegerYesAmount in cents. Min 2000 ($20), max 1000000 ($10,000)
returnUrlstringNoURL to redirect to after checkout. Appends ?payment_id=...&status=success or ?status=cancelled
{
"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.


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.

FieldTypeRequiredDescription
amountCentsintegerYesAmount 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:

Terminal window
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:

Terminal window
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"
}
}

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.

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 /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.

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.

FieldTypeRequiredDescription
planKeystringYesindie_monthly, indie_yearly, studio_monthly or studio_yearly
{
"data": {
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_...",
"sessionId": "cs_...",
"subscriptionId": "sub-uuid",
"isPlanChange": false
}
}

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.

POST /api/v1/billing/subscription/resume

Scope: game_owner

Undoes a pending cancellation. 400 when the subscription is not scheduled to cancel.

GET /api/v1/billing/playtest-hours

Scope: billing

{
"data": { "balanceMinutes": 270, "balanceHours": 4.5 }
}

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"
}
]
}
}

Subscriptions emit subscription.created, subscription.renewed, subscription.cancelled and subscription.payment_failed — see Webhooks.