Vaults overview
KnoxCall’s Vaults feature is a data tokenization service. You exchange a sensitive value (an SSN, a bank account, an email, an arbitrary blob) for a token — a shorter, opaque string. Your databases and analytics pipelines store the token. Whenever you need the original value, you call KnoxCall’s detokenize endpoint. The Vault parallel: this is what HashiCorp calls a Tokenization Engine. Use it for:- Keeping sensitive values out of your own datastores — your DB, logs and analytics extracts hold tokens; KnoxCall holds the values behind a per-vault key.
- PII lifecycle controls — set TTLs so emails / SSNs auto-expire; delete one token for cryptographic erasure of that value alone, or destroy a vault to erase everything in it.
- Format preservation — tokens that pass the same format checks as the originals (NNN-NN-NNNN for SSNs, a fixed digit shape for card-shaped columns), so existing systems keep working without schema changes.
- Centralised audit — every detokenize writes an audit row naming the vault, the token, the key version and the user or key that read it. Every release path writes the same row: a plain read on the API or the dashboard, detokenization inside a
/v1/proxyrequest (a template referencing three tokens writes three rows, not one row carrying a count), and a Routedetokenizefield action, which until 2026-09-15 released values with no row at all. The row is anchored on the vault (resource_id), carries the token indetails.token_id, and names the destination host when the value was sent somewhere — so “where did this vault’s data go, and when?” is one query. A detokenize that is refused is recorded too, asvault.detokenize_refusedwith the reason, because a credential repeatedly being turned away from your card vault is the thing you most want to see.
Why use it
How it works
- Create a vault with a chosen token format. The format is locked at creation — changing it later would invalidate every existing token.
- Tokenize values:
POST /v1/vaults/{name}/tokenswith{ value, metadata?, ttl_seconds? }. KnoxCall returns a token string + an internal id. Store the token in your DB. - Detokenize when you need the original:
GET /v1/vaults/{name}/tokens/{token}. Audited every time. - Rotate the vault’s encryption key when you want to bump versions. Existing tokens still decrypt under their original version; new tokens encrypt under the new version. No re-encryption of stored data needed.
- Destroy a vault to cryptographically shred every token in it. Permanently unreadable.
Quick start (UI)
- Go to Protect → Vaults in the admin UI and click New Vault.
- Pick a token format:
generic— opaque token, safe for any value type. Default.pan— credit card numbers (16 digits, Luhn-valid, BIN + last4 preserved).ssn— US SSNs (9XX-XX-XXXX, last 4 preserved).email— local-part tokenised, domain preserved.
- Optional: set a default TTL so tokens auto-expire.
- Save. Use the Playground tab to tokenize / detokenize a few values.
Quick start (API)
All responses use the standard{ data: ..., meta: { request_id: "..." } } envelope. The examples below show the data fields.
Token formats
The
pan and ssn generators solve the math so the resulting token validates: PAN tokens pass the Luhn check, SSN tokens fit the regex your form already enforces. You drop the token into the same column the original would have gone in.
pan vaults are destination-bound, and cannot be read in the clear
A pan vault is the one format with two extra rules, both enforced in the vault
service so no caller can route around them:
-
It requires a non-empty
allowed_destinationslist at creation, naming the hosts a card number may be sent to (e.g.["api.stripe.com"]). The list cannot later be cleared. Creating one without it is refused. -
It refuses the plain read.
GET /v1/vaults/{vault}/tokens/{token}, its/admintwin and the browser capability consume all ask for a value with no destination attached — and apanvault answers all three with a refusal. The number leaves only through detokenize-and-send, where KnoxCall knows the host it is about to reach:POST /v1/proxyresolving a{{ token: … }}towards the URL inX-Knox-Proxy-URL, or- a Route with a request-direction
detokenizefield action, which names that route’s own resolved upstream host.
detokenizeaction (either direction) to a new upstream is an Owner or Admin act with a fresh second factor, or, on/v1, needsvaults:detokenize— see Moving a route that decrypts or detokenizes. A Route’s response-directiondetokenizeis still a plain read and is still refused: the value there would be handed back to the caller, not sent to a processor, which is the one thing apanvault never does.
Card numbers only go in pan vaults
The rules above are what makes a pan vault different, and they are worth
nothing if a card number can simply be tokenized somewhere else. So it cannot
be: a value that looks like a payment card number is refused by every vault
whose token_format is not pan, with 400 wrong_format and a message
naming the format to use. The refusal covers value and every string inside
metadata — a number in metadata.card_number is stored just as surely as one
in value.
It applies wherever a value is tokenized: POST /v1/vaults/{vault}/tokens and
its bulk twin, the /admin pair, a Route’s tokenize field action, and the
browser capability consume behind a hosted field. In a bulk call the whole
batch is refused — nothing is stored for any value in it — and the message names
the index of the offending element.
A value is treated as a card number when it is 13-19 digits (spaces and hyphens
are ignored), passes the Luhn check, and begins in a range cards are
actually issued in. All three are required: a Luhn-valid sixteen-digit order
reference such as 7000000000000005 is not in an issued range and tokenizes
normally, and 4111111111111112 is in one but fails Luhn, so it tokenizes too.
We refuse rather than accept because a card number in a generic vault would
sit in KnoxCall’s custody carrying none of the protections above — no
destination binding, no plain-read refusal, no format-preserving token — and
outside whatever boundary anyone later draws around card data. There is no flag
to turn this off: it is what keeps a tenant who never uses the card format out
of that boundary entirely.
A processor that echoes the card number back
When a Route’s request-directiondetokenize opens a pan vault, the real
card number goes to the upstream on purpose — that is the whole point of
detokenize-and-send. Many processors then echo it straight back in their
confirmation object, in a Location redirect or in a plain-text receipt.
KnoxCall scans the reply for that exact number and puts the token back before
the response reaches you — and before it reaches the four other places that copy
a response: your outbound webhooks, the workflow event that Route
responses trigger, the S3 response archive, and the response_body field of
the request log. None of the five sees the card number. (The two that are also
log surfaces — response_body and the webhook payload — mask any card-shaped
value to its last four on top of this, so what you read there is
••••••••••••1111 rather than the token itself.)
Scope is deliberately narrow and exact:
- Only the values this request opened. A different card number the processor mentions is your own data and is left alone; this is not a general card scan.
- Only
panvaults — ageneric,ssnoremailvalue echoed back is untouched. - However the processor spelled it — spaces, dashes and the form-encoded
+/%20forms all match, in JSON string and number fields at any depth, in object keys, in form and text bodies, and in header values.
- A request that detokenizes a
pantoken is always answered buffered, even if the caller asks fortext/event-stream: a card number can straddle any chunk boundary the upstream picks, so a streamed reply cannot be re-tokenized honestly. - If the reply cannot be read at all — an encoding KnoxCall does not decode, a
binary body, or a body past the response-log ceiling — the response is withheld
with
502 relay_response_not_retokenizablerather than delivered unscanned. Do not blindly retry — the upstream call already happened.
vault.retokenize audit entry naming the
vault and the occurrence count — never the number and never the token. A
non-zero count is worth acting on: it means your processor is echoing card data.
KnoxCall alerts you when card data goes somewhere new
Every successful detokenize is counted against where the value went — the destination pattern it matched on the vault’sallowed_destinations list, or the
exact hostname when no pattern covers it. The counter holds the tenant, the
vault, the data space, that destination bucket, a five-minute window and a count,
and nothing else: never the token, never the value, never the last four digits.
Two things then raise an alert. The alert itself — Vault Destination
Monitoring — is installed for you the moment your workspace has its first
vault, and it uses whichever notification channels your other alerts use. You do
not need a Route, and you do not need to configure anything:
- A destination this vault has never used. A detokenize to a bucket with no prior window in the last 30 days. A card number arriving at a processor you have never sent to is the first signal of a stolen credential, and it fires on the very first one — you do not have to wait for a pattern to form.
- A spike over that destination’s own normal. The current window’s count measured against the baseline KnoxCall learned for that vault-and-destination pair, using the same statistics the rest of the anomaly detection uses. A quieter-than-usual window is not an incident and never fires.
KnoxCall tells you before a stored card expires
A card on file stops working the day it expires, and nothing on KnoxCall’s side errors when it does — we hold a token, not an issuing relationship with the card’s bank. The first anyone hears about it is a decline at your payment processor, usually on a renewal, with no mention of an expiry date anywhere in the message. So a card token can carry the card’s own expiry, and KnoxCall raises an event 60 days and again 30 days before it, once each:card_exp_month (1–12) and card_exp_year (four digits — 2029, never
29) are optional, go together, and are accepted only by a pan vault. Half an
expiry, a two-digit year, or either field sent to a generic / ssn / email
vault is refused with 400 validation_error rather than quietly dropped —
a quiet drop means no notice ever fires and you never find out.
card_expires_on comes back as the last day of the expiry month, because a
card is valid through the end of its month. You will also see it on every row of
GET /v1/vaults/{vault}/tokens.
Subscribe a webhook to vault.token.expiring to receive the notice:
token is the token you already store — for a pan vault it preserves the real
BIN and last four, which is what makes “your card ending 4116 expires next
month” possible without anyone holding the number. The payload never carries
the card number, and that is enforced rather than promised: a delivery whose
contents look card-shaped is refused server-side instead of sent.
Every notice is also written to your audit log as vault.token.expiring against
the token’s id, so a workspace already piping audit.event into a SIEM receives
it without subscribing to anything new. A Test-mode card only ever reaches a
Test-mode webhook.
The notice is decided by a job that runs once a day, and it uses a window
rather than an exact day: if a run is missed, the notice catches up on the next
one instead of being skipped. A “60 days” event can therefore arrive a few days
late. It never arrives twice.
Tokens created before 17 September 2026 carry
card_expires_on: null, and
there is no backfill. The expiry was never captured, so it is not in the
stored ciphertext and KnoxCall cannot recover it — those tokens get no notice
until the card is tokenized again. If you hold cards on file and want the
notices, re-tokenize on the next successful charge or the next time the customer
updates their details. The same applies to any token created without the two
fields, and to the tokenize field action on a Route, which sees one field
value and has no expiry to carry.Test cards
Test mode has card numbers that fail on purpose, so you can exercise your decline path, your 3-D Secure path and your expiry path before the first real card. They are ordinary card numbers in apan vault — you tokenize them the
way you tokenize anything — and KnoxCall recognises them only in the Test data
space.
The two refusals answer the ordinary
/v1 error envelope on POST /v1/proxy
(and on /wg and your wrap subdomain), and the ordinary Route envelope on a
configured Route:
X-Knox-Error: sandbox_test_card_declined and no
X-Knox-Destination-Status, which is how a wrapped provider SDK tells a
KnoxCall answer from its provider’s — so stripe-node sees a KnoxCall error,
never a Stripe decline.
402 is also plan_limit. Branch on error.type (or X-Knox-Error), never
on the status alone.
Four things worth knowing:
- The Live data space treats all four as ordinary card numbers. There is no Live behaviour here to switch off — the check returns before it reads anything when the data space is Live.
- Only a value a vault released is classified. Pasting
4000 0000 0000 0002into a request body as clear text is refused by the raw-card-number egress control (403 raw_pan_egress_refused), exactly as any other card number in a clear body is. Test cards are not a way around that. - The card still leaves the vault. A simulated decline writes the same single
vault.detokenizeaudit row, counts against the same detokenize meter, and appears in the vault’s Destinations view attributed to the destination you named — because the number really was decrypted. Only the forward is withheld. 4000 0000 0000 0069is not simulated at all. Itscard_expires_onis a real date in a real column, so the tokenize response, the token list and the expiry job all read it the way they read a real card’s. Supply your owncard_exp_month/card_exp_yearand yours wins — KnoxCall never rewrites an expiry you spelled out.
An already-expired card gets no
vault.token.expiring notice, and that is
not specific to the test card. The 60- and 30-day windows look forward, so a
card whose expiry has passed is in neither. The test card proves the expiry is
captured, stored and readable; it cannot be used to make a notice fire on
demand.BIN intelligence: two fields that ship empty
A card token also carries two fields describing the issuing bank, derived from the card’s first six digits:
They appear on the tokenize response, the bulk tokenize response and every row of
GET /v1/vaults/{vault}/tokens, beside card_expires_on. There is nothing to
send: both are derived at tokenize time, never supplied.
Treat both as optional indefinitely. Even with a table in place they are
null for every non-pan vault and for any card whose issuer the table does not
carry. Code that requires either field to be present will break on cards that
are perfectly valid.
brand and the last four digits are a different thing and have always
worked. Those come from the published issuer ranges, which need no licence —
and for a pan vault the token string itself preserves the real BIN and last
four, which is how “your card ending 4116” works without anyone holding the
number.
Only the first six digits are ever used for this lookup, and that is a
boundary in the code rather than a habit: the truncation and the lookup live in
one module, and the lookup refuses anything that is not exactly six digits.
Why a Route field action refused your request
A Route can carry field actions —tokenize, detokenize, encrypt,
decrypt applied to selected JSON fields on the way out or on the way back.
When one of them cannot run, KnoxCall refuses the request: the upstream is
not called with the untransformed value, and the upstream’s untransformed reply
is not delivered to you. That part never varies — the whole point of the action
is that the raw value does not travel.
What the answer tells you is why, and whether retrying can possibly help:
Three rules hold across the whole table:
- A
4xxis final. Replaying the identical request is refused identically.503is the only code that means “try again”. - The response direction is always
502, whatever the reason, because the upstream request was performed. A blind retry of aPOSTthat already charged a card is its own incident. - Every refusal is in your audit log, as
route.action.refusedwith the reason and therequest_idfrom the response body. Genuine KnoxCall faults areroute.action.failedinstead, so the log answers “was this us or them?” without guesswork.
Encryption + rotation
Each vault has its own encryption key (managed via Crypto Keys). Tokens carry the key version they were encrypted under. When you rotate:- A new key version becomes active.
- New tokens encrypt under the new version.
- Existing tokens still decrypt under their original version (no migration needed).
Bulk and TTL
- Bulk tokenize: up to 1000 values per call via
POST /v1/vaults/{name}/tokens/bulk. The batch runs in one transaction — all of it is stored or none of it is, so a failure partway through leaves nothing behind to reconcile. - TTL: per-vault default + per-call override. Expired tokens 404 on detokenize and get pruned by an hourly cron. This is the token’s lifetime and has nothing to do with
card_expires_on, the card’s expiry date — see “KnoxCall tells you before a stored card expires” above. - Metadata: attach a small JSON object alongside each token (read back on detokenize). Encrypted at rest.
See where your data went
Every vault’s detail page has a Destinations tab — “Where this data went”. It answers, for the last 7, 30 or 90 days:- Which hosts a value was released to, one row per host.
- How many releases went to each, and how many per day, so a sudden spike to a host you do not recognise is visible rather than buried.
- First seen and last seen per host.
- A link straight into the audit trail for that vault, and a Download as CSV of the table.
vault.detokenize audit rows themselves — there is no second ledger that
could disagree with the log an auditor reads. Nothing on that screen contains a token, a value or
the last four digits of anything; the rows name hosts and counts.
For a pan vault the list is bounded by allowed_destinations: a host that is not on the list
could not have received a card number, which is why the page says so above the table.
One egress path is not in this table yet: a Route with a request-direction
detokenize
field action releases a value without writing a per-token audit row, so it does not appear.
The page states this beneath the table rather than letting an empty table read as “nothing
left”. POST /v1/proxy and the plain reads through /v1 and the dashboard are all counted.Plan limits
Who can list tokens and detokenize
Listing a vault’s tokens (their strings keep a card’s BIN and last four) and detokenizing a value are both reveals. In the dashboard this is a Class B reveal: only an Owner or an Admin can do it (an Editor cannot, even with Reveal secrets turned on; that setting covers secret values, certificates and credentialed workflow tests only), and every use asks for a fresh second factor (an authenticator code, a passkey or a security key; an emailed code does not count). Each use writes aplaintext.reveal entry to the audit log before anything is shown, and counts against the same per-person reveal budget as showing a secret’s value (60 per 5 minutes). If the audit entry cannot be written, or the budget cannot be counted, the request is refused rather than served. On the Management API (/v1) a credential still needs the matching permission. A token issued to a person never carries vault:detokenize; listing a vault’s tokens on /v1 needs vault:read and does not take this check.
The dashboard no longer lists a vault’s tokens when you open it: click Show tokens on the
Tokens tab. A token you delete is removed from the list on screen without listing again.
Next steps
- Crypto Keys overview → — the encryption layer Vaults builds on
- API reference →