Coinme Vault SDK for Javascript
@coinme-security/vault-sdk-js is a typed, framework-agnostic JavaScript wrapper that lets
any web app capture debit card data - card number, expiry, CVC, billing address, and other
sensitive values - without those values ever touching your JavaScript, your DOM, your logs, or
your backend. You render containers for the secure fields; the SDK mounts VGS-hosted inputs
into them; your code receives only a confirmation response.
The SDK works with any web stack - vanilla JS, React, Vue, Angular, Svelte, or any
SSR framework - because the secure fields are cross-origin iframes, not framework components.
You wire them up in a handful of calls.
How it works
The SDK wraps VGS Collect.js,
a browser library from Very Good Security that handles the mechanics of secure card capture.
VGS acts as a secure proxy: each field the SDK creates is a cross-origin iframe served by VGS,
not an input element in your page. Values typed by the user are captured inside those iframes
and submitted directly to Coinme's vault - they never enter your JavaScript, your DOM tree, or
any network request your code can inspect.
The SDK pre-binds Coinme's VGS vault configuration. You never supply a vault ID, ingress
route, or submit path - just pick an environment (sandbox or live) and the SDK resolves
everything else.
The iframe model - what it means for your integration
Because each secure field is an iframe served from VGS's domain, a few things work
differently from a regular <input>:
- Your page CSS cannot reach inside the fields. Styling is applied through a
cssoption
you pass at field creation time - see Styling. - You can't read the field values back. The fields are intentionally opaque. You get state
metadata -isValid,isDirty,last4,cardBrand- but never the raw value. - Field containers must exist in the DOM before you create fields. The factory calls
(vault.createCardNumberField, etc.) mount iframes into the container elements you pass. If
a container doesn't exist yet, the field won't render. - Vault creation is browser-only.
createCoinmeVaultloads VGS Collect.js from the VGS
CDN and needs awindow. Calling it during server-side rendering will reject - see
SSR frameworks.
What VGS Collect.js version is used?
VGS Collect.js is loaded at runtime from the VGS CDN (for PCI script attestation). The SDK
pins to version 4.0.1 - exposed as the VGS_COLLECT_VERSION export if you need to
reference it. The SDK also ships full TypeScript typings for the VGS Collect surface, which
VGS itself does not provide.
Prerequisites
Before you install the SDK:
| Requirement | Details |
|---|---|
| Browser target | Evergreen Chrome, Firefox, Edge; Safari 10+. Vault creation is browser-only. |
| Node (build-time) | Any version that supports your build tool. Importing the package in Node is side-effect safe; creating a vault there is not. |
CLOUDSMITH_TEAM_TOKEN | Coinme Cloudsmith entitlement token - see below. |
| Content-Security-Policy | If your site sets a CSP, you must add VGS domains before the SDK will work - see CSP. |
Obtaining the CLOUDSMITH_TEAM_TOKEN
CLOUDSMITH_TEAM_TOKENThe SDK is published to Coinme's private npm registry. Access is authenticated by a
Cloudsmith entitlement token. This token is used only at npm install time. To obtain one,
contact the Coinme business development team.
Content-Security-Policy
If your site sets a Content-Security-Policy, the secure fields (iframes) and the VGS
Collect.js script load from VGS's domains. Add these entries before you begin:
script-src https://js.verygoodvault.com https://js3.verygoodvault.com
frame-src https://js.verygoodvault.com https://js3.verygoodvault.com
connect-src https://js.verygoodvault.com https://js3.verygoodvault.com https://vgs-collect-keeper.apps.verygood.systems
Also add your Coinme ingress host under connect-src. (js3.verygoodvault.com is VGS's
fallback CDN; vgs-collect-keeper receives VGS product-interaction analytics.) If either
domain is missing, the fields will fail to render or createCoinmeVault will reject with a
network error.
Installation
Step 1 - Configure the private registry
.npmrc (npm or Yarn Classic):
@coinme-security:registry=https://npm.coinme.com/coinme-security-wrapper/
//npm.coinme.com/coinme-security-wrapper/:_authToken=${CLOUDSMITH_TEAM_TOKEN}
always-auth=true
.yarnrc.yml (Yarn Berry / v2+):
nodeLinker: node-modules
npmScopes:
coinme-security:
npmRegistryServer: 'https://npm.coinme.com/coinme-security-wrapper/'
npmAuthToken: '${CLOUDSMITH_TEAM_TOKEN}'
npmAlwaysAuth: trueCommit the config file. Keep the token itself out of source control - supply it as an
environment variable.
Step 2 - Install
export CLOUDSMITH_TEAM_TOKEN=<your-token>
yarn add @coinme-security/vault-sdk-jsThere is no peer dependency to install separately. The @vgs/collect-js loader ships as a
regular dependency of the SDK.
Quickstart
Here is the minimum integration for vanilla JS - container elements, vault creation, field
mounting, gated submit, and error handling. If you're using React, Vue, or an SSR framework,
see Framework setup below.
<div id="pan"></div>
<div id="expiry"></div>
<div id="cvc"></div>
<button id="submit" disabled>Submit</button>import {
CoinmeVaultError,
createCoinmeVault,
} from '@coinme-security/vault-sdk-js';
// createCoinmeVault is async - it loads VGS Collect.js from the VGS CDN.
const vault = await createCoinmeVault({ environment: 'sandbox' });
// Mount secure fields into the container elements.
// These calls return raw VGS field instances.
vault.createCardNumberField('#pan', { placeholder: 'Card number' });
vault.createExpiryField('#expiry', { placeholder: 'MM / YYYY' });
vault.createCvcField('#cvc', { placeholder: 'CVC' });
// Subscribe to form state changes to gate the submit button.
const button = document.querySelector<HTMLButtonElement>('#submit')!;
vault.subscribe(() => {
button.disabled = !vault.getFormState().isValid;
});
button.addEventListener('click', async () => {
try {
const result = await vault.submit();
// result.status is the HTTP status; result.data is the parsed response body.
// Raw field values are never available here.
console.log('OK', result.status);
} catch (error) {
if (error instanceof CoinmeVaultError) {
console.log(`${error.kind}: ${error.message}`);
}
}
});
// Call when the hosting view is removed.
// vault.dispose();Three things to notice:
createCoinmeVaultis async. It loads VGS Collect.js from the CDN before resolving.
Wait for it before calling any field factories.- Field containers must exist first. The factory calls (
createCardNumberField, etc.)
mount iframes into DOM elements. Create the vault and call the factories only after your
containers are in the DOM. vault.subscribenotifies on every field-state change and returns an unsubscribe
function. Pollvault.getFormState().isValidinside it to keep your submit button in sync.
Framework setup
The SDK is framework-agnostic, but a few integration patterns are worth following per
framework to get lifecycle and cleanup right.
React
Vault creation is async and browser-only, so it belongs in a useEffect. Secure fields must
be created after their container <div>s mount, and deleted in cleanup to prevent React 18
StrictMode's double-invocation from stacking duplicate iframes:
import {
createCoinmeVault,
type CoinmeVault,
} from '@coinme-security/vault-sdk-js';
import { useEffect, useState, useSyncExternalStore } from 'react';
/** Creates the vault on mount (browser-only), disposes on unmount. */
const useCoinmeVault = () => {
const [vault, setVault] = useState<CoinmeVault>();
useEffect(() => {
let disposed = false;
let created: CoinmeVault | undefined;
createCoinmeVault({ environment: 'sandbox' }).then((v) => {
if (disposed) return v.dispose();
created = v;
setVault(v);
});
return () => {
disposed = true;
created?.dispose();
};
}, []);
return vault;
};
/** Reactive form state - snapshot is referentially stable, no memo needed. */
const useVaultFormState = (vault: CoinmeVault) =>
useSyncExternalStore(vault.subscribe, vault.getFormState);Create fields in a separate effect, with cleanup:
useEffect(() => {
if (!vault) return;
const fields = [
vault.createCardNumberField('#pan'),
vault.createExpiryField('#expiry'),
vault.createCvcField('#cvc'),
];
// Delete fields in cleanup so React StrictMode's double-run doesn't stack iframes.
return () => fields.forEach((field) => field.delete());
}, [vault]);A full working example is in examples/react in the SDK repository.
Vue 3
Same lifecycle, Vue idioms - create the vault in onMounted, dispose in onUnmounted,
mirror form state into a ref:
<script setup lang="ts">
import {
createCoinmeVault,
type CoinmeVault,
type VaultFormState,
} from '@coinme-security/vault-sdk-js';
import { onMounted, onUnmounted, ref } from 'vue';
const vault = ref<CoinmeVault>();
const formState = ref<VaultFormState>({ isValid: false, fields: {} });
let unsubscribe: (() => void) | undefined;
onMounted(async () => {
const v = await createCoinmeVault({ environment: 'sandbox' });
vault.value = v;
v.createCardNumberField('#pan', { placeholder: 'Card number' });
v.createExpiryField('#expiry', { placeholder: 'MM / YYYY' });
v.createCvcField('#cvc', { placeholder: 'CVC' });
unsubscribe = v.subscribe(() => {
formState.value = v.getFormState();
});
});
onUnmounted(() => {
unsubscribe?.();
vault.value?.dispose();
});
</script>
<template>
<div id="pan" />
<div id="expiry" />
<div id="cvc" />
<button :disabled="!formState.isValid" @click="handleSubmit">Submit</button>
</template>SSR frameworks (Next.js, Nuxt, SvelteKit, …)
Importing the package on the server is safe - the module has no load-time browser side
effects. Creating a vault is browser-only - createCoinmeVault rejects if there is no
window. Keep these points in mind:
- Create the vault inside client-side lifecycle code:
useEffect(Next.js),onMounted
(Nuxt),onMount(SvelteKit). The React and Vue examples above already do this. - In Next.js App Router, add the
'use client'directive to your form component. - Don't render secure-field containers on the server expecting them to be filled - fields
mount only after the client-side vault exists. - Add CSP headers in your framework's config (e.g.
next.config.jsheaders) - see
CSP.
Secure fields
Five factory methods are available on the vault instance. Each one mounts a cross-origin
iframe into the container matched by the CSS selector you pass, and returns the raw VGS field
instance. All VGS field methods - mask(), replacePattern(), update(), reset(),
delete(), focus(), on()/off(), and the promise readiness signal - work exactly as
VGS documents them.
| Factory | Purpose |
|---|---|
createCardNumberField | Card number with Luhn validation, brand detection, and masking. Defaults to name pan. Add showCardIcon: true for a brand icon inside the field. |
createExpiryField | Expiry date in MM / YYYY format, with a built-in not-in-the-past check. On submit, a serializer splits it into exp_month and a 4-digit exp_year. Defaults to name expiry. |
createCvcField | Security code; adapts to the detected card brand (3 or 4 digits). Defaults to name cvc. |
createCardHolderField | Cardholder name - for flows that need it collected securely. (The payment-method helper accepts name literals instead.) Defaults to name cardholder. |
createTextField | Generic secure field for other sensitive values: billing address lines, city, ZIP, SSN, etc. name is required. Choose field behaviour with type ('text', 'ssn', 'zip-code', 'password', 'textarea', and others). |
Every factory passes all options through to VGS unfiltered, so new VGS options work without
an SDK update.
Expiry year width. The payment-method endpoint requires a 4-digit year. The expiry
factory defaults toMM / YYYYaccordingly. OverrideyearLength: 2only if your endpoint
accepts 2-digit years.
Field names
VAULT_FIELD_NAMES provides the conventional field-name constants - pan, cvc, expiry,
address1, address2, city, zipcode. The card factories default to them. Pass them
explicitly on createTextField so collected values line up with the payment-method request
template:
import { VAULT_FIELD_NAMES } from '@coinme-security/vault-sdk-js';
vault.createTextField('#address1', {
name: VAULT_FIELD_NAMES.address1,
validations: ['required'],
});Styling
Because secure fields render inside VGS iframes, ordinary page CSS cannot reach inside them.
There are two complementary approaches.
Inside the field: the css option
css optionStyle the field's text input with the css option. It supports regular CSS properties,
pseudo-classes and pseudo-elements, nested & selectors, the VGS field state classes, web
fonts via @font-face, and media queries:
vault.createCardNumberField('#pan', {
css: {
'font-family': '"Inter", system-ui, sans-serif',
'font-size': '16px',
color: '#1b1d1f',
'&::placeholder': { color: '#9aa0a6' },
'&:focus': { color: '#000' },
// VGS state classes: valid, invalid, empty, dirty, touched
'&.invalid.touched': { color: '#c5221f' },
'@font-face': {
'font-family': 'Inter',
src: 'url("https://example.com/inter.woff2")',
},
},
});Media queries inside
cssare computed against the iframe's width, not the page's.
Around the field: the classes option
classes optionYour host container <div> receives state classes from VGS that you can target with regular
page CSS. Map them to your own class names with the classes option:
vault.createCvcField('#cvc', {
classes: { invalid: 'field-invalid', focused: 'field-focused' },
});.field-focused { border-color: #1a73e8; }
.field-invalid { border-color: #c5221f; }VGS's default class names (if you don't override them) are vgs-collect-container__invalid,
__focused, __empty, __dirty, and __touched.
Masks and input shaping
Text-like fields expose VGS's chainable formatting API on the returned instance:
const ssn = vault.createTextField('#ssn', { name: 'ssn', type: 'ssn' });
ssn.mask('999-99-9999', '*'); // 9: digit, a: letter, *: alphanumeric
ssn.replacePattern('/[^0-9-]+/g'); // strip anything else as the user typesFor the full option-by-option reference, see the VGS
customization
and formatting
pages - everything there is reachable through the factories' options and returned instances.
Form state and validity
vault.getFormState() returns { isValid, fields }. vault.subscribe(listener) fires on
every field-state change and returns an unsubscribe function.
const unsubscribe = vault.subscribe(() => {
const { isValid, fields } = vault.getFormState();
submitButton.disabled = !isValid;
panHint.hidden = !(fields.pan?.isDirty && !fields.pan?.isValid);
});isValid is true only when at least one field exists and every field reports valid. A
validation-free createTextField (e.g. an optional apartment line) counts as valid while
empty and won't block submission; fields with validations stay invalid until they pass.
Each entry in fields carries non-sensitive state and metadata: isValid, isDirty,
isEmpty, isFocused, isTouched, errorMessages, and for card fields last4, bin, and
cardType. Raw field values are never included.
The form state snapshot is referentially stable between changes, so it plugs directly into
React's useSyncExternalStore:
useSyncExternalStore(vault.subscribe, () => vault.getFormState())Plain (non-PCI) inputs collected with a regular
<input>- such as a billingstate
dropdown - are not part of the vault. Gate your submit onisValidand your own checks
for any plain inputs.
Submitting a payment method
For Coinme's payment-method endpoint specifically, the SDK ships a submitPaymentMethod
helper that assembles the nested request body for you. The card number, expiry, CVC, and
billing address fields are wired in by their VAULT_FIELD_NAMES; the remaining values
(customerId, billing first/last name, etc.) are plain literals you supply.
import {
submitPaymentMethod,
VAULT_FIELD_NAMES,
} from '@coinme-security/vault-sdk-js';
// Mount secure fields using VAULT_FIELD_NAMES so values line up with the request template.
vault.createCardNumberField('#pan');
vault.createExpiryField('#expiry');
vault.createCvcField('#cvc');
vault.createTextField('#address1', { name: VAULT_FIELD_NAMES.address1, validations: ['required'] });
vault.createTextField('#address2', { name: VAULT_FIELD_NAMES.address2 });
vault.createTextField('#city', { name: VAULT_FIELD_NAMES.city, validations: ['required'] });
vault.createTextField('#zipcode', { name: VAULT_FIELD_NAMES.zipcode, validations: ['required', 'postal_code/us'] });
const result = await submitPaymentMethod(vault, {
customerId: 'customer_123',
paymentProcessAssociation: 'BUY', // 'BUY' | 'SELL'
paymentProviderId: 'provider_123',
webSessionId: 'session_123', // from the Coinme Risk SDK
billingAddress: {
firstName: 'Jane',
lastName: 'Doe',
state: 'WA', // plain literal, e.g. from a regular <select>
country: 'US', // optional; defaults to 'US'
},
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer <token>',
'X-Device-Fingerprint': '<fingerprint>', // must match the one given to the Risk SDK
},
});submitPaymentMethod input fields
submitPaymentMethod input fields| Field | Type | Notes |
|---|---|---|
customerId | string | The signed-in Coinme customer the payment method belongs to. |
paymentProcessAssociation | 'BUY' | 'SELL' | Whether the card is being added for a buy or sell ramp. |
paymentProviderId | string | Payment provider identifier; sent as providerId in the request body. |
webSessionId | string | Risk SDK web session identifier (from the Coinme Risk SDK). Required - always sent in the body. |
billingAddress | object | { firstName, lastName, state, country? } - plain literals. country defaults to 'US'. |
headers | Record<string, string>? | Optional request headers, e.g. Authorization, X-Device-Fingerprint. |
timeoutMs | number? | Per-call submit timeout. Defaults to 60,000 ms. |
Every submit auto-attaches Coinme distributed-tracing headers (X-B3-TraceId, X-B3-SpanId);
pass your own values in headers to override them. Headers are per-call only - they do not
persist to later submits.
Error handling
Every failure during submit - VGS field validation, network error, HTTP error, or a Coinme
application-level error - is normalized to a CoinmeVaultError. Catch it and branch on
error.kind:
try {
await vault.submit();
} catch (error) {
if (error instanceof CoinmeVaultError) {
switch (error.kind) {
case 'validation':
// One or more fields failed VGS validation before the request was sent.
showFieldErrors(error.fields); // Record<string, string[]>
break;
case 'server':
// The request completed with a non-2xx HTTP status.
reportStatus(error.status);
break;
case 'caas':
// 2xx response, but Coinme's backend returned an application error envelope.
reportCaas(error.errorResponse);
break;
case 'network':
// The request never completed - offline, DNS/TLS failure, timeout,
// a blocked iframe, or a VGS CDN load failure.
showRetry();
break;
default:
// 'unknown' - any other unexpected failure.
showRetry();
}
}
}kind | Meaning | Extra fields |
|---|---|---|
validation | A field failed VGS validation before the request was sent. | fields - Record<string, string[]> of VGS messages |
server | The request completed with a non-2xx status. | status |
caas | 2xx response carrying a Coinme application-level error envelope. | status, errorResponse |
network | Request never completed: offline, DNS/TLS failure, timeout, blocked iframe, or CDN load failure during createCoinmeVault. | - |
unknown | Any other unexpected failure. | - |
CoinmeVaultError never contains raw card data. The original underlying error is always
available on error.cause.
Verifying your integration in sandbox
- Create the vault with
environment: 'sandbox'. - Use VGS test card values: card number
4111 1111 1111 1111, any future expiry, any
3-digit CVC. - Watch
vault.getFormState().isValidflip totrueas all fields fill with valid data. - Submit and confirm you receive either a 2xx result or a
CoinmeVaultErrorwhosekind
matches what you intentionally broke (going offline →network, a bad token →serveror
caas).
Advanced topics
These sections cover less common integration scenarios. Most integrations won't need them.
Vault configuration options
createCoinmeVault(options) resolves once at creation. To change configuration, create a
new vault (call dispose() on the old one first).
| Option | Type | Notes |
|---|---|---|
environment | 'sandbox' | 'live' | Required. Selects the vault, ingress host, and submit path. |
config | RuntimeVaultConfig? | Optional ingress and submit-path overrides. Both sandbox and live entries must be provided; environment decides which is active. |
onError | (error: unknown) => void | Optional observe-only callback for initialization failures - use for logging. Handle the returned promise rejection to recover. |
Custom ingress or submit path
To override the default ingress host or submit path, pass a config object. Both sandbox
and live entries are required; environment decides which is active. The vault ID itself
is not configurable at runtime.
const vault = await createCoinmeVault({
environment: 'sandbox',
config: {
sandbox: { ingress: 'https://secure.sandbox.example.com', path: '/your/submit/path' },
live: { ingress: 'https://secure.example.com', path: '/your/submit/path' },
},
});Ingress caveat. A VGS ingress hostname is a custom CNAME that must be registered and
activated in VGS. VGS does not validate it client-side: an unregistered hostname silently
falls back to the default vault URL rather than failing. Always verify a new ingress value
in sandbox before using it in production.
Custom request body
When your ingress expects a body shape other than the built-in payment-method one, call
vault.submit() directly. Pass a data callback - VGS invokes it at submit time withformValues, a map of each field's name to an opaque placeholder token (never the raw
value). VGS resolves the real values inside its iframes before the request leaves the
browser.
const result = await vault.submit({
data: (formValues) => ({
sensitiveData: {
cardNumber: formValues[VAULT_FIELD_NAMES.pan],
},
nonSensitiveData: 'some-literal',
}),
headers: { Authorization: `Bearer ${authToken}` },
});buildPaymentMethodRequest(input) returns the { data } callback the payment-method helper
uses internally - start from there if you need the standard body shape plus additional keys.
Raw VGS access
The field factories return raw VGS field instances, and the underlying Collect form is exposed
as vault.form. This gives you access to the full
Collect.js API,
fully typed by this SDK (VGS ships no types of its own):
import type { CollectForm, CollectFieldState } from '@coinme-security/vault-sdk-js';
vault.form.on?.('enterPress', () => submitButton.click());
const raw = vault.form.field('#custom', { type: 'zip-code', name: 'zip' });Fields created directly via vault.form.field() are not tracked by the SDK's form state or
readiness checks - prefer the factory methods unless you need something they can't express.
Security rules
These rules apply to your integration. They are enforced by the iframe model and carry
through from VGS:
- Never log or persist raw card values. Use only the state metadata surfaced by
vault.getFormState()-isValid,isDirty,last4,cardType,bin- and the parsed
response body. The iframe model keeps values out of your JavaScript; keep it that way. - Always gate submit on validity. Disable your submit button until
vault.getFormState().isValidistrue. This prevents malformed card data from ever
being sent. - Import only from the package root. Never import from internal subpaths
(@coinme-security/vault-sdk-js/core/...) - use only the public exports.
Example apps
Runnable reference implementations are included in the SDK repository:
examples/vanilla- Vite + vanilla TypeScript with the full payment-method form.examples/react- Vite + React 18 usinguseSyncExternalStore.
Both examples render a complete card + billing address form, gate submit on validity, branch
CoinmeVaultError by kind, and demonstrate the styling options. Copy-paste recipes for
individual tasks are in examples/HOWTOS.md.
Troubleshooting
401 during install - CLOUDSMITH_TEAM_TOKEN is missing or expired. Re-export it and
reinstall:
export CLOUDSMITH_TEAM_TOKEN=<your-token>
yarn installcreateCoinmeVault rejects with "can only load in a browser" - you called it during
server-side rendering. Move vault creation into client-side lifecycle code (useEffect,
onMounted, onMount). Importing the package in Node is safe; creating a vault there is
not.
createCoinmeVault rejects with "Failed to load VGS Collect.js" - the CDN request was
blocked. Check your CSP configuration and confirm
js.verygoodvault.com is not blocked by a browser extension or firewall.
Fields never render / submit throws unknown - the secure field iframes were blocked,
or the container elements didn't exist when the factory was called. Check for ad blockers or
firewall rules blocking js.verygoodvault.com, and confirm containers are in the DOM before
calling the factory methods.
Submit button never enables - isValid requires every registered field to be valid.
Check that required fields carry validations: ['required'], and that any plain <input>
values are validated separately - they don't register with the vault and don't count towardisValid.
Submits succeed but hit the wrong host - an unregistered ingress CNAME silently falls
back to the default vault URL rather than failing. Verify the ingress value in sandbox before
using it in production - see the ingress caveat.
React StrictMode shows doubled fields in development - create fields in a useEffect and
delete them in its cleanup function. See the React setup section and
examples/react in the SDK repository.
