Zuplo API v4
Designing Resources

Methods and Status Codes

In v4 the method and the URL shape fully describe what an operation does and what it returns. A client, an SDK generator or an agent never has to read prose to know whether a call creates, replaces or merges, or which status code to expect.

The method table

V4-METH-1 · V4-METH-2

Each URL shape allows a fixed set of methods, and each method returns a fixed success status.

Lintv4-method-shapev4-success-responsesShared rules: METH-1, METH-2
MethodUsed onMeaningSuccessRetry-safe
GETanyRead. No body, no side effects.200yes (safe)
POSTcollectionCreate a resource with a server-assigned ID.201 + Locationno (no Idempotency-Key)
POSTassociation collectionAdd an association, or replace it if it exists (upsert).201 created + Location, or 200 replacedyes
PATCHitem, singletonChange some fields (JSON Merge Patch).200yes
PUTsingleton, association itemReplace a resource whose identity the client knows.201 created + Location, or 200 replacedyes
DELETEitem, association itemDelete.204 no bodyyes

The shared guidelines (METH-1, METH-2) allow more combinations. v4 narrows them, so there is exactly one way to do each thing:

Do
PATCH a resource with a server ID
Code
PATCH /v4/buckets/{bucketId}/ai/apps/{appId}
Don't
PUT a resource with a server ID
Code
PUT /v4/buckets/{bucketId}/ai/apps/{appId}

There is one way to update a thing, and no risk of a PUT that silently clears fields the client did not send.

Do
POST to create or associate
Code
POST /v4/buckets/{bucketId}/ai/apps
Code
POST /v4/buckets/{bucketId}/consumers
Code
POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members

There are no actions. Rolling a key is creating its replacement (see actions).

Don't
POST to update
Code
POST /v4/buckets/{bucketId}/ai/apps/{appId}

No HEAD or OPTIONS in the spec. The gateway answers them (CORS, for example).

Bodies

JSON in, problem JSON out

Request and response bodies are application/json. Errors are application/problem+json (see errors).

Bodies on writes only

GET and DELETE have no request body. POST, PATCH and PUT always have one. Send {} when all fields are optional.

The body is the new resource

A create's body holds the new resource and nothing else. What the create also does to other resources is a query parameter: include[]=apiKey issues a new consumer's first key, and previousKeyGracePeriodSeconds on a roll says how long the keys it replaces keep working.

Do
Return the full resource
Code
PATCH /v4/buckets/{bucketId}/ai/pools/{poolId} Content-Type: application/json { "name": "Engineering" }
Code
HTTP/1.1 200 OK Content-Type: application/json { "id": "config_…", "name": "Engineering", … }

The same representation as GET, so a client never needs a follow-up read.

Don't
Return an acknowledgement
Code
PATCH /v4/buckets/{bucketId}/ai/pools/{poolId} Content-Type: application/json { "name": "Engineering" }
Code
HTTP/1.1 200 OK Content-Type: application/json { "success": true }
Code
DELETE /v4/buckets/{bucketId}/ai/pools/{poolId}
Code
HTTP/1.1 200 OK Content-Type: application/json { "deleted": true }

DELETE returns 204 with no body.

Do
Other resources in the query
Code
POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys?previousKeyGracePeriodSeconds=86400 Content-Type: application/json { "description": "Production backend" }

The body is the new key. The grace period is about the keys it replaces.

Don't
Other resources in the body
Code
POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys Content-Type: application/json { "description": "Production backend", "previousKeyGracePeriodSeconds": 86400 }

PATCH is JSON Merge Patch

PATCH bodies follow RFC 7396 (the "tri-state" of DATA-2):

In the body → effect
property absent

unchanged

property is null

cleared (set to its empty value)

property has a value

set

nested object

merged recursively with the same rules

array

replaced as a whole

Code
PATCH /v4/buckets/bckt_Y40ypGODCDP3S7iZxNwCAbIe/ai/pools/config_IM1F8Z4rUT6rDPhabvfnrt27 Content-Type: application/json { "name": "Platform", "budgetRules": [] }

The pool before and after (abridged):

Code
{ - "name": "Engineering", + "name": "Platform", "parentPoolId": null, - "budgetRules": [{ "budgetBy": "app", "limits": […] }], + "budgetRules": [], "policyTemplate": [{ "policy": { "type": "auth", … }, … }] }

This renames the pool, removes all of its own budget rules, and keeps its policy template.

The consequences for the spec: Update*Request schemas have no required properties (v4-request-schemas checks this), and fields that can be cleared are nullable. Properties that cannot change after creation (an app's poolId, a provider assignment's providerId) are simply not in the update schema.

