Skip to main content
Protect your Cloudflare Workers with Unkey’s globally distributed verification.

Install

npm create cloudflare@latest my-api
cd my-api
npm install @unkey/api

Basic Worker

src/index.ts
import { Unkey } from "@unkey/api";

export interface Env {
  UNKEY_ROOT_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // 1. Extract API key
    const authHeader = request.headers.get("Authorization");

    if (!authHeader?.startsWith("Bearer ")) {
      return Response.json(
        { error: "Missing API key" },
        { status: 401 }
      );
    }

    const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY });
    const apiKey = authHeader.slice(7);

    try {
      // 2. Verify with Unkey
      const { meta, data, error } = await unkey.keys.verifyKey({
        key: apiKey,
      });

      // 3. Handle errors
      if (error) {
        console.error("Unkey error:", error);
        return Response.json(
          { error: "Authentication service unavailable" },
          { status: 503 }
        );
      }

      // 4. Check validity
      if (!data.valid) {
        return Response.json(
          { error: data.code },
          { status: data.code === "RATE_LIMITED" ? 429 : 401 }
        );
      }

      // 5. Request is authenticated
      return Response.json({
        message: "Access granted",
        user: data.identity?.externalId,
        remaining: data.credits,
      });
    } catch (error) {
      console.error("Unkey error:", error);
      return Response.json(
        { error: "Authentication service unavailable" },
        { status: 503 }
      );
    }
  },
};

Configure Secrets

npx wrangler secret put UNKEY_ROOT_KEY
# Enter your API ID when prompted

With Hono

For a cleaner routing experience, use Hono:
npm install hono @unkey/hono
src/index.ts
import { Hono } from "hono";
import { unkey } from "@unkey/hono";

