# Set Up an AI Gateway in Six Calls

This is the flow behind the AI Gateway setup wizard, where an agent asks you a few
questions and configures everything. It is also a tour of the v4 conventions: short
URLs and typed bodies.

All calls use `Authorization: Bearer $TOKEN`, either a Zuplo API key or the OAuth
token of the signed-in user.

To use every feature (guardrails, caching, tracing, gateway-wide budgets, sub-pools,
key rotation), follow the [end-to-end tutorial](/ai-gateway/tutorial).

Calls name the account and the bucket by ID. If you only know your account's name,
look up its ID (a UUID) first:

```bash
curl "https://api.zuplo.com/v4/accounts?name=acme-corp" \
  -H "Authorization: Bearer $TOKEN"
```

## 1. Create the project

```bash
curl -X POST "https://api.zuplo.com/v4/accounts/5f0c7c2e-8d3a-4b1e-9f2a-6c1d0e7b3a49/projects" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "acme-ai-gateway", "type": "ai-gateway" }'
```

```json
{
  "id": "prj_Hn4QzW7cVb2LpX9sTe5RgK1m",
  "accountId": "5f0c7c2e-8d3a-4b1e-9f2a-6c1d0e7b3a49",
  "name": "acme-ai-gateway",
  "type": "ai-gateway",
  "buckets": [],
  "createdOn": "2026-07-31T01:52:37.366Z",
  "updatedOn": "2026-07-31T01:52:37.366Z"
}
```

An AI Gateway project has one bucket, its production bucket. It is created right
after the response, so `buckets` is usually still empty. List the account's buckets
with the project's ID until it is there:

```bash
curl "https://api.zuplo.com/v4/accounts/5f0c7c2e-8d3a-4b1e-9f2a-6c1d0e7b3a49/buckets?projectId=prj_Hn4QzW7cVb2LpX9sTe5RgK1m" \
  -H "Authorization: Bearer $TOKEN"
```

Its `id` is the bucket ID. Everything the gateway has lives in this bucket, and
every other call in this guide is under its URL:

```bash
BUCKET=https://api.zuplo.com/v4/buckets/bckt_Y40ypGODCDP3S7iZxNwCAbIe
```

For a project that already exists, the same list returns its bucket. The gateway
deploys in the background while you carry on. Its production environment
has the gateway's `url`:

```bash
curl "https://api.zuplo.com/v4/projects/prj_Hn4QzW7cVb2LpX9sTe5RgK1m/environments?type=production" \
  -H "Authorization: Bearer $TOKEN"
```

The environment has no `status` for an AI Gateway (it is null). To know when the
gateway is deployed, read the environment's newest deployment until its `status` is
`succeeded`:

```bash
curl "https://api.zuplo.com/v4/projects/prj_Hn4QzW7cVb2LpX9sTe5RgK1m/deployments?environmentId=4f8b6c1e-2d7a-4e3b-9c5f-8a1d2e6b7c90&limit=1" \
  -H "Authorization: Bearer $TOKEN"
```

A succeeded deployment does not guarantee that every edge serves it yet. Only a
request to the gateway's `url` shows what it serves.

## 2. Assign a provider

Providers are Zuplo's built-in ones (and any custom ones you define). List them,
and the models one offers with their prices:

```bash
curl "$BUCKET/ai/providers?type=anthropic" \
  -H "Authorization: Bearer $TOKEN"
```

```bash
curl "$BUCKET/ai/providers/Anthropic/models" \
  -H "Authorization: Bearer $TOKEN"
```

The API never takes the provider's key. Add it to the project as an environment
variable of its production environment, `ANTHROPIC_API_KEY` here, then assign the
provider to the gateway:

```bash
curl -X POST "$BUCKET/ai/provider-assignments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "providerId": "Anthropic",
    "label": "anthropic",
    "apiKeyEnvironmentVariable": "ANTHROPIC_API_KEY",
    "models": [{ "name": "claude-sonnet-4-5" }, { "name": "claude-haiku-4-5" }]
  }'
```

`label` is the routing prefix, and `models` are the provider's models the
gateway serves. The variable is not checked now: if it is missing, requests to
these models fail until you set it.

