kasskeep

The ledger behind your in-app revenue.

Kasskeep verifies purchases from the App Store, Google Play and Stripe, tracks every subscription through renewals and refunds, confirms rewarded-ad views, and tells your backend about all of it in one signed event format. It is a hosted service, so there is nothing for you to run.

POST /webhooks/kasskeep
Kasskeep-Event-Type: subscription.renewed
Kasskeep-Signature: t=1767225600,v1=5257a869e7ec…08d8bd
{
  "id": "evt_3f0c8a91d2b74e6f",
  "type": "subscription.renewed",
  "app_user_id": "6f1c2a3b-4d5e-4f60-8a71-92b3c4d5e6f7",
  "platform": "ios",
  "environment": "production",
  "product_id": "pro_monthly",
  "expires_at": "2026-12-01T12:00:00.000Z",
  "price_micros": 9990000,
  "currency": "USD"
}

signature verified

Every store, every ad network, one event shape. Signed with HMAC-SHA256 and retried until your server answers 2xx.

Connects to

  • Apple App Store
  • Google Play
  • Stripe
  • Test store
  • AdMob
  • AppLovin MAX
  • Unity LevelPlay

Where Kasskeep sits

Your app hands purchases to Kasskeep instead of trusting the device. Stores and ad networks report to Kasskeep. Your backend hears from one place.

Your app React Native SDK, WebView bridge, web script
receipts
Kasskeep Verify, deduplicate, store, reconcile
signed events
Your backend Signed webhooks, events and customer API
notifications
Stores and ad networks Server notifications, RTDN, Stripe webhooks, S2S reward callbacks

What it does today

Verification you can trust

StoreKit 2 transactions are checked against Apple's root certificates, Google purchases against the Play Developer API, and Stripe objects are re-fetched from the Stripe API before anything is recorded.

One event model

Purchases, renewals, cancellations, grace periods, refunds, chargebacks and ad rewards arrive as 18 event types with the same fields on every channel.

Delivery that survives outages

A failed webhook is retried for about two and a half days, eight attempts in all. Event ids are stable, so duplicates are easy to drop, and GET /v1/events lets you catch up.

Hourly reconciliation

Kasskeep pulls Apple notification history, Google voided purchases and Stripe events every hour and fills in anything a notification missed.

Your data, walled off

Every account is isolated with Postgres row-level security and its own encryption key for store credentials. Team members get roles, and sign-in always needs a second factor.

Rewarded ads, verified server-side

AdMob SSV, AppLovin MAX and Unity LevelPlay callbacks are verified, deduplicated and mapped to your own ad unit names. Unknown ad units are rejected by default.

See all features

From zero to the first signed event

  1. Open an account

    The self-service console is coming soon. Until then we open accounts on request and invite you as the owner.

  2. Create an app

    One CLI command returns a public key for the client, a secret key for your backend and a webhook signing secret.

  3. Connect stores

    Upload your App Store and Play credentials and point each store's server notifications at your Kasskeep URL.

  4. Add the SDK

    Configure the React Native SDK with the public key. Purchases go to Kasskeep before the transaction is finished.

  5. Handle events

    Verify the Kasskeep-Signature header, deduplicate by event id and grant or revoke access in your own code.

A few lines on each side

In the app
import { Kasskeep } from "@kasskeep/react-native";

Kasskeep.configure({
  apiKey: "pk_...",
  baseUrl: "https://api.kasskeep.com",
  appUserId: user.id,
});

const result = await Kasskeep.purchase({
  productId: "pro_monthly",
  type: "subs",
});
if (result.status === "completed") showPro(result.customer);
On your server
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody) {
  const p = Object.fromEntries(
    header.split(",").map((part) => part.split("=")),
  );
  const age = Math.abs(Date.now() / 1000 - Number(p.t));
  if (age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${p.t}.${rawBody}`)
    .digest();
  const actual = Buffer.from(p.v1, "hex");
  return actual.length === expected.length
    && timingSafeEqual(actual, expected);
}

Test the whole flow without a store

The built-in test store runs purchases, renewals, grace periods, refunds and revocations through the same pipeline as real stores, tagged environment: test. It works in Expo Go, and the server can fast-forward a subscription to its next renewal.

How the test store works

Start with the documentation

Every endpoint, parameter and event in the docs is written from the source code.

Talk to us

Questions, or want an account before the console opens? Write to us at [email protected]