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.

Create subscription checkout

When a player selects a plan, create a subscription checkout link.

Handle webhooks

Process subscription lifecycle events on your backend.

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",
      "billing_period_value": 1,
      "billing_period_unit": "month",
      "prices": [
        { "currency": "USD", "amount_cents": 999 },
        { "currency": "EUR", "amount_cents": 899 }
      ],
      "trial_period_value": 7,
      "trial_period_unit": "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.

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 notify your backend about payment events related to subscriptions. These events use the v2 payload format.

EventDescription
payment.succeededPayment completed successfully
payment.failedPayment attempt failed
payment.refundedPayment was refunded

payment.succeeded

Triggered when a payment completes successfully.

payment.succeeded Webhook
{
  "type": "payment.succeeded",
  "data": {
    "id": "pay_abc123",
    "external_account_id": "player_123",
    "currency": "USD",
    "subscription_id": "sub_xyz789",
    "succeeded_at": "2024-01-01T00:00:00Z",
    "tax": "1.50",
    "total": "10.99",
    "metadata": {
      "custom-key": "customer-value"
    }
  }
}

payment.failed

Triggered when a payment attempt fails.

payment.failed Webhook
{
  "type": "payment.failed",
  "data": {
    "id": "pay_abc456",
    "external_account_id": "player_123",
    "currency": "USD",
    "subscription_id": "sub_xyz789",
    "failed_at": "2024-01-01T00:00:00Z",
    "tax": "1.50",
    "total": "10.99",
    "metadata": {
      "custom-key": "customer-value"
    }
  }
}

payment.refunded

Triggered when a payment is refunded.

payment.refunded Webhook
{
  "type": "payment.refunded",
  "data": {
    "id": "pay_abc789",
    "external_account_id": "player_123",
    "currency": "USD",
    "subscription_id": "sub_xyz789",
    "refunded_at": "2024-01-01T00:00:00Z",
    "total_refunded": "10.99",
    "metadata": {
      "custom-key": "customer-value"
    }
  }
}

dispute.opened

Triggered when a dispute is opened.

dispute.opened Webhook
{
  "type": "dispute.opened",
  "data": {
    "id": "pay_abc123",
    "external_account_id": "player_123",
    "currency": "USD",
    "subscription_id": "sub_xyz789",
    "total_disputed": "10.99",
    "metadata": {
      "custom-key": "custom-value"
    }
  }
}

dispute.closed

Triggered when a dispute is closed (with a won boolean).

dispute.closed Webhook
{
  "type": "dispute.opened",
  "data": {
    "id": "pay_abc123",
    "won": true,
    "external_account_id": "player_123",
    "currency": "USD",
    "subscription_id": "sub_xyz789",
    "total_disputed": "10.99",
    "metadata": {
      "custom-key": "custom-value"
    }
  }
}

Payment webhook object fields

FieldTypeDescription
idstringPayment identifier
external_account_idstringIdentifier for the player/user
currencystringCurrency code (e.g., "USD", "EUR")
subscription_idstring (optional)Subscription identifier if payment is related to a subscription
succeeded_atstringISO 8601 timestamp when payment succeeded (payment.succeeded only)
failed_atstringISO 8601 timestamp when payment failed (payment.failed only)
refunded_atstringISO 8601 timestamp when payment was refunded (payment.refunded only)
taxstring (optional)Tax amount as decimal string
totalstringTotal payment amount as decimal string (payment.succeeded and payment.failed)
total_refundedstringTotal refunded amount as decimal string (payment.refunded only)
metadataobject (optional)Custom metadata attached to the payment

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.

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?