Ephemeral Proxy
The Ephemeral Proxy is a one-shot HTTPS proxy. Unlike Routes, there is no pre-configured row — every call is its own invocation. You pass the upstream URL in a header, KnoxCall validates it, swaps any{{ token: ... }} references for the underlying vault values, and forwards the request.
The composition with Vaults is the killer feature: store the token in your database, send the token to KnoxCall, KnoxCall sends the raw value to the upstream. On this path the raw SSN, bank detail or API secret is never returned to your systems.
Scope of that guarantee: the proxy never hands the value back to you, but the
same credential that can detokenize on the proxy path can also read the value
outright via
GET /v1/vaults/:vault/tokens/:token. The control is about where
values flow, not a ceiling on what a detokenize-capable credential can reach.
Binding a vault to allowed destinations, and refusing the plain read for
card-shaped vaults, is planned work.When to use it
Use Ephemeral Proxy when:- The destination URL is dynamic — chosen per-request, not per-route.
- You want to detokenize vault tokens last-mile (PII handling, secret injection).
- You need a quick passthrough without configuring a Route, ApiKey scope, environment overrides, etc.
- You call the same upstream repeatedly and want signing, rate limits, env overrides, and full per-request observability.
- You need IP allowlisting, mTLS, or method-specific configuration.
- You want logs grouped by route in the dashboard.
How a request flows
Quick example
tok_01HZX... came from a prior POST /v1/vaults/.../tokenize call — see the Vaults overview.
Template syntax
Two reference forms are supported in the JSON body and query string:
Templates are scanned in body and query only. Headers pass through unchanged in v1 — header-injection is a separate hardening pass.
If a token isn’t found, KnoxCall returns
400 token_not_found rather than silently sending an empty value to the upstream. Anything else brace-shaped that isn’t one of the forms above — {{ secret: x }}, {{ token: }}, a mis-cased {{ TOKEN: ... }} — is likewise refused with 400 template_error rather than forwarded verbatim, so an unresolved template can never reach the upstream in place of a credential.
Required and optional headers
Reserved headers (
Authorization, Cookie, Host, Content-Length, anything starting with X-Knox-) are stripped before the request reaches the upstream, and so is X-Api-Key when it holds a KnoxCall credential (a value beginning tk_, kc_, kr_ or AKE) — that is the key you authenticated to KnoxCall with, and forwarding it would put a key granting your whole management API into your upstream’s access logs. An X-Api-Key carrying your upstream’s own key (sk-ant-…, and so on) is forwarded unchanged, so providers that authenticate with that header keep working. The headers KnoxCall reads itself and the headers a proxy in front of KnoxCall adds are not forwarded either: X-KnoxCall-Key, X-KnoxCall-Route, X-KnoxCall-Environment, X-Forwarded-For, X-Real-IP, True-Client-IP, CF-Connecting-IP and the rest of the exact names in Request headers KnoxCall does not forward. Those are matched by exact name, not by prefix. Every other header you send is forwarded unchanged, including a CF-Access-Client-Id / CF-Access-Client-Secret pair for an upstream behind Cloudflare Access.
What you get back
KnoxCall passes the upstream response through verbatim — same status code, same headers (minus hop-by-hop), same body bytes. The one exception is a request that detokenized a card number; see Responses are re-tokenized below. KnoxCall adds two headers so you can correlate:
If the upstream is unreachable, KnoxCall returns
502 upstream_error.
Responses are re-tokenized
If your request detokenized a token from apan vault, KnoxCall scans the
upstream’s reply for that exact card number and puts the token back before the
response reaches you. Many processors echo the card number in their confirmation
object, in a Location redirect or in a plain-text receipt — without this, the
number they echoed would land in your backend, your logs and your database, which
is precisely what tokenization exists to prevent.
What is replaced, and what is not:
- Only the values this request opened, matched exactly. A different card number the processor happens to mention is your own data and is left alone; this is not a general card scan of your responses.
- Only
panvaults. A value from ageneric,ssnoremailvault echoed back is untouched. - However the processor spelled it.
4111111111111111,4111 1111 1111 1111,4111-1111-1111-1111and the form-encoded4111+1111+1111+1111/4111%201111%201111%201111all match. - Body and headers. JSON fields (at any depth, including object keys and
values sent as bare JSON numbers), form-encoded values, text bodies, and header
values such as
Location. A header value is replaced, never stripped, so a 3-D Secure redirect keeps working.
- A rewritten body is returned uncompressed. KnoxCall sends
Accept-Encoding: identityupstream on any request that detokenizes apantoken, because a compressed reply cannot be scanned. If the upstream ignores that and compresses anyway, the response is withheld with502 response_not_retokenizablerather than forwarded unscanned. The same answer is given for a binary body. - Streaming is refused for these requests. A card number can straddle any
chunk boundary the upstream picks, so a streamed reply cannot be re-tokenized
honestly. A request that both detokenizes a
pantoken and asks for streaming (Accept: text/event-stream, or"stream": truein the body) is answered409 pan_streaming_unsupported. Send the card request without streaming.
vault.retokenize audit entry
naming the vault and how many occurrences were replaced — never the number and
never the token. A non-zero count is worth acting on: it tells you your processor
is echoing card data back.
When a referenced token cannot be resolved
A{{ token: … }} that KnoxCall cannot exchange for its value stops the
request before the upstream is called — nothing is sent with the hole
unfilled. The status says whether retrying can help:
Every
4xx above is final: the identical request is refused identically. The
Routes surface answers the same conditions with its own relay_* codes — see
why a Route field action refused your request.
Raw card numbers are refused on egress
-
Tokenize the card and send the token — the intended path. Put the number
in a vault whose
token_formatispanand send{{ token: kcv_… }}. KnoxCall detokenizes it only toward that vault’s ownallowed_destinations, so the number reaches your processor and nothing else. A value detokenized this way is never refused by this check. -
Allow the destination — if a host genuinely must receive the number
itself, an owner or admin replaces the workspace’s raw-card egress allowlist:
Entries are bare hostnames or
*.suffixwildcards — no scheme, port, path or IP literal. Adding a host requires a fresh second factor; removing one does not, because shutting the door is the lever you want reachable during an incident. On a configured Route there is a narrower option — the route’s Security tab grants raw-card egress for that route in that environment alone, without opening the destination to/v1/proxy, workflows or webhooks. See Routes. Every change is audited asegress.pan_allowed_hosts_updatedwith the previous list, so it can be reverted from the trail alone. The list is empty by default, which is why the refusal is what you see first. (There is no dashboard screen for this yet — the API is the surface.) The matchingGETreturns the list plusrefused_this_month, so you can see how often the refusal has fired without trawling the audit log.
egress.raw_pan_refused audit entry
naming the destination host and a masked sample (last four digits only); the
number itself is never logged, never stored and never echoed back to you.
Limits and gates
Audit and observability
Every invocation writes:- An
api_requestsrow withproxy_mode = 'ephemeral', status code, latency, upstream host, token count, and source IP. The body, headers, and token values are not stored. - An
audit_logentry taggedephemeral_proxy.invoke, carryingretokenized_occurrences— how many echoed card numbers were replaced with their token on the way back (0on every request that opened none). - One
vault.retokenizeentry per vault whose card number the upstream echoed, with the occurrence count. The vault’s id is recorded; the number and the token never are. - A Fleet Graph signal carrying
(method, upstream_host, status_code, latency_ms)— never the body, never the token strings, never the customer’s path.
GET /v1/audit-logs?action=ephemeral_proxy.invoke (Audit Logs).
Comparison: Routes vs Ephemeral Proxy
Next steps
- See the Ephemeral Proxy API reference for the complete request/response schema.
- See Vaults to create the tokens you’ll reference.
- See Routes when you need a persistent, observable proxy instead.