Skip to content

Commerce & Wallet ​

Store products, the Play Coin wallet, and subscriptions. All endpoints require authentication.

Money is always integer cents. Play Coins are whole units.

Products ​

MethodPathPurpose
GET/productsList products
GET/products/featuredFeatured products
GET/products/:idOne product
POST/products/:id/buyBuy with Play Coins

The store sells profile customisation — avatars, avatar frames, banners, banner frames, profile backgrounds, themes. Buying creates an Order with OrderItem rows, an Inventory row for ownership, and a STORE_SPEND coin transaction, all in one database transaction. Too few coins returns 402 with code INSUFFICIENT_COINS, and nothing is recorded. Equipping an owned item is recorded as a UserAsset.

Wallet ​

MethodPathPurpose
GET/walletCoin balances by bucket
GET/wallet/transactionsThe caller's coin history, newest first (?cursor=&limit=, limit 1–100)
GET/me/coinsSpendable coins, as { balance }

Balances come from the coin ledger. GET /wallet:

json
{
  "purchased": 120, "received": 0, "earnedPending": 0, "earnedAvailable": 0,
  "debt": 0, "spendable": 120, "withdrawable": 0,
  "coinBalance": 120
}

coinBalance equals spendable, kept for clients that show a single number. spendable and withdrawable are 0 while debt is non-zero.

GET /wallet/transactions returns { items, nextCursor }. Each item is { id, type, createdAt, amount, buckets: [{ bucket, amount }] }, where amount is the net change to the caller's buckets. Pass nextCursor back as cursor for the next page.

Coin errors share codes across endpoints:

StatuscodeMeaning
402INSUFFICIENT_COINSSpendable coins don't cover it; data has needed and available
403COIN_DEBTSpending is on hold until a refund clawback is repaid
409IDEMPOTENCY_CONFLICTThe request key was already used for a different request

Wallet.coinBalance and User.playcoin_balance are legacy and no longer written.

Tips ​

MethodPathPurpose
GET/tips/configPresets and limits for the tip sheet
POST/tipsTip a post or a live stream (needs an Idempotency-Key header)
POST/tips/:id/hide-messageThe recipient hides a tip's message

POST /tips takes { targetType: "post" | "livestream", targetId, coins, message?, confirmed? } and an Idempotency-Key header: one per tip attempt, 8–128 characters of [A-Za-z0-9_-]. The same key with the same body replays the tip (200, replayed: true); with a different body it is 409. A new tip is 201 with { tip, replayed, wallet }. See Monetization for the rules.

StatuscodeMeaning
400TIP_AMOUNT_INVALIDOutside 1–10,000, or not a whole number
400TIP_CONFIRMATION_REQUIREDOver confirmAbove; resend with confirmed: true
400TIP_MESSAGE_NOT_ALLOWEDdata.reason: too_long, contact_or_link, control_characters, blocked_term
400TIP_SELFYour own post
400IDEMPOTENCY_KEY_REQUIREDMissing or malformed header
402INSUFFICIENT_COINSNot enough spendable coins
403BIRTHDATE_REQUIREDNo date of birth on file
403TIP_DAILY_LIMITOver the 24-hour cap; data has cap, sentToday, remaining
400LIVE_ENDEDThe live stream isn't on air
404—Post deleted or unpublished, stream missing, blocked either way, or recipient inactive
409IDEMPOTENCY_CONFLICTKey reused for a different tip
429TIP_RATE_LIMITEDToo many tips in the last minute

GET /wallet/transactions items for tips carry tip: { id, targetType, postId, liveStreamId, sender, recipient }.

Earnings (PlayPals) ​

MethodPathPurpose
GET/earningsBalances, rate, eligibility, upcoming releases, requests and payouts
POST/earnings/cashoutsRequest a cash-out { coins } (needs an Idempotency-Key header)
POST/earnings/cashouts/:id/cancelWithdraw a request not yet in a payout run

GET /earnings returns balances { available, pending, spendOnly, debt }, rate { centsPer1000, minCoins, stepCoins }, canCashOut, blockers (payout_account, verification, age), nextMaturityAt, upcoming [{ maturesAt, coins }], nextPayoutDate, cashouts and payouts. See Monetization.

StatuscodeMeaning
400CASHOUT_AMOUNT_INVALIDBelow the minimum, or not a multiple of the step
402INSUFFICIENT_COINSMore than EARNED_AVAILABLE
403CASHOUT_NOT_ELIGIBLEPayout account, verification, or age missing
403COIN_DEBTA refund debt is open
404CASHOUT_NOT_FOUNDNot yours, or doesn't exist
409CASHOUT_NOT_CANCELLABLEAlready scheduled, paid or returned

