Zuplo Developer API
One API for everything you can do in Zuplo, from the portal, the CLI, MCP and Terraform.
Version 4 of the Zuplo Developer API (https://api.zuplo.com/v4). It is the
one canonical API behind the Zuplo portal, the Zuplo CLI, the Zuplo MCP server
and generated SDKs and Terraform providers. This preview covers the AI Gateway,
from creating its project to issuing API keys, the API keys and consumers of
every gateway, and the environments and deployments of every project.
This is a preview. Every request is validated against this contract. An
operation whose backend is not ready yet answers 501 Not Implemented, and its
problem's requiredChanges names the change it waits for. Today that is
listing, reading and adding account members, and three single cases: changing
a provider assignment's label, a provider assignment without a model of
some kind, and redeploying the artifact of a deployment that is not live.
Conventions
Every v4 endpoint follows the v4 API design guidelines, and CI enforces them with a lint ruleset. In short:
- URLs start at a container. Everything in a bucket is addressed under
/v4/buckets/{bucketId}, and everything in a project under/v4/projects/{projectId}. Projects and buckets are listed and created in their account (/v4/accounts/{accountId}/projects) and read by their ID at the root (/v4/projects/{projectId}). A resource with an ID is listed, created and addressed in one collection in its container (…/ai/apps,…/ai/apps/{appId}). Its owner is a property, and a filter on the list (…/ai/apps?poolId=…). - Updates and deletes are safe to retry.
PATCH,PUTandDELETEare idempotent, and so is thePOSTthat adds or changes an association, such as a pool member: sending it again changes the existing one. OtherPOSTs do not accept anIdempotency-Keyyet, so retrying a create whose response was lost can create a second resource. - The last write wins. v4 does not support
ETagandIf-Matchyet, so a write overwrites changes someone made since you read the resource. - Responses always have the same shape. Every property is present, and a
missing value is
null. Some properties cost an extra lookup, such as an app'sconsumers: ask for them withinclude[](?include[]=consumers), and they arenullotherwise. - Lists paginate with
limitandoffsetand return{ data, limit, offset, total, hasMore, next }. - Errors are RFC 9457 problems
(
application/problem+json).
Authentication
Send Authorization: Bearer <token>. The token is the OAuth access token of
a Zuplo sign-in, as the portal and the CLI after zuplo login use. Zuplo API
keys (zpka_…) for automation come later.
Rate limits
Requests are rate limited per caller: 600 requests a minute during the
preview. A throttled request receives 429 Too Many Requests with a
Retry-After header. Every response carries the IETF RateLimit-Policy
header with the limit and its window ("per-caller";q=600;w=60). A throttled
response also carries RateLimit with no requests left and the seconds until
the window resets ("per-caller";r=0;t=30).
GET /v4/accounts
lists the accounts you can use, and its name filter finds an account's ID
from the name the portal shows.POST /v4/accounts/{accountId}/members with their email returns their
userId (user_…) right away, whether or not they have ever signed in, and
every pool and project accepts that userId at once. There is no separate
invitation resource and nothing to poll.
How members behave:
- Who. Account admins add, change and remove members. Every account member can list and read them.
- Invited, then active. Someone new to the account is added with
status: invited, and Zuplo emails them a link. They becomeactivewhen they sign in: through the account's enterprise SSO connection if it has one, otherwise by accepting the email. Accepting happens in the Zuplo portal, not in v4. Everything they were given while invited (pools, projects) takes effect at that moment. Until then they cannot use it. - One per person. A person is in an account once, whatever the case of
their email address. Adding an email that is already a member changes their
role, and for an invited member sends the email again with a new link: that is how an invitation is resent. - Seven days. The link in the email works for seven days
(
invitationExpiresOn). After that the member staysinvited; add them again to send a new link. - Seats. An invited member does not hold a seat. Adding someone new fails
with
409when the account already has as many active members as its plan allows, and so does their first sign-in, if the account filled up in the meantime. sub. A member'ssub, the subject their identity provider assigned them, is read-only andnulluntil their first sign-in. It is not an identifier in v4: every path and reference usesuserId.
ai-gateway project to get
an AI Gateway: Zuplo creates its production environment and that environment's
bucket, and deploys the gateway without a Git repository.
A project is listed and created in its account
(/v4/accounts/{accountId}/projects) and read at the root by its ID
(/v4/projects/{projectId}), and its buckets list the
bucket of each of its environment types. An AI Gateway project has one, its
production bucket, and everything the gateway holds is under
/v4/buckets/{bucketId}.
A project is also a container, like a bucket: what is in it, its members,
environments and deployments, is addressed under /v4/projects/{projectId}.POST to the project's members with their
userId. Give someone only personal AI Gateway keys with the ai-user role.production: one, for the project's default Git branch.preview: one for each other branch, for example to try a change before it is merged.working-copy: one for each person who edits the project in the portal. Each person sees only their own.
/v4/projects/{projectId}/environments/{environmentId}). A project connected
to a Git repository creates a branch's environment on the first push to it,
and deletes it with the branch. What an environment serves is its live
deployment (see Deployments).
An AI Gateway environment has no status (it is null): nothing reliably says
that a gateway serves its newest configuration, so v4 does not claim it. Read
its deployments instead (see Deployments).liveDeploymentId).
A deployment takes minutes, so it is created, then read until it is done:
POST /v4/projects/{projectId}/deployments returns it pending, and
GET …/deployments/{deploymentId} shows it running, then succeeded or
failed. There are no actions: redeploying and rolling back are deployments
too, whose source is an earlier deployment.
Deployments are listed, created and read in their project
(/v4/projects/{projectId}/deployments/{deploymentId}), next to its
environments. The environment is a property, and a filter on the list
(…/deployments?environmentId=…).
An AI Gateway deploys itself when one of its provider assignments is added,
changed or deleted: Zuplo creates a deployment (source.type zuplo) of the
production environment. Nothing links the change to its deployment: to wait for it, list
the environment's deployments (newest first) and read the newest zuplo
deployment created after the change until it has succeeded or failed.
succeeded means the rollout finished, not that every edge serves it yet: an
update never takes the gateway offline, but an edge may serve the previous
version for a while. Only a request to the gateway shows what it serves.
Other changes to an AI Gateway (pools, apps, policies, budgets, keys) need no
deployment, are not tracked, and apply within about a minute./v4/accounts/{accountId}/buckets) and read at the root by their ID
(/v4/buckets/{bucketId}). A project's buckets hold
the bucket ID of each of its environment types, so creating or reading the
project gives you the ID. Everything in a bucket, every AI Gateway resource
included, is addressed under /v4/buckets/{bucketId}.
Every project has a bucket for each of its environment types, and the
environments of one type share it. Custom buckets, created in the account, are
not tied to an environment. Consumers and their API
keys can live in one, so several projects can share them./v4/buckets/{bucketId}/ai/budget-rules, never created or deleted. The
gateway's URL, and whether it serves a change yet, are the project's
production environment and its deployments
(/v4/projects/{projectId}/environments).GET …/ai/providers/{providerId}/models lists the models a provider offers,
with their list prices: Zuplo's catalog for a built-in provider, and the
models it was defined with for a custom one.label, the provider's models it serves, and the environment variable that
holds the provider's API key. Clients select a model in requests as
<label>/<model>, for example anthropic1/claude-sonnet-4-6, and policies,
fallbacks and load balancing name it the same way. So a label cannot be
changed yet: nothing rewrites the policies that name it. One provider can be
assigned several times, with different labels, models or keys.
The key is never sent to this API. It is an environment variable of the
bucket's project, read when the gateway calls the provider, so an assignment
belongs to that project and is not shared. An assignment's models can have
negotiated prices, which replace the list prices in cost metering and cost
budgets.POST to the pool's members with their userId.…/ai/apps: set poolId when you create one, and filter the list by
poolId to see a pool's apps.
An app's API keys belong to its consumers, one for each client or environment
that calls it. Create a consumer in the bucket (POST …/consumers), then link it
to the app by its ID (POST …/ai/apps/{appId}/consumers with its consumerId).
The app returns its linked consumers in consumers.metering policy's per-person rule (budgetBy: expression on
request.user.sub) sets the limits every person gets.
Every gateway has exactly one user app, created with the gateway. It is listed,
read and changed, but never created or deleted.
Everything about one person in the user app is under
…/ai/user-apps/{userAppId}/users/{userId}:
consumers: their API keys, for example one consumer per laptop,budget: their own limits, which replace the per-person defaults, for example a higher daily spend for one engineer,usage: their current usage against their limits.
…/users lists the people who can use the user app. A person has no ID of their
own in it: they are identified by the user app and their userId.
People who can edit the user app are its admins. Everyone who can use the
user app manages their own consumers and keys, and reads their own budget and
usage, with their own userId. Admins can do the same for anyone: issue each
person a key, one request per person, revoke it, see their usage, and set
their budget.- the bucket, for API key authentication on any gateway, such as a customer
or a partner, and for AI apps:
…/consumers. Link a consumer to an AI app, one per client or environment, with its ID:POST …/ai/apps/{appId}/consumerswith itsconsumerId. The app returns its linked consumers inconsumers. - a person in an AI user app, for example one per laptop:
…/ai/user-apps/{userAppId}/users/{userId}/consumers.
/v4/buckets/{bucketId}/consumers/{consumerId},
whatever holds it. What holds it is not a property of the consumer. Create a
consumer with ?include[]=apiKey to get its first key in the same response.
When a key of a consumer in the bucket authenticates a request, the API key
authentication policy sets request.user.sub to the consumer's name and
request.user.data to its metadata. The consumers of apps and people get a
generated name.
A consumer the bucket holds can have managers: the people who see and roll
its keys in your developer portal. They sign in to the developer portal, not to
Zuplo, so they are listed by email, with their portal sub once they have
signed in. They are set with the consumer and have no URLs of their own. The
consumers of apps and people have no managers.
Who manages consumers. A person's consumers in a user app are managed by
the person and the user app's admins (see User Apps). Every other consumer in
the bucket, linked to an app or not, and its keys are managed by the people
who can change the bucket: for a project's bucket, project admins (and
developers, on a preview or development bucket) and account admins; for a
custom bucket, account admins. They list, create, change, link, unlink, roll
and delete them. The members of a pool, its admins included, read the
consumers linked to the pool's apps (include[]=consumers on the app) and
their masked keys, but cannot create, link, change, roll or delete them.
Everyone else gets 403.…/consumers/{consumerId}/api-keys, and each key is
addressed by its ID, /v4/buckets/{bucketId}/api-keys/{apiKeyId}, for example
to revoke a leaked key without looking up its consumer.
Rolling is creating the replacement: a new key for the consumer, whose other
keys expire right away or after previousKeyGracePeriodSeconds. Outside a
roll's grace period, a consumer has one working key.
The full secret is returned when a key is created. After that, reads show only
maskedKey. The one exception is a person's own consumers in a user app: the
person reads a key's secret at …/api-keys/{apiKeyId}/secret, for example for
a key an admin issued for them. The secret belongs to a key, not to its
consumer, which can have several keys during a roll's grace period.