type Bindings = {
  UNKEY_ROOT_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

// Public routes
app.get("/health", (c) => c.json({ status: "ok" }));

// Protected routes with Unkey middleware
app.use("/api/*", async (c, next) => {
  const handler = unkey({
    rootKey: c.env.UNKEY_ROOT_KEY,
    getKey: (c) => c.req.header("Authorization")?.replace("Bearer ", ""),
    onError: (c, error) => {
      console.error("Unkey error:", error);
      return c.json({ error: "Service unavailable" }, 503);
    },
    handleInvalidKey: (c, result) => {
      return c.json({ error: result.code }, 401);
    },
  });
  
  return handler(c, next);
});

app.get("/api/data", (c) => {
  const auth = c.get("unkey");
  
  return c.json({
    message: "Access granted",
    user: auth.identity?.externalId,
    remaining: auth.credits,
  });
});

app.get("/api/users", (c) => {
  return c.json({ users: [] });
});

export default app;

Reusable Auth Middleware

Create a clean middleware pattern:
src/middleware/auth.ts
import { Unkey } from "@unkey/api";
import { UnkeyError } from "@unkey/api/models/errors";
import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components";
import { Context, Next } from "hono";

declare module "hono" {
  interface ContextVariableMap {
    auth: V2KeysVerifyKeyResponseData;
  }
}

interface AuthOptions {
  getKey?: (c: Context) => string | null;
  permissions?: string;
}

type Bindings = {
  UNKEY_ROOT_KEY: string;
};

export function authMiddleware(options: AuthOptions = {}) {
  return async (c: Context, next: Next) => {
    const getKey = options.getKey ?? ((c) =>
      c.req.header("Authorization")?.replace("Bearer ", "") ?? null
    );

    const apiKey = getKey(c);

    if (!apiKey) {
      return c.json({ error: "Missing API key" }, 401);
    }

    try {
      const unkey = new Unkey({ apiKey: c.env.UNKEY_ROOT_KEY });
      const { data, error } = await unkey.verifyKey({
        key: apiKey,
        permissions: options.permissions,
      });

      if (error) {
        console.error("Unkey error:", error);
        return c.json({ error: "Service unavailable" }, 503);
      }

      if (!data.valid) {
        if (data.code === "INSUFFICIENT_PERMISSIONS") {
          return c.json({ error: "Forbidden" }, 403);
        }
        return c.json({ error: data.code }, { status: 401 });
      }
      c.set("auth", data);
      await next();
    } catch (err) {
      return c.json({ error: "Service unavailable" }, 503);
    }
  };
}
Use it:
src/index.ts
import { Hono } from "hono";
import { authMiddleware } from "./middleware/auth";

const app = new Hono();

// Basic auth
app.use("/api/*", (c, next) => 
  authMiddleware()(c, next)
);

// Permission-based auth for admin routes
app.use("/admin/*", (c, next) => 
  authMiddleware({ 
    permissions: "admin",
  })(c, next)
);

app.get("/api/data", (c) => {
  const auth = c.get("auth");
  return c.json({ user: auth.identity?.externalId });
});

app.delete("/admin/users/:id", (c) => {
  // Only accessible with admin permission
  return c.json({ deleted: c.req.param("id") });
});

export default app;

Rate Limit Headers

Add standard rate limit headers:
src/middleware/auth.ts

import { Unkey } from "@unkey/api";
import { UnkeyError } from "@unkey/api/models/errors";
import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components";

export function authMiddleware(options: AuthOptions = {}) {
  return async (c: Context, next: Next) => {
    const apiKey = c.req.header("Authorization")?.replace("Bearer ", "");
    
    if (!apiKey) {
      return c.json({ error: "Missing API key" }, 401);
    }

    try {
      const unkey = new Unkey({ apiKey: c.env.UNKEY_ROOT_KEY });
      const { meta, data, error } = await unkey.verifyKey({
        key: apiKey,
      });

      // Set rate limit headers (v2 uses ratelimits array)
      if (data.ratelimits?.[0]) {
        const rl = data.ratelimits[0];
        c.header("X-RateLimit-Limit", rl.limit.toString());
        c.header("X-RateLimit-Remaining", rl.remaining.toString());
        c.header("X-RateLimit-Reset", rl.reset.toString());
      }

      if (data.credits !== undefined) {
        c.header("X-Credits-Remaining", data.credits.toString());
      }

      if (!data.valid) {
        const status = data.code === "RATE_LIMITED" ? 429 : 401;
        return c.json({ error: data.code }, status);
      }

      c.set("auth", data);
      await next();
    } catch (err) {
      return c.json({ error: "Service unavailable" }, 503);
    }
  };
}

With Durable Objects

For stateful applications with Durable Objects:
src/index.ts
import { Unkey } from "@unkey/api";
import { DurableObject } from "cloudflare:workers";

export interface Env {
  UNKEY_ROOT_KEY: string;
}

export class Counter extends DurableObject {
  async fetch(request: Request) {
    let count = (await this.ctx.storage.get("count")) as number || 0;
    count++;
    await this.ctx.storage.put("count", count);
    return Response.json({ count });
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY });

    // Verify API key first
    const apiKey = request.headers.get("Authorization")?.slice(7);

    if (!apiKey) {
      return Response.json({ error: "Missing API key" }, { status: 401 });
    }
    try {
      const { meta, data, error } = await unkey.keys.verifyKey({
        key: apiKey,
      });

      if (error || !data.valid) {
        return Response.json({ error: "Unauthorized" }, { status: 401 });
      }

      // Use external ID as Durable Object ID for per-user state
      const id = env.COUNTER.idFromName(data.identity?.externalId ?? "anonymous");
      const counter = env.COUNTER.get(id);

      return counter.fetch(request);
    } catch (error) {
      return Response.json({ error: "Service unavailable" }, { status: 503 });
    }
  },
};

wrangler.toml

wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# Store UNKEY_ROOT_KEY as a secret, not in config
# npx wrangler secret put UNKEY_ROOT_KEY

Deploy

npx wrangler deploy
Your API is now protected globally at the edge! 🌍
Last modified on February 6, 2026