# Recruitment fees: collect placement invoices by card

Source: https://fragment.dev/solutions/recruitment-fees

## How do you use Fragment for recruitment fees?

Collect recruitment fees by card after the employer's placement fee becomes due. Record the approved placement invoice, then create a Fragment Payment and embed checkout. Fragment posts settlement and processing fees against the placement receivable, keeping each employer payment tied to its invoice.

| Operation | Who handles it? |
| --- | --- |
| Authorize the placement fee invoice and collectible amount | Your application |
| Collect a card payment from the employer | Fragment checkout and payments API |
| Post settlement and processing-fee accounting | Fragment, using your Payment Type |
| Record approved placement fee invoice | Your application posts `placement_invoice_approve` |

## Set up the Ledger and Payment Account

Use the [Quickstart](/guides/quickstart) to configure your SDK and create a Ledger with the [example schema](/examples/vertical-ai/staffing--independent-recruiter-marketplace/schema.json). Use `recruitment-agency` as its `ik`. Configure a Payment Account before collecting payments. The schema marks `assets/payments` with `payment: { "enabled": true }`.

- `assets/payments`
- `assets/receivables:<id>/outstanding`
- `income/recruitment-services:<id>`
- `expenses/payment-fees`

The receivable records an approved, earned invoice. Settlement reduces the outstanding amount without recognizing that income twice. Processing fees are a separate expense.

| Account | What it tracks |
| --- | --- |
| `assets/payments` | Card receipts less processing fees |
| `assets/receivables:<placement_id>/outstanding` | Uncollected invoice amount |
| `income/recruitment-services:<placement_id>` | Recognized income |
| `expenses/payment-fees` | Fragment processing fees |

## Record the approved invoice before collecting

Confirm the contractual fee trigger, employer, placement identifier, and approved amount before issuing the invoice. Post the approved invoice once before creating its Payment. The entry records the receivable and earned income. Your accounting policy determines the recognition date. Use the [AddLedgerEntry mutation](/examples/vertical-ai/AddLedgerEntry.graphql) with these variables.

```json title="AddLedgerEntry variables"
{
  "ik": "placement_invoice_approve-1-v1",
  "entry": {
    "ledger": {
      "ik": "recruitment-agency"
    },
    "type": "placement_invoice_approve",
    "typeVersion": 1,
    "parameters": {
      "placement_id": "1",
      "amount": "10000"
    }
  }
}
```

## Define the Payment Type

Add `placement_invoice` 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](/examples/vertical-ai/staffing--independent-recruiter-marketplace/schema.json) contains both.

