API Reference

Interact with Keyva programmatically.

Authentication

Authenticate management requests by sending your API key as a Bearer token in the Authorization header.

Authorization: Bearer sk_live_...

The validation endpoint is the exception: it takes a license key rather than an API key, and needs no authentication at all.

Generating API Keys

To generate an API key:

  1. Go to your Dashboard Settings.
  2. Scroll to the API Keys section.
  3. Click Create New Key.
  4. Copy the key immediately, as it will not be shown again.

Base URL

Every endpoint is versioned and prefixed with:

https://keyva.dev/api/v1

The short validation URL

Validation is also reachable without the version prefix, athttps://keyva.dev/validate. It is an alias for/api/v1/validate — same parameters, same response, same status codes — kept short because it is the one URL that ends up compiled into your customers' installs.

The short URL always points at the latest API version. When a new version of the validation API ships, /validate follows it, and its behaviour can change with it. Call the versioned URL directly if you need the contract to stay fixed:

https://keyva.dev/validate → always the latest version
https://keyva.dev/api/v1/validate → pinned to v1

Both URLs are supported indefinitely. v1 is current, so today they behave identically.

Endpoints

GEThttps://keyva.dev/api/v1/validate

Checks if a license key is valid, not expired, and not revoked.

Also available as https://keyva.dev/validate, which always resolves to the latest API version.

Parameters

  • key (string, required): The license key to validate. Can be passed as a query parameter or in the X-License-Key header.
  • release (string, optional): A version number (e.g., 1.0.0) to check eligibility for.
  • usage.clients (integer, optional): The installation's current unit count, for keys sold on a usage-priced plan — the highest count reported each calendar month is what the customer is billed for. clients and the generic usage are accepted as bare aliases. Ignored for every other key, and only counted when the answer is valid: true.
  • usage.ip (string, optional): The address the installation sees itself running on. On keys that track installs automatically it is preferred over the connection's source address for the allowed-IP list and check logs — software validating through a CDN or proxy is otherwise tracked by the proxy's address instead of its own. Internal addresses are expected; the point is noticing an installation moving to a new machine. Keys with a hand-managed IP allowlist ignore it.

Example Request

curl "https://keyva.dev/api/v1/validate?key=LICENSE_KEY"

# or, always the latest version:
curl "https://keyva.dev/validate?key=LICENSE_KEY"

# reporting a usage count and the machine's own address alongside the validation:
curl "https://keyva.dev/validate?key=LICENSE_KEY&usage.clients=137&usage.ip=10.0.1.5"

Response

{
  "valid": true,
  "expires_at": "2027-01-01T00:00:00Z",
  "token": "<Ed25519-signed JWT>"
}

An invalid key returns valid: false with areason — including a key that does not exist, which returns {"valid": false, "reason": "not_found"}.

Status codes

  • 200 — a definitive answer about the key. Read valid.
  • 400 — no key was supplied.
  • 429 — rate limited. Retry.
  • 503 — validation is temporarily unavailable. Retry; do not treat this as an invalid license.

Only a 200 tells you anything about the key itself. If your integration caches an offline key, coast on the cached value through429 and 503rather than failing closed.

POSThttps://keyva.dev/api/v1/licenses

Creates a new license.

Body Parameters

  • product_id (string, required): The ID of the product. Its prefix, separator, length and auto-allowed-IP settings shape the license.
  • type (string, optional): perpetual, subscription or trial. Recorded on the license; when omitted it is taken from the product (or its group). Validity is governed by the expiry, not by this.
  • expires_at (string, optional): Expiration date (ISO 8601). Omit for a license that never expires.
  • key (string, optional): Store the license under a key you choose instead of generating one. 1–255 printable non-whitespace ASCII characters; a key already in use returns 409.

Set on update, not on create

feature_codes, release_versions, allowed_ips and allowed_networks are ignored here. Create the license, then apply them with a PUT — one extra call, and safe to repeat. duration is likewise update-and-activate only: on create, send an absolute expires_at.

Example Request

curl -X POST https://keyva.dev/api/v1/licenses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_live_..." \
  -d '{"product_id": "prod_123", "expires_at": "2027-01-01T00:00:00Z"}'

Response

201 Created

