Webhook List
Comprehensive list of all webhook events that Stash can send to your backend, including event types, triggers, payload structures, and detailed examples for purchase, subscription, and payment webhooks across Stash products.
This page lists all webhook events Stash can send to your backend. Each event is triggered by a specific user action or system event. Use the table below to see what's available, then expand the tabs for full payload examples.
Product-specific events: Some events are specific to Stash Pay or Stash Webshop. See the Stash Pay integration guide or Stash Webshop integration guide for product-specific webhook details.
Available webhooks
PURCHASE_SUCCEEDED is the primary event for granting items; always subscribe to it. PURCHASE_REFUNDED should be handled to revoke or adjust granted items. The remaining events are optional and used mainly for analytics, marketing, and behavior tracking.
| Event Type | Required | Products | Description |
|---|---|---|---|
PURCHASE_SUCCEEDED | Yes | Pay, Webshop | Purchase successfully completed. Use to grant items to the player. source is "StashPay", "Cart" or "BuyNow". |
PURCHASE_FAILED | No | Pay, Webshop | A payment was refused or canceled. Use it for support and analytics, not to grant or revoke items. reason carries the refusal reason: never show it to the player. |
PURCHASE_REFUNDED | Recommended | Pay, Webshop | Full or partial refund processed. Use to revoke or adjust granted items. |
OPEN_PAYMENT | No | Pay, Webshop | Payment process initiated (user starts checkout). |
FREE_ITEM_REDEEMED | No | Pay, Webshop | A player claimed a free item. |
LOGGED_IN | No | Webshop | A player signed in to your webshop with a login code, an authenticated URL or your own OAuth 2.0 server. |
MUTATE_CART | No | Webshop | Cart contents changed (add/remove/quantity). |
VIEW_ITEM | No | Webshop | User viewed an item. At most once per 100 seconds per item per player; a view without a userId is sent once per view, up to a per-shop limit. |
VIEW_CATALOG_SECTION | No | Webshop | User scrolled a catalog section into view. At most once per 100 seconds per section per player; a view without a userId is sent once per view, up to a per-shop limit. |
VIEW_CHECKOUT_PAGE | No | Webshop | User opened the checkout page. |
VIEW_PRODUCT_DETAIL_PAGE | No | Webshop | User opened a product detail page. |
CART_BUTTON_CLICK | No | Webshop | User interacted with a cart button. |
BUY_NOW_ITEM_CLICK | No | Webshop | User clicked an item that opens checkout directly, without the cart. |
PAY_BUTTON_CLICK | No | Pay, Webshop | User clicked the pay button on the checkout page. |
LOYALTY_MILESTONE_CLAIMED | No | Webshop | Player claimed a loyalty milestone. Only fires when the loyalty program uses async webhook delivery mode. |
LOGIN_FAILED | No | Webshop | A player's sign-in to your webshop was declined, by your server or by Stash. reason says why. Off until Stash turns it on for your shop. |
OFFER_SLOT_MISMATCH | No | Webshop | Offers used a template key, or reward, buy button or text slot names, their catalog's templates do not have. The offers still showed. At most once per clock hour per template and set of mismatches. |
CATALOG_REJECTED | No | Webshop | Stash could not fetch your Real-time Catalog or decode its response. At most once per shop per 10-minute window. |
CATALOG_ITEMS_DROPPED | No | Webshop | Your Real-time Catalog was shown, but items were left out or a declared bonus has a missing or invalid picture. At most once per shop per 10-minute window from Stash's server, and once from your webshop. |
CATALOG_DATA_CONFLICT | No | Webshop | Your Real-time Catalog contradicts itself or your shop's configuration. At most once per shop and conflict per 10-minute window. |
CATALOG_TEXT_FALLBACK | No | Webshop | Catalog texts showed their defaultText because their localizationKey is not in your shop's localizations, or has no translation in the player's language. The offers still showed. At most once per shop per clock hour. |
CATALOG_REJECTED, CATALOG_ITEMS_DROPPED, CATALOG_DATA_CONFLICT and CATALOG_TEXT_FALLBACK report problems in your Real-time Catalog that you can fix. To start receiving them, contact your Stash engineering partner.
LOGIN_FAILED is off by default, so an endpoint that rejects unknown event types is not affected. Once your endpoint accepts the event, contact your Stash engineering partner to turn it on for your shop.
Payload structure
All v1 webhooks follow a consistent base structure:
{
"type": "PURCHASE_SUCCEEDED",
"environment": "test",
"shopId": "your-shop-uuid",
"purchaseSucceeded": {
// Event-specific payload
}
}type, environment and shopId are on every v1 event. The event's payload sits under a key named for the event, such as purchaseSucceeded for PURCHASE_SUCCEEDED; each example below shows its key.
environmentis"test"(test/staging) or"production"(live). Use it to distinguish test traffic from real transactions.shopIdis the ID of the shop that sent the event. Use it to route events when one endpoint serves several shops.
Webshop session
checkoutSessionId identifies one player's browsing session in your webshop. Events from the same session carry the same value, from catalog views through checkout and the purchase. After 30 minutes without activity, the player's next action starts a new session with a new checkoutSessionId. A new browser tab can also start a new session. Treat it as an opaque string.
- Group a session's events: collect every event with the same
checkoutSessionId. - Join a purchase to its funnel: a
PURCHASE_SUCCEEDEDorPURCHASE_FAILEDcarries thecheckoutSessionIdof the views and clicks that led to it. - Match the purchase to your purchase calls: use
transactionId(ororderIdon a"Cart"purchase), as described underPURCHASE_SUCCEEDED.checkoutSessionIdis not sent onRegisterPaymentorConfirmPayment.
On PURCHASE_REFUNDED, checkoutSessionId is the session of the original purchase, not of the refund.
These events can carry it: PURCHASE_SUCCEEDED, PURCHASE_FAILED, PURCHASE_REFUNDED, FREE_ITEM_REDEEMED, LOGGED_IN, VIEW_ITEM, VIEW_CATALOG_SECTION, VIEW_CHECKOUT_PAGE, VIEW_PRODUCT_DETAIL_PAGE, CART_BUTTON_CLICK, BUY_NOW_ITEM_CLICK and PAY_BUTTON_CLICK. It is omitted when the event is not tied to a webshop session, for example a purchase through a Stash Pay checkout link your backend created. It may also be omitted when analyticsConsent is "DECLINED" or "UNANSWERED". OPEN_PAYMENT, MUTATE_CART, LOYALTY_MILESTONE_CLAIMED, LOGIN_FAILED, OFFER_SLOT_MISMATCH and the CATALOG_* events never carry it.
Analytics consent
analyticsConsent is the player's answer to your webshop's analytics consent banner. VIEW_ITEM, VIEW_CATALOG_SECTION and BUY_NOW_ITEM_CLICK carry it.
| Value | Meaning |
|---|---|
"ACCEPTED" | The player accepted analytics. |
"DECLINED" | The player declined analytics. |
"UNANSWERED" | The player has not answered the banner yet. |
Depending on your webshop, userId and checkoutSessionId are either sent with every answer, or omitted unless the answer is "ACCEPTED". A view or click without a userId can also come from a signed-out visitor. To find out which applies to your shop, contact your Stash engineering partner.
The field is omitted when no answer was reported, for example when your webshop shows no consent banner. It describes the banner answer only: it is not a permission that Stash grants, and it does not authenticate the event.
Detailed payload examples
Pick the event you're integrating to see its full JSON payload.
Triggered when a purchase is successfully completed. The source field says where the purchase started:
source | Purchase |
|---|---|
"StashPay" | A Stash Pay checkout link your backend created |
"Cart" | A Webshop cart checkout |
"BuyNow" | A single item bought directly from the Webshop, without the cart |
To match the event to the purchase your backend saw on RegisterPayment or ConfirmPayment, use transactionId: it is the transaction ID Stash sent on those calls. On a "Cart" purchase, orderId carries that ID. transactionId may be absent on a "Cart" purchase, so match on orderId there.
Each entry in items describes one purchased item. For a Real-time Catalog, the offer fields repeat what your catalog sent for that item when the purchase started, so you can attribute the purchase to the offer as the player saw it:
| Field | Type | Description |
|---|---|---|
id | string | The item's product ID. Several offers can share one product ID; use guid to tell them apart. |
guid | string (optional) | The guid of the offer the item was sold under: the guid your catalog sent, or the one Stash generated for the item when your catalog sent none. On a "Cart" purchase it matches the guid on ConfirmPayment. |
quantity | number | Units bought. |
price | string | Price of one unit. |
priceId | string (optional) | The Studio price ID the item was priced from: your catalog's price.priceId, or the item's product ID when the price was looked up by product ID. Only on "Cart" purchases, and omitted when your catalog sent a price.amount. See Studio price lookup. |
metadata | object (optional) | Metadata attached to the item, such as the metadata you set on a catalog item. |
purchasePackId | string (optional) | The id of the purchase pack the item belongs to. |
version | string (optional) | The offer's version. |
templateKey | string (optional) | The offer's templateKey. |
Optional fields are omitted when Stash has no value for them, for example when the catalog did not send them. PURCHASE_FAILED and PURCHASE_REFUNDED items carry the same fields.
paymentMethodType says how the player paid. It is omitted when the payment method is unknown.
paymentMethodType | Payment method |
|---|---|
"PAYMENT_METHOD_TYPE_CARD" | Credit or debit card |
"PAYMENT_METHOD_TYPE_APPLE_PAY" | Apple Pay |
"PAYMENT_METHOD_TYPE_GOOGLE_PAY" | Google Pay |
"PAYMENT_METHOD_TYPE_PAYPAL" | PayPal |
"PAYMENT_METHOD_TYPE_KLARNA" | Klarna |
"PAYMENT_METHOD_TYPE_IDEAL" | iDEAL |
"PAYMENT_METHOD_TYPE_EPS" | EPS |
"PAYMENT_METHOD_TYPE_AFTERPAY" | Afterpay |
"PAYMENT_METHOD_TYPE_BANCONTACT" | Bancontact |
{
"type": "PURCHASE_SUCCEEDED",
"environment": "test",
"shopId": "your-shop-uuid",
"purchaseSucceeded": {
"timeMillis": 1640995200000,
"transactionId": "txn_789",
"orderId": "order_abc123",
"checkoutLinkId": "checkout_xyz789",
"currency": "USD",
"userId": "user_123",
"items": [
{
"id": "item_456",
"guid": "item_456_summer",
"quantity": 2,
"price": "9.99",
"metadata": {
"category": "weapons",
"rarity": "legendary"
},
"purchasePackId": "summer-offer",
"version": "5",
"templateKey": "starter-offer-v2"
}
],
"tax": "1.50",
"total": "21.48",
"taxDetails": {
"items": [
{
"label": "Sales Tax",
"isInclusive": false,
"amount": "1.50",
"percentage": "7.5",
"country": "US",
"state": "CA"
}
]
},
"emailMarketingOptIn": true,
"regionCode": "US",
"source": "StashPay",
"ipAddress": "192.168.1.1",
"paymentMethodType": "PAYMENT_METHOD_TYPE_CARD",
"checkoutSessionId": "8f3c2b1e-6a4d-4c9e-9b2f-5d7a1e0c3b46"
}
}Common fields
Several fields appear across multiple webhook types. Only type, environment and shopId are on every v1 event; check the event's example above for the rest.
| Field | Type | Description |
|---|---|---|
type | string | Event type identifier (e.g. "PURCHASE_SUCCEEDED"). |
environment | string | Environment that sent the webhook. Always present. "test" or "production". |
shopId | string | ID of the shop that sent the event. Always present on v1 events. |
userId | string | External user identifier from your system. |
regionCode | string | Unicode CLDR region code (e.g. "US", "FR") based on user location. |
ipAddress | string | User's IP address when the event occurred. |
timeMillis | number | When the event happened, in milliseconds since the Unix epoch. Present on PURCHASE_SUCCEEDED, PURCHASE_REFUNDED, FREE_ITEM_REDEEMED, MUTATE_CART, LOYALTY_MILESTONE_CLAIMED, CATALOG_REJECTED, CATALOG_ITEMS_DROPPED, CATALOG_DATA_CONFLICT and CATALOG_TEXT_FALLBACK. Not on the view and click events. |
checkoutSessionId | string | The player's webshop browsing session: the same value on every event of the session, and a new value after 30 minutes without activity. Omitted when the event is not tied to a webshop session, and may be omitted when analyticsConsent is "DECLINED" or "UNANSWERED". See Webshop session. |
analyticsConsent | string | The player's answer to your webshop's analytics consent banner: "ACCEPTED", "DECLINED" or "UNANSWERED". On VIEW_ITEM, VIEW_CATALOG_SECTION and BUY_NOW_ITEM_CLICK; omitted when no answer was reported. See Analytics consent. |
Subscription Events (v2)
Subscription webhooks use a v2 payload format with lowercase dot-notation event types, unlike the v1 format used by purchase and webshop events above.
Subscription events notify your system of subscription lifecycle changes. See the Subscriptions guide for full integration details.
Available subscription events
| Event Type | Description | Trigger |
|---|---|---|
subscription.created | New subscription created | Player completes subscription checkout |
subscription.updated | Subscription changed | Plan or status was modified |
subscription.canceled | Subscription canceled | Player cancels their subscription |
subscription.reactivated | Subscription reactivated | Canceled subscription was reactivated before expiration |
subscription.expired | Subscription expired | Subscription reached terminal state |
subscription.payment_failed | Payment failed | Renewal payment attempt failed; subscription enters past_due |
subscription.payment_succeeded | Payment succeeded | Renewal payment processed successfully |
v2 Payload structure
Subscription webhooks use a different structure from v1 events:
{
"type": "subscription.event_name",
"data": {
// Subscription object
}
}Detailed subscription payloads
Triggered when a new subscription is created.
{
"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"
}
}Subscription object fields
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the subscription. |
external_account_id | string | Identifier for the player/user. |
plan_id | string | Plan identifier. |
status | string | active, past_due, canceled, or expired. |
period | object | Billing period with value (integer) and unit (day / week / month / year). |
trial_end | string (nullable) | ISO 8601 timestamp when trial ends. |
access_end_date | string | ISO 8601 timestamp when access expires. |
current_period_end | string | ISO 8601 timestamp for current period end. |
next_billing_date | string | ISO 8601 timestamp for next billing attempt. |
cancel_at_period_end | boolean | Whether cancellation is scheduled. |
canceled_at | string (nullable) | ISO 8601 timestamp when canceled. |
created_at | string | ISO 8601 timestamp when created. |
Payment Events (v2)
Payment webhooks notify your backend about individual payment outcomes. They use the same v2 payload format as subscription events (type + data). These events fire for subscription payments and other Stash Pay checkout flows.
Available payment events
| Event Type | Description | Trigger |
|---|---|---|
payment.succeeded | Payment completed successfully | A payment is captured |
payment.failed | Payment attempt failed | A payment is declined or fails |
payment.refunded | Payment was refunded | A refund is processed |
dispute.opened | Dispute opened | A cardholder disputes a payment |
dispute.closed | Dispute closed | A dispute is resolved (includes won boolean) |
Detailed payment payloads
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 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 when the 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). |
reason | string | Normalized failure reason (payment.failed only). |
refunded_at | string | ISO 8601 timestamp when payment was refunded (payment.refunded only). |
tax | string (optional) | Tax amount as decimal string (payment.succeeded and payment.failed). |
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). |
total_disputed | string | Total disputed amount as decimal string (dispute.opened and dispute.closed). |
won | boolean | Whether you won the dispute (dispute.closed only). |
metadata | object (optional) | Custom metadata attached to the payment. |
How is this guide?
Webhook Listener
Learn how to create a webhook listener on your backend to receive incoming Stash webhook requests, verify their authenticity using HMAC SHA-256 signatures, and respond appropriately to process events securely.
Webhook Retries and Idempotency
Learn about webhook retry behavior, how many retries are attempted, what happens on failure, and best practices for implementing idempotent webhook handlers.