Zuplo API v4
Request Handling

Errors

v4 uses the shared error rules (ERR-1 to ERR-6) unchanged and adds one: every operation documents every error it can return (V4-ERR-1). An error is written for the person or agent who has to act on it.

Problem JSON

Every 4xx and 5xx response is an RFC 9457 problem, served as application/problem+json and described by the shared Problem schema:

JSONCode
{ "type": "https://zup.fail/http-status/400", "title": "Bad Request", "status": 400, "detail": "The request body has 2 invalid fields.", "instance": "/v4/buckets/bckt_Y40ypGODCDP3S7iZxNwCAbIe/ai/pools/config_IM1F8Z4rUT6rDPhabvfnrt27/apps", "trace": { "timestamp": "2026-10-07T09:14:02.117Z", "requestId": "req_7f3k2mQx9LpA" }, "errors": [ { "location": "body.name", "message": "Must not be empty." }, { "location": "body.budgetRules[0].limits[0].period", "message": "Must be one of hourly, daily, weekly, monthly." } ] }
Anatomy of a problem
type

https://zup.fail/{slug} for a specific Zuplo error, or https://zup.fail/http-status/{code} for a generic one (ERR-3).

title

The HTTP reason phrase, exactly. Never anything occurrence-specific (ERR-2).

status

The HTTP status code.

detail

Written for people. It can be shown in the portal or by an agent as is. If the UI cannot show an API error, fix the API.

instance

The path of the request.

trace.requestId

On every error, and matches the request-id header (ROB-5).

errors

Every invalid field on a validation failure, not only the first.

Do
Fixed title, specific detail
JSONCode
{ "title": "Forbidden", "detail": "Editing an AI pool requires pool admin privileges." }
Don't
An occurrence-specific title
JSONCode
{ "title": "You are not a pool admin", "detail": "Forbidden" }
Do
A 500 says what to do
JSONCode
{ "title": "Internal Server Error", "detail": "Something went wrong. Quote the request ID when you contact support." }
Don't
A 500 that leaks internals
JSONCode
{ "detail": "duplicate key value violates unique constraint \"pools_name_key\"" }

No stack traces, upstream errors or SQL.

Status codes

400Invalid request

Malformed JSON, schema violations, cross-field rules ("a warn limit must be below the block limit"), unknown query parameters, and a body field that names something that does not exist (a poolId). All validation errors are 400.

401Not authenticated

Missing, invalid or expired credentials. Has WWW-Authenticate. Never used for "not allowed".

403Not allowed

Authenticated but not allowed (an OpenFGA denial). The detail names the missing role.

404Not found

The resource does not exist, or the caller has no access to the account it belongs to (masking, ERR-5).

409Conflict

With the current state: name already taken, resource still in use, or singleton already exists.

429Rate limited

Has Retry-After.

500Unexpected

No internal details. Quote trace.requestId to support.

Do
400 for validation
Code
HTTP/1.1 400 Bad Request Content-Type: application/problem+json
Don't
422 for validation
Code
HTTP/1.1 422 Unprocessable Content Content-Type: application/problem+json

One code to handle. v4 does not use 422.

Documented on every operation

V4-ERR-1

Every operation documents the errors it can return, using the shared responses in #/components/responses.

Lintv4-error-responsesShared rule: ERR-6

The shared rule (ERR-6) asks for 401 and default. v4 documents what the operation can actually return, so agents and SDKs can plan error handling from the spec:

OperationDocuments
every operation401 403 429 default
the path has an IDalso 404
there are query parameters or a bodyalso 400

Operations add other codes when they can occur. DELETE of a pool that still has apps documents 409, for example, and so does creating a provider assignment, whose label must be unique.

Do
Reference the shared responses
YAMLCode
responses: "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict"
Don't
Inline error responses
YAMLCode
responses: "404": description: Not found content: application/json: …

v4 error types

Generic conditions use https://zup.fail/http-status/{code}. Conditions that clients act on get a registered slug in the shared catalog (@zuplo/errors):

no-access-{action}
403

https://zup.fail/no-access-{action}, for example no-access-edit-ai-pool.

Lint
Fails CI on any finding
v4-error-responsesv4-document

The required responses, application/problem+json, the Problem schema, WWW-Authenticate and Retry-After.

Review
Checked by people in the pull request

The wording of title and detail.

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