API Keys

Working with API Key and Consumer Metadata

Consumer metadata is a JSON object (up to 1 KB) attached to each API key consumer that is available at runtime on every authenticated request. It is the primary mechanism for passing tenant-specific data — such as a plan tier, customer ID, or organization — through the Zuplo gateway to your policies and backend.

This guide covers how to set, read, and forward metadata, and shows the most common patterns for using it in production.

Prerequisites

How metadata flows

When a request arrives with a valid API key, the API Key Authentication policy validates the key and populates request.user:

  • request.user.sub — the consumer's name (its unique identifier within the bucket).
  • request.user.data — the consumer's metadata object.

Every policy and handler that runs after authentication can read these values. Metadata is set on the consumer, not on individual keys. All keys belonging to the same consumer share the same identity and metadata. For more on the consumer/key data model, see API Keys.

Metadata vs tags

Consumers support two kinds of key-value data. Mixing them up is a common mistake:

 MetadataTags
Available atRuntime (request.user.data)Management API only
PurposeAuthorization, routing, rate limiting, meteringFiltering, querying, and organizing consumers
Size limit1 KB JSON objectUp to 5 key-value pairs per consumer
Sent to runtimeYes — on every authenticated requestNo — never reaches your policies or handlers
Set viaPortal, Developer API, on-login consumer creationPortal, Developer API

Tags are never sent to the runtime

If you store data in tags expecting to read it in a policy or handler, it will not be there. Tags are for management operations only (for example, querying all consumers on a specific plan via the Developer API). Use metadata for anything your code needs at request time.

Setting metadata

You can set metadata when creating or updating a consumer through three channels.

Zuplo Portal

Navigate to Services → API Keys → Consumers in your project. When creating or editing a consumer, enter a JSON object in the Metadata field:

JSONCode
{ "plan": "gold", "customerId": "cust_123", "orgId": "org_456" }

Developer API

Use the Zuplo Developer API to create or update consumers programmatically. Pass the metadata field in the request body:

TerminalCode
curl -X POST "https://dev.zuplo.com/v1/projects/$PROJECT_ID/buckets/$BUCKET_NAME/consumers" \ -H "Authorization: Bearer $ZUPLO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-consumer", "metadata": { "plan": "gold", "customerId": "cust_123", "orgId": "org_456" } }'

Replace $PROJECT_ID with your project's ID (visible in the Portal URL) and $BUCKET_NAME with your API key bucket name (visible under Services → API Keys in the Portal). $ZUPLO_API_KEY is a Zuplo Developer API key.

To update metadata on an existing consumer, use a PATCH request to the same endpoint with the consumer name appended. See the API reference for the full endpoint documentation.

On-login consumer creation

If you use the Developer Portal (the self-serve portal where your API consumers manage their keys) with an identity provider like Auth0, you can create consumers automatically when a user signs in and set metadata from the identity token claims. See Create Consumer on Auth for the setup.

Reading metadata at runtime

After the API Key Authentication policy runs, access metadata in any downstream policy or handler through request.user.data:

TypeScriptCode
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function handler( request: ZuploRequest, context: ZuploContext, ) { const plan = request.user?.data?.plan; const customerId = request.user?.data?.customerId; context.log.info("Authenticated request", { consumer: request.user?.sub, plan, customerId, }); return new Response(`Plan: ${plan}, Customer: ${customerId}`); }

For type safety, define an interface for your metadata shape and pass it under the UserData key in the ZuploRequest type parameter. UserData is the key name the generic expects; it maps to request.user.data. See ZuploRequest for all available type parameters.

