B Checkout#

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.

Card collection$49.99Card number4242 4242 4242 4242Expiry04 / 27CVC123Pay $49.99

a. Overview

#
  1. Declare a payin Payment Type in your Schema for card payments.
  2. Call createPayment from your backend to create a Payment.
  3. Mount the checkout form using FRAGMENT Payment Elements and pass the returned Payment to PaymentSession.
  4. Render your own Pay button and call confirm() from usePayment() to charge the card.
CheckoutBrowserYour serverFragmentCard railcreatePaymentid + clientSecretid + clientSecretconfirmPaymentcapturecaptured

b. Define the Payment Type

#

This example collects an existing order receivable and records the processing fee.

View the checkout Schema JSON
fragment/schema.jsonc
{
  "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.

c. Create the Payment

#

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.

d. Collect the card

#

Install the frontend SDK.

npm install @fragment-dev/payment-elements

Your 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:

Checkout.tsx
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.

PayButton.tsx
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>}
    </>
  );
};

e. Testing

#

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 numberOutcome
4242 4242 4242 4242The payment succeeds.
4000 0000 0000 0002The payment is declined.
4000 0000 0000 0341The card saves, but a later charge against it is declined.
5385 3083 6013 51813D 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.

f. Styling and theming

#

Pass a theme to PaymentElements:

Checkout.tsx
<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>
FieldValues
appearancelight or dark. Defaults to whether <html> has the dark class.
colorstext, mutedText, inputBackground, inputFocusBackground, surface, border, error.
fontFamilyA font available inside the payment iframe, with fallback families. Fonts loaded by your page are not loaded into the iframe.
fontSizeA pixel size, such as 16px.
borderRadiusA corner radius, such as 8px.

Use literal colors rather than CSS custom properties: the payment iframe cannot read your page’s variables.