Use case

Online course payments and course-access entitlement

A student buys your course, and one question decides whether the integration is correct: at what exact moment do they get access? Get it wrong by trusting the browser redirect and you either hand out the course before the money settles or you lock out a student who paid and closed the tab. The job is narrow — take payment for course access and grant the entitlement off the verified invoice.paid webhook, so access is tied to settled money and nothing else. Everything around it (your fiat catalogue price, the chains a student can pay on, refunds) hangs off that single decision.

01

The job: tie course access to settled money, not to a redirect

Selling a course is a digital-goods sale with one sharp edge: the moment you grant access, the value is delivered and largely unrecoverable. A student who can stream the lessons has the product. So the entitlement — the row in your database that says "this account owns this course" — has to be written at the moment the payment is real, and not a moment before.

The common mistake is to write that entitlement when the student lands back on your success page after paying. The browser is the wrong source of truth: a student on a phone can pay from their wallet, the transaction confirms on-chain, and the success redirect never fires because they switched apps or lost signal. Drive access off that redirect and you have a paid student staring at a locked course. Drive it off the redirect's presence in the other direction and a forged success URL grants access for free.

halfin's answer is to make the signed invoice.paid webhook the only thing that grants access. You create an invoice for the course, send the student to a hosted page that handles every wallet and network detail, and your server grants the entitlement when — and only when — it receives an HMAC-signed invoice.paid event and verifies the signature. The redirect becomes a courtesy that shows the student a nice "you're in" page; the webhook is what actually unlocks the course.

02

The flow, from buy button to granted access

The whole integration is small and maps onto things your platform already does. You are adding a payment branch and a webhook handler, not a new access-control system.

Price the course in the currency your catalogue already uses. You create the invoice with a fiat amount — the course costs what it costs in USD or EUR — and halfin computes the payable crypto amount from the live rate. That rate locks when the invoice activates and an expiry is stamped, so a student who opens the page and pays a few minutes later owes the exact amount they were shown, even if the market moves while they fish out their wallet.

Then access follows the webhook, not the tab. The student can pay and close the browser before the success redirect ever runs; the signed invoice.paid webhook still lands on your server. That event is where you write the entitlement and send the welcome email. Build the success page as a nicety and the webhook handler as the part that actually changes state.

  • Student clicks buy on a course; your server creates a fiat-anchored invoice with the order id as the idempotency key.
  • You redirect the student to the hosted checkout URL returned in the response.
  • On the hosted page the student picks a network they hold funds on, sees the exact amount, a QR code, and the deposit address; halfin watches the chain and shows live status.
  • halfin applies the per-chain confirmation threshold and credits the payment in a reorg-aware way.
  • Your server receives an HMAC-signed invoice.paid webhook — you verify the signature, then write the course entitlement and grant access.
  • The student is returned to your success page; if they never land there, the webhook has already unlocked the course.
03

Granting the entitlement off invoice.paid

The entitlement write is the heart of this integration, so it is worth being precise about the order of operations. When the invoice.paid event arrives, you do three things in sequence: verify the HMAC signature over the raw request body with your signing secret, match the invoice back to the course and the buyer, and only then create the entitlement. Skip the verification and an unverified payload is just an HTTP request anyone could have sent — acting on a forged invoice.paid would hand out the course for free.

Match the webhook to the purchase using the idempotency key you set on the create call. Passing your own order id as the idempotency key means a retried create request — a double-click, a network blip, a queue redelivery — returns the same invoice instead of minting a second one for the same purchase. When invoice.paid lands, you look up the order it belongs to and you are never deciding between two invoices fighting over one course.

Make the handler idempotent on your side too. A webhook can be delivered more than once, so granting access should be an upsert keyed on the invoice or order, not a blind insert — a redelivered invoice.paid for an entitlement you already wrote should be a no-op, not a duplicate enrollment. The table below is the set of invoice events a course platform reacts to; the exact envelope and signing scheme are documented at docs.thehalfin.com.

