Skip to main content
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

Last modified on February 6, 2026