2026-09-17
How to Create Crypto Payment Links

To the person clicking it, a crypto payment link looks almost too simple: open a URL, send what it asks for, see a confirmation. What you can't see from that side is who actually receives the funds and on what schedule — and that one detail is the real difference between a payment link that's a thin wrapper around a real settlement system and one that's a pretty page in front of someone else's balance sheet.
A link generator that hides a hosted checkout, pools funds in a wallet you don't control, or turns "did this get paid" into a spreadsheet exercise isn't really solving the problem. What it should do is create a genuine on-chain payment request, show the customer clearly what to send and where, detect settlement without you having to babysit a block explorer, and tell your application about it the moment it happens. Your customer pays your address, directly. The infrastructure's job is to watch and coordinate, not to hold anything.
What a payment link is actually standing in for
A crypto payment link is a shareable URL tied to one specific payment request — an invoice, a donation, a single product, one installment of a subscription. What makes it useful isn't the URL itself; it's the charge behind it. A good link page can show the asset, the network, the exact amount, the recipient, an expiration, and live status, because all of that lives on a real object in your system, not in the URL's query string.
The chain is the actual payment rail here — the link is just the front door. That's why a link backed by a charge and a deterministic address beats a link that just happens to display a wallet address somewhere on the page: your backend gets a durable record to reconcile against whatever actually settles on-chain, instead of having to infer intent after the fact.
This is also the point where custodial processors quietly change the deal. Plenty of them will happily receive the customer's funds into an address they control, keep an internal balance, and pay you out later, in batches, on their schedule. That's a perfectly normal thing for a payments company to offer — it's just not the same product as "the money is yours the moment it lands," and it's worth knowing which one you signed up for before it matters.
Build the link from a charge, not a wallet address pasted into a page
The tempting shortcut is a static page with your wallet address and a "send USDC here" instruction. It'll technically accept money. It won't answer the questions you'll actually get asked: which order was this for, did the amount match, which network did it come in on, did it arrive before the deadline. A wallet address has no memory. A charge does.
Create the charge on your backend first — asset, network, amount, an internal reference, whatever metadata your product needs once payment lands — and build the link around its ID:
const merchant = await klap.recipients.create({address: '0xYourBusinessWallet',label: 'merchant',})const charge = await klap.charges.create({amount: 49.0,expiresIn: 3600,idempotencyKey: `link_${order.id}`,externalRef: order.id,acceptedPayments: [{ token: 'USDC', network: 'base' }],splitRecipients: [{ recipientId: merchant.id, percent: 100, label: 'merchant' }],})const paymentLink = `${APP_URL}/pay/${charge.id}`
Your server creates the charge and decides what "paid" means for your product. Your application controls what the customer actually sees. Your wallet receives settlement, directly. The provider's job is address derivation, chain monitoring, and typed SDK primitives — not becoming a second business partner in every transaction. Passing your own idempotencyKey here matters more for a link than for a normal checkout: a payment link tends to get opened, closed, and reopened by the same person more than once, and you don't want each visit quietly minting a fresh charge.
Pick one asset and one network, on purpose
Where possible, a payment link should ask for exactly one thing: "USDC on Base," not a menu. It reads as more trustworthy, not less — ambiguity is what makes people nervous about sending crypto, not precision. Supporting several tokens and chains can genuinely help conversion with a crypto-native audience, but each one you add multiplies the number of routing, pricing, and reconciliation edge cases your team has to actually handle, not just display.
If you do support more than one, make the customer pick the network before you show them a destination, and never assume an address is safe to reuse across chains just because it looks the same. USDC on Base and USDC on Polygon are the same token in spirit and two completely different destinations in practice — a customer who sends to the right-looking address on the wrong chain isn't having a normal failed-payment experience, they're having an operational recovery problem, and sometimes an unrecoverable one.
The page around the link doesn't have to be someone else's
A hosted checkout page is often the fastest way to get a payment link working, and also the first compromise most teams end up regretting. It's fast for a demo, and then it quietly becomes a permanent part of your product surface — someone else's branding, someone else's UI constraints, a page your team can't touch without a ticket to a vendor.
None of that is required. A payment link can just open a route inside your own application, where your frontend fetches the charge and renders whatever fits the moment — a one-click wallet transaction for someone with an extension installed, a QR code and copyable address for a mobile wallet, a clean invoice view with a countdown for a business buyer paying from a treasury. This is exactly the gap @klappay/checkout-kit exists to close — the charge-to-payment-options math and the wallet plumbing come pre-built, so "build our own payment page" doesn't mean re-deriving chain IDs from scratch.
Whatever you build, be specific about status. "Processing" tells a nervous customer nothing. "Transaction detected, waiting for confirmation" or "Settled" tells them exactly where things stand, because that's genuinely the information you have — a blockchain payment has real, observable state, so there's no good reason for the UI to be vaguer than the system underneath it.
And plan for the customer who doesn't do the happy path: sends too little, sends the wrong token, or shows up after the link expired. Don't silently mark that order paid because a payment id matched. Keep the transaction reference, apply whatever policy your product needs, and make sure someone on your team can find that record without opening a chat with the provider.
Let the charge tell your backend what happened, not the browser
A frontend polling a block explorer is not a notification system, it's a workaround. Your backend should hear about settlement through server-sent events, webhooks, or both and update your own order record from there — never from a callback a browser tab happened to fire.
It's worth separating "we detected a matching transfer" from "we're confident enough to act on it." A $5 digital product and a $50,000 marketplace payout have no business sharing the same confidence threshold, even though both might technically be the same charge.confirmed event. Decide that policy once, ahead of time, rather than negotiating it live in a support thread.
When an event lands, verify its signature, make the handler idempotent by the event ID, and record the transaction hash alongside your order. Idempotent specifically because delivery is allowed to retry — your customer should never get two entitlement grants because a webhook happened to arrive twice, which is normal, expected behavior for any retrying delivery system, not a bug you need to report.
Splits, when more than one party is owed
Payment links aren't limited to a single merchant wallet. Marketplaces, creator platforms, referral programs, and group fundraising all need the incoming payment divided among more than one recipient — and the wrong way to do that is collecting everything into one platform wallet and creating a payout obligation you now have to run yourself.
const seller = await klap.recipients.create({ address: sellerAddress, label: 'seller' })const platform = await klap.recipients.create({ address: platformAddress, label: 'platform' })const affiliate = await klap.recipients.create({ address: affiliateAddress, label: 'affiliate' })const charge = await klap.charges.create({amount: 100.0,expiresIn: 3600,idempotencyKey: `link_${order.id}`,externalRef: order.id,acceptedPayments: [{ token: 'USDC', network: 'base' }],splitRecipients: [{ recipientId: seller.id, percent: 85, label: 'seller' },{ recipientId: platform.id, percent: 10, label: 'platform' },{ recipientId: affiliate.id, percent: 5, label: 'affiliate' },],})
Get the splits right at the moment a link is generated — recipient addresses, percentages, everything — and treat that configuration as fixed once the link has actually been sent to someone. Changing who gets paid after a customer already has an invoice in front of them isn't a minor API update, it's the kind of thing that makes someone stop trusting the whole system.
Try to break it before someone else does
A sandbox that only ever shows you the happy path isn't really testing anything. Before a link goes anywhere near a real customer, run it through underpayment, overpayment, an expired charge, and a duplicate webhook delivery — each of those is a single call away if the SDK's sandbox is any good:
await klap.sandbox.underpay(charge.id)await klap.sandbox.overpay(charge.id, 12.5)await klap.sandbox.expire(charge.id)
If your link page, your webhook handler, and your support tooling all behave sensibly for each of those before you've ever taken a real payment, you've actually built a payment system — not just a page that looks like one until the first customer does something the demo never covered.
The best crypto payment link doesn't feel complicated, precisely because the architecture behind it isn't hiding anything. A real charge, settlement straight to the intended recipients, events you can verify instead of trust blindly, and a page your own team built and can change without asking permission. That's what turns a shareable URL into something you'd actually want to run your revenue through.
Ready to create your first payment link? Start at klappay.com/developers.