Webhook eventWhat happened on-chainWhat the course platform does
invoice.confirmingThe invoice went live; the rate is locked and the payment window is open.Optionally mark the purchase as awaiting payment; do not grant access yet.
invoice.paidPayment confirmed to the per-chain threshold; the billed amount settled.Verify the signature, then write the entitlement and grant course access.
invoice.underpaidA deposit arrived but fell short of the expected amount.Leave access locked; request a top-up or settle partially per your policy.
invoice.overpaidThe deposit exceeded the expected amount.Grant access; reconcile the excess and refund it if your policy says so.
invoice.expiredThe window elapsed before a sufficient payment arrived.Leave the purchase unpaid; re-issue at the current rate if the student returns.
04

Why a confirmed course payment stays bought

A card payment for a course can be reversed weeks after the student has watched the lessons. For a product that is consumed immediately, a chargeback usually means the content is gone and the money is clawed back, plus a dispute fee — and contesting it means assembling screenshots of login and viewing activity to prove a student you cannot identify well actually took the course.

A crypto payment confirmed through halfin does not work that way. Once invoice.paid fires under the chain's confirmation threshold, the payment has settled on-chain and there is no issuer-initiated reversal. The entitlement you wrote on that event is backed by money that is not going to disappear. That removes the after-the-fact-fraud category from course sales: nobody completes the course and then forces a reversal.

It also changes how you handle the legitimate case of a student who wants out. Because nothing forces a reversal on you, a refund is a deliberate action you take through the refund flow against the original invoice — for example for a student who withdraws in a cooling-off window — rather than a dispute you have to defend. The access grant and the refund are both decisions you make on your own terms, recorded against the same invoice.

05

What students pay in, and what you keep

Students buying a USD-priced course are spread across the same map as your marketing reaches, and the asset they hold is usually a stablecoin — frequently USDT on Tron or Solana, where fees are low and predictable. Because the course is fiat-anchored, the student pays the dollar-equivalent amount at the locked rate and you book the figure your catalogue already shows. A stablecoin payment barely moves between the rate lock and confirmation, so the amount feels intuitive on both sides.

What lands in your halfin balances is your decision to shape afterward. A student can also pay in a native asset like BTC, ETH, or SOL; balance conversion then folds a spread of incoming assets into the one stablecoin you keep reserves in, automatically on a policy you set or manually when you want to consolidate. The networks and assets below are the real on-chain gates — there is no placeholder option on the checkout that quietly does nothing.

NetworkNative assetStablecoins on this network
BitcoinBTC
EthereumETHUSDT (ERC-20), USDC (ERC-20)
BaseETHUSDC
ArbitrumETH
Polygonnative gas token
BNB Smart ChainBNB
TronTRXUSDT (TRC-20)
XRP LedgerXRP
SolanaSOLUSDT (SPL), USDC (SPL)
06

The create call your course platform makes

The platform side is one authenticated POST. You send the course price as a fiat amount and currency; halfin pins the payable crypto amount at activation and returns an invoice with a hosted checkout URL to redirect the student to. Amounts are strings end to end — never floats — and the currency is your fiat anchor, not a crypto code.

Pass your order id as the idempotency key so a retried request returns the same invoice instead of creating a duplicate. Then wait for the signed invoice.paid webhook before you grant access. The full request and response schema, and the webhook signing scheme, live at docs.thehalfin.com.

curl -X POST https://api.thehalfin.com/api/v1/invoices \
  -H "X-API-Key: $HALFIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "149.00",
    "fiat_currency": "USD",
    "description": "Course access — Advanced TypeScript",
    "idempotency_key": "enroll-7741"
  }'

# The fiat amount anchors the course price. The student settles in the
# crypto asset they pick on the hosted checkout page returned in the
# response. Grant course access only after you verify the signed
# invoice.paid webhook on your server. See docs.thehalfin.com for the schema.