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 COIN_SPEND_STORE ledger entry. Equipping an owned item is recorded as a UserAsset.

Wallet

MethodPathPurpose
GET/walletBalances and, where implemented, ledger history
GET/me/coinsCoin balance

Wallet carries two balances:

FieldUnit
coinBalanceWhole Play Coins
balanceUSD cents (creator redeemable balance)

Two coin balances exist

User.playcoin_balance duplicates Wallet.coinBalance with nothing keeping them in sync. Wallet is backed by the Ledger and is the one to trust. Check which column a given code path reads before debugging a "wrong balance" report.

Every balance change should have a matching Ledger row carrying a unique idempotencyKey. See Monetization.

Subscriptions

MethodPathPurpose
GET/me/subscriptionYour platform subscription status
GET/me/vip-subscriptionsCreators you subscribe to
POST/vip-subscriptionsRecord a creator subscription after purchase

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 webhookRevenueCat webhook plus the client call below

POST /vip-subscriptions

json
{ "vipUserId": "clx…", "productId": "vip_9_99" }

Returns 201.

This exists to work around a gap in the webhook: RevenueCat tells the server that a VIP product was purchased but not which creator it was for, because the target is a customer attribute the handler does not read. So the mobile client calls this endpoint immediately after a successful purchase.

A purchase can be lost

If the app is backgrounded or killed between the store confirming the purchase and this call completing, the fan has paid and the subscription is not recorded. Resolving vip_user_id from the RevenueCat subscriber attributes inside the webhook would remove the dependency on the client. Partial

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 bought as a consumable in-app purchase, not through this API. The flow is:

Mobile → RevenueCat SDK → App Store / Play → RevenueCat
       → POST /api/v1/webhooks/revenuecat → creditCoins() → Ledger COIN_TOPUP

Product-id-to-coin-amount mapping lives in COIN_PRODUCT_MAP (apps/api/src/modules/revenuecat/RevenueCatTypes.ts). Adding a coin SKU means editing that map.

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