@verapay/verapay-js
v1.3.0
Published
Simple TypeScript SDK for Stripe payment processing
Readme
verapay-js
A simple, PCI-compliant TypeScript SDK for Stripe payment processing.
Features
- PCI Compliant: Uses Stripe.js and Elements for secure tokenization
- TypeScript First: Full TypeScript support with comprehensive type definitions
- Customizable: Flexible styling and layout options
- Validation: Built-in card validation and error handling
- 3D Secure: Automatic 3D Secure (SCA) support
- Extensible: Plugin architecture for pre/post payment hooks
- Tree-shakable: ES Module format for optimal bundle sizes
Installation
npm install @verapay/verapay-jsQuick Start
Partners should integrate once with a single publishable key and pass merchantId dynamically per transaction:
import { VerapayClient } from '@verapay/verapay-js';
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...', // partner publishable key
merchantId: currentMerchant.id, // dynamic per order
paymentData: { amount: order.total, currency: 'gbp' },
});
await verapay.mountPaymentForm('#payment-form');For single-merchant integrations, pass a fixed merchant ID in the same config.
Then process the payment when your form is submitted:
const result = await verapay.processPayment({
customerEmail: '[email protected]',
});Backend-Initialized Flow (Recommended)
For more control, create the payment intent on your server and pass the clientSecret to the SDK. This is the recommended approach for custom checkout flows.
1. Initialize with Client Secret
const verapay = new VerapayClient({
clientSecret: 'vpy_cs_test_...', // Obtained from your backend
});
await verapay.initialize();
await verapay.mountPaymentForm('#payment-form');2. Confirm Payment
// All-in-one method
const result = await verapay.processPayment({
customerEmail: '[email protected]',
});
// OR manual confirmation
const result = await verapay.confirmPayment();Processing Payments
import { VerapayClient, ValidationError, PaymentError } from '@verapay/verapay-js';
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
});If your Merchant account is associated with a Partner, include partnerFee — the fee you are taking from the payment, which you are responsible for calculating. It must not exceed the payment amount:
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp', partnerFee: 250 },
});
await verapay.mountPaymentForm('#payment-form');
try {
const result = await verapay.processPayment({
customerEmail: '[email protected]',
});
console.log('Payment succeeded:', result);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation error:', error.details.formattedMessage);
} else if (error instanceof PaymentError) {
console.error('Payment failed:', error.details.formattedMessage);
}
}Test Mode
The SDK automatically detects the environment from your API key prefix — no flags needed:
| Key prefix | Backend | Stripe key |
|---|---|---|
| vpy_pk_test_... | test-api.verapay.app | Stripe test publishable key |
| vpy_pk_live_... | api.verapay.app | Stripe live publishable key |
Legacy vpy_test_... keys remain supported for backwards compatibility but are deprecated and will be removed in the next major release.
// Test mode — inferred from the key
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
});When using a test key:
- The SDK connects to the Verapay Test environment
- Stripe is initialised with the Test publishable key — no real charges are made
- You must use Merchant IDs created in the Test environment; attempting to access a live merchant with a test key returns a
403 Forbiddenerror
Environment Mismatch
The backend strictly enforces environment isolation. If the key environment does not match the merchant or payment intent environment, the request is rejected:
try {
await verapay.initialize();
} catch (error) {
if (error instanceof PaymentError && error.details.code === 'FORBIDDEN') {
// Key environment does not match merchant environment
// Ensure your API key and merchantId are from the same environment
}
}Common cause: Using a vpy_pk_test_... key with a merchantId that was created in the live environment (or vice versa).
Going Live
To switch from test to live mode:
- Obtain a live publishable key from your Verapay dashboard (
vpy_pk_live_...) - Replace the
apiKeyin your config — no other code changes are needed:
// Before (test mode)
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'test-merchant-uuid',
paymentData: { amount: 5000, currency: 'gbp' },
});
// After (live mode)
const verapay = new VerapayClient({
apiKey: 'vpy_pk_live_...',
merchantId: 'live-merchant-uuid', // must be a live-environment merchant
paymentData: { amount: 5000, currency: 'gbp' },
});Merchant IDs are environment-specific. You must obtain the corresponding live
merchantIdfrom your Verapay dashboard. A testmerchantIdwill not work with a live key.
- Remove any
apiUrlOverridefrom your config — the correct endpoint is automatically derived from the key prefix - Ensure your backend has
STRIPE_PUBLISHABLE_KEY(live) andSANDBOX_STRIPE_PUBLISHABLE_KEY(test) both configured
Partner Integration
Partners integrate with a single publishable key and pass merchantId dynamically per transaction:
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...', // partner's publishable key
merchantId: currentMerchant.id, // set dynamically from your order logic
paymentData: { amount: order.total, currency: 'gbp' },
});Local Development
Use apiUrlOverride to point at a locally running backend:
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
apiUrlOverride: 'http://localhost:8083',
});Advanced Usage
Custom Elements Layout
Mount individual card elements for a custom layout:
<div id="card-number"></div>
<div id="card-expiry"></div>
<div id="card-cvc"></div>const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
});
await verapay.mountCardNumber('#card-number');
await verapay.mountCardExpiry('#card-expiry');
await verapay.mountCardCvc('#card-cvc');Address Collection
Mount an address element for billing or shipping:
await verapay.mountAddress('#billing-address', 'billing');
await verapay.mountAddress('#shipping-address', 'shipping');Styling the Payment Form and Address Element
When using mountPaymentForm or mountAddress, pass an appearance object to customise their look. See Stripe's Appearance API for the full set of options.
Note: Styling is split across two APIs due to a Stripe limitation.
mountPaymentFormandmountAddressuse newer Stripe elements that support theappearanceAPI.mountCardNumber,mountCardExpiry, andmountCardCvcuse older Stripe element types that predateappearanceand only support per-elementstyleconfiguration viaelementsConfig.
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#6200ee',
fontFamily: 'Inter, sans-serif',
},
},
});Styling Individual Form Elements
This section applies only when mounting individual form elements (
mountCardNumber,mountCardExpiry,mountCardCvc). It does not apply tomountPaymentForm.
The SDK ships with a default style for card fields that works out of the box — no configuration required. If you want to match your own design, pass elementsConfig to override specific properties.
Default appearance
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
// no elementsConfig needed — sensible defaults are applied automatically
});Overriding styles
Pass only the properties you want to change. Your values are merged with the defaults, so unspecified properties are preserved.
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
elementsConfig: {
style: {
base: {
color: '#1a1a1a',
fontSize: '14px',
},
complete: {
color: '#30a46c',
},
invalid: {
color: '#ff4d4f',
},
},
placeholders: {
cardNumber: '1234 1234 1234 1234',
cardExpiry: 'MM / YY',
cardCvc: 'CVC',
},
iconStyle: 'default', // 'default' | 'solid' — defaults to 'solid'
showIcon: true, // defaults to true
},
});Container styling
The SDK mounts Stripe Elements into whichever div selectors you provide. Style those containers however you like using your own CSS — there are no SDK-specific class names.
Locale
By default Stripe Elements renders in the browser's detected language. Pass locale to override this:
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
locale: 'fr',
});Use 'auto' to explicitly opt in to Stripe's automatic detection, or any IETF language tag supported by Stripe Elements (e.g. 'en', 'de', 'ja').
Custom fonts
Stripe Elements run inside sandboxed iframes and cannot access fonts loaded on your page. To use a custom font, pass it via fonts and reference it in elementsConfig.style.base.fontFamily:
const verapay = new VerapayClient({
apiKey: 'vpy_pk_test_...',
merchantId: 'your-merchant-id',
paymentData: { amount: 5000, currency: 'gbp' },
fonts: [
{ cssSrc: 'https://fonts.googleapis.com/css?family=Inter' },
],
elementsConfig: {
style: {
base: { fontFamily: 'Inter, sans-serif' },
},
},
});Error Handling
import {
VerapayError,
ValidationError,
PaymentError,
ErrorFormatter,
} from '@verapay/verapay-js';
try {
await verapay.processPayment(request);
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.details.field); // e.g., 'amount'
console.log(error.details.formattedMessage); // user-friendly message
} else if (error instanceof PaymentError) {
console.log(error.details.code); // e.g., 'CARD_DECLINED'
console.log(error.details.stripeCode); // original Stripe code
}
const userMessage = ErrorFormatter.formatForUser(error);
alert(userMessage);
}TypeScript Support
import type {
VerapayConfig,
ProcessPaymentRequest,
PaymentResult,
ElementsConfig,
CardValidation,
ErrorCode,
} from '@verapay/verapay-js';Extension Architecture
Use extensions to hook into the payment lifecycle:
import type { PaymentExtension } from '@verapay/verapay-js';
const analyticsExtension: PaymentExtension = {
name: 'analytics',
version: '1.0.0',
async beforePayment(request) {
// track payment attempt
return request;
},
async afterPayment(result) {
// track payment success
},
async onPaymentError(error) {
// track payment failure
},
};
verapay.addExtension(analyticsExtension);Testing with Stripe
Use test cards (see below) to simulate payment outcomes. Never enable this in production.
Use Stripe's test cards:
- Successful payment:
4242 4242 4242 4242 - 3D Secure required:
4000 0027 6000 3184 - Declined:
4000 0000 0000 0002
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Security & PCI Compliance
This SDK maintains PCI compliance by:
- Never touching raw card data: All card input is handled by Stripe Elements (isolated iframes)
- Tokenization: Card data is tokenized by Stripe.js before reaching your code
- No storage: SDK never stores or transmits raw card details
Your application needs to:
- Use HTTPS in production
- Follow Stripe's security best practices
API Reference
VerapayClient
Constructor
new VerapayClient(config: VerapayConfig)| Property | Type | Required | Description |
|---|---|---|---|
| apiKey | string | No* | Your Verapay publishable key (vpy_pk_test_... or vpy_pk_live_...). Required for frontend-initialized flows. |
| clientSecret | string | No* | The client secret of a Payment Intent created on your backend. Required for backend-initialized flows. |
| merchantId | string | No | Merchant ID. Required for frontend-initialized single-merchant integrations. |
| paymentData | PaymentData | No* | Amount, currency, and optional Partner fee. Required for frontend-initialized flows. |
| appearance | Appearance | No | Styles mountPaymentForm and mountAddress elements |
| locale | StripeElementLocale | No | Locale for Stripe Elements |
| fonts | StripeElementsOptions['fonts'] | No | Custom fonts for Stripe Elements |
| elementsConfig | ElementsConfig | No | Styles mountCardNumber, mountCardExpiry, and mountCardCvc elements |
| apiUrlOverride | string | No | Override the backend URL. For local development only (e.g. http://localhost:8083) — never use in production. |
PaymentData
| Property | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | Amount in the smallest currency unit (e.g., pence for GBP) |
| currency | string | Yes | ISO 4217 currency code (e.g., 'gbp') |
| partnerFee | number | No | Your fee in the smallest currency unit, taken from the payment amount. Only applicable if your Merchant account is associated with a Partner — ignored otherwise. You are responsible for calculating this value. Must not exceed amount. |
Methods
| Method | Returns | Description |
|---|---|---|
| initialize() | Promise<void> | Initialize the SDK (required when using clientSecret) |
| mountPaymentForm(selector) | Promise<void> | Mount the Stripe Payment Element |
| mountCardNumber(selector) | Promise<void> | Mount individual card number field |
| mountCardExpiry(selector) | Promise<void> | Mount individual expiry date field |
| mountCardCvc(selector) | Promise<void> | Mount individual CVC field |
| mountAddress(selector, mode) | Promise<void> | Mount address element ('billing' or 'shipping') |
| confirmPayment() | Promise<PaymentResult> | Confirm a backend-initialized payment |
| retrievePaymentIntent() | Promise<PaymentResult> | Retrieve the current status of the payment intent |
| processPayment(request) | Promise<PaymentResult> | Process the payment (works for both flow types) |
| addExtension(extension) | void | Register a payment lifecycle extension |
ProcessPaymentRequest
| Property | Type | Required | Description |
|---|---|---|---|
| customerEmail | string | Yes | Customer email address |
| description | string | No | Payment description |
| metadata | Record<string, string> | No | Arbitrary key-value metadata |
| customerFirstName | string | No | Customer first name |
| customerLastName | string | No | Customer last name |
PaymentResult
| Property | Type | Description |
|---|---|---|
| success | boolean | Whether the payment succeeded |
| status | 'succeeded' \| 'processing' \| 'failed' | Payment status |
| paymentIntentId | string | Stripe PaymentIntent ID |
| error | string | Error message if the payment failed |
License
Copyright © Lopay Ltd. All rights reserved.
This software is proprietary and confidential. Unauthorised copying, distribution, or use of this software, in whole or in part, is strictly prohibited without prior written permission from Lopay Ltd.
Support
For issues and questions, contact the Lopay engineering team.
