Identities let you connect multiple API keys to a single entity — a user, an organization, a service account, or any logical grouping in your system. Once connected, keys can share configuration, metadata, and most importantly, rate limits.
The Problem Identities Solve
Without identities, each API key is an island. If a user has 3 keys, you need to:
Update rate limits on each key separately
Piece together analytics from multiple sources
Track usage per-key instead of per-user
Allow users to bypass limits by creating more keys
With identities:
Without Identities With Identities Rate limits per key (bypass with more keys) Shared rate limit pool across all keys Metadata duplicated on each key Shared metadata accessible from any key Analytics scattered across keys Aggregated analytics per identity Update each key individually Update once, applies to all keys
Real-World Example
Imagine you’re building a developer platform like Stripe. Each customer has:
A test key for development
A production key for live traffic
Multiple restricted keys for different microservices
All these keys belong to the same customer. With identities:
import { Unkey } from "@unkey/api" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY });
// 1. Create identity for the customer
try {
const { meta , data } = await unkey . identities . create ({
externalId: "customer_acme_123" , // Your internal customer ID
meta: {
plan: "pro" ,
company: "Acme Corp" ,
email: "[email protected] "
},
ratelimits: [{
name: "requests" ,
limit: 1000 ,
duration: 60000 // 1000 req/min shared across ALL keys
}]
});
const identityId = data . identityId ;
// 2. Create keys linked to this identity
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "customer_acme_123" ,
name: "Production Key"
});
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "customer_acme_123" ,
name: "Test Key"
});
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "customer_acme_123" ,
name: "Webhook Service Key"
});
} catch ( err ) {
console . error ( err );
}
Now:
All 3 keys share the 1000 req/min limit
Updating the identity’s plan metadata updates it for all keys
Analytics aggregate across all keys
Customer can’t bypass limits by using different keys
Use Cases
Shared rate limits Enforce a single rate limit across all of a user’s keys. Prevents bypass through multiple keys.
Aggregate analytics See total usage per customer instead of piecing together data from individual keys.
Centralized configuration Store plan tier, feature flags, or custom limits once. All keys inherit the configuration.
Multi-tenant organizations Map identities to organizations. Team members and services share the org’s identity and limits.
Service accounts Group keys for different microservices under a single service account identity.
User management Track all keys belonging to a user. Revoke all keys when user leaves or account is suspended.
Creating Identities
import { Unkey } from "@unkey/api" ;
const unkey = new Unkey ({ rootKey: process . env . UNKEY_ROOT_KEY });
try {
const { meta , data } = await unkey . identities . create ({
externalId: "user_123" , // Your internal ID
meta: {
plan: "enterprise" ,
email: "[email protected] " ,
features: [ "advanced-analytics" , "sso" ]
}
});
console . log ( data . identityId ); // id_...
} catch ( err ) {
console . error ( err );
}
curl -X POST https://api.unkey.com/v2/identities.createIdentity \
-H "Authorization: Bearer $UNKEY_ROOT_KEY " \
-H "Content-Type: application/json" \
-d '{
"externalId": "user_123",
"meta": {
"plan": "enterprise",
"email": "[email protected] "
}
}'
Identity Data Model
Unkey’s internal identifier for this identity. Use this in API calls that reference the identity.
Your identifier for this entity. This links the Unkey identity to your system — typically a user ID, org ID, or customer ID.If you use Clerk, Auth0, WorkOS, or similar: use their user/org ID as the externalId. // Example with Clerk
const identity = await unkey . identities . create ({
externalId: clerkUserId , // "user_2NNEqL2nrIRdJ194ndJqAHwEfxC"
});
Arbitrary JSON metadata. Store anything useful: plan tier, feature flags, company name, billing info. {
"plan" : "enterprise" ,
"features" : [ "advanced-analytics" , "sso" ],
"billingEmail" : "[email protected] " ,
"company" : "Acme Corp" ,
"stripeCustomerId" : "cus_..."
}
Rate limits that apply to all keys linked to this identity: ratelimits : [
{
name: "requests" ,
limit: 1000 ,
duration: 60000 // 1000 requests per minute (shared)
},
{
name: "tokens" ,
limit: 50000 ,
duration: 86400000 // 50k tokens per day (shared)
}
]
Linking Keys to Identities
Create keys that automatically link to an identity using the externalId field:
Automatic Linking
Explicit Linking
// Keys with the same externalId automatically link to the identity
try {
const { meta : meta1 , data : data1 } = await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" , // Links to identity
name: "Production Key"
});
const { meta : meta2 , data : data2 } = await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" , // Same identity
name: "Staging Key"
});
// Both keys now share the identity's rate limits and metadata
} catch ( err ) {
console . error ( err );
}
// Explicitly reference the identity ID
try {
const { meta , data } = await unkey . keys . create ({
apiId: "api_..." ,
identityId: "id_..." , // Unkey identity ID
name: "Mobile App Key"
});
} catch ( err ) {
console . error ( err );
}
Shared Rate Limits
The killer feature: enforce a single rate limit across all of a user’s keys.
The Problem
Without shared limits, rate limits apply per-key:
User has 3 API keys, each with 100 req/s limit
→ User can actually make 300 req/s (100 × 3 keys)
The Solution
Create an identity with rate limits, then link all keys to it:
Identity: user_123
└── Rate limit: 100 req/s (shared pool)
├── Key A (production)
├── Key B (staging)
└── Key C (mobile app)
→ User can only make 100 req/s total across ALL keys
Implementation
Create identity with rate limits
try {
const { meta , data } = await unkey . identities . create ({
externalId: "user_123" ,
ratelimits: [
{
name: "requests" ,
limit: 100 ,
duration: 1000 // 100 requests per second
}
]
});
} catch ( err ) {
console . error ( err );
}
Create keys linked to identity
// All keys with same externalId share the identity's rate limits
try {
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" ,
name: "Production Key"
});
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" ,
name: "Staging Key"
});
} catch ( err ) {
console . error ( err );
}
Verify — limits enforced automatically
const { meta , data } = await unkey . keys . verifyKey ({ key: "sk_prod_..." });
if ( ! data . valid && data . code === "RATE_LIMITED" ) {
// User exceeded 100 req/s across ALL their keys
return new Response ( "Rate limit exceeded" , { status: 429 });
}
Multiple Rate Limits
Apply different limits to different resource types:
try {
const { meta , data } = await unkey . identities . create ({
externalId: "user_123" ,
ratelimits: [
{
name: "requests" ,
limit: 500 ,
duration: 3600000 // 500 requests per hour
},
{
name: "ai-tokens" ,
limit: 20000 ,
duration: 86400000 // 20k tokens per day
},
{
name: "exports" ,
limit: 10 ,
duration: 86400000 // 10 exports per day
}
]
});
} catch ( err ) {
console . error ( err );
}
Check specific limits during verification:
// Check the "ai-tokens" limit and consume 150 tokens
const { meta , data } = await unkey . keys . verifyKey ({
key: "sk_..." ,
ratelimits: [
{ name: "ai-tokens" , cost: 150 }
]
});
if ( ! data . valid ) {
console . log ( data . code ); // "RATE_LIMITED" if tokens exhausted
}
If any specified limit is exceeded, verification fails.
Accessing Identity Data
When you verify a key linked to an identity, you get the identity info:
const { meta , data } = await unkey . keys . verifyKey ({ key: "sk_..." });
if ( data . valid ) {
console . log ( data . identity );
// {
// id: "id_...",
// externalId: "customer_acme_123",
// meta: {
// plan: "pro",
// company: "Acme Corp",
// features: ["analytics", "sso"]
// }
// }
// Use identity metadata to control features
if ( data . identity . meta . features ?. includes ( "analytics" )) {
// Show advanced analytics
}
}
Updating Identities
Update identity metadata or rate limits — changes apply to all linked keys:
Managing Keys per Identity
List all keys belonging to an identity:
try {
const { meta , data } = await unkey . keys . list ({
apiId: "api_..." ,
externalId: "user_123"
});
console . log ( data . keys );
// [
// { id: "key_...", name: "Production Key", ... },
// { id: "key_...", name: "Staging Key", ... },
// { id: "key_...", name: "Mobile App Key", ... }
// ]
} catch ( err ) {
console . error ( err );
}
Revoke All Keys for an Identity
When a user leaves or account is suspended:
try {
// Get all keys for the user
const { meta , data } = await unkey . keys . list ({
apiId: "api_..." ,
externalId: "user_123"
});
// Delete them all
for ( const key of data . keys ) {
await unkey . keys . delete ({ keyId: key . id });
}
} catch ( err ) {
console . error ( err );
}
Common Patterns
Integration with auth providers
Link Unkey identities to Clerk, Auth0, or WorkOS: // After user signs up in Clerk
const clerkUserId = user . id ; // "user_2NNEqL..."
// Create matching Unkey identity
try {
const { meta , data } = await unkey . identities . create ({
externalId: clerkUserId ,
meta: {
email: user . emailAddresses [ 0 ]. emailAddress ,
name: user . fullName
}
});
} catch ( err ) {
console . error ( err );
}
Multi-tenant organizations
Use organization IDs as externalIds for team-based rate limiting: // All team members share org-level rate limits
try {
const { meta , data } = await unkey . identities . create ({
externalId: `org_ ${ organization . id } ` ,
meta: {
orgName: organization . name ,
plan: organization . plan
},
ratelimits: [{
name: "requests" ,
limit: 10000 ,
duration: 60000 // 10k/min for entire org
}]
});
// Create key for team member
await unkey . keys . create ({
apiId: "api_..." ,
externalId: `org_ ${ organization . id } ` ,
name: ` ${ user . name } - ${ organization . name } `
});
} catch ( err ) {
console . error ( err );
}
Tiered rate limits by plan
Different plans get different limits: const PLAN_LIMITS = {
free: { limit: 100 , duration: 60000 }, // 100/min
pro: { limit: 1000 , duration: 60000 }, // 1000/min
enterprise: { limit: 10000 , duration: 60000 } // 10k/min
};
try {
const { meta , data } = await unkey . identities . create ({
externalId: userId ,
meta: { plan: "pro" },
ratelimits: [{
name: "requests" ,
... PLAN_LIMITS [ "pro" ]
}]
});
} catch ( err ) {
console . error ( err );
}
// When user upgrades
try {
await unkey . identities . update ({
externalId: userId ,
meta: { plan: "enterprise" },
ratelimits: [{
name: "requests" ,
... PLAN_LIMITS [ "enterprise" ]
}]
});
} catch ( err ) {
console . error ( err );
}
Environment-specific keys
Production and staging keys for the same user: try {
const { meta , data } = await unkey . identities . create ({
externalId: "user_123" ,
ratelimits: [{ name: "requests" , limit: 1000 , duration: 60000 }]
});
// Production key
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" ,
name: "Production Key" ,
meta: { environment: "production" }
});
// Staging key
await unkey . keys . create ({
apiId: "api_..." ,
externalId: "user_123" ,
name: "Staging Key" ,
meta: { environment: "staging" }
});
// Both share the 1000/min rate limit
} catch ( err ) {
console . error ( err );
}
Analytics & Insights
Identities enable powerful analytics:
Per-customer usage : See total API usage per customer instead of per-key
Churn prediction : Identify customers with declining usage
Upsell opportunities : Find users hitting rate limits who might upgrade
Cost attribution : Track API costs per customer for accurate billing
Analytics are automatically aggregated by identity. View in the Unkey dashboard under Analytics → filter by externalId.
Best Practices
Use meaningful external IDs Use your existing user/org IDs as externalId. Makes it easy to join Unkey data with your database.
Store plan tier in metadata Keep plan information in identity metadata for easy access during verification without extra DB queries.
One identity per customer Even if users have multiple projects, use one identity per user. Use key metadata for project scoping.
Set rate limits on identities Always set rate limits on the identity, not individual keys, to prevent bypass through multiple keys.
Update identities on upgrades When users upgrade plans, update the identity’s rate limits and metadata immediately.
Clean up on churn Delete identities and all associated keys when users churn to keep your workspace clean.
Next Steps
API Keys Learn about creating and managing API keys
Rate Limiting Deep dive into rate limiting strategies
Analytics Track usage patterns per identity
API Reference Complete identity API documentation