Fiber LogoFiber Docs
Community SDK

fiber-checkout

Community-maintained React components and hooks for Fiber invoice checkout flows

Requirements
Updated 8/5/2026
stable

fiber-checkout is a community-maintained React library for receiving invoice-based Fiber payments. It can generate an invoice, render a QR code, poll for settlement, detect expiry, and expose success or failure through a ready-made component or lower-level hooks.

Compatibility and support

fiber-checkout is a community project delivered through the Spark Program and is now in stable maintenance. Version 0.1.2 declares an optional peer dependency on @nervosnetwork/fiber-js v0.7.x, while current Fiber releases have moved beyond that API line. Test the exact package and node versions you deploy, especially when using the browser WASM backend.

Where it fits

fiber-checkout is intentionally narrower than a wallet SDK. It is a good fit for a merchant or service that needs to receive a payment and show a compact checkout state. It does not provide peer discovery, channel management, routing controls, passkey wallets, or a general node dashboard.

If the product needs those broader capabilities, use fiber-pay or the JavaScript SDK.

Understand the payment lifecycle

The default component coordinates this sequence:

Mount checkout
    |
    v
Generate a random 32-byte preimage
    |
    v
new_invoice -> show invoice text and QR code
    |
    v
get_invoice every 2 seconds
    |
    +-- Open ------> pending
    +-- Received --> processing
    +-- Paid ------> success -> onSuccess(paymentHash)
    +-- Cancelled -> failed
    +-- Expired ---> expired -> onExpired()

After a successful invoice poll, the hook also attempts get_payment to obtain a routing fee. A receiver commonly has no outgoing payment session for that hash, so this lookup is optional and failure leaves feePaid as null.

Do not fulfill an order from a browser callback

onSuccess is a client-side UI event, not a trusted receipt. Your backend must bind the order to its payment hash, expected amount, asset, and expiry, then verify settlement against a trusted Fiber Node before delivering goods or changing durable order state.

Install

npm install fiber-checkout

The package supports React 18 and 19 and ships ESM and CommonJS builds. Choose a backend based on the application architecture:

ModeUse it whenMain trade-off
HTTPS RPC proxyA web checkout talks to a server-managed Fiber NodeRecommended for production; requires a small backend route
Direct RPCLocal development against a trusted nodeExposes the node address and must never be enabled for public production clients
FiberWasmBackendThe app already owns and runs a compatible browser Fiber instanceNo remote RPC, but adds a large WASM runtime and strict version coupling

Add the checkout component

The amount is a 0x-prefixed integer in the asset's smallest unit. Use the provided conversion helper rather than writing hex literals by hand:

import {
  FiberCheckout,
  ckbToShannonHex,
  type FiberError,
} from "fiber-checkout";

export default function CheckoutPage() {
  function handleError(error: FiberError) {
    console.error(error.code, error.method, error.message);
  }

  return (
    <FiberCheckout
      amount={ckbToShannonHex(1)}
      asset="CKB"
      nodeUrl="/api/fiber-rpc"
      description="Order #1234"
      expirySeconds={900}
      qrSize={240}
      onSuccess={(paymentHash) => {
        // Update the UI, then ask your server to verify the order.
        console.log("Payment observed:", paymentHash);
      }}
      onExpired={() => console.log("Invoice expired")}
      onError={handleError}
    />
  );
}

The component owns invoice creation, polling, QR rendering, copy, retry, and terminal-state UI. Its public props in v0.1.2 are:

PropRequiredMeaning
amountYesHex integer in the selected asset's smallest unit
assetYesBuilt-in or custom asset identifier
nodeUrlYesSame-origin proxy path, HTTPS proxy URL, or development RPC URL
descriptionNoText embedded in the Fiber invoice
expirySecondsNoInvoice lifetime; defaults to 3,600 seconds
qrSizeNoQR size in pixels; defaults to 240
onSuccessNoCalled with the payment hash after Paid is observed
onExpiredNoCalled when node or client-side expiry is observed
onErrorNoReceives a typed FiberError
customAssetsNoAdditional asset definitions; see the v0.1.2 limitation below
dangerouslyAllowDirectRpcNoPermits a bare IP or localhost RPC URL for trusted development

