@paypercut/checkout-js
v1.0.14
Published
Lightweight JavaScript SDK for Paypercut Checkout
Readme
Paypercut Checkout JavaScript SDK
A lightweight, framework-agnostic JavaScript SDK for embedding Paypercut Checkout into your web application. Works seamlessly with vanilla JavaScript, TypeScript, React, Vue, Angular, and other modern frameworks.
Table of Contents
- Features
- How It Works
- Installation
- Quick Start
- API Reference
- Events
- Form Validation for Wallet Payments
- Types
- Usage Examples
- Security
- Troubleshooting
- Performance Optimization
- Best Practices
- FAQ
- Support
Installation
Via NPM
npm install @paypercut/checkout-jsVia Yarn
yarn add @paypercut/checkout-jsVia PNPM
pnpm add @paypercut/checkout-jsVia CDN
<!-- jsDelivr (recommended with SRI) -->
<script src="https://cdn.jsdelivr.net/npm/@paypercut/[email protected]/dist/paypercut-checkout.iife.min.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
<!-- or UNPKG -->
<script src="https://unpkg.com/@paypercut/[email protected]/dist/paypercut-checkout.iife.min.js"></script>Quick Start
This returns a checkout session ID like CHK_12345.
1. Embed the Checkout
Use the checkout ID to initialize the checkout on your frontend:
import { PaypercutCheckout } from '@paypercut/checkout-js';
// Initialize the checkout
const checkout = PaypercutCheckout({
id: 'CHK_12345', // Your checkout session ID from step 1
containerId: '#checkout' // CSS selector or HTMLElement
});
// Listen for payment events
checkout.on('success', () => {
console.log('Payment successful!');
});
checkout.on('error', () => {
console.error('Payment failed');
});
// Optional: Listen for when iframe finishes loading (useful for modal implementations)
checkout.on('loaded', () => {
console.log('Checkout loaded and ready');
});
// Render the checkout
checkout.render();That's it! The checkout is now embedded in your page.
2. Display Mode
The SDK supports one display mode:
| Mode | When to Use | Configuration |
|------|-------------|---------------|
| Embedded | Checkout is part of your page layout | Provide containerId |
API Reference
PaypercutCheckout(options)
Creates a new checkout instance.
Options
| Option | Type | Required | Default | Description |
|------------------|------|----------|---------|----------------------------------------------------------------------------------------------------------------------------|
| id | string | Yes | — | Checkout session identifier (e.g., CHK_12345) |
| containerId | string \| HTMLElement | Yes | — | CSS selector or element where iframe mounts |
| locale | string | No | 'auto' | Locale for checkout UI. Options: 'auto', 'en', 'en-GB', 'bg', 'bg-BG' |
| lang | string | No | 'auto' | Locale for checkout UI. Options: 'auto', 'en', 'en-GB', 'bg', 'bg-BG' |
| ui_mode | 'hosted' \| 'embedded' | No | 'embedded' | UI mode for checkout display |
| wallet_options | string[] | No | ['apple_pay', 'google_pay'] | Digital wallet options. Pass [] to disable all wallets |
| form_only | boolean | No | false | Show only payment form (no Pay Now button - use external button with submit()) |
| validate_form | boolean | No | false | This indicates that Google Pay/Apple Pay flow to proceed you need to confirm form validation. For EMBEDDED checkouts only) |
Examples
Basic initialization:
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout-container'
});With all options:
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout-container',
locale: 'en', // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
lang: 'en', // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
ui_mode: 'embedded', // 'hosted' | 'embedded'
wallet_options: ['apple_pay', 'google_pay'], // Can be empty array [] or contain one/both options
form_only: false, // Set true to hide Pay Now button (use external button),
validate_form: true // Set true to require form validation before wallet payment
});Disable wallet payments:
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout-container',
wallet_options: [] // No Apple Pay or Google Pay buttons
});Only Apple Pay:
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout-container',
wallet_options: ['apple_pay'] // Only Apple Pay, no Google Pay
});Form-only mode (external submit button):
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout-container',
form_only: true // No Pay Now button inside checkout
});
// Use your own button to trigger payment
document.getElementById('my-pay-button').addEventListener('click', () => {
checkout.submit();
});Instance Methods
render()
Mounts and displays the checkout iframe.
checkout.render();destroy()
Destroys the instance and cleans up all event listeners. Call this when you're done with the checkout instance.
checkout.destroy();on(event, handler)
Subscribes to checkout events. Returns an unsubscribe function.
const unsubscribe = checkout.on('success', () => {
console.log('Payment successful!');
});
// Later, to unsubscribe
unsubscribe();once(event, handler)
Subscribes to a checkout event that automatically unsubscribes after the first emission. Returns an unsubscribe function.
checkout.once('loaded', () => {
console.log('Checkout loaded - this will only fire once');
});off(event, handler)
Unsubscribes from checkout events.
const handler = () => console.log('Payment successful!');
checkout.on('success', handler);
checkout.off('success', handler);isMounted()
Returns whether the checkout is currently mounted.
if (checkout.isMounted()) {
console.log('Checkout is visible');
}Events
Subscribe to events using the on() method. You can use string event names or the SdkEvent enum.
Event Reference
| Event | Enum | Description | Payload |
|-------|------|-------------|---------|
| loaded | SdkEvent.Loaded | Checkout iframe has finished loading | void |
| success | SdkEvent.Success | Payment completed successfully | PaymentSuccessPayload |
| error | SdkEvent.Error | Terminal failure (tokenize, confirm, or 3DS) | ApiErrorPayload |
| expired | SdkEvent.Expired | Checkout session expired | void |
| threeds_started | SdkEvent.ThreeDSStarted | 3DS challenge flow started | object |
| threeds_complete | SdkEvent.ThreeDSComplete | 3DS challenge completed | object |
| threeds_canceled | SdkEvent.ThreeDSCanceled | 3DS challenge canceled by user | object |
| threeds_error | SdkEvent.ThreeDSError | 3DS challenge error | ApiErrorPayload or object |
Usage Examples
Using string event names:
const checkout = PaypercutCheckout({ id: 'CHK_12345', containerId: '#checkout' });
checkout.on('loaded', () => {
console.log('Checkout loaded');
});
checkout.on('success', (payload) => {
// PaymentSuccessPayload
console.log('Payment successful');
console.log('Card brand:', payload.payment_method.brand);
console.log('Last 4:', payload.payment_method.last4);
console.log('Expiry:', payload.payment_method.exp_month + '/' + payload.payment_method.exp_year);
});
checkout.on('error', (err) => {
console.error('Payment error:', err.code, err.message);
});
checkout.on('expired', () => {
console.warn('Checkout session expired');
});Using SdkEvent enum (recommended for TypeScript):
import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';
const checkout = PaypercutCheckout({ id: 'CHK_12345', containerId: '#checkout' });
checkout.on(SdkEvent.Loaded, () => {
console.log('Checkout loaded');
});
checkout.on(SdkEvent.Success, (payload) => {
console.log('Payment successful', payload.payment_method);
});
checkout.on(SdkEvent.Error, (err) => {
console.error('Payment error:', err.code, err.message);
});
checkout.on(SdkEvent.Expired, () => {
console.warn('Checkout session expired');
});
// 3DS events
checkout.on(SdkEvent.ThreeDSStarted, (ctx) => {
console.log('3DS challenge started');
});
checkout.on(SdkEvent.ThreeDSComplete, (payload) => {
console.log('3DS completed');
});
checkout.on(SdkEvent.ThreeDSCanceled, (payload) => {
console.log('3DS canceled by user');
});
checkout.on(SdkEvent.ThreeDSError, (err) => {
console.error('3DS error:', err);
});Success Payload
The success event returns a PaymentSuccessPayload with the payment method details:
type PaymentSuccessPayload = {
payment_method: {
brand: string; // e.g., 'visa', 'mastercard', 'amex'
last4: string; // Last 4 digits of card number
exp_month: number; // Expiration month (1-12)
exp_year: number; // Expiration year (e.g., 2030)
};
};Example:
checkout.on('success', (payload) => {
// payload:
// {
// payment_method: {
// brand: 'visa',
// last4: '4242',
// exp_month: 12,
// exp_year: 2030
// }
// }
console.log(`Paid with ${payload.payment_method.brand} ending in ${payload.payment_method.last4}`);
// Output: "Paid with visa ending in 4242"
});Error Handling
checkout.on('error', (err) => {
switch (err.code) {
case 'card_validation_error':
// err.errors[] has field-level issues. Messages are localized.
break;
case 'card_declined':
// Optional err.decline_code and user-friendly err.message
break;
case 'threeds_error':
case 'threeds_authentication_failed':
case 'threeds_canceled':
// 3DS issue. Some servers use status_code 424 with a detailed message.
break;
default:
// Other terminal errors (e.g., authentication_failed, session_expired)
break;
}
});Notes
error: the SDK forwards a normalizedApiErrorPayloadwhen provided by Hosted Checkout.threeds_error: forwardspayload.errorwhen available; otherwise the raw message data.threeds_*non-error events: payload is forwarded as-is from Hosted Checkout (shape may evolve).
Form Validation for Wallet Payments
How It Works
- User clicks Apple Pay or Google Pay button in the checkout
- SDK emits
form_validationevent to your code - You validate your form and call either:
completeFormValidation(wallet)- allows wallet to proceedfailFormValidation(wallet, errors)- blocks wallet, you show your own errors
- If validation passes, the wallet payment sheet opens
Event: form_validation
| Property | Type | Description |
|----------|------|-------------|
| checkoutId | string | The checkout session ID |
| wallet | 'apple_pay' \| 'google_pay' | Which wallet button was clicked |
Methods
completeFormValidation(wallet)
Call this when your form is valid. The wallet payment sheet will open.
checkout.completeFormValidation('google_pay');failFormValidation(wallet, errors?)
Call this when your form has errors. The wallet will not open, and you should display your own error messages.
checkout.failFormValidation('apple_pay', [
{ code: 'invalid_email', message: 'Please enter a valid email address' },
{ code: 'missing_address', message: 'Shipping address is required' }
]);Basic Example
import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout',
wallet_options: ['apple_pay', 'google_pay']
});
// Handle form validation for wallet payments
checkout.on(SdkEvent.FormValidation, ({ wallet, checkoutId }) => {
// Validate your own form fields
const email = document.getElementById('email').value;
const isEmailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
if (isEmailValid) {
// Form is valid - allow wallet to proceed
checkout.completeFormValidation(wallet);
} else {
// Form has errors - block wallet and show your errors
document.getElementById('email-error').textContent = 'Please enter a valid email';
checkout.failFormValidation(wallet, [
{ code: 'invalid_email', message: 'Please enter a valid email' }
]);
}
});
checkout.render();React Example
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance, SdkEvent } from '@paypercut/checkout-js';
export function CheckoutWithForm({ checkoutId }: { checkoutId: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const checkoutRef = useRef<CheckoutInstance | null>(null);
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
useEffect(() => {
if (!containerRef.current) return;
const checkout = PaypercutCheckout({
id: checkoutId,
containerId: containerRef.current,
ui_mode: 'embedded',
wallet_options: ['apple_pay', 'google_pay'],
validate_form: true
});
// Handle wallet form validation
checkout.on(SdkEvent.FormValidation, ({ wallet }) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
if (isValid) {
setEmailError('');
checkout.completeFormValidation(wallet);
} else {
setEmailError('Please enter a valid email address');
checkout.failFormValidation(wallet, [
{ code: 'invalid_email', message: 'Invalid email' }
]);
}
});
checkout.on('success', (payload) => {
console.log('Payment successful:', payload.payment_method);
});
checkout.render();
checkoutRef.current = checkout;
return () => checkout.destroy();
}, [checkoutId, email]);
return (
<div>
{/* Your custom form fields */}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
/>
{emailError && <span style={{ color: 'red' }}>{emailError}</span>}
</div>
{/* Checkout iframe */}
<div ref={containerRef} />
</div>
);
}Timeout Behavior
If you don't respond to the form_validation event within 10 seconds, the SDK will:
- Emit an
errorevent with codeform_validation_timeout - Block the wallet payment from starting
This ensures the checkout doesn't hang indefinitely if the handler is not implemented.
When to Use
Use form validation when you have:
- Custom email/phone fields outside the checkout iframe
- Shipping address forms that must be filled before payment
- Terms & conditions checkboxes that must be accepted
- Any other merchant-side validation requirements
If you don't need to validate anything, you can simply auto-approve:
checkout.on('form_validation', ({ wallet }) => {
// No validation needed - always allow wallet to proceed
checkout.completeFormValidation(wallet);
});Types
ApiErrorPayload
The SDK forwards the error payload provided by Hosted Checkout. Common shapes include:
// Card validation error with per-field details (messages are localized)
type CardField = 'card_number' | 'expiration_date' | 'cvc' | 'cardholder_name';
type CardValidationErrorCode =
| 'invalid_number' | 'incomplete_number'
| 'invalid_expiry' | 'incomplete_expiry'
| 'invalid_cvc' | 'incomplete_cvc'
| 'required';
type VendorValidationType = 'invalid' | 'incomplete' | 'required';
type CardValidationErrorItem = {
field: CardField;
code: CardValidationErrorCode;
message: string; // localized per current checkout locale
vendor_type: VendorValidationType;
};
type ApiErrorPayload =
| {
checkoutId: string;
code: 'card_validation_error';
message: string; // e.g., 'Card details are incomplete or invalid'
status_code: number; // typically 400
timestamp: string; // ISO8601
errors: CardValidationErrorItem[];
}
| {
code:
| 'threeds_error'
| 'threeds_authentication_failed'
| 'threeds_canceled'
| 'card_declined'
| 'authentication_failed'
| 'session_expired'
| string; // other server codes may appear, e.g. 'permission_denied'
type?: 'card_error' | string;
message?: string;
status_code?: number; // some sources use `status`
status?: number; // compatibility with some payloads
decline_code?: string; // Optional decline reason
trace_id?: string;
timestamp?: string; // ISO8601
[key: string]: any; // passthrough
};Error payload examples
{
"checkoutId": "CHK_12345",
"code": "card_validation_error",
"message": "Card details are incomplete or invalid",
"status_code": 400,
"timestamp": "2025-01-01T12:00:00.000Z",
"errors": [
{ "field": "card_number", "code": "invalid_number", "message": "Card number is invalid", "vendor_type": "invalid" },
{ "field": "expiration_date", "code": "incomplete_expiry", "message": "Incomplete date", "vendor_type": "incomplete" }
]
}{
"checkoutId": "01KA8EG3KA2A61YXX4XVD4FYPT",
"code": "permission_denied",
"message": "forbidden",
"status_code": 403,
"trace_id": "00761104aff14f33adb84d7437d7e320",
"timestamp": "2025-12-02T09:31:22.779Z"
}{
"checkoutId": "01KA8EG3KA2A61YXX4XVD4FYPT",
"code": "threeds_error",
"type": "card_error",
"message": "Card authentication failed.",
"status": 400,
"timestamp": "2025-12-02T09:31:44.250Z"
}Note: Some payloads provide
status_codewhile others providestatus. Treat them interchangeably.
Error codes explained
- authentication_failed — General authentication failure during confirm or 3DS. Usually recoverable by retrying or using another card.
- card_declined — Issuer declined the card. Optional
decline_codemay be present (e.g.,insufficient_funds). - wallet_authentication_canceled — User canceled Apple/Google Pay authentication sheet.
- wallet_not_available — Apple/Google Pay not available on the device/browser.
- card_validation_error — Client-side validation of card fields failed.
errors[]contains localized, per-field issues. - threeds_error — 3DS flow encountered an error (e.g., ACS unavailable). Often accompanied by a server message.
- threeds_authentication_failed — 3DS authentication failed (e.g., challenged and consumer failed/denied).
- threeds_canceled — Customer canceled the 3DS challenge.
- session_expired — Checkout session is no longer valid; create a new session.
- permission_denied — Server responded with permission error (e.g., session not allowed). Not enumerated above; SDK forwards unknown codes unchanged.
Notes:
- For 3DS issues, you may receive
threeds_errorwithstatus_code424 and a server-provided message. - For declines, expect
code: 'card_declined', optionaldecline_code, and a user-friendlymessage. - The validation error
messagefields are already localized based on the checkout’s locale.
3DS payloads (representative samples)
The SDK forwards whatever Hosted Checkout sends for 3DS events. Shapes may evolve.
// threeds_started
{
type: 'THREEDS_START_FLOW',
checkoutId: 'CHK_12345',
// additional context as provided by Hosted Checkout (e.g., method, acsUrl)
}
// threeds_complete
{
type: 'THREEDS_COMPLETE',
checkoutId: 'CHK_12345',
// authentication result context (e.g., status: 'Y' | 'A' | 'C' | 'D')
}
// threeds_canceled
{
type: 'THREEDS_CANCELED',
checkoutId: 'CHK_12345',
}
// threeds_error (non-terminal)
// Note: terminal failures will also be emitted on `error` with ApiErrorPayload
{
type: 'THREEDS_ERROR',
checkoutId: 'CHK_12345',
error?: ApiErrorPayload,
}Tip: Prefer subscribing with the SdkEvent enum for stronger typing.
Usage Examples
Vanilla JavaScript (CDN)
<!DOCTYPE html>
<html>
<head>
<title>Paypercut Checkout</title>
</head>
<body>
<div id="checkout"></div>
<script src="https://cdn.jsdelivr.net/npm/@paypercut/[email protected]/dist/paypercut-checkout.iife.min.js"></script>
<script>
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout',
locale: 'en',
ui_mode: 'embedded',
wallet_options: ['apple_pay', 'google_pay']
});
checkout.on('success', function(payload) {
// payload.payment_method: { brand, last4, exp_month, exp_year }
alert('Payment successful with ' + payload.payment_method.brand + ' ending in ' + payload.payment_method.last4);
});
checkout.on('error', function(error) {
alert('Payment failed: ' + error.message);
});
checkout.render();
</script>
</body>
</html>TypeScript / ESM
import { PaypercutCheckout } from '@paypercut/checkout-js';
const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout',
locale: 'auto',
ui_mode: 'embedded',
wallet_options: ['apple_pay', 'google_pay'],
form_only: false
});
checkout.on('success', (payload) => {
// payload.payment_method: { brand, last4, exp_month, exp_year }
console.log('Payment successful');
console.log(`Paid with ${payload.payment_method.brand} **** ${payload.payment_method.last4}`);
// Redirect to success page
window.location.href = '/success';
});
checkout.on('error', (error) => {
console.error('Payment error:', error);
// Show error message to user
alert(`Payment failed: ${error.message}`);
});
checkout.render();React
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js';
interface PaymentMethod {
brand: string;
last4: string;
exp_month: number;
exp_year: number;
}
export function CheckoutComponent({ checkoutId }: { checkoutId: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const checkoutRef = useRef<CheckoutInstance | null>(null);
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const checkout = PaypercutCheckout({
id: checkoutId,
containerId: containerRef.current,
locale: 'auto',
ui_mode: 'embedded',
wallet_options: ['apple_pay', 'google_pay'],
form_only: false
});
checkout.on('loaded', () => {
setStatus('idle');
console.log('Checkout loaded');
});
checkout.on('success', (payload) => {
setStatus('success');
setPaymentMethod(payload.payment_method);
console.log('Payment successful:', payload.payment_method);
});
checkout.on('error', (error) => {
setStatus('error');
console.error('Payment error:', error);
});
checkout.render();
checkoutRef.current = checkout;
return () => {
checkout.destroy();
};
}, [checkoutId]);
return (
<div>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
{status === 'loading' && <p>Processing payment...</p>}
{status === 'success' && paymentMethod && (
<p>✅ Payment successful with {paymentMethod.brand} ending in {paymentMethod.last4}!</p>
)}
{status === 'error' && <p>❌ Payment failed. Please try again.</p>}
</div>
);
}Modal-like Implementation
If you want to create a modal-like experience, you can use the loaded event to show/hide a loading overlay:
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js';
export function ModalCheckout({ checkoutId, onClose }: { checkoutId: string; onClose: () => void }) {
const containerRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!containerRef.current) return;
const checkout = PaypercutCheckout({
id: checkoutId,
containerId: containerRef.current
});
checkout.on('loaded', () => {
setIsLoading(false);
});
checkout.on('success', () => {
console.log('Payment successful');
onClose();
});
checkout.on('error', (error) => {
console.error('Payment error:', error);
});
checkout.render();
return () => {
checkout.destroy();
};
}, [checkoutId, onClose]);
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}>
<div style={{
position: 'relative',
width: '90%',
maxWidth: '500px',
height: '90%',
maxHeight: '700px',
backgroundColor: '#fff',
borderRadius: '12px',
overflow: 'hidden'
}}>
{isLoading && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
zIndex: 1
}}>
<p>Loading checkout...</p>
</div>
)}
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
<button
onClick={onClose}
style={{
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 2
}}
>
✕
</button>
</div>
</div>
);
}Container Styling
The SDK automatically resizes the iframe to match the checkout content height. To ensure proper display, style your container with max-width and max-height constraints rather than fixed dimensions.
Recommended Container Styles
.checkout-container {
/* Use max-width/max-height to constrain the container */
max-width: 450px;
max-height: 600px;
overflow: hidden;
overflow-y: auto;
scroll-behavior: smooth;
}
/* The mount point should be full width with no fixed height */
#checkout-mount {
width: 100%;
/* Height is set dynamically by SDK via resize messages */
overflow: hidden;
position: relative;
}Key Points
- Avoid fixed
heightormin-heighton the container - the SDK will resize the iframe to fit the checkout content - Use
max-heightif you want to limit the container size and enable scrolling for longer content - Use
max-widthto constrain the container width (recommended: 375px–450px for optimal checkout display) - Set
overflow-y: autoon the container if usingmax-heightto enable scrolling
Example HTML
<div class="checkout-container">
<div id="checkout-mount"></div>
</div>const checkout = PaypercutCheckout({
id: 'CHK_12345',
containerId: '#checkout-mount'
});
checkout.render();Security
Origin Validation
The SDK automatically validates all postMessage communications against the configured checkoutHost origin. This prevents unauthorized messages from being processed.
Content Security Policy
For enhanced security, configure your Content Security Policy headers:
Content-Security-Policy:
frame-src https://buy.paypercut.io OR something else;
script-src 'self' https://cdn.jsdelivr.net https://unpkg.com;
connect-src https://buy.paypercut.io OR something else;HTTPS Only
Always use HTTPS in production. The SDK is designed for secure communication over HTTPS.
Troubleshooting
Checkout not displaying
Problem: The iframe doesn't appear after calling render().
Solutions:
- Ensure the container element exists in the DOM before calling
render() - Check that the
idis a valid checkout session ID - Verify there are no CSP errors in the browser console
- Enable debug mode:
debug: trueto see detailed logs
Events not firing
Problem: Event handlers are not being called.
Solutions:
- Ensure you subscribe to events before calling
render() - Check that the checkout session is active and not expired
- Enable debug mode to see message logs
TypeScript errors
Problem: TypeScript shows type errors when using the SDK.
Solutions:
- Ensure you're importing types:
import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js' - Update to the latest version of the SDK
- Check that your
tsconfig.jsonincludes the SDK's type definitions
Performance Optimization
Preconnect to Checkout Host
Add DNS prefetch and preconnect hints to your HTML for faster loading:
<link rel="preconnect" href="https://buy.paypercut.io ">
<link rel="dns-prefetch" href="https://buy.paypercut.io ">Lazy Loading
Load the SDK only when needed to reduce initial bundle size:
async function loadCheckout() {
const { PaypercutCheckout } = await import('@paypercut/checkout-js');
return PaypercutCheckout;
}
// Load when user clicks pay button
payButton.addEventListener('click', async () => {
const PaypercutCheckout = await loadCheckout();
const checkout = PaypercutCheckout({ id: 'CHK_12345' });
checkout.render();
});Best Practices
1. Always Verify Payments on Your Backend
Never rely solely on frontend events for payment confirmation. Always verify payments using webhooks on your backend:
// ✅ Good: Use frontend events for UI updates only
checkout.on('success', () => {
// Show success message
showSuccessMessage();
// Redirect to order confirmation page
window.location.href = '/orders/confirmation';
});
// ❌ Bad: Don't grant access based on frontend events alone
checkout.on('success', () => {
// This can be manipulated by users!
grantPremiumAccess(userId); // Don't do this!
});2. Handle All Event Types
Always handle both success and error events:
checkout.on('success', () => {
// Handle success
showSuccessMessage();
});
checkout.on('error', () => {
// Show user-friendly error message
alert('Payment failed. Please try again.');
});3. Clean Up on Component Unmount
Always call destroy() when your component unmounts to prevent memory leaks:
// React example
useEffect(() => {
const checkout = PaypercutCheckout({ id: checkoutId });
checkout.render();
return () => {
checkout.destroy(); // Clean up!
};
}, []);FAQ For Internal Validation
Q: How do I handle successful payments?
Listen to the success event and redirect users or update your UI accordingly. Always verify the payment on your backend using webhooks.
Q: Can I use this with server-side rendering (SSR)?
Yes, but ensure the SDK is only initialized on the client side. For Next.js, use dynamic imports with ssr: false.
Q: What happens if the user closes the browser during payment?
Q: Can I have multiple checkouts on one page? Maybe Yes, but only one should be active at a time to avoid user confusion. If no, we destroy the previous one and create a new one.
Q: How do I handle errors?
Listen to the error event and display user-friendly error messages. Log errors for debugging.
