Card payments

Accept credit and debit cards with secure, PCI-compliant input elements.

View as Markdown

PaymentKit.js renders card inputs inside isolated iframes, ensuring sensitive card data never touches your servers.

Setup

1<script src="https://unpkg.com/@payment-kit-js/vanilla/dist/cdn/paymentkit.min.js"></script>
2<script>
3 const paymentKit = PaymentKit.default({
4 environment: 'sandbox',
5 secureToken: 'aBcDeFgHiJkLmNoPqRsTuVwXyZ123456', // From your backend
6 paymentMethods: [PaymentKit.PaymentMethods.card]
7 });
8</script>

Create and mount card elements

1// Create elements
2const cardNumber = paymentKit.card.createElement('card_pan');
3const cardExpiry = paymentKit.card.createElement('card_exp');
4const cardCvc = paymentKit.card.createElement('card_cvc');
5
6// Mount to DOM containers — mount() returns a handle for unmounting later
7const mountedCardNumber = cardNumber.mount('#card-number');
8const mountedCardExpiry = cardExpiry.mount('#card-expiry');
9const mountedCardCvc = cardCvc.mount('#card-cvc');
ElementTypeDescription
Card numbercard_panCard number (13-19 digits)
Expirationcard_expMM / YY expiration date
Security codecard_cvc3 or 4-digit CVC/CVV

Unmount elements

Each mount() call returns a handle with an unmount() method. Call it when the card form is removed from the page while the rest of your checkout stays alive — for example, when a modal closes or the user switches to another payment method. Always unmount the three card elements together; they are provisioned as a set.

1mountedCardNumber.unmount();
2mountedCardExpiry.unmount();
3mountedCardCvc.unmount();

Calling unmount():

  • Removes the secure input iframes from your containers and disconnects their message channels.
  • Cancels the mount if it hasn’t finished yet, so it is safe to call immediately after mount() (for example, in a React effect cleanup).
  • Leaves the element reusable — calling mount() on it again renders a fresh input.

unmount() tears down individual inputs; it does not replace the instance-wide paymentKit.cleanup(). When you are done with the PaymentKit instance entirely (for example, navigating away from checkout), still call paymentKit.cleanup() to release remaining SDK resources such as hidden helper iframes and injected scripts. Calling cleanup() after elements have been unmounted is safe.

Element options

Pass options to createElement to customize appearance and behavior:

1const cardNumber = paymentKit.card.createElement('card_pan', {
2 style: {
3 fontSize: '16px',
4 fontFamily: 'Inter, system-ui, sans-serif',
5 color: '#1f2937'
6 },
7 placeholder: '1234 1234 1234 1234',
8 onLoaded: () => console.log('Ready'),
9 onFocusChange: (isFocused) => console.log('Focus:', isFocused)
10});
OptionTypeDescription
styleRecord<string, string>CSS properties applied to the input inside the iframe
placeholderstringPlaceholder text for the input field
onLoaded() => voidCalled when the input element is ready
onFocusChange(isFocused: boolean) => voidCalled when the input gains or loses focus

Styling

The style object applies CSS properties directly to the input element inside the iframe. For layout properties like height, border, and background, style the parent container in your HTML instead:

1<div id="card-number" style="height: 44px; padding: 12px; border: 1px solid #e5e7eb; border-radius: 8px; background-color: #ffffff;"></div>

PaymentKit.js automatically reads your container’s padding and applies it to the input inside the iframe, so text alignment and vertical centering match your container’s styling.

Submit payment

1paymentKit.submit({
2 fields: {
3 customer_name: 'Jane Smith',
4 customer_email: 'jane@example.com',
5 customer_country: 'US',
6 customer_zip_code: '94102'
7 },
8 paymentMethod: 'card',
9 // Optional submit options
10 options: { skipCustomerValidation: false },
11 onSuccess: (result) => {
12 console.log('Payment ID:', result.paymentIntentId);
13 window.location.href = '/success';
14 },
15 onError: (errors) => {
16 // Card errors: errors.card_pan, errors.card_exp, errors.card_cvc
17 // Form errors: errors.customer_name, errors.customer_email, etc.
18 // General error: errors.root
19 console.error(errors);
20 }
21});

Submit options

OptionTypeDescription
skipCustomerValidationbooleanSkips PaymentKit.js’s built-in client-side validation of the customer fields. Defaults to false (validation runs unless you pass exactly true).

By default, submit() validates the customer fields on the client before anything is sent:

  • customer_name — required, 4–40 characters
  • customer_email — required, must be a valid email format
  • customer_country — required
  • customer_zip_code — required

If any of these checks fail, onError is called with per-field errors ("required" or "invalid") and the payment is not submitted.

Set skipCustomerValidation: true when you validate customer details in your own form logic, or when the built-in rules don’t fit your data (for example, customer names shorter than 4 characters). The field values are then sent to the checkout API as-is.

skipCustomerValidation only skips the customer field checks. Card field validation (card number, expiry, CVC) always runs and cannot be skipped.

3D Secure

3D Secure (3DS) authentication is handled automatically by PaymentKit.js when required by the card issuer or payment processor.

When 3DS is required, PaymentKit.js displays a Stripe.js authentication modal. The customer completes authentication (entering a code or biometric), and PaymentKit.js verifies the result and completes the payment. No additional code is required in your submit() call.

For 3DS to work, include Stripe.js in your page: <script src="https://js.stripe.com/v3/"></script>

Error codes

Error codeFieldDescription
requiredAllField is empty
invalidAllBasic validation failed
unknown_errorAllUnexpected error during card processing
penpal_not_connectedAllCard input iframe failed to connect
missing_checkout_tokenAllCheckout token missing from request

Payment failure details: errors.checkout_response

When a card payment fails after the card details were submitted successfully — that is, the failure comes from the payment itself rather than from form validation — the error object passed to onError contains two extra keys:

  • errors.root — a customer-facing error message you can display directly (falls back to "Payment failed" when the backend provides none).
  • errors.checkout_response — the full checkout response object from the API, so you can inspect the exact failure reason programmatically.

checkout_response is present in these cases:

  • The charge was declined (checkout concluded in a payment_failed or checkout_failed state).
  • 3D Secure authentication failed and no fallback processor was available.
  • 3D Secure authentication passed, but the charge was subsequently declined.

It is not present for validation errors (the per-field codes in the table above), iframe connection errors, or when too many authentication attempts were made — so always guard for it before use.

Useful fields on checkout_response:

FieldDescription
stateCheckout state, e.g. payment_failed or checkout_failed
errorCodeMachine-readable decline/failure code
errorMessageForCustomerCustomer-facing failure message (same value used for errors.root)
errorMessageForDebugDebug-oriented message for your logs — do not show to customers
checkoutAttemptIdID of the failed checkout attempt, useful for support and log correlation
checkoutSessionIdID of the checkout session
processorUsedPayment processor that handled the attempt
cardBrand, cardLast4Card details of the attempted payment method
1onError: (errors) => {
2 if (errors.checkout_response) {
3 // Payment was attempted but failed — inspect the decline details
4 const { state, errorCode, checkoutAttemptId } = errors.checkout_response;
5 console.error(`Payment failed (${state}): ${errorCode}, attempt: ${checkoutAttemptId}`);
6 showMessage(errors.root); // customer-facing message
7 } else {
8 // Validation or setup error — check per-field codes
9 console.error(errors);
10 }
11}