Subscriptions

Integrating Subscriptions

Learn how to integrate Stash Subscriptions into your game. This guide covers listing plans, creating subscription checkouts, handling webhooks, and managing subscription lifecycle.

This guide explains how to integrate Stash Subscriptions into your game, from displaying available plans to handling subscription lifecycle events.

Integration overview

Display available plans

Fetch plans from the API and show them to players.

Manage subscriptions

Let players view, cancel, or reactivate their subscriptions.

Display available plans

Fetch available subscription plans using the List Plans endpoint.

List Plans Request
curl -X GET "https://api.stash.gg/sdk/plans" \
  -H "X-Stash-Api-Key: YOUR_API_KEY"

Plan response

List Plans Response
{
  "plans": [
    {
      "id": "plan_abc123",
      "code": "monthly_premium",
      "name": "Premium Monthly",
      "description": "Access to all premium features",
      "billingPeriodValue": 1,
      "billingPeriodUnit": "month",
      "prices": [
        { "currency": "USD", "amountCents": 999 },
        { "currency": "EUR", "amountCents": 899 }
      ],
      "trialPeriodValue": 7,
      "trialPeriodUnit": "day",
      "status": "active"
    }
  ]
}

Display these plans in your game UI, showing the name, description, price, and trial period (if any).

Use the code field (e.g., monthly_premium) to identify plans in your game logic. The id is Stash's internal identifier.

Create subscription checkout

When a player selects a plan, create a subscription checkout link using the Create Subscription Checkout Link endpoint.

Checkout creation should be done from your server to keep your API key private.

Create Subscription Checkout Link
curl -X POST "https://api.stash.gg/sdk/subscriptions/checkout-links" \
  -H "X-Stash-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plan_abc123",
    "user": {
      "id": "player_123"
    },
    "currency": "USD"
  }'

The response includes a checkout URL to present to the player:

Checkout Link Response
{
  "url": "https://checkout.stash.gg/subscribe/abc123",
  "id": "checkout_abc123"
}

Initial payment discounts

You can customize the first payment amount using the initialPayment field. This is useful for offering promotional pricing, trials with reduced cost, or custom first-month deals.

Discount by fixed amount:

Initial Payment with Fixed Discount
curl -X POST "https://api.stash.gg/sdk/subscriptions/checkout-links" \
  -H "X-Stash-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plan_abc123",
    "user": {
      "id": "player_123"
    },
    "currency": "USD",
    "initialPayment": {
      "discount": {
        "amountOffCents": 500
      }
    }
  }'

Discount by percentage:

Initial Payment with Percentage Discount
curl -X POST "https://api.stash.gg/sdk/subscriptions/checkout-links" \
  -H "X-Stash-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plan_abc123",
    "user": {
      "id": "player_123"
    },
    "currency": "USD",
    "initialPayment": {
      "discount": {
        "percentOff": 50
      }
    }
  }'

Custom initial amount:

Initial Payment with Custom Amount
curl -X POST "https://api.stash.gg/sdk/subscriptions/checkout-links" \
  -H "X-Stash-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plan_abc123",
    "user": {
      "id": "player_123"
    },
    "currency": "USD",
    "initialPayment": {
      "customAmountCents": 199
    }
  }'

The initialPayment only affects the first billing cycle. Subsequent renewals will be charged at the plan's regular price.

After the player completes the checkout, Stash creates the subscription and sends a subscription.created webhook to your backend.

Subscription change (upgrade) checkout

To change an existing subscription (plan upgrade and/or payment method), create a subscription change checkout link with Create a subscription change checkout link:

Create Subscription Change Checkout Link
curl -X POST "https://api.stash.gg/sdk/subscriptions/sub_xyz789/change-checkout-links" \
  -H "X-Stash-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "newPlan": "plan_higher_tier"
  }'