The response is the assignment, along with a
`Location: /v4/buckets/bckt_Y40ypGODCDP3S7iZxNwCAbIe/ai/provider-assignments/aip_6eljoh` header.
If the connection drops, retrying is safe here: labels are unique, so an assignment that was already created makes the
retry fail with `409` instead of creating a second one.

To see what you can route to, read the assignment's `models` in the response (or
`GET $BUCKET/ai/provider-assignments/{assignmentId}`). Clients name a model in
requests by the label, `anthropic/claude-sonnet-4-5`, and policies name it the
same way: that is the model's `ref`. So the label cannot be changed later.

## 3. Create a pool

```bash
curl -X POST "$BUCKET/ai/pools" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Engineering",
    "budgetRules": [
      {
        "budgetBy": "app",
        "limits": [
          {
            "meter": "cost",
            "period": "monthly",
            "action": "block",
            "value": 500
          }
        ]
      }
    ],
    "policyTemplate": [
      { "policy": { "type": "auth" } },
      {
        "policy": {
          "type": "model-filtering",
          "completions": {
            "allowList": [
              "anthropic/claude-haiku-4-5",
              "anthropic/claude-sonnet-4-5"
            ]
          }
        },
        "canEdit": true
      },
      { "policy": { "type": "metering" }, "canEdit": true }
    ]
  }'
```

`budgetRules` is the pool's own budget: $500 a month for all of its apps together.
`policyTemplate` is what every new app in the pool starts with: authentication,
which apps cannot change, and model filtering and metering, which they can.

## 4. Create an app in the pool

```bash
curl -X POST "$BUCKET/ai/apps" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "poolId": "config_IM1F8Z4rUT6rDPhabvfnrt27", "name": "Support bot" }'
```

The app starts with a copy of the pool's policy template. The response includes its
`policies` and its `baseUrl`, the URL its clients call.

## 5. Issue an API key

An app's keys belong to its consumers, one for each client or environment. Create
the production consumer in the bucket with its first key:

```bash
curl -X POST "$BUCKET/consumers?include[]=apiKey" \
  --globoff \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support-bot-production",
    "description": "Support bot production"
  }'
```

```json
{
  "id": "csmr_brw0leTAVw478VFKD4F6OqVI",
  "bucketId": "bckt_Y40ypGODCDP3S7iZxNwCAbIe",
  "name": "support-bot-production",
  "description": "Support bot production",
  "tags": {},
  "metadata": {},
  "createdOn": "2026-10-07T09:20:11.002Z",
  "updatedOn": "2026-10-07T09:20:11.002Z",
  "apiKey": {
    "id": "key_XKrbtT8H72PH0gQpblaXfstB",
    "consumerId": "csmr_brw0leTAVw478VFKD4F6OqVI",
    "description": null,
    "maskedKey": "zpka_xxxx…xxxx",
    "expiresOn": null,
    "createdOn": "2026-10-07T09:20:11.002Z",
    "updatedOn": "2026-10-07T09:20:11.002Z",
    "secret": "zpka_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxx"
  }
}
```

`apiKey.secret` appears only here. If the response is lost, a retry fails with
`409` because the name is taken: roll the key with a `POST` to
`$BUCKET/consumers/csmr_brw0leTAVw478VFKD4F6OqVI/api-keys` to get a new secret,
which is also how you rotate it later.

Then link the consumer to the app, so its key calls the app. Sending it again
changes nothing:

```bash
curl -X PUT "$BUCKET/ai/apps/config_Kx7gDiGsr1c2tOOUW5kpmXZn/consumers/csmr_brw0leTAVw478VFKD4F6OqVI" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

The app now lists the consumer in its `consumers`.

## 6. Call the gateway

```bash
curl -X POST "https://acme-ai-gateway-main-b8e2f1a.zuplo.app/a/Kx7gDiGsr1c2tOOUW5kpmXZn/v1/chat/completions" \
  -H "Authorization: Bearer zpka_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-haiku-4-5",
    "messages": [{ "role": "user", "content": "Hello" }]
  }'
```

## Changing things later

To give the app a budget of its own, read it, then write. `policies` is replaced
as a whole, so send the complete list:

```bash
curl "$BUCKET/ai/apps/config_Kx7gDiGsr1c2tOOUW5kpmXZn" \
  -H "Authorization: Bearer $TOKEN"

