v4 API Design Guidelines
These are the rules every endpoint of the Zuplo Developer API v4
(https://api.zuplo.com/v4) follows. They are written to be enforced. Every rule
that a machine can check is a lint rule that runs in CI, and a spec with a single
finding does not merge.
Status: draft for review
This is the first version of the v4 guidelines, written together with the first v4 surface (the AI Gateway). Everything here is open for discussion. The decisions page lists what is decided, what deliberately differs from today's guidelines, and the open questions.
Why v4
Zuplo has grown several API surfaces, each with its own shape. The same concept (an API key, a bucket, a "configuration") is reached through different URLs with different payloads.
api.zuplo.comfor the portal and Agent Zdev.zuplo.comv1 to v3 for customers- services that each picked their own URL scheme
- one prefix,
/v4, for every product - one set of conventions, enforced by lint
- one OpenAPI document
The portal is the first consumer. Building it on v4 also retires
api.zuplo.com over time.
The Zuplo CLI, the MCP server (an MCP tool for each operation) and generated SDKs and Terraform providers all come from one OpenAPI document.
It should read like Stripe or GitHub, because that is also what agents are trained on.
The rules in one minute
Every resource is listed, created and addressed in one collection, directly in
its container, and its owner is a filter:
/v4/buckets/{bucketId}/ai/apps?poolId=… then
/v4/buckets/{bucketId}/ai/apps/{appId}Gateway data lives in a bucket, and every path to it starts with
/v4/buckets/{bucketId}. Accounts and projects are the other
containers: what a project deploys starts with /v4/projects/{projectId}.
Fixed methods per URL shape, and fixed status codes per method.
PUT, PATCH, DELETE and the POST that upserts an association are idempotent
by design. A POST that creates has no Idempotency-Key yet.
No free-form JSON, no metadata blobs, and separate request and response
schemas.
camelCase fields, kebab-case URLs and enums, timestamps ending in On, opaque
IDs exactly as the services mint them (people too: user_…), and null instead
of omitted fields.
Errors are RFC 9457 problems, and every operation documents the errors it can return.
One Authorization: Bearer header for API keys and OAuth tokens. Authorization
is decided in the service.
How to read these pages
Every page follows the same pattern: the rule, an example to copy, and the counter-example that fails.
Green cards show what a v4 API does. Copy them.
Red cards show what lint or review rejects, usually something today's APIs do.
A box with a pink edge states a rule. Lint messages and reviews cite its ID, and every ID is a link target: V4-URL-2.
Lint means a rule in lint/ruleset.yaml fails CI. Review means a person
checks it in the pull request. Every page ends with what enforces it.
Paths on these pages are colored by the role of each segment:
/aiNamespace/appsCollection/{appId}ID/usageSingletonHow these guidelines relate to the shared Zuplo guidelines
Zuplo already has API design guidelines: the original
Notion page,
refined into the decided rules (June 2026) in zuplo/internal-tools
(plugins/zuplo-engineering/skills/api-design) with IDs like URL-4 and COLL-2.
Those rules are not wrong. They were written to steer an existing fleet, so they
allow grandfathered exceptions, "use judgment" cases, and several valid shapes.
v4 does not add a competing standard. It builds on the shared guidelines:
These pages cite the shared rule IDs, and the v4 spec also passes the shared lint ruleset with a perfect score. That ruleset checks the mechanical rules only. A reviewer checks the rest, URL-4 for example.
One /v4 for everything (VER-1), URLs that keep their container (URL-4),
account-rooted URLs (URL-4a), IDs as the services mint them, not always prefixed
(DATA-3), a hand-written spec (SPEC-1), and no Idempotency-Key (ROB-1) or
ETag (ROB-2).
Where the shared rules offer options or say SHOULD, v4 picks one and makes it a MUST: one URL scheme and pagination on every list.
Those carry V4-* IDs: containers and re-rooting inside them, typed schemas only,
separate request and response schemas, and operation IDs derived from the URL.
v4 starts clean, so there are no ignore files and no exceptions list. If a rule is wrong, change the rule.
When v4 settles, its additions can move into the shared ruleset so other APIs benefit too. Every place where v4 deliberately chooses differently is listed in Decisions and Open Questions.
How the rules are enforced
spec/openapi.yamlOpenAPI 3.1, hand-written, reviewed in pull requests.
lint/tests/rules.test.mjsEach rule is shown a spec that breaks it.
npm run lintRoutes up to date, zero findings at any severity, and every rule test passes.
The rule index maps every rule ID to the lint rule that enforces it, or marks it "review" when it needs human judgment.