Skip to main content
The SDK throws fast and throws specifically. Constructor validation rejects bad config before any network call; API calls throw a ChipiApiError with a usable status code; missing resources return null (not a throw) where it makes sense. This page walks the patterns you’ll actually hit.

The error hierarchy

Concrete classes live in @chipi-stack/shared and all extend a base ChipiError:
import {
  ChipiError,           // base — { code: string, status?: number }
  ChipiApiError,        // HTTP-level failures — carries the HTTP status (its
                        //   constructor takes `status: number`, though the
                        //   inherited type signature is `status?: number`)
  ChipiAuthError,       // 401 from the API
  ChipiValidationError, // 400 from the API
  ChipiWalletError,     // wallet ops (encryption, lookup)
  ChipiTransactionError,// transfer / contract calls
  ChipiSessionError,    // session keys
  ChipiSkuError,        // sku purchases / catalog
  WalletNotCompatibleError,    // < ChipiWalletError
  WalletClassHashNotFoundError,
  PaymasterIncompatibleError,
} from "@chipi-stack/shared";
Every ChipiError has .code (string) and .message. ChipiApiError carries .status (HTTP status code) — its constructor requires it, though the inherited type makes it optional, so devs reading the field still need a fallback (err.status ?? 500) to satisfy strict TypeScript. Use instanceof for type narrowing or pattern-match on .code / .name / .status.

Constructor validation

The ChipiServerSDK constructor throws synchronously on bad config — before any network call. This is intentional: catch typos in your env vars at boot, not at first request.
import { ChipiServerSDK } from "@chipi-stack/backend";

// Throws: empty / invalid keys
try {
  new ChipiServerSDK({
    apiPublicKey: "invalid-key-12345",
    apiSecretKey: "invalid-secret-12345",
  });
} catch (e) {
  // catch binds `e` as `unknown` under TS strict mode — narrow before reading
  const message = e instanceof Error ? e.message : String(e);
  console.error("SDK config rejected:", message);
  process.exit(1);
}

// Throws: empty secret
try {
  new ChipiServerSDK({
    apiPublicKey: "pk_live_xxx",
    apiSecretKey: "",
  });
} catch {
  console.error("Missing CHIPI_SECRET_KEY in env");
}
Wire this into your bootstrap so the process can’t start with broken auth.

API-level rejections

API calls throw ChipiApiError (or one of the more specific subclasses) when the server returns a non-2xx. The status field tells you which HTTP code — branch on it.
import { ChipiApiError, ChipiAuthError } from "@chipi-stack/shared";

try {
  await sdk.transfer({
    params: {
      encryptKey: "",  // ← invalid: empty PIN
      wallet: userWallet,
      token: "USDC",
      recipient: "0x...",
      amount: 1000n,
    },
  });
} catch (err) {
  if (err instanceof ChipiAuthError) {
    // 401 — your CHIPI_SECRET_KEY is wrong / revoked / scoped wrong
    return res.status(401).send("Auth misconfigured");
  }
  if (err instanceof ChipiApiError && err.status === 400) {
    // Validation failure on the request body
    return res.status(400).send(err.message);
  }
  if (err instanceof ChipiApiError && err.status === 404) {
    // Resource missing
    return res.status(404).send("Wallet not found");
  }
  throw err; // unexpected — let it bubble
}

getWallet returns null, doesn’t throw

For lookups that “might not exist”, the SDK returns null rather than throwing. This is true for getWallet — wrap your code accordingly:
const wallet = await sdk.getWallet({ externalUserId: "non-existent-user" });
if (!wallet) {
  // No wallet for this user — your call to make: create one, or ask them to onboard
  return res.status(404).send("User has no wallet");
}
// wallet is non-null here
null is a happy-path signal here, not an error. The same call only throws if the API itself failed (auth, network, 5xx) — which you’d catch with the ChipiApiError patterns above.

Invalid on-chain inputs throw before sending

Inputs that don’t pass on-chain validation (malformed addresses, unknown tokens) reject before the paymaster fires the transaction — you don’t pay gas for a rejected request.
import { Chain, ChainToken } from "@chipi-stack/backend";

try {
  await sdk.getTokenBalance({
    walletPublicKey: "0xinvalid",  // ← not a valid Starknet address
    chainToken: ChainToken.USDC,
    chain: Chain.STARKNET,
  });
} catch (err) {
  // Throws synchronously with a validation error — no on-chain cost
  const message = err instanceof Error ? err.message : String(err);
  console.error("Bad address:", message);
}

Putting it together

A small middleware that maps every Chipi error class to the right HTTP response:
import {
  ChipiError,
  ChipiApiError,
  ChipiAuthError,
  ChipiValidationError,
  ChipiWalletError,
} from "@chipi-stack/shared";

export function chipiErrorHandler(err: unknown, req, res, next) {
  if (err instanceof ChipiAuthError) {
    return res.status(401).json({ error: err.code, message: err.message });
  }
  if (err instanceof ChipiValidationError) {
    return res.status(400).json({ error: err.code, message: err.message });
  }
  if (err instanceof ChipiApiError) {
    return res.status(err.status ?? 500).json({ error: err.code, message: err.message });
  }
  if (err instanceof ChipiWalletError) {
    return res.status(409).json({ error: err.code, message: err.message });
  }
  if (err instanceof ChipiError) {
    return res.status(500).json({ error: err.code, message: err.message });
  }
  next(err); // not a Chipi error — let your generic handler take it
}
✅ Verified against the live API on 2026-05-11.