Skip to main content
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:
FieldTypeDescription
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

Build docs developers (and LLMs) love