npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clink-ai/clink-js

v1.0.2

Published

Clink hosted checkout JS SDK (V1)

Readme

Clink Embedded Checkout SDK

Clink Checkout SDK:

  • redirectToCheckout: hosted checkout redirect
  • initEmbeddedCheckout: embedded checkout

Installation

# Once published
npm install @clink/js
# or pnpm add @clink/js

# Local development inside this repository
npm --prefix packages/clink-js run test
npm --prefix packages/clink-js run build

API

loadClink(publicKey, options?)

import { loadClink } from '@clink/js';

const clink = await loadClink('pk_uat_xxx', {
  checkoutEnvironment: 'sandbox',
  locale: 'zh-CN',
});
  • publicKey must match pk_test_*, pk_uat_*, or pk_prod_*
  • Prefer checkoutEnvironment so the SDK can resolve the correct Clink bootstrap endpoint automatically, or pass checkoutBaseUrl directly
  • checkoutEnvironment is optional: sandbox / production
  • checkoutBaseUrl is optional: explicitly set the checkout base URL, for example https://checkout.clinkbill.com

clink.redirectToCheckout(params) (hosted checkout redirect)

await clink.redirectToCheckout({
  sessionParam: 'sess_123#token',
  replace: false,
});

Behavior:

  • At least one of sessionParam or sessionId is required
  • If both are provided, sessionParam takes precedence
  • Redirect URL format: {checkoutBaseUrl}/pay/{encodeURIComponent(sessionParam)}
  • replace: true uses location.replace; default is location.assign

clink.initEmbeddedCheckout(options) (embedded checkout)

import { loadClink } from '@clink/js';

const clink = await loadClink('pk_uat_xxx');
// Prefer passing checkoutEnvironment or checkoutBaseUrl explicitly
// const clink = await loadClink('pk_uat_xxx', { checkoutEnvironment: 'sandbox' });

const embedded = await clink.initEmbeddedCheckout({
  fetchSession: async () => {
    const resp = await fetch('/api/checkout/session', { method: 'POST' });
    const data = await resp.json();
    return {
      checkoutUrl: data.checkoutUrl as string,
      sessionId: data.sessionId as string,
      orderId: data.orderId as string,
    };
  },
  pollStatus: async ({ orderId }) => {
    if (!orderId) return null;

    const resp = await fetch(`/api/topup/status?order_id=${orderId}`);
    const data = await resp.json();

    if (data.credited) return 'success';
    if (data.status === 'failed') return 'error';
    if (data.status === 'refunded') return 'cancelled';
    return 'pending';
  },
  onEvent(event) {
    console.log('[embedded event]', event.type, event.payload);
  },
  // default true
  autoDestroyOnComplete: true,
});

embedded.mount('#checkout');

React integration example:

import { useEffect, useRef, useState } from 'react';
import {
  CLINK_ERROR_CODES,
  ClinkError,
  loadClink,
  type EmbeddedCheckout,
} from '@clink/js';

interface CheckoutPageProps {
  publicKey: string;
}

export function CheckoutPage({ publicKey }: CheckoutPageProps) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const embeddedRef = useRef<EmbeddedCheckout | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function setup() {
      try {
        const clink = await loadClink(publicKey, {
          checkoutEnvironment: 'production',
          locale: 'en-US',
        });

        const embedded = await clink.initEmbeddedCheckout({
          fetchSession: async () => {
            const resp = await fetch('/api/checkout/session', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
            });

            if (!resp.ok) {
              throw new Error('failed to create checkout session');
            }

            const data = (await resp.json()) as {
              checkoutUrl: string;
              sessionId: string;
              orderId?: string;
            };
            return data;
          },
          pollStatus: async ({ orderId }) => {
            if (!orderId) {
              return null;
            }

            const resp = await fetch(`/api/topup/status?order_id=${orderId}`);
            if (!resp.ok) {
              return null;
            }

            const data = (await resp.json()) as {
              credited?: boolean;
              status?: string;
            };

            if (data.credited || data.status === 'paid') {
              return 'success';
            }
            if (data.status === 'failed') {
              return 'error';
            }
            if (data.status === 'refunded') {
              return 'cancelled';
            }
            return 'pending';
          },
          onEvent(event) {
            if (event.type === 'hosted_return') {
              window.location.assign('/payment/result');
              return;
            }

            if (event.type === 'complete' && event.payload?.state === 'success') {
              window.location.assign('/payment/success');
              return;
            }

            if (event.type === 'error') {
              setError('Payment failed. Please try again.');
            }
          },
        });

        if (cancelled || !containerRef.current) {
          embedded.destroy();
          return;
        }

        embedded.mount(containerRef.current);
        embeddedRef.current = embedded;
        setLoading(false);
      } catch (err) {
        if (cancelled) {
          return;
        }

        if (
          err instanceof ClinkError &&
          err.code === CLINK_ERROR_CODES.SESSION_ID_FETCH_FAILED
        ) {
          setError('Unable to create checkout session.');
        } else {
          setError('Unable to load checkout.');
        }
        setLoading(false);
      }
    }

    void setup();

    return () => {
      cancelled = true;
      embeddedRef.current?.destroy();
      embeddedRef.current = null;
    };
  }, [publicKey]);

  return (
    <div>
      {loading ? <div>Loading checkout...</div> : null}
      {error ? <div role="alert">{error}</div> : null}
      <div ref={containerRef} id="checkout" />
    </div>
  );
}

