Using Elements with a Custom VGS Vault
Introduction
By default, Payment Elements store the card in Forward's vault and hand your application back a
Payment Method id (pm_...) that you charge through the Payments API.
As part of our migration efforts, we will provide partners with their own dedicated vault to be used with the SDK as a bridge solution to help with token migrations from an existing provider. In this mode the SDK collects the card inside VGS Collect secure fields, tokenizes it into your dedicated vault, and returns both of the following from a single form submission:
- VGS aliases (
aliases.cardNumber,aliases.cardCvc) — reusable references in your own vault that you send through a VGS outbound proxy to your existing gateway that our team with help set up. - A Forward
payment_method_id(token) — the same card, already registered with Forward and ready to charge.
Because one card entry produces both, you can keep running your current provider unchanged, and switch to Forward as the processor later by changing which id your backend sends — with no second form, no re-entry by the cardholder, and no bulk data migration.
Throughout this guide, "partner vault" is the term used in the SDK for a customer-supplied VGS vault, and "Forward vault" for the default behavior.
Custom vault mode must be enabled for your account. Contact your implementation specialist before starting — see Before you begin.
Before you begin
You will need:
- Your VGS tenant vault id — the
tnt...identifier of the vault the card should be stored in. This is required; there is no default and will be provided to you. - A VGS outbound route in that vault, this will be dependent on you current provider(s), in some cases you may have to get permission to use these endpoints because they allow raw pan data sent to the processor.
- Your Forward publishable API key (
pkey_...).
Unlike the default flow, no payment intent is required. Partner vault Elements authenticate with
your publishable key alone, so you do not need a client_secret and your page does not need a
backend round trip before the form can render.
Environments
| Environment | SDK | environment value |
|---|---|---|
| Sandbox | https://sandbox-cdn.pci.getfwd.com/sdk/forward.js | sandbox |
| Production | https://cdn.pci.getfwd.com/sdk/forward.js | live |
How the flow works
Browser Your VGS vault Forward API Your PSP
─────── ────────────── ─────────── ────────
1. Cardholder types into
VGS Collect fields
│
▼
2. Submit ────────────────► 3. PAN + CVC redacted
into aliases in YOUR
vault, request forwarded
│
▼
4. Payment method created,
referencing your aliases
│
5. onSuccess ◄──────────────────────────┘
{ token, card, aliases }
│ │
├── aliases ──► persist in your systems ──► outbound proxy ───────────────►│ authorize today
│
└── token (pm_…) ──► persist alongside it ──► POST /payment_intents/:id/payments (later)
The raw card number never reaches your servers or Forward's — it is redacted at the VGS edge inside your own vault, which is what keeps the flow in PCI scope for VGS rather than for you.
Step 1: Add the SDK to your page
<head>
<script src="https://sandbox-cdn.pci.getfwd.com/sdk/forward.js"></script>
</head>
Add a container element for the form to mount into:
<form id="checkout-form">
<div id="card-element"></div>
<button type="submit">Save card</button>
</form>
Step 2: Create a partner vault Element
Forward is available on window once the SDK loads. Use the createPartnerVault* factories —
they are the custom-vault counterparts of createCardElement, createBankElement, and
createPaymentElement.
Card
const cardElement = await window.Forward.createPartnerVaultCardElement({
apiKey: 'pkey_123948342832',
vaultId: 'tnt1a2b3c4d5e', // your vgs vault — required
environment: 'sandbox', // 'sandbox' | 'live'
// routeId: 'a1b2c3d4-...', // optional: pin a specific inbound route, this will be provided to you if necessary
});
Bank account
US ACH and Canadian EFT are both supported; select the rail explicitly.
const bankElement = await window.Forward.createPartnerVaultBankElement({
apiKey: 'pkey_123948342832',
vaultId: 'tnt1a2b3c4d5e',
environment: 'sandbox',
rail: 'us_bank_ach', // 'us_bank_ach' | 'ca_bank_eft'
});
Card and bank in one Element
createPartnerVaultPaymentElement renders a Card / Bank selector over the same secure fields.
Declare the methods you want with paymentMethodTypes:
const paymentElement = await window.Forward.createPartnerVaultPaymentElement({
apiKey: 'pkey_123948342832',
vaultId: 'tnt1a2b3c4d5e',
environment: 'sandbox',
paymentMethodTypes: ['card', 'bank'],
});
The partner vault Payment Element composes the card and bank panels only. Apple Pay, Google Pay, and instant bank linking are Forward-vault flows that require a payment intent, so they are not rendered in custom vault mode. If you need them, use the standard Payment Element.
Launching without a custom vault
For contrast, here is the same page wired to the Forward vault. There are two ways to launch it,
selected by the version field. Both require a client_secret from a
payment intent — that is the main practical
difference from custom vault mode, and it means your page needs a backend call before the form can
render.
version: 1 (default) — Forward hosts the entire form inside an iframe. Omit version
entirely to get this:
const cardElement = await window.Forward.createCardElement({
apiKey: 'pkey_123948342832',
clientSecret: 'pi_123948342832_secret_123948342832',
});
version: 2 — the same VGS Collect secure fields this guide uses, but tokenizing into
Forward's vault instead of yours. Element config comes first, vault config second:
const cardElement = await window.Forward.createCardElement(
{
apiKey: 'pkey_123948342832',
clientSecret: 'pi_123948342832_secret_123948342832',
version: 2,
},
{ environment: 'sandbox' } // vault config
);
createBankElement and createPaymentElement take the same version parameter. Mounting is
identical in every case — the only thing that changes across all of these is the onSuccess
payload, which carries aliases in custom vault mode and omits it for the Forward vault.
version: 2 returns a directly-rendered element rather than an iframe, so the style options theme
the fields on your page. If you are migrating from version: 1, re-check your styling before
release.
Step 3: Mount the Element and handle onSuccess
Mounting is identical to the standard Card Element: you
supply callbacks, receive a tokenize function from onReady, and call it when the buyer submits.
The difference is the onSuccess payload, which now carries aliases.
let tokenize;
const unmount = cardElement.mount('#card-element', {
showLabels: true,
showCardholderName: true,
onChange: (event) => {
// Enable or disable your submit button
document.querySelector('#checkout-form button').disabled = !event.ready;
},
onReady: (readyTokenize) => {
tokenize = readyTokenize;
},
onSuccess: async ({ token, card, aliases }) => {
// Send both identifiers to your backend and store them together.
await fetch('/api/cards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
forwardPaymentMethodId: token, // pm_... — charge through Forward
vgsCardNumberAlias: aliases?.cardNumber, // charge through your existing PSP
vgsCardCvcAlias: aliases?.cardCvc,
brand: card?.brand,
lastFour: card?.lastFourDigits,
expMonth: card?.expMonth,
expYear: card?.expYear,
}),
});
},
onError: (error) => {
console.error(error);
},
});
document.querySelector('#checkout-form').addEventListener('submit', async (event) => {
event.preventDefault();
await tokenize?.();
});
Store the alias and the payment_method_id on the same record. They describe the same card, and
keeping them together is what makes the later cutover a configuration change rather than a
migration.
What onSuccess returns
type SuccessInfo = {
type: 'card' | 'bank' | 'ca_bank';
token: string; // Forward payment method id (pm_...)
card?: { // present for card
brand?: string;
cardType?: string;
bin?: string;
firstSixDigits?: string;
lastFourDigits?: string;
expMonth?: number;
expYear?: number;
enrichedAttributes?: object; // issuer / capability metadata, when available
};
bank?: { // present for bank / ca_bank
accountHolderName?: string;
lastFourDigits?: string;
bankName?: string;
accountType?: 'checking' | 'savings';
ownerType?: 'individual' | 'business';
routingNumber?: string; // US
institutionNumber?: string; // CA
transitNumber?: string; // CA
};
// Custom vault only — the aliases in YOUR vault.
aliases?:
| { cardNumber?: string; cardCvc?: string } // card
| { accountNumber?: string }; // bank
};
| Scenario | token | card / bank | aliases |
|---|---|---|---|
| Card, custom vault | pm_... | card | { cardNumber, cardCvc } |
| Bank, custom vault | pm_... | bank | { accountNumber } |
| Card or bank, Forward vault | pm_... | card / bank | — |
Bank identifiers such as the routing number are returned in the bank object rather than as
aliases; they are not secret. The account number is the only aliased bank field.
Canadian EFT
For rail: 'ca_bank_eft' you must pass a pre-authorized debit agreement when tokenizing:
await tokenize?.({
padAgreement: {
ip: '203.0.113.7', // the payer's captured IP address
agreement: 'I authorize this pre-authorized debit.',
},
});
Step 4: Keep charging at your existing provider
Nothing about your current processing changes. The alias you stored is a reference into your own vault, so you send it to your gateway through your VGS outbound proxy exactly as you do today — VGS substitutes the real card number on the way out.
curl --location 'https://my-gateway.example.com/authorizations' \
--proxy 'https://<vgs-username>:<vgs-password>@<vault-id>.sandbox.verygoodproxy.com:8443' \
--header 'Content-Type: application/json' \
--data '{
"amount": 4999,
"currency": "USD",
"card": {
"number": "tok_sandbox_abc123def456",
"cvc": "tok_sandbox_ghi789jkl012",
"exp_month": 1,
"exp_year": 2029
}
}'
The number and cvc values above are the aliases.cardNumber and aliases.cardCvc returned by
onSuccess. A team member will help configure outbound routes for your current service provider to proxy requests in a secure manner.
Step 5: Switch to Forward as the PSP
Because the same card entry already produced a Forward payment_method_id, moving a payment to
Forward is a change to which identifier your backend sends — not a new integration on the page.
Create a Payment Intent from your backend:
curl -X POST "https://<api-host>/payment_intents" \
-H "x-account-id: acct_12345678" \
-H "x-api-key: key_123456789" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999
}'
Then charge the stored payment method:
curl --location 'https://<api-host>/payment_intents/pi_2SG6ibTHTo4N6Sdx6Oh66IGg3o3/payments' \
--header 'x-account-id: acct_12345678' \
--header 'x-api-key: key_123456789' \
--header 'Content-Type: application/json' \
--data '{
"payment_method_id": "pm_2SXKvZzhPgZJ4nbcVq8UB9eT1B7"
}'
Forward authorizes, applies fees, and settles as described in Accept Payments. Refunds, voids, disputes, and webhooks all behave the same as for any Forward payment method.
Stored-credential rules still apply. If you are charging a card that the cardholder is not present for, mark the payment accordingly — see Storing Card and Bank Data.
Reference
Create options
| Option | Type | Required | Notes |
|---|---|---|---|
apiKey | string | yes | Your publishable key (pkey_...), sent as x-api-key. |
vaultId | string | yes | Your VGS tenant vault id (tnt...). No default in custom vault mode. |
environment | 'sandbox' \| 'live' | no | Defaults to live. |
routeId | string | no | Pin a specific inbound route when the vault has more than one. |
cname | string | no | Serve VGS Collect from your own domain. |
paymentMethodTypes | ('card' \| 'bank' \| 'ca_bank')[] | no | Payment Element only. Defaults to ['card']. |
rail | 'us_bank_ach' \| 'ca_bank_eft' | no | Bank Element only. |
showFieldLoader | boolean | no | Shimmer placeholder while the secure fields load. Defaults to true. |
clientSecret | string | no | Discouraged. Supplying one makes the SDK fetch the intent to read its payment method types, adding a round trip that custom vault mode does not otherwise need. Pass paymentMethodTypes instead. |
Mount options
The mount surface matches the standard Elements —
onChange, onReady, onSuccess,
onError, onLoad, style, showLabels, showCardholderName, hidePostalCode, and
hideCountry. See the Card Element for the
full style reference; the same style attributes theme the secure fields here.
Custom vault vs. Forward vault
| Forward vault | Custom (partner) VGS vault | |
|---|---|---|
| Who stores the instrument | Forward | You |
| Authentication | client_secret from a payment intent | Publishable apiKey |
| Payment intent required? | Yes | No |
vaultId | Optional | Required |
onSuccess returns | { token, card \| bank } | { token, card \| bank, aliases } |
| Apple Pay / Google Pay | Supported | Not available |
| Instant bank linking | Supported (US) | Not available |
| Factories | createCardElement, createBankElement, createPaymentElement | createPartnerVaultCardElement, createPartnerVaultBankElement, createPartnerVaultPaymentElement |
Security notes
- The cardholder types into VGS Collect iframes served from VGS, not from your page and not from Forward. Neither your servers nor Forward's ever see the cleartext card number.
- Aliases are references, not card data — but treat them as credentials. Anyone who can send them through your outbound proxy can transact. Store them with the same controls you apply to gateway tokens.
- Never log an alias next to the BIN and last four in a way that reconstructs a searchable card record.
- Aliases are scoped to your vault. They are meaningless in any other vault, including Forward's,
which is what makes them safe to hold and what makes the
payment_method_idthe right identifier to use when charging through Forward.