Skip to main content

What you’ll build

A Bun HTTP server that requires a valid API key on every request. Invalid or missing keys get rejected with a 401. Time to complete: ~3 minutes

Prerequisites

Want to skip ahead?

Clone the complete example and run it locally.
1

Create a new Bun project

mkdir unkey-bun && cd unkey-bun
bun init -y
2

Install the SDK

bun add @unkey/api
3

Add your root key

Create a .env file with your credentials from the Unkey dashboard:
.env
UNKEY_ROOT_KEY="unkey_..."
4

Create your server

Replace the contents of index.ts:
index.ts
import { verifyKey } from "@unkey/api";

const server = Bun.serve({
  async fetch(req) {
    // 1. Extract the key from the Authorization header
    const key = req.headers.get("Authorization")?.replace("Bearer ", "");

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

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

      if (!data.valid) {
        // Key is invalid, expired, rate limited, etc.
        return Response.json(
          { error: "Invalid API key", code: data.code },
          { status: 401 }
        );
      }

      // 3. Key is valid — return your response
      return Response.json({
        message: "Hello from protected endpoint!",
        keyId: data.keyId,
        identity: data.identity,
      });
    } catch (err) {
      console.error(err);
      return Response.json({ error: "Verification failed" }, { status: 500 });
    }
  },
  port: 3000,
});

console.log(`Server running at http://localhost:${server.port}`);
5

Run your server

bun run index.ts
6

Test it

Create a test key in your Unkey dashboard, then:
Test with valid key
curl http://localhost:3000 \
  -H "Authorization: Bearer YOUR_API_KEY"
You should see:
{
  "message": "Hello from protected endpoint!",
  "keyId": "key_...",
  "identity": null
}
Try without a key:
Test without key
curl http://localhost:3000
You’ll get:
{
  "error": "Missing API key"
}

What’s in data?

After successful verification:
FieldTypeDescription
validbooleanWhether the key passed all checks
codestringStatus code (VALID, NOT_FOUND, RATE_LIMITED, etc.)
keyIdstringThe key’s unique identifier
namestring?Human-readable name of the key
metaobject?Custom metadata associated with the key
expiresnumber?Unix timestamp (in milliseconds) when the key will expire. (if set)
creditsnumber?Remaining uses (if usage limits set)
enabledbooleanWhether the key is enabled
rolesstring[]?Permissions attached to the key
permissionsstring[]?Permissions attached to the key
identityobject?Identity info if externalId was set when creating the key
ratelimitsobject[]?Rate limit states (if rate limiting configured)

Adding routes

Bun’s built-in server uses a single fetch handler. For multiple routes, pattern match on the URL:
index.ts
import { verifyKey } from "@unkey/api";

// Helper to verify key
async function authenticate(req: Request) {
  const key = req.headers.get("Authorization")?.replace("Bearer ", "");
  if (!key) return { valid: false, error: "Missing API key" };

  try {
    const { meta, data } = await verifyKey({ key });
    return data;
  } catch (err) {
    console.error(err);
    return { valid: false, error: "Verification failed" };
  }
}

const server = Bun.serve({
  async fetch(req) {
    const url = new URL(req.url);

    // Public route
    if (url.pathname === "/") {
      return Response.json({ message: "Welcome! Try /api/secret" });
    }

    // Protected routes
    if (url.pathname.startsWith("/api/")) {
      const auth = await authenticate(req);
      
      if (!auth.valid) {
        return Response.json({ error: auth.error || "Unauthorized" }, { status: 401 });
      }

      // Route to specific handlers
      if (url.pathname === "/api/secret") {
        return Response.json({ secret: "data", keyId: auth.keyId });
      }

      if (url.pathname === "/api/user") {
        return Response.json({ user: auth.identity });
      }
    }

    return Response.json({ error: "Not found" }, { status: 404 });
  },
  port: 3000,
});

Next steps

Troubleshooting

  • Ensure the key hasn’t expired or been revoked
  • Verify the header format: Authorization: Bearer YOUR_KEY (note the space)
Bun automatically loads .env files. Make sure:
  • The .env file is in your project root
  • You’re using Bun.env.VAR_NAME not process.env.VAR_NAME
  • Restart the server after changing .env
Run bun init -y to ensure you have a proper tsconfig.json. Bun handles TypeScript natively, no extra setup needed.
Last modified on February 6, 2026