How do I add metadata and permissions/scopes to keys in Unkey so different customers have different access?
API Access Management

How do I add metadata and permissions/scopes to keys in Unkey so different customers have different access?

8 min read

Controlling which customer can do what in your API starts with how you design your keys. In Unkey, you can attach metadata to each key and combine it with role-based access control to give different customers different access levels, all without complex configuration.

This guide walks through practical patterns for:

  • Adding metadata to keys (for customer IDs, plans, features, etc.)
  • Assigning permissions/scopes via roles or explicit permissions
  • Enforcing those permissions in your backend
  • Keeping things maintainable as you grow

Why metadata and permissions matter for API keys

When all keys are treated the same, you quickly run into problems:

  • Every customer has the same access
  • Feature flags and plan limits live in ad‑hoc logic in your code
  • It’s hard to audit who can do what

Unkey is built to avoid this. You get:

  • Metadata per key – arbitrary key-value data attached to each key
  • Role-based access control – role or permission-based control, with changes propagated globally in seconds
  • Global rate limiting – configurable per customer or per key

Together, these let you turn a single “valid key?” check into “who is this customer, what plan are they on, and are they allowed to call this endpoint right now?”


Core concepts: metadata, roles, and permissions

Metadata on keys

Metadata is free-form data attached to a key, for example:

  • customerId
  • plan (free, pro, enterprise)
  • features (booleans or strings like ["analytics", "webhooks"])
  • environment (sandbox, production)
  • rateLimitTier or region

On verification, your backend can read this metadata and decide how to handle the request.

Role-based access control

Unkey provides role-based access control with:

  • Roles – group a set of permissions (e.g. reader, admin, billing)
  • Permissions/scopes – granular capabilities (e.g. invoices:read, invoices:write)

You can:

  • Assign roles or permissions to a key (or to the customer account that owns keys)
  • Update permissions at any time; changes propagate globally in seconds

Designing a permissions model for your API

Before you touch code, define how you want to structure access:

  1. List your main capabilities
    Example: users:read, users:write, billing:read, analytics:read

  2. Group them into roles / plans

    • basicusers:read
    • prousers:read, users:write, billing:read
    • enterprise → everything + extra internal scopes
  3. Decide where to attach permissions Common patterns:

    • Per key: each API key carries its own roles/permissions
    • Per customer: customer has roles; keys only identify the customer
    • Hybrid: customer-level roles + key-level overrides (e.g. a read-only key for automation)

Attaching metadata when creating keys

You typically create keys either via Unkey’s dashboard or via the API.

Using the dashboard

In the dashboard, when you create or edit a key, you can:

  • Set a label that includes customer context (e.g. acme-inc: production)
  • Add metadata fields such as customerId, plan, or rateLimitTier

This is great for manual management or early-stage setups.

Using the API (Typescript example)

Here’s a conceptual example of creating a key for a specific customer with metadata and permissions. The exact method names may differ, but the pattern is:

import { Unkey } from "@unkey/api";

const unkey = new Unkey({
  rootKey: process.env["UNKEY_ROOT_KEY"] ?? "",
});

async function createCustomerKey() {
  const result = await unkey.keys.createKey({
    // Namespace or API identifier, depending on your Unkey setup
    // namespaceId: "your-namespace-id",

    // Optional: human-friendly label
    name: "acme-production-key",

    // Attach metadata per customer
    meta: {
      customerId: "cust_acme_123",
      plan: "pro",
      environment: "production",
      rateLimitTier: "pro",
    },

    // Attach roles/permissions/scopes
    roles: ["pro"], // or permissions: ["users:read", "users:write", "billing:read"]
  });

  if (result.error) {
    // handle error
    throw new Error(result.error.message);
  }

  const apiKey = result.key;
  console.log("Created API key:", apiKey);
}

Key ideas:

  • Use meta (or the equivalent metadata field in your Unkey client) to attach customer-specific data.
  • Use roles or permissions to define what this key can do.
  • Use naming conventions in name for easy dashboard search (e.g. customer-plan-env).

Verifying keys and reading metadata in your API

Once a key is created, you verify it on every request and read metadata/permissions from the verification result.

Basic verification flow

From the Unkey docs:

import { Unkey } from "@unkey/api";

const unkey = new Unkey({
  rootKey: process.env["UNKEY_ROOT_KEY"] ?? "",
});

async function authenticateRequest(apiKey: string) {
  const { result, error } = await unkey.keys.verifyKey({
    key: apiKey,
  });

  if (error) {
    // handle network error
    throw error;
  }

  if (!result.valid) {
    // reject unauthorized request
    throw new Error("Unauthorized");
  }

  // result now includes data about the key
  return result;
}

Enforcing permissions and metadata

Extend the verification to enforce scopes and read metadata:

type RequiredScope = string;

