> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# API request guard pipeline: the ordered guard chain every request walks

> The ordered sequence of per-request guards every Orbit API call traverses — retired-host gate, platform IP denylist, under-pressure load-shed, ETag, request id, W3C trace context, rate-limit, idempotency, policy scan, auth, and the error envelope — with the status code and response shape each guard returns when it fires.

# API request guard pipeline

Every API request to Orbit walks an **ordered chain of per-request guards**
before it reaches a route handler. One request can fail for many reasons —
a denylisted IP, a saturated event loop, a spent rate-limit bucket, a
duplicate idempotency key — and the guards are sequenced so the cheapest,
most defensive checks reject first. When you know which guard fired and in
what order, a `403`/`429`/`503` stops being a support ticket and becomes a
reading exercise on response headers and the error envelope.

This page maps the chain, the signal each guard returns, and how to read
the order in the code. For per-endpoint limit tables see
[Rate limits](/guides/rate-limits); for the full limiter taxonomy see
[Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy).

## Why a chain matters

Guard ordering fixes three questions that otherwise arrive as support
tickets:

1. **Which guard fired?** A 403 from the IP denylist is a block; a 403
   from your org-level allowlist is a misconfiguration. The order tells you
   which surface rejected the request before auth ever ran.
2. **What got skipped?** A rejected request never reaches later guards —
   a denylist 403 never consumed rate-limit quota, and a 429 shows the
   idempotency guard was skipped on that attempt.
3. **Where is the fix?** Each guard names a distinct owner. Denylist and
   allowlist entries are operator-curated; rate-limit buckets are your
   plan's per-endpoint tables; the under-pressure shed is platform-side
   and retryable.

The chain is deliberately layered in **fastest-reject-first** order. A
brute-force source is turned away by the IP denylist before it ever
reaches the auth code path it was hammering.

## The ordered guard chain

The table below lists every guard in registration order. Guards marked
**terminal** end the request when they fire; the rest annotate the
request for downstream consumers.

| #  | Guard                                            | Type     | Fires on                                           | Status               |
| -- | ------------------------------------------------ | -------- | -------------------------------------------------- | -------------------- |
| 1  | Client-IP normalisation                          | annotate | always                                             | —                    |
| 2  | Retired host guard                               | terminal | request names the retired pre-cutover API host     | 410                  |
| 3  | Platform IP denylist guard                       | terminal | client IP on the platform denylist                 | 403                  |
| 4  | CORS + security headers + compression            | annotate | always                                             | —                    |
| 5  | Under-pressure load-shed                         | terminal | event loop wedged; probes/preflight exempt         | 503                  |
| 6  | ETag conditional reads                           | terminal | matching `If-None-Match` on a cacheable GET/HEAD   | 304                  |
| 7  | Global rate limit (global unless route opts out) | terminal | per-endpoint or global ceiling spent               | 429                  |
| 8  | Request-id assignment                            | annotate | always (honors `X-Request-Id` if sent)             | —                    |
| 9  | W3C trace context                                | annotate | inbound `traceparent`/`tracestate` parsed + echoed | —                    |
| 10 | Sentry scope hooks                               | annotate | always                                             | —                    |
| 11 | Platform metrics                                 | annotate | always                                             | —                    |
| 12 | Locale parse                                     | annotate | `Accept-Language` parsed for error localisation    | —                    |
| 13 | Request logger                                   | annotate | always                                             | —                    |
| 14 | Per-tenant request metrics                       | annotate | authenticated requests                             | —                    |
| 15 | Idempotency guard                                | terminal | replayed `Idempotency-Key` on POST                 | replay or conflict   |
| 16 | Policy-scan hook                                 | terminal | flagged request body patterns                      | flagged response     |
| 17 | Authentication (Clerk session or API key)        | terminal | missing/invalid credentials                        | 401                  |
| 18 | Tenant-resolution preHandler                     | annotate | org/IP allowlist checks per surface                | 403 on violation     |
| 19 | Route handler                                    | —        | business logic                                     | —                    |
| 20 | Error-handler envelope                           | wrap     | converts any thrown error to the standard envelope | status with envelope |

Two consequences fall out of the ordering:

* **429 before 401 means auth effects are never wasted.** The rate
  limiter runs at `preHandler`, keying on the API-key header or client
  IP, before auth populates the request context — so a throttled client
  never consumes auth/tenant work it would have failed anyway.