TypeScriptCode
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; interface ConsumerMetadata { plan: string; customerId: string; orgId?: string; } export default async function handler( request: ZuploRequest<{ UserData: ConsumerMetadata }>, context: ZuploContext, ) { // TypeScript knows the shape of request.user.data const { plan, customerId } = request.user!.data; return new Response(`Plan: ${plan}, Customer: ${customerId}`); }

See RequestUser for the full type reference.

Forwarding metadata to your backend

A common pattern is to extract metadata values and forward them as HTTP headers so your backend receives tenant identity without needing to validate API keys itself. Create a custom inbound policy that runs after authentication:

  1. Create the policy module

    Create a new module file at modules/forward-metadata.ts:

    TypeScriptmodules/forward-metadata.ts
    import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function forwardMetadata( request: ZuploRequest, context: ZuploContext, ) { // Strip any client-supplied headers to prevent spoofing request.headers.delete("x-consumer-name"); request.headers.delete("x-customer-id"); request.headers.delete("x-org-id"); request.headers.delete("x-plan"); if (!request.user) { return request; } const { customerId, orgId, plan } = request.user.data; // Set trusted headers for the upstream backend request.headers.set("x-consumer-name", request.user.sub); if (customerId) { request.headers.set("x-customer-id", customerId); } if (orgId) { request.headers.set("x-org-id", orgId); } if (plan) { request.headers.set("x-plan", plan); } return request; }

    Always delete client-supplied copies of metadata headers before setting them from request.user.data. If a metadata field is absent and you skip the delete, the client's spoofed value passes through to your backend. Your backend should trust these headers only because the gateway sets them.

  2. Register the policy

    Add the policy to your config/policies.json:

    JSONCode
    { "name": "forward-metadata", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/forward-metadata)" } }
  3. Attach the policy to your route

    Add forward-metadata to the route's inbound policy array after api-key-inbound so that request.user is populated before the policy runs. See Custom Code Inbound Policy for the full policy configuration reference.

Common patterns

Feature gating by plan

Check request.user.data.plan to allow or deny access to specific routes:

TypeScriptCode
import { ZuploContext, ZuploRequest, HttpProblems } from "@zuplo/runtime"; export default async function requirePlan( request: ZuploRequest, context: ZuploContext, ) { const allowedPlans = ["gold", "enterprise"]; if (!allowedPlans.includes(request.user?.data?.plan)) { return HttpProblems.forbidden(request, context, { detail: "This endpoint requires a Gold or Enterprise plan.", }); } return request; }

Multi-tenant backend routing

Route each customer to a dedicated backend based on metadata. The following policy sets context.custom.downstreamUrl, which the URL Rewrite handler reads to determine the upstream destination. See Route to Backends Based on User Identity for the full setup including handler configuration.

TypeScriptCode
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function tenantRouting( request: ZuploRequest, context: ZuploContext, ) { const { customerId } = request.user!.data; // The URL Rewrite handler reads this to route the request context.custom.downstreamUrl = `https://${customerId}.api.example.com`; return request; }

Dynamic rate limiting

Use metadata to apply different rate limits per plan tier. See Dynamic Rate Limiting for the full setup, which reads request.user.data to select the rate limit policy to apply.

Metadata scope and per-key behavior

Each consumer can have any number of API keys. All keys for a consumer share the same identity (request.user.sub) and metadata (request.user.data). This is by design — a consumer represents an identity, and metadata describes that identity.

Per-key metadata is not supported. There is no way to attach different metadata to different keys under the same consumer. If you need different metadata for different keys — for example, different permissions or tenant identifiers — create separate consumers. Each consumer is an identity, so creating one consumer per identity that needs distinct metadata is the intended pattern.

For example, if a single organization has two teams that need different access levels, create two consumers (acme-team-a and acme-team-b) with appropriate metadata on each, rather than two keys on a single acme consumer.

If two keys need different runtime behavior, create separate consumers.

Visibility and security

Who can see consumer metadata

Understanding who has access to metadata is important when deciding what to store in it:

AudienceCan see metadata?Details
Your policies and handlersYesVia request.user.data on every authenticated request
Zuplo Portal adminsYesVia Services → API Keys → Consumers in the project
Developer API callersYesVia the Consumers API (requires a Zuplo API key)
Developer Portal key holdersNoKey managers can view, copy, and rotate their keys but do not see consumer metadata or tags

What to store (and what not to store)

Metadata is available to your runtime code and to anyone with Developer API access. Follow these guidelines:

  • Good for metadata: plan tier, customer ID, organization ID, feature flags, environment identifiers, external system references.
  • Not for metadata: secrets, passwords, tokens, PII (email addresses, phone numbers), or any data that would cause harm if exposed through the Developer API.

Keep metadata small. The maximum size is 1 KB per consumer.

API key format

Zuplo API keys use the zpka_<random>_<checksum> format, enabling leak detection via GitHub secret scanning. For the full format breakdown and service limits, see API Keys concepts and API Key Service Limits.

Next steps

Last modified on