Tokenization and Saved Cards
Tokenization replaces a customer's raw card details (PAN, expiry, CVV) with an opaque reference called a card token. Once a card is tokenized you can charge it again without ever touching, storing, or transmitting raw card data yourself.
Every card entered through a MoneyHash-hosted card form - the Vault embed, the Web or Mobile SDK, or the standalone tokenization embed - is captured inside MoneyHash's PCI-compliant Vault, an isolated environment that never exposes raw card data to your backend or to MoneyHash's core systems. The Vault returns a card token that you store and reuse. The card number itself stays inside the Vault.
Every tokenization flow on this page needs a customer id. A card token is always created for a Customer - it is how MoneyHash knows whose saved cards to surface at checkout later. If you have not created a Customer yet, start with the Customers page.
This page covers the whole save-and-reuse lifecycle:
- the three ways to create a card token (automatic, customer-choice, or standalone)
- how Vault storage works, and what
UNIVERSALvsPROVIDERactually changes - how to charge a saved token, and when to add
card_token_data.cvv - CIT vs MIT, and why every charge in an agreement carries the same
agreement_id - how to delete a saved card
The full lifecycle, in motion
Tokenize once with the customer present, charge the first payment as a CIT, then bill without them as often as you need - and if a provider declines, retry the same token somewhere else.

