Skip to main content
Elements embeds the payment inputs, wallet buttons, 3DS, QR codes, and third-party payment interactions that Clink manages into a merchant-owned page. The order summary, page structure, pay button, and status messages stay with the merchant. Use Hosted Checkout to get money moving fastest. Reach for Elements when the order summary, page structure, and interaction design have to be custom.
@clink-ai/clink-elements is currently published at 0.0.3 and the API may still change. Pin the version in package.json.

What the full chain looks like

Elements only replaces the frontend segment. Creating the Session still has to happen on the merchant server, because that call uses the Secret Key. Step 1 submits a product identifier, not a final amount computed in the browser. The amount is recalculated server-side in step 2.

Who owns what

1. Server: write a wrapper Session endpoint

This layer is not an optional optimization. It is three boundaries at once: the Secret Key security boundary, the amount validation boundary, and the local order association boundary. The clinkRequest helper below is the authenticated helper from Hosted Checkout; it returns the data field of the response.
Three things to get right:
  • uiMode is elements and returnUrl is required. Put {ELEMENTS_SESSION_ID} in the URL and Clink substitutes the real Session ID
  • The field is returnUrl. Older material calls it redirectUrl, which is wrong — redirectUrl is a response field used in requires_action flows
  • merchantReferenceId is for reconciliation only. It is not an idempotency key; the same value twice produces two Sessions
The data that clinkRequest hands back looks like this:
Clink’s raw response wraps this in a { "code": 200, "msg": "success", "data": { ... } } envelope; clinkRequest has already unwrapped it.
expireTime currently comes back as "2026-07-30 12:00:00"no timezone designator, and not RFC 3339.Do not hand it straight to new Date() or parse it as local time; different runtimes will disagree.Do not infer a “merchant account timezone” or the browser timezone either. The backend currently serializes using the service process default timezone, with no per-account conversion, so the string does not state which zone it belongs to.Precise cross-timezone handling has to wait until the backend upgrades this contract to RFC 3339 with an offset, or to a Unix timestamp. Until then, read status to decide whether a Session can still be paid rather than computing from this string.
That is an excerpt; the full field list is in Create checkout session. Two things to note:
  • The response contains no Publishable Key and no environment. Both come from the merchant application config — see the next section
  • The response does contain url, the hosted checkout address. An Elements integration does not use it
Do not forward Clink’s raw response to the browser. The wrapper endpoint should return only what the frontend actually needs — ideally just sessionId.

2. Frontend: call the wrapper endpoint, then initialize

The browser does two things: ask the merchant backend for a sessionId, then initialize the SDK with it.
PUBLIC_CLINK_PUBLISHABLE_KEY and environment are the application deployment config, taken from the Publishable Key under Developers > API Keys (it starts with pk_uat_). They are safe to expose in the browser, but they do not come from the Create Session response.
The SDK parameter is named publishKey. When this page says Publishable Key, that is the field it means — write publishKey in code.

3. Mounting, submitting, and third-party buttons

Two kinds of button can appear on a checkout page, and they behave differently: The merchant pay button — for cards and anything else where the host triggers submission. Call clink.submit() on click. SDK built-in third-party buttons — Apple Pay, Google Pay, PayPal, and similar. The paymentMethod element renders these internally and handles the click itself. When one takes over, the SDK emits submit-visible: false and the host page hides its own button.
Three things not to do:
  • Do not hardcode button visibility by payment method name. Follow submit-visible
  • Do not draw a second set of Apple Pay, Google Pay, or PayPal buttons. The SDK renders those; a hand-built copy does nothing when clicked
  • Do not call clink.submit() for third-party buttons. The SDK handles their clicks
Which third-party methods appear depends on merchant configuration, the Session, currency, browser, and device. Do not promise a fixed set on the page.
submit-enabled reports whether submission is allowed. Write disabled = !enabled rather than passing the event value straight into disabled, which inverts the logic.

Configure SDK button appearance

