A token is a stand-in for a card number that is useless to anyone who steals it. Your system stores the token; the provider stores the card. If your database is breached, the attacker gets a list of identifiers that only work against your own account with that provider — not card numbers they can spend.
That single property is why tokenisation is the default architecture for anything storing a card, and why the alternative — encrypting card numbers yourself — is a harder problem with a worse outcome.
Tokenization is not encryption
The two get used interchangeably and they are structurally different:
| Encryption | Tokenization | |
|---|---|---|
| Reversible | Yes, with the key | No — the token has no mathematical relationship to the number |
| If your store is breached | The attacker needs the key, which may be breached alongside it | The token alone is worthless |
| PCI scope | An encrypted PAN is still cardholder data, still in scope | The token is generally out of scope |
| Format | Ciphertext | Often format-preserving, so it looks like a card number |
The row that matters is the third. Encrypting card numbers is real work — key management, rotation, access control, an audit trail — and at the end of it you still hold cardholder data and still carry the scope that comes with it. The PCI guide sets out what that entails. Tokenisation sidesteps the whole category by not holding the data.
The format-preserving row is worth a warning too. Because tokens frequently look like card numbers — right length, sometimes a plausible prefix, sometimes even Luhn-valid — code that sees one may treat it as a PAN and log it, mask it, or validate it. That is harmless but confusing, and it is why token values should be labelled as such in your schema rather than sharing a column type with card numbers.
The two kinds of token
This is the distinction most explanations skip, and it has real operational consequences.
Gateway tokens are issued by your payment provider: Stripe’s pm_… payment methods,
Braintree’s payment method tokens, and the equivalents elsewhere. They are provider-specific
and not portable. If you change providers, you migrate — most providers support a
provider-to-provider migration, but it is a project rather than a config change, and it is
worth knowing that before you have a million saved cards.
Network tokens are issued by the card networks themselves: the Visa Token Service, Mastercard’s MDES, and American Express’s equivalent. They sit one level up, and that buys three things:
- The token survives card reissuance. When a customer’s card is lost, replaced or simply expires, the network repoints the token at the new card. On a subscription business this is the difference between a customer churning silently and never noticing anything happened.
- Approval rates are generally higher. Issuers treat network-tokenised transactions as lower risk, because the token carries cryptographic provenance the network vouches for.
- Wallets already use them. Apple Pay and Google Pay are network tokenisation with a device-specific token and a per-transaction cryptogram, which is why a wallet payment exposes nothing reusable even if intercepted.
Most merchants reach network tokens through their gateway rather than directly — the gateway enrols the card and manages the network relationship. Ask your provider whether it is enabled, because it often is not by default and the reissuance benefit alone usually justifies turning it on.
How a token is created
The flow, and the property that makes it work:
- The customer types their card into a field rendered by the provider — a hosted field or an iframe, not an input you own.
- That field sends the card directly to the provider, bypassing your server entirely.
- The provider returns a token to the browser.
- Your code sends the token to your server and stores it.
The card number never touches your infrastructure. Not in a request body, not in a log, not in memory. That is not a policy you enforce; it is a consequence of where the input element lives.
// Stripe Elements: the card details go from the iframe to Stripe, never through you.
const stripe = Stripe(process.env.STRIPE_PUBLISHABLE_KEY);
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const { paymentMethod, error } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
showError(error.message); // never surface a raw decline code to the customer
return;
}
// paymentMethod.id is "pm_..." — this is what your server stores.
// paymentMethod.card gives you brand and last4 for display, without the number.
await fetch('/api/save-payment-method', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
});
});
Two details in that snippet do real work. The card object returned alongside the token
carries the brand and last four digits, which is everything a “saved cards” list needs — you
never need the number to show a customer which card they used. And nothing card-shaped
appears in the request to your own API, which is what keeps that endpoint out of scope. The
Elements documentation and the
payment methods reference cover the rest
of the API surface.
Using tokens
- One-off payments — tokenise, charge, discard. The customer is present and authenticating.
- Card on file — store the token against the customer, charge it later with their agreement.
- Subscriptions — the same, on a schedule. This is where network tokens earn their keep.
- Merchant-initiated transactions — charges the customer is not present for. These must be flagged as MITs, because an unflagged one may be challenged for authentication when nobody is there to answer; the 3-D Secure exemptions interact directly with this.
- Multi-provider setups — tokens do not transfer between providers, so a second provider means a second tokenisation of the same card, not a shared reference.
What to store alongside the token
A token on its own is not enough to run a checkout, and the metadata you keep with it is worth deciding deliberately rather than accumulating:
- Brand and last four digits, so the customer can tell their saved cards apart. Both come back with the token; neither is card data on its own.
- Expiry month and year, for showing a card as expiring soon. Useful, and it goes stale — the provider’s copy is authoritative.
- A provider identifier, if you use more than one. A token is meaningless without knowing which provider it belongs to, and that is exactly the field people forget until the second provider arrives.
- Your own customer reference, not the provider’s, as the primary key. Providers change.
What not to store: anything you would have to protect. If a field would be awkward in a breach notification, the question to ask is why you are holding it rather than how you are encrypting it.
What a token cannot do
- It cannot be used on the card network directly. It is meaningful only in the context of your account with your provider.
- It cannot be reversed to a card number by you. The mapping is held elsewhere by design.
- It cannot be used by another merchant. A token stolen from you is worthless to anyone else, which is the property that makes the whole approach worthwhile.
- It does not zero your PCI scope.
That last point corrects the most common misconception on this subject. Storing tokens instead of card numbers takes your database out of scope. It does not take out the flow that created the token: the checkout page, every script running on it, and your integration code. Hosted fields on your own page typically map to SAQ A-EP rather than the shorter SAQ A, because the page hosting the iframe can still be attacked — swap the frame, overlay a fake field, or read keystrokes before the real field sees them.
The scope reduction is large and it is not total, and a team that believes otherwise stops doing the things — script inventory, CSP, Subresource Integrity — that the remaining scope requires.
Tokens and the rest of the card data
A token replaces the number. It does not replace the other two credentials, and the rules for those are unchanged: the expiry is stored by the provider alongside the card, and the security code may never be retained after authorisation by anyone, tokenised or not. This is why a saved card cannot ask for its code on a repeat charge — there is nothing stored to compare against, and there was never allowed to be.
Testing tokenised flows
The paths worth exercising deliberately:
- Tokenisation succeeds and the token is stored against the right customer
- Tokenisation fails — a declined card at the tokenisation step is a different branch from a declined charge
- A stored token is charged successfully later
- A token that has expired or been revoked is handled without an unhandled exception
- The customer removes a saved card, and the token is deleted at the provider as well as in your database
- A card is reissued: with a network token the charge still succeeds, with a gateway token it does not — confirm which behaviour you actually have
- A provider migration, if you ever expect one, is at least rehearsed
Use your provider’s sandbox for all of it — the test card reference collects the published numbers, and the Stripe set documents the tokens that stand in for raw card numbers in server-side tests. Numbers from the generator are the right input for the form layer that sits in front of tokenisation, and the wrong input the moment the request leaves the browser for the provider.