Skip to main content

Subscriptions

Beta

Subscriptions are in beta. The API, payload shapes, and Developer Console UI may change without notice. Contact the Jest team before shipping a subscription to production, and keep an eye on What's new for breaking changes.

The Jest platform allows developers to sell recurring subscriptions to their games using the payments SDK methods. Subscriptions complement one-off product purchases and are a good fit for things like ad removal, premium tiers, or access to additional content.

As with one-off purchases, Jest handles the entire checkout and recurring billing with the player via popular digital wallets (Apple Pay, Google Wallet, card on file). Your game is responsible for reading the player's current entitlement and unlocking the relevant features.

Subscriptions on Jest
Subscriptions on Jest via digital wallets

How subscriptions differ from products

Unlike one-off product purchases, subscriptions:

  • Are tied to the player's wallet, not a single transaction. The wallet is what's billed on the recurring schedule.
  • Don't require a complete step. The platform manages the billing lifecycle; your game only needs to read the wallet's current entitlement on startup (and after beginSubscription succeeds) and unlock features accordingly.
  • Are only available to registered users. Guests must register before they can subscribe.
  • Are returned alongside their current status (active or inactive), so the same response tells your game both what's on offer and what the player already has.

Subscription lifecycle

A subscription represents an ongoing entitlement granted to a wallet for as long as billing succeeds.

  1. On startup, call getSubscriptions() to read the catalog and the wallet's current entitlement.
  2. Unlock subscription-gated features based on each subscription's status.
  3. If the player chooses to subscribe to an inactive offering, call beginSubscription({ subscriptionSku }).
  4. If checkout succeeds, the SDK returns the now-active subscription. Apply the entitlement in your game.
  5. Cancellations, expirations, and renewals are handled by the platform; the next call to getSubscriptions() will reflect the updated status.

Happy path (new subscriber)

Returning subscriber (startup reconciliation)

On every startup, your game should re-read getSubscriptions() and apply the resulting entitlements. This is how you reflect cancellations, expiries, and renewals that happened while the player was away.

How to use the SDK

Payload shapes

Where the SDK references SubscriptionData, it contains:

{
sku: string; // the subscription's SKU, configured in the Developer Console
displayName: string; // suitable for display in your game's UI
displayDescription: string | null; // optional short description
price: number; // the subscription price in the currency specified in `currency`
currency: string; // the currency (ISO 4217 code) the price is in
billingPeriod: "weekly" | "monthly" | "yearly"; // the billing cadence
status: "active" | "inactive"; // the wallet's current entitlement for this subscription
estimatedRevenue: number; // the (estimated) USD share of revenue from a single billing period that the publisher will receive
}

A status of "active" means the player's wallet currently has the entitlement and should have access to whatever the subscription unlocks. "inactive" means they don't have it (either never subscribed, or it has expired/been cancelled).

Set up subscriptions

Set up and price subscriptions using the Jest Developer Console.

For more information, see Manage subscriptions.

List subscriptions (getSubscriptions)

note

If a player has ended their subscription, but it is still within their last paid billing period, it will remain "active" and be returned in the getSubscriptions() response until the billing period ends.

To retrieve the subscriptions available to the player along with their current entitlement, call getSubscriptions.

// Returns a promise resolving to the subscription catalog plus the wallet's
// current entitlement status for each subscription.
const { subscriptions, signed } = await JestSDK.payments.getSubscriptions();

for (const subscription of subscriptions) {
if (subscription.status === "active") {
unlockFeaturesFor(subscription.sku);
}
}

The response contains:

PropertyNote
subscriptionsThe array of SubscriptionData objects (see Payload shapes).
signedA signed JWT carrying the same subscriptions array. See below.
note

For guest players, getSubscriptions returns an empty subscriptions array

Displaying prices

The way subscription prices are displayed should be based on the price, currency, and billingPeriod. currency is an ISO 4217 code which can be used to select the correct currency symbol or formatting.

Start a subscription (beginSubscription)

Call beginSubscription with the sku of a subscription returned by getSubscriptions().

The method returns one of the following results. In mock mode, you can simulate each outcome using the JestSDK debug menu.

Success

  • { result: "success"; subscription: SubscriptionData; subscriptionSigned: string } Checkout completed successfully. The returned subscription is now "active" for the player's wallet.

Cancellation

  • { result: "cancel" } The player closed or abandoned the checkout flow.

Error

  • { result: "error"; error: string } One of the following error codes is returned:
    • internal_error - A transient error occurred. Your game may retry.
    • invalid_subscription - The requested sku is not available. Do not retry with the same sku. If this persists and the subscription configuration appears correct, contact support.
    • already_subscribed - The player's wallet already has an active entitlement for this subscription. Refresh the wallet's state via getSubscriptions().
    • guest_not_allowed - The player is a guest. Prompt them to register before starting a subscription.

Any other error (for example, a timeout) should be handled by your game and may be retried.

note

beginSubscription may also throw (for example, due to a timeout). Treat thrown errors as retryable.

const { subscriptions } = await JestSDK.payments.getSubscriptions();
const premium = subscriptions.find((s) => s.sku === "premium_monthly");

if (!premium || premium.status === "active") {
// Already entitled; no need to start checkout.
return;
}

