Integrating the Payments Vault
Load the Payments Vault SDK, render the card fields into your checkout, turn the card into a single-use token, and charge that token through the Vrio API.
This guide walks through a full integration: getting the card fields onto your page, turning what the customer types into a payment token, and charging that token through the Vrio API.
If you haven't yet, start with the Payments Vault overview — it covers how the pieces fit together and what you'll need before you write any code.
Before You Start
Make sure your Vrio instance is already set up to accept orders — a merchant account, an item, an offer and a campaign. The Quick Start Guide walks through all of it.
With that in place, the integration is four steps.
Step 1: Add the SDK to Your Page
Add the script to your checkout page with your public key on the query string.
<script src="https://payments.vrio.app/sdk/v1/vault.js?key=pk_live_yourKeyHere"></script>The Vault checks that the request is coming from a domain registered against that key. If the domain isn't registered — or if the browser isn't telling us which domain it is — the script is refused and no card fields appear.
Check your Referrer-Policy before anything else
The Vault identifies your site by the
Refererheader the browser sends. A page-levelReferrer-Policy: same-originstrips that header from cross-origin requests, so the SDK can't tell us who's asking and the request is refused.Use
strict-origin-when-cross-origininstead. It sends only the scheme and host — never the path or query string — so it's just as private in practice, and it's what most sites use.Check the header your site actually serves rather than the one your application sets. A CDN or edge configuration will often override it.
Your Content Security Policy needs to allow the payments host too. Add https://payments.vrio.app to both script-src and frame-src.
Step 2: Render the Card Fields
Give the SDK an empty element for each field you want, then ask it to fill them in. The containers live inside your own form, so your layout, labels, spacing and submit button all stay yours.
<div id="card-number"></div>
<div id="card-exp-month"></div>
<div id="card-exp-year"></div>
<div id="card-cvv"></div>const vault = PaymentVault.init({ publicKey: "pk_live_yourKeyHere" });
vault.createField("cardNumber", { container: document.getElementById("card-number") });
vault.createField("expirationMonth", { container: document.getElementById("card-exp-month") });
vault.createField("expirationYear", { container: document.getElementById("card-exp-year") });
vault.createField("cvv", { container: document.getElementById("card-cvv") });Each field renders as an isolated frame served from Vrio. That isolation is the whole point — it's also why your page can't read the values, and why your stylesheet can't reach the inputs. Styling is passed in when you create the field. See Styling the Card Fields.
Available fields
| Field | Required | What the customer sees |
|---|---|---|
cardNumber | Yes | Text input that formats itself as they type |
expirationMonth | Yes | Dropdown, 01–12 |
expirationYear | Yes | Dropdown, this year through ten years out |
cvv | No | Numeric, 3 digits — 4 for American Express |
cardholderName | No | Text input, 2–50 characters |
billingZip | No | Alphanumeric, 2–10 characters |
Pasting into a field is blocked by default, which cuts down on customers pasting the wrong value. Pass disablePaste: false on a field if you'd rather allow it.
You probably don't need the cardholder name field
Most checkouts already collect a billing name elsewhere on the page, and because this field is hosted by Vrio there's no way to pre-fill it from a name you already hold. Rendering it anyway means the customer types their name twice.
Leave it out unless you have a reason to collect the name separately from the billing address.
Reacting as the customer types
Since you can't watch the inputs directly, the SDK reports back through callbacks. Each field accepts onReady, onChange, onValidation, onFocus and onBlur.
Use these to show inline errors, drive a floating label, light up a card brand icon, or enable your pay button once everything's filled in. onChange on the card number tells you the detected brand as soon as it's recognizable, which is how you'd reject an unsupported one before the customer gets to the end of the form.
Step 3: Create the Payment Token
When the customer submits, call tokenize(). The card goes directly from their browser to the Vault, and you get a token back.
try {
const result = await vault.tokenize();
// result.token → "tok_use2_aB3dEf7hJk9mNp2qRs4tUv6w"
// result.card → { last4, bin, cardType, cardholderName,
// expirationMonth, expirationYear, billingZip }
// Hand the token off to whatever creates your orders. That might be your
// own endpoint, as here, or the Vrio API called directly from the browser.
await fetch("/your-checkout-handler", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payment_token: result.token }),
});
} catch (err) {
// err.errors → ["cardNumber is invalid."]
showErrors(err.errors);
}Alongside the token you get back safe display details — the last four digits, the card brand, the expiration — which are fine to show on a confirmation screen or store on your own order record. The full card number and security code are never returned.
If the customer has left something blank or mistyped it, tokenize() rejects instead, and err.errors gives you a list you can put in front of them.
A few things worth knowing:
- Styles are set when the field is created. There's no way to restyle a live field — destroy and re-create it instead.
vault.destroy()removes every field. Call it before initializing again; only one instance can be active at a time.- Create the token at submit time, not while the customer is still shopping. A token expires 15 minutes after it's created.
Step 4: Process the Order
You now have a token and no card data. Create the order the way you normally would — whether that call comes from your own backend or straight from the browser — with two changes: set payment_method_id to 16, pass the token as payment_token, and send no card fields at all.
curl -X POST "https://api.vrio.app/orders" \
-H "X-Api-Key: YOUR_VRIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connection_id": 1,
"campaign_id": 18,
"offers": "[{\"offer_id\": 89, \"order_offer_quantity\": 1, \"order_offer_price\": 25.00}]",
"action": "process",
"payment_method_id": 16,
"payment_token": "tok_use2_aB3dEf7hJk9mNp2qRs4tUv6w",
"email": "[email protected]",
"bill_fname": "Jane",
"bill_lname": "Doe",
"bill_address1": "123 Main St",
"bill_city": "Denver",
"bill_state": "CO",
"bill_zipcode": "80202",
"bill_country": "US"
}'Vrio redeems the token, retrieves the card, and processes the order as an ordinary credit card charge. The response is the standard order response — there's nothing token-specific in it, and response_code: 100 means the same thing it always does.
The response carries a customer_card_id. The card is now stored against the order, and that — not the token — is the long-term handle on it. The token has done its job and is gone.
This is where the Payments Vault stops being special. Payment routing, merchant selection, upsells, captures, refunds and recurring cycles all work exactly as they do for any other order.
Learn more: Upsell Processing →
Where tokens work
Tokens are accepted anywhere you'd normally supply a card to create or authorize an order:
- POST /orders — with
actionset toprocessorauthorize - POST /orders/
{order_id}/process - POST /orders/
{order_id}/authorize
They aren't used for capture, complete, or order updates — those reference the card already stored on the order, so there's nothing for a token to do. Sending one to those endpoints has no effect.
Multi-Step Checkouts
If your checkout collects information across several pages, create the order first and process it once the customer submits payment. A token expires 15 minutes after it's created, so create it at the payment step — not at the start of the flow.
Create the order without a payment method:
curl -X POST "https://api.vrio.app/orders" \
-H "Content-Type: application/json" \
-H "X-API-KEY: YOUR_API_KEY" \
-d '{
"connection_id": 1,
"campaign_id": 18,
"offers": "[{\"offer_id\":89, \"order_offer_quantity\": 1,\"order_offer_price\":25.00}]",
"email": "[email protected]",
"bill_fname": "Jane",
"bill_lname": "Doe",
"bill_address1": "123 Main St",
"bill_city": "Anytown",
"bill_state": "NY",
"bill_zipcode": "12345",
"bill_country": "US"
}'Then process it with the token:
curl -X POST "https://api.vrio.app/orders/1114897/process" \
-H "Content-Type: application/json" \
-H "X-API-KEY: YOUR_API_KEY" \
-d '{
"payment_method_id": 16,
"payment_token": "tok_use2_aB3dEf7hJk9mNp2qRs4tUv6w"
}'To authorize now and capture later, use action: "authorize" or the authorize endpoint in exactly the same way. See Auth & Capture.
When the Token Fails
A token failure is not a decline. A decline means the card was reached and refused — you handle that exactly as you always have. The responses below mean something went wrong with the token itself.
Whatever the cause, never retry with the same token.
| What you get back | What it means | What to do |
|---|---|---|
404 | The token was never valid, or has already been used or expired | Tokenize again and process the new token |
500 | Vrio couldn't reach the Vault, so the payment was never attempted | Safe to retry — tokenize again and process the new token |
502 | The payment may or may not have gone through — Vrio never got an answer back | Don't retry. Check whether the order went through first |
"Tokenize again" always means exactly that: a token only comes from card entry, so the customer re-enters their card and the SDK issues a fresh one. There is no way to renew or reissue a token you already have.
A 502 may have charged the customer
Vrio got no answer back, so the payment may have succeeded. Processing a fresh token without checking risks charging twice — and re-sending the spent one just fails. Look up the order before you do either.
Error responses include a payment_trace_id where one is available. Quote it if you contact support — because no token or card data is ever logged, it's the only identifier that ties your request to our records.
Testing Your Integration
Point your checkout at a campaign that routes to the Test gateway. Orders processed through it are flagged as tests and never reach a real processor, and the standard test cards behave as they do everywhere else. A fresh token is created every time you submit, so you can reuse the same card as often as you like.
Register every domain you'll test from against your key, the same way you register your live checkout. That includes staging and local domains — and they need to be served over HTTPS, since the Vault won't load over plain HTTP.
When the Fields Don't Appear
Nearly every setup problem looks the same from the outside: your containers are there, but the fields inside them are empty or won't accept typing. It reads like a CSS problem and almost never is. Open your browser's Network panel and look at the requests to the payments host.
| What you see | Almost always means |
|---|---|
| The SDK request is refused | The key isn't registered for this exact hostname. Remember that www. and the bare domain are separate entries |
The request is refused and carries no Referer | A Referrer-Policy is stripping it — see the callout at the top of this page |
Refused to load… in the console | Your Content Security Policy doesn't allow the payments host |
The page is on http:// | The Vault requires HTTPS, on every domain including local ones |
Related Documentation
- Payments Vault Overview — What it is and when to use it
- Styling the Card Fields — Make the hosted fields match your checkout
- Payments Vault Examples — A complete, runnable tokenization page
- Auth & Capture — Authorize with a token now, capture later
- Upsell Processing — Bill additional offers against the order
- Duplicate Prevention — How Vrio protects against repeated submissions
Updated about 1 hour ago