Starting with 0.0.3, pass sdkButtons under presetOptions to configure Apple Pay, Google Pay, Link, and PayPal buttons.
The public options are:
  • Keep button height between 40 and 55 pixels for compatibility across the current button renderers
  • A non-finite or non-positive height falls back to 45; other unsupported positive values can make the affected button fail to initialize instead of falling back
  • A non-finite or negative radius falls back to 6; values above half the button height are capped at half the height
  • Unsupported theme and type values fall back to their defaults
  • Every SDK button uses 100% width
  • When Apple Pay has no explicit theme, light mode uses black and dark mode uses white
  • Clink applies PayPal height and radius to a wrapper around the PayPal component rather than passing them as PayPal SDK options
Button options are read during initialization. To change them, destroy the current instance and call loadClinkElements() again.

Track SDK button availability

Listen for sdk-button-initialized before mounting paymentMethod so the first event is not missed.
The payload type is:
  • true means the button initialized and is available on the current device
  • false means the device is unsupported, the configuration is invalid, the gateway does not support it, the third-party SDK failed to load, or the button exceeded the 10-second timeout for its current initialization cycle
  • Only buttons present in the current Session are included; the payload is {} when there are no third-party buttons
Each button’s current loading cycle has a 10-second timeout. The event fires after all third-party buttons in the current round succeed, fail, or time out. A button that re-enters loading, or a change in available payment methods, starts a new waiting cycle. The event fires again after that cycle finishes, even when the result is unchanged.

4. Events

Two groups, by what they are used for. Update host UI Flow signals
session-success is not proof of payment, and neither is returnUrl. Browser events can be forged.Do this instead: on session-success, navigate to the merchant result page, and have that page query the merchant backend for the order status. Shipping, top-ups, and entitlements are decided by the backend from signature-verified webhooks and server-side queries.Verification, deduplication, and order matching are covered in Hosted Checkout.

5. Promo codes

Optional. Creating Coupons and Promotion Codes server-side is covered in Discounts and promotion codes. This section is frontend only.
The payload is { amount }, with the fields nested under amount. Writing (info) => info.enablePromotionCode yields undefined, and the promo code entry never appears. Discounts and the final amount due come from amount. Do not recompute them.

6. Error handling

0.0.3 throws three different kinds of thing, and they are handled differently.

Initialization: loadClinkElements()

It does not throw only ClinkApiError. There are at least three paths: A response body that is not valid JSON also surfaces as a native SyntaxError from await res.json(). So treat the catch parameter as unknown and always keep a fallback branch:
0.0.3 also exports SessionExpiredError, SessionCompleteError, SessionLoadError, SessionNotSupportedError, and PromoCodeError. In the shipped build those are class definitions with no throw sites at all.Do not write instanceof branches against them — the conditions never hold. Revisit once a later version actually uses them.
When an initialization error does arrive, check these. Each has its own source of truth — do not attribute all three to one endpoint:

After initialization: some errors are thrown synchronously

Not every runtime problem arrives as an event. These are synchronously thrown native Errors and need a try/catch:

Errors during the payment flow: events

Errors that belong to the payment flow itself are forwarded from the checkout iframe as events, and are not wrapped in any error class.

7. Implementation constraints

Get these wrong and the integration breaks. Everything else about layout is a free design decision.

Before shipping

  • No Secret Key and no webhook signing key in browser Network traffic or the build output
  • The browser only calls the merchant Session wrapper endpoint, never Clink’s Create Session directly
  • The backend does not trust a final amount from the browser; it recalculates from the product or cart
  • The local order, merchantReferenceId, and sessionId mapping is stored
  • The Publishable Key and environment come from application config, and are not described as Create Session response fields
  • submit-visible hides the host button when Apple Pay, Google Pay, PayPal, or similar appear
  • session-success does not trigger fulfillment; the final state comes from verified webhooks and backend queries
  • Old instances are destroyed when the Session changes or the page unmounts

Next

Hosted Checkout

The backend and webhook halves are identical to Hosted Checkout.

Go live

The checklist before switching to production.