const result = await JestSDK.payments.beginSubscription({
subscriptionSku: premium.sku,
});

if (result.result === "cancel") {
// Handle cancellation with UI feedback.
return;
}

if (result.result === "error") {
if (result.error === "guest_not_allowed") {
// Prompt the player to register, then re-try.
return;
}
if (result.error === "already_subscribed") {
// Re-read entitlement state and unlock features.
return;
}
// internal_error / invalid_subscription: handle with UI feedback.
return;
}

// result.result === "success"
const { subscription, subscriptionSigned } = result;
unlockFeaturesFor(subscription.sku);
tip

After a successful beginSubscription, prefer using the returned subscription (or, better, subscriptionSigned) to unlock features immediately. The next getSubscriptions() call will return the same "active" status.

Signed subscription data (JWT)

The beginSubscription and getSubscriptions methods return subscription data in two forms:

  1. As plain objects (subscription / subscriptions) for convenience.
  2. As signed tokens (subscriptionSigned / subscriptionsSigned) in the form of a signed JSON Web Token (JWT).

The data inside the signed token is equivalent to the plain object. For critical actions such as unlocking paid content or applying entitlements server-side, you must only trust the signed token after verifying its signature.

You can use any standard JWT/JWS library to verify the token signature using your game's shared secret and extract the subscription data.

warning

Failure to verify the signed token can leave your game vulnerable to exploitation. Players can otherwise spoof an "active" status without paying.

Shared secret

Each game has a shared secret configured in the Developer Console (see Games > Secrets). This secret is provided as a base64-encoded string and must be kept confidential between you and the Jest platform.

The same shared secret is used to verify the signed tokens from Payments and Player.

warning

If the shared secret is ever leaked, rotate it immediately by generating a new one in the Developer Console.

Signed payload shapes

Signed JWT payload from beginSubscription (subscriptionSigned):

{
subscription: SubscriptionData; // the single subscription that was just activated
aud: string; // game ID as the 'audience' claim
sub: string; // player ID as the 'subject' claim
}

Signed JWT payload from getSubscriptions (subscriptionsSigned):

{
subscriptions: SubscriptionData[]; // the player's full subscription catalog with current statuses
aud: string; // game ID as the 'audience' claim
sub: string; // player ID as the 'subject' claim
}

Signed payloads may include additional standard JWT claims (such as iat).

Verifying and decoding (server-side)

Use a standard JWT/JWS library on your backend to verify the token signature and decode its payload.

The verification process is the same as for the token returned by JestSDK.getPlayerSigned(). For server-side verification examples in other languages, see Player: JestSDK.getPlayerSigned().

To ensure the subscription data is correct and applies to the expected game and player, validate the aud (audience) and sub (subject) claims:

  • aud: your game ID (available in the Developer Console)
  • sub: the player ID (from JestSDK.getPlayer())
warning

Always validate sub. Without it, your backend will accept a legitimately-signed token from player A and silently apply it to player B's entitlements — a cross-player substitution that doesn't require forging anything. Take the sub claim from the verified subscription token, compare it against the expected player ID on your backend, and reject the request if they don't match.

If you don't provide aud and sub as parameters to your JWT library, you are responsible for verifying them on the decoded payload.

An example (Node.js) using jose:

note

This example is server-side.

import { jwtVerify } from "jose";

// This value should be kept secret and NOT checked into source control
// or used client-side.
const sharedSecret = "<your game's shared secret>";
const gameId = "<your game id>";

// Resolve this from the verified `playerSigned` token's player ID, on your
// backend. Do not trust a player ID supplied by the client.
const expectedPlayerId = "<player id for the current player>";

// The `signed` value from the `getSubscriptions` response, or the
// `subscriptionSigned` value from `beginSubscription`.
// Send it to your server *without* any modification.
const token = "<token>";

const verified = await jwtVerify(token, Buffer.from(sharedSecret, "base64"), {
audience: gameId,
subject: expectedPlayerId,
});

// The verified payload contains { subscriptions, aud, sub } (or
// { subscription, aud, sub } for `beginSubscription`) plus standard JWT claims.
console.log(verified.payload);

We strongly recommend using an established library to verify JWTs rather than implementing verification yourself. If for whatever reason this doesn't happen, your implementation must follow the JWT spec and current best practices.

Testing

When using a sandbox user, the Stripe checkout flow still needs to be completed — the player goes through the same checkout UI as a normal player — but the order total is $0 and no real charge is made. Once checkout is completed, the resulting subscription is treated as "active" for the duration of a normal billing period, so you can test the subscribed and unsubscribed flows end-to-end without spending real money.

In mock mode, you can simulate each beginSubscription outcome (success, cancel, errors) and toggle the wallet's entitlement on each subscription SKU via the JestSDK debug menu.

Revenue estimate

Jest's economics model splits revenue between the publisher, the platform, and publishers who bring users in to the platform. SubscriptionData includes an estimate, in USD, of the share you (as the publisher) are entitled to from a single billing period of this subscription.

This is an estimate only, as the final revenue may depend on user registration or other factors. See our economics model for more information.

Subscription revenue is recognized on each successful billing cycle, not at signup. Final revenue will be reported via the developer console (details coming soon).