Put a narrow RPC proxy in front of the node

For production, point nodeUrl at a same-origin server route. The proxy keeps the Fiber Node location and credentials private and makes the public RPC surface explicit:

Browser -> /api/fiber-rpc -> private Fiber Node RPC

Here is a compact Next.js App Router example:

// app/api/fiber-rpc/route.ts
import { NextRequest, NextResponse } from "next/server";

const ALLOWED_METHODS = new Set(["new_invoice", "get_invoice"]);

type RpcRequest = {
  jsonrpc: "2.0";
  id: number | string | null;
  method: string;
  params: unknown[];
};

function rpcError(id: RpcRequest["id"], code: number, message: string) {
  return NextResponse.json({
    jsonrpc: "2.0",
    id,
    error: { code, message },
  });
}

export async function POST(request: NextRequest) {
  const nodeUrl = process.env.FIBER_NODE_URL;
  if (!nodeUrl) return rpcError(null, -32603, "Proxy not configured");

  let body: Partial<RpcRequest>;
  try {
    body = await request.json();
  } catch {
    return rpcError(null, -32700, "Parse error");
  }

  if (
    body.jsonrpc !== "2.0" ||
    typeof body.method !== "string" ||
    !Array.isArray(body.params) ||
    !ALLOWED_METHODS.has(body.method)
  ) {
    return rpcError(body.id ?? null, -32600, "Request not allowed");
  }

  try {
    const response = await fetch(nodeUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(30_000),
      cache: "no-store",
    });

    return NextResponse.json(await response.json(), {
      status: response.status,
    });
  } catch {
    return rpcError(body.id ?? null, -32603, "Fiber Node unavailable");
  }
}

Set FIBER_NODE_URL only in the server environment, for example http://127.0.0.1:8227. Do not expose it through a NEXT_PUBLIC_ variable.

The component needs new_invoice and get_invoice. The hook attempts get_payment after success only to populate feePaid; because that failure is non-fatal, keep it blocked unless your custom UI really displays that value. If you allow it, validate the request shape and restrict access to payment hashes belonging to the authenticated merchant.

The method allowlist is only the beginning. A production proxy should also:

  • authenticate the merchant or checkout session;
  • validate method-specific parameters, amount, asset, description, and expiry;
  • rate-limit invoice creation and polling independently;
  • cap body size and upstream response time;
  • associate each created payment hash with the server-side order; and
  • avoid returning sensitive upstream error details to untrusted clients.

Direct RPC is for local development only

dangerouslyAllowDirectRpc bypasses the package's block on bare IP and localhost URLs. It does not make the endpoint safe, add authentication, or limit methods. Never enable it in a public production application.

A local-only setup can opt in explicitly:

<FiberCheckout
  amount={ckbToShannonHex(1)}
  asset="CKB"
  nodeUrl="http://127.0.0.1:8227"
  dangerouslyAllowDirectRpc
/>

Build a custom checkout with hooks

Use useFiberInvoice and useFiberPayment when the product owns its visual design or needs custom polling behavior:

import {
  ckbToShannonHex,
  useFiberInvoice,
  useFiberPayment,
} from "fiber-checkout";

export function CustomCheckout() {
  const invoice = useFiberInvoice({
    nodeUrl: "/api/fiber-rpc",
    amount: ckbToShannonHex(1),
    asset: "CKB",
    expirySeconds: 900,
    description: "Order #1234",
  });

  const payment = useFiberPayment({
    nodeUrl: "/api/fiber-rpc",
    paymentHash: invoice.paymentHash,
    expiresAt: invoice.expiresAt,
    pollIntervalMs: 2_000,
  });

  if (invoice.isLoading) return <p>Creating invoice…</p>;
  if (invoice.error) return <button onClick={invoice.regenerate}>Retry</button>;

  return (
    <section>
      <code>{invoice.invoiceAddress}</code>
      <p>Status: {payment.status}</p>
      {payment.feePaid && <p>Routing fee: {payment.feePaid}</p>}
      <button onClick={payment.poll}>Refresh now</button>
    </section>
  );
}

