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.
curl -X GET "https://api.stash.gg/sdk/plans" \
-H "X-Stash-Api-Key: YOUR_API_KEY"Plan 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.
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:
{
"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:
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:
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:
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
| Event | Description |
|---|---|
subscription.created | New subscription created |
subscription.updated | Subscription plan or status changed |
subscription.canceled | Player canceled their subscription |
subscription.reactivated | Canceled subscription was reactivated |
subscription.expired | Subscription reached terminal state |
subscription.payment_failed | Renewal payment failed |
subscription.payment_succeeded | Renewal payment succeeded |
Webhook payload (v2)
Subscription webhooks use a v2 payload format:
{
"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:
- Store the subscription ID and player mapping
- Grant the player access to subscription benefits
- Update your game's subscription UI
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:
- Revoke the player's subscription benefits
- Update your game's subscription UI
- 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.
| Event | Description |
|---|---|
payment.succeeded | Payment completed successfully |
payment.failed | Payment attempt failed |
payment.refunded | Payment was refunded |
payment.succeeded
Triggered when a payment completes successfully.
{
"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.
{
"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.
{
"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.
{
"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).
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Payment identifier |
external_account_id | string | Identifier for the player/user |
currency | string | Currency code (e.g., "USD", "EUR") |
subscription_id | string (optional) | Subscription identifier if payment is related to a subscription |
succeeded_at | string | ISO 8601 timestamp when payment succeeded (payment.succeeded only) |
failed_at | string | ISO 8601 timestamp when payment failed (payment.failed only) |
refunded_at | string | ISO 8601 timestamp when payment was refunded (payment.refunded only) |
tax | string (optional) | Tax amount as decimal string |
total | string | Total payment amount as decimal string (payment.succeeded and payment.failed) |
total_refunded | string | Total refunded amount as decimal string (payment.refunded only) |
metadata | object (optional) | Custom metadata attached to the payment |
Manage subscriptions
Check subscription status
Use List Subscriptions to check a player's active 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:
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:
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_endbecomestrue- Player keeps access until
current_period_end - Webhook
subscription.canceledis sent
Reactivate a subscription
If a player changes their mind before the period ends, reactivate with 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?