Skip to content

E. Tuition payments: collect course prepayments by card

How do you use FRAGMENT for tuition payments?

Collect tuition payments by card with FRAGMENT checkout and associate each Payment with a course enrollment. FRAGMENT records the settled prepayment and processing fee automatically. Keep tuition in a prepaid balance until the institution authorizes applying it to delivered instruction.

OperationWho handles it?
Authorize the course enrollment and collectible amountYour application
Collect a card payment from the student or sponsorFRAGMENT checkout and payments API
Post settlement and processing-fee accountingFRAGMENT, using your Payment Type
Apply funding to delivered course milestoneYour application posts tuition_payment_apply

Set up the Ledger and Payment Account

Use the Quickstart to configure your SDK and create a Ledger with the example schema. Use education-provider as its ik. Configure a Payment Account before collecting payments. The schema marks assets/payments with payment: { "enabled": true }.

  • assets
    • payments
  • liabilities
    • deposits:<id>
      • available
  • income
    • tuition:<id>
  • expenses
    • payment-fees
Chart of Accounts

Collected funds remain a liability until an authorized application entry moves them to income. Processing fees are a separate expense.

AccountWhat it tracks
assets/paymentsCard receipts less processing fees
liabilities/deposits:<enrollment_id>/availableFunds available for this record
income/tuition:<enrollment_id>Recognized income
expenses/payment-feesFRAGMENT processing fees

Define the Payment Type

Add tuition_payment to payments.types in your Schema. Its payment.amount reads the server-approved amount. Add the settlement accounting below to these fields; the complete schema contains both.

Payment Type fields
↓
{
  "type": "tuition_payment",
  "typeVersion": 1,
  "status": "active",
  "payment": {
    "amount": "{{amount}}",
    "direction": "payin"
  }
}
 

Set accounting.settled.lines to these four lines. FRAGMENT supplies settled_amount and fragment_fee_amount; your application supplies enrollment_id. The student or sponsor receives credit for the gross amount. The Payment Account receives that amount less the processing fee.

Settlement lines
↓
[
  {"key": "settlement", "account": {"path": "assets/payments"}, "amount": "{{settled_amount}}", "system": "payment_settlement_line"},
  {"key": "fee", "account": {"path": "assets/payments"}, "amount": "-{{fragment_fee_amount}}", "system": "payment_fee_line"},
  {"key": "receipt", "account": {"path": "liabilities/deposits:{{enrollment_id}}/available"}, "amount": "{{settled_amount}}"},
  {"key": "fee-expense", "account": {"path": "expenses/payment-fees"}, "amount": "{{fragment_fee_amount}}"}
]
 

Create a Payment from your backend

Resolve the approved course enrollment, payer, and amount from authenticated application records. Call createPayment with a stable idempotency key. Amounts are integer strings in USD cents. Return the Payment to your frontend; keep API credentials on your server.

Create a $100 card payment
↓
const { createPayment } = await fragment.createPayment({
  ik: 'tuition_payment-1-v1',
  ledgerIk: 'education-provider',
  type: 'tuition_payment',
  typeVersion: 1,
  parameters: { enrollment_id: '1', amount: '10000' },
});
if (createPayment.__typename !== 'CreatePaymentResult') {
  throw new Error(createPayment.message);
}
return { payment: createPayment.payment };
 

Embed FRAGMENT checkout in your product

Mount Checkout inside PaymentElements from @fragment-dev/payment-elements. Pass the Payment returned by your backend and the payer's email. The Payment starts in needs_payment_method. Your Pay button calls confirm() to charge the card.

Checkout
  1. Browser
  2. Your server
  3. FRAGMENT
  4. Card rail
    1. createPayment
    2. Payment
    1. Payment
    2. confirm
    1. capture
    2. captured

Place checkout in the course enrollment screen. Use onConfirmed to update that screen. Verify the Payment's settled status on your backend before marking the receipt available.

PaymentCheckout.tsx
↓
import { Checkout, PaymentElements, type Payment } from '@fragment-dev/payment-elements';
import { PayButton } from './PayButton';

type Props = { payment: Payment; email: string; onConfirmed: () => void };

export const PaymentCheckout = ({ payment, email, onConfirmed }: Props) => (
  <PaymentElements>
    <Checkout
      payment={payment}
      billingDetails={{ email }}
      onConfirmed={onConfirmed}
      onError={(message) => console.error(message)}
    >
      <PayButton />
    </Checkout>
  </PaymentElements>
);
 

Disable the Pay button until checkout can confirm the Payment. Show errors beside the form so the customer can correct their card details.

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={() => canConfirm && confirm()}>Pay by card</button>
    {error && <p role="alert">{error}</p>}
  </>;
};
 

