API Reference
Interact with Keyva programmatically.
Authentication
Authenticate management requests by sending your API key as a Bearer token in the Authorization header.
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:
- Go to your Dashboard Settings.
- Scroll to the API Keys section.
- Click Create New Key.
- Copy the key immediately, as it will not be shown again.
Base URL
Every endpoint is versioned and prefixed with:
https://keyva.dev/api/v1The 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:
Both URLs are supported indefinitely. v1 is current, so today they behave identically.
Endpoints
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 theX-License-Keyheader.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.clientsand the genericusageare accepted as bare aliases. Ignored for every other key, and only counted when the answer isvalid: 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. Readvalid.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.
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,subscriptionortrial. 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 itKeys 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.
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"}'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 noexpires_atis 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_..."
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_..."
Permanently deletes the license.
Example Request
curl -X DELETE https://keyva.dev/api/v1/licenses/LICENSE_KEY \ -H "Authorization: Bearer sk_live_..."
- 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
keyyou 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.