Skip to main content

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.
Vaults are not a payment-card compliance control. KnoxCall holds no PCI DSS attestation — no AOC, no ROC, no QSA engagement — so putting card numbers in a vault does not move your cardholder-data scope; it just moves where the numbers are stored. If scope reduction is what you need, use an assessed card vault. Our current status is on the trust page.
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/proxy request (a template referencing three tokens writes three rows, not one row carrying a count), and a Route detokenize field 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 in details.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, as vault.detokenize_refused with 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

  1. Create a vault with a chosen token format. The format is locked at creation — changing it later would invalidate every existing token.
  2. Tokenize values: POST /v1/vaults/{name}/tokens with { value, metadata?, ttl_seconds? }. KnoxCall returns a token string + an internal id. Store the token in your DB.
  3. Detokenize when you need the original: GET /v1/vaults/{name}/tokens/{token}. Audited every time.
  4. 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.
  5. Destroy a vault to cryptographically shred every token in it. Permanently unreadable.

Quick start (UI)

  1. Go to Protect → Vaults in the admin UI and click New Vault.
  2. 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.
  3. Optional: set a default TTL so tokens auto-expire.
  4. 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_destinations list 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 /admin twin and the browser capability consume all ask for a value with no destination attached — and a pan vault 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/proxy resolving a {{ token: … }} towards the URL in X-Knox-Proxy-URL, or
    • a Route with a request-direction detokenize field action, which names that route’s own resolved upstream host.
    A destination not on the list is refused before anything is decrypted. Moving a route that carries a detokenize action (either direction) to a new upstream is an Owner or Admin act with a fresh second factor, or, on /v1, needs vaults:detokenize — see Moving a route that decrypts or detokenizes. A Route’s response-direction detokenize is 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 a pan vault never does.
Absence of a destination is treated as a plain read rather than as “unknown”, so a future caller that forgets to say where the value is going gets the stricter answer, not the weaker one.

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-direction detokenize 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 pan vaults — a generic, ssn or email value echoed back is untouched.
  • However the processor spelled it — spaces, dashes and the form-encoded + / %20 forms all match, in JSON string and number fields at any depth, in object keys, in form and text bodies, and in header values.
Two consequences to plan for:
  • A request that detokenizes a pan token is always answered buffered, even if the caller asks for text/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_retokenizable rather than delivered unscanned. Do not blindly retry — the upstream call already happened.
Each re-tokenized response writes a 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’s allowed_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.
The alerts are about Live traffic only. Test-mode detokenizes are counted in their own separate history — so a test run can never teach the detector that an unfamiliar host is familiar, and a Live destination you have used for months is still a first for Test — but they never raise an alert. Pointing your sandbox at a new endpoint is development, not an incident, and KnoxCall does not page you for it. Each window raises each alert at most once, so a burst of detokenizes to one new destination is one notification rather than one per request.

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.
This is not the token’s TTL. ttl_seconds and expires_at are how long KnoxCall’s token lives; card_exp_* and card_expires_on are when the card stops working. They are unrelated — a token with no TTL at all can belong to a card that dies in fourteen months.
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 a pan 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:
The response also carries 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.
This is a simulation, and it is KnoxCall’s shape — not any processor’s. KnoxCall is a vault and a proxy; it does not authorize, capture, or hold an issuing relationship, so it cannot tell you what your processor would have said. What it can do is refuse to forward and say exactly why. Nothing is sent upstream on a decline or a 3-D Secure outcome: no request reaches your destination at all.
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 0002 into 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.detokenize audit 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 0069 is not simulated at all. Its card_expires_on is 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 own card_exp_month / card_exp_year and 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.
Both are null on every token today, and we would rather say so than let you discover it. Naming a card’s funding type and issuing country from its first six digits requires a commercially licensed BIN table. KnoxCall has not licensed one, and whether we do is an open business decision. Everything around that table is built and shipped — the storage, the lookup, the API shape, the SDK models — so on the day we license one the fields populate with no change to your integration. Until then, expect null.
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 4xx is final. Replaying the identical request is refused identically. 503 is 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 a POST that already charged a card is its own incident.
  • Every refusal is in your audit log, as route.action.refused with the reason and the request_id from the response body. Genuine KnoxCall faults are route.action.failed instead, 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).
Destroying a vault destroys every key version. Tokens become permanently unreadable. This is cryptographic erasure — the data is unrecoverable even if the encrypted ciphertext is leaked from a backup. Deleting a single token is an erasure too, not just a row delete. Each token’s value is sealed under that token’s own data key, and the vault key wraps only the data key. Deleting the token destroys the data key and removes the row in one transaction — so a copy of the row taken from a backup, a replica or a WAL segment before the delete can no longer be opened, even by someone who also holds the vault key. That is what makes a per-customer erasure request answerable without destroying the whole vault.

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.
It is derived from the 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 a plaintext.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