← halfin journalMay 29, 2026 · 8 min read
Playbooks

Collecting cross-border tuition without losing the dollar

A playbook for schools and bootcamps that price in USD but get paid from forty countries — fiat-anchored invoices that don't drift, instructor payouts, and refunds that aren't chargebacks.

SB
S. BrandtSolutions
playbooks · cover

The course costs $1,800. Set that number once, at the top of the funnel, and don't let the payment step quietly renegotiate it.

A bootcamp's landing page reaches a hundred countries on the day it ships. The curriculum is identical for everyone, the price is set in one currency — almost always USD — and then the checkout step re-introduces every border the marketing erased. A student in Lagos can't complete a Visa form. A wire from Manila lands three business days later, short the correspondent-bank fee, in an amount that no longer matches what you quoted. By the time finance reconciles it, the cohort has started and nobody is sure whether seat 28 actually paid.

We've onboarded enough education businesses to know the shape of this problem cold. This is the playbook we walk them through.

The one number that must not move

The catalog says $1,800. Your revenue report says $1,800. Your refund policy says $1,800. The job of the payment rail is to make the student pay the crypto equivalent of $1,800 — not "roughly that, depending on when they got around to clicking pay."

That is the entire argument for fiat-anchored invoicing. You create the invoice in USD, not in a crypto amount:

{
  "amount_fiat": "1800.00",
  "fiat_currency": "USD",
  "deferred": true
}

Amounts go over the wire as strings, and you create this with a POST /api/v1/invoices carrying your X-API-Key — the full request schema lives at docs.thehalfin.com.

deferred: true means the rate locks when the student activates the invoice, not when you created it. This matters more than it sounds. A student in a different timezone opens your enrollment email six hours later; the market has moved; the rate they pay against is the one struck at activation, and it holds through the expiry window. The school collects what it quoted. (See invoicing for the full state model — activated, paid, underpaid, overpaid, expired.)

The mistake we see teams make is reaching for the fixed-asset shape instead — quoting 0.018 ETH because that's what $1,800 happened to be at lunchtime. Don't. The moment ETH moves, your $1,800 course is collecting $1,740 or $1,910, and your finance team is reconciling a currency you never priced in.

One thing worth saying plainly, because people get it backwards: the currency field in a fixed-asset invoice is the crypto code (BTC, ETH, USDC), never USD. USD is what fiat_currency is for. If you find yourself writing "currency": "USD", you wanted the fiat-anchored shape.

Stablecoins do most of the quiet work

A student paying USD-priced tuition in USDC or USDT is sending a dollar-denominated asset. The rate lock barely has to move, the amounts feel intuitive on both sides of the screen, and the student doesn't watch a volatile number tick while they fumble for their wallet.

In practice, most of your international tuition lands as USDT on Tron or Solana — fees there are low and predictable, which is exactly what a student paying from a region with thin card penetration wants. halfin runs real gates on the chains that matter for this: USDT on Tron (TRC-20), Ethereum, and Solana; USDC on Ethereum, Base, and Solana. Let the student pick the rail their wallet already defaults to. We wrote a whole piece on TRC-20 vs ERC-20 if you want the regional breakdown.

Grant the seat on the webhook, not the redirect

Here is the rule that saves the most support tickets: the thing that opens the course is the signed webhook, not the redirect the student sees.

A student who pays and then closes the tab before the browser redirect should still get into the cohort. A student who lands on your "thank you" page because a redirect fired — but whose payment underpaid or never confirmed — should not. The browser redirect is a UX nicety. The invoice.paid webhook is the truth.

The canonical events you care about for enrollment:

EventWhat it meansWhat your system does
invoice.confirmingRate locked, student is payingHold the seat, start the expiry clock
invoice.paidConfirmed under the chain's thresholdOpen the course, email the welcome
invoice.underpaidStudent sent less than quotedDon't grant access; surface the shortfall to support
invoice.overpaidStudent sent moreGrant access, then queue a refund of the surplus
invoice.expiredWindow closed, no paymentRelease the seat

Verify the HMAC signature on the raw body before you act on any of these. A webhook is an instruction to mutate your enrollment ledger — treat it with the same suspicion you'd treat any request to flip a paid flag. (Our webhook reliability post goes deep on why you sign raw bytes and process by event id.)

The underpaid and overpaid states are the ones that quietly earn their keep. Without them you discover the mismatch at month-end, staring at an invoice that's $40 short with no idea why seat 28 has access. With them, the shortfall lands as a defined state your back office can act on the same hour.

The same map runs in the other direction

The instructors recording lessons, the TAs grading projects, the contractor maintaining the platform — they're scattered across the same countries your students are. Paying them by international wire is the slowest, most expensive line item in the whole operation, per transaction.

  • A guest lecturer or a one-off course-design fee is a single payout — operator-reviewed, with an audit record, before the money leaves.
  • A cohort of teaching assistants, a batch of affiliate educators, a monthly contractor cycle is a mass payout — a fan-out over the single-payout API, one line per payee, each with its own idempotency_key.

That per-line idempotency key is load-bearing, and it's worth understanding what it is not: there's no magic "batch" endpoint that double-checks itself. The protection is that each line carries a key you assign. If your payroll run times out and you retry it, the keys make every line settle exactly once — the TA who was already paid in the failed attempt doesn't get paid twice. Build the run so the key is derived from something stable (the contractor id plus the pay period), not a random UUID you regenerate on retry, or you've thrown the guarantee away.

Because collection and payouts live behind the same API and dashboard, finance closes the loop in one place: tuition came in against these invoices, instructor pay went out in these batches, and balance conversion rebalanced treasury in between.

Refunds are a step, not a dispute

This is the part that changes the revenue model. A confirmed on-chain tuition payment is final — there is no card network to call, no chargeback button, no acquirer clawing the money back six weeks into the cohort after the student has consumed half the course. For a product that delivers value immediately and continuously, removing that reversal window is the whole point.

But finality cuts both ways, and a student who withdraws in your cooling-off window genuinely needs their money back. That's where refunds come in — and the design decision that matters is that a halfin refund is bound to the invoice the student already paid. It is not a free-form transfer to an address someone pasted out of a support email.

Because the refund references the paid invoice, the system already knows who paid, how much, in which asset, on which network. That context turns a risky manual wire into a controlled action:

SituationRefund against the invoice
Student withdraws in the cooling-off windowReturn the full paid amount
Student overpaid the invoiceReturn the surplus over what was quoted
Duplicate enrollmentRefund one, keep the other
Dropped after week one, partial-credit policyReturn part of the paid amount

Refunds run through operator approval and scoped permissions — the same model that governs payouts. A read-only reporting key cannot move money. A support lead can be granted exactly the permission to process refunds within policy, and every refund lands in the log with its actor, amount, destination, and the invoice it points at. At month-end, finance sees a refund attached to invoice X, not a mysterious outbound transfer nobody can map to a sale.

Where to start

If you're standing up tuition collection this quarter, the smallest viable integration is three moves:

  1. Quote in USD with a deferred fiat-anchored invoice. Let the rate lock at activation; let the student pick the chain.
  2. Drive enrollment off invoice.paid, verified by signature. The redirect is cosmetic; the webhook is the contract.
  3. Treat instructor pay and student refunds as first-class outbound flows — idempotent mass payouts for the bench, invoice-bound refunds for the cooling-off window.

Do that and the $1,800 you put on the landing page is the $1,800 that hits your books — collected from forty countries, on the rails your students already hold funds on, without a chargeback tail hanging off every enrollment.

S. Brandt, halfin solutions

↳ end of articlehalfin journal · May 29, 2026