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
Section titled “Errors”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 |
Rate limits
Section titled “Rate limits”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.
Idempotency
Section titled “Idempotency”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.
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.commatches anyhttps://subdomain at any depth, but notexample.comitself,http://, or origins with a port.http://localhost:*andhttp://127.0.0.1:*are allowed only while the app is in a non-production mode:apple_environmentissandbox, 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.
Health
Section titled “Health”GET /health returns {"ok": true}.