Drop FRAGMENT's Payment Elements into your frontend to charge a credit or debit card. You never touch a card number, and the payment posts to your Ledger as it moves.
payin Payment Type in your Schema for card payments.createPayment from your backend to create a Payment.Payment to PaymentSession.confirm() from usePayment() to charge the card.This example collects an existing order receivable and records the processing fee.
{
"key": "card-checkout",
"chartOfAccounts": {
"defaultCurrency": {
"code": "USD"
},
"defaultConsistencyConfig": {
"totalBalanceUpdates": "strong"
},
"accounts": [
{
"type": "asset",
"key": "assets-root",
"children": [
{
"key": "payments",
"status": "active",
"payment": {
"enabled": true
}
},
{
"key": "receivable",
"template": true
}
]
},
{
"type": "expense",
"key": "expense-root",
"children": [
{
"key": "payment-fees"
}
]
}
]
},
"ledgerEntries": {
"types": []
},
"payments": {
"types": [
{
"type": "card_payin",
"typeVersion": 1,
"status": "active",
"payment": {
"amount": "{{order_amount}}",
"direction": "payin"
},
"accounting": {
"settled": {
"description": "Order {{order_id}} settled",
"lines": [
{
"key": "settlement",
"system": "payment_settlement_line",
"account": {
"path": "assets-root/payments"
},
"amount": "{{settled_amount}}"
},
{
"key": "fee",
"system": "payment_fee_line",
"account": {
"path": "assets-root/payments"
},
"amount": "-{{fragment_fee_amount}}"
},
{
"key": "receivable",
"account": {
"path": "assets-root/receivable:{{order_id}}"
},
"amount": "-{{settled_amount}}"
},
{
"key": "fee-expense",
"account": {
"path": "expense-root/payment-fees"
},
"amount": "{{fragment_fee_amount}}"
}
]
}
}
}
]
}
}The settled entry must include exactly one payment_settlement_line and one payment_fee_line, both posting to the Payment Account with the amount expressions shown above. FRAGMENT supplies settled_amount and fragment_fee_amount; do not pass them in createPayment.parameters. The other lines balance the entry.
A payin Payment Type can define an entry for each of:
initiated — when the payment is captured. Use it to mark down a receivable and mark the funds in flight. Optional.settled — when the Payment settles. Hits the Payment Account for the payment amount, net of fees. Required.Call createPayment from your backend and pass the returned Payment to your frontend.
const { createPayment } = await fragment.createPayment({
ik: orderId,
ledgerIk: 'main',
type: 'card_payin',
typeVersion: 1,
parameters: {
order_amount: '4999',
order_id: orderId,
},
});
if (createPayment.__typename !== 'CreatePaymentResult') {
throw new Error(createPayment.message);
}
return { payment: createPayment.payment };The Payment starts in needs_payment_method. No money has moved yet.
Install the frontend SDK.
npm install @fragment-dev/payment-elementsYour application supplies the customer's email through billingDetails.email.
Mount PaymentElements once for every checkout session. Provide the created Payment to PaymentSession and render the card and address elements inside it:
import {
AddressElement,
CardElement,
PaymentElements,
PaymentSession,
type Payment,
} from '@fragment-dev/payment-elements';
import { PayButton } from './PayButton';
const Checkout = ({
payment,
email,
onSuccess,
}: {
payment: Payment;
email: string;
onSuccess: () => void;
}) => (
<PaymentElements>
<PaymentSession
payment={payment}
billingDetails={{ email }}
onError={(message) => console.error('Payment failed:', message)}
onConfirmed={onSuccess}
>
<AddressElement />
<CardElement />
<PayButton />
</PaymentSession>
</PaymentElements>
);You can also pass in the address under billingDetails instead of using <AddressElement>. Include name and an address with line1 and country (a two-letter country code).
The SDK calls onConfirmed once the card has been charged. On failure, it calls onError and sets usePayment().error. The Pay button below displays that error and stays disabled until the form is ready to submit.
canConfirm becomes true when the card and required billing address fields are complete.
import { usePayment } from '@fragment-dev/payment-elements';
export const PayButton = () => {
const { confirm, canConfirm, isConfirming, isLoading, error } = usePayment();
return (
<>
<button
type="button"
disabled={isLoading || !canConfirm || isConfirming}
onClick={() => confirm()}
>
Pay
</button>
{error && <p role="alert">{error}</p>}
</>
);
};Sandbox takes the same card fields as live. Use any future expiry date and any three-digit CVC, and pick the card number for the outcome you want to exercise:
| Card number | Outcome |
|---|---|
4242 4242 4242 4242 | The payment succeeds. |
4000 0000 0000 0002 | The payment is declined. |
4000 0000 0000 0341 | The card saves, but a later charge against it is declined. |
5385 3083 6013 5181 | 3D Secure is required. Enter Checkout1! on the 3DS screen. |
A sandbox Payment moves to settled on its own, so there is no settlement control to call.
Pass a theme to PaymentElements:
<PaymentElements
theme={{
appearance: 'light',
colors: { text: 'rgb(20 20 20)', error: 'rgb(200 30 30)' },
fontFamily: 'Arial, sans-serif',
fontSize: '16px',
borderRadius: '8px',
}}
>
{/* PaymentSession */}
</PaymentElements>| Field | Values |
|---|---|
appearance | light or dark. Defaults to whether <html> has the dark class. |
colors | text, mutedText, inputBackground, inputFocusBackground, surface, border, error. |
fontFamily | A font available inside the payment iframe, with fallback families. Fonts loaded by your page are not loaded into the iframe. |
fontSize | A pixel size, such as 16px. |
borderRadius | A corner radius, such as 8px. |
Use literal colors rather than CSS custom properties: the payment iframe cannot read your page’s variables.