> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bill Payments — React

> Browse the SKU catalog, submit a bill payment against your Chipi credits, and poll for settlement using @chipi-stack/chipi-react hooks plus React Query. No end-user wallet, no on-chain signing.

`@chipi-stack/chipi-react` exposes the read side of the Bills flow as React Query hooks (`useGetSkuList`, `useGetSku`, `useGetSkuPurchase`). The purchase itself is a plain `useMutation` calling `POST /v1/sku-purchases` — the SDK's `usePurchaseSku` hook wraps an on-chain wallet path that's reserved for Chipi's internal PWA, not what you want here.

<Note>
  Every endpoint on this page is guarded by Chipi's `BearerTokenGuard` and requires *both* an `x-api-key` header *and* a customer JWT in the `Authorization: Bearer` header. The JWT is issued by *your* auth provider (Clerk, Auth0, BetterAuth, your own); Chipi validates it against the JWKS URL you register for this API key.
</Note>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @chipi-stack/chipi-react @tanstack/react-query
  ```

  ```bash pnpm theme={null}
  pnpm add @chipi-stack/chipi-react @tanstack/react-query
  ```

  ```bash yarn theme={null}
  yarn add @chipi-stack/chipi-react @tanstack/react-query
  ```
</CodeGroup>

## Wrap your app in ChipiProvider

`ChipiProvider` accepts your `ChipiSDKConfig`. It mounts a built-in `QueryClient`, so you don't need to provide one separately for Chipi hooks.

```tsx theme={null}
import { ChipiProvider } from "@chipi-stack/chipi-react";

export function App({ children }) {
  return (
    <ChipiProvider
      config={{
        apiPublicKey: process.env.NEXT_PUBLIC_CHIPI_PUBLIC_KEY!,
      }}
    >
      {children}
    </ChipiProvider>
  );
}
```

## Resolve a customer JWT

Every hook + the purchase mutation need a customer JWT. Wrap your auth-provider's session-token getter in a `getBearerToken` function. With Clerk:

```tsx theme={null}
import { useAuth } from "@clerk/nextjs";

export function useBearerToken() {
  const { getToken } = useAuth();
  return () => getToken(); // returns Promise<string | null>
}
```

The same pattern works with Auth0 (`getAccessTokenSilently`), BetterAuth (`session.token`), or any provider that exposes a signed session token. The token is sent verbatim in the `Authorization: Bearer` header and validated against your registered JWKS URL.

## Browse the catalog — `useGetSkuList`

Filters are typed in `@chipi-stack/types@14.4.0`: `provider` (`"TET" | "CHIPI"`), `category` (`SkuCategory` enum), `chipiCategory` (curated taxonomy code), `carrierName` (case-insensitive substring).

```tsx theme={null}
import { useGetSkuList } from "@chipi-stack/chipi-react";

function TelcelOptions() {
  const getBearerToken = useBearerToken();
  const { data, isLoading, isError } = useGetSkuList({
    query: {
      chipiCategory: "RECARGAS",
      carrierName: "Telcel",
      limit: 20,
    },
    getBearerToken,
  });

  if (isLoading) return <p>Loading…</p>;
  if (isError) return <p>Failed to load catalog</p>;

  return (
    <ul>
      {data.data.map((sku) => (
        <li key={sku.id}>
          {sku.name} — ${sku.fixedAmount} MXN
        </li>
      ))}
    </ul>
  );
}
```

The hook is automatically disabled until `getBearerToken` returns a non-null token, so it's safe to render before sign-in.

`data` shape: `PaginatedResponse<Sku>` — `{ data: Sku[], total, page, limit, totalPages }`.

## Look up a single SKU — `useGetSku`

```tsx theme={null}
import { useGetSku } from "@chipi-stack/chipi-react";

function SkuDetail({ skuId }: { skuId: string }) {
  const getBearerToken = useBearerToken();
  const { data: sku, isLoading } = useGetSku({ id: skuId, getBearerToken });
  if (isLoading || !sku) return <p>Loading…</p>;
  return <p>{sku.name} costs ${sku.fixedAmount} MXN</p>;
}
```

## Submit a purchase — custom `useMutation` against `/v1/sku-purchases`

Use React Query's `useMutation` to wrap a plain `fetch`. The request body matches the `CreateSkuPurchaseInput` DTO on chipi-back. `transactionHash` is purely an idempotency key — any unique string is fine; re-sending the same value within the day returns the existing Transaction row instead of double-charging.

```tsx theme={null}
import { useMutation } from "@tanstack/react-query";

interface PurchaseInput {
  skuId: string;
  skuReference: string;     // phone, account, etc.
  currencyAmount: number;   // must match sku.fixedAmount
  orgMarkup?: number;       // analytics; defaults to 0
}

