keys.verifyKey on every request that carries a key. Send the key exactly as your user sent it. Unkey runs the checks set on the key, plus any you add to the request, and returns one answer. The response is HTTP 200 for every outcome, so read data.valid and data.code, not the status code.
You need a root key with the permissions listed on this page. Create one in the dashboard under Settings > Root Keys, and pass it as
Authorization: Bearer <root key>. See Permission reference for every permission.api.*.verify_key or api.<api_id>.verify_key for the key’s keyspace. Without it you get a 200 with code: NOT_FOUND, not a permission error. A missing or invalid root key gets HTTP 401. See Root key permissions.
Request
string
required
The key your user sent, 1 to 512 characters, including its prefix. Any change to the string gives
NOT_FOUND. You don’t pass an API ID because the key already belongs to one keyspace.string[]
1 to 5 keyspace IDs (
ks_...), each up to 100 characters, matched exactly. The key must belong to one of them or the result is NOT_FOUND. Use it when one root key verifies several keyspaces but an endpoint should only accept keys from some of them. An empty array is HTTP 400.string
A permission query, 1 to 1000 characters, such as
documents.read AND (billing.read OR billing.write). AND binds tighter than OR, and parentheses group. Fails the verification with INSUFFICIENT_PERMISSIONS when the key’s direct and role-derived permissions don’t satisfy it. A malformed query is HTTP 400 err:user:bad_request:permissions_query_syntax_error. Asterisks inside slugs are matched literally. See Permission queries.object
credits.cost (integer, 0 to 1,000,000,000,000) is how many credits this verification spends. Omitting the object spends 1 on keys that have credits configured and nothing on unlimited keys. cost: 0 checks the key without spending. See Credits and refill.object[]
Rate limits to check on this call. Each entry names a limit configured on the key or its identity (
name, 3 to 255 characters) and may pass cost (default 1). Naming a limit that doesn’t exist on the key or its identity fails the whole request with HTTP 412 err:unkey:application:precondition_failed. An entry that also carries both limit and duration defines an inline, key-scoped limit for this call instead. Limits with autoApply: true are checked whether or not you list them.An inline limit must be 1 or more and duration at least 1000 milliseconds. Values below that aren’t rejected with an error. Instead the verification skips rate limiting entirely, including the auto-applied limits, and can still return VALID. Check these two values in your own code before you send them.string[]
Up to 20 strings of up to 512 characters, recorded with the verification for analytics only. They never affect the verdict. See Metadata and tags.
string
Up to 256 characters. Lets Unkey match a key imported from another provider’s hash format on its first use and convert it to the native hash. See Migrating existing keys.
Order of checks
Checks run in this order and stop at the first failure. The order decides whichcode you get, and whether a rate limit or credits get used up.
- Existence. The key must exist, its workspace and keyspace must not be deleted, and your root key must be allowed to verify keys in that keyspace. Anything else is
NOT_FOUND. - Keyspace restriction. If the request lists
keyspacesand the key’s keyspace isn’t one of them, the result isNOT_FOUNDwith no key details. - Workspace. A disabled workspace produces
FORBIDDEN. - Enabled. A key with
enabled: falseproducesDISABLED. - Expiration. A key whose
expiresis in the past producesEXPIRED. - IP allow list. If the keyspace has one, the request’s client IP must be on it, or the result is
FORBIDDEN. - Permissions. If the request carries a
permissionsquery, the key must satisfy it, or the result isINSUFFICIENT_PERMISSIONS. - Rate limits. Auto-applied limits and limits named in the request are checked together. If any is exceeded, the result is
RATE_LIMITED, and the other limits aren’t used up. - Credits. Only if everything above passed, the cost is deducted. Not enough credits gives
USAGE_EXCEEDEDand nothing is deducted.
Response
boolean
required
true only when every check passed. Read this first.string
required
One of
VALID, NOT_FOUND, FORBIDDEN, INSUFFICIENT_PERMISSIONS, USAGE_EXCEEDED, RATE_LIMITED, DISABLED, EXPIRED. Code values explains each one.string
The key’s identifier. Present for every outcome except
NOT_FOUND.string
The keyspace the key belongs to (
ks_...). Present for every outcome except NOT_FOUND.string
The key’s internal name, when set.
boolean
Whether the key is enabled.
integer
Expiry as Unix milliseconds, when set.
integer
Credits remaining after this call. Omitted for unlimited keys. After
USAGE_EXCEEDED it’s the unchanged balance.object
The key’s metadata, when set.
string[]
Every permission slug the key holds, directly or through roles.
string[]
Every role name assigned to the key.
object
When the key is linked to an identity:
id, externalId, the identity’s meta, and its ratelimits (each with id, name, limit, duration, autoApply).object[]
One entry per rate limit that was checked:
id (empty for inline limits), name, limit, duration, remaining, reset (Unix milliseconds), exceeded, and autoApply.Code values
Why some failures say NOT_FOUND
Unkey never tells a caller that a key exists unless they’re allowed to verify it. So you get a 200 withcode: NOT_FOUND when the key belongs to another workspace, its keyspace was deleted, your root key has no verify permission for its keyspace, or your keyspaces list leaves it out. The response looks the same in every case. If you get NOT_FOUND for a key you know exists, check your root key’s permissions first.
How quickly changes take effect
A change to a key (disable, delete, new expiry, new permissions or roles) takes about 10 seconds to reach verification, and a few verifications just after that can still see the old state. Plan incident runbooks around a little more than 10 seconds, not an instant cutoff. Credit balances change instantly. Rate limit counts sync across regions within moments, soremaining is approximate. See How rate limiting works.
What gets recorded
Every verification, whatever its outcome, is recorded in analytics with its outcome, key, identity, tags, credits spent, and latency. Verifications made with a root key also produce akey.verify audit log event. See Analytics and Audit logs.