Skip to main content

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.

note

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:

  1. 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.
  2. 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.
  3. Your Forward publishable API key (pkey_...).
info

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

EnvironmentSDKenvironment value
Sandboxhttps://sandbox-cdn.pci.getfwd.com/sdk/forward.jssandbox
Productionhttps://cdn.pci.getfwd.com/sdk/forward.jslive

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'],
});
note

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.

note

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?.();
});
info

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
};
Scenariotokencard / bankaliases
Card, custom vaultpm_...card{ cardNumber, cardCvc }
Bank, custom vaultpm_...bank{ accountNumber }
Card or bank, Forward vaultpm_...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.

note

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

OptionTypeRequiredNotes
apiKeystringyesYour publishable key (pkey_...), sent as x-api-key.
vaultIdstringyesYour VGS tenant vault id (tnt...). No default in custom vault mode.
environment'sandbox' \| 'live'noDefaults to live.
routeIdstringnoPin a specific inbound route when the vault has more than one.
cnamestringnoServe VGS Collect from your own domain.
paymentMethodTypes('card' \| 'bank' \| 'ca_bank')[]noPayment Element only. Defaults to ['card'].
rail'us_bank_ach' \| 'ca_bank_eft'noBank Element only.
showFieldLoaderbooleannoShimmer placeholder while the secure fields load. Defaults to true.
clientSecretstringnoDiscouraged. 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 vaultCustom (partner) VGS vault
Who stores the instrumentForwardYou
Authenticationclient_secret from a payment intentPublishable apiKey
Payment intent required?YesNo
vaultIdOptionalRequired
onSuccess returns{ token, card \| bank }{ token, card \| bank, aliases }
Apple Pay / Google PaySupportedNot available
Instant bank linkingSupported (US)Not available
FactoriescreateCardElement, createBankElement, createPaymentElementcreatePartnerVaultCardElement, 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_id the right identifier to use when charging through Forward.