Zuplo API v4
Zuplo Developer API

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, PUT and DELETE are idempotent, and so is the POST that adds or changes an association, such as a pool member: sending it again changes the existing one. Other POSTs do not accept an Idempotency-Key yet, so retrying a create whose response was lost can create a second resource.
  • The last write wins. v4 does not support ETag and If-Match yet, 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's consumers: ask for them with include[] (?include[]=consumers), and they are null otherwise.
  • Lists paginate with limit and offset and 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).

Tags
Accounts
An account holds an organization's people, projects and buckets. Accounts are addressed by ID, like everything else in v4. An account ID is a UUID. GET /v4/accounts lists the accounts you can use, and its name filter finds an account's ID from the name the portal shows.
Account Members
The people in an account. A person exists in Zuplo as soon as they are added: 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 become active when 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 stays invited; add them again to send a new link.
  • Seats. An invited member does not hold a seat. Adding someone new fails with 409 when 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's sub, the subject their identity provider assigned them, is read-only and null until their first sign-in. It is not an identifier in v4: every path and reference uses userId.
Projects
A project is one gateway in an account. Create an 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}.
Project Members
The people with a role on a project. A membership has no ID of its own. It is identified by the project and the user, so adding someone and changing their role are the same request: a POST to the project's members with their userId. Give someone only personal AI Gateway keys with the ai-user role.
Environments
An environment is one deployed copy of a project's gateway, with its own URL. A project has three types:
  • 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.
An AI Gateway project has only its production environment. Environments are listed, created and read in their project (/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).
Deployments
A deployment is one build of a project's source, rolled out to one environment. An environment keeps its deployments, and serves the last one that succeeded (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.
Buckets
A bucket is the container for one gateway's data. An AI Gateway keeps all of its custom providers, provider assignments, pools, apps, user apps and API keys in the bucket of the project's production environment, so the bucket determines the project. Buckets are listed and created in their account (/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.
Budget Rules
The AI Gateway's own budget: the rules for all of its traffic, on top of the budgets of its pools and apps. A project has exactly one AI Gateway, which keeps its data in the project's production bucket, so its budget rules are a singleton list, read and set as a whole at /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).
Providers
An AI provider is an upstream LLM service the gateway can call: one of Zuplo's built-in providers (OpenAI, Anthropic, Azure AI, Bedrock, …), which every bucket lists and nobody can change, or a custom OpenAI-compatible endpoint defined in the bucket. A provider is a catalog entry. It holds no key and routes nothing until a provider assignment uses it. 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.
Provider Assignments
A provider assignment is the gateway's choice to use a provider: a routing 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.
Pools
A pool groups apps that share a budget, a policy template and people (pools were called "teams" before). The policy template is the list of policies a new app starts with, and says which of them the app may change or remove. Pools can be nested: a sub-pool's budget rules apply in addition to its parent's, and it uses its parent's template unless it has its own.
Pool Members
The people who manage a pool. A membership has no ID of its own. It is identified by the pool and the user, so adding someone and changing their role are the same request: a POST to the pool's members with their userId.
Apps
An app is one client of the AI Gateway, such as a product feature, a service or an agent. It has its own policies (authentication, model routing, budgets, guardrails, caching and tracing), and belongs to exactly one pool. A new app starts with a copy of its pool's policy template. Apps are listed and created at …/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.
User Apps
A user app gives every person in the account their own personal API keys and personal budget for the AI Gateway. That covers coding agents like Claude Code or Cursor, scripts, and notebooks. Its policies work like an app's, and its 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.
Consumers
A consumer is one credential: an API key and the keys that replace it when it is rolled. Every API key belongs to a consumer, and every consumer is created under what holds it:
  • 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}/consumers with its consumerId. The app returns its linked consumers in consumers.
  • a person in an AI user app, for example one per laptop: …/ai/user-apps/{userAppId}/users/{userId}/consumers.
A consumer is then addressed by its ID, /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.
API Keys
An API key is one version of a consumer's credential. A consumer's keys are listed and rolled at …/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.