Environments let you issue distinct keys for development, staging, and production. Test keys can’t affect real resources, while live keys have full access.
When to use this
Safe development Developers can test integrations without touching production data.
Automated testing CI/CD pipelines can run tests with keys that won’t affect live resources.
Different limits Test keys might have lower rate limits or be free to use.
Prevent accidents Prefixes like sk_test_ make it obvious when using non-production keys.
How it works
Environments are implemented using key metadata and prefixes — there’s no special “environment” field. This gives you full flexibility to model environments however you want.
Common pattern:
Prefix : sk_test_ vs sk_live_ (visible to users)
Metadata : { "environment": "test" } (returned on verification)
Create environment-specific keys
curl -X POST https://api.unkey.com/v2/keys.createKey \
-H "Authorization: Bearer $UNKEY_ROOT_KEY " \
-H "Content-Type: application/json" \
-d '{
"apiId": "api_...",
"prefix": "sk_test",
"meta": {
"environment": "test"
},
"ratelimits": [{
"name": "requests",
"limit": 100,
"duration": 60000
}]
}'
Using prefixes makes it obvious to users which environment a key is for. This prevents accidental use of test keys in production (or worse, live keys in tests).
Verify and check environment
The metadata is returned during verification, so your API can behave differently:
curl -X POST https://api.unkey.com/v2/keys.verifyKey \
-H "Content-Type: application/json" \
-d '{ "key": "sk_test_abc123..." }'
Response:
{
"meta" : { "requestId" : "req_..." },
"data" : {
"valid" : true ,
"keyId" : "key_..." ,
"meta" : {
"environment" : "test"
}
}
}
Handle environments in your API
try {
const { meta , data } = await verifyKey ({ key: request . apiKey });
if ( ! data . valid ) {
return unauthorized ();
}
const environment = data . meta ?. environment ?? "production" ;
if ( environment === "test" ) {
// Use test database, sandbox resources, etc.
return handleTestRequest ( request );
}
// Production request
return handleLiveRequest ( request );
} catch ( err ) {
console . error ( err );
return Response . json ({ error: "Internal error" }, { status: 500 });
}
Common patterns
Different rate limits per environment
// Test: 100/min, Live: 10,000/min
const ratelimits = environment === "test"
? [{ name: "requests" , limit: 100 , duration: 60000 }]
: [{ name: "requests" , limit: 10000 , duration: 60000 }];
try {
const { meta , data } = await unkey . keys . create ({
apiId ,
prefix: environment === "test" ? "sk_test" : "sk_live" ,
meta: { environment },
ratelimits ,
});
} catch ( err ) {
console . error ( err );
return Response . json ({ error: "Internal error" }, { status: 500 });
}
Sandbox vs real resources
if ( data . meta ?. environment === "test" ) {
// Return mock data or use sandbox
return {
charged: false ,
response: mockResponse
};
}
// Real billing, real data
try {
await chargeUser ( data . identity . externalId , amount );
return { charged: true , response: realResponse };
} catch ( err ) {
console . error ( err );
return Response . json ({ error: "Internal error" }, { status: 500 });
}
Block test keys from production endpoint
if ( data . meta ?. environment === "test" && isProductionEndpoint ) {
return Response . json (
{ error: "Test keys cannot access production endpoints" },
{ status: 403 }
);
}
// Continue with your API logic
Next steps