Naming and Data
One casing per context, one ID format, one timestamp format, and a type for every value. A v4 payload looks the same whichever product it comes from.
Casing
Acronyms are words: apiKey, id.
Acronyms are words here too: Ai, Api.
Schema, parameter and response names are PascalCase, and an acronym is written
as a word, whatever its length: AiPool, ApiKey, AiGatewaySettings.
Component names become type names in SDKs, MCP tool schemas and Terraform. One
spelling means nobody has to remember whether a two-letter acronym is capitalized,
and a name reads the same as the property it types (apiKey is an ApiKey).
Code
Code
Header components are the exception: they are named after the header they define
(RetryAfter, WWWAuthenticate).
New values can appear in responses, and clients must handle values they do not know. Every enum description says so.
Identifiers
Resources are identified by opaque IDs, exactly as the service that owns the
resource mints them. v4 never adds, renames or strips a prefix, so an ID in v4 is
the same string as in the service, its logs and its database. Most carry the
service's prefix (prj_, bckt_, csmr_), and some have none: accounts are
UUIDs, environments and deployments are plain strings, and a built-in AI provider
is its name (Anthropic). IDs are
strings, and clients never parse them: the URL and the …Id property say what an
ID is. People have one too: their Zuplo user ID, user_…
(V4-DATA-5). AI pools, apps and user apps share gateway-service's
config_ (question 1,
question 33).
Code
Code
People are their user ID
People are always identified by their Zuplo user ID, user_…. A field or parameter
that names a person (userId, ownerId, …UserId) references the shared
UserId schema. A person's sub is a read-only property, never an identifier.
The user ID is the id of the person's user record in tenant-api. Zuplo assigns
it when a person is first added to an account, before they ever sign in, so a
client can add them to pools and projects in the next request
(question 5). The sub, the subject the
identity provider assigned them (auth0|…, samlp|…), exists only after their
first sign-in. Account members carry it as sub, null until then, and the
members list filters by it (?sub=).
Code
Code
Code
A person who has not signed in yet has no sub, so nothing could be granted to
them until they did.
The managers of a consumer sign in to your developer portal, not to Zuplo, so they
have no userId. A manager is their email, with their portal sub
(DeveloperPortalUserSub) once they have signed in
(question 18). A sub always references
UserSub or DeveloperPortalUserSub, so it never reads like a Zuplo user's ID.
Timestamps
Every timestamp is an RFC 3339 UTC string with format: date-time (DATA-1), and
its name ends in On (NAME-2). It also works the other way round: a property
ending in On is always a timestamp.
Code
Code
Every response has the same shape
Every property of a response is always present (DATA-2). In the spec, every
response property is required, and optional values are nullable
(type: [string, "null"]).
Code
Code
Responses are open: new properties may be added at any time (non-breaking), and clients ignore properties they do not know.
Everything is typed
Today's AI Gateway "configuration" stores everything in an untyped metadata
JSON object. It cannot be validated, documented, or turned into MCP tools or
Terraform.
Code
type says which policy it is, and the schema for that type says what its
options are.
Code
Every object declares its properties. additionalProperties: true and {} are
never used, and neither is an object without properties: a request that has
nothing to set has no body.
A map (deploymentModels) declares the schema of its values and, in requests,
maxProperties.
A oneOf/anyOf has a discriminator, so clients can tell the variants apart.
There are two exceptions: the nullable reference oneOf: [$ref, {type: null}],
and exactly one of several properties, a oneOf of required lists next to the
properties it names (for example a model filter's allowList or blockList).
There, the property that is set tells the variants apart.
An app's policies are a union discriminated by type, with typed options, not
an inboundPolicyChain of raw policy names and untyped options.
A consumer's metadata values are any JSON (ConsumerMetadataValue), as in v1.
It is the customer's data, passed to their own code as request.user.data, and
Zuplo never reads it. No other schema accepts any JSON value
(question 19).
Code
Code
Requests and responses are different schemas
A resource has one response schema (AiApp) and one request schema per write
(CreateAiAppRequest, UpdateAiAppRequest). Nested request objects are named
…Input (BudgetRuleInput).
Request schemas never contain server-owned fields (id, createdOn), so SDK
method signatures and MCP tool input schemas only ever show what a client can
actually send. This is stricter than ROB-4, which allows a single schema with
readOnly/writeOnly markers.
Request schemas (*Request, *Input) | Response schemas | |
|---|---|---|
| Unknown properties | rejected: additionalProperties: false | allowed (open for extension) |
| Required | what the client must send (nothing for PATCH) | every property |
| Strings | maxLength (or an enum or a date format) | unbounded |
| Arrays | maxItems | unbounded |
| Maps | maxProperties | unbounded |
readOnly / writeOnly | no readOnly | no writeOnly |
Scalar components such as enums (ProviderType) are shared by both.
Code
Code
Secrets
Secrets are write-only on input, revealed once on output (…WithSecret), and
masked afterwards. The one exception is a person's own consumers in a user app:
the person can read their keys' secrets again from a singleton that nobody else
can read.
Creating a key, which is also how a key is rolled. The response is an
ApiKeyWithSecret with the secret, exactly once.
Only the maskedKey: zpka_xxxx…xxxx.
…/api-keys/{apiKeyId}/secret returns the key's secret to the person who holds
the consumer, with Cache-Control: no-store. Everyone else gets 403, admins
included. The secret belongs to a key, not to the consumer, which can have
several keys during a roll's grace period.
A person's consumer is theirs, and a user app admin can create it for them. The admin sees the first key's secret once, in the response, and the person reads it when they need it. Only the keys of personal consumers have this singleton: the keys of an app's or the bucket's consumers are seen once, by whoever creates them.
Secrets sent by the client are request-only (writeOnly) and never come back.
A provider's API key is not even sent: a provider assignment names the
environment variable that holds it.
Secrets never appear in URLs, query strings or error details.
Money, units and durations
Money is a JSON number in US dollars (major units), and names carry their unit:
inputPerMillionTokens, previousKeyGracePeriodSeconds.
Code
Code
Unreadable.
Code
Code
The property name or description states the unit ("value is in US dollars for
cost"). If a schema ever needs another currency, it gets an explicit currency
field.
Booleans
Code
Code
The default is the safe or common case.
That an ID is the one the owning service mints, units and money (V4-DATA-2), and the handling of secrets (V4-DATA-3).