The response includes url and id (for example a checkout URL like https://checkout.stash.gg/subscription/change/{checkoutLinkId}). At least one of newPlan or updatePaymentMethod must be set; see the API reference for optional fields such as billingAnchor, initialPayment, and paymentMethod.

Eligibility: active, trialing, or canceled with current_period_end still in the future (canceled but still in the paid window). If the subscription is canceled after that paid window, create a new subscription with Create Subscription Checkout Link instead.

Behavior: Creating the link only validates eligibility and stores the link—it does not reactivate a canceled subscription or apply the plan change. If the buyer abandons checkout, the subscription stays as-is (for example, still canceled). When checkout completes successfully, Stash applies the upgrade and, if the subscription was canceled-but-not-expired, reactivates it as part of that update.

See the API reference for full request and response fields.

Handle webhooks

Set up a webhook endpoint to receive subscription lifecycle events. See the Webhooks guide for general webhook setup.

Subscription webhook events

EventDescription
subscription.createdNew subscription created
subscription.updatedSubscription plan or status changed
subscription.canceledPlayer canceled their subscription
subscription.reactivatedCanceled subscription was reactivated
subscription.expiredSubscription reached terminal state
subscription.payment_failedRenewal payment failed
subscription.payment_succeededRenewal payment succeeded

Webhook payload (v2)

Subscription webhooks use a v2 payload format:

subscription.created Webhook
{
  "type": "subscription.created",
  "data": {
    "id": "sub_xyz789",
    "external_account_id": "player_123",
    "plan_id": "plan_abc123",
    "status": "active",
    "period": {
      "value": 1,
      "unit": "month"
    },
    "trial_end": "2024-02-01T00:00:00Z",
    "access_end_date": "2024-03-01T00:00:00Z",
    "current_period_end": "2024-03-01T00:00:00Z",
    "next_billing_date": "2024-03-01T00:00:00Z",
    "cancel_at_period_end": false,
    "canceled_at": null,
    "created_at": "2024-01-01T00:00:00Z"
  }
}

Handling subscription.created

When you receive subscription.created:

  1. Store the subscription ID and player mapping
  2. Grant the player access to subscription benefits
  3. Update your game's subscription UI
Handle subscription.created
app.post('/webhooks/stash', (req, res) => {
  const { type, data } = req.body;

  if (type === 'subscription.created') {
    // Grant subscription benefits to player
    await grantSubscriptionAccess(data.external_account_id, data.plan_id);

    // Store subscription for later reference
    await saveSubscription(data);
  }

  res.status(200).send('OK');
});

Handling subscription.expired

When you receive subscription.expired:

  1. Revoke the player's subscription benefits
  2. Update your game's subscription UI
  3. Optionally prompt the player to resubscribe

Payment webhook events

Payment webhooks (payment.succeeded, payment.failed, payment.refunded, dispute.opened, dispute.closed) use the same v2 payload format as subscription events. See Payment Events in the webhook list for event types and payload schemas.

Manage subscriptions

Check subscription status

Use List Subscriptions to check a player's active subscriptions:

List Player Subscriptions
curl -X GET "https://api.stash.gg/sdk/subscriptions?external_account_id=player_123" \
  -H "X-Stash-Api-Key: YOUR_API_KEY"

Filter by status to find only active subscriptions:

List Active Subscriptions
curl -X GET "https://api.stash.gg/sdk/subscriptions?external_account_id=player_123&status=active" \
  -H "X-Stash-Api-Key: YOUR_API_KEY"

Cancel a subscription

Allow players to cancel their subscription using Cancel Subscription:

Cancel Subscription
curl -X POST "https://api.stash.gg/sdk/subscriptions/sub_xyz789/cancel" \
  -H "X-Stash-Api-Key: YOUR_API_KEY"

After cancellation:

  • cancel_at_period_end becomes true
  • Player keeps access until current_period_end
  • Webhook subscription.canceled is sent

Reactivate a subscription

If a player changes their mind before the period ends, reactivate with Reactivate Subscription:

Reactivate Subscription
curl -X POST "https://api.stash.gg/sdk/subscriptions/sub_xyz789/reactivate" \
  -H "X-Stash-Api-Key: YOUR_API_KEY"

Reactivation only works while the subscription is canceled. Once it's expired, the player must create a new subscription.

Testing renewal scenarios

In the test environment, Stash supports simulation cards that return scripted payment outcomes for subscription initial payments and renewals. Use these cards to exercise grace periods, retry recovery, declines, and system errors without waiting on a real PSP.

Simulation cards only apply in the test environment. Use any future expiration date and a valid CVC (for example, 03/30 and 737) with the card numbers below. For one-off checkout testing, see Test Card Numbers.

Simulated payments do not go through the PSP, even in the test environment. Stash will not send payment.succeeded, payment.failed, or other payment.* webhook events for these cards. Rely on subscription.* lifecycle events where they apply — but note that the system-error card (0028) does not emit subscription.payment_failed either; see System errors vs payment declines below.

How simulation works

Each simulation card defines a sequence of outcomes keyed by billing period and retry attempt:

IndexMeaning
Period 0Initial subscription payment at checkout
Period 1First renewal
Period 2Second renewal
Period NNth renewal

Within each period, retry count tracks payment attempts during that billing cycle:

Retry countMeaning
0First payment attempt for the period
1First retry (for example, during grace period)
2Second retry

Simulation test cards

Card numberScenarioExpected behavior
4000 0000 0000 0002Grace period and retry exhaustionInitial payment succeeds. First renewal succeeds. From the second renewal onward, every attempt declines with Insufficient Funds — including retries during the grace period. Subscription moves to past_due, then expired if retries are exhausted.
4000 0000 0000 0010Retry recoveryInitial payment succeeds. First renewal fails on the first attempt (Card Expired), then succeeds on the first retry. All subsequent renewals succeed.
4000 0000 0000 0036Immediate declineEvery attempt declines with Do Not Honor.

Testing payment declines and reasons

The simulation cards above exercise subscription lifecycle transitions but do not produce payment.* webhooks. To test the payment-failure webhooks themselves, and the reason they carry, use the decline cards below. Each is declined with a specific reason, and Stash delivers a payment.failed webhook (and, when a renewal on an existing subscription is declined, a subscription.payment_failed webhook) whose reason field is set to the value shown.

These test cards use a new payment implementation that is currently enabled for newer partners. If a card below doesn't produce the expected decline, reach out to your Stash representative to confirm your account has access.

Enter any future expiration date (for example, 03/30) and any valid CVC — the card number alone decides the outcome.

Card numberreasonMeaning
4544 2491 6767 3670insufficient_fundsThe card has insufficient funds.
4485 3815 7718 2090invalid_cardInvalid card number or account.
4897 4535 6848 5113suspected_fraudThe payment was flagged as potential fraud.
4818 9242 5013 1070card_blockedThe card is restricted or blocked.
4941 2020 6099 9329card_lost_or_stolenThe card was reported lost or stolen.
4539 4679 8710 9256issuer_declinedThe bank declined the payment.
4276 0385 7859 6818transaction_not_allowedThis payment is not permitted for the card.
4556 2945 9375 7189limit_exceededAn amount or frequency limit was exceeded.
4500 6228 6834 1387authentication_requiredThe card requires 3D Secure authentication.
4485 8998 0515 6040payment_stoppedThe cardholder stopped or revoked the payment.
4556 2537 5271 2245declined_otherDeclined for another reason.

Treat any reason value you don't recognize as declined_other — new values may be added over time.

To test card_expired, use any card number above with an expiration date in the past; the payment is declined as an expired card and reason is card_expired. A failed first-time payment delivers payment.failed only; subscription.payment_failed is sent when a renewal on an existing subscription fails.

Best practices

Validate access server-side

Always validate subscription access on your game server, not just the client. Check the subscription status and access_end_date before granting premium features.

Handle payment failures gracefully

When you receive subscription.payment_failed, don't immediately revoke access. The player is in a grace period and Stash is retrying the payment. Only revoke access when you receive subscription.expired.

Cache subscription status

Cache subscription status locally to avoid API calls on every game action. Update the cache when you receive webhook events or when the player opens subscription UI.

Provide clear subscription UI

Show players:

  • Their current plan and status
  • Next billing date
  • Option to cancel or manage payment method
  • Clear indication if they're in a trial period

How is this guide?