function useSubmitPurchase() {
  const getBearerToken = useBearerToken();

  return useMutation({
    mutationFn: async (input: PurchaseInput) => {
      const bearerToken = await getBearerToken();
      if (!bearerToken) throw new Error("Sign in first");

      const res = await fetch(
        `${process.env.NEXT_PUBLIC_CHIPI_API_URL}/v1/sku-purchases`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "x-api-key": process.env.NEXT_PUBLIC_CHIPI_PUBLIC_KEY!,
            Authorization: `Bearer ${bearerToken}`,
          },
          body: JSON.stringify({
            transactionHash: `order-${crypto.randomUUID()}`,
            skuId: input.skuId,
            skuReference: input.skuReference,
            currencyAmount: input.currencyAmount,
            chain: "STARKNET",
            token: "USDC",
            walletAddress: "0x0", // required by the DTO but unused for credits flow
            orgMarkup: input.orgMarkup ?? 0,
          }),
        },
      );
      if (!res.ok) throw new Error(`Purchase failed: ${res.status}`);
      return res.json(); // Transaction
    },
  });
}
```

The mutation returns a `Transaction`. `id` is what you pass to `useGetSkuPurchase` to poll for settlement.

## Poll for settlement — `useGetSkuPurchase`

```tsx theme={null}
import { useGetSkuPurchase } from "@chipi-stack/chipi-react";

function PurchaseStatus({ purchaseId }: { purchaseId: string }) {
  const getBearerToken = useBearerToken();
  const { data: purchase } = useGetSkuPurchase({
    id: purchaseId,
    getBearerToken,
    queryOptions: {
      // React Query refetches while status is non-terminal
      refetchInterval: (q) => {
        const s = q.state.data?.status;
        return s === "SUCCESS" || s === "FAILED" ? false : 2_500;
      },
    },
  });

  if (!purchase) return <p>Loading…</p>;
  if (purchase.status === "SUCCESS") return <p>✅ Done — file {purchase.skuFileNumber}</p>;
  if (purchase.status === "FAILED") return <p>❌ Failed</p>;
  return <p>{purchase.status}…</p>;
}
```

For production traffic, prefer a webhook over polling — configure one at `/configure/notifications` in the dashboard.

## DEV sandbox

When `NEXT_PUBLIC_CHIPI_PUBLIC_KEY` is a DEV key (`pk_dev_...`), every purchase is sandboxed:

* **No real credits debited.** Your `OrgBalance.availableUsd` is never touched.
* **No carrier call.** TET is never invoked.
* **Deterministic SUCCESS.** Every purchase succeeds within milliseconds.

Sandbox responses are tagged so your UI can distinguish them — `ledgerEntryId` starts with `le-dev-` and `skuFileNumber` starts with `dev-sandbox-`. Swap to `pk_prod_...` (and point the catalog reads at the production data) and the same code path runs against the real carrier.

## Putting it together

```tsx theme={null}
import { useState } from "react";
import { useGetSkuList, useGetSkuPurchase } from "@chipi-stack/chipi-react";

export function TelcelRecharge({ userPhone }: { userPhone: string }) {
  const getBearerToken = useBearerToken();
  const [purchaseId, setPurchaseId] = useState<string | null>(null);

  const { data: list } = useGetSkuList({
    query: { chipiCategory: "RECARGAS", carrierName: "Telcel", limit: 20 },
    getBearerToken,
  });

  const submit = useSubmitPurchase();

  const { data: purchase } = useGetSkuPurchase({
    id: purchaseId ?? "",
    getBearerToken,
    queryOptions: {
      enabled: !!purchaseId,
      refetchInterval: (q) => {
        const s = q.state.data?.status;
        return s === "SUCCESS" || s === "FAILED" ? false : 2_500;
      },
    },
  });

  const buy = async (sku: any) => {
    const result = await submit.mutateAsync({
      skuId: sku.id,
      skuReference: userPhone,
      currencyAmount: sku.fixedAmount,
    });
    setPurchaseId(result.id);
  };

  return (
    <div>
      {list?.data.map((sku) => (
        <button key={sku.id} onClick={() => buy(sku)}>
          ${sku.fixedAmount} MXN
        </button>
      ))}
      {purchase && <p>Status: {purchase.status}</p>}
    </div>
  );
}
```

## What's next

* Need server-side billing instead? See the [Node guide](/services/bills/node) — same REST pattern with `fetch` + customer JWT.
* Need to filter by carrier? Use `carrierName` — case-insensitive substring match.
* Need credits to actually charge for purchases? See [Billing & Credits](/services/billing/overview).

> ✅ Verified against the live API on **2026-05-19** — same REST contract used in the production smoke purchase against Legaria (transaction `tx-75456794-f4b8-4835-8282-0f89f1964eda`, sku-VIR020 Virgin \$20 MXN, settled SUCCESS in \< 5s).
