Skip to main content
@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.
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.

Install

npm install @chipi-stack/chipi-react @tanstack/react-query

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.
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:
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).
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

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.
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

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

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 — 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.
✅ 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).