Skip to content

Event types

Every event has the same top-level fields, whichever channel it came from.

{
"id": "evt_3f0c...",
"type": "subscription.renewed",
"created_at": "2026-11-01T12:00:03.000Z",
"app_user_id": "6f1c2a3b-4d5e-4f60-8a71-92b3c4d5e6f7",
"platform": "ios",
"environment": "production",
"product_id": "pro_monthly",
"transaction_id": "2000000789",
"original_transaction_id": "2000000123",
"purchased_at": "2026-11-01T12:00:00.000Z",
"expires_at": "2026-12-01T12:00:00.000Z",
"price_micros": 9990000,
"currency": "USD",
"ownership": "purchased",
"data": { "store": "app_store", "notification_type": "DID_RENEW", "transaction_reason": "RENEWAL", "status": "active", "will_renew": true }
}
Field Meaning
id Stable event id. Use it to deduplicate
type One of the types below
platform ios, android, web or test
environment production, sandbox or test. See Environments
price_micros Price times 1,000,000 in the original currency; 9.99 USD is 9990000
ownership purchased or family_shared
data Channel details. data.store is app_store, google_play, stripe, ccbill or test_store
Type When
purchase A one-time product (consumable, non-consumable, non-renewing subscription) was recorded
subscription.started A new subscription, or a resubscription after expiry
subscription.renewed A renewal succeeded, including recovery after a failed charge (data.recovered)
subscription.product_changed Upgrade (immediate, new transaction) or downgrade (data.change: downgrade or downgrade_canceled, data.effective: next_renewal, data.new_product_id)
subscription.canceled Auto-renew turned off. Access continues until expires_at
subscription.uncanceled Auto-renew turned back on
subscription.billing_issue A charge failed. data.in_grace_period tells whether access continues
subscription.grace_period_expired The grace period ended without a successful charge
subscription.expired The subscription ended. data.expiration_reason: VOLUNTARY, BILLING_RETRY, PRICE_INCREASE, PRODUCT_NOT_FOR_SALE, and on other channels DEVELOPER or DISPUTE
subscription.paused Android only. data.auto_resume_time
refund Refunded. Revoke access
refund.reversed Apple reversed a refund. Restore access
refund.consumption_requested Apple asks for consumption information (data.deadline_at, data.default_refund_preference)
revoked Family sharing access was revoked
chargeback Stripe dispute opened. Access is not revoked; decide yourself
chargeback.closed Dispute closed, data.outcome is won or lost. On lost a refund with data.reason: "chargeback_lost" follows
ad.reward A rewarded ad was watched to the end. See Rewarded ads
test Sent only by the test webhook endpoint. Verify, return 2xx, do nothing else

Events exist only when Kasskeep knows the user. A transaction without an appAccountToken, such as an offer code redeemed outside the app, is stored silently and its events are sent once a user submits it.

Not every channel produces every type.

Type App Store Google Play Stripe Test store
subscription.paused ✓
refund.reversed ✓ ✓
refund.consumption_requested ✓
revoked ✓ ✓
chargeback, chargeback.closed ✓

On Android, data also carries purchase_token, subscription_state, base_plan_id, offer_id and notification_type (the RTDN name). On Stripe it carries notification_type, notification_id, subscription_id, invoice_id, stripe_status and checkout_session_id where they apply.

  1. Verify the signature.
  2. Store the event id. If you have seen it, return 2xx and stop.
  3. Do not rely on order. Use created_at and the fields in the event, or read the current state from GET /v1/customers/:app_user_id with your secret key.
  4. Treat sandbox and test events as non-revenue.
  5. Return 2xx within 10 seconds. Do slow work after responding.