Skip to content

Stripe

The Stripe channel sells to web and desktop versions of your app. Customers pay on a Stripe-hosted Checkout page and the money goes straight to your own Stripe account. Kasskeep uses your secret key to create Checkout Sessions and to read objects back; it records the result and sends events. Channel id: stripe, platform: web.

Capabilities: subscriptions, one-time purchases, refunds, refund lookup, chargebacks, reconciliation and test mode. Refunds are issued in the Stripe Dashboard; Kasskeep does not refund actively.

Terminal window
curl -X PUT https://api.kasskeep.com/v1/admin/tenants/ten_xxx/apps/app_xxx/credentials/stripe \
-H "Authorization: Bearer kkp_..." -H "Content-Type: application/json" \
-d '{"values": {"secret_key": "sk_test_...", "webhook_secret": "whsec_..."}}'

Or with the CLI, reading from files so keys stay out of shell history:

Terminal window
npx @kasskeep/cli apps update --id app_xxx --stripe-secret-key-file ./stripe-sk.txt --stripe-webhook-secret-file ./stripe-whsec.txt
  • secret_key: sk_live_... or sk_test_.... A restricted key (rk_live_..., rk_test_...) works if it can write Checkout Sessions and read Subscriptions, Invoices, Charges, Disputes and Events. The prefix sets the environment: sk_test → sandbox, sk_live → production.
  • webhook_secret: the endpoint signing secret whsec_... from the next step.
  • Other key names or prefixes are rejected with 400. Use one Kasskeep app for test keys and another for live keys.

In the Stripe Dashboard, Developers → Webhooks → Add endpoint:

  • URL: https://api.kasskeep.com/v1/stores/stripe/<app_id>/webhook (the app’s stripe_webhook_path).
  • Any API version. Kasskeep only takes object ids from events and reads the current state from the API.
  • Events:
Event Used for
checkout.session.completed One-time purchase paid; subscription started
checkout.session.async_payment_succeeded Delayed payment methods
invoice.paid First period, renewals, recovery
invoice.payment_failed Failed charge, grace period
customer.subscription.created, customer.subscription.updated, customer.subscription.deleted Plan changes, cancel and resume, expiry
charge.refunded Full and partial refunds
charge.dispute.created, charge.dispute.closed Chargebacks

Reveal the endpoint’s Signing secret and save it as webhook_secret.

For every request Kasskeep:

  • checks Stripe-Signature (v1 = HMAC-SHA256 of t + "." + body, 300 second tolerance) and returns 400 invalid_signature otherwise;
  • rejects events whose livemode does not match the key with 400 livemode_mismatch;
  • deduplicates by event id;
  • reads the object from the Stripe API and trusts only that, so retries and out-of-order events cannot roll state back (a failed API call returns 502 and Stripe retries);
  • ignores objects whose metadata.kasskeep_app_id is a different app, so one Stripe account can serve several apps and tenants.

Map each of your product ids to a Stripe price. The product id can be the same one you use in the App Store and Google Play.

Terminal window
npx @kasskeep/cli products set --id app_xxx --product-id pro_monthly --external-id price_1Q... --type subscription
npx @kasskeep/cli products set --id app_xxx --product-id gems_100 --external-id price_1Q... --type one_time
npx @kasskeep/cli products list --id app_xxx

type is subscription (a recurring price, Checkout mode=subscription) or one_time (mode=payment). A price can belong to one product id only (409 external_id_in_use).

Set the hosts that Checkout may return to. Each entry is a lowercase host, optionally with a port; *. matches any subdomain but not the bare domain:

Terminal window
npx @kasskeep/cli apps update --id app_xxx --checkout-return-hosts "play.example.com,*.preview.example.com"

success_url and cancel_url must be https on one of these hosts; only localhost and 127.0.0.1 may use http. With an empty list every checkout fails with 422 return_hosts_not_configured. This prevents the endpoint from being used as an open redirect.

From the client, with the public key:

POST /v1/checkout
X-Kasskeep-Key: pk_...
{"app_user_id": "6f1c...", "channel": "stripe", "product_id": "pro_monthly",
"success_url": "https://play.example.com/done?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://play.example.com/shop", "quantity": 1}

The response is {"url": "https://checkout.stripe.com/...", "session_id": "cs_..."}. Send the browser to url. Returning to success_url does not prove payment; grant access from the webhook or GET /v1/customers. The SDKs wrap this call: checkout() and KasskeepWeb.checkout().

  • One-time purchases: the transaction id is the PaymentIntent id (pi_...).
  • Subscriptions: original_transaction_id is the Subscription id (sub_...); each period is a transaction keyed by the Invoice id (in_...).
  • Amounts are stored in the original currency, price_micros in millionths of a unit (zero-decimal currencies such as JPY are converted correctly).
Stripe status status
active, trialing active (canceled when cancel_at_period_end)
past_due grace_period until the next payment attempt, billing_retry when Stripe stops retrying
unpaid, paused billing_retry
canceled, incomplete_expired expired
incomplete Not recorded
Event From Stripe
purchase Checkout completed and paid
subscription.started First invoice paid
subscription.renewed Renewal invoice paid; data.recovered after a failed charge
subscription.product_changed Price changed, effective immediately
subscription.canceled, subscription.uncanceled cancel_at_period_end turned on or off
subscription.billing_issue invoice.payment_failed
subscription.grace_period_expired Grace period ended without payment
subscription.expired Subscription canceled; data.expiration_reason is VOLUNTARY, BILLING_RETRY or DISPUTE
refund Full refund revokes; partial refund only notifies (data.partial: true)
chargeback Dispute opened. Access is not revoked
chargeback.closed Dispute closed with data.outcome. On lost access is revoked and a refund with data.reason: "chargeback_lost" is sent

Every hour Kasskeep reads GET /v1/events from Stripe for apps with a secret key (first run looks back 3 days, then overlaps by one hour, at most 29 days) and runs each event through the webhook handler.

  • Use an sk_test_ key. Card 4242 4242 4242 4242 succeeds, 4000 0000 0000 0341 attaches but fails on renewal, 4000 0000 0000 0259 creates a dispute.
  • Create the webhook endpoint in Stripe’s test mode and give the test app its own whsec_ secret.
  • Objects created by stripe trigger have no kasskeep_app_id metadata and are ignored. Start from POST /v1/checkout to test the full flow; use a Stripe test clock to fast-forward renewals.