Skip to content

API overview

The API is JSON over HTTPS at https://api.kasskeep.com. All paths start with /v1; changes within v1 are additive.

This reference is written from the service’s route definitions. An OpenAPI specification is planned.

Group Who calls it Auth
Client API Your app or web page X-Kasskeep-Key: pk_...
Server API Your backend Authorization: Bearer sk_...
Inbound callbacks Stores and ad networks The app id in the path plus the channel’s own signature
Admin API You, the CLI, automation Authorization: Bearer kks_... (session) or kkp_... (personal access token)

Errors use the HTTP status and this body:

{ "error": { "code": "invalid_public_key", "message": "invalid_public_key" } }

code is stable; message is for people and may change.

Status Common codes
400 invalid_json, invalid_request (the message lists each failing field), invalid_app_user_id, invalid_environment, invalid_cursor
401 invalid_public_key, invalid_secret_key, invalid_admin_token, invalid_credentials
403 tenant_suspended, forbidden, test_store_disabled, test_ads_disabled
404 not_found, app_not_found, tenant_not_found
409 Conflicts such as already_subscribed, external_id_in_use, ad_unit_id_in_use
422 Missing configuration, such as android_not_configured, stripe_not_configured
429 rate_limited
502 An upstream store API was unavailable; safe to retry

The client and server APIs are rate limited per tenant with a token bucket. Over the limit they return 429 rate_limited. Inbound store callbacks are never rate limited, so no notification is dropped.

Submitting the same store transaction twice records it once and returns the same customer. Restores are idempotent too. Inbound callbacks are deduplicated by each channel’s own id.

Browsers can call Kasskeep directly (web checkout, the test store from a web page, customer lookup) only from origins in the app’s cors_origins. The list is empty by default.

Terminal window
npx @kasskeep/cli apps update --id app_xxx --cors-origins "https://game.example.com,*.example.com"
  • A full origin such as https://game.example.com (with a port if not default).
  • *.example.com matches any https:// subdomain at any depth, but not example.com itself, http://, or origins with a port.
  • http://localhost:* and http://127.0.0.1:* are allowed only while the app is in a non-production mode: apple_environment is sandbox, or the test store or test ads are on.

Only these public-key endpoints send CORS headers: POST /v1/receipts, POST /v1/checkout, GET /v1/customers/:id, POST /v1/customers/:id/restore, POST /v1/test-store/purchases, GET /v1/test-store/products, GET /v1/customers/:id/ad-rewards, POST /v1/ads/test/rewards.

The response echoes the specific origin with Vary: Origin and never sets Access-Control-Allow-Credentials. Preflight requests carry no key, so they pass if any app lists the origin; the real request is then checked against that app’s own list. A failed key check returns no CORS headers and shows up in the browser as a CORS error, so check the key first.

GET /health returns {"ok": true}.