CanopyAPI docs

React checkout and hooks

Render checkout inside your React app, read its status from any component, and retry a failed checkout.

Install

@canopypay/react-checkout@0.3.0 includes the checkout component, useCheckout and useCheckoutStatus. It requires:

  • React and React DOM ^18.3.0 or ^19.0.0.
  • A browser bundler that supports dynamic imports.
  • Your exact frontend origin, registered as an embed origin.
npm install @canopypay/react-checkout@0.3.0

Import the stylesheet once in your app. Create the intent on your backend, then pass its ID and your publishable merchant key to the client. Keep secret keys on the server. On the React client, canopyOrigin defaults to https://www.canopypay.io.

Connect the checkout

Wrap the client in useMemo so it stays stable. Pass the hook's checkoutProps to one widget. Pass its checkout handle to any other components that show status. While your backend creates the intent, set intentId to null and the widget shows its shell. Mount it right away instead of hiding it until ready.

Checkout.tsxts
"use client";
import { useMemo } from "react";
import {
  CanopyCheckout,
  createCheckoutClient,
  useCheckout,
  useCheckoutStatus,
  type CheckoutHandle,
} from "@canopypay/react-checkout";
import "@canopypay/react-checkout/styles.css";

function DepositStatus({ checkout }: { checkout: CheckoutHandle }) {
  const status = useCheckoutStatus(checkout);
  return (
    <p aria-live="polite">
      {status.fatalError
        ? "Checkout unavailable"
        : status.paid
          ? "Payment update received. Checking your account…"
          : status.depositDetected
            ? "Deposit detected"
            : status.ready
              ? "Ready to deposit"
              : "Preparing checkout…"}
    </p>
  );
}

export function Checkout({
  merchant,
  intentId,
  refreshOrder,
}: {
  merchant: string;
  intentId: string | null;
  refreshOrder: () => void;
}) {
  const client = useMemo(() => createCheckoutClient({ merchant }), [merchant]);
  const flow = useCheckout({ client, intentId, onPaid: refreshOrder });
  return (
    <section>
      <CanopyCheckout {...flow.checkoutProps} />
      <DepositStatus checkout={flow.checkout} />
      {flow.fatalError && (
        <button type="button" onClick={() => void flow.retry().catch(() => {})}>
          Retry checkout
        </button>
      )}
    </section>
  );
}

useCheckoutStatus(checkout) reads the same in-memory state. It makes no requests and starts no timers. Live deposit and payment updates need the mounted widget. The hooks report the widget's state; they cannot run a payment without it. Render one CanopyCheckout per handle. The handle and status contain no bearer token or prepared response.

Lifecycle state

Both hooks return these fields. useCheckout also returns checkout, checkoutProps and the three controls below. Preparation, shell ready and usable checkout are separate steps.

FieldMeaning
preparingPreparation is in progress.
preparedPreparation finished and was verified. The widget may still be loading.
shellReadyThe widget shell has drawn its first frame.
readyThe widget reports that checkout is usable.
depositDetectedThe widget sent a deposit_detected event in this flow.
paidThe widget sent a paid event in this flow.
errorThe latest error, or null. This includes nonfatal errors such as a rejected theme variable.
fatalErrorcheckout_unavailable or invalid_features, or null. Theme errors are never fatal.

Prepare, retry and reset

  • prepare(): Promise<void> starts preparation before you mount, or resumes after reset(). It shares the client's in-flight work and 45-second cache. It does nothing when the intent is null. Mounting the widget with a non-null intent prepares it automatically.
  • retry(): Promise<void> clears this intent's cached preparation and errors, then starts again. Use it to recover from a failed runtime load or a widget error boundary. Cached preparations for other intents are kept.
  • reset(): void clears the flow state and unmounts the payment widget. The widget comes back on prepare(), retry(), or a new client or intent. It does not cancel a payment, revoke a session or clear the client cache.

prepare() and retry() resolve when preparation finishes, not when the widget is ready. If preparation fails, they reject with a generic checkout error. Catch the rejection and read fatalError to show recovery UI. Reset, retry, a client or intent change, and unmount all discard stale results and callbacks. Requests already sent may still finish in the background.

To load the UI code ahead of time, call preloadCheckout() and handle its rejection. It loads the manual checkout runtime without wallet discovery. Neither hook makes a network request during render or server rendering. Scope each client to one browser checkout flow. Never share one global client between server-rendered users.

Events and callbacks

onEvent receives shell_ready, ready, deposit_detected, paid and error. The typed callbacks onReady, onDepositDetected, onPaid and onError each receive the matching event object. Both the hook and the component accept them. Handle each side effect in one place so it does not run twice.

Callbacks run after state updates. Replacing a callback does not restart checkout. A callback that throws cannot break checkout or stop other callbacks. ready and shell_ready fire once per flow. A new flow starts on reset, retry, or a client or intent change. Deposit and payment events are not limited to once; each one is still delivered. Theme-variable errors set error, not fatalError.

paid and deposit_detected are for UI only. Confirm deposits and update balances from your backend order state or verified settlement webhooks.

Upgrade an integration

Update your dependency to @canopypay/react-checkout@0.3.0, then rebuild and deploy your app. Components that pass client and intentId still work. To use the hooks, pass those options to useCheckout and replace the widget's two props with {...flow.checkoutProps}. Do not pass a checkout handle together with client or intentId props.

React checkout 0.2.0 added the versioned checkout status contract. 0.3.0 keeps it and adds hooks. Canopy's hosted server already supports the contract. If you self-host, serve it before you upgrade. Your stylesheet import, feature switches and theme options work with both component forms.

The browser package is separate: @canopypay/checkout-sdk@0.7.2, with loader version 10. It uses mount() and onEvent. The React hooks are only in react-checkout. If you use the npm package, update it and rebuild to get changes. Script-tag integrations can use the rolling /v1/canopy.js or pin /v1/10/canopy.js.