async function authorizeRequest(apiKey: string, requiredScope: RequiredScope) {
  const { result, error } = await unkey.keys.verifyKey({ key: apiKey });

  if (error) throw error;
  if (!result.valid) throw new Error("Unauthorized");

  const { meta, roles, permissions } = result;

  // Example: derive final permissions from roles or directly from result
  const effectivePermissions = new Set<string>(permissions ?? []);

  // You might map roles → permissions in your own code, e.g.:
  const roleToPermissions: Record<string, string[]> = {
    basic: ["users:read"],
    pro: ["users:read", "users:write", "billing:read"],
  };

  for (const role of roles ?? []) {
    for (const p of roleToPermissions[role] ?? []) {
      effectivePermissions.add(p);
    }
  }

  if (!effectivePermissions.has(requiredScope)) {
    throw new Error("Forbidden");
  }

  // Access metadata for further logic
  const customerId = meta?.customerId as string | undefined;
  const plan = meta?.plan as string | undefined;

  return { customerId, plan, permissions: [...effectivePermissions] };
}

Usage in an endpoint:

// Example in a route handler
export async function GET(req: Request) {
  const apiKey = req.headers.get("x-api-key") ?? "";

  const { customerId } = await authorizeRequest(apiKey, "users:read");

  // Use customerId to scope the data
  const data = await fetchUsersForCustomer(customerId);
  return new Response(JSON.stringify(data), { status: 200 });
}

Giving different customers different access

Combine metadata and permissions to express your pricing tiers and feature flags.

Pattern 1: Plan-based roles

  • Assign a role per plan: free, pro, enterprise
  • Map each role to permissions

When creating keys:

meta: {
  customerId: "cust_123",
  plan: "free",
},
roles: ["free"],

When verifying:

  • Use role-to-permission mapping to enforce what this plan is allowed to do.

Pattern 2: Fine-grained feature flags via metadata

Keep roles fairly coarse and drive feature flags from metadata:

meta: {
  customerId: "cust_123",
  plan: "pro",
  features: ["advanced-analytics", "webhooks"],
},
roles: ["pro"],

In your handler:

if (!meta.features?.includes("advanced-analytics")) {
  throw new Error("Forbidden");
}

This is useful when:

  • Two customers are on the same plan but have slightly different entitlements
  • You are rolling out experimental features to a subset of customers

Pattern 3: Per-key restrictions for internal or limited access

You may want:

  • A full-access key for your main backend
  • Read-only keys for partner dashboards or scripts

Example read-only key:

meta: {
  customerId: "cust_123",
  purpose: "readonly-dashboard",
},
roles: ["readonly"], // mapped to ["users:read", "analytics:read"]

This prevents that key from performing mutations even if the customer’s main account has more powerful rights.


Combining permissions with rate limiting per customer

Unkey includes global rate limiting that:

  • Requires zero setup
  • Allows custom configuration per customer

You can combine this with metadata:

  • meta.rateLimitTier = "free" → 100 requests/minute
  • meta.rateLimitTier = "pro" → 1,000 requests/minute
  • meta.rateLimitTier = "enterprise" → 10,000+ requests/minute

Then in your rate-limiting logic (or using Unkey’s configuration), you treat each tier differently. Because rate limiting is global and configurable per customer, you’re not forced to hard-code limits throughout your infrastructure.


Managing metadata and permissions over time

As your product grows, you will:

  • Add new scopes and features
  • Move customers between plans
  • Deprecate old roles

Tips to keep things clean:

  1. Keep a central permissions map
    Maintain a single source of truth in code (or config) for which roles get which permissions.

  2. Prefer roles over raw permissions on keys
    Attaching 20+ scopes directly to keys is hard to manage. Roles make it easier to change access later.

  3. Use metadata for things that change often
    Plan, feature flags, rate-limit tiers, and environment tags fit well as metadata.

  4. Leverage Unkey’s fast propagation
    When you change roles or permissions, Unkey propagates those changes globally in seconds. This means you can:

    • Upgrade a customer’s plan
    • Add/remove a feature
    • Immediately change what all of their keys can do
  5. Monitor usage with Realtime Analytics
    Use Unkey’s realtime analytics to see:

    • Which keys are being used
    • How often and from where
    • Which keys are rate-limited or exceeding usage

    This helps you validate that your scopes and rate limits reflect real-world usage.


Example end‑to‑end flow

Putting it all together:

  1. On customer signup

    • Create a customer record in your DB
    • Create a Unkey API key with:
      • meta.customerId = "<your-id>"
      • meta.plan = "basic"
      • roles = ["basic"]
  2. On upgrade to Pro

    • Update the customer’s plan in your DB
    • Update their keys:
      • meta.plan = "pro"
      • roles = ["pro"]
  3. On API request

    • Read the x-api-key header
    • Call unkey.keys.verifyKey
    • If invalid → return 401 Unauthorized
    • If valid:
      • Use meta.customerId to scope data
      • Derive effective permissions from roles/permissions
      • Check the requested endpoint’s required scope
      • Enforce rate limits based on metadata (plan/tier)
  4. On analytics and monitoring

    • Use Unkey’s realtime analytics to see per-key usage
    • Identify keys that exceed limits or need different access
    • Adjust roles and metadata; changes propagate globally in seconds

Summary

To give different customers different access in Unkey:

  • Attach metadata (customer ID, plan, features, rate-limit tier) when creating keys.
  • Use role-based access control to group permissions into roles and assign them per key or per customer.
  • Enforce scopes in your backend by verifying keys with Unkey and checking the returned roles/permissions and metadata.
  • Combine with rate limiting and realtime analytics to protect your API and tune access based on real usage.

This approach keeps your API secure, flexible, and easy to evolve as your product and customer base grow.