Skip to content

Quickstart

This guide takes you from a new account to the first signed event on your backend. Kasskeep is a hosted service: you do not run any servers for it. The API lives at https://api.kasskeep.com.

The self-service console is coming soon. Until it launches, accounts are opened on request: contact us and we create your tenant and invite you as its owner.

The invitation contains a one-time token. Accept it and set your password, then enroll two-step verification. TOTP is mandatory for every member.

Terminal window
curl -X POST https://api.kasskeep.com/v1/admin/invitations/accept \
-H "Content-Type: application/json" \
-d '{"token": "<invitation token>", "password": "<your password>", "name": "Your Name"}'

The response contains a session that can only enroll TOTP. Call POST /v1/admin/me/totp/setup, add the returned otpauth_uri to your authenticator app, and confirm with POST /v1/admin/me/totp/enable and {"code": "123456"}. Then sign in and create a personal access token for the CLI:

Terminal window
curl -X POST https://api.kasskeep.com/v1/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "...", "totp_code": "123456"}'
curl -X POST https://api.kasskeep.com/v1/admin/me/tokens \
-H "Authorization: Bearer kks_..." -H "Content-Type: application/json" \
-d '{"name": "cli", "expires_in_days": 90}'

The token (kkp_...) is shown once. Store it in your password manager. See Keys and access.

Each product that sells something is an app in your tenant. The CLI runs with npx and needs Node.js 22 or later. Log in once with your token; it is saved in ~/.config/kasskeep/credentials:

Terminal window
npx @kasskeep/cli login # paste the kkp_... token when asked
npx @kasskeep/cli apps create \
--name "My App" \
--ios-bundle-id com.example.app \
--android-package com.example.app \
--webhook-url https://api.example.com/webhooks/kasskeep

The output contains three keys. They are shown once and cannot be read back later.

Key Where it goes
public_key (pk_...) The app. It can submit receipts and read the user’s own state.
secret_key (sk_...) Your backend only. It can read transaction details and the event log.
webhook_secret (whsec_...) Your backend only. It verifies webhook signatures.

Note the id (app_...): store notification URLs contain it.

  1. In App Store Connect, open Users and Access → Integrations → In-App Purchase, generate a key and download the .p8 file. Note the Key ID and Issuer ID.
  2. Upload it. The CLI reads the file locally and sends it over HTTPS; Kasskeep encrypts it and never returns it.
Terminal window
npx @kasskeep/cli apps update --id app_xxx \
--apple-issuer-id <Issuer ID> --apple-key-id <Key ID> --apple-app-apple-id 1234567890 \
--apple-private-key-file ./AuthKey_XXXX.p8
  1. In App Information → App Store Server Notifications, choose Version 2 and set both the production and sandbox URL to https://api.kasskeep.com/v1/stores/apple/<app_id>/notifications.

Create a service account, grant it access in Play Console, upload its JSON key and set up a Pub/Sub push subscription to https://api.kasskeep.com/v1/stores/google/<app_id>/rtdn. The Google Play page has every step.

Turn on the test store and you can run the rest of this guide without any store account:

Terminal window
npx @kasskeep/cli test-store enable --id app_xxx

Install @kasskeep/react-native (see React Native and Expo) and configure it with the public key. appUserId is your own user id and must be a UUID.

import { Kasskeep } from "@kasskeep/react-native";
Kasskeep.configure({
apiKey: "pk_...",
baseUrl: "https://api.kasskeep.com",
appUserId: user.id,
testStore: __DEV__,
});
const result = await Kasskeep.purchase({ productId: "pro_monthly", type: "subs" });
if (result.status === "completed") {
console.log(result.customer.subscriptions);
}

The SDK sends the transaction to Kasskeep and only finishes it after Kasskeep has stored it. Unfinished transactions are submitted again on the next launch.

Kasskeep POSTs every event to the app’s webhook_url. Verify the signature against the raw request body, deduplicate by event id, then act on it.

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const secret = process.env.KASSKEEP_WEBHOOK_SECRET;
const app = express();
app.post("/webhooks/kasskeep", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Kasskeep-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = createHmac("sha256", secret).update(`${parts.t}.`).update(req.body).digest();
const actual = Buffer.from(parts.v1 ?? "", "hex");
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "test") return res.sendStatus(200);
if (alreadyHandled(event.id)) return res.sendStatus(200);
handleEvent(event);
res.sendStatus(200);
});

Send a signed test event to check the endpoint:

Terminal window
curl -X POST https://api.kasskeep.com/v1/admin/tenants/<tenant_id>/apps/<app_id>/webhooks/test \
-H "Authorization: Bearer kkp_..."

The response shows the status code, timing and the first 500 bytes of your endpoint’s reply.