Purchase verification & licensing API

The server that vouches for your app's purchases and license keys.

Keywarden turns App Store and Google Play purchases and license keys into one signed entitlement bundle your app verifies offline — ECDSA P-256, no header, no server round trip after the first sync, and never a lockout on a network hiccup. Nothing to run yourself: add the SDK, ship.

Invite-only while we scale. Free for everyone in early access.

POST /v1/store/verify · api.keywarden.dev
// request
{ "store": "play", "productId": "pro",
  "purchaseToken": "…", "device": { "installId": "…" } }

// response — signed, verifiable offline
{ "ok": true,
  "purchase": { "state": "active" },
  "bundle": "eyJ2IjoyLCJhcHAiOi….MEUCIQ…" }
IN PRODUCTIONVerifying purchases for KeyFlow on Google Play
SDKSAndroid 6.0+ (Kotlin & Java) · iOS 15+ & macOS 12+ (Swift)
SECURITYECDSA P-256 signed entitlements · keys never leave your app
How it works

Four calls, no infrastructure to run.

01

Create an app

Sign in at the dashboard, create an app, get a public key (pk_live_…) and a signing key pair.

02

Embed & pin

Ship the public key in your binary; pin the signing key's SPKI so bundles verify fully offline.

03

Sync & verify

Call /v1/devices/sync or /v1/store/verify; check the signed bundle on device.

04

Issue licenses

Selling outside the store too? Issue KW-… keys from the dashboard or the CLI.

What's in the box

Everything a paid app needs, one API.

App Store & Google Play verification

Receipts are checked against the store and recorded. StoreKit 2 transactions are verified against Apple Root CA G3 offline, then confirmed with the App Store Server API; refunds arrive as signed server notifications. The voided-purchases list is polled in the background so refunds drop on the next check — and if that list is ever unreachable, the health endpoint says so instead of silently letting a refund through.

License keys with floating seats

KW-XXXX-XXXX-XXXX-XXXX keys, issued from the dashboard or the CLI. Seats can be fixed or floating — a floating seat frees itself after a heartbeat TTL, or the moment a device deactivates.

Signed entitlement bundles, verified offline

The server never says "you own it" — it signs it, ECDSA P-256, no header. The app verifies the bundle on device, so editing a preferences file on a rooted phone buys nothing.

Devices & GDPR erase

Every install is tracked (platform, last seen, active entitlements) from the dashboard. A single erase call removes a device and everything tied to it, in one transaction.

Metered features, trials & gift thresholds

"Try it 3 times" counters live on the server, not in a local file. Optional purchases add up and unlock a tier once a threshold is reached; amounts freeze at purchase time, so a later price change never rewrites history.

A dashboard for all of it

Overview, devices, stores, license keys, catalog, signing keys, audit log — all at app.keywarden.dev, sign-in by magic link.

Management API, fully documented

Organizations, apps, entitlements, licenses, devices, audit log — behind sk_live_… keys, with a published OpenAPI schema.

kw — a real CLI

Create apps, issue and revoke license keys, list devices, run a full self-test or inspect a bundle — without leaving the terminal.

Signing-key rotation, safely

Rotate a signing key and the old one stays valid through the overlap window. A key with usage in the last 30 days can't be retired — no silently bricked clients.

Integrate in an afternoon

Configure once. Ask anywhere.

Android · Kotlin (Java-friendly) Source available · Maven Central soon
Keywarden.configure(
    Config.Builder(this)
        .apiKey(BuildConfig.KEYWARDEN_PK)
        .publicKey("k1", SPKI_B64)   // pinned; verifies offline
        .appSlug("my-app")
        .build()
)

Keywarden.get().sync(object : SyncCallback {
    override fun onResult(verdict: Verdict, error: KeywardenException?) { refreshUi() }
})
if (Keywarden.get().has("pro")) unlockPro()   // local, never blocks

Android SDK docs →

iOS & macOS · Swift Source available · SwiftPM
let client = Keywarden.configure(try Configuration(
    apiKey: "pk_live_…",
    baseURL: URL(string: "https://api.keywarden.dev")!,
    publicKeys: ["k1": SPKI_B64],   // pinned; verifies offline
    appSlug: "my-app"
))

// StoreKit 2: launch scan + Transaction.updates, verified server-side
StoreKitSync(client: client, source: StoreKitTransactionSource()).start()

if client.has("pro") { unlockPro() }   // local, never blocks

Apple SDK docs →

Early access

Ask for an invite.

Keywarden runs on our own infrastructure and every account costs us something, so we are letting people in a handful at a time. Leave your e-mail and we will send you a sign-in link when a slot opens.

Self-hosting instead? The server ships as a Docker image — see the docs. No invite needed.

Pricing

Free while we are in early access.

Everyone invited today uses Keywarden for free, with no card and no expiry date on what you have already shipped. The plans below are what we intend to charge once billing exists — they are published early so nothing about the price is a surprise later.

EARLY ACCESS · TODAY
$0

Everything below, while we are invite-only. Store verification, signed entitlements, license keys, trial counters, devices, dashboard and CLI.

Request an invite →
FREE · PLANNED
$0/ month

1 app · 100 users. Store verification, signed entitlements, trial counters, dashboard.

When billing ships
PRO · PLANNED
$19/ month

10 apps · 50,000 users. License keys and device activation, webhooks, e-mail support.

When billing ships
BUSINESS · PLANNED
$39/ month

25 apps · 200,000 users. Payment-provider integrations, 3 team members, priority support.

When billing ships

Billing, payment-provider webhooks (Paddle, Lemon Squeezy) and plan quotas Not built yet — no card is collected anywhere on this site today.