# Methods and Status Codes

<Lead>

In v4 the method and the [URL shape](/guidelines/urls#the-four-url-shapes) fully
describe what an operation does and what it returns. A client, an SDK generator
or an agent never has to read prose to know whether a call creates, replaces or
merges, or which status code to expect.

</Lead>

## <N n="1" />The method table

<Rule id="V4-METH-1 V4-METH-2" lint="v4-method-shape v4-success-responses" shared="METH-1 METH-2">

Each URL shape allows a fixed set of methods, and each method returns a fixed
success status.

</Rule>

| Method | Used on | Meaning | Success | Retry-safe |
| --- | --- | --- | --- | --- |
| <Method m="GET" /> | any | Read. No body, no side effects. | <Status code="200" /> | yes (safe) |
| <Method m="POST" /> | collection | Create a resource with a server-assigned ID. | <Status code="201" /> + `Location` | no ([no `Idempotency-Key`](/guidelines/idempotency#no-idempotency-key)) |
| <Method m="POST" /> | association collection | Add an association, or replace it if it exists ([upsert](#associations-are-added-or-set)). | <Status code="201" /> created + `Location`, or <Status code="200" /> replaced | yes |
| <Method m="PATCH" /> | item, singleton | Change some fields ([JSON Merge Patch](#patch-is-json-merge-patch)). | <Status code="200" /> | yes |
| <Method m="PUT" /> | singleton, association item | Replace a resource whose identity the client knows. | <Status code="201" /> created + `Location`, or <Status code="200" /> replaced | yes |
| <Method m="DELETE" /> | item, association item | Delete. | <Status code="204" /> no body | yes |

The shared guidelines (METH-1, METH-2) allow more combinations. v4 narrows them,
so there is exactly one way to do each thing:

<DoDont>
<Do title="PATCH a resource with a server ID">

```http
PATCH /v4/buckets/{bucketId}/ai/apps/{appId}
```

</Do>
<Dont title="PUT a resource with a server ID">

```http
PUT /v4/buckets/{bucketId}/ai/apps/{appId}
```

There is one way to update a thing, and no risk of a `PUT` that silently clears
fields the client did not send.

</Dont>
<Do title="POST to create or associate">

```http
POST /v4/buckets/{bucketId}/ai/apps
```

```http
POST /v4/buckets/{bucketId}/consumers
```

```http
POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members
```

There are no actions. Rolling a key is creating its replacement (see
[actions](/guidelines/urls#singletons-and-no-actions)).

</Do>
<Dont title="POST to update">

```http
POST /v4/buckets/{bucketId}/ai/apps/{appId}
```

</Dont>
</DoDont>

**No `HEAD` or `OPTIONS` in the spec.** The gateway answers them (CORS, for
example).

## <N n="2" />Bodies

<Tiles>
<Tile title="JSON in, problem JSON out">

Request and response bodies are `application/json`. Errors are
`application/problem+json` (see [errors](/guidelines/errors)).

</Tile>
<Tile title="Bodies on writes only">

`GET` and `DELETE` have no request body. `POST`, `PATCH` and `PUT` always
have one. Send `{}` when all fields are optional.

</Tile>
<Tile title="The body is the new resource">

A create's body holds the new resource and nothing else. What the create also
does to other resources is a query parameter: `include[]=apiKey` issues a new
consumer's first key, and `previousKeyGracePeriodSeconds` on a roll says how
long the keys it replaces keep working.

</Tile>
</Tiles>

<DoDont>
<Do title="Return the full resource">

```http
PATCH /v4/buckets/{bucketId}/ai/pools/{poolId}
Content-Type: application/json

{ "name": "Engineering" }
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{ "id": "config_…", "name": "Engineering", … }
```

The same representation as `GET`, so a client never needs a follow-up read.

</Do>
<Dont title="Return an acknowledgement">

```http
PATCH /v4/buckets/{bucketId}/ai/pools/{poolId}
Content-Type: application/json

{ "name": "Engineering" }
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{ "success": true }
```

```http
DELETE /v4/buckets/{bucketId}/ai/pools/{poolId}
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{ "deleted": true }
```

`DELETE` returns `204` with no body.

</Dont>
</DoDont>

<DoDont>
<Do title="Other resources in the query">

```http
POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys?previousKeyGracePeriodSeconds=86400
Content-Type: application/json

{ "description": "Production backend" }
```

The body is the new key. The grace period is about the keys it replaces.

</Do>
<Dont title="Other resources in the body">

```http
POST /v4/buckets/{bucketId}/consumers/{consumerId}/api-keys
Content-Type: application/json

{
  "description": "Production backend",
  "previousKeyGracePeriodSeconds": 86400
}
```

</Dont>
</DoDont>

## <N n="3" />PATCH is JSON Merge Patch

`PATCH` bodies follow [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) (the
"tri-state" of DATA-2):

<Fields title="In the body → effect">
<Field name="property absent">

unchanged

</Field>
<Field name="property is null">

cleared (set to its empty value)

</Field>
<Field name="property has a value">

set

</Field>
<Field name="nested object">

merged recursively with the same rules

</Field>
<Field name="array">

**replaced as a whole**

</Field>
</Fields>

```http
PATCH /v4/buckets/bckt_Y40ypGODCDP3S7iZxNwCAbIe/ai/pools/config_IM1F8Z4rUT6rDPhabvfnrt27
Content-Type: application/json

{
  "name": "Platform",
  "budgetRules": []
}
```

The pool before and after (abridged):

```diff
 {
-  "name": "Engineering",
+  "name": "Platform",
   "parentPoolId": null,
-  "budgetRules": [{ "budgetBy": "app", "limits": […] }],
+  "budgetRules": [],
   "policyTemplate": [{ "policy": { "type": "auth", … }, … }]
 }
```

This renames the pool, removes all of its own budget rules, and keeps its policy
template.

The consequences for the spec: `Update*Request` schemas have **no required
properties** (`v4-request-schemas` checks this), and fields that can be cleared
are nullable. Properties that cannot
change after creation (an app's `poolId`, a provider assignment's `providerId`) are simply not in
the update schema.

<DoDont>
<Do title="Optional, mutable fields only">

```yaml
UpdateAiAppRequest:
  type: object
  additionalProperties: false
  properties:
    name: …
    policies: …
```

</Do>
<Dont title="Required or immutable fields">

```yaml
UpdateAiAppRequest:
  type: object
  required: [name]
  properties:
    name: …
    poolId: …
```

</Dont>
</DoDont>

## <N n="4" />Associations are added or set

An [association](/guidelines/urls#associations-resources-without-an-id) has no ID
of its own. It is identified by its parent and a key the client knows, such as a
person's `userId`. Either way of writing it creates it or replaces it, so sending
the same request twice leaves the same state. Each association uses exactly one of
the two, or is read-only (a person in a user app):

<Tiles>
<Tile method="POST" title="Added to a collection">

A person **joins** a pool or a project, and a consumer is **linked** to an app.
The association is added with a `POST` to the collection, with the key in the
body. Sending it again changes the existing one (a member's role) or nothing (a
link that already exists).

<Path method="POST" p="/v4/buckets/{bucketId}/ai/pools/{poolId}/members" />

<Path method="POST" p="/v4/buckets/{bucketId}/ai/apps/{appId}/consumers" />

</Tile>
<Tile method="PUT" title="Set for a key">

A value a person has exactly one of, which always exists, is a singleton under
the association, **set** with a `PUT` and the complete value in the body: their
budget in a user app.

<Path method="PUT" p="/v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/{userId}/budget" />

</Tile>
</Tiles>

```http
POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members
Content-Type: application/json

{ "userId": "user_Jm5tR8wQz2KcX7vB4nLp9HdS", "role": "admin" }
```

```http
PUT /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/user_Jm5tR8wQz2KcX7vB4nLp9HdS/budget
Content-Type: application/json

{
  "limits": [
    { "meter": "cost", "period": "daily", "action": "block", "value": 50 }
  ]
}
```

<Flow>
<FlowStep title="It does not exist yet">

<Status code="201">Created</Status> with `Location`, its URL
(`…/members/{userId}`, `…/users/{userId}/budget`).

</FlowStep>
<FlowStep title="It exists: same request again">

<Status code="200">OK</Status>. It is replaced.

</FlowStep>
</Flow>

Both operations are named `upsert-{resource}` and take an
`Upsert{Resource}Request` with the complete writable representation. An
association that is written with a `POST` has no `PUT`, and the other way round.
`PUT` is otherwise left for singletons, which have no collection to `POST` to,
such as a person's budget, or the gateway's budget rules, where the body is the
[list itself](/guidelines/urls#singletons-and-no-actions).

## <N n="5" />DELETE

<Tiles cols={3}>
<Tile status="204" title="Deleted">

No body.

</Tile>
<Tile status="404" title="Already gone">

Idempotent behavior. Clients, SDKs and Terraform treat `404` on `DELETE` as
success.

</Tile>
<Tile status="409" title="Blocked by dependents">

The `detail` names the dependents (a pool that still has apps). A delete never
removes them silently.

</Tile>
</Tiles>

A resource that exists but cannot be changed or deleted, such as one of Zuplo's
built-in AI providers, answers `PATCH` and `DELETE` with <Status code="409" />,
whose `detail` says it is read-only. It is never `405`: the method is allowed on
the path, and only this resource refuses it.

If the delete does cascade, the operation description says what goes with it:

<DoDont>
<Do title="Say what a delete takes with it">

```yaml
summary: Delete an app
description: Deletes an app and revokes all of its API keys.
```

</Do>
<Dont title="Cascade silently">

```yaml
summary: Delete an app
description: Deletes an app.
```

</Dont>
</DoDont>

## <N n="6" />Changes that take effect asynchronously

Some writes are stored immediately but reach the data plane a few seconds later.
For example, a new provider assignment is usable once the AI Gateway picks up
its configuration.

<Tiles>
<Tile tone="do" title="A 2xx means accepted and durable, never maybe">

The response is returned once the change is **stored and validated**.

</Tile>
<Tile tone="do" title="The description says when it takes effect">

For example: "Read the newest deployment until it has `succeeded`. That does
not guarantee every edge serves the change yet."

</Tile>
</Tiles>

When a change needs a deployment, the deployment reports it, so a client can wait
for it. An AI Gateway is its project's production environment, and a provider
assignment change creates a deployment of it: a client reads the newest one
(<Path method="GET" p="/v4/projects/{projectId}/deployments" /> with its
`environmentId`) until it has `succeeded`. The environment's `status` is null for
an AI Gateway, because nothing reliably says that every edge serves the newest
configuration: a succeeded deployment has rolled out, but an edge may serve the
previous version for a while.

Operations that take longer than a request should wait use the shared
**create-then-poll** pattern (METH-3): the operation is a resource of its own.
A deployment is the first in v4.

<Flow>
<FlowStep method="POST" title="Create an operation">

`POST` creates an operation resource.

</FlowStep>
<FlowStep method="GET" title="Poll it">

Until its `status` is `succeeded` or `failed`.

</FlowStep>
<FlowStep label="status">

`pending` / `running` / `succeeded` / `failed` / `cancelled`

</FlowStep>
</Flow>

<DoDont>
<Do title="Create a deployment, then read it">

```http
POST /v4/projects/{projectId}/deployments
Content-Type: application/json

{
  "environmentId": "4f8b6c1e-2d7a-4e3b-9c5f-8a1d2e6b7c90",
  "source": {
    "type": "deployment",
    "deploymentId": "7e1d4b9a-3c6f-4d2e-8a5b-0f9c2e7d1b34"
  }
}
```

```http
GET /v4/projects/{projectId}/deployments/a3c9e5d1-6b2f-4a8e-b7d4-1f0e9c8b2a65
```

The `201` returns it `pending`. Read it until its `status` is `succeeded` or
`failed`.

</Do>
<Dont title="A redeploy action">

```http
POST /v4/projects/{projectId}/environments/{environmentId}/redeploy
```

A verb to learn, and nothing to read afterwards. Redeploying is creating a
deployment.

</Dont>
</DoDont>

<EnforcedBy
  lint="v4-method-shape v4-success-responses v4-request-bodies v4-request-schemas"
  checks="Methods per URL shape, success codes, request bodies, and no required properties on PATCH."
>

Create-then-poll for long-running operations (METH-3).

</EnforcedBy>