Where the card number actually goes
The Vault is a hard boundary, not a convention. Your servers see a token; they never see a PAN. That is what takes your integration out of PCI scope for card data.
When to use it
Reach for tokenization whenever a card needs to outlive a single payment:
- One-click / express checkout - a returning customer pays without re-typing their card.
- Card-on-file - you keep a card on record for a subscription, wallet top-up, or invoice.
- Merchant-Initiated Transactions (MIT) - recurring or unscheduled charges made when the customer is not present.
- PCI-scope reduction - raw card data never touches your own servers.
- Cross-provider routing and failover - one saved token you can charge through any connected provider, not just the one that captured it.
How Vault storage works
Every card token is stored in one place or the other, and that choice is the single most consequential decision on this page. You set it with card_token_type when the token is created.
card_token_type | Where the card lives | What you can do with it |
|---|---|---|
UNIVERSAL | The PAN sits in MoneyHash's own PCI-isolated Vault. | The token is provider-agnostic. MoneyHash detokenizes it through the Vault Proxy at charge time, so it can be routed, retried, and failed over across any connected provider. |
PROVIDER | The token lives at the provider that processed the original transaction. | The token is locked to that one connection and can only be charged there. Onboarding a new provider means these cards cannot be reused on it. |
BOTH | A MoneyHash Vault token and a provider token for the same card. | Maximum compatibility, at the cost of tokenizing twice. |
Unless you have a specific reason to lock a token to one provider, request a UNIVERSAL token. It is what makes routing, retries, failover, and recurring recovery possible on a saved card.
Three ways to create a card token
Pick exactly one per intent. They are mutually exclusive - combining them returns a validation error.
1. Tokenize automatically during a payment
Set tokenize_card: true on a normal payment intent. The card is saved the moment the payment succeeds, with no checkbox and no extra customer action.
{
"amount": 100,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"tokenize_card": true,
"webhook_url": "https://your-server.com/webhook"
}Use this when saving the card is a business rule rather than a customer decision - the first payment of a subscription, for example.
2. Let the customer choose at checkout
Show a save-card checkbox next to the card form:
show_save_card_checkbox: true- optional. The payment succeeds whether or not the customer ticks it.show_mandatory_save_card_checkbox: true- the same checkbox, but ticking it is required to complete the payment.
{
"amount": 100,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"show_save_card_checkbox": true,
"webhook_url": "https://your-server.com/webhook"
}Use this for a standard e-commerce checkout, where saving the card is the customer's call.
3. Tokenize standalone, with no payment
Create a card token outside any payment. Nothing is charged - the customer only goes through card entry, plus 3DS or verification if you have it configured.
{
"customer": "<CUSTOMER_ID>",
"webhook_url": "https://your-server.com/webhook"
}The response carries an embed_url - render or open it so the customer can enter their card inside the Vault iframe. Use this to save a card ahead of time, during onboarding or before the first invoice exists. Full schema: Create a card token, token intent details for polling the result and Get card token for reading one back.
The SDK equivalents
If you drive the card form through the Web or Mobile SDK rather than the embed URL yourself, the same two ideas map onto SDK calls. Mobile SDKs expose the same saveCard parameter on their card form's pay() method.
// Backend: create the intent with "allow_tokenize_card": true
const moneyHash = new MoneyHash({ type: "payment", publicApiKey: "<PUBLIC_API_KEY>" });
moneyHash.cardForm.pay({
intentId: "<INTENT_ID>",
saveCard: true, // needs allow_tokenize_card on the intent
});// Backend: create a card intent with "card_token_type": "UNIVERSAL"
const cardData = await moneyHash.cardForm.collect();
moneyHash.cardForm.createCardToken({
cardData,
cardIntentId: "<CARD_INTENT_ID>",
}).then(({ state, stateDetails }) => {
console.log({ state, stateDetails });
});Splitting collect() from createCardToken() lets you run your own business logic between collecting the card and tokenizing it. See SDK Architecture and Create a Payment.
Charging a saved token
Three shapes, ordered by how much friction the customer sees.
1. Show the saved cards and let the customer pick
Send the customer id and no card_token. MoneyHash surfaces every active token on that customer as a payment option.
{
"amount": 50,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"webhook_url": "https://your-server.com/webhook"
}2. Charge one specific token
Send card_token to skip the picker. Depending on the provider and your risk rules the customer may still be routed through the embed for a step-up challenge such as 3DS or a CVV re-entry.
{
"amount": 50,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"card_token": "<CARD_TOKEN_ID>",
"webhook_url": "https://your-server.com/webhook"
}3. Token plus CVV, fully server-side
Add card_token_data.cvv when you have collected the CVV yourself and want the charge to complete in a single API call, with no redirect and no embed.
{
"amount": 50,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"card_token": "<CARD_TOKEN_ID>",
"card_token_data": { "cvv": "123" },
"webhook_url": "https://your-server.com/webhook"
}The same payload works against the dedicated charge endpoint, which returns the payment status directly instead of an embed_url - the shape you want for merchant-initiated charges, where there is no customer to redirect.
Reference: Charge card token and Create a card-token payment on an intent.
card_token alone vs. card_token_data.cvv
card_tokenalone - for cardholder-present (CIT) flows, where the network or provider may still demand a challenge. The customer may be routed through the embed to finish.card_tokenpluscard_token_data.cvv- for a single, fully server-side charge with no redirect. This is the shape for MIT and recurring charges, and for thecharge-card-tokenendpoint.
The examples above use the Payment API with the hosted Vault and embed, which is right for almost every integration. Merchants who are PCI Level 1 certified and integrate through the Direct API charge a saved token on a separate, analogous endpoint: POST /direct/payments/intents/{intent_id}/card_token/. See the API Reference for its schema.
CIT vs MIT, and the agreement_id
Once a card is on file, every charge is one of two kinds, and card networks treat them very differently:
| CIT - customer-initiated | MIT - merchant-initiated | |
|---|---|---|
| Who starts it | The customer, in your app right now | Your backend, on a schedule or a trigger |
| Is the customer there? | Yes | No |
| Field on the intent | merchant_initiated: false | merchant_initiated: true |
| 3DS / step-up | Possible, and this is where it belongs | Not possible - nobody is there to complete it |
| Typical use | First payment, one-click re-order | Renewal, wallet auto-top-up, retry, invoice |
The order matters. A CIT establishes the agreement: the customer is present, any challenge is completed, and the provider records that this merchant is authorized to charge this card again. Every MIT afterwards refers back to that agreement - which is exactly what agreement_id is for.
You generate the agreement_id yourself - a UUID or any unique string - and send it, unchanged, inside recurring_data on the first CIT and on every MIT that follows.
{
"amount": 20,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"card_token": "<CARD_TOKEN_ID>",
"merchant_initiated": false,
"payment_type": "UNSCHEDULED",
"recurring_data": {
"agreement_id": "<YOUR_AGREEMENT_ID>"
},
"webhook_url": "https://your-server.com/webhook"
}{
"amount": 20,
"amount_currency": "USD",
"operation": "purchase",
"customer": "<CUSTOMER_ID>",
"card_token": "<CARD_TOKEN_ID>",
"merchant_initiated": true,
"payment_type": "UNSCHEDULED",
"recurring_data": {
"agreement_id": "<YOUR_AGREEMENT_ID>"
},
"webhook_url": "https://your-server.com/webhook"
}Use the same agreement_id, the same customer, and the same card_token for every charge in one agreement. A fresh id on a later charge reads as a brand-new agreement with no CIT behind it, which is the most common cause of an unexplained MIT decline.
Agreement coverage across providers
Here is the part that pays off. MoneyHash keeps one agreement_id on your side, but each provider stores its own agreement reference underneath it - its own field name, its own value, and the CIT that produced it. A provider that has one is covered for that agreement.
Coverage is what decides whether a recurring charge can run somewhere. An MIT sent to a provider with no coverage has no CIT to refer back to, so it fails. Three routing actions are built around this, configurable in your routing flows:
- Smart Recurring Setup (CIT) - maximizes coverage. MoneyHash spots which providers are still uncovered and aims CITs at them, so future MITs have more places to run.
- Smart Recurring Processing (MIT) - sends each MIT to the provider with the best agreement coverage.
- Smart Recurring Recovery (MIT) - when a recurring charge fails, retries it on another covered provider instead of dropping the billing cycle.
Only providers that support agreement mapping can hold coverage, and only a UNIVERSAL token can be charged across providers in the first place. Together they are what turn a failed renewal into a retry somewhere else rather than lost revenue.
Deleting a saved card
Deleting a saved card removes the reference on the Customer record and removes the underlying token from every vault it is held in - MoneyHash's, the provider's, or both.
customer- the customer's id.to_delete_customer_card_token- the id of the saved card token to remove.
Once deleted the token can no longer be surfaced at checkout or charged. If it backs an active subscription, the next billing attempt fails until a new card is attached. See Customers for listing and managing a customer's saved cards.
Common errors and troubleshooting
| Issue | Cause | Fix |
|---|---|---|
400 - more than one tokenization flag set | tokenize_card, show_save_card_checkbox and show_mandatory_save_card_checkbox are mutually exclusive. | Set exactly one of the three on a given intent. |
400 - customer not found | The customer id does not exist, or belongs to a different account or mode. | Check the id, and that the customer was created in the same mode (test or live) as the intent. |
404 - card token not found | The token was already deleted, or belongs to a different customer. | Re-fetch the customer's saved cards before charging or deleting. |
| Payment succeeded but no card was saved | The Vault was briefly unreachable. MoneyHash lets the payment through rather than failing it, so no token is created. | Check the intent response for a card token id. If it is absent, offer to save the card again on a later visit. |
| Charge fails on a provider the token was not created on | The token is PROVIDER-type and scoped to one connection. | Re-tokenize the card as UNIVERSAL, which is chargeable on any connected provider. |
A challenge is requested even though you sent card_token_data.cvv | Some providers and networks require 3DS on top of CVV, especially for a present cardholder or a new device fingerprint. | Handle the embed_url in the response exactly as you would for a first-time payment. |
| MIT declines with no obvious reason | The agreement_id differs from the one on the originating CIT, or the target provider has no agreement coverage. | Reuse the original agreement_id, and route MITs to a covered provider. |
| Subscription or MIT charge fails after a card was deleted | The token backing the agreement was removed. | Attach a new saved card to the customer and update the recurring agreement. |
Updated 11 days ago