React integration recommendations:

  • Initialize loadClink() and initEmbeddedCheckout() once inside useEffect
  • Store the EmbeddedCheckout instance in a ref and call destroy() on unmount
  • Listen to complete for the business final state; listen to hosted_return for UI cleanup after merchant-defined success or cancel return pages
  • fetchSession() must return { checkoutUrl, sessionId, orderId? }, and checkoutUrl should be the url returned by your server-side createCheckoutSession()
  • By default the SDK destroys the iframe automatically after a successful complete; pass autoDestroyOnComplete: false if you need to keep it mounted
  • fetchSession() should call only your own backend; never expose secret keys in the frontend

options:

  • fetchSession: calls your backend and must return { checkoutUrl, sessionId, orderId? }
  • onEvent (optional): unified event callback
  • autoResize (optional, default true): automatically update iframe height from resize events
  • autoDestroyOnComplete (optional, default true): destroy the iframe after completion so the host page can take over the success state
  • pollStatus (optional): fallback polling for merchant-confirmed payments; the SDK polls on pollIntervalMs and emits complete automatically on terminal states
  • pollIntervalMs (optional, default 2000)

Instance API:

  • mount(container: string | HTMLElement)
  • unmount()
  • destroy()
  • on(type, handler) (returns an unsubscribe function)
  • getState() -> { mounted, destroyed }

Event types:

  • ready
  • resize
  • state_change
  • complete
  • hosted_return
  • error

Recommended mental model:

  • complete: payment terminal-state event. It comes either from the checkout page itself or from terminal-state confirmation via pollStatus. Treat this as the source of truth for payment status.
  • hosted_return: UI cleanup event after the merchant's custom successUrl / cancelUrl return page. Use it to close the iframe, return to the host page, or show your own result screen.
  • error: SDK or polling error. This does not necessarily mean the payment itself failed.

Embedded checkout error handling example

import { ClinkError, CLINK_ERROR_CODES } from '@clink/js';

try {
  const embedded = await clink.initEmbeddedCheckout({
    fetchSession: async () => ({ checkoutUrl: '', sessionId: 'sess_xxx' }),
  });
  embedded.mount('#checkout');
} catch (error) {
  if (error instanceof ClinkError) {
    if (error.code === CLINK_ERROR_CODES.INVALID_SESSION_ID) {
      console.error('Invalid checkoutUrl or sessionId');
    }
  }
}

Bootstrap environment control

The SDK supports fixing the remote bootstrap environment via environment variables:

  • CLINK_ENV=sandbox -> https://uat-api.clinkbill.com/api/sdk/bootstrap
  • CLINK_ENV=production -> https://api.clinkbill.com/api/sdk/bootstrap

checkoutEnvironment should match CLINK_ENV, using sandbox / production.

Priority order (high -> low):

  1. loadClink(..., { checkoutBaseUrl })
  2. loadClink(..., { checkoutEnvironment })
  3. CLINK_ENV

Remote bootstrap request format:

POST /api/sdk/bootstrap
X-API-Key: pk_uat_xxx / pk_prod_xxx
X-Timestamp: <unix_ms>
Accept-Language: zh-CN, en-US;q=0.9
Content-Type: application/json

Request body:

{
  "origin": "https://merchant.example.com",
  "sdkVersion": "1.0.0",
  "locale": "zh_CN"
}

Successful response:

{
  "code": 200,
  "msg": "Success",
  "data": {
    "checkoutBaseUrl": "https://checkout.clinkbill.com",
    "merchantId": "mcht_xxx",
    "merchantName": "Demo Merchant",
    "environment": "production",
    "mode": "production",
    "features": {
      "embeddedCheckout": true
    },
    "allowedParentOrigins": ["https://merchant.example.com"]
  }
}

Error handling

The SDK throws ClinkError with a code field:

import { ClinkError, CLINK_ERROR_CODES, loadClink } from '@clink/js';

try {
  const clink = await loadClink('pk_prod_xxx');
  await clink.redirectToCheckout({ sessionId: 'sess_001' });
} catch (error) {
  if (error instanceof ClinkError) {
    if (error.code === CLINK_ERROR_CODES.INVALID_PUBLIC_KEY) {
      console.error('Invalid public key format');
    }
  }
}

Common error codes:

  • INVALID_PUBLIC_KEY
  • INVALID_CHECKOUT_ENV
  • BOOTSTRAP_REQUEST_FAILED
  • INVALID_BOOTSTRAP_RESPONSE
  • INVALID_REDIRECT_PARAMS
  • INVALID_EMBEDDED_OPTIONS
  • INVALID_SESSION_ID
  • SESSION_ID_FETCH_FAILED
  • EMBEDDED_CHECKOUT_DISABLED
  • CONTAINER_NOT_FOUND
  • NOT_IN_BROWSER

Security boundaries

  • The SDK does not handle sensitive payment data such as card numbers or CVV
  • The SDK does not contain any secret-key logic
  • The SDK uses only publicKey for bootstrap requests
  • For embedded checkout, the recommended flow is for the merchant backend to create the checkout session and return checkoutUrl, sessionId, and related metadata to the frontend
  • complete only means checkout or backend-confirmed terminal state; use hosted_return for merchant return-page UI cleanup instead of payment confirmation

Integration guide

  • Use loadClink + redirectToCheckout for full-page redirects
  • Use loadClink + initEmbeddedCheckout + mount for iframe embedding
  • Listen to complete for the business terminal state
  • Listen to hosted_return for host-page UI cleanup such as hiding the iframe or returning to the merchant page

Backend contracts

Bootstrap contract: docs/sdk-bootstrap-contract.md

Embedded checkout contract: docs/sdk-embedded-backend-contract.md