Skip to main content

Client-side tokenization for card payments

For card payments, Pomelo offers a secure way to capture card details and return a token you can use to create charges. Card data is sent directly from the customer's browser to a secure endpoint — it never touches your server. The returned token can then be safely used in server-side operations.

This is the recommended pattern for card payments: it keeps your servers out of PCI scope while still letting you build a fully custom checkout UI.

Using the PomeloJS drop-in client

PomeloJS is a client-side library that handles tokenization. It exposes an easy-to-use API for integrating card capture into your web app, with events for success and error states.

You'll need your public API key from the app you've created on the merchant dashboard.

Setup

Include the library:

<script src="https://js.pomelopay.com/pomelo-2.0.0.min.js"></script>

Then initialise it. In a browser environment the library exposes a global PomeloJS constructor:

const pomelo = new PomeloJS('your-public-key', 'transaction-id', { debug: false });

Usage

Vanilla JavaScript example:

// Create a transaction id from your backend using your private API key,
// then pass it to the client.
const transactionId = 'transaction-id';
const client = new PomeloJS('pk_production_1234ABCDEFhbvl', transactionId);

document.body.innerHTML = `
<pp-card-payment></pp-card-payment>
`;

document.addEventListener('DOMContentLoaded', () => {
const $cardPayment = document.querySelector('pp-card-payment');

$cardPayment.addEventListener('paymentTransactionSuccess', (event) => {
console.log('Payment success:', event.detail);
});

$cardPayment.addEventListener('error', (event) => {
console.error('Payment error:', event.detail);
});

$cardPayment.addEventListener('formChanged', (event) => {
console.log('Form changed:', event.detail);
});
});

API

PomeloJS instance methods

updateOptions(opts)

Updates the component's options using a deep merge and applies the new state.

Parameters:

  • opts: Partial<Options> — options to update.
  • targetElement: HTMLElement — optional. If omitted, options are updated for all elements.
client.updateOptions({
transactionId: '<transaction id>',
optionalFields: ['cardCvv'],
});

createElement(tagName, options)

Creates a new element with the given tag and options. Throws if the tag isn't allowed. The created element is added to the DOM.

Parameters:

  • tagName: string — must be a supported tag name (see below).
  • options: Record<string, string> — optional. Attributes to set on the created element.
try {
const el = client.createElement('pp-card-payment', {
'payment-flow': 'rate',
});
document.body.appendChild(el);
} catch (error) {
console.error(error.message);
}

destroyElement($el)

Removes the element from the internal store and the DOM.

const el = document.querySelector('pp-card-payment');
if (el) client.destroyElement(el);

Options

FieldTypeDescription
fieldMaxLengthsRecord<keyof FieldIDs, number>Maximum length per card-payment field.
optionalFieldsArray<keyof FieldIDs>Fields that are visible but not required.
publicKeystringPublic API key for client-side tokenization.
transactionIdstringUnique identifier for the transaction.
debugbooleanToggle verbose logging.
demobooleanToggle demo mode.
textsRecord<string, string>Custom text strings for UI elements (see below).
allowedCardSchemesArray<PaymentCardType>Card schemes accepted for payment.
excludedFieldsArray<keyof FieldIDs>Fields excluded from the form.
cardSchemeLogosArray<PaymentCardType | string>Card-scheme logos to display (data-URI or URL).

Note: Excluding essential card fields (number, expiry) will cause transaction failures. Configure carefully.

Customisable text strings

KeyDefaultFormat placeholders
submitButtonPay
cardNameName on card
cardNumberCard Number
cardExpiryMM/YY
cardCvvCVV
cvvInfo"Where do I find my CVV2?" with platform-specific guidance
requiredErrThis field is required
allowedCardsOur platform only supports payments from {cards}{cards}
invalidCardNumberErrInvalid card number
invalidCvvErrInvalid CVV
invalidCardExpiryErrInvalid expiry date
invalidCardNameErrInvalid name
ccyRateExchangeRate: {counterCurrency} 1 = {currency} {rate}{counterCurrency}, {currency}, {rate}
rateOptionInfoThe exchange rate and any fees will be set by your issuing bank
invalidInputErrInvalid input

Allowed card schemes

american-express, diners-club, discover, elo, hiper, hipercard, jcb, maestro, mastercard, mir, unionpay, visa.

Card payment element

The <pp-card-payment> custom element renders the actual card form.

Attributes

payment-flow

The current flow of the payment process.

  • Type: string
  • One of: card_entry | rate | moto
  • Default: card_entry
<pp-card-payment payment-flow="rate"></pp-card-payment>

options

A JSON string parsed into an options object. Overrides the constructor options.

  • Type: string (JSON)
<pp-card-payment options='{"transactionId":"","allowedCardSchemes":["visa","mastercard"]}'></pp-card-payment>

invalid

Whether the form is currently invalid.

  • Type: boolean
  • Default: false
<pp-card-payment invalid></pp-card-payment>

Events

The element fires CustomEvents you can listen to with addEventListener. All examples assume const $el = document.querySelector('pp-card-payment').

