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.
1. Credentials
Section titled “1. Credentials”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:
npx @kasskeep/cli apps update --id app_xxx --stripe-secret-key-file ./stripe-sk.txt --stripe-webhook-secret-file ./stripe-whsec.txtsecret_key:sk_live_...orsk_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 secretwhsec_...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.
2. Webhook endpoint
Section titled “2. Webhook endpoint”In the Stripe Dashboard, Developers → Webhooks → Add endpoint:
- URL:
https://api.kasskeep.com/v1/stores/stripe/<app_id>/webhook(the app’sstripe_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 oft + "." + body, 300 second tolerance) and returns400 invalid_signatureotherwise; - rejects events whose
livemodedoes not match the key with400 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_idis a different app, so one Stripe account can serve several apps and tenants.
3. Map products and return hosts
Section titled “3. Map products and return hosts”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.
npx @kasskeep/cli products set --id app_xxx --product-id pro_monthly --external-id price_1Q... --type subscriptionnpx @kasskeep/cli products set --id app_xxx --product-id gems_100 --external-id price_1Q... --type one_timenpx @kasskeep/cli products list --id app_xxxtype 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:
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.
4. Start a checkout
Section titled “4. Start a checkout”From the client, with the public key:
POST /v1/checkoutX-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().
Recording and events
Section titled “Recording and events”- One-time purchases: the transaction id is the PaymentIntent id (
pi_...). - Subscriptions:
original_transaction_idis the Subscription id (sub_...); each period is a transaction keyed by the Invoice id (in_...). - Amounts are stored in the original currency,
price_microsin 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 |
Reconciliation
Section titled “Reconciliation”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.
Testing
Section titled “Testing”- Use an
sk_test_key. Card4242 4242 4242 4242succeeds,4000 0000 0000 0341attaches but fails on renewal,4000 0000 0000 0259creates a dispute. - Create the webhook endpoint in Stripe’s test mode and give the test app its own
whsec_secret. - Objects created by
stripe triggerhave nokasskeep_app_idmetadata and are ignored. Start fromPOST /v1/checkoutto test the full flow; use a Stripe test clock to fast-forward renewals.