# Authentication and Authorization

<Lead>

One header for every caller. **Who** you are is checked at the gateway. **What**
you may do is decided by the service that owns the resource.

</Lead>

## <N n="1" />One header, two kinds of tokens

<Rule id="AUTH-1 AUTH-2" lint="v4-security v4-parameters">

Every v4 request authenticates with `Authorization: Bearer <token>`. The spec
declares a single `bearerAuth` scheme and applies it to every operation.

</Rule>

<Tiles>
<Tile label="Automation" title="Zuplo API key">

For CI, Terraform, scripts and backend services.

```http
Authorization: Bearer zpka_0f1e…
```

</Tile>
<Tile label="Interactive clients" title="OAuth 2.0 access token">

For the portal, the CLI after `zuplo login`, and an agent you signed in to (the AI
Gateway setup wizard).

```http
Authorization: Bearer eyJhbGciOi…
```

</Tile>
</Tiles>

Both reach the same operations with the same permissions model.

<DoDont>
<Do title="The Authorization header">

```http
GET /v4/buckets/{bucketId}/ai/apps/{appId}
Authorization: Bearer zpka_0f1e…
```

</Do>
<Dont title="Another header, or the URL">

```http
GET /v4/buckets/{bucketId}/ai/apps/{appId}
X-Api-Key: zpka_0f1e…
```

```http
GET /v4/buckets/{bucketId}/ai/apps/{appId}?apiKey=zpka_0f1e…
```

</Dont>
</DoDont>

:::caution{title="Prerequisite"}

API-key callers on v4 need OpenFGA to know about API-key principals (consumers),
not only users. This is the long-lead item from the scoping document. Until it
lands, `api.zuplo.com` accepts the OAuth access token of a Zuplo sign-in only, as
the portal sends it.

:::

## <N n="2" />Authorization happens in the service

<Flow>
<FlowStep label="At the gateway" title="Who are you?" status="401">

Zuplo auth policies check the credential.

</FlowStep>
<FlowStep label="In the service" title="What may you do?" status="403 404">

The service that owns the resource decides, through OpenFGA. `404` when the
caller has no access to the account at all (ERR-5).

</FlowStep>
</Flow>

The gateway does **not** make OpenFGA checks for v4 routes. With
[re-rooted URLs](/guidelines/urls#re-root-at-the-container), a path like
`/v4/buckets/{bucketId}/ai/apps/{appId}` names the bucket, but not the pool the
app is in, and only the service knows whether the app really is in that bucket.
So only the service can decide in one check, and it answers `404` for a resource
that is not in the container in the path. This follows the 2026-10-06 decision to
stop doing OpenFGA at the edge.

## <N n="3" />Personal resources without /me

Operations that are about the caller ("my personal keys", "my usage") use the
same URLs as everyone else's, with the caller's own `userId`.

<DoDont>
<Do title="One URL, scoped by permission">

```http
GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/user_Jm5tR8wQ…/usage
```

```http
GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/user_Jm5tR8wQ…/consumers
```

With your own `userId`, returns your usage and your consumers. User app admins can
pass anyone's `userId`.

</Do>
<Dont title="A separate route for the caller">

```http
GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/me/consumers
```

```http
GET /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/me/usage
```

</Dont>
</DoDont>

There are no `/users/me/...` routes. The same URL works for a person, an admin and
an API key.

## <N n="4" />Rate limits and correlation

<DoDont>
<Do title="IETF RateLimit headers (ROB-3)">

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit: …
RateLimit-Policy: …
```

</Do>
<Dont title="Vendor X- headers">

```http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
```

</Dont>
</DoDont>

<Tiles>
<Tile title="Limited per caller">

A throttled request gets `429` with `Retry-After`. The limits are stated once, in
the spec's `info.description`.

</Tile>
<Tile title="Every response has a request-id">

Every response has a `request-id` header, and every error repeats it in
`trace.requestId` (ROB-5).

</Tile>
</Tiles>

<EnforcedBy
  lint="v4-security v4-parameters"
  checks="Bearer-only security schemes, declared on every operation. No Authorization, X-* or zuplo-user parameters, and no cookies."
>

Where authorization happens.

</EnforcedBy>
