Zuplo API v4
Guidelines

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.

Today
Several APIs, several shapes
  • api.zuplo.com for the portal and Agent Z
  • dev.zuplo.com v1 to v3 for customers
  • services that each picked their own URL scheme
v4
One canonical API
  • one prefix, /v4, for every product
  • one set of conventions, enforced by lint
  • one OpenAPI document
Portal first
If the portal cannot be built on v4, v4 is wrong

The portal is the first consumer. Building it on v4 also retires api.zuplo.com over time.

One document
Every client from one spec

The Zuplo CLI, the MCP server (an MCP tool for each operation) and generated SDKs and Terraform providers all come from one OpenAPI document.

Human-first
Reads like the APIs people know

It should read like Stripe or GitHub, because that is also what agents are trained on.

The rules in one minute

01URLs are short

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}
Read more
02The bucket is the container

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}.

Read more
03The method and URL shape tell you everything

Fixed methods per URL shape, and fixed status codes per method.

Read more
04Updates and deletes are safe to retry

PUT, PATCH, DELETE and the POST that upserts an association are idempotent by design. A POST that creates has no Idempotency-Key yet.

Read more
05Every list paginates

Every list returns { data, limit, offset, total, hasMore, next }.

Read more
06Everything is typed

No free-form JSON, no metadata blobs, and separate request and response schemas.

Read more
07Names follow one convention

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.

Read more
08Errors are problems

Errors are RFC 9457 problems, and every operation documents the errors it can return.

Read more
09One header for every caller

One Authorization: Bearer header for API keys and OAuth tokens. Authorization is decided in the service.

Read more

How to read these pages

Every page follows the same pattern: the rule, an example to copy, and the counter-example that fails.

Do
An example to copy

Green cards show what a v4 API does. Copy them.

Don't
A counter-example

Red cards show what lint or review rejects, usually something today's APIs do.

Rule boxes
The rule itself

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.

Enforcement
Lint or review

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/usageSingleton

How 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:

Applies
Most shared rules apply to v4

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.

Departs
v4 contradicts seven shared rules

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).

Narrows
v4 removes the choices

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.

Adds
v4 adds rules where the shared ones are silent

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.

Never
Nothing is grandfathered

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

The contract
spec/openapi.yaml

OpenAPI 3.1, hand-written, reviewed in pull requests.

The ruleset
lint/ruleset.yaml

vacuum plus 20 custom rules in lint/rules/.

The proof
lint/tests/rules.test.mjs

Each rule is shown a spec that breaks it.

The gate
npm run lint

Routes 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.

Last modified on