@capxul/sdk
v0.1.0-alpha.12
Published
Headless TypeScript client for Capxul's /v1/* HTTP contract.
Downloads
1,169
Maintainers
Readme
@capxul/sdk
Headless TypeScript client for Capxul — issue and accept stablecoin payments, manage organization treasuries, and orchestrate on-chain flows backed by audited custody contracts.
Alpha —
0.xis pre-release. APIs can change between alpha versions. Pin to an exact version (@capxul/[email protected]) and read the changelog before upgrading.
Install
pnpm add @capxul/sdk @capxul/sdk-react
# or: npm install @capxul/sdk @capxul/sdk-react@capxul/sdk is the headless contract — flows, types, errors. The
React-flavored entry point lives in @capxul/sdk-react. Pick:
- Browser apps (recommended). Install both. Use
<CapxulProvider config={{ mode: "publishable-key", publishableKey }}>from@capxul/sdk-reactand theuseMe/useCapxulStatushooks. See that package's README for the lazy-DX example. - Server / CLI / scripts. Install only
@capxul/sdk. Build aCapxulClientdirectly with your own auth and Convex adapters (see "Server-side construction" below).
Two-layer trust
Capxul splits the platform into two layers with different licenses:
| Layer | What it does | License |
|---|---|---|
| Custody (on-chain) | Safe v1.4.1 + ERC-4337 modules — every contract that holds, transfers, or signs over user funds | Open source, audited, in the Capxul GitHub repo |
| Orchestration (this package) | Workflow logic, business rules, integration glue, the /v1/* HTTP client | Proprietary — see LICENSE |
You don't have to take our word for the parts that hold money — those contracts are open and auditable. The parts that orchestrate workflows on top are our product.
Quickstart (server / CLI)
import { createCapxulClient, createLocalSigner, tryCatch } from "@capxul/sdk";
const signer = createLocalSigner(process.env.CAPXUL_PRIVATE_KEY!);
const capxul = createCapxulClient({
apiKey: process.env.CAPXUL_API_KEY!, // server-to-server partner key
signer,
});
const [err, payment] = await tryCatch(
capxul.payments.create({
to: { email: "[email protected]" },
amount: { value: "100", currency: "USD" },
}),
);
if (err) {
if (err.code === "INSUFFICIENT_BALANCE") {
// narrow `err.code` to route the UI / reply
}
throw err;
}
console.log(payment.id);Quickstart (browser via React)
See @capxul/sdk-react.
The browser path uses a publishable key (cap_pk_…) and bootstraps
runtime URLs lazily — no need to ship secrets to the client:
import { CapxulProvider, useMe, useCapxulStatus } from "@capxul/sdk-react";
<CapxulProvider
config={{
mode: "publishable-key",
publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
}}
>
<App />
</CapxulProvider>;Publishable-key transport
The browser transport accepts a publishable key, then lazily resolves
runtime URLs by POSTing to /v1/client/bootstrap on the first auth or
ensureRuntime() call. The successful backend response is intentionally
small:
{ "authBaseUrl": "https://<deployment>.convex.site/api/auth", "convexUrl": "https://<deployment>.convex.cloud" }The resolved runtime is cached for the lifetime of the transport. Concurrent
callers share one bootstrap request, successful resolutions are reused, and
failed bootstrap attempts reset so the next call can retry. Tests and
non-default deployments can inject both fetchImpl and an absolute
bootstrapUrl:
import { makeHttpTransport, CapxulError } from "@capxul/sdk";
const transport = makeHttpTransport({
mode: "publishable-key",
publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
bootstrapUrl: "https://api.capxul.com/v1/client/bootstrap",
fetchImpl: fetch,
});
await transport.ensureRuntime();Local setup errors and backend bootstrap errors both use CapxulError, but
carry different details.source values:
try {
await transport.ensureRuntime();
} catch (error) {
if (error instanceof CapxulError) {
if (error.details?.source === "sdk-config") {
// Missing or malformed local config, such as publishableKey or bootstrapUrl.
}
if (error.details?.source === "backend-bootstrap") {
// Sanitized backend refusal, such as NOT_AUTHENTICATED or PERMISSION_DENIED.
}
}
}Runtime proof status:
| Surface | Source support | Runtime proof | Status |
|---|---|---|---|
| SDK transport | makeHttpTransport({ mode: "publishable-key", publishableKey }) | packages/sdk/tests/unit/transport.test.ts proves config validation, singleflight bootstrap, retry after failure, lifecycle transitions, and sanitized backend errors. | Proven with mocked fetch |
| React provider | CapxulProvider config={{ mode: "publishable-key", ... }} | packages/sdk-react/ops/proof/react-headless.test.tsx proves bootstrap before a real useMe() read through the provider and lazy Convex data client. | Proven with mocked fetch + headless React |
| Reference CLI | bootstrap probe --mock --json | apps/reference-cli/scripts/agent-driver.ts phase 0 and the direct CLI command prove provider/bootstrap/auth ordering and sanitized output. | Proven locally; live endpoint remains manual-key gated |
Copy-paste local replication:
corepack pnpm --filter @capxul/sdk check-types
corepack pnpm --filter @capxul/sdk build
corepack pnpm --filter @capxul/sdk-react check-types
corepack pnpm --filter @capxul/sdk-react build
corepack pnpm --filter @capxul/reference-cli check-types
corepack pnpm --filter @capxul/reference-cli build
node apps/reference-cli/dist/cli.js bootstrap probe --mock --jsonRun the SDK and React SDK builds before the reference CLI typecheck in a fresh checkout; the CLI depends on their generated declaration outputs.
Expected sanitized pass signal:
{"command":"bootstrap.probe","ok":true,"mode":"publishable-key","status":"ready","bootstrapRequests":1,"authRequests":1,"keyLengthClass":"provided","mocked":true}Public surface (alpha)
capxul.auth // sendOtp, verifyOtp, signOut, serviceTokenMint
capxul.me // get, update — first-party caller identity
capxul.accounts // create, retrieve, list
capxul.organizations // CRUD + members + payments + treasury
capxul.payments // create, retrieve, list
capxul.invoices // create, retrieve, list
capxul.withdrawals // create, retrieve, list
capxul.documents // KYC uploads, invoices, receipts, tax forms
capxul.flows.{auth, onboarding, provisioning} // XState v5 flowsHelpers:
tryCatch(promise)— error tuple[error, data]; routes onerror.code(typedCapxulErrorCodeunion)matchError,matchStatus,matchAction— exhaustive pattern matchers per CANON.md §4.44- Branded type constructors —
toEmail,toAccountId,toOrganizationId, … — never cast raw strings
Error model
Every method returns a CapxulResult<T, Codes> tuple. Errors are
instances of CapxulError with a typed code (narrowed per method).
const [err, account] = await tryCatch(capxul.me.get());
if (err) {
switch (err.code) {
case "PROFILE_NOT_FOUND": return router.push("/onboarding");
case "NOT_AUTHENTICATED": return router.push("/login");
default: throw err; // unhandled — bubble to the error boundary
}
}There is no second error type. Backend CapxulError → Convex
serialization → SDK deserialization → frontend CapxulError — one
class, 34 codes, end-to-end.
Telemetry (opt-out)
The SDK reports operational telemetry (operation IDs, latencies,
correlation IDs, error codes) to Capxul. PII is never sent — events
are scrubbed at the source. Disable by passing
{ telemetry: { enabled: false } } (coming in 0.2.0); current alpha
sends only operational signals which Capxul Terms disclose explicitly.
License
Proprietary — see LICENSE. The orchestration code in
this package is governed by the Capxul Terms of Service. The
value-bearing custody contracts are open source — see "Two-layer trust"
above.
Contact: [email protected]