Subscriptions ​

MethodPathPurpose
GET/me/subscriptionYour platform subscription status
GET/me/vip-subscriptionsPlayPals whose VIP content you can see, and until when
POST/vip-subscriptionsRetired: 410 VIP_FLOW_RETIRED. Use /checkout/intents

Two kinds of subscription ​

Platform subscriptionCreator (VIP) subscription
ModelSubscriptionCreatorSubscription
EndpointGET /me/subscriptionGET /me/vip-subscriptions
MeaningPlayPalz premium tierAccess to one creator's channel
PriceFixed tiersThe creator's monthlyPrice
SourceRevenueCat webhookVerified 30-day VIP passes (VipPass); see Monetization

Each /me/vip-subscriptions row has source: "pass" (access ends at expiresAt) or "revenuecat_legacy" (an old auto-renewing subscription, active until the store expires it).

Checkout (VIP passes) ​

MethodPathPurpose
GET/price-tiers?kind=VIP_PASSPrice tiers of a kind, with offered
POST/checkout/intentsStart a checkout: { "playpalId": "…", "kind": "VIP_PASS" } → 201 { checkout }
GET/checkout/intents/:idPoll a checkout (yours only)
POST/checkout/intents/:id/completeReport the store purchase: { "transactionId": "…" }
GET/me/purchase-creditsUnused credits, soonest-expiring first

checkout is { intentId, kind, playpalId, productId, status, expiresAt, accessUntil }, with status open | fulfilled | expired, and paidWithCredit on creation (an unused credit for the product was applied, so there is nothing to buy).

The app sets the RevenueCat subscriber attribute checkout_intent_id to intentId before purchaseStoreProduct(productId), then calls complete. complete answers result: fulfilled, credited (kept as a credit), or pending (the store or RevenueCat hasn't confirmed it yet, or sync is off; the webhook will apply it, so poll the checkout).

StatusCodeWhen
403MEMBERSHIP_REQUIREDNo Pro or Elite membership
403BLOCKEDA block exists either way
400SELF_PURCHASEYour own pass
404PLAYPAL_NOT_FOUNDNot an active PlayPal
409TIER_UNAVAILABLEThe PlayPal's tier isn't on sale yet
403PURCHASE_NOT_YOURScomplete with someone else's transaction
400PRODUCT_MISMATCHcomplete with a purchase of another product
409PURCHASE_REFUNDEDcomplete with a refunded purchase
403SANDBOX_NOT_ALLOWEDA sandbox purchase where they're refused
404CHECKOUT_NOT_FOUNDNot yours, or doesn't exist

What a subscription unlocks ​

An active CreatorSubscription is what canAccessRoom checks for vip rooms:

ts
const sub = await prisma.creatorSubscription.findFirst({
  where: {
    fanId: userId,
    creatorId: room.channel.userId,
    status: "active",
    OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
  },
});

Channel membership alone is not enough.

Buying coins ​

Coins are a consumable in-app purchase. This API never takes payment; it credits purchases the store has verified. See Monetization for the whole flow.

MethodPathPurpose
GET/coins/packagesPackages on sale, in display order
POST/coins/purchases/syncVerify and credit store transactions now

GET /coins/packages returns { packages: [{ productId, coins, bonusCoins, label, badge }] }. coins includes the bonus. There is no price: the app shows the store's localized price, and leaves out any package the store doesn't return.

POST /coins/purchases/sync takes { transactionIds: string[] } (1–20 store transaction ids, from the SDK) and returns { enabled, results: [{ transactionId, outcome }], wallet }. Outcomes:

outcome.statusMeaning
creditedVerified and credited now (coins)
already_creditedCredited before, by the webhook or an earlier sync
ignoredNot credited: reason is sandbox_not_allowed, unknown_product or bad_quantity
not_foundRevenueCat has no one-time purchase with exactly this id
not_yoursIt belongs to another customer
refundedIt was refunded
not_coinsIt isn't a coin product
unverifiedRevenueCat couldn't be reached; the webhook will credit it

With enabled: false (sync off, or the RevenueCat API isn't configured) nothing is checked, and the app waits for the webhook instead.

Adding a coin product: create it in App Store Connect and Play Console, then insert a CoinPackage row (offered = true to sell it). No code change.

Creator payouts ​

Not exposed on this API. Payouts are calculated and executed by anansi on a monthly cycle, behind its own admin API.

Internal documentation — PlayPalz platform