# IP allowlist

# IP allowlist

Optional, per client, set by the club admin in **Settings → API access**. Empty (the default) means any
source IP may use the credential. When set, it is enforced on **every** call that authenticates with that
client — the token endpoint and every data endpoint.

## What counts as "the caller's IP"

The address compared against the allowlist (and recorded as the client's `usage.lastUsedIp` in the panel) is
the address our infrastructure's own edge saw your connection come from — not a header you can set. There is
nothing to configure on your side; just make sure the **outbound** IP your server actually uses to reach us
is the one on the allowlist (not, for example, a different NAT/proxy egress than you expect).

## Entry formats

- A single address: `203.0.113.7` (stored/normalized as `/32`) or an IPv6 address (`/128`).
- A CIDR range: `198.51.100.0/24`, `2001:db8::/32`. The base address is masked server-side, so
  `10.1.2.3/8` is stored (and matches) as `10.0.0.0/8`.
- Up to 50 entries per client. Zone ids (`%eth0`) are rejected. Duplicates are dropped automatically.

## What a denial looks like

| Where | Response |
|---|---|
| Token request | `400 unauthorized_client`, `code: "API_CLIENT_DISABLED"` is *not* this — IP denial on the token endpoint is `code: "API_IP_NOT_ALLOWED"` |
| Data call | `403`, `application/problem+json`, `code: "API_IP_NOT_ALLOWED"` |

Both are indistinguishable from "this credential genuinely doesn't have access" from the outside on purpose
— don't build logic that tries to detect "IP vs. scope" from the HTTP status alone; the `code` field is the
one to branch on. See [Errors](/docs/errors#API_IP_NOT_ALLOWED).

## Changing the list

Effective **within 60 seconds** everywhere (client state is cached at our end for up to a minute) — no need
to wait for a token to expire after the club admin adds or removes an address.