```json title="Payment Type fields"
{
  "type": "placement_invoice",
  "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 `placement_id`. The employer receives credit for the gross amount. The Payment Account receives that amount less the processing fee.

```json title="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": "assets/receivables:{{placement_id}}/outstanding"}, "amount": "-{{settled_amount}}"},
  {"key": "fee-expense", "account": {"path": "expenses/payment-fees"}, "amount": "{{fragment_fee_amount}}"}
]
```

## Create a Payment from your backend

Resolve the approved placement fee invoice, 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.

```typescript title="Create a $100 card payment"
const { createPayment } = await fragment.createPayment({
  ik: 'placement_invoice-1-v1',
  ledgerIk: 'recruitment-agency',
  type: 'placement_invoice',
  typeVersion: 1,
  parameters: { placement_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 flow: your backend creates a Payment, your frontend renders Fragment checkout, and the customer confirms the card. [View the diagram](https://fragment.dev/solutions/recruitment-fees#embed-fragment-checkout-in-your-product).

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

```typescript title="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.

```typescript title="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_placement_invoice_settled`

Records the gross card receipt and processing fee. Reduces the outstanding receivable.

### `placement_invoice_approve`

Record approved placement fee invoice. Post this entry once before collecting the invoice.

## Walk through the expected balances

These diagrams show a $100 approved invoice followed by its $100 card payment. The assumed $3 fee illustrates accounting; it is not a pricing quote. Fragment supplies the actual fee. Only `placement_invoice_approve` is posted by your application.

<a id="step-1-invoice-approved"></a>

### Step 1: invoice-approved

[Link to this step](https://fragment.dev/solutions/recruitment-fees#step-1-invoice-approved)

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

Expected result: The assets/receivables:1/outstanding balance increases from 0 to 10,000 USD cents. The income balance increases from 0 to 10,000 USD cents.

Entry: `placement_invoice_approve`. Posted changes (USD cents):

- `assets/receivables:1/outstanding`: +10000
- `income/recruitment-services:1`: +10000

Balances after this step (USD cents):

- `assets/payments`: 0
- `assets/receivables:1/outstanding`: 10000
- `income/recruitment-services:1`: 10000
- `expenses/payment-fees`: 0

<a id="step-2-payment-settled"></a>

### Step 2: payment-settled

[Link to this step](https://fragment.dev/solutions/recruitment-fees#step-2-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 assets/receivables:1/outstanding balance decreases from 10,000 to 0 USD cents. The expenses/payment-fees balance increases from 0 to 300 USD cents.

Entry: `payment_placement_invoice_settled`. Posted changes (USD cents):

- `assets/payments`: +9700
- `assets/receivables:1/outstanding`: -10000
- `expenses/payment-fees`: +300

Balances after this step (USD cents):

- `assets/payments`: 9700
- `assets/receivables:1/outstanding`: 0
- `income/recruitment-services:1`: 10000
- `expenses/payment-fees`: 300

## What happens if the card payment fails?

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

| Situation | Application behavior |
| --- | --- |
| Card is declined | Keep the receipt unavailable. Let the payer correct checkout or use the documented `retryPayment` flow. |
| Request times out | Recover the operation with its existing key and exact payload. |
| Payment remains processing | Display its pending state and avoid creating a second collection. |
| Payment settles | Use the automatically posted entries; do not post a second receipt. |

## Enforce the application-side input contract

Serialize collection attempts and cap each payment at the approved invoice balance. Payment settlement accounting does not enforce that collection limit. Bind the payer, Ledger, record, and amount on your server. The [application contract](/examples/vertical-ai/staffing--independent-recruiter-marketplace/application-contract.json) records these checks.

| Check | Required behavior |
| --- | --- |
| Ownership | Resolve the record from the authenticated payer's permissions. |
| Amount | Validate positive integer cents against the approved amount. |
| Retry | Reuse each operation's key and exact payload. |
| Concurrent requests | Serialize decisions for the same record and recover in-flight operations. |

## Check the staffing business rules

Confirm the contractual fee trigger, employer, placement identifier, and approved amount before issuing the invoice. Keep those decisions in the recruitment agency's application and pass only approved identifiers and amounts to Fragment.

| Situation | Handling |
| --- | --- |
| Start-date change | Verify that the fee trigger still applies before collecting payment. |
| Placement guarantee | Track guarantee terms and later adjustments in the recruitment system; settlement does not resolve them. |
| Candidate privacy | Use an opaque placement identifier in Payment parameters instead of candidate documents or personal details. |

## Test the checkout and accounting flow

Use a sandbox Ledger and the [documented test cards](/guides/make-payments#testing). Compare actual entries with the [illustrative snapshots](/examples/vertical-ai/staffing--independent-recruiter-marketplace/scenarios.json). The snapshots do not execute checkout or prove settlement.

1. Post the approved invoice once, then collect it through checkout.
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

- [Make Payments: Payment Types and embedded checkout](/guides/make-payments)
- [Quickstart: SDK and Ledger setup](/guides/quickstart)
- [Posting Ledger Entries](/guides/post-ledger-entries)
- [Payment schema](/examples/vertical-ai/staffing--independent-recruiter-marketplace/schema.json)
- [Illustrative accounting results](/examples/vertical-ai/staffing--independent-recruiter-marketplace/scenarios.json)
- [Payment and Ledger Entry request variables](/examples/vertical-ai/staffing--independent-recruiter-marketplace/requests.json)
- [Application contract](/examples/vertical-ai/staffing--independent-recruiter-marketplace/application-contract.json)
