executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
unkeyUnkey is an open-source API key management service: it issues, verifies and revokes API keys with built-in rate limits, usage credits, expiry, roles and permissions. Use when the user wants to add API key authentication to an API, issue keys to customers, rate limit per key, meter usage, rotate keys, or self-host key infrastructure instead of building it.
Independent GitHub snapshot · Automated classification · Original documentation governs
Unkey stores hashed API keys for you and answers one question fast: "is this key valid, and what is it allowed to do?" You create keys inside an API (called a keyspace in the v2 docs), attach limits and permissions, and call verifyKey from your server on each request.
This skill targets the v2 API and @unkey/api 2.x (2.5.x at the time of writing). The old v1 SDK (verifyKey imported as a function, result.valid, ownerId, remaining, ratelimit: { type: "fast" }, keys.delete({keyId}) on a flat client) is gone: v2 responses are wrapped as { meta, data }, the owner is externalId, and rate limits are an array of named limits.
Two kinds of secret exist: a root key (from the dashboard, used by your backend to call Unkey; never ship it to a client) and the API keys you hand to your customers.
npm install @unkey/api
export UNKEY_ROOT_KEY=... # create it in the dashboard with only the permissions you need
export UNKEY_API_ID=... # the api_... id of the API that will own the keys
import { Unkey } from "@unkey/api";
const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });
Create a key (unkey.keys.createKey). Fields: apiId, prefix, name, externalId (your customer id), meta, roles, permissions, expires (Unix ms), credits ({ remaining, refill? }), ratelimits (array of { name, limit, duration (ms), autoApply }), enabled, recoverable, byteLength (default 16). The response is { data: { key, keyId } }; key is shown once.
Verify on every request (unkey.keys.verifyKey). Send key, and optionally permissions (a query such as "documents.read AND documents.write"), credits: { cost }, ratelimits: [{ name, cost }], tags. Read data.valid and data.code (VALID, NOT_FOUND, FORBIDDEN, INSUFFICIENT_PERMISSIONS, USAGE_EXCEEDED, RATE_LIMITED, DISABLED, EXPIRED). A rejected key is a normal 200 response with valid: false, not an exception; network or auth failures throw UnkeyError.
Manage the lifecycle: keys.updateKey (pass null to clear a field), keys.updateCredits, keys.deleteKey (permanent: true to hard delete), keys.rerollKey (new key plus a grace period for the old one), apis.listKeys (filter by externalId; the result is an async iterable: for await (const page of result)), keys.addRoles / setRoles / addPermissions.
Usage analytics: analytics.getVerifications({ query }) takes a read-only SQL SELECT over the key_verifications_*_v1 views; check the dashboard or docs for column names before writing queries.
const created = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID!,
prefix: "sk_live",
name: "Northwind Traders production key",
externalId: "customer-42",
meta: { plan: "pro", team: "engineering" },
permissions: ["api.read", "api.write"],
ratelimits: [{ name: "requests", limit: 100, duration: 60_000, autoApply: true }],
credits: { remaining: 10_000 },
expires: Date.now() + 30 * 24 * 60 * 60 * 1000, // optional: 30-day trial
});
console.log(created.data.key); // sk_live_... show it to the customer once
console.log(created.data.keyId); // key_... store this to manage the key later
Result: the customer gets one secret; you keep only keyId. After 10,000 verifications verifyKey returns USAGE_EXCEEDED.
async function requireApiKey(req: Request): Promise<Response | { customerId?: string }> {
const key = req.headers.get("Authorization")?.replace(/^Bearer /, "");
if (!key) return new Response("Missing API key", { status: 401 });
const { data } = await unkey.keys.verifyKey({
key,
permissions: req.method === "GET" ? "api.read" : "api.write",
});
if (!data.valid) {
const status = data.code === "RATE_LIMITED" ? 429
: data.code === "NOT_FOUND" ? 401 : 403;
return Response.json({ error: data.code }, { status });
}
return { customerId: data.identity?.externalId };
}
Result: unknown keys get 401, over-limit keys 429, expired/disabled/under-privileged keys 403. Wrap the call in try/catch and decide whether to fail closed (recommended) when Unkey is unreachable.
const rerolled = await unkey.keys.rerollKey({
keyId: "key_2cGKbMxRyIzhCxo1Idjz8q",
expiration: 24 * 60 * 60 * 1000, // old key keeps working for 24 hours
});
console.log(rerolled.data.key); // new secret, returned once
Result: the new key carries the same settings; the old one stops working after the grace period (use 0 to cut it off at once for a real leak).
autoApply: true; otherwise name them in verifyKey({ ratelimits: [{ name }] }).meta is returned on every verification: no secrets, keep it small (docs advise under 10 KB).sk_live / sk_test, nothing sensitive.recoverable: true, which stores it encrypted: use sparingly).valid: true result for long; revocation and credits are checked by Unkey on each verify.