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

# Route-aware interception

> Send an untouched third-party SDK's traffic through the Route that covers it — no base-URL swap, no per-SDK wiring. Preview.

# Route-aware interception

**Preview.** Turn a Route's **Intercept** toggle on, install the KnoxCall SDK's interceptor once, and every call your code — or any third-party SDK inside it — makes to that Route's upstream host goes through the Route. The Route injects the stored credential; the provider key never enters your process. Turn the toggle off and the traffic goes back to where it went before, on the next refresh, with no code change.

```ts theme={"dark"}
const knox = new KnoxCall();                                     // credentials from KNOXCALL_* env
const stop = knox.wrap.intercept();                              // route discovery is on by default
await stop.ready;                                                // first manifest loaded

const hubspot = new Client({ accessToken: "placeholder" });      // an untouched SDK
await hubspot.crm.contacts.basicApi.getPage();                   // via the Route that covers api.hubapi.com
```

The same feature exists in every SDK — [Node](/sdks/node#route-aware-interception-preview), [Python](/sdks/python#route-aware-interception-preview), [Go](/sdks/go#route-aware-interception-preview), [PHP](/sdks/php#route-aware-interception-preview), [Ruby](/sdks/ruby#route-aware-interception-preview) — and in the [Go agent's intercept mode](/infrastructure/go-agent) for code you cannot change at all. All of them read one server answer, so a Route you toggle on in the dashboard takes effect for all of them.

## How it works

1. **You opt a Route in.** `intercept_enabled` is a per-environment setting on the Route (the **Intercept** toggle on each environment card of the Route page, or `intercept_enabled` on the [create](/api-reference/routes/create), [update](/api-reference/routes/update) and [environment](/api-reference/routes/environments) endpoints). It is off by default. Only one intercept-enabled Route may cover a given upstream host and base path in an environment — turning on a second is refused with `409 intercept_route_conflict`, because two Routes claiming the same traffic would be resolved by a tie-break nobody chose.
2. **The SDK polls the manifest.** `GET /v1/wrap/intercept-manifest` lists, for one environment, every intercept-enabled Route's upstream `host`, `base_path` and `slug`. The SDK refreshes it every 60 seconds (jittered), and immediately when KnoxCall refuses a route-mode request — a `401`, or the `404 route_not_found` an authenticated credential gets for a Route that no longer exists — so a Route created, enabled or disabled later is picked up without a restart. A `404` of type `environment_not_configured` or `environment_disabled` is surfaced as-is: a refresh cannot fix an environment. Your provider's own 404s are never mistaken for either — they carry `X-Knox-Upstream-Status`.
3. **Each outbound request is decided in a fixed order.** The first matching rule wins:

| # | Condition | Outcome |
| - | - | - |
| 0 | `KNOXCALL_INTERCEPT=off` in the environment | direct — the kill switch, per process, no deploy |
| 1 | Not `http(s)`, or the URL does not parse | direct |
| 2 | The SDK's own KnoxCall hosts, or any host under `knoxcall.com` | direct (anti-recursion) |
| 3 | A route-around rule matches (the built-in raw-card defaults, plus yours) | direct |
| 4 | You asked for `requireContext` and this call is outside `routed()` | direct |
| 5 | A manifest entry covers the host **and** the path (longest `base_path` wins) | **route mode** — through that Route |
| 6 | The host is in the `hosts` list you gave `intercept()` | **ephemeral mode** — through the ephemeral proxy |
| 7 | otherwise | direct — untouched |

Rule 5 before rule 6 is the whole feature: a host you listed is served by the ephemeral proxy until a Route covers it, upgrades to the Route the moment one does, and downgrades back when the Route is disabled.

## Route mode

In route mode the SDK sends the request to your tenant's data plane with `x-knoxcall-route: <slug>`, the path rebased under the Route's `base_path` (a Route targeting `https://api.hubapi.com/crm/v3` receives `/objects/contacts` for a request to `/crm/v3/objects/contacts`), and the query string intact. The request's own `Authorization` header is dropped before it leaves — the Route injects the stored secret, so a placeholder token in the third-party SDK is exactly that. Everything a Route does applies: injected headers, per-environment overrides, rate limits, client requirements, logs and analytics. The API Log marks each call the SDK rerouted as **SDK intercept** (a direct `call()` shows as **Direct**), so you can see exactly which traffic the interceptor is carrying.

A request under a covered host but **not** under any covered base path is not rerouted; it falls to rule 6 (ephemeral if the host is listed, direct otherwise) and fires `onUnmatchedPath` once per host and path prefix so you can see it.

## Ephemeral mode

A host you list in `hosts` that no Route covers goes through the [ephemeral proxy](/essentials/ephemeral-proxy/what-is-the-ephemeral-proxy) in transit mode: the request's own credential is carried on a KnoxCall header and the SDK's KnoxCall token authenticates the hop. Per-host options put a host in escrow mode instead (`credential: { secret: "<escrowed name>" }`), or let transit go direct when KnoxCall is unreachable (`unavailable: "direct"`).

## When KnoxCall is unreachable

Route mode and escrow fail closed: there is no credential in your process to go direct with, so the SDK surfaces the connection error. Transit is the one mode that can opt out, per host, with `unavailable: "direct"`; it fires `onFallback` when it does.

## What it is, and is not

* **It is a convenience, not a security boundary.** Each SDK patches the process-wide HTTP seam its language has (global `fetch` and `node:http` in Node; `httpx` and `urllib3` in Python; `http.DefaultTransport` in Go; `Net::HTTP` in Ruby). PHP has no process-wide seam, so the SDK offers a route-aware PSR-18 client and a Guzzle middleware instead. Traffic that does not pass through that seam is not reached — each SDK page states exactly what is and is not covered.
* **The custody path is route mode.** Ephemeral transit carries your own credential through KnoxCall; escrow and route mode keep it out of your process entirely.
* **The manifest is a read.** It needs `route:read` on the SDK's credential, the same permission as listing routes; it is not permission to escrow, mint or spend anything.
* **One kill switch, no deploy.** `KNOXCALL_INTERCEPT=off` makes every interceptor and route-aware transport pass everything through, per process.

## Which stacks are reached

| Language | Installed by | Reached | Not reached |
| - | - | - | - |
| Node | `knox.wrap.intercept()` | global `fetch` (undici), `node:http` / `https` — axios, node-fetch, got, cross-fetch, OpenAPI-generated clients, `@hubspot/api-client` | a `fetch` or `request` reference captured before install; a custom undici `Dispatcher` |
| Python | `knox.wrap.intercept(stacks=["httpx", "urllib3"])`; on the async client also `stacks=["aiohttp"]` (opt-in) | `httpx` clients on the default transport; `requests`, `botocore` and OpenAPI-generated clients (urllib3); with the opt-in, any `aiohttp.ClientSession` — aiobotocore, slack\_sdk's async client, azure-core's aiohttp transport | an injected custom transport; raw `http.client`; `pycurl`; a WebSocket handshake (`ws_connect` goes direct) |
| Go | `client.Wrap.Intercept(ctx)` | `http.DefaultClient` and any `*http.Client` with a nil `Transport` | a client built with its own `Transport` — hand it `client.Wrap.RoundTripper(knoxcall.WithRoutes())` |
| PHP | `$knox->wrap->httpClient(['routes' => 'auto'])`, `$knox->wrap->guzzleMiddleware()` | any SDK that accepts a PSR-18 client or a Guzzle handler stack | an SDK that builds its own curl handle — point it at a gateway URL |
| Ruby | `knox.wrap.intercept!` (opt-in, experimental) | Faraday's default adapter, `rest-client`, `httparty`, raw `Net::HTTP` | Typhoeus, Curb, `http.rb` |

Every SDK also offers the same decisions on an explicit transport you inject yourself (`wrap.fetch({ routes: "auto" })`, `wrap.transport(routes="auto")`, `Wrap.RoundTripper(WithRoutes())`, `wrap->httpClient(['routes' => 'auto'])`, `wrap.faraday_connection(routes: :auto)`), for code that would rather not patch process globals.

## Uncovered egress

The interceptor already sees every outbound request the process makes, so it can also tell you which ones it **did not** intercept: calls that carried a credential to a host no Route covers, or to a host whose Route has interception switched off. Those are reported to KnoxCall and shown on **Proxy → Uncovered egress** in the dashboard, with one action per row — create a Route, or turn interception on for the one that already matches.

**What is sent.** Per host, the first path segment (`/` or `/<one segment>`), the HTTP method, the *name* of the header the credential travelled in (`authorization`, `x-api-key`, …) and how many calls, with first- and last-seen timestamps. Nothing else: no credential values, no full paths, no query strings, no request or response bodies, no client addresses. The server refuses an entry that carries anything beyond that shape, and the header name must be on a fixed allowlist of credential headers — so a value cannot be reported even by mistake. Some APIs put a key in the path itself (a Telegram bot token is `/bot<id>:<token>/…`), so a first path segment that looks like a credential — a known key prefix, a token, anything unusually long — is stored as `/` instead: the host is still listed, the value is not kept.

**On by default.** Every `intercept()` install reports unless told not to. Switch it off per install with `observe_uncovered: false`, or per process with `KNOXCALL_OBSERVE_UNCOVERED=off`; either stops the reporter entirely. `KNOXCALL_INTERCEPT=off` stops it too, since there is no interceptor to observe with.

**Retention.** Observations are kept per host and day for **30 days**, then deleted. A workspace may add at most 5,000 distinct (host, path segment, method, header name) combinations per day; beyond that the SDK's report is accepted and the excess is dropped. The endpoint is `POST /v1/wrap/egress-observations` and needs `route:read`, the same permission as the manifest.

## Hooks

`onReroute` (before a KnoxCall send: host, URL, mode, slug), `onRefresh` (after a manifest change: added and removed hosts), `onManifestError`, `onUnmatchedPath`, `onRefused` (a route-mode refusal that triggered a refresh, and what the re-decision was), `onFallback` (a transit request went direct). Names follow each language's casing.

## See also

* [What are Routes?](/essentials/routes/what-are-routes) and [Creating routes](/essentials/routes/creating-routes)
* [Route environments](/api-reference/routes/environments) — `intercept_enabled` per environment
* [Go agent](/infrastructure/go-agent) — the same manifest, for processes whose code you cannot change


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.