***
title: Card payments
subtitle: 'Accept credit and debit cards with secure, PCI-compliant input elements.'
------------------------------------------------------------------------------------
PaymentKit.js renders card inputs inside isolated iframes, ensuring sensitive card data never touches your servers.
# Setup
```html
```
```typescript
import PaymentKit from '@payment-kit-js/vanilla';
import CardPaymentMethod from '@payment-kit-js/vanilla/payment-methods/card';
const paymentKit = PaymentKit({
environment: 'sandbox',
secureToken: 'aBcDeFgHiJkLmNoPqRsTuVwXyZ123456', // From your backend
paymentMethods: [CardPaymentMethod]
});
```
# Create and mount card elements
```typescript
// Create elements
const cardNumber = paymentKit.card.createElement('card_pan');
const cardExpiry = paymentKit.card.createElement('card_exp');
const cardCvc = paymentKit.card.createElement('card_cvc');
// Mount to DOM containers
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvc.mount('#card-cvc');
```
| Element | Type | Description |
| ------------- | ---------- | -------------------------- |
| Card number | `card_pan` | Card number (13-19 digits) |
| Expiration | `card_exp` | MM / YY expiration date |
| Security code | `card_cvc` | 3 or 4-digit CVC/CVV |
# Element options
Pass options to `createElement` to customize appearance and behavior:
```typescript
const cardNumber = paymentKit.card.createElement('card_pan', {
style: {
fontSize: '16px',
fontFamily: 'Inter, system-ui, sans-serif',
color: '#1f2937'
},
placeholder: '1234 1234 1234 1234',
onLoaded: () => console.log('Ready'),
onFocusChange: (isFocused) => console.log('Focus:', isFocused)
});
```
| Option | Type | Description |
| --------------- | ------------------------------ | ----------------------------------------------------- |
| `style` | `Record` | CSS properties applied to the input inside the iframe |
| `placeholder` | `string` | Placeholder text for the input field |
| `onLoaded` | `() => void` | Called when the input element is ready |
| `onFocusChange` | `(isFocused: boolean) => void` | Called 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:
```html
```
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
```typescript
paymentKit.submit({
fields: {
customer_name: 'Jane Smith',
customer_email: 'jane@example.com',
customer_country: 'US',
customer_zip_code: '94102'
},
paymentMethod: 'card',
onSuccess: (result) => {
console.log('Payment ID:', result.paymentIntentId);
window.location.href = '/success';
},
onError: (errors) => {
// Card errors: errors.card_pan, errors.card_exp, errors.card_cvc
// Form errors: errors.customer_name, errors.customer_email, etc.
// General error: errors.root
console.error(errors);
}
});
```
# 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: ``
# Error codes
| Error code | Field | Description |
| ------------------------ | ----- | --------------------------------------- |
| `required` | All | Field is empty |
| `invalid` | All | Basic validation failed |
| `unknown_error` | All | Unexpected error during card processing |
| `penpal_not_connected` | All | Card input iframe failed to connect |
| `missing_checkout_token` | All | Checkout token missing from request |