useFiberPayment stops polling after success, failed, or expired. Passing expiresAt also lets it expire the UI locally without waiting for the next node status transition.

Handle failures by category

Every library error is a FiberError with a stable code:

CodeTypical meaningSuggested response
RPC_ERRORFiber Node returned a JSON-RPC errorLog rpcCode and method; show a safe retry or support message
DIRECT_RPC_BLOCKEDA bare IP or localhost URL was used without explicit opt-inAdd a production proxy, or enable only for trusted local development
NETWORK_ERRORProxy/node could not be reachedRetry with backoff and check service health
INVALID_RESPONSEResponse was malformed or incompleteStop fulfillment and investigate proxy/node compatibility
REQUEST_TIMEOUTThe request exceeded the default 30-second limitRetry cautiously; invoice creation may have reached the node

Invoice creation is not automatically idempotent. After a timeout, do not assume that no invoice was created; reconcile the order before blindly regenerating multiple invoices.

Assets and amount safety

Version 0.1.2 includes these registry entries:

AssetStatus in the package
CKBBuilt in, 8 decimal places
RUSDBuilt-in testnet type script; verify it for the network you deploy
SEALDeclared but marked unsupported because its type script is unset

For CKB, ckbToShannonHex(1) returns 0x5f5e100, or 100,000,000 shannons. For UDTs, calculate the integer using the token's verified decimal count and pass it as hex. Never derive trust from a token symbol alone.

Custom asset limitation in v0.1.2

The v0.1.2 public API exposes customAssets, but its invoice-generation path does not pass that registry into the asset lookup. Treat custom-token support as unavailable in this release unless you have reviewed and patched the upstream behavior. Even after that is fixed, verify the UDT type script and ensure the connected Fiber Node has the asset in its whitelist.

Reuse an existing browser Fiber node

The hooks accept a FiberBackend. If the application already starts a compatible Fiber WASM instance, wrap it instead of calling an HTTP proxy:

import { Fiber, FiberWasmBackend } from "fiber-checkout";

const fiber = new Fiber();
await fiber.start(config, keyPair, secretKey);

const backend = new FiberWasmBackend(fiber);

Pass the same backend to both hooks:

const invoice = useFiberInvoice({
  nodeUrl: "unused-with-wasm-backend",
  backend,
  amount: ckbToShannonHex(1),
  asset: "CKB",
});

const payment = useFiberPayment({
  nodeUrl: "unused-with-wasm-backend",
  backend,
  paymentHash: invoice.paymentHash,
  expiresAt: invoice.expiresAt,
});

nodeUrl remains required by the v0.1.2 hook types but is ignored when a backend is provided. The high-level FiberCheckout component does not expose a backend prop, so WASM mode requires composing the hooks yourself.

Because this mode relies on the optional fiber-js v0.7.x peer API, check browser isolation headers, bundle size, key persistence, and node version compatibility separately. If another library already owns a Fiber browser node, prefer adapting that same instance rather than starting a second node.

Production checklist

  • Put a method- and parameter-restricted proxy in front of a server Fiber Node.
  • Store the expected amount, asset, payment hash, and expiry with the order.
  • Verify Paid server-side before fulfillment; never trust UI state alone.
  • Use integer base units and verify every UDT type script and network deployment.
  • Make invoice regeneration and order reconciliation explicit after timeouts.
  • Stop polling terminal orders and rate-limit active polling sessions.
  • Test React, package, fiber-js, and Fiber Node versions as one release unit.
  • Keep dangerouslyAllowDirectRpc and raw node URLs out of production bundles.

Examples and resources