# Working with API Key and Consumer Metadata

Consumer metadata is a JSON object (up to 1 KB) attached to each
[API key consumer](./api-key-management.mdx) 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

- A Zuplo project with the
  [API Key Authentication policy](../policies/api-key-inbound.mdx) configured on
  at least one route.
- At least one API key consumer. See
  [Add API Key Authentication](../getting-started/gateway/add-api-key-auth/index.mdx)
  if you haven't created one yet.

## 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](../concepts/api-keys.md).

## Metadata vs tags

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

| &nbsp;              | Metadata                                          | Tags                                          |
| ------------------- | ------------------------------------------------- | --------------------------------------------- |
| **Available at**    | Runtime (`request.user.data`)                     | Management API only                           |
| **Purpose**         | Authorization, routing, rate limiting, metering   | Filtering, querying, and organizing consumers |
| **Size limit**      | 1 KB JSON object                                  | Up to 5 key-value pairs per consumer          |
| **Sent to runtime** | Yes — on every authenticated request              | No — never reaches your policies or handlers  |
| **Set via**         | Portal, Developer API, on-login consumer creation | Portal, Developer API                         |

:::caution{title="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](./api-key-api.mdx)). 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:

```json
{
  "plan": "gold",
  "customerId": "cust_123",
  "orgId": "org_456"
}
```

### Developer API

Use the [Zuplo Developer API](./api-key-api.mdx) to create or update consumers
programmatically. Pass the `metadata` field in the request body:

```bash
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](./api-key-api.mdx).

To update metadata on an existing consumer, use a `PATCH` request to the same
endpoint with the consumer name appended. See the
[API reference](../api/api-keys-consumers) for the full endpoint documentation.

### On-login consumer creation

If you use the [Developer Portal](../dev-portal/overview.mdx) (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](../dev-portal/dev-portal-create-consumer-on-auth.mdx)
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`:

```ts
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](../programmable-api/zuplo-request.mdx) for all available type
parameters.

```ts
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](../programmable-api/request-user.mdx) 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](../policies/custom-code-inbound.mdx)
that runs after authentication:

<Stepper>

1. **Create the policy module**

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

   ```ts title="modules/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;
   }
   ```

   :::warning

   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.

   :::

1. **Register the policy**

   Add the policy to your `config/policies.json`:

   ```json
   {
     "name": "forward-metadata",
     "policyType": "custom-code-inbound",
     "handler": {
       "export": "default",
       "module": "$import(./modules/forward-metadata)"
     }
   }
   ```

1. **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](../policies/custom-code-inbound.mdx) for the
   full policy configuration reference.

</Stepper>

## Common patterns

### Feature gating by plan

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

```ts
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](../guides/user-based-backend-routing.mdx)
for the full setup including handler configuration.

```ts
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](../rate-limiting/dynamic-rate-limiting.mdx) 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.

:::tip

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:

| Audience                                                       | Can see metadata? | Details                                                                                     |
| -------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------- |
| **Your policies and handlers**                                 | Yes               | Via `request.user.data` on every authenticated request                                      |
| **Zuplo Portal admins**                                        | Yes               | Via **Services → API Keys → Consumers** in the project                                      |
| **[Developer API](./api-key-api.mdx) callers**                 | Yes               | Via the [Consumers API](../api/api-keys-consumers) (requires a Zuplo API key)               |
| **[Developer Portal](../dev-portal/overview.mdx) key holders** | No                | Key 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](./api-key-leak-detection.mdx) via GitHub secret scanning. For
the full format breakdown and service limits, see
[API Keys concepts](../concepts/api-keys.md#api-key-format) and
[API Key Service Limits](./api-key-service-limits.mdx).

## Next steps

- [API Key Authentication](./api-key-authentication.mdx) — how the
  authentication policy sets `request.user`
- [Route to Backends Based on User Identity](../guides/user-based-backend-routing.mdx)
  — full guide on metadata-driven routing
- [Dynamic Rate Limiting](../rate-limiting/dynamic-rate-limiting.mdx) — apply
  different rate limits based on metadata
- [Developer API](./api-key-api.mdx) — manage consumers and metadata
  programmatically
- [API Key Best Practices](./api-key-best-practices.mdx) — security and
  operational guidance
