Mapping from api.zuplo.com
What the portal and Agent Z call today (management-api → gateway-service and
tenant-api), and the v4 operation that replaces each call. This is the work list for
the backend, and for moving the portal onto v4.
Accounts and projects by ID
Today's URLs name the account and the project (/v1/accounts/{accountName}/projects/{projectName}).
v4 addresses both by ID: {accountId} and {projectId} (prj_…). Accounts have
an ID today (a UUID, the id in /v1/accounts), and v4 shows it unchanged. Every
ID in v4 is the one the service mints, with its prefix or without one
(question 33): projects keep tenant-api's
prj_, and pools, apps and user apps keep gateway-service's config_
(question 1). Responses carry
accountId and projectId instead of accountName and projectName, and
buckets are filtered by projectId.
| Today | v4 |
|---|---|
GET /v1/accounts | GET /v4/accounts (?name=… finds one by name) |
GET /v1/accounts/{accountName}/projects/{projectName} | GET /v4/projects/{projectId} |
Configurations → pools, apps, user apps
| Today | v4 |
|---|---|
GET /v1/buckets/{bucketId}/configurations (root), PATCH …/configurations/{rootId} | GET/PUT /v4/buckets/{bucketId}/ai/budget-rules (the rules, as the body) |
GET /v1/buckets/{bucketId}/configurations/teams | GET /v4/buckets/{bucketId}/ai/pools |
GET /v1/buckets/{bucketId}/configurations/apps | GET /v4/buckets/{bucketId}/ai/apps (?poolId= for one pool) |
GET /v1/buckets/{bucketId}/configurations/user-apps | GET /v4/buckets/{bucketId}/ai/user-apps |
POST /v1/buckets/{bucketId}/configurations with metadata.type | POST /v4/buckets/{bucketId}/ai/pools, POST /v4/buckets/{bucketId}/ai/apps (the user app is created with the gateway) |
GET/PATCH/DELETE /v1/buckets/{bucketId}/configurations/{configurationId} | /v4/buckets/{bucketId}/ai/pools/{poolId}, /v4/buckets/{bucketId}/ai/apps/{appId}, GET/PATCH /v4/buckets/{bucketId}/ai/user-apps/{userAppId} (never deleted) |
GET /v1/buckets/{bucketId}/configurations/{configurationId}/usage | GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/usage (per person); app and pool usage are phase 2 |
Payload changes:
Today (metadata) | v4 |
|---|---|
label | name |
parentId | parentPoolId (pools), poolId (apps) |
inboundPolicyChain[] { name, options, enabled } of apps and user apps | policies[] { type, enabled, …options }, in the same order. type is the entry's policy type without ai-gateway- (ai-gateway-metering → metering, internal-only); any other declared policy is { type: "custom", name } |
metering options.budgetRules[].meters[], throwOnFailure | metering budgetRules[].limits[], failOnError |
model filtering and model override: one of allowList/blockList, default/force per capability | both fields, the unused one null |
semantic cache expirationSecondsTtl | semantic-cache ttlSeconds |
smart router classifier.app, smartRoutingEnabled, intents[].id | smart-router classifier, routingEnabled, intents[].name |
DLP partialMask.keepLast | dlp maskKeepLast |
Akamai firewall api-key | akamai-firewall apiKey |
classifierPrompt as an array of lines | one string |
credentials in options (apiKey, classifier.app.apiKey) | write-only, never returned; omitted on a write, the stored one is kept |
deprecated options (metering limits, smart router classifierAppID, classifierModel, classifierAppApiKey, classifier.chatCompletions, classifier.jev, adapter), semantic cache onUnknownShape, DLP engine | not exposed |
metadata.budgetRules on pools | AiPool.budgetRules |
the pool's inboundPolicyChain (its template), each entry's permissions.{canEdit,canRemove}, inheritPolicyTemplate | policyTemplate[] { policy, canEdit, canRemove }, inheritPolicyTemplate |
personal budget rule (budgetBy: expression on request.user.sub) | unchanged, in the user app's metering policy |
the root configuration's metadata.budgetRules | the body of …/ai/budget-rules (AiBudgetRules) |
consumer.apiKeys[] inline, with plaintext key | the app's consumers (consumers in the bucket, linked with POST /v4/buckets/{bucketId}/ai/apps/{appId}/consumers { consumerId }), and their keys (masked), secret only on create |
type, version, deploymentName, $schema, bucketId inside metadata, children, deleted | not exposed |
createdOn, updatedOn | unchanged |
Members and budgets
| Today | v4 |
|---|---|
GET /v1/buckets/{bucketId}/configurations/{configurationId}/members | GET /v4/buckets/{bucketId}/ai/pools/{poolId}/members |
POST …/members { sub, role } | POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members { userId, role }, which also changes an existing member's role |
GET/PUT/PATCH/DELETE …/members/{sub} | GET/DELETE /v4/buckets/{bucketId}/ai/pools/{poolId}/members/{userId} |
GET /v1/auth/members?entity=ai-team&entityId={poolId} | GET /v4/buckets/{bucketId}/ai/pools/{poolId}/members |
POST /v1/auth/members { user, entity: "ai-team", entityId, role } for an account member | POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members { userId, role } |
PUT /v1/auth/members { user, entity: "ai-team", entityId, role } | POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members { userId, role }, or DELETE …/members/{userId} for role: null |
GET/PUT /v1/buckets/{bucketId}/configurations/{configurationId}/budget-overrides/{sub} | GET/PUT /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/budget (PUT { limits } sets or replaces) |
DELETE …/budget-overrides/{sub} | PUT /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/budget { "limits": [] } |
| none (no v1 route lists a user app's overrides) | GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users?hasBudgetOverride=true&include[]=budget, each person with their budget |
People are identified by their Zuplo user ID (user_…) in a field named userId
(V4-DATA-5), not by their sub as
today. GET /v4/accounts/{accountId}/members?email= (or ?sub=) finds someone's
userId. /v1/auth/members takes an email or a sub as user and decides itself
whether to add the person or invite them. In v4, someone new is added to the
account by email first, which returns their userId at once, and pools and
projects take that userId in the next request (next section).
Account members and invitations
The portal invites people with /v1/auth/members and /v1/auth/invites (unified
members API), and resends, revokes and accepts invitations with the
account-member-invites routes. Both write the same AccountMemberInvite rows.
v4 has no invitation resource: adding someone to the account by email creates a
member with status: invited and a userId right away
(question 5).
| Today | v4 |
|---|---|
POST /v1/auth/members { user: email, entity: "account", entityId, role } | POST /v4/accounts/{accountId}/members { email, role } |
POST /v1/auth/members { user: email, entity: "ai-team", entityId: poolId, role } for someone not in the account | POST /v4/accounts/{accountId}/members { email }, then POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members { userId, role } with the userId it returned |
POST /v1/auth/members { user: sub, entity: "project", entityId, role } for an account member | POST /v4/projects/{projectId}/members { userId, role } |
POST /v1/auth/members { user: email, entity: "project", … } for someone not in the account | POST /v4/accounts/{accountId}/members { email }, then POST /v4/projects/{projectId}/members { userId, role }, ai-user included |
POST /v1/accounts/{accountName}/account-member-invites { userEmail, metadata } | POST /v4/accounts/{accountId}/members { email, role } |
GET /v1/auth/invites?entity=account&entityId={accountName}, GET /v1/accounts/{accountName}/account-member-invites | GET /v4/accounts/{accountId}/members: invited people are members with status: invited |
GET /v1/auth/invites?entity=ai-team&entityId={poolId} | GET /v4/buckets/{bucketId}/ai/pools/{poolId}/members: invited people are pool members already |
PATCH /v1/accounts/{accountName}/account-member-invites/{inviteId}/$resend | POST /v4/accounts/{accountId}/members with the same email |
DELETE /v1/accounts/{accountName}/account-member-invites/{inviteId} | DELETE /v4/accounts/{accountId}/members/{userId} |
GET/POST /v1/account-member-invites/{code} (the invitee reads and accepts) | not in v4: accepting stays in the portal |
The behavior is the same in both, except where the last column says what has to change in tenant-api before v4 can serve it:
| Behavior | Today (tenant-api) | v4 |
|---|---|---|
| How a person is identified | Their sub, which exists only after their first sign-in | A Zuplo user ID (user_…), assigned when they are added. tenant-api keeps a ledger of pending grants keyed by email and writes them at the first sign-in, or keys users by an internal ID and maps the sub to it then. The contract is the same either way. |
| Grants for someone who has not signed in | One invitation per grant (metadata.account, .project, .team), so one project or one pool | Any number, on pools and projects, by userId. They take effect at the first sign-in. |
| Who adds people | Account admins, also when a project or pool add needs a new account member | Account admins |
| Who lists | Account members (/v1/auth/invites). The legacy list needs an admin. | Account members, invited and active in one list |
| Someone who uses Zuplo but is not in the account | Invited, never added directly | Added as invited, active once they accept |
| SSO accounts | Cannot add anyone without an invitation | The first SSO sign-in makes an invited member active, with nothing to accept |
| An account member | entity: "account": 409. A project or pool: added directly, no invitation. | 200: their role is changed |
| Default account role | member with RBAC, admin without. /v1/auth/members with entity: "account" requires role. | member with RBAC, admin without |
| Adding the same address again | Refused with 400 while the first invitation is unexpired. After it expires, a second row is created next to it. | Changes the role, and an invited member gets a new link and invitationExpiresOn |
| Sending it again | $resend: same ID and link, the seven days restart | Add the member again |
| Upper and lower case in the email | Ignored when looking up the person (User.email is citext) and when accepting, but not by the check for an earlier invitation, so [email protected] and [email protected] can each get one | Ignored everywhere |
| Expiry | Seven days after the email is handed off; $resend restarts them | Seven days (invitationExpiresOn); adding them again restarts them |
| Accepting after expiry | The accept route does not check expiration. Only the portal's invite page refuses an expired link. | The link stops working |
| Expired invitations | Hidden by /v1/auth/invites, but kept until revoked or accepted | The member stays invited, with a past invitationExpiresOn, until removed or added again |
| List order | Newest first | Oldest first, the default order |
| Seats | Pending invitations hold none. Checked when inviting and again when accepting. | Same: invited members hold none |
| Pool deleted before they accept | Accepting fails after the account and project roles are written. The invitation stays, so every retry fails the same way. | The pool membership goes with the pool; they still become active |
| Removing someone who has not accepted | Revoke the invitation | DELETE …/members/{userId}, which also removes their pool and project roles |
| Status codes | Non-admin role without RBAC: 403 (/v1/auth/members), 400 (legacy). Member limit: 403 (/v1/auth/members), 400 (legacy and accept). | 400 for a role the plan does not allow, 409 for the member limit |
| Response | 200 { email, role, result: "invited" }, without the invitation's ID | 201 with the member, its userId and its Location, or 200 for an existing member |
Providers and models
| Today | v4 |
|---|---|
| Today | v4 |
| --- | --- |
GET /v1/buckets/{bucketId}/providers | GET /v4/buckets/{bucketId}/ai/providers (built-in and custom) |
POST /v1/buckets/{bucketId}/providers (custom provider) | POST /v4/buckets/{bucketId}/ai/providers { name, baseUrl, models } |
PATCH/DELETE /v1/buckets/{bucketId}/providers/{providerId} | PATCH/DELETE /v4/buckets/{bucketId}/ai/providers/{providerId} (custom only; built-in is 409) |
GET /v1/buckets/{bucketId}/providers/{providerId}/models, …/models/{modelId} | GET /v4/buckets/{bucketId}/ai/providers/{providerId}/models, …/models/{modelId} |
POST /v1/buckets/{bucketId}/providers/{providerId}/models, PATCH/DELETE …/models/{modelId} | PATCH /v4/buckets/{bucketId}/ai/providers/{providerId} { models } (replaces a custom provider's models) |
GET /v1/buckets/{bucketId}/provider-assignments, GET /v1/accounts/{account}/projects/{project}/ai-provider-assignments | GET /v4/buckets/{bucketId}/ai/provider-assignments |
POST /v1/accounts/{account}/projects/{project}/ai-provider-assignments | POST /v4/buckets/{bucketId}/ai/provider-assignments { providerId, label, models, apiKeyEnvironmentVariable } |
GET/PATCH/DELETE …/ai-provider-assignments/{assignmentId} | GET/PATCH/DELETE /v4/buckets/{bucketId}/ai/provider-assignments/{assignmentId} |
PATCH /v1/buckets/{bucketId}/provider-assignments/{assignmentId}/models/{modelId} (negotiated price) | PATCH /v4/buckets/{bucketId}/ai/provider-assignments/{assignmentId} { models: [{ name, negotiatedPricing }] } |
An assignment serves the models in its models and nothing else
(decision 36). Requests and policies name
a model <label>/<model>, as in v1, so a label cannot be changed yet: a change
answers 501 until something rewrites the policies that name it. The bucket
determines the project, so v4 finds the assignment's project from the bucket
instead of the account and project names in the URL.
Payload changes: a provider's type is kebab-case (OpenAI → openai,
BedrockMantle → bedrock-mantle, AzureAI → azure-ai), and its id stays
the backend's (BedrockMantle, aiprov_…). An assignment's provider and
customProviderName become one providerId, envVarName becomes
apiKeyEnvironmentVariable, endpointUrl becomes baseUrl, and
modelSelections ({ completions, embeddings }) becomes models, one list of
names. metadata as a JSON string is gone. Model prices move from per token
(inputCostPerToken) to per million tokens (pricing.inputPerMillionTokens), and
an assignment's overriddenTokenRateCard becomes the model's negotiatedPricing.
The dialect enum google_predict is not exposed.
API keys
| Today | v4 |
|---|---|
GET /v1/api-keys/{bucketId}/consumers (app consumers) | GET /v4/buckets/{bucketId}/ai/apps/{appId} (consumers) |
POST /v1/api-keys/{bucketId}/consumers (app consumers) | POST /v4/buckets/{bucketId}/consumers, then POST /v4/buckets/{bucketId}/ai/apps/{appId}/consumers { consumerId } |
GET /v1/api-keys/{bucketId}/consumers/{consumerId}/keys | GET /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys |
POST /v1/api-keys/{bucketId}/consumers/{consumerId}/roll-key { expiresInSeconds } | POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys?previousKeyGracePeriodSeconds= |
GET/POST /v1/api-keys/{bucketId}/users/me/consumers | GET/POST /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/consumers, with the caller's own userId |
GET/PATCH/DELETE /v1/api-keys/{bucketId}/users/me/consumers/{consumerId} | GET/PATCH/DELETE /v4/buckets/{bucketId}/consumers/{consumerId} |
GET /v1/api-keys/{bucketId}/users/me/consumers/{consumerId} (the key in full) | GET /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys, then GET /v4/buckets/{bucketId}/api-keys/{apiKeyId}/secret (the person only) |
GET …/users/me/consumers/{consumerId}/keys, DELETE …/keys/{keyId} | GET /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys, DELETE /v4/buckets/{bucketId}/api-keys/{apiKeyId} |
POST …/users/me/consumers/{consumerId}/roll-key | POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys?previousKeyGracePeriodSeconds= |
POST /v1/api-keys/{bucketId}/consumers { personalOwnerSub, configurationId } (admin) | POST /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/consumers |
GET /v1/api-keys/{bucketId}/consumers?ownerSub=… (admin) | GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/consumers |
GET /v1/api-keys/{bucketId}/users/me/usage | GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/usage, with the caller's own userId |
A consumer is one credential, as in the key service: an app holds one for each
client or environment, and a person one per device. v4 creates them under the app
or the person, the app returns its consumers in consumers with
include[]=consumers, and consumers and keys
carry no appId, userAppId or ownerId. There is no roll endpoint: a POST to
a consumer's keys rolls, which is the v1 roll-key behavior. Its other keys expire
immediately, or after previousKeyGracePeriodSeconds (a query parameter). Creating
an app no longer creates a key
implicitly. The caller creates a consumer with include[]=apiKey and gets the key's
secret once.
Consumers of any gateway
The public key-bucket API on dev.zuplo.com/v1 manages the consumers of API key
authentication. Its key buckets are the v4 buckets, and its consumers and keys move
into them.
| Today | v4 |
|---|---|
GET/POST /v1/accounts/{accountName}/key-buckets | GET/POST /v4/accounts/{accountId}/buckets |
GET …/key-buckets/{bucketName} | GET /v4/buckets/{bucketId} (by ID) |
GET/POST …/key-buckets/{bucketName}/consumers | GET/POST /v4/buckets/{bucketId}/consumers (?name=… finds one by name) |
GET/PATCH/DELETE …/consumers/{consumerName} | GET/PATCH/DELETE /v4/buckets/{bucketId}/consumers/{consumerId} (by ID) |
POST …/consumers?with-api-key=true | POST /v4/buckets/{bucketId}/consumers?include[]=apiKey |
GET …/consumers/{consumerName}/keys | GET /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys |
POST …/consumers/{consumerName}/keys (adds a key) | none: a POST to …/consumers/{consumerId}/api-keys rolls (question 15) |
GET/PATCH/DELETE …/consumers/{consumerName}/keys/{keyId} | GET/PATCH/DELETE /v4/buckets/{bucketId}/api-keys/{apiKeyId} |
POST …/consumers/{consumerName}/roll-key { expiresOn } | POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys?previousKeyGracePeriodSeconds= |
GET/POST …/consumers/{consumerName}/managers { email, sub } | the consumer's managers (GET /v4/buckets/{bucketId}/consumers/{consumerId}), set with PATCH …/consumers/{consumerId} { managers: [{ email }] } |
DELETE …/consumers/{consumerName}/managers/{managerId} | PATCH /v4/buckets/{bucketId}/consumers/{consumerId} with the other managers |
Payload changes: a consumer's keys are not embedded (include-api-keys, apiKeys[]).
List them at …/consumers/{consumerId}/api-keys. A consumer's managers are part
of it, managers: [{ email, sub }], set on create and replaced as a whole with a
PATCH (question 18). email identifies a
manager, so managers added by email alone are in v4, with sub (from your
developer portal's identity provider) null until they sign in. Managers have no
eusr_… ID and no URLs of their own. metadata takes any
JSON object of up to 3 KB (3,072 bytes), as in v1
(question 19). Keys are always masked
(maskedKey), and the secret is returned once, when the key is created.
keys/$bulk, a key value chosen by the client, tag.{name}=… filters and
key-format=visible are not in phase 1, so the developer portal keeps using v1
to show a key (question 16), and neither
are updating and deleting a bucket.
Environments and deployments
A v1 "deployment" is an environment: one per Git branch, plus a working copy per
person, named by its deploymentName. v4 calls it an environment and gives it an
ID. A v4 deployment is one build of an environment, today's deployment version
(buildId), and it is created, then read until it is done
(open question 31). Both are in the
project, which v4 makes a container like a bucket: what it deploys, and its
members, are under /v4/projects/{projectId}.
| Today | v4 |
|---|---|
GET /v1/accounts/{accountName}/projects/{projectName}/deployments | GET /v4/projects/{projectId}/environments |
GET /v1/environments?accountName&projectName (self-hosted, without working copies) | GET /v4/projects/{projectId}/environments, with working copies |
GET /v1/deployments/{deploymentName}, GET /v1/environments/{environmentName} | GET /v4/projects/{projectId}/environments/{environmentId} (…/environments?branch= finds a branch's) |
DELETE /v1/deployments/{deploymentName} | DELETE /v4/projects/{projectId}/environments/{environmentId} (preview only) |
| Created on the first deploy of a branch | POST /v4/projects/{projectId}/environments { branch }, or the first push |
GET /v1/deployments/{deploymentName}/versions | GET /v4/projects/{projectId}/deployments?environmentId=… |
GET /v1/deployments/{deploymentName}/versions/{buildId} | GET /v4/projects/{projectId}/deployments/{deploymentId} |
POST /v1/deployments/source-url { accountName, projectName, environment, sha }, PUT to uploadUrl | POST /v4/projects/{projectId}/deployments { environmentId, source: { type: "upload", commitSha } }, PUT to upload.url |
GET /v1/accounts/{accountName}/projects/{projectName}/deployment-status/{statusId} | GET /v4/projects/{projectId}/deployments/{deploymentId} |
POST /v1/deployments/{deploymentName}/deploy, POST /v1/deployment-versions/{id}/redeploy { type: "artifact" } or { type: "rebuild" } | POST /v4/projects/{projectId}/deployments { environmentId, source: { type: "deployment", deploymentId, rebuild } } |
Payload changes:
| Today | v4 |
|---|---|
name (deploymentName) as the identifier | id, tenant-api's UUID for the environment (Deployment.id); name stays, shown only |
branchName, label, urlParameter | branch, null for a working copy |
environmentType PRODUCTION / PREVIEW / WORKING_COPY, or production / preview / development | type production / preview / working-copy |
state (IS_STARTING, STARTED, ERRORED, DELETE_SCHEDULED, …), set by the last stage that reported | status provisioning / deploying / live / deleting, from the environment's deployments; null for an AI Gateway, whose deployments say where a change is |
userSub (deployer-app@zuplo for every non-working copy) | userId, the person of a working copy, null otherwise |
isProtected | hasCustomDomain |
deploymentVersion (the latest build, inline) | liveDeploymentId; the builds are deployments |
A build's id (buildId) and traceId (statusId) | One deployment id, the build's id (DeploymentVersion.id, a UUID) |
A build's SUCCESS / ERROR / IN_PROGRESS, or its stages' in-progress / success / failed / skipped | status pending / running / succeeded / failed |
sourceType (github, cli, …), sha, commitMessage, commitAuthor, commitTimestamp | source.type (git, upload, deployment, zuplo), source.commit { sha, message, authorName, committedOn } |
{ "status": "request enqueued" } on delete and redeploy | 204 for a delete; the new deployment for a redeploy |
loggingId, environmentTarget, deploymentSecret, stages, build steps, pullRequest* | not exposed |
Where v1 and v4 deliberately differ
Both versions read and write the same rows, and each keeps its own behavior:
| Behavior | v1 (unchanged) | v4 |
|---|---|---|
| Creating an app | Creates a consumer named after the app, with a key | Creates neither: link a consumer to it |
| Deleting an app | Deletes the app's consumers and their keys | Unlinks its consumers, which stay in the bucket |
| Rolling a key | Expires the keys that have no expiry | Expires every other key at the end of the grace period, in one transaction |
| Addressing a consumer | By name | By ID |
| Deleting a provider assignment, or one of its models, that a policy uses | Allowed | 409 |
During the preview
These answer 501 Not Implemented until the change in requiredChanges ships:
| Operation | Waits for |
|---|---|
GET/POST /v4/accounts/{accountId}/members, GET …/members/{userId} | account memberships with a status and a start, and people with a user ID before they sign in (tenant-api T1–T4, with the OpenFGA account_membership type) |
POST /v4/projects/{projectId}/deployments with source.type: deployment, rebuild: false and a deployment that is not the environment's live one | rolling an environment back to an earlier deployment's artifact: the deployer can only push the artifact the environment runs (deployer, T6). Send rebuild: true instead. |
POST/PATCH /v4/buckets/{bucketId}/ai/provider-assignments… whose models include no model of a kind the provider offers | an assignment that selects no model of a kind, where none means every model of that kind today (tenant-api, T8) |
PATCH /v4/buckets/{bucketId}/ai/provider-assignments/{assignmentId} that changes label | renaming a label, and the policies that name its models <label>/<model> with it (tenant-api, T7) |
The plan item IDs in requiredChanges (T1, T7, …) can change while v4 is a
preview.
Not in v4 phase 1
- Analytics (
/v2/analytics/ai-gateway…) and request logs: phase 2, once push vs pull is decided. - Audit logs (
/v1/audit-logs/{bucketId}/…): phase 2. They are append-only, so they use cursor pagination. …/configurations/{configurationId}/resolvedanduser-app/ensure: internal.- Build logs (
/v1/deployments/{deploymentName}/build-logs/{buildId}) and request logs: phase 2. They are append-only, so they use cursor pagination. - Environment variables (
…/environment-variables,…/branches/{branchName}/variables), custom domains and changing a project's default branch ($update-default-branch). - Self-hosted environments (
POST /v1/environments) and the configuration a self-hosted runtime reads (…/configurations,…/local-configurations).