Do
Optional, mutable fields only
YAMLCode
UpdateAiAppRequest: type: object additionalProperties: false properties: name: … policies: …
Don't
Required or immutable fields
YAMLCode
UpdateAiAppRequest: type: object required: [name] properties: name: … poolId: …

Associations are added or set

An association has no ID of its own. It is identified by its parent and a key the client knows, such as a person's userId. Either way of writing it creates it or replaces it, so sending the same request twice leaves the same state. Each association uses exactly one of the two, or is read-only (a person in a user app):

POSTAdded to a collection

A person joins a pool or a project, and a consumer is linked to an app. The association is added with a POST to the collection, with the key in the body. Sending it again changes the existing one (a member's role) or nothing (a link that already exists).

POST/v4/buckets/{bucketId}/ai/pools/{poolId}/membersPOST/v4/buckets/{bucketId}/ai/apps/{appId}/consumers
PUTSet for a key

A value a person has exactly one of, which always exists, is a singleton under the association, set with a PUT and the complete value in the body: their budget in a user app.

PUT/v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/budget
Code
POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members Content-Type: application/json { "userId": "user_Jm5tR8wQz2KcX7vB4nLp9HdS", "role": "admin" }
Code
PUT /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/user_Jm5tR8wQz2KcX7vB4nLp9HdS/budget Content-Type: application/json { "limits": [ { "meter": "cost", "period": "daily", "action": "block", "value": 50 } ] }
It does not exist yet

201Created with Location, its URL (…/members/{userId}, …/users/{userId}/budget).

It exists: same request again

200OK. It is replaced.

Both operations are named upsert-{resource} and take an Upsert{Resource}Request with the complete writable representation. An association that is written with a POST has no PUT, and the other way round. PUT is otherwise left for singletons, which have no collection to POST to, such as a person's budget, or the gateway's budget rules, where the body is the list itself.

DELETE

204Deleted

No body.

404Already gone

Idempotent behavior. Clients, SDKs and Terraform treat 404 on DELETE as success.

409Blocked by dependents

The detail names the dependents (a pool that still has apps). A delete never removes them silently.

A resource that exists but cannot be changed or deleted, such as one of Zuplo's built-in AI providers, answers PATCH and DELETE with 409, whose detail says it is read-only. It is never 405: the method is allowed on the path, and only this resource refuses it.

If the delete does cascade, the operation description says what goes with it:

Do
Say what a delete takes with it
YAMLCode
summary: Delete an app description: Deletes an app and revokes all of its API keys.
Don't
Cascade silently
YAMLCode
summary: Delete an app description: Deletes an app.

Changes that take effect asynchronously

Some writes are stored immediately but reach the data plane a few seconds later. For example, a new provider assignment is usable once the AI Gateway picks up its configuration.

A 2xx means accepted and durable, never maybe

The response is returned once the change is stored and validated.

The description says when it takes effect

For example: "Read the newest deployment until it has succeeded. That does not guarantee every edge serves the change yet."

When a change needs a deployment, the deployment reports it, so a client can wait for it. An AI Gateway is its project's production environment, and a provider assignment change creates a deployment of it: a client reads the newest one (GET/v4/projects/{projectId}/deployments with its environmentId) until it has succeeded. The environment's status is null for an AI Gateway, because nothing reliably says that every edge serves the newest configuration: a succeeded deployment has rolled out, but an edge may serve the previous version for a while.

Operations that take longer than a request should wait use the shared create-then-poll pattern (METH-3): the operation is a resource of its own. A deployment is the first in v4.

POSTCreate an operation

POST creates an operation resource.

GETPoll it

Until its status is succeeded or failed.

status

pending / running / succeeded / failed / cancelled

Do
Create a deployment, then read it
Code
POST /v4/projects/{projectId}/deployments Content-Type: application/json { "environmentId": "4f8b6c1e-2d7a-4e3b-9c5f-8a1d2e6b7c90", "source": { "type": "deployment", "deploymentId": "7e1d4b9a-3c6f-4d2e-8a5b-0f9c2e7d1b34" } }
Code
GET /v4/projects/{projectId}/deployments/a3c9e5d1-6b2f-4a8e-b7d4-1f0e9c8b2a65

The 201 returns it pending. Read it until its status is succeeded or failed.

Don't
A redeploy action
Code
POST /v4/projects/{projectId}/environments/{environmentId}/redeploy

A verb to learn, and nothing to read afterwards. Redeploying is creating a deployment.

Lint
Fails CI on any finding
v4-method-shapev4-success-responsesv4-request-bodiesv4-request-schemas

Methods per URL shape, success codes, request bodies, and no required properties on PATCH.

Review
Checked by people in the pull request

Create-then-poll for long-running operations (METH-3).

Every rule ID and what enforces it: the rule index
Last modified on