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.
Calls name the account and the bucket by ID. If you only know your account's name,
look up its ID (a UUID) first:
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:
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:
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:
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:
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.
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.
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:
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
Code
# Find the gateway's user app: every gateway has exactly onecurl "$BUCKET/ai/user-apps" \ -H "Authorization: Bearer $TOKEN"# Give everyone a default of $20 per day: the metering policy's per-person rulecurl -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}/secretcurl -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 retrycurl -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
Code
# Look up the person's userId by emailcurl "https://api.zuplo.com/v4/accounts/5f0c7c2e-8d3a-4b1e-9f2a-6c1d0e7b3a49/[email protected]" \ -H "Authorization: Bearer $TOKEN"# Make them a pool admin: same request, same result, every timecurl -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": "[email protected]" }'# 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.