Skip to main content
This page assumes you have picked an integration path. If not, start with Choose an Integration. The examples use Hosted Checkout with Node.js and Express. The structure is the same in any language or framework.
The order model, Session creation, and webhook handling on this page are shared by every kind of payment — one-time, subscription, and discounted payments all build on them.The examples themselves are one-time payments. Recurring billing is in Subscriptions, and discounts are in Discounts and promotion codes.

The three endpoints you need

What to store on your own order

At minimum these fields. Without them you cannot debug anything later.
Snapshot the product at purchase time. After a price change you still need to know what this customer paid. Payment status and fulfillment status are separate columns because they go out of sync: money arrives, fulfillment fails, and you need to be able to find those orders and retry.

1. Create the order and the Session

The server can also use @clink-ai/clink-typescript-sdk, which handles the auth headers and ships type definitions. The direct-API version above is here so you can see where each field goes.
Things that bite people:
  • Read the price from your database. Never trust an amount sent by the frontend, or a customer can edit the request and buy a membership for one dollar.
  • Amounts are in the major currency unit. USD 19.99 is 19.99. Sending 1999 charges USD 1999 — a factor of 100. JPY, KRW, and IDR have no decimals and take integers only.
  • Do not multiply amounts as floats. In JavaScript 0.1 * 3 is 0.30000000000000004, and Clink checks that the line items sum exactly to the total, so that drift is rejected. Store prices as integer minor units, multiply, and convert once; use a decimal library for anything more involved.
  • merchantReferenceId is your order number. Clink does not use it for idempotency — the same value twice gives you two Sessions. Preventing duplicate orders is your job.
  • referenceCustomerId is your user ID. Send it and Clink links later Sessions for the same person to the same Clink customer.

2. The return page

The customer comes back to successUrl. This page does one thing: read your own order status and show it.
The webhook may not have arrived yet when the customer returns, so the order can still be pending. Show “confirming payment” and poll a few times rather than declaring failure. If it stays pending, your backend can call GET /checkout/session/{id} to sync the status directly.

3. Receiving webhooks

This is the part that decides whether money is real. Get it right.

Verifying the signature

Clink signs with HMAC SHA-256 over timestamp + "." + raw request body. On Node, the SDK does this for you:
Verify against the raw request body. Parsing JSON and re-serializing changes key order and whitespace, and the signature will never match. In Express that means express.raw(), not express.json().
The rule is simple enough to implement anywhere:
Three headers carry it: X-Clink-Timestamp is the timestamp, X-Clink-Signature is the signature, and X-Clink-SignType is currently always SHA256.

The full handler

Order matching, terminal-state protection, and idempotent fulfillment apply to every order.* event, not just the success branch.The common mistake is validating carefully on success and then updating straight from the order number on failure. One late order.failed is then enough to mark money you already received as failed, and reverse the fulfillment with it.
Refunds need their own branch because the payload differs: a refund.* object has no merchantReferenceId, only orderId and refundMerchantOrderId. Look up your local order by the clinkOrderId you stored at payment time.

All five of these are required

1

Verify the signature

HMAC SHA-256 over the raw body, compared against X-Clink-Signature. Return 401 on mismatch.
2

Deduplicate by event ID

Clink retries failed deliveries, so you will see the same event repeatedly. Deduplicate on event.id and return 200 for repeats.
3

Match the order on two fields

Check both merchantReferenceId and sessionId. If only one matches, treat it as an anomaly and do not update anything.
4

Tolerate out-of-order delivery

Clink does not guarantee event order. An order already paid or refunded must not be moved back to pending by an older event arriving late.
5

Return 2xx only after writing

A 200 means “handled”. Returning 200 before persisting loses the event permanently.

Which events to subscribe to

For one-time payments, at least these: Subscription products need their own set of subscription and invoice events — see Subscriptions. The full list is in the Webhook reference, and GET /webhook/events returns the currently supported event names.
Subscribe with full event names. A wildcard like subscription.* is not a value the API accepts.

Developing locally

The webhook URL must be publicly reachable over HTTPS. localhost, loopback, and private IPs are rejected. If you already have a public address — a preview deployment, a Vercel or Netlify URL, your own domain — use it. Tunnels are only for purely local development:
If QUIC fails to connect, retry with --protocol http2. Tunnel URLs change on restart, so update the webhook endpoint in the dashboard when yours does.

Testing verification without paying

A tunnel plus a real payment proves the whole chain, but paying with a card every time you tweak your verification logic is slow. Build a signed request yourself and send it locally:
Change a few fields and you can cover everything that matters:

Who owns what

Common mistakes

Fulfilling on the return page. successUrl is an ordinary address a customer can type. Only a verified webhook should trigger fulfillment. Fulfilling on every webhook without deduplication. Clink retries, so one purchase becomes three shipments. Treating pending as failure and letting the customer pay again. pending means “not known yet”. Charging again here bills the customer twice. Wait for the webhook or poll GET /order/{id}. Parsing the webhook body with express.json(). The re-serialized body produces a different signature and every event fails verification. A Secret Key in frontend code. Anyone who finds it can take payments and issue refunds on your account. The browser gets a Publishable Key only.

Next

API keys & webhooks

Key rotation, IP restrictions, delivery and retry rules.

Go live

The scenarios to verify before switching to production.