{
  "key": "PROD_A1B2C3D4E5F6G7H8J9K",
  "product_id": "prod_123",
  "type": "perpetual",
  "status": "active",
  "expires_at": "2027-01-01T00:00:00Z",
  "created_at": "2026-01-01T09:15:00Z",
  "features": ["sso"],
  "releases": ["2.0.0"]
}

Note the names on the way out: a stored license reports features and releases, while requests name the same things feature_codes and release_versions. Read both if you compare what you sent against what came back.

Repeating a create safely

Creating a license is not idempotent on its own: if you send the request twice — a timeout, a retried job — you get two licenses. Supplying your own key makes it idempotent. Derive the key from the record you are issuing against (an order id, a subscription id) so every attempt derives the same one, and a repeat comes back 409: the first attempt landed, and that license is the one to use.

curl -X POST https://keyva.dev/api/v1/licenses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_live_..." \
  -d '{"product_id": "prod_123", "key": "ACME-4KJ2A-8FQ7T-9WZC3-M5NXE"}'

# 201 -> the license was created under your key
# 409 -> that key already exists; an earlier attempt created it

Keys you supply are stored verbatim, so the product's prefix, separator and length settings do not apply to them. Make them unguessable — derive them with an HMAC under a secret you hold, not from the record id alone — because anyone who can guess a key can present it to /validate.

PUThttps://keyva.dev/api/v1/licenses/:key

Updates an existing license. Every parameter is optional and only what you send is touched — anything omitted is left exactly as it is. This is the endpoint that applies feature codes, releases and IP restrictions, on a license the create endpoint has just minted or on one issued long ago.

Body Parameters

  • expires_at (string, optional): New expiration date (ISO 8601). Moves the expiry; it cannot be cleared back to "never" from here.
  • feature_codes (string[], optional): The feature codes enabled on the license. Replaces the list rather than adding to it.
  • release_versions (string[], optional): The release versions the license is valid for. Replaces the list.
  • allowed_ips (string[], optional): Allowed IP addresses. Replaces the list.
  • allowed_networks (string[], optional): Allowed network CIDRs. Replaces the list.

duration is not accepted here — send an absolute expires_at, or use /activate, which takes either.

Example Request

curl -X PUT https://keyva.dev/api/v1/licenses/LICENSE_KEY \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_live_..." \
  -d '{"feature_codes": ["sso", "premium"], "expires_at": "2028-01-01T00:00:00Z"}'
POSThttps://keyva.dev/api/v1/licenses/:key/activate

Activates a license that has been previously revoked or suspended, and optionally moves its expiry in the same call — how a resumed subscription comes back on the same key.

Body Parameters

  • expires_at (string, optional): New expiration date (ISO 8601). Wins if you send both.
  • duration (string, optional): A span measured from now (e.g., "14d", "1mo", "1y"), used when no expires_at is given.

Send an empty body ({}) to reactivate without touching the expiry.

Example Request

curl -X POST https://keyva.dev/api/v1/licenses/LICENSE_KEY/activate \
  -H "Authorization: Bearer sk_live_..."
POSThttps://keyva.dev/api/v1/licenses/:key/revoke

Revokes the license, preventing it from passing validation.

Example Request

curl -X POST https://keyva.dev/api/v1/licenses/LICENSE_KEY/revoke \
  -H "Authorization: Bearer sk_live_..."
DELETEhttps://keyva.dev/api/v1/licenses/:key

Permanently deletes the license.

Example Request

curl -X DELETE https://keyva.dev/api/v1/licenses/LICENSE_KEY \
  -H "Authorization: Bearer sk_live_..."
Status codes on the management endpoints
  • 200 / 201 — done. Create answers 201 with the license; update answers the updated license; revoke and activate answer {"success": true, "status": "..."}, and delete just {"success": true}.
  • 400 — the request was wrong: a malformed body, an unknown product_id, a key that is not printable ASCII. Retrying identically will fail identically.
  • 401 — missing or invalid API key.
  • 403 — your account is suspended, or the license would exceed your plan's allowance.
  • 404 — no such license under this API key. Ownership is checked first, so someone else's key is a 404, never a 403.
  • 409 — the key you supplied is already in use. For a derived key this is the answer to "did my earlier attempt land?", not a failure.
  • 5xx — we could not tell you either way. Retry; if you supplied your own key, the retry is safe.