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.