* **The denylist rejects cheaper than rate-limit.** A platform-denied IP
  is rejected in guard 3, before any Redis-side bucket decrement — which
  is why a denylisted source can hammer the API forever without ever
  spending quota.

## Per-guard triggers, status, and response envelope

Every terminal guard returns the error envelope the error-handler
registers last — an `error` object plus a `meta` block carrying
`request_id` and `docs_url`. You can correlate a guard's `request_id`
against your own logs even when the guard fired before any handler ran.

| Guard                    | Trigger                                                                   | Status                  | Envelope notes                                                                                                                                                      |
| ------------------------ | ------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Retired host guard       | Host header names the retired pre-cutover API host                        | **410 Gone**            | Points at the canonical host; intentionally loud so mis-pointed integrations migrate                                                                                |
| Platform IP denylist     | Client IP on the super-admin-curated denylist                             | **403**                 | One 403 class; guard **fails open** on a config-read error so a transient DB blip never blocks the platform                                                         |
| Under-pressure load-shed | Event-loop lag trips the shed threshold                                   | **503** + `Retry-After` | Retryable by design; `/health`, liveness/readiness, and CORS preflight are exempt so the LB never loses backends                                                    |
| ETag                     | `If-None-Match` matches the computed content ETag on a cacheable GET/HEAD | **304 Not Modified**    | Empty body; still carries version and rate-limit headers                                                                                                            |
| Rate limit               | Endpoint/global bucket spent                                              | **429** + `Retry-After` | `X-RateLimit-Bucket` names the bucket (`global` or the route's named bucket); `X-RateLimit-Limit/Remaining/Reset` mirrored to the IETF `RateLimit-*` no-prefix form |
| Idempotency              | `Idempotency-Key` header replayed on a POST route                         | replayed response       | Returns the first execution's stored response instead of re-running the route                                                                                       |
| Policy-scan hook         | Request body matches a flagged pattern                                    | flagged status          | Scans the body pre-auth                                                                                                                                             |
| Authentication           | Missing/invalid Clerk session or API key                                  | **401**                 | API-key path accepts `dv_`-prefixed keys                                                                                                                            |
| IP allowlist (per org)   | Client IP outside the org's configured allowlist                          | **403**                 | Tenant-controlled list; distinct from the platform denylist                                                                                                         |

<Note>
  The same `403` status fires from both the platform denylist and the per-org
  allowlist. If you get a 403 with no allowlist configured on the org, treat
  it as the platform denylist and contact support; if the org has an
  allowlist, fix the list first.
</Note>

## Where to read the order in code

The chain is registered in one block — the biggest part of the sequence
sits in `apps/api/src/app.ts` between the first `clientIpSocketPlugin`
registration and the final `errorHandler` registration, with the
request-annotation plugins (request id, trace context, Sentry, metrics,
locale, logger, metrics, idempotency, policy scan, error handler) in one
contiguous block near the end of `buildApp`. The plugin implementations
live one-per-file in `apps/api/src/plugins/`, named after the guard they
run (`retired-host-guard.ts`, `platform-ip-denylist-guard.ts`,
`under-pressure.ts`, `etag.ts`, `request-id.ts`, `trace-context.ts`,
`locale.ts`, `request-logger.ts`, `request-metrics.ts`, `idempotency.ts`,
`policy-scan-hook.ts`, `error-handler.ts`).

Reading that block top-to-bottom is the definitive order; this page is the
map, the file is the territory.

## Who owns each guard

All the envelope and ordering above is **platform-owned** plumbing —
not tenant compliance controls. The tenant-owned controls sit behind
auth (the org-level IP allowlist, per-endpoint `config.rateLimit`
buckets on routes that opt out of the global limiter, idempotency-key
semantics you choose). See
[Tenant isolation](/concepts/tenant-isolation) for the tenancy split and
[Compliance](/compliance/send-gates) for the tenant-ownership model.

## See also

* [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy) — every limiter family and its error code
* [Tenant isolation](/concepts/tenant-isolation) — the tenant-ownership split
* [Idempotency and safe retries](/concepts/idempotency-and-safe-retries) — the replay contract the idempotency guard enforces
* [Rate limits](/guides/rate-limits) — per-endpoint tables and retry patterns
* [Error codes reference](/reference/error-codes) — the full error catalog and envelope shapes
* [Request logs model](/concepts/request-logs-model) — how the logger guard feeds the Developer API analytics