EventWhenevent.detail
rateOptionsLoadedRate options finish loadingRateOption[]
rateOptionChangeA rate option is selectedRateOption
formChangedThe form changesFormEventDetail
paymentTransactionSuccessA payment transaction succeeds{ paymentFlow }
formSubmittedThe form is submittedFormEventDetail
errorA payment error occurs{ error }
cardPaymentStateChangeThe card-payment state changesfull state object
cardPaymentFlowChangeThe flow changesnew flow value
threeDsCloseThe 3D-Secure modal closes{ success: boolean }
$el.addEventListener('paymentTransactionSuccess', (event) => {
console.log('Payment transaction successful:', event.detail);
});

$el.addEventListener('error', (event) => {
console.error('Payment error:', event.detail);
});

$el.addEventListener('threeDsClose', (event) => {
console.log('3D Secure modal closed:', event.detail);
});

Styling

The card-payment element is styled via CSS variables. Three groups:

Base styles

pp-card-payment {
--pp-button-background-color: #f00;
--pp-font-family: serif;
--pp-input-font-size: 32px;
--pp-input-font-weight: 900;
--pp-button-font-family: cursive;
--pp-background-color: #fff;
}

Input styles

pp-card-payment {
--pp-input-width: 100%;
--pp-input-background: #fff;
--pp-input-border-color: #d1d1d1;
--pp-input-font-size: 16px;
--pp-input-margin: 0;
--pp-input-padding: 8px;
--pp-input-height: 40px;
--pp-input-error-border-color: #ff4d4f;
--pp-input-error-border-radius: 4px;
--pp-input-border: 1px solid #d1d1d1;
--pp-input-font-family: cursive;
--pp-input-font-weight: 400;
--pp-input-text-indent: 0;
}

Button styles

pp-card-payment {
--pp-button-background-color: #007bff;
--pp-button-border-radius: 4px;
--pp-button-color: #fff;
--pp-button-font-size: 16px;
--pp-button-font-weight: 400;
--pp-button-height: 40px;
--pp-button-letter-spacing: 0.5px;
--pp-button-padding: 0 12px;
}

Deprecated v1 keys

  • cardInputInvalid

Types

Options

interface Options {
fieldMaxLengths: Record<keyof FieldIDs, number>;
optionalFields: Array<keyof FieldIDs>;
publicKey: string;
transactionId: string;
debug: boolean;
demo: boolean;
texts: Record<string, string>;
allowedCardSchemes: Array<PaymentCardType>;
excludedFields: Array<keyof FieldIDs>;
cardSchemeLogos: Array<PaymentCardType>;
}

FieldIDs

interface FieldIDs {
cardCvv: string;
cardExpiry: string;
cardNumber: string;
cardName: string;
}

PaymentCardType

type PaymentCardType =
| 'visa'
| 'mastercard'
| 'american-express'
| 'amex'
| 'diners-club'
| 'discover'
| 'jcb'
| 'unionpay'
| 'maestro'
| 'elo'
| 'mir'
| 'hiper'
| 'hipercard';

FormEventDetail

interface FormEventDetail {
values: Partial<Record<keyof FieldIDs, string>>;
cardDetails: PaymentCard;
errors: Partial<Record<keyof FieldIDs, string>>;
touched: Partial<Record<keyof FieldIDs, boolean>>;
invalid: boolean;
}

PaymentCard

interface PaymentCard {
number: string;
name: string;
expiryMonth: string;
expiryYear: string;
cvv: string;
}

RateOption

interface RateOption {
pair: string;
currency: string;
counterCurrency: string;
rate: number;
payAmount: number;
amountBeforeDiscount: number;
bankPayAmount: number;
fxSavings: number;
country: string;
}

BinLookup

interface BinLookup {
tokenId: string;
_id: string;
bin8: string;
issuer: string;
brand: string;
type: string;
country: string;
expiredAt: Date;
createdAt: Date;
updatedAt: Date;
}

PaymentFlow

type PaymentFlow = 'card_entry' | 'rate' | 'motopay';

More examples

Styling the card form

Customise the appearance of the card form via CSS variables:

.payment-form {
--pp-button-background-color: #f00;
--pp-font-family: serif;
--pp-font-size: 16px;
--pp-input-font-weight: 900;
--pp-button-font-family: cursive;
}
<head>
<script src="https://js.pomelopay.com/pomelo-2.0.0.min.js"></script>
<script>
const transactionId = 'transaction-id';
const client = new PomeloJS('pk_production_1234ABCDEFhbvl', transactionId);
</script>
</head>
<body>
<pp-card-payment class="payment-form"></pp-card-payment>
</body>

Making fields optional

Specify field ids in optionalFields:

const client = new PomeloJS(_PUB_KEY, transactionId, {
optionalFields: ['cardName'],
});

The card-name field becomes optional in the rendered form.

Restricting card schemes

Limit which schemes the form accepts via allowedCardSchemes. You can also specify which logos to display via cardSchemeLogos (in display order).

The card schemes must also be configured on your merchant account — restricting client-side won't enable a scheme that isn't enabled server-side.

const allowedCards = ['mastercard', 'visa', 'unionpay', 'jcb'];
const cardSchemeLogos = [
'mastercard',
'visa',
'unionpay',
'jcb',
'data:image/png;base64,…', // additional branding logo (data URI or valid image URL)
];

const client = new PomeloJS(_PUB_KEY, transactionId, {
allowedCardSchemes: allowedCards,
cardSchemeLogos,
});

License

PomeloJS is MIT licensed.