Inspect the allowed entry types

FRAGMENT posts settlement accounting automatically. Your application posts the separate business entry after authorization. Do not submit native settlement entries through addLedgerEntry.

Payment Settled

Records the gross card receipt and processing fee. Credits the available funds for this record.

payment_tuition_payment_settled

Funds Applied

Apply funding to delivered course milestone. The schema rejects an entry that makes available funds negative.

tuition_payment_apply

Apply collected funds after approval

Resolve the enrolled student, payer, course price, and approved installment amount before creating the Payment. After settlement, apply $70 of the $100 receipt to delivered course milestone. The entry leaves $30 available. Your accounting policy determines the recognition date. Use the AddLedgerEntry mutation with these variables.

AddLedgerEntry variables
↓
{
  "ik": "tuition_payment_apply-1-v1",
  "entry": {
    "ledger": {
      "ik": "education-provider"
    },
    "type": "tuition_payment_apply",
    "typeVersion": 1,
    "parameters": {
      "enrollment_id": "1",
      "amount": "7000"
    }
  }
}
 

Walk through the expected balances

These diagrams show a $100 card receipt followed by a $70 application, leaving $30 available. The assumed $3 fee illustrates accounting; it is not a pricing quote. FRAGMENT supplies the actual fee. Only tuition_payment_apply is posted by your application.

Step 1: payment-settled

Amount parameters, in cents: settled_amount=10000, fragment_fee_amount=300. Expected outcome: posted.

Expected result: The assets/payments balance increases from 0 to 9,700 USD cents. The available funds balance increases from 0 to 10,000 USD cents. The expenses/payment-fees balance increases from 0 to 300 USD cents.

Posted changes
payment_tuition_payment_settled
assets/payments9700
liabilities/deposits:1/available10000
expenses/payment-fees300
Accounting Equation
Assets9700
Liabilities10000
-300
Income0
Expense300
-300
Total-300−-300=0
Balances after this step
assets/payments
9700
liabilities/deposits:1/available
10000
income/tuition:1
0
expenses/payment-fees
300
Payment Settled

Step 2: funds-applied

Amount parameters, in cents: amount=7000. Expected outcome: posted.

Expected result: The available funds balance decreases from 10,000 to 3,000 USD cents. The income balance increases from 0 to 7,000 USD cents.

Posted changes
tuition_payment_apply
liabilities/deposits:1/available-7000
income/tuition:17000
Accounting Equation
Assets0
Liabilities-7000
7000
Income7000
Expense0
7000
Total7000−7000=0
Balances after this step
assets/payments
9700
liabilities/deposits:1/available
3000
income/tuition:1
7000
expenses/payment-fees
300
Funds Applied

What happens if the card payment fails?

Store the Payment identifier with the course enrollment and operation key. A client callback alone does not establish settlement. Recover server-side status when the response is lost.

SituationApplication behavior
Card is declinedKeep the receipt unavailable. Let the payer correct checkout or use the documented retryPayment flow.
Request times outRecover the operation with its existing key and exact payload.
Payment remains processingDisplay its pending state and avoid creating a second collection.
Payment settlesUse the automatically posted entries; do not post a second receipt.

Enforce the application-side input contract

Authorize each application of collected funds against the business record. The balance condition prevents overuse; it does not replace business approval. Bind the payer, Ledger, record, and amount on your server. The application contract records these checks.

CheckRequired behavior
OwnershipResolve the record from the authenticated payer's permissions.
AmountValidate positive integer cents against the approved amount.
RetryReuse each operation's key and exact payload.
Concurrent requestsSerialize decisions for the same record and recover in-flight operations.

Check the education business rules

Resolve the enrolled student, payer, course price, and approved installment amount before creating the Payment. Keep those decisions in the education provider's application and pass only approved identifiers and amounts to FRAGMENT.

SituationHandling
Sponsor pays for a studentAssociate the receipt with the student's enrollment while retaining the authorized payer identity in your application.
Installment scheduleCreate a separate Payment and stable operation key for each approved installment.
Withdrawal or deferralUpdate the enrollment decision before applying prepaid funds. A card receipt does not determine academic eligibility.

Test the checkout and accounting flow

Use a sandbox Ledger and the documented test cards. Compare actual entries with the illustrative snapshots. The snapshots do not execute checkout or prove settlement.

  1. Collect through checkout, then apply part of the settled funds after approval.
  2. Verify the Payment status, gross receipt, fee expense, and remaining balance.
  3. Retry with the same key and verify that the operation does not duplicate its financial effect.
  4. Test a decline, a lost response, and concurrent collection requests.

Sources and companion files