curl -X PATCH "$BUCKET/ai/apps/config_Kx7gDiGsr1c2tOOUW5kpmXZn" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "policies": [
      { "type": "auth" },
      {
        "type": "model-filtering",
        "completions": {
          "allowList": [
            "anthropic/claude-haiku-4-5",
            "anthropic/claude-sonnet-4-5"
          ]
        }
      },
      {
        "type": "metering",
        "budgetRules": [
          {
            "budgetBy": "app",
            "limits": [
              {
                "meter": "requests",
                "period": "daily",
                "action": "block",
                "value": 50000
              }
            ]
          }
        ]
      }
    ]
  }'
```

v4 does not support `If-Match` yet, so the last write wins: a change someone
made to the app's policies between your read and your write is overwritten.

## Personal keys for your team

```bash
# Find the gateway's user app: every gateway has exactly one
curl "$BUCKET/ai/user-apps" \
  -H "Authorization: Bearer $TOKEN"

# Give everyone a default of $20 per day: the metering policy's per-person rule
curl -X PATCH "$BUCKET/ai/user-apps/config_UsTroIp2e0K8mWYGj1GT8U13" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "policies": [
      { "type": "auth" },
      {
        "type": "metering",
        "budgetRules": [
          {
            "budgetBy": "expression",
            "expression": "request.user.sub",
            "limits": [
              {
                "meter": "cost",
                "period": "daily",
                "action": "block",
                "value": 20
              }
            ]
          }
        ]
      }
    ]
  }'

# Each person creates a consumer with its first key (for example for Claude Code),
# with their own userId in the path. A user app admin sends the same request for
# anyone, and the person reads the key with GET $BUCKET/api-keys/{apiKeyId}/secret
curl -X POST "$BUCKET/ai/user-apps/config_UsTroIp2e0K8mWYGj1GT8U13/users/user_Jm5tR8wQz2KcX7vB4nLp9HdS/consumers?include[]=apiKey" \
  --globoff \
  -H "Authorization: Bearer $THEIR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Claude Code on my laptop"
  }'

# Give one person a higher daily budget: idempotent, safe to retry
curl -X PUT "$BUCKET/ai/user-apps/config_UsTroIp2e0K8mWYGj1GT8U13/users/user_Jm5tR8wQz2KcX7vB4nLp9HdS/budget" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "limits": [
      { "meter": "cost", "period": "daily", "action": "block", "value": 50 }
    ]
  }'
```

## Add someone to the pool

```bash
# Look up the person's userId by email
curl "https://api.zuplo.com/v4/accounts/5f0c7c2e-8d3a-4b1e-9f2a-6c1d0e7b3a49/members?email=jane@acme.com" \
  -H "Authorization: Bearer $TOKEN"

# Make them a pool admin: same request, same result, every time
curl -X POST "$BUCKET/ai/pools/config_IM1F8Z4rUT6rDPhabvfnrt27/members" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user_Jm5tR8wQz2KcX7vB4nLp9HdS", "role": "admin" }'

# Not in the account yet? Add them by email: the response has their userId right
# away, with status "invited", and Zuplo emails them a link.
curl -X POST "https://api.zuplo.com/v4/accounts/5f0c7c2e-8d3a-4b1e-9f2a-6c1d0e7b3a49/members" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "raj@acme.com" }'

# Add that userId to the pool in the next request, without waiting. Raj can use
# the pool once he signs in. A pool admin works on every account; member needs
# role-based access control.
curl -X POST "$BUCKET/ai/pools/config_IM1F8Z4rUT6rDPhabvfnrt27/members" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user_Rq4Ys9LmB2xT7nKc5WvH8dJe", "role": "admin" }'
```

## Give someone personal keys only

An account member with the project role `ai-user` can use the user app with
their own keys and sees nothing else in the project. An invited member works too:
the role takes effect when they sign in.

```bash
curl -X POST "https://api.zuplo.com/v4/projects/prj_Hn4QzW7cVb2LpX9sTe5RgK1m/members" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user_Jm5tR8wQz2KcX7vB4nLp9HdS", "role": "ai-user" }'
```
