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 css option
    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. createCoinmeVault loads VGS Collect.js from the VGS
    CDN and needs a window. 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:

RequirementDetails
Browser targetEvergreen 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_TOKENCoinme Cloudsmith entitlement token - see below.
Content-Security-PolicyIf your site sets a CSP, you must add VGS domains before the SDK will work - see CSP.

Obtaining the CLOUDSMITH_TEAM_TOKEN

The 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: true

Commit 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-js

There 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:

  • createCoinmeVault is 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.subscribe notifies on every field-state change and returns an unsubscribe
    function. Poll vault.getFormState().isValid inside 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.js headers) - 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.

FactoryPurpose
createCardNumberFieldCard number with Luhn validation, brand detection, and masking. Defaults to name pan. Add showCardIcon: true for a brand icon inside the field.
createExpiryFieldExpiry 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.
createCvcFieldSecurity code; adapts to the detected card brand (3 or 4 digits). Defaults to name cvc.
createCardHolderFieldCardholder name - for flows that need it collected securely. (The payment-method helper accepts name literals instead.) Defaults to name cardholder.
createTextFieldGeneric 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 to MM / YYYY accordingly. Override yearLength: 2 only 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

Style 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 css are computed against the iframe's width, not the page's.

Around the field: the classes option

Your 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 types

For 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 billing state
dropdown - are not part of the vault. Gate your submit on isValid and 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

FieldTypeNotes
customerIdstringThe signed-in Coinme customer the payment method belongs to.
paymentProcessAssociation'BUY' | 'SELL'Whether the card is being added for a buy or sell ramp.
paymentProviderIdstringPayment provider identifier; sent as providerId in the request body.
webSessionIdstringRisk SDK web session identifier (from the Coinme Risk SDK). Required - always sent in the body.
billingAddressobject{ firstName, lastName, state, country? } - plain literals. country defaults to 'US'.
headersRecord<string, string>?Optional request headers, e.g. Authorization, X-Device-Fingerprint.
timeoutMsnumber?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();
    }
  }
}
kindMeaningExtra fields
validationA field failed VGS validation before the request was sent.fields - Record<string, string[]> of VGS messages
serverThe request completed with a non-2xx status.status
caas2xx response carrying a Coinme application-level error envelope.status, errorResponse
networkRequest never completed: offline, DNS/TLS failure, timeout, blocked iframe, or CDN load failure during createCoinmeVault.-
unknownAny other unexpected failure.-

CoinmeVaultError never contains raw card data. The original underlying error is always
available on error.cause.


Verifying your integration in sandbox

  1. Create the vault with environment: 'sandbox'.
  2. Use VGS test card values: card number 4111 1111 1111 1111, any future expiry, any
    3-digit CVC.
  3. Watch vault.getFormState().isValid flip to true as all fields fill with valid data.
  4. Submit and confirm you receive either a 2xx result or a CoinmeVaultError whose kind
    matches what you intentionally broke (going offline → network, a bad token → server or
    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).

OptionTypeNotes
environment'sandbox' | 'live'Required. Selects the vault, ingress host, and submit path.
configRuntimeVaultConfig?Optional ingress and submit-path overrides. Both sandbox and live entries must be provided; environment decides which is active.
onError(error: unknown) => voidOptional 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 with
formValues, 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().isValid is true. 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 using useSyncExternalStore.

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 install

createCoinmeVault 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 toward
isValid.

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.