Documentation Index Fetch the complete documentation index at: https://mintlify.com/unkeyed/unkey/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide covers everything you need to protect your Next.js application with Unkey, including App Router API routes, Pages Router endpoints, and reusable middleware patterns.
What you’ll learn:
Quick setup with @unkey/nextjs wrapper
Manual verification for full control
Reusable middleware patterns
Permission-based access control
Best practices for production
Prerequisites
Quick Start with @unkey/nextjs
The fastest way to get started is with the official Next.js wrapper.
Install the SDK
npm install @unkey/nextjs
Add your root key
Get a root key from Settings → Root Keys and add it to your environment: UNKEY_ROOT_KEY = "unkey_..."
Never commit your root key. Add .env.local to .gitignore.
Create a protected App Router route
app/api/protected/route.ts
import { NextRequestWithUnkeyContext , withUnkey } from "@unkey/nextjs" ;
export const POST = withUnkey (
async ( req : NextRequestWithUnkeyContext ) => {
// The key has already been verified at this point
// Access verification details via req.unkey.data
// Your API logic here
return Response . json ({
message: "Hello!" ,
keyId: req . unkey . data . keyId ,
// If you set an externalId when creating the key:
externalId: req . unkey . data . identity ?. externalId ,
});
},
{ rootKey: process . env . UNKEY_ROOT_KEY ! }
);
The withUnkey wrapper handles key extraction, verification, and error responses automatically. Invalid keys never reach your handler.
Test it
First, create a test key in your Unkey dashboard , then: curl -X POST http://localhost:3000/api/protected \
-H "Authorization: Bearer YOUR_API_KEY"
You should see: {
"message" : "Hello!" ,
"keyId" : "key_..."
}
Now try without a key: curl -X POST http://localhost:3000/api/protected
You’ll get a 401 Unauthorized response.
Pages Router
For Next.js Pages Router, wrap your API handler:
import { withUnkey , NextRequestWithUnkeyContext } from "@unkey/nextjs" ;
import { NextApiRequest , NextApiResponse } from "next" ;
async function handler ( req : NextApiRequest , res : NextApiResponse ) {
// Type assertion needed for Pages Router
const unkeyReq = req as unknown as NextRequestWithUnkeyContext ;
return res . json ({
message: "Access granted" ,
user: unkeyReq . unkey ?. data . identity ?. externalId ,
});
}
export default withUnkey ( handler , { rootKey: process . env . UNKEY_ROOT_KEY ! }) ;
By default, withUnkey looks for a Bearer token in the Authorization header. Customize this:
export const POST = withUnkey (
async ( req ) => {
return Response . json ({ message: "Success" });
},
{
rootKey: process . env . UNKEY_ROOT_KEY ! ,
getKey : ( req ) => req . headers . get ( "x-api-key" ), // Custom header
}
);
Custom Error Handling
export const POST = withUnkey (
async ( req ) => {
return Response . json ({ message: "Success" });
},
{
rootKey: process . env . UNKEY_ROOT_KEY ! ,
handleInvalidKey : ( req , result ) => {
// result.code tells you why it failed
return Response . json (
{
error: "Unauthorized" ,
reason: result . code , // "NOT_FOUND", "EXPIRED", "RATE_LIMITED", etc.
},
{ status: 401 }
);
},
onError : ( req , error ) => {
console . error ( "Unkey error:" , error );
return Response . json (
{ error: "Authentication service unavailable" },
{ status: 503 }
);
},
}
);
Manual Verification
For full control over the authentication flow:
app/api/protected/route.ts
import { Unkey } from "@unkey/api" ;
import { UnkeyError } from "@unkey/api/models/errors" ;
import { NextRequest , NextResponse } from "next/server" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
export async function POST ( req : NextRequest ) {
// 1. Extract API key
const authHeader = req . headers . get ( "authorization" );
if ( ! authHeader ?. startsWith ( "Bearer " )) {
return NextResponse . json (
{ error: "Missing API key" },
{ status: 401 }
);
}
const apiKey = authHeader . slice ( 7 );
// 2. Verify with Unkey
try {
const { meta , data } = await unkey . keys . verifyKey ({
key: apiKey ,
});
if ( ! data . valid ) {
return Response . json ({ error: "Invalid API key" }, { status: 401 });
}
// 3. Your API logic here
return Response . json ({ data: "hello world" });
} catch ( err ) {
if ( err instanceof UnkeyError ) {
console . error ( "Unkey API Error:" , {
statusCode: err . statusCode ,
body: err . body ,
message: err . message ,
});
return Response . json (
{ error: "API error occurred" , details: err . message },
{ status: err . statusCode },
);
}
console . log ( "Unknown error:" , err );
return Response . json ({ error: "Internal Server Error" }, { status: 500 });
}
}
Reusable Middleware Pattern
Create a helper for consistent auth across routes:
import { Unkey } from "@unkey/api" ;
import { UnkeyError } from "@unkey/api/models/errors" ;
import { NextRequest , NextResponse } from "next/server" ;
import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components" ;
type AuthenticatedHandler = (
req : NextRequest ,
auth : V2KeysVerifyKeyResponseData ,
) => Promise < NextResponse >;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
export function withAuth ( handler : AuthenticatedHandler ) {
return async ( req : NextRequest ) => {
const authHeader = req . headers . get ( "authorization" );
if ( ! authHeader ?. startsWith ( "Bearer " )) {
return NextResponse . json ({ error: "Missing API key" }, { status: 401 });
}
try {
const { meta , data } = await unkey . keys . verifyKey ({
key: authHeader . slice ( 7 ),
});
if ( ! data . valid ) {
return NextResponse . json ({ error: data . code }, { status: 401 });
}
return handler ( req , data );
} catch ( error ) {
if ( error instanceof UnkeyError ) {
return NextResponse . json (
{ error: error . message },
{ status: error . statusCode },
);
}
return NextResponse . json (
{ error: "Service unavailable" },
{ status: 503 },
);
}
};
}
Use it in your routes:
import { withAuth } from "@/lib/auth" ;
export const GET = withAuth ( async ( req , auth ) => {
// auth contains the full verification result
const users = await db . users . findMany ({
where: { organizationId: auth . identity ?. externalId },
});
return Response . json ({ users });
});
Permission-Based Access Control
Require specific permissions for sensitive endpoints:
import { Unkey } from "@unkey/api" ;
import { UnkeyError } from "@unkey/api/models/errors" ;
import { NextRequest , NextResponse } from "next/server" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY ! });
export async function POST ( req : NextRequest ) {
const authHeader = req . headers . get ( "authorization" );
if ( ! authHeader ?. startsWith ( "Bearer " )) {
return NextResponse . json (
{ error: "Missing API key" },
{ status: 401 }
);
}
const apiKey = authHeader . slice ( 7 );
try {
const result = await unkey . keys . verifyKey ({
key: apiKey ,
permissions: [ "admin.delete" ],
});
if ( ! result . data . valid ) {
if ( result . data . code === "INSUFFICIENT_PERMISSIONS" ) {
return Response . json ({ error: "Admin access required" }, { status: 403 });
}
return Response . json ({ error: "Unauthorized" }, { status: 401 });
}
return Response . json ({ data: "hello world" });
} catch ( err ) {
if ( err instanceof UnkeyError ) {
console . error ( "Unkey API Error:" , {
statusCode: err . statusCode ,
body: err . body ,
message: err . message ,
});
return Response . json (
{ error: "API error occurred" , details: err . message },
{ status: err . statusCode },
);
}
console . log ( "Unknown error:" , err );
return Response . json ({ error: "Internal Server Error" }, { status: 500 });
}
}
Verification Response Data
After verification, req.unkey.data (or data in manual mode) contains:
Field Type Description 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 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 ratelimitsobject[]?Rate limit states (if rate limiting configured)
Next Steps
Add rate limiting Limit requests per key
Set usage limits Cap total requests per key
Add permissions Fine-grained access control
Next.js SDK Reference Full SDK documentation
Troubleshooting
Getting 401 even with a valid key?
Ensure the key hasn’t expired or been revoked
Verify the Authorization header format: Bearer YOUR_KEY (note the space)
Check that your root key has the verify_key permission
Environment variable not loading?
Restart your dev server after adding .env.local
Make sure the file is in your project root
Check for typos in the variable name