The official Unkey TypeScript/JavaScript SDK provides two packages for managing API keys and rate limiting in your applications.
Packages
@unkey/api — Manage keys, APIs, and rate limits (requires root key)
@unkey/ratelimit — Standalone rate limiting (requires root key)
For framework-specific wrappers, see @unkey/nextjs and @unkey/hono .
Installation
npm install @unkey/api @unkey/ratelimit
Both packages support CommonJS and ES Modules. Compatible with Node.js 18+, Deno 1.25+, Bun 1.0+, Cloudflare Workers, and edge runtimes.
Initialization
Initialize the API client
import { Unkey } from "@unkey/api" ;
const unkey = new Unkey ({
rootKey: process . env . UNKEY_ROOT_KEY ! ,
});
Initialize the rate limiter
import { Ratelimit } from "@unkey/ratelimit" ;
const limiter = new Ratelimit ({
rootKey: process . env . UNKEY_ROOT_KEY ! ,
namespace: "my-app" ,
limit: 10 ,
duration: "60s" ,
});
Never expose your root key in client-side code. These SDKs are designed for server-side use only.
Verify API Keys
The most common operation — validate a user’s API key:
import { Unkey } from "@unkey/api" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
async function handleRequest ( request : Request ) {
const apiKey = request . headers . get ( "x-api-key" );
if ( ! apiKey ) {
return new Response ( "Missing API key" , { status: 401 });
}
try {
const { meta , data } = await unkey . keys . verifyKey ({
key: apiKey ,
});
if ( ! data . valid ) {
// Key is invalid, expired, rate limited, etc.
return new Response ( `Unauthorized: ${ data . code } ` , { status: 401 });
}
// Key is valid — access granted
return handleAuthenticatedRequest ( request , data );
} catch ( err ) {
console . error ( err );
return new Response ( "Service unavailable" , { status: 503 });
}
}
Verification Response
The data object contains:
Field Type Description validbooleanWhether the key passed all checks codestringStatus code (VALID, NOT_FOUND, RATE_LIMITED, INSUFFICIENT_PERMISSIONS, etc.) keyIdstringThe key’s unique identifier namestring?Human-readable name of the key metaobject?Custom metadata associated with the key expiresnumber?Unix timestamp (ms) when the key expires creditsnumber?Remaining uses (if usage limits set) enabledbooleanWhether the key is enabled rolesstring[]?Roles attached to the key permissionsstring[]?Permissions attached to the key identityobject?Identity info (if externalId was set) ratelimitsobject[]?Rate limit states
Verify with Permission Check
const { data } = await unkey . keys . verifyKey ({
key: apiKey ,
permissions: "documents.write" , // Required permission
});
if ( ! data . valid && data . code === "INSUFFICIENT_PERMISSIONS" ) {
return new Response ( "Forbidden" , { status: 403 });
}
Create API Keys
Issue new keys for your users:
const { meta , data } = await unkey . keys . create ({
apiId: "api_..." ,
// Optional but recommended
prefix: "sk_live" , // Visible prefix
externalId: "user_123" , // Link to your user
name: "Production key" , // Human-readable name
// Optional limits
expires: Date . now () + 30 * 24 * 60 * 60 * 1000 , // 30 days
remaining: 1000 , // Usage limit
refill: {
amount: 1000 ,
interval: "monthly" , // Auto-refill
},
ratelimits: [{
name: "requests" ,
limit: 100 ,
duration: 60000 , // 100/minute
}],
// Optional metadata
meta: {
plan: "pro" ,
createdBy: "admin" ,
},
});
// Send data.key to your user (only time you'll see it!)
console . log ( "New key:" , data . key );
console . log ( "Key ID:" , data . keyId );
The full API key is only returned once at creation. Unkey stores only a cryptographic hash.
Create Key with Permissions
const { data } = await unkey . keys . create ({
apiId: "api_..." ,
prefix: "sk_live" ,
permissions: [ "documents.read" , "documents.write" ],
roles: [ "admin" ],
});
Update Keys
Modify an existing key’s configuration:
await unkey . keys . update ({
keyId: "key_..." ,
// Update any fields
name: "Updated name" ,
meta: { plan: "enterprise" },
enabled: true ,
expires: Date . now () + 90 * 24 * 60 * 60 * 1000 ,
ratelimits: [{
name: "requests" ,
limit: 1000 , // Upgraded limit
duration: 60000 ,
}],
});
Delete Keys
Permanently revoke a key:
await unkey . keys . delete ({
keyId: "key_..." ,
});
Or disable temporarily (can re-enable later):
await unkey . keys . update ({
keyId: "key_..." ,
enabled: false ,
});
Rate Limiting
Using @unkey/ratelimit
Standalone rate limiting without API keys:
import { Ratelimit } from "@unkey/ratelimit" ;
const limiter = new Ratelimit ({
rootKey: process . env . UNKEY_ROOT_KEY ! ,
namespace: "api" ,
limit: 100 ,
duration: "60s" ,
});
async function handleRequest ( request : Request ) {
const userId = request . headers . get ( "x-user-id" ) ?? "anonymous" ;
const { success , remaining , reset } = await limiter . limit ( userId );
if ( ! success ) {
return new Response ( "Rate limit exceeded" , {
status: 429 ,
headers: {
"Retry-After" : Math . ceil (( reset - Date . now ()) / 1000 ). toString (),
"X-RateLimit-Remaining" : "0" ,
},
});
}
// Process request...
}
Cost-Based Rate Limiting
Expensive operations can consume more of the limit:
// Normal request costs 1
await limiter . limit ( userId );
// Expensive request costs 10
await limiter . limit ( userId , { cost: 10 });
Duration Options
// Seconds
new Ratelimit ({ duration: "60s" , limit: 60 });
// Minutes
new Ratelimit ({ duration: "1m" , limit: 100 });
// Hours
new Ratelimit ({ duration: "1h" , limit: 1000 });
// Days
new Ratelimit ({ duration: "1d" , limit: 10000 });
// Milliseconds
new Ratelimit ({ duration: 60000 , limit: 60 });
Rate Limit Response
interface RatelimitResponse {
success : boolean ; // Whether the request is allowed
remaining : number ; // Requests remaining in current window
reset : number ; // Unix timestamp (ms) when limit resets
limit : number ; // Total limit per window
}
Error Handling
Use try/catch for error handling:
try {
const { meta , data } = await unkey . keys . create ({
apiId: "api_..." ,
});
console . log ( "Request ID:" , meta . requestId );
console . log ( "Key ID:" , data . keyId );
} catch ( err ) {
console . error ( "Unkey API error:" , err );
// Handle error
}
Production-Ready Verification
import { Unkey } from "@unkey/api" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
async function verifyApiKey ( key : string ) : Promise < VerifyResult | null > {
try {
const { data } = await unkey . keys . verifyKey ({ key });
return data ;
} catch ( err ) {
// Network error, timeout, etc.
console . error ( "Unkey verification error:" , err );
// Decide on fallback behavior:
// - Return null and deny access (fail closed)
// - Return default allow (fail open - risky)
return null ;
}
}
TypeScript Types
The SDK is fully typed. Import types as needed:
import type {
VerifyKeyResult ,
CreateKeyRequest ,
CreateKeyResponse ,
UpdateKeyRequest ,
Key ,
Api ,
RatelimitResponse ,
} from "@unkey/api" ;
Framework Integration
Next.js
Use the @unkey/nextjs wrapper:
import { NextRequestWithUnkeyContext , withUnkey } from "@unkey/nextjs" ;
export const POST = withUnkey (
async ( req : NextRequestWithUnkeyContext ) => {
// Key is already verified
return Response . json ({
message: "Hello!" ,
keyId: req . unkey . data . keyId ,
});
},
{ rootKey: process . env . UNKEY_ROOT_KEY ! }
);
Hono
Use the @unkey/hono middleware:
import { Hono } from "hono" ;
import { unkey } from "@unkey/hono" ;
const app = new Hono ();
app . use (
"/api/*" ,
unkey ({
rootKey: process . env . UNKEY_ROOT_KEY ! ,
})
);
app . get ( "/api/protected" , ( c ) => {
const { keyId } = c . get ( "unkey" );
return c . json ({ message: "Access granted" , keyId });
});
Express
import express from "express" ;
import { Unkey } from "@unkey/api" ;
const app = express ();
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
const authMiddleware = async ( req , res , next ) => {
const apiKey = req . headers [ "x-api-key" ];
if ( ! apiKey ) {
return res . status ( 401 ). json ({ error: "Missing API key" });
}
try {
const { data } = await unkey . keys . verifyKey ({ key: apiKey });
if ( ! data . valid ) {
return res . status ( 401 ). json ({ error: data . code });
}
req . unkey = data ;
next ();
} catch ( err ) {
console . error ( err );
res . status ( 503 ). json ({ error: "Service unavailable" });
}
};
app . get ( "/api/protected" , authMiddleware , ( req , res ) => {
res . json ({
message: "Access granted" ,
keyId: req . unkey . keyId ,
});
});
app . listen ( 3000 );
Cloudflare Workers
import { Unkey } from "@unkey/api" ;
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const unkey = new Unkey ({ rootKey: env . UNKEY_ROOT_KEY });
const apiKey = request . headers . get ( "x-api-key" );
if ( ! apiKey ) {
return new Response ( "Missing API key" , { status: 401 });
}
const { data } = await unkey . keys . verifyKey ({ key: apiKey });
if ( ! data . valid ) {
return new Response ( `Unauthorized: ${ data . code } ` , { status: 401 });
}
return new Response ( "Access granted" );
} ,
} ;
Advanced Usage
Managing APIs
Create and manage API namespaces:
// Create an API
const { data } = await unkey . apis . createApi ({
name: "production-api" ,
});
console . log ( "API ID:" , data . apiId );
// Get API details
const api = await unkey . apis . getApi ({ apiId: "api_..." });
// List all keys in an API
const { data : keys } = await unkey . apis . listKeys ({
apiId: "api_..." ,
limit: 100 ,
});
Identity Management
Link keys to your users:
// Create key with identity
const { data } = await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" , // Your user ID
meta: {
email: "[email protected] " ,
plan: "pro" ,
},
});
// Verify returns identity info
const { data : verifyResult } = await unkey . keys . verifyKey ({
key: "sk_live_..." ,
});
console . log ( verifyResult . identity ?. externalId ); // "user_123"
Custom Caching
Reduce latency with caching:
import { Unkey } from "@unkey/api" ;
import { createCache } from "@unkey/cache" ;
import { MemoryStore } from "@unkey/cache/stores" ;
const unkey = new Unkey ({
rootKey: process . env . UNKEY_ROOT_KEY ! ,
cache: createCache ({
stores: [ new MemoryStore ({ persistentMap: new Map () })],
}),
});
Complete Examples
E-commerce API with Usage Limits
import { Unkey } from "@unkey/api" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
// Create a key for a new customer on the Pro plan
async function createCustomerKey ( customerId : string , plan : string ) {
const limits = {
free: { remaining: 100 , refill: null },
pro: { remaining: 10000 , refill: { amount: 10000 , interval: "monthly" as const } },
enterprise: { remaining: null , refill: null },
};
const { data } = await unkey . keys . create ({
apiId: process . env . UNKEY_API_ID ! ,
prefix: "shop" ,
externalId: customerId ,
name: ` ${ plan } plan` ,
remaining: limits [ plan ]. remaining ?? undefined ,
refill: limits [ plan ]. refill ?? undefined ,
meta: {
plan ,
customerId ,
},
});
return data . key ;
}
// Verify and track usage
async function handleApiRequest ( request : Request ) {
const apiKey = request . headers . get ( "authorization" )?. replace ( "Bearer " , "" );
if ( ! apiKey ) {
return new Response ( "Missing API key" , { status: 401 });
}
const { data } = await unkey . keys . verifyKey ({ key: apiKey });
if ( ! data . valid ) {
return new Response ( `Unauthorized: ${ data . code } ` , { status: 401 });
}
// Check if customer has credits
if ( data . credits !== undefined && data . credits <= 0 ) {
return new Response ( "Credits exhausted" , { status: 402 });
}
// Process request...
return new Response ( JSON . stringify ({
success: true ,
creditsRemaining: data . credits ,
}));
}
GitHub Repository
@unkey/api on GitHub Complete auto-generated API reference and source code
Next Steps
Next.js SDK Framework wrapper for Next.js
Hono SDK Middleware for Hono apps
Rate Limiting Guide Advanced rate limiting features
API Reference REST API documentation