Skip to main content

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.
The same feature exists in every SDK — Node, Python, Go, PHP, Ruby — and in the Go agent’s intercept mode 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, update and environment 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:
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 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

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