("#submit")!;
form.addEventListener("submit", async (event) => {
event.preventDefault();
errorEl.hidden = true;
submitBtn.disabled = true;
submitBtn.textContent = "Processing…";
try {
const token = await tokenize({ cardNumber, cardExpiration, cardCvc });
const res = await fetch("/api/payment-instruments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tokenId: token.id }),
});
if (!res.ok) throw new Error(`Server error: ${res.status}`);
window.location.href = "/checkout/success";
} catch (err) {
errorEl.hidden = false;
errorEl.textContent = (err as Error).message;
submitBtn.disabled = false;
submitBtn.textContent = "Pay $19.99";
}
});
// On SPA route change, beforeunload, etc.:
window.addEventListener("beforeunload", () => {
cardNumber.unmount();
cardExpiration.unmount();
cardCvc.unmount();
});
```
### 3. Server endpoint [#3-server-endpoint]
The browser hands you a Basis Theory token reference. Exchange it for a charged payment instrument server-side:
```ts
// server/payment-instruments.ts
import { createClient } from "@easylabs/node";
const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });
export async function createPaymentInstrument(req, res) {
const { tokenId, cardholderName } = req.body;
const { data: instrument } = await easy.createPaymentInstrument({
type: "PAYMENT_CARD",
identityId: req.session.customerId,
tokenId,
// `name` is required by `CreatePaymentInstrumentBase`. Pass the
// cardholder name your form collected, or fall back to the
// customer's stored name.
name: cardholderName,
});
// Charge it, attach it to a subscription, save it for later — your call.
res.json({ instrumentId: instrument.id });
}
```
### 4. Server-side reaction [#4-server-side-reaction]
For long-lived flows (subscriptions, scheduled charges, fulfilment) listen for the relevant webhooks rather than trusting the browser's success redirect. See [`@easylabs/node`'s webhook helpers](https://www.npmjs.com/package/@easylabs/node).
## Improvements you might add [#improvements-you-might-add]
* **Disable submit until all three are complete.** Track each element's `getState().complete` (or wire a `change` listener for each) and toggle `submitBtn.disabled` accordingly.
* **Pass `cardBrand` to the CVC element.** Read `state.brand` off the card-number `change` event and re-mount the CVC element with `{ cardBrand }` so length validation matches the network (Amex = 4 digits, others = 3).
* **Render a brand logo.** Subscribe to `cardNumber.on("change", ...)` and swap an inline SVG based on `state.brand`.
* **Surface server errors inline.** Catch `EasyApiError` on the server and forward `code` / `message` back to the page so the inline error reflects the API's reason rather than a generic 500.
## Tradeoffs [#tradeoffs]
* **Pro:** total control over layout, copy, validation timing, and submit affordance.
* **Pro:** PAN never reaches your origin — Basis Theory's iframes own it. PCI scope stays minimal (typically SAQ A-EP).
* **Con:** more code than [Embedded Checkout](../embedded-checkout). You own the form, the error UX, and the cleanup.
* **Con:** wallets (Apple Pay / Google Pay) are not built into this flow — add them separately via the wallet button factories. See [Wallet Checkout](../wallet-checkout).
* **Watch out:** always `unmount()` the elements on navigation. Forgetting leaks one iframe per mount.
# Vanilla JS integration (/docs/sdks/javascript/examples/vanilla)
A complete, runnable Vanilla JS example lives at `easy-sdk/examples/vanilla`. This page is a guided tour — it points to the most-relevant files in that example so you can see the SDK wired into a real project, not just isolated snippets.
> **Branch note (pre-0.2.0):** the file paths linked below reference the 0.2.0 example wiring (Elements + wallets) that ships with `@easylabs/browser` 0.2.0. Until that release lands on `main`, the linked files live on the [`feat/easy-browser-parity`](https://github.com/itseasyco/easy-sdk/tree/feat/easy-browser-parity/examples/vanilla) branch — replace `/main/` with `/feat/easy-browser-parity/` in any link below if it 404s. Once 0.2.0 merges, the `main` links resolve normally.
The example exercises every payment surface added in `0.2.0`:
* A custom card form built from `mountCardNumberElement`, `mountCardExpirationDateElement`, and `mountCardVerificationCodeElement`, then converted to a token via `tokenize`.
* An Apple Pay button via `createApplePayButton`.
* A Google Pay button via `createGooglePayButton`.
It is a Vite + TypeScript project (no framework), so it represents the "smallest possible" integration target for `@easylabs/browser`.
## Source code [#source-code]
* **Repository:** [`itseasyco/easy-sdk`](https://github.com/itseasyco/easy-sdk)
* **Path:** [`examples/vanilla`](https://github.com/itseasyco/easy-sdk/tree/main/examples/vanilla)
## What this example covers [#what-this-example-covers]
| Feature | How it's wired |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Client bootstrap | `await createEasyClient({ apiKey })` in `src/main.ts`. The API key is read from `import.meta.env.VITE_EASY_API_KEY`. |
| Custom card form | Three `mount*Element` calls bind iframes into `#card-number`, `#card-exp`, and `#card-cvc`. Each gets `classes: { focus, complete, invalid }` for live styling via the CSS in `src/style.css`. |
| Brand surfacing | `cardNumber.on("change", ...)` writes the detected brand onto a `data-brand` attribute — the integrator could feed this into a card-network logo. |
| Tokenization | A "Create token" button calls `tokenize({ cardNumber, cardExpiration, cardCvc })` and prints the resulting `{ id, type, fingerprint }` to a ``. |
| Apple Pay | `createApplePayButton` mounts into `#apple-pay-slot` with a `merchantSession` callback that POSTs to `/api/apple-pay/session` (your backend wires this to Apple's `paymentSession` endpoint). |
| Google Pay | `createGooglePayButton` mounts into `#google-pay-slot`. The button defers to `isReadyToPay` and only renders when the device supports it. |
| Graceful unavailability | If `apple.isAvailable` is false the slot renders a "not available in this browser" hint. Google Pay does the same on a short timer (its readiness flips async). |
## Key files [#key-files]
| File | Demonstrates |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`examples/vanilla/index.html`](https://github.com/itseasyco/easy-sdk/tree/main/examples/vanilla/index.html) | Container markup for the card form and wallet slots. Also loads Apple's `apple-pay-sdk.js` and Google's `pay.js` — both wallets need their vendor SDKs present. |
| [`examples/vanilla/src/main.ts`](https://github.com/itseasyco/easy-sdk/tree/main/examples/vanilla/src/main.ts) | App bootstrap: `createEasyClient`, three `mount*Element` calls, `tokenize`, plus both wallet button factories. The whole integration in one file. |
| [`examples/vanilla/src/style.css`](https://github.com/itseasyco/easy-sdk/tree/main/examples/vanilla/src/style.css) | Stripe-like state styling — `.el-host.is-focused`, `.is-complete`, `.is-invalid` paired with the `classes` option on each element. |
| [`examples/vanilla/package.json`](https://github.com/itseasyco/easy-sdk/tree/main/examples/vanilla/package.json) | Project manifest. Run `dev`, `build`, `preview` via the standard Vite scripts. |
| [`examples/vanilla/tsconfig.json`](https://github.com/itseasyco/easy-sdk/tree/main/examples/vanilla/tsconfig.json) | TypeScript config — minimum settings for Vite + ESM. |
## Run it locally [#run-it-locally]
```bash
git clone https://github.com/itseasyco/easy-sdk.git
cd easy-sdk
pnpm install
pnpm --filter vanilla-example dev
```
Drop a sandbox key into `examples/vanilla/.env.local`:
```bash
VITE_EASY_API_KEY=sk_test_...
```
Vite will print a local URL (typically `http://localhost:5173`). Without the key the page still renders — the elements will surface an `error` event when they try to mount, so the surface is visible without paid setup.
To consume the SDK from your own project rather than the workspace, install it directly:
```bash
pnpm add @easylabs/browser
```
## Adapting it [#adapting-it]
* **Drop the surfaces you don't need.** The example wires all three (Embedded Checkout is shown elsewhere); strip the wallet sections if you only need a custom card form, or strip the card section if wallets are enough.
* **Add a backend.** The browser SDK can't (and shouldn't) mint sessions on its own. Add a tiny server endpoint — Express, Hono, Cloudflare Worker, anything — that exchanges the token reference for a payment instrument or a checkout session.
* **Wire `merchantSession` for real.** The example's `merchantSession` callback POSTs to `/api/apple-pay/session`. That endpoint must call Apple's `paymentSession` URL with your merchant identity certificate. Apple refuses to validate from the browser.
* **Move the API key out of source.** Vite exposes `import.meta.env.VITE_*` variables at build time. Use `VITE_EASY_API_KEY` for the sandbox key in development; in production, mint sessions on your backend with `sk_live_*` and never ship that key to the browser.
# E-commerce Flow (/docs/sdks/node/examples/ecommerce-flow)
A canonical "buy a product" flow with `@easylabs/node`: customer + payment instrument captured from the frontend, charged via `checkout`, fulfilment metadata attached to the resulting order, refund issued on cancellation.
## Goal [#goal]
Single endpoint that turns a cart-and-card payload from the browser into a charged order, idempotently. The frontend tokenizes the card with the Easy Labs frontend SDK and posts only the resulting `tokenId` plus the cart — your server never touches PAN data.
## Implementation [#implementation]
### 1. Create products + prices once [#1-create-products--prices-once]
```ts
import { createClient } from "@easylabs/node";
const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });
const product = await easy.createProduct({ name: "Notebook", active: true });
const price = await easy.createPrice({
product_id: product.data.id,
active: true,
recurring: false,
currency: "USD",
unit_amount: 2499,
tax_behavior: "exclusive",
});
```
Persist `price.data.id` in your catalog table.
### 2. Charge endpoint [#2-charge-endpoint]
```ts
// POST /api/checkout
type Body = {
buyer: { first_name: string; last_name: string; email: string };
card: { tokenId: string; name: string };
cart: { price_id: string; quantity: number }[];
cart_id: string;
};
export async function checkout(body: Body) {
const result = await easy.checkout({
customer_creation: true,
customer_details: {
first_name: body.buyer.first_name,
last_name: body.buyer.last_name,
email: body.buyer.email,
},
source: {
type: "PAYMENT_CARD",
tokenId: body.card.tokenId,
name: body.card.name,
},
line_items: body.cart,
metadata: { cart_id: body.cart_id },
});
if (result.data.orderId) {
await easy.updateOrderTags(result.data.orderId, {
cart_id: body.cart_id,
fulfilment_state: "queued",
});
}
return result.data;
}
```
### 3. Mark fulfilment complete [#3-mark-fulfilment-complete]
```ts
async function markShipped(orderId: string, trackingNumber: string) {
const { data: order } = await easy.getOrder(orderId);
await easy.updateOrderTags(orderId, {
...order.tags,
fulfilment_state: "shipped",
tracking_number: trackingNumber,
});
}
```
### 4. Cancel + refund [#4-cancel--refund]
```ts
async function cancelAndRefund(orderId: string, reason: string) {
const { data: order } = await easy.getOrder(orderId);
if (!order.transfer) throw new Error("Order has no captured transfer.");
await easy.createRefund(order.transfer.id, {
refund_amount: order.transfer.amount,
tags: { reason, source: "support_console" },
});
await easy.updateOrderTags(orderId, {
...order.tags,
fulfilment_state: "cancelled",
cancellation_reason: reason,
});
}
```
## Tradeoffs [#tradeoffs]
* **`checkout` does a lot.** It creates the customer, instrument, order, and transfer in one call. If you need to insert validation between steps (KYC, fraud rules, address verification), call `createCustomer` → `createPaymentInstrument` → `createTransfer` separately and wrap them in your own state machine.
* **Idempotency.** `checkout` doesn't accept an idempotency key today. Dedupe at the application layer — store `cart_id` in `metadata` and refuse a second submission that maps to the same cart.
* **Returning customers.** Switch the call to `customer_creation: false` and pass the saved `identity_id` + a stored `instrument_id` (string) so you charge a saved card without prompting again.
* **Subscriptions in the same cart.** If the cart contains a recurring price, `result.data.subscriptions` is populated — link those subscription IDs back to your local user record.
* **Refund accumulation.** Partial refunds add up against the original capture; the API returns 422 if you exceed it.
# Fastify integration (/docs/sdks/node/examples/fastify)
A complete, runnable Fastify example lives at `easy-sdk/examples/node`. This page is a guided tour — it points to the most-relevant files in that example so you can see the SDK wired into a real project, not just isolated snippets.
## Source code [#source-code]
* **Repository:** [`itseasyco/easy-sdk`](https://github.com/itseasyco/easy-sdk)
* **Path:** [`examples/node`](https://github.com/itseasyco/easy-sdk/tree/main/examples/node)
* **README:** [`examples/node/README.md`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/README.md)
## What this example covers [#what-this-example-covers]
* Initializing the SDK once at startup as a Fastify plugin.
* Customer CRUD (`getCustomers`, `getCustomer`, `createCustomer`, `updateCustomer`).
* Storing payment instruments (`createPaymentInstrument`, `updatePaymentInstrument`).
* Charging via the all-in-one `checkout` flow — both the `customer_creation: true` and `customer_creation: false` branches.
* Product + price CRUD (`createProduct`, `createPrice`, archive).
* Subscription lifecycle (`createSubscription`, `cancelSubscription`).
* Direct transfers (`createTransfer`, `getTransfer`).
* Generated OpenAPI documentation served at `/documentation` so you can poke every endpoint from a browser.
## Key files [#key-files]
| File | Demonstrates |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| [`src/server.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/server.ts) | Fastify bootstrap: `helmet`, `sensible`, Swagger UI, autoloaded plugins and routes. |
| [`src/plugins/easy-client.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/plugins/easy-client.ts) | Wraps `createClient` in a Fastify plugin and decorates the instance with `fastify.easyClient`. |
| [`src/routes/customers/index.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/routes/customers/index.ts) | List / get / create / update via `fastify.easyClient.getCustomers(...)`, etc. |
| [`src/routes/checkout/index.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/routes/checkout/index.ts) | Splits `CreateCheckoutSession` on `customer_creation` to expose two endpoints. |
| [`src/routes/subscriptions/index.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/routes/subscriptions/index.ts) | Subscription create + cancel. |
| [`src/routes/products/index.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/routes/products/index.ts), [`src/routes/prices/index.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/node/src/routes/prices/index.ts) | Catalog management. |
The plugin is the most-copied file:
```ts
// src/plugins/easy-client.ts
import { createClient } from "@easylabs/node";
import type { FastifyPluginAsync } from "fastify";
import fp from "fastify-plugin";
declare module "fastify" {
interface FastifyInstance {
easyClient: Awaited>;
}
}
const easyClientPlugin: FastifyPluginAsync = async (fastify) => {
if (!process.env.EASY_API_KEY) {
throw new Error("EASY_API_KEY environment variable is required");
}
const client = await createClient({ apiKey: process.env.EASY_API_KEY });
fastify.decorate("easyClient", client);
};
export default fp(easyClientPlugin, { name: "easy-client" });
```
## Run it locally [#run-it-locally]
```bash
git clone https://github.com/itseasyco/easy-sdk.git
cd easy-sdk
pnpm install
pnpm --filter node-example build # builds the workspace deps
cd examples/node
echo "EASY_API_KEY=sk_test_..." > .env.local
pnpm dev
# Visit http://localhost:8008/documentation
```
## Adapting it [#adapting-it]
* **Webhooks.** The example doesn't ship a webhook route. Add one with `fastify.addContentTypeParser("application/json", { parseAs: "string" }, …)` so you can pass the raw body to `EasyWebhooks.constructEvent`. See [Webhooks](../webhooks#fastify) for the full snippet.
* **Per-request error handling.** Today routes catch errors locally and return `{ error: "..." }`. Switch to a global `setErrorHandler` that branches on `err instanceof EasyApiError` (status + code) for a consistent API surface.
* **Multi-tenant.** Replace the singleton plugin with a request-scoped factory keyed on tenant ID — see [Authentication → Multi-tenant](../authentication#multi-tenant-authentication).
* **Strip `__dev`.** The example sets `__dev: true` for local convenience. In production, omit it — the SDK picks the URL from your key prefix.
# Next.js integration (/docs/sdks/node/examples/nextjs)
A complete, runnable Next.js example lives at `easy-sdk/examples/next-example`. This page is a guided tour — it points to the most-relevant files in that example so you can see the SDK wired into a real project, not just isolated snippets.
## Source code [#source-code]
* **Repository:** [`itseasyco/easy-sdk`](https://github.com/itseasyco/easy-sdk)
* **Path:** [`examples/next-example`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example)
## What this example covers [#what-this-example-covers]
* Customer signup with a synced local user record (Prisma) and an Easy customer.
* Admin dashboards backed by Server Components + Server Actions hitting the SDK.
* Product / price catalog management (create, edit, archive — both products and prices).
* Subscription lifecycle (list, create, cancel).
* Order detail pages with refund / fulfilment actions.
* Disputes triage screen.
* Revenue + analytics widgets.
* Profile screen with stored payment instruments.
## Key files [#key-files]
| File | Demonstrates |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [`src/lib/easy-sync.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/lib/easy-sync.ts) | Lazy singleton (`getEasyClient`) wrapping `createClient` so a single SDK instance is reused across server-side modules. |
| [`src/app/api/auth/signup/route.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/app/api/auth/signup/route.ts) | Route Handler that creates a local user + an Easy customer in one step. |
| [`src/app/admin/products/actions.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/app/admin/products/actions.ts) | Server Actions calling `createProduct`, `updateProduct`, `archiveProduct`. |
| [`src/app/admin/subscriptions/actions.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/app/admin/subscriptions/actions.ts) | `createSubscription`, `cancelSubscription` from a Server Action invoked by a Client Component. |
| [`src/app/admin/orders/[id]/actions.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/app/admin/orders/\[id]/actions.ts) | Order detail mutations (`updateOrderTags`, refund flows). |
| [`src/app/admin/disputes/actions.ts`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/app/admin/disputes/actions.ts) | Dispute triage (`acceptDispute`, `updateDispute`, evidence upload). |
| [`src/app/checkout/page.tsx`](https://github.com/itseasyco/easy-sdk/blob/main/examples/next-example/src/app/checkout/page.tsx) | Buyer-side checkout wiring (delegates to `@easylabs/react` for the UI). |
The lazy-singleton pattern is what most teams copy first:
```ts
// src/lib/easy-sync.ts
import { createClient } from "@easylabs/node";
let easyClient: Awaited> | null = null;
async function getEasyClient() {
if (easyClient) return easyClient;
if (!process.env.EASY_API_KEY) throw new Error("EASY_API_KEY is required");
easyClient = await createClient({ apiKey: process.env.EASY_API_KEY });
return easyClient;
}
```
Call `getEasyClient()` from any Server Component, Server Action, or Route Handler. Never import this module from a Client Component — the SDK is server-only.
## Run it locally [#run-it-locally]
```bash
git clone https://github.com/itseasyco/easy-sdk.git
cd easy-sdk
pnpm install
pnpm --filter next-example db:init # provisions the local Prisma DB
cd examples/next-example
echo "EASY_API_KEY=sk_test_..." > .env.local
pnpm dev
# http://localhost:3000
```
## Adapting it [#adapting-it]
* **Cache the SDK at module scope.** Next.js may hot-reload modules in dev — the `let easyClient` cache survives normal renders but resets on reload. That's fine; the validation cost is low. In production it's effectively a process singleton.
* **Pin the runtime.** Set `export const runtime = "nodejs"` on Route Handlers and Server Actions that use the SDK — `@easylabs/node` depends on `node:crypto` / `FormData` and won't run on the Edge runtime.
* **Webhooks.** The example doesn't include a webhook route. Add one as a Route Handler with `await req.text()` to capture the raw body, then `EasyWebhooks.constructEvent(rawBody, sig, secret)`. See [Webhooks](../webhooks#nextjs-route-handler).
* **Tenant scoping.** Replace the singleton with a `Map>` cache keyed on the merchant ID resolved from the session.
* **Strip the `NEXT_PUBLIC_` prefix.** The example reads `NEXT_PUBLIC_EASY_API_KEY` for local dev convenience — do not deploy with a public-prefixed secret. Read from a non-public env var on the server.
# Refunds (/docs/sdks/node/examples/refunds)
Refunding orders end-to-end with `@easylabs/node` — covering full and partial refunds, automatic accept-on-dispute, and tying the reversal back to your local ledger.
## Goal [#goal]
A reusable refund service that:
1. Resolves the originating transfer from an order, invoice, or charge.
2. Applies a partial or full reversal.
3. Persists the reversal ID against your domain object for accounting.
4. Reacts to `dispute.created` webhooks by pre-emptively refunding low-value disputes.
## Implementation [#implementation]
### 1. Refund service [#1-refund-service]
```ts
import { createClient } from "@easylabs/node";
// EasyApiError ships from @easylabs/common; @easylabs/node re-exports the
// *type* but not the runtime value, so `instanceof` needs the common import.
import { EasyApiError } from "@easylabs/common";
const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });
type RefundReason = "customer_request" | "duplicate" | "fraud" | "shipping_delay" | "dispute_received";
export async function refundOrder(
orderId: string,
amountCents: number | "full",
reason: RefundReason,
approvedBy: string,
) {
const { data: order } = await easy.getOrder(orderId);
if (!order.transfer) throw new Error("Order has no captured transfer.");
if (order.transfer.state !== "SUCCEEDED") {
throw new Error(`Cannot refund transfer in state ${order.transfer.state}.`);
}
const refund_amount = amountCents === "full" ? order.transfer.amount : amountCents;
try {
const reversal = await easy.createRefund(order.transfer.id, {
refund_amount,
tags: {
reason,
approved_by: approvedBy,
order_number: order.order_number,
},
});
// Persist reversal.data.id against the order in your DB
return reversal.data;
} catch (err) {
if (err instanceof EasyApiError && err.status === 422) {
// Probably trying to refund more than was captured
throw new Error(`Refund rejected: ${err.message}`);
}
throw err;
}
}
```
### 2. Auto-refund small disputes [#2-auto-refund-small-disputes]
```ts
import { EasyWebhooks } from "@easylabs/node";
export async function handleWebhook(rawBody: string, sigHeader: string) {
const event = EasyWebhooks.constructEvent(
rawBody,
sigHeader,
process.env.EASY_WEBHOOK_SECRET!,
);
if (event.type === "dispute.created") {
const dispute = event.data as { id: string; amount: number; transfer?: { id: string } };
if (dispute.amount < 1000 && dispute.transfer) {
// Cheaper to refund than to fight
await easy.createRefund(dispute.transfer.id, {
refund_amount: dispute.amount,
tags: { reason: "dispute_received", dispute_id: dispute.id },
});
await easy.acceptDispute(dispute.id);
}
}
}
```
### 3. Reconcile a reversal back to your ledger [#3-reconcile-a-reversal-back-to-your-ledger]
```ts
const reversal = await refundOrder(orderId, "full", "customer_request", "agent_42");
console.log({
ledger_entry: "refund",
amount_cents: reversal.amount,
parent_transfer: reversal.parent_transfer,
reversal_id: reversal.id,
state: reversal.state,
});
```
## Tradeoffs [#tradeoffs]
* **No dedicated list endpoint.** To find every reversal for a transfer, list transfers and filter by `type === "REVERSAL"` and `parent_transfer === originalId`, or follow `_links.reversals.href` on the parent.
* **Partial-refund accumulation.** Multiple partial refunds add up against the original capture. The API returns 422 once you exceed it — handle that case in your service.
* **Pending state.** A new reversal starts as `state: "PENDING"` and only flips to `SUCCEEDED` when the funds clear back. Don't notify the customer "refunded" until you've seen the `payment.updated` (or refund-specific) webhook flip the state.
* **Disputes vs refunds.** A refund issued *after* a dispute is opened may not stop the dispute fee — `acceptDispute` is the cleanest way to close the case in the cardholder's favor.
* **Subscription invoice refunds.** Refunding a subscription's invoice transfer doesn't cancel the subscription. If the customer wants to stop billing too, also call `cancelSubscription(subId, { at_period_end: false })`.
# Subscription System (/docs/sdks/node/examples/subscription-system)
A complete subscription system on `@easylabs/node`: pricing catalog, signup with trial, in-flight upgrade with proration preview, metered usage reporting, dunning, and cancellation at period end.
## Goal [#goal]
Everything you need to ship a B2B SaaS with monthly + yearly tiers, optional metered add-ons, and graceful failed-payment handling — all driven by the SDK plus webhooks.
## Implementation [#implementation]
### 1. Catalog [#1-catalog]
```ts
const product = await easy.createProduct({ name: "Pro plan", active: true });
const monthly = await easy.createPrice({
product_id: product.data.id,
active: true,
recurring: true,
currency: "USD",
unit_amount: 4900,
interval: "month",
interval_count: 1,
tax_behavior: "exclusive",
trial_period_days: 14,
});
const yearly = await easy.createPrice({
product_id: product.data.id,
active: true,
recurring: true,
currency: "USD",
unit_amount: 49000, // $490 — 2 months free
interval: "year",
interval_count: 1,
tax_behavior: "exclusive",
});
const meteredApiCalls = await easy.createPrice({
product_id: product.data.id,
active: true,
recurring: true,
currency: "USD",
unit_amount: 1, // $0.01 per unit
interval: "month",
interval_count: 1,
tax_behavior: "exclusive",
pricing_model: "metered",
});
```
### 2. Signup with a 14-day trial [#2-signup-with-a-14-day-trial]
```ts
const sub = await easy.createSubscription({
identity_id: customer.id,
items: [
{ price_id: monthly.data.id, quantity: 1 },
{ price_id: meteredApiCalls.data.id }, // qty driven by usage reports
],
instrument_id: paymentInstrumentId,
trial_period_days: 14,
metadata: { plan: "pro_monthly" },
});
```
### 3. Upgrade with proration preview [#3-upgrade-with-proration-preview]
```ts
const preview = await easy.getSubscriptionProrationPreview(sub.data.id, {
items: [{ price_id: yearly.data.id, quantity: 1 }],
remove_items: [
sub.data.items.find((i) => i.price_id === monthly.data.id)!.id,
],
});
// Show preview.data to the user, then commit:
await easy.updateSubscription(sub.data.id, {
items: [{ price_id: yearly.data.id, quantity: 1 }],
remove_items: [
sub.data.items.find((i) => i.price_id === monthly.data.id)!.id,
],
proration_behavior: "create_prorations",
});
```
### 4. Metered usage [#4-metered-usage]
```ts
import { randomUUID } from "node:crypto";
async function reportApiCall(subscriptionId: string, itemId: string) {
await easy.reportSubscriptionUsage(subscriptionId, {
subscription_item_id: itemId,
quantity: 1,
action: "increment",
timestamp: new Date().toISOString(),
idempotency_key: randomUUID(),
});
}
// Period summary for an in-app dashboard:
const summary = await easy.getSubscriptionUsageSummary(sub.data.id, {
subscription_item_id: meteredItemId,
from: periodStart,
to: periodEnd,
});
```
### 5. Dunning [#5-dunning]
```ts
await easy.createOrReplaceDunningConfig({
retry_mode: "smart",
smart_retry_attempts: 8,
smart_retry_window: "2_weeks",
subscription_terminal_action: "cancel",
invoice_terminal_action: "uncollectible",
payment_failed_email_enabled: true,
expiring_card_email_enabled: true,
card_expiry_warn_days: 30,
});
```
Wire your webhook handler to react to `invoice.payment_failed`, `subscription.paused`, and `subscription.deleted` — see [Webhooks](../webhooks).
### 6. Cancellation at period end [#6-cancellation-at-period-end]
```ts
await easy.cancelSubscription(sub.data.id, { at_period_end: true });
```
### 7. Discount on retention [#7-discount-on-retention]
```ts
const winback = await easy.createCoupon({
duration: "repeating",
duration_in_months: 3,
percent_off: 50,
name: "Winback offer",
});
await easy.applySubscriptionDiscount(sub.data.id, { coupon_id: winback.data.id });
```
## Tradeoffs [#tradeoffs]
* **Trial accounting.** Pass `trial_period_days` (or `trial_end`) at create time *and* set `instrument_id` so the card is on file for the first paid charge. Listen for `subscription.trial_will_end` to nudge users 3 days out.
* **Proration policy.** `"create_prorations"` issues credits/charges immediately; `"none"` defers everything to the next invoice. Preview first if your users are price-sensitive.
* **Metered idempotency.** Reuse the upstream event ID (e.g. webhook delivery ID) as `idempotency_key` so retries don't double-bill.
* **Cancellation timing.** `at_period_end: true` keeps service active until renewal; omit it for instant termination + final invoice.
* **Multiple plans on one subscription.** A single `SubscriptionData` may carry many `items` (e.g. base seat + metered calls + storage). Use `addSubscriptionItem` / `removeSubscriptionItem` for granular changes.
# Analytics (/docs/sdks/node/resources/analytics)
Analytics endpoints aggregate transactions, disputes, settlements, revenue, and revenue-recovery activity over a date range. Each method accepts the same `AnalyticsQuery` and returns an open-ended JSON shape suitable for dashboards or warehouse loads.
## Methods [#methods]
```ts
easy.getTransactionAnalytics(params?); // GET /analytics/transactions
easy.getDisputeAnalytics(params?); // GET /analytics/disputes
easy.getSettlementAnalytics(params?); // GET /analytics/settlements
easy.getRevenueAnalytics(params?); // GET /analytics/revenue
easy.getRevenueRecoveryAnalytics(params?); // GET /analytics/revenue-recovery
```
`AnalyticsQuery`:
```ts
type AnalyticsQuery = {
period?: "day" | "week" | "month";
start_date?: string; // ISO date or datetime
end_date?: string;
};
```
```ts
const monthly = await easy.getRevenueAnalytics({
period: "month",
start_date: "2026-01-01",
end_date: "2026-04-30",
});
const weekly = await easy.getDisputeAnalytics({
period: "week",
start_date: "2026-04-01",
});
```
## Object shape [#object-shape]
Each analytics method returns `ApiResponse>` — the SDK does not narrow the response shape because the aggregations are computed server-side and may include slice-specific fields. Refer to the API reference for the canonical schema of each report. {/* TODO: tighten the analytics response types as the schemas stabilize. */}
## Examples [#examples]
### Daily revenue chart for the current quarter [#daily-revenue-chart-for-the-current-quarter]
```ts
const start = "2026-04-01";
const end = "2026-06-30";
const daily = await easy.getRevenueAnalytics({ period: "day", start_date: start, end_date: end });
// pipe daily.data into your charting library
```
### Build a recovery-rate KPI [#build-a-recovery-rate-kpi]
```ts
const recovery = await easy.getRevenueRecoveryAnalytics({
period: "month",
start_date: "2026-01-01",
end_date: "2026-12-31",
});
// recovery.data shape is open — inspect once and code your KPI extractor against the live response.
```
### Snapshot for a board deck [#snapshot-for-a-board-deck]
```ts
const [tx, disputes, settlements] = await Promise.all([
easy.getTransactionAnalytics({ period: "month" }),
easy.getDisputeAnalytics({ period: "month" }),
easy.getSettlementAnalytics({ period: "month" }),
]);
```
# Authorizations (/docs/sdks/node/resources/authorizations)
An authorization is a hold on funds you can later capture in part or in full. Use it for marketplaces, hotels, rentals, or any flow where the final amount isn't known when the customer pays.
## Methods [#methods]
```ts
easy.listAuthorizations(params?); // GET /authorizations
easy.getAuthorization(authorizationId); // GET /authorizations/:id
easy.captureAuthorization(authorizationId, amount); // POST /authorizations/:id/capture
easy.voidAuthorization(authorizationId); // POST /authorizations/:id/void
```
```ts
const auths = await easy.listAuthorizations({ limit: 25 });
const { data: auth } = await easy.getAuthorization(auths.data[0].id);
// Capture a portion of the held amount:
await easy.captureAuthorization(auth.id, 7500); // smallest currency unit
// Or release the hold without capturing:
await easy.voidAuthorization(auth.id);
```
There is no `createAuthorization` method on the SDK — authorizations are produced by upstream charge APIs (auth-and-capture flows initiated through `checkout` or the embedded checkout) rather than created directly.
`captureAuthorization` requires the `amount` to be ≤ `auth.amount_requested`. Capturing 0 is not permitted; void instead.
## Object shape [#object-shape]
`AuthorizationData`:
| Field | Type | Notes |
| ------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `id` | `string` | |
| `amount` / `amount_requested` | `number` | Captured amount and original hold, smallest currency unit. |
| `currency` | `string` | ISO 4217. |
| `state` | `"PENDING" \| "SUCCEEDED" \| "FAILED" \| "VOIDED" \| "CAPTURED"` (or string) | |
| `source` / `destination` / `merchant` | `string \| null` | |
| `failure_code` / `failure_message` | `string \| null` | Populated on `FAILED`. |
| `trace_id` | `string \| null` | |
| `tags` | `Record \| null` | |
| `expires_at` | `string \| null` | After this, the network releases the hold automatically. |
| `captured_at` / `voided_at` | `string \| null` | |
## Examples [#examples]
### Capture less than the original hold [#capture-less-than-the-original-hold]
```ts
await easy.captureAuthorization(authId, finalCartTotalCents);
```
### Auto-void expiring holds [#auto-void-expiring-holds]
```ts
const stale = (await easy.listAuthorizations({ limit: 100 })).data.filter(
(a) => a.state === "PENDING" && a.expires_at && Date.parse(a.expires_at) < Date.now(),
);
for (const a of stale) await easy.voidAuthorization(a.id);
```
### React to an authorization webhook [#react-to-an-authorization-webhook]
```ts
if (event.type === "authorization.updated") {
const auth = event.data as { id: string; state: string };
if (auth.state === "SUCCEEDED") {
await easy.captureAuthorization(auth.id, finalAmountCents);
}
}
```
# Checkout (/docs/sdks/node/resources/checkout)
`checkout` is the all-in-one charge endpoint: in a single request it can create a customer, create a payment instrument, create line-item-backed orders / subscriptions, and capture the transfer. It exists as one method on the SDK — `easy.checkout(...)` — backed by `POST /checkout`.
For embedded UI flows, see [Embedded Checkout](./embedded-checkout). For shareable hosted pages, see [Payment Links](./payment-links).
## Methods [#methods]
### `checkout(session)` [#checkoutsession]
`POST /checkout`. Returns `ApiResponse<{ transfer?: TransferData; orderId?: string; subscriptions: (SubscriptionData & { order_id: string; order_number: string })[] }>`.
The request body is a discriminated union (`CreateCheckoutSession`):
```ts
type CreateCheckoutSession =
| {
customer_creation: true;
customer_details: CreateCustomer;
source: Omit;
line_items: { price_id: string; quantity: number }[];
metadata?: Record;
}
| {
customer_creation: false;
identity_id: string;
source: Omit | string; // existing instrument ID or new instrument
line_items: { price_id: string; quantity: number }[];
metadata?: Record;
};
```
### New customer [#new-customer]
```ts
const result = await easy.checkout({
customer_creation: true,
customer_details: {
first_name: "Ada",
last_name: "Lovelace",
email: "ada@example.com",
},
source: {
type: "PAYMENT_CARD",
tokenId: cardTokenFromFrontend,
name: "Ada Lovelace",
},
line_items: [{ price_id: "PR_...", quantity: 1 }],
metadata: { cart_id: "cart_42" },
});
console.log(result.data.transfer?.id, result.data.subscriptions);
```
### Existing customer + new card [#existing-customer--new-card]
```ts
await easy.checkout({
customer_creation: false,
identity_id: customer.id,
source: {
type: "PAYMENT_CARD",
tokenId: cardTokenFromFrontend,
name: "Ada Lovelace",
},
line_items: [{ price_id: "PR_...", quantity: 2 }],
});
```
### Existing customer + saved instrument [#existing-customer--saved-instrument]
```ts
await easy.checkout({
customer_creation: false,
identity_id: customer.id,
source: instrumentId, // string ID of a previously stored instrument
line_items: [{ price_id: "PR_...", quantity: 1 }],
});
```
## Object shape [#object-shape]
The successful response shape:
| Field | Type | Notes |
| --------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `transfer` | `TransferData \| undefined` | The captured charge — undefined for subscription-only carts where billing is deferred. |
| `orderId` | `string \| undefined` | The order created for one-time line items. |
| `subscriptions` | `(SubscriptionData & { order_id, order_number })[]` | One subscription per recurring price in the cart. |
`line_items` may freely mix one-time and recurring prices — Easy Labs charges the one-time amount up front via `transfer` and creates one subscription per recurring price.
## Examples [#examples]
### Mixed cart (one-time + subscription) [#mixed-cart-one-time--subscription]
```ts
await easy.checkout({
customer_creation: false,
identity_id: customer.id,
source: instrumentId,
line_items: [
{ price_id: "PR_setup_fee_oneTime", quantity: 1 },
{ price_id: "PR_monthly_plan", quantity: 1 },
],
});
```
### Surface decline reasons [#surface-decline-reasons]
```ts
// EasyApiError is the runtime class from @easylabs/common; @easylabs/node
// re-exports the *type* but not the value, so import from common directly.
import { EasyApiError } from "@easylabs/common";
try {
await easy.checkout({ /* … */ });
} catch (err) {
if (err instanceof EasyApiError && err.status === 402) {
// Card declined — show the message from err.details to the buyer.
} else {
throw err;
}
}
```
# Compliance Forms (/docs/sdks/node/resources/compliance-forms)
Compliance forms (e.g. card-network agreements, sponsor disclosures) are issued to your company by Easy Labs and must be signed before certain product capabilities are unlocked. The SDK lets you list pending and historical forms, fetch one by ID, and submit a signature.
## Methods [#methods]
```ts
easy.listComplianceForms(); // GET /compliance-forms
easy.getComplianceForm(formId); // GET /compliance-forms/:id
easy.signComplianceForm(formId, body); // PUT /compliance-forms/:id/sign
```
```ts
const forms = await easy.listComplianceForms();
const pending = forms.data.filter((f) => f.status !== "signed");
for (const f of pending) {
await easy.signComplianceForm(f.id, {
name: "Ada Lovelace",
title: "CEO",
});
}
```
## Object shape [#object-shape]
`ComplianceFormData`:
| Field | Type | Notes |
| ------------------------------ | ------------------------- | ------------------------------------------- |
| `id` | `string` | |
| `type` | `string` | The form template, e.g. card-network terms. |
| `status` | `string` | `pending`, `signed`, etc. |
| `signed_at` | `string \| null` | ISO timestamp. |
| `signed_name` / `signed_title` | `string \| null` | |
| `due_at` | `string \| null` | Deadline before features lock. |
| `metadata` | `Record` | |
`SignComplianceForm` body: `{ name: string; title: string }`.
## Examples [#examples]
### Block deploys when a form is overdue [#block-deploys-when-a-form-is-overdue]
```ts
const forms = await easy.listComplianceForms();
const overdue = forms.data.filter(
(f) => f.status !== "signed" && f.due_at && Date.parse(f.due_at) < Date.now(),
);
if (overdue.length) {
throw new Error(`Sign these compliance forms first: ${overdue.map((f) => f.type).join(", ")}`);
}
```
### Audit who signed what [#audit-who-signed-what]
```ts
const forms = await easy.listComplianceForms();
console.table(
forms.data.filter((f) => f.signed_at).map((f) => ({
type: f.type,
signed_by: `${f.signed_name} (${f.signed_title})`,
signed_at: f.signed_at,
})),
);
```
# Coupons (/docs/sdks/node/resources/coupons)
A coupon is a reusable discount template — percentage-off or fixed-amount, applied once / for N months / forever. Coupons are attached to subscriptions via [`applySubscriptionDiscount`](./subscriptions#discounts) or surfaced to buyers through [promotion codes](./promotion-codes).
## Methods [#methods]
```ts
easy.createCoupon(body); // POST /coupons
easy.listCoupons(params?); // GET /coupons
easy.getCoupon(couponId); // GET /coupons/:id
easy.updateCoupon(couponId, body); // PATCH /coupons/:id
easy.deleteCoupon(couponId); // DELETE /coupons/:id
```
`CreateCoupon` is a discriminated union — pass `percent_off` **or** `amount_off + currency`, never both:
```ts
// Percentage off
await easy.createCoupon({
duration: "repeating",
duration_in_months: 3,
percent_off: 25,
name: "Q2 promo",
max_redemptions: 1000,
valid_until: "2026-06-30T23:59:59Z",
applies_to_products: ["PD_pro"],
metadata: { campaign: "spring_2026" },
});
// Fixed amount off (smallest currency unit)
await easy.createCoupon({
duration: "once",
amount_off: 1000,
currency: "USD",
name: "$10 welcome credit",
});
```
`UpdateCoupon` is intentionally narrow — only `name`, `active`, and `metadata` are mutable. To change the discount itself, create a new coupon and migrate users.
## Object shape [#object-shape]
`CouponData`:
| Field | Type | Notes |
| --------------------- | ------------------------------------ | ----------------------------------------- |
| `id` | `string` | |
| `name` | `string \| null` | |
| `duration` | `"once" \| "repeating" \| "forever"` | |
| `duration_in_months` | `number \| null` | Only set when `duration === "repeating"`. |
| `percent_off` | `number \| null` | 0–100. |
| `amount_off` | `number \| null` | Smallest currency unit. |
| `currency` | `string \| null` | Required with `amount_off`. |
| `max_redemptions` | `number \| null` | Lifetime cap. |
| `times_redeemed` | `number` | Counter. |
| `valid_until` | `string \| null` | ISO timestamp. |
| `applies_to_products` | `string[] \| null` | Restrict eligibility. |
| `active` | `boolean` | |
| `metadata` | `Record` | |
## Examples [#examples]
### Forever 50%-off VIP coupon [#forever-50-off-vip-coupon]
```ts
const coupon = await easy.createCoupon({
duration: "forever",
percent_off: 50,
name: "VIP",
});
```
### Pause a coupon without deleting redemptions [#pause-a-coupon-without-deleting-redemptions]
```ts
await easy.updateCoupon(couponId, { active: false });
```
### List active coupons [#list-active-coupons]
```ts
const all = await easy.listCoupons({ limit: 100 });
const active = all.data.filter((c) => c.active);
```
# Customers (/docs/sdks/node/resources/customers)
A customer is the buyer-side identity that owns payment instruments, orders, subscriptions, and wallets. Create one before charging via the standalone resource flow, or skip ahead and let `checkout({ customer_creation: true })` create the customer + instrument inline.
## Methods [#methods]
### `createCustomer(customer)` [#createcustomercustomer]
`POST /customer`. Returns `ApiResponse`.
```ts
const { data: customer } = await easy.createCustomer({
first_name: "Ada",
last_name: "Lovelace",
email: "ada@example.com",
phone: "+15555550101",
personal_address: {
line1: "1 Pioneer Way",
city: "Menlo Park",
region: "CA",
postal_code: "94025",
country: "USA",
},
tags: { signup_source: "web" },
});
```
### `updateCustomer(customerId, partial)` [#updatecustomercustomerid-partial]
`PATCH /customer/:id`. Accepts a `Partial`.
```ts
await easy.updateCustomer(customer.id, { phone: "+15555550199" });
```
### `getCustomer(customerId)` [#getcustomercustomerid]
`GET /customer/:id`.
### `getCustomers(params?)` [#getcustomersparams]
`GET /customer`. Standard `PaginationParams` (`limit`, `offset`, `ids[]`).
```ts
const { data: customers } = await easy.getCustomers({ limit: 50, offset: 0 });
```
### `getCustomerPaymentInstruments(customerId)` [#getcustomerpaymentinstrumentscustomerid]
`GET /customer/:id/instruments`. Returns the customer's stored cards and bank accounts.
### `getCustomerOrders(customerId, params?)` [#getcustomerorderscustomerid-params]
`GET /customer/:id/orders`.
### `getCustomerSubscriptions(customerId, params?)` [#getcustomersubscriptionscustomerid-params]
`GET /customer/:id/subscriptions`. Adds an optional `status?: SubscriptionStatus` filter alongside pagination, and returns a `Paginated` envelope (`{ data, total, limit, offset }`).
```ts
const page = await easy.getCustomerSubscriptions(customer.id, {
status: "ACTIVE",
limit: 25,
});
console.log(page.data.total, page.data.data.length);
```
### `getCustomerWallets(customerId, params?)` [#getcustomerwalletscustomerid-params]
`GET /customer/:id/wallets`. Connected crypto wallets.
## Object shape [#object-shape]
`CustomerData` mirrors the underlying entity record: top-level `id`, `created_at`, `updated_at`, `tags` (string-valued bag including `company_id`), `entity` (the full PII / merchant-onboarding record), `identity_roles`, and `_links`. Refer to the API reference for the canonical schema; the SDK re-exports the type as `CustomerData` from `@easylabs/node`.
## Examples [#examples]
### Idempotent upsert by email [#idempotent-upsert-by-email]
The API does not enforce email uniqueness — implement upsert in user code by listing first:
```ts
async function upsertCustomerByEmail(email: string, defaults: { first_name: string; last_name: string }) {
const all = await easy.getCustomers({ limit: 100 });
const existing = all.data.find((c) => c.entity.email === email);
if (existing) return existing;
const { data } = await easy.createCustomer({ ...defaults, email });
return data;
}
```
### Walk every order for a customer [#walk-every-order-for-a-customer]
```ts
async function ordersFor(customerId: string) {
const limit = 100;
let offset = 0;
const acc = [];
while (true) {
const { data } = await easy.getCustomerOrders(customerId, { limit, offset });
acc.push(...data);
if (data.length < limit) break;
offset += limit;
}
return acc;
}
```
### Tag-driven partial update [#tag-driven-partial-update]
```ts
await easy.updateCustomer(customer.id, {
tags: { ...customer.tags, kyc_status: "verified" },
});
```
# Disputes (/docs/sdks/node/resources/disputes)
A dispute is opened by the cardholder's bank to challenge a charge. The SDK lets you list and read disputes, attach tags, accept the chargeback, upload evidence files, and submit the response back to the network.
## Methods [#methods]
```ts
easy.getDisputes(params?); // GET /disputes
easy.getDispute(disputeId); // GET /disputes/:id
easy.updateDispute(disputeId, tags); // PATCH /disputes/:id (tags only)
easy.acceptDispute(disputeId); // POST /disputes/:id/accept
easy.uploadDisputeEvidence(disputeId, file); // POST /disputes/:id/evidence (multipart)
easy.listDisputeEvidence(disputeId); // GET /disputes/:id/evidence
easy.submitDisputeEvidence(disputeId); // POST /disputes/:id/submit
```
### Read [#read]
```ts
const disputes = await easy.getDisputes({ limit: 50 });
const { data: dispute } = await easy.getDispute(disputes.data[0].id);
```
### Tag for triage [#tag-for-triage]
`updateDispute` only writes `tags`. The body shape is the new tag bag (server replaces, does not merge):
```ts
await easy.updateDispute(disputeId, {
triage_owner: "agent_42",
intended_response: "challenge",
});
```
### Accept the chargeback [#accept-the-chargeback]
Forfeits the disputed funds and closes the case in the bank's favor — no evidence is sent.
```ts
await easy.acceptDispute(disputeId);
```
### Upload + submit evidence [#upload--submit-evidence]
`uploadDisputeEvidence` sends `multipart/form-data` and bypasses the SDK's JSON content-type. Each file must be a `Blob` or `File` (Node 22's global `File` works), max 1 MB, allowed types `image/jpeg`, `image/png`, `application/pdf`.
```ts
import { readFile } from "node:fs/promises";
const bytes = await readFile("./receipt.pdf");
await easy.uploadDisputeEvidence(
disputeId,
new File([bytes], "receipt.pdf", { type: "application/pdf" }),
);
await easy.uploadDisputeEvidence(disputeId, /* … another file … */);
const evidence = await easy.listDisputeEvidence(disputeId);
console.log(`Attached ${evidence.data.length} files.`);
// When ready, submit the bundle:
await easy.submitDisputeEvidence(disputeId);
```
Once submitted, the response is locked in — uploads after `submitDisputeEvidence` are rejected.
## Object shape [#object-shape]
`DisputeData` mirrors the underlying processor record (snake\_case identifiers, state, amounts, network reason codes, deadlines). Refer to the API reference for the canonical schema; the SDK re-exports it as `DisputeData` from `@easylabs/node`. `DisputeEvidenceData` is currently typed as `{ id: string; [key: string]: unknown }` — refer to the API reference for the file metadata fields. {/* TODO: tighten DisputeEvidenceData once the schema stabilizes. */}
## Examples [#examples]
### Webhook-driven response workflow [#webhook-driven-response-workflow]
```ts
import { EasyWebhooks } from "@easylabs/node";
if (event.type === "dispute.created") {
const dispute = event.data as { id: string; amount: number };
if (dispute.amount < 1000) {
await easy.acceptDispute(dispute.id); // not worth fighting
} else {
await easy.updateDispute(dispute.id, { triage_owner: "queue:dispute_team" });
}
}
```
### Bulk-list open disputes [#bulk-list-open-disputes]
```ts
let offset = 0;
const open = [];
while (true) {
const page = await easy.getDisputes({ limit: 100, offset });
open.push(...page.data);
if (page.data.length < 100) break;
offset += 100;
}
```
# Dunning (/docs/sdks/node/resources/dunning)
Dunning controls what happens when a recurring charge fails: how often Easy Labs retries, what email goes out, what the recovery page looks like, and what the subscription / invoice transitions to once retries are exhausted. Revenue-recovery *automations* layer on top — event-triggered, condition-gated, multi-action workflows (e.g. "after `invoice_overdue`, if amount > $500, email Sales").
## Methods [#methods]
### Dunning config [#dunning-config]
The dunning config is a singleton per company.
```ts
easy.createOrReplaceDunningConfig(body); // POST /dunning-config
easy.getDunningConfig(); // GET /dunning-config
easy.updateDunningConfig(body); // PATCH /dunning-config
```
```ts
await easy.createOrReplaceDunningConfig({
retry_mode: "smart",
smart_retry_attempts: 8,
smart_retry_window: "2_weeks",
bank_debit_retries_enabled: true,
bank_debit_retry_schedule: [3, 7, 14], // days after failure
subscription_terminal_action: "cancel", // "cancel" | "unpaid" | "past_due" | "pause"
invoice_terminal_action: "uncollectible", // "past_due" | "uncollectible"
payment_failed_email_enabled: true,
expiring_card_email_enabled: true,
card_expiry_warn_days: 30,
payment_failed_recovery_page_mode: "hosted",
expiring_card_recovery_page_mode: "custom_link",
expiring_card_custom_link_url: "https://app.example.com/billing/update-card",
});
```
Switch to `retry_mode: "custom"` to drive the cadence yourself with `custom_retry_schedule: number[]` (days after failure).
### Revenue-recovery automations [#revenue-recovery-automations]
```ts
easy.listRevenueRecoveryAutomations(); // GET /revenue-recovery-automations
easy.createRevenueRecoveryAutomation(body); // POST /revenue-recovery-automations
easy.updateRevenueRecoveryAutomation(id, body); // PATCH /revenue-recovery-automations/:id
easy.deleteRevenueRecoveryAutomation(id); // DELETE /revenue-recovery-automations/:id
easy.listRevenueRecoveryAutomationRuns(id); // GET /revenue-recovery-automations/:id/runs
```
```ts
await easy.createRevenueRecoveryAutomation({
name: "Big-ticket overdue → Sales",
trigger_type: "invoice_overdue",
conditions: [
{ type: "invoice_amount", operator: "more_than", amount_cents: 50000 },
],
actions: [
{
type: "email_team_member",
delay_days: 0,
recipient_email: "sales@example.com",
note: "High-value overdue invoice — please follow up.",
},
],
active: true,
});
```
## Object shape [#object-shape]
### `DunningConfig` [#dunningconfig]
A single document. Highlights:
| Field | Type | Notes |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------- |
| `retry_mode` | `"smart" \| "custom"` | |
| `smart_retry_attempts` | `4 \| 8` | When `retry_mode = "smart"`. |
| `smart_retry_window` | `"1_week" \| "2_weeks" \| "3_weeks" \| "1_month" \| "2_months"` | |
| `custom_retry_schedule` | `number[]` | Days after failure, when `retry_mode = "custom"`. |
| `bank_debit_retries_enabled` / `bank_debit_retry_schedule` | `boolean` / `number[]` | Bank-debit-specific retry policy. |
| `subscription_terminal_action` | `"cancel" \| "unpaid" \| "past_due" \| "pause"` | What happens when retries are exhausted. |
| `invoice_terminal_action` | `"past_due" \| "uncollectible"` | |
| `payment_failed_email_enabled` / `expiring_card_email_enabled` / `email_action_required` | `boolean` | |
| `card_expiry_warn_days` | `number` | |
| `payment_failed_recovery_page_mode` / `expiring_card_recovery_page_mode` | `"hosted" \| "custom_link"` | |
| `payment_failed_custom_link_url` / `expiring_card_custom_link_url` | `string \| null` | |
### Revenue-recovery automation [#revenue-recovery-automation]
`CreateRevenueRecoveryAutomation`:
| Field | Type |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `trigger_type` | `"invoice_due_date_upcoming" \| "invoice_finalized" \| "invoice_overdue" \| "subscription_payment_failed" \| "subscription_canceled"` |
| `conditions[]` | `{ type: "invoice_amount" \| "invoice_metadata" \| "product_on_invoice"; operator?, amount_cents?, key?, value?, product_id?, product_name? }` |
| `actions[]` | `{ type: "email_team_member" \| "mark_invoice_uncollectible"; delay_days?, recipient_user_id?, recipient_email?, recipient_name?, note? }` |
| `active` | `boolean` |
The list endpoint and run records are currently typed as `Record[]` pending a stronger schema. {/* TODO: lock down the automation list/run shape once it stabilizes. */}
## Examples [#examples]
### Switch to custom retry cadence [#switch-to-custom-retry-cadence]
```ts
await easy.updateDunningConfig({
retry_mode: "custom",
custom_retry_schedule: [1, 3, 7, 14, 30],
});
```
### Disable a dunning automation without deleting it [#disable-a-dunning-automation-without-deleting-it]
```ts
await easy.updateRevenueRecoveryAutomation(automationId, { active: false });
```
### Audit recent runs [#audit-recent-runs]
```ts
const runs = await easy.listRevenueRecoveryAutomationRuns(automationId);
console.table(runs.data);
```
# Embedded Checkout (/docs/sdks/node/resources/embedded-checkout)
Embedded checkout creates a server-side session that the browser opens in an iframe. The session carries a one-time `client_secret` so the iframe-side validation and confirmation calls can be authorized without your secret API key.
`@easylabs/node` is a server-side SDK — `createClient({ apiKey })` validates a secret API key, so the package itself must never be loaded in a browser bundle. Two of the methods on this resource — `validateEmbeddedCheckoutSession` and `confirmEmbeddedCheckoutSession` — still authenticate with the session's `client_secret` rather than the API key (the SDK suppresses the API-key header for those two requests), but you call them from your own server endpoints, which the iframe reaches via your own `fetch`. If you'd rather hit `POST /embedded-checkout/validate` and `POST /embedded-checkout/confirm` directly from the browser with the `client_secret`, that's also supported — just don't reach for the Node SDK to do it.
## Methods [#methods]
### `createEmbeddedCheckoutSession(data)` [#createembeddedcheckoutsessiondata]
`POST /embedded-checkout`. Returns `ApiResponse`.
```ts
const { data: session } = await easy.createEmbeddedCheckoutSession({
line_items: [{ price_id: "PR_...", quantity: 1 }],
mode: "payment", // or "subscription"
return_url: "https://app.example.com/checkout/return",
success_url: "https://app.example.com/checkout/success",
cancel_url: "https://app.example.com/checkout/cancel",
customer_email: "ada@example.com",
payment_methods: ["card"], // include "crypto" to accept Solana Pay
metadata: { cart_id: "cart_42" },
});
// Hand session.client_secret to the iframe; render session.url.
```
### `getEmbeddedCheckoutSession(sessionId)` [#getembeddedcheckoutsessionsessionid]
`GET /embedded-checkout/:id`. Returns `EmbeddedCheckoutSessionStatus` — `status` (`"open" | "complete" | "expired"`), `payment_status`, `amount_total`, `line_items`, `customer_email`, `metadata`, `created_at`, `completed_at`.
### `getCryptoPaymentStatus(sessionId)` [#getcryptopaymentstatussessionid]
`GET /embedded-checkout/:id/crypto-status`. Poll for the on-chain confirmation when `payment_methods` includes `"crypto"`.
### `validateEmbeddedCheckoutSession(body)` *(server-side, client-secret authed)* [#validateembeddedcheckoutsessionbody-server-side-client-secret-authed]
`POST /embedded-checkout/validate`. Lives on the server-side client returned by `createClient`, but authenticates with the session's `client_secret` rather than the API key — the SDK suppresses the `x-easy-api-key` header for this single call. Rate-limited (\~30 req/min per session). Returns the resolved session config — branding, line items, allowed origins. Expose this through your own server endpoint that the iframe calls.
### `confirmEmbeddedCheckoutSession(body)` *(server-side, client-secret authed)* [#confirmembeddedcheckoutsessionbody-server-side-client-secret-authed]
`POST /embedded-checkout/confirm`. Same auth model as `validateEmbeddedCheckoutSession`: server-side method, `client_secret`-authed, API-key header suppressed. Submits the tokenized payment source and finalizes the session.
### `getEmbeddedCheckoutConfig()` [#getembeddedcheckoutconfig]
`GET /embedded-checkout/config`. Server-only. Returns the company's allowed-origins config.
### `updateEmbeddedCheckoutConfig(body)` [#updateembeddedcheckoutconfigbody]
`PATCH /embedded-checkout/config`. Pass `{ allowed_origins: ["https://app.example.com", "https://*.example.com"] }`. An empty array disables embedding entirely.
## Object shape [#object-shape]
`EmbeddedCheckoutSessionData` (returned by create):
| Field | Type | Notes |
| ---------------- | -------------------------------- | --------------------------------------------------- |
| `id` | `string` | Session ID. |
| `client_secret` | `string` | One-time secret for the browser. Do not log. |
| `url` | `string` | URL to embed in the iframe. |
| `amount_total` | `number` | Smallest currency unit. |
| `currency` | `string` | |
| `status` | `string` | Initial status. |
| `expires_at` | `string` | ISO timestamp. |
| `crypto_payment` | `CryptoPaymentInfo \| undefined` | Present when `payment_methods` includes `"crypto"`. |
`EmbeddedCheckoutConfig`: `id`, `company_id`, `allowed_origins: string[]`, `created_at`, `updated_at`.
## Examples [#examples]
### Create + render [#create--render]
```ts
// app/api/checkout/route.ts
export async function POST(req: Request) {
const { priceId } = await req.json();
const { data } = await easy.createEmbeddedCheckoutSession({
line_items: [{ price_id: priceId, quantity: 1 }],
mode: "payment",
return_url: "https://app.example.com/checkout/return",
});
return Response.json({ clientSecret: data.client_secret, url: data.url });
}
```
### Authorize an embedding origin [#authorize-an-embedding-origin]
```ts
await easy.updateEmbeddedCheckoutConfig({
allowed_origins: [
"https://app.example.com",
"https://*.example.com", // wildcard subdomains
],
});
```
### Poll a crypto session [#poll-a-crypto-session]
```ts
async function awaitCryptoConfirmation(sessionId: string, deadlineMs: number) {
while (Date.now() < deadlineMs) {
const { data } = await easy.getCryptoPaymentStatus(sessionId);
if (data.status === "confirmed") return data;
if (data.status === "failed" || data.status === "expired") throw new Error(data.status);
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error("timeout");
}
```
# Invoices (/docs/sdks/node/resources/invoices)
Invoices are itemized bills you send to a buyer with a due date — either to be paid online (`charge_automatically`) or via the hosted invoice page (`send_invoice`). The SDK covers the full lifecycle: create, list, send, charge, remind, void, and pull the data you'd need to render the PDF yourself.
## Methods [#methods]
```ts
easy.createInvoice(body); // POST /invoices
easy.listInvoices(query?); // GET /invoices
easy.getInvoice(invoiceId); // GET /invoices/:id
easy.updateInvoice(invoiceId, body); // PATCH /invoices/:id
easy.sendInvoice(invoiceId, body?); // POST /invoices/:id/send
easy.payInvoice(invoiceId, body?); // POST /invoices/:id/pay
easy.remindInvoice(invoiceId); // POST /invoices/:id/remind
easy.voidInvoice(invoiceId); // POST /invoices/:id/void
easy.getInvoicePdfData(invoiceId); // GET /invoices/:id/pdf
```
### Create [#create]
```ts
const invoice = await easy.createInvoice({
to_email: "ada@example.com",
to_company_name: "Analytical Engines Co.",
collection_method: "send_invoice",
currency: "USD",
due_date: "2026-06-15",
issue_date: "2026-05-15",
items: [
{ description: "Consulting (May)", quantity: 20, unit_price: 25000 }, // $250/hr × 20
],
tax_rate: 8.5, // percentage
notes: "Net-30, thanks!",
terms: "Pay by ACH or card.",
attach_pdf: true,
});
```
### List with filters [#list-with-filters]
```ts
const overdue = await easy.listInvoices({
status: "OVERDUE",
collection_method: "send_invoice",
due_date_to: new Date().toISOString(),
limit: 100,
});
```
### Send [#send]
```ts
await easy.sendInvoice(invoice.data.id, {
cc_recipients: ["billing@example.com"],
attach_pdf: true,
});
```
### Charge an open invoice [#charge-an-open-invoice]
```ts
import { randomUUID } from "node:crypto";
await easy.payInvoice(invoice.data.id, {
instrument_id: paymentInstrumentId, // omit to use the customer's default
amount: 50000, // omit to charge full balance
idempotency_key: randomUUID(),
});
```
### Remind, void, or void-and-write-off [#remind-void-or-void-and-write-off]
```ts
await easy.remindInvoice(invoiceId); // sends reminder email
await easy.voidInvoice(invoiceId); // marks VOID; cannot be undone
```
## Object shape [#object-shape]
`InvoiceData` (subset of the most-touched fields):
| Field | Type | Notes |
| ----------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `id` / `invoice_number` | `string \| null` | Number assigned on finalization. |
| `status` | `InvoiceStatus` | `DRAFT`, `OPEN`, `SENT`, `VIEWED`, `OVERDUE`, `PARTIALLY_PAID`, `PAID`, `UNPAID`, `VOID`, `VOIDED`. |
| `collection_method` | `"charge_automatically" \| "send_invoice"` | |
| `currency` | `string` | |
| `to_email` / `to_company_name` / `to_contact_name` / `to_address` | various | Recipient. |
| `subtotal_amount` / `tax_amount` / `discount_amount` / `shipping_amount` / `total_amount` | `number` | Smallest currency unit. |
| `amount_paid` / `amount_due` | `number` | Running totals. |
| `due_date` / `issue_date` / `paid_at` / `voided_at` / `last_sent_at` / `last_viewed_at` | `string \| null` | ISO timestamps. |
| `items` | `InvoiceItem[]` | Line items. |
| `is_recurring` / `recurrence_interval` / `recurrence_end_date` | various | Recurring invoice config. |
| `auto_reminders` | `boolean` | Enable scheduled reminders. |
| `tax_ids` / `custom_fields` / `branding_overrides` | various | |
| `pdf_storage_path` | `string \| null` | Where the rendered PDF lives. |
## Examples [#examples]
### Recurring monthly invoice [#recurring-monthly-invoice]
```ts
await easy.createInvoice({
to_email: "ops@example.com",
collection_method: "send_invoice",
due_date: "2026-06-30",
is_recurring: true,
recurrence_interval: "MONTHLY",
recurrence_end_date: "2027-05-31",
recurrence_auto_send: true,
items: [{ description: "Retainer", quantity: 1, unit_price: 500000 }],
});
```
### Sweep all overdue invoices nightly [#sweep-all-overdue-invoices-nightly]
```ts
const overdue = await easy.listInvoices({ status: "OVERDUE", limit: 100 });
for (const inv of overdue.data) {
if (inv.collection_method === "send_invoice") {
await easy.remindInvoice(inv.id);
} else {
await easy.payInvoice(inv.id, { idempotency_key: `retry:${inv.id}:${new Date().toISOString().slice(0, 10)}` });
}
}
```
### Render the PDF yourself [#render-the-pdf-yourself]
`getInvoicePdfData` returns a normalized payload (currently typed as `Record`) shaped for client-side PDF generation. Pipe it into your renderer of choice:
```ts
const { data } = await easy.getInvoicePdfData(invoiceId);
// hand `data` to your PDF template (e.g. react-pdf, puppeteer, weasyprint)
```
# Orders (/docs/sdks/node/resources/orders)
An order is the application-level record of a purchase: who bought what, for how much, against which payment instrument, and which transfer captured the funds. Orders are produced by `checkout`, embedded checkout, payment links, and subscription invoicing — there is no `createOrder` method.
## Methods [#methods]
### `getOrder(orderId)` [#getorderorderid]
`GET /orders/:id`.
### `getOrders(params?)` [#getordersparams]
`GET /orders`. Standard pagination.
### `updateOrderTags(orderId, tags)` [#updateordertagsorderid-tags]
`PATCH /orders/:id`. The only mutable field is `tags`. Pass the full tag bag you want stored — server replaces, it does not merge.
```ts
await easy.updateOrderTags(orderId, {
fulfilment_state: "shipped",
tracking_number: "1Z999AA10123456784",
});
```
To list orders for a single customer, use [`getCustomerOrders`](./customers#getcustomerorderscustomerid-params).
## Object shape [#object-shape]
`OrderData` includes:
| Field | Type | Notes |
| ----------------------------------------------------- | ------------------------- | ------------------------------------------------------- |
| `id` / `order_number` | `string` | UUID + human-friendly number. |
| `subtotal_cents` / `tax_amount_cents` / `total_cents` | `number` | All in the smallest currency unit. |
| `currency` | `string` | ISO 4217. |
| `merchant_id` / `company_id` | `string` | |
| `source` | `string` | `"checkout"`, `"payment_link"`, `"subscription"`, etc. |
| `payment_instrument` | `PaymentInstrumentData` | Instrument used. |
| `transfer` | `TransferData \| null` | The capture (null for declined / cancelled orders). |
| `payment_link_id` | `string \| null` | Set when sourced from a payment link. |
| `purchase_items` | `PurchaseItemData[]` | Line items with embedded `price_data` + `product_data`. |
| `buyer_details` | object | `email`, `phone`, name, billing address. |
| `shipping_address` / `billing_address` | `Address \| null` | |
| `failure_code` / `failure_message` | `string \| null` | Populated on declines. |
| `tax_rate_id` | `string \| null` | |
| `metadata` / `tags` | `Record` | |
## Examples [#examples]
### Reconcile recent orders [#reconcile-recent-orders]
```ts
const orders = await easy.getOrders({ limit: 100 });
for (const o of orders.data) {
if (!o.transfer || o.transfer.state !== "SUCCEEDED") continue;
// post o.total_cents to your ledger keyed on o.order_number
}
```
### Attach fulfilment metadata when a webhook fires [#attach-fulfilment-metadata-when-a-webhook-fires]
```ts
import { EasyWebhooks } from "@easylabs/node";
// Inside your webhook handler:
const event = EasyWebhooks.constructEvent(rawBody, sigHeader, secret);
if (event.type === "checkout.session.completed") {
const order = event.data as { id: string; order_number: string };
await easy.updateOrderTags(order.id, {
fulfilment_state: "queued",
queued_at: new Date().toISOString(),
});
}
```
### Surface a declined order to support [#surface-a-declined-order-to-support]
```ts
const { data: order } = await easy.getOrder(orderId);
if (order.failure_code) {
console.warn(`Order ${order.order_number} failed: ${order.failure_message} (${order.failure_code})`);
}
```
# Payment Instruments (/docs/sdks/node/resources/payment-instruments)
A payment instrument is a tokenized card or bank account attached to a customer (`identityId`). Card and bank PANs never touch your servers — collect them through the Easy frontend SDK or Basis Theory and pass the resulting `tokenId` here.
## Methods [#methods]
### `createPaymentInstrument(data)` [#createpaymentinstrumentdata]
`POST /payment`. Validates that `tokenId` is present, then sends one of two discriminated bodies:
```ts
// Card
await easy.createPaymentInstrument({
type: "PAYMENT_CARD",
identityId: customer.id,
tokenId: tokenFromFrontend,
name: "Ada Lovelace",
address: {
line1: "1 Pioneer Way",
city: "Menlo Park",
region: "CA",
postal_code: "94025",
country: "USA",
},
});
// Bank account
await easy.createPaymentInstrument({
type: "BANK_ACCOUNT",
identityId: customer.id,
tokenId: tokenFromFrontend,
name: "Ada Lovelace",
accountType: "PERSONAL_CHECKING",
attempt_bank_account_validation_check: true,
});
```
The SDK throws synchronously if `tokenId` is missing or `type` is unrecognized.
### `updatePaymentInstrument(paymentInstrumentId, data)` [#updatepaymentinstrumentpaymentinstrumentid-data]
`PATCH /payment/:id`. Accepts an `UpdatePaymentInstrument` shape:
```ts
await easy.updatePaymentInstrument(piId, {
enabled: false,
account_updater_enabled: true,
network_token_enabled: true,
address: { line1: "2 Pioneer Way", city: "Menlo Park", region: "CA", postal_code: "94025", country: "USA", line2: null },
tags: { source: "manual_review" },
});
```
### `getCustomerPaymentInstruments(customerId)` [#getcustomerpaymentinstrumentscustomerid]
Listed under [Customers](./customers) — there is no top-level "list all instruments" endpoint; always scope by customer.
## Object shape [#object-shape]
Two response shapes show up depending on which endpoint you hit:
* `FinixPaymentInstrumentData` — returned by `createPaymentInstrument`. Mirrors the underlying processor record (snake\_case identifiers, `_links`, `instrument_type`, etc.).
* `PaymentInstrumentData` — returned by listing endpoints and `updatePaymentInstrument`. The application-level shape: `id`, `identity_id`, `type` (`"PAYMENT_CARD" | "BANK_ACCOUNT"`), brand / `last_four` / `expiration_*` for cards, `bank_code` / `masked_account_number` / `account_type` / `bank_account_validation_check` for bank accounts, plus `enabled`, `account_updater_enabled`, `network_token_enabled`, `address`, `tags`.
## Examples [#examples]
### Disable an instrument without deleting it [#disable-an-instrument-without-deleting-it]
```ts
await easy.updatePaymentInstrument(instrumentId, { enabled: false });
```
Disabled instruments stay attached to the customer for audit but cannot be charged.
### Re-trigger bank-account validation [#re-trigger-bank-account-validation]
```ts
await easy.updatePaymentInstrument(bankAccountId, {
attempt_bank_account_validation_check: true,
});
```
### Choose the default for a customer [#choose-the-default-for-a-customer]
The default-instrument decision is currently part of charge calls (`payInvoice({ instrument_id })`, `createSubscription({ instrument_id })`, etc.) rather than a dedicated method on the instrument itself. {/* TODO: confirm whether a dedicated "set default" endpoint exists. */}
# Payment Links (/docs/sdks/node/resources/payment-links)
Payment links are hosted, no-code checkout pages — share a URL and Easy Labs handles collection, instrument capture, branding, and receipts. Use them for invoicing-as-a-link, donation pages, single-product sales, or multi-use storefronts.
## Methods [#methods]
### `createPaymentLink(payload)` [#createpaymentlinkpayload]
`POST /payment-link`. Returns `ApiResponse<{ id: string }>` — fetch the full link with `getPaymentLink` if you need the URL.
```ts
// `products[].id` references a Product you've already created (see
// /resources/products-pricing). Each entry then lists one or more
// `product_prices` by Price ID, with the quantity to bill.
const { data } = await easy.createPaymentLink({
nickname: "Conference ticket",
amount_type: "FIXED",
products: [
{
id: "PROD_vip_pass",
product_prices: [{ id: "PR_vip_pass_25000", quantity: 1 }],
},
],
allowed_payment_methods: ["PAYMENT_CARD"],
payment_limit: 100,
collect_buyer_details: "more",
});
const link = await easy.getPaymentLink(data.id);
console.log(link.data.link_url);
```
### `getPaymentLinks(params?)` [#getpaymentlinksparams]
`GET /payment-link/`. Standard pagination.
### `getPaymentLink(paymentLinkId)` [#getpaymentlinkpaymentlinkid]
`GET /payment-link/:id`.
### `updatePaymentLink(paymentLinkId, body)` [#updatepaymentlinkpaymentlinkid-body]
`PATCH /payment-link/:id`. Accepts `UpdatePaymentLinkPayload` (all `CreatePaymentLinkPayload` fields are optional).
```ts
await easy.updatePaymentLink(linkId, { branding_overrides: { brand_color: "#0F172A" } });
```
### `deletePaymentLink(paymentLinkId)` [#deletepaymentlinkpaymentlinkid]
`DELETE /payment-link/:id`.
### `getPaymentLinkPayments(paymentLinkId, params?)` [#getpaymentlinkpaymentspaymentlinkid-params]
`GET /payment-link/:id/payments`. Returns the orders that closed against the link.
## Object shape [#object-shape]
`PaymentLinkData` carries the rendered hosted-page configuration plus runtime state: `id`, `link_url`, `link_expires_at`, `state` (`"DRAFT" | "ACTIVE" | "INACTIVE" | "EXPIRED"`), `payment_count`, `payment_limit`, `payment_frequency` (`"ONE_TIME" | "RECURRING"`), `is_multiple_use`, `items[]` (with `price_details`), `amount_details` (`amount_type`, totals, optional min/max for variable links), `branding`, `branding_overrides`, `additional_details` (`collect_*`, return URLs, expiration), `buyer_details` once the buyer has filled them in, and the standard `tags` bag.
## Examples [#examples]
### Variable-amount donation link [#variable-amount-donation-link]
```ts
// For a VARIABLE-amount link, omit the product `id` and pass an empty
// `product_prices` array — the buyer chooses the amount within
// `min_amount` / `max_amount`.
await easy.createPaymentLink({
nickname: "Donations",
amount_type: "VARIABLE",
products: [{ product_prices: [] }],
min_amount: 500,
max_amount: 100000,
payment_limit: null, // unlimited
});
```
### Single-use invoice replacement [#single-use-invoice-replacement]
```ts
// Pre-create a Product + Price for "Invoice #2024-117" via createProduct /
// createPrice, then reference the resulting IDs here.
const link = await easy.createPaymentLink({
amount_type: "FIXED",
products: [
{
id: "PROD_invoice_2024_117",
product_prices: [{ id: "PR_invoice_2024_117", quantity: 1 }],
},
],
payment_limit: 1,
collect_buyer_details: "more",
});
```
### Reconcile payments for a link [#reconcile-payments-for-a-link]
```ts
const orders = await easy.getPaymentLinkPayments(linkId, { limit: 100 });
for (const o of orders.data) {
console.log(o.order_number, o.total_cents, o.identity?.entity.email);
}
```
# Products & Pricing (/docs/sdks/node/resources/products-pricing)
Products are the catalog rows ("Pro plan", "Setup fee"); prices attach a currency, amount, and (for recurring prices) an interval to a product. Subscriptions and checkout line items reference *prices*, not products — a single product typically has several active prices (monthly vs. yearly, USD vs. EUR, etc.).
## Methods [#methods]
### Products [#products]
```ts
easy.createProduct(body); // POST /products
easy.getProduct(productId); // GET /products/:id
easy.getProducts(params?); // GET /products
easy.updateProduct(productId, body); // PATCH /products/:id
easy.archiveProduct(productId); // PATCH /products/:id/archive
easy.getProductWithPrices(productId); // GET /products/:id/prices
easy.getProductWithPrice(productId, priceId); // GET /products/:id/prices/:priceId
```
`createProduct` payload:
```ts
await easy.createProduct({
name: "Pro plan",
active: true,
description: "Everything in Free, plus team seats.",
image_url: "https://cdn.example.com/pro.png",
statement_descriptor: "EXAMPLE PRO",
unit_label: "seat",
metadata: { tier: "pro" },
default_price_id: undefined, // set after you create a price
});
```
`updateProduct` accepts `Partial` — including switching `default_price_id` once you have one. `archiveProduct` flips it to inactive without deleting historical orders.
### Prices [#prices]
```ts
easy.createPrice(body); // POST /product-prices
easy.getPrice(priceId); // GET /product-prices/:id
easy.getPrices(params?); // GET /product-prices
easy.updatePrice(priceId, body); // PATCH /product-prices/:id
easy.archivePrice(priceId); // PATCH /product-prices/:id/archive
```
`CreatePrice` is a discriminated union on `recurring`:
```ts
// Recurring
await easy.createPrice({
product_id: "PD_...",
active: true,
recurring: true,
currency: "USD",
unit_amount: 2900, // $29.00
interval: "month",
interval_count: 1,
tax_behavior: "exclusive",
pricing_model: "per_unit",
trial_period_days: 14,
});
// One-time
await easy.createPrice({
product_id: "PD_...",
active: true,
recurring: false,
currency: "USD",
unit_amount: 4999,
tax_behavior: "exclusive",
});
```
`updatePrice` accepts only the fields the API treats as editable in flight: `active`, `metadata`, `description`. Everything else (amount, currency, interval) is immutable — create a new price and switch the product's `default_price_id`.
## Object shape [#object-shape]
### `ProductData` [#productdata]
`id`, `name`, `description`, `active`, `image_url`, `statement_descriptor`, `unit_label`, `default_price_id`, `metadata`, `created_at`, `updated_at`, plus `company_id` (stripped on the nested `getProductWithPrices` response via `PickExcept`).
### `PriceData` [#pricedata]
| Field | Type | Notes |
| ------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| `id` | `string` | |
| `product_id` | `string` | |
| `currency` | `CurrencyCode` | ISO 4217. |
| `unit_amount` | `number` | Smallest currency unit. |
| `recurring` | `boolean` | |
| `interval` | `"day" \| "week" \| "month" \| "year" \| null` | Null on one-time prices. |
| `interval_count` | `number \| null` | E.g. `3` with `interval: "month"` for quarterly. |
| `pricing_model` | `"per_unit" \| "tiered_volume" \| "tiered_graduated" \| "package" \| "metered" \| "flat_rate" \| null` | |
| `trial_period_days` | `number \| null` | |
| `tax_behavior` | `"exclusive" \| "inclusive"` | |
| `tax_rate_id` | `string \| null` | |
| `description` | `string \| null` | |
| `metadata` | `Record` | |
| `active` | `boolean` | |
## Examples [#examples]
### Spin up a new monthly plan [#spin-up-a-new-monthly-plan]
```ts
const product = await easy.createProduct({ name: "Team plan", active: true });
const price = await easy.createPrice({
product_id: product.data.id,
active: true,
recurring: true,
currency: "USD",
unit_amount: 4900,
interval: "month",
interval_count: 1,
tax_behavior: "exclusive",
});
await easy.updateProduct(product.data.id, { default_price_id: price.data.id });
```
### Migrate to a new price (deprecate the old one) [#migrate-to-a-new-price-deprecate-the-old-one]
```ts
const newPrice = await easy.createPrice({ /* … */ });
await easy.updateProduct(productId, { default_price_id: newPrice.data.id });
await easy.archivePrice(oldPriceId);
```
### Read product + every price in one round-trip [#read-product--every-price-in-one-round-trip]
```ts
const { data } = await easy.getProductWithPrices(productId);
for (const p of data.prices) {
console.log(p.id, p.unit_amount, p.interval);
}
```
# Promotion Codes (/docs/sdks/node/resources/promotion-codes)
Promotion codes are the buyer-facing strings ("`SPRING25`") that resolve to a [coupon](./coupons). One coupon can have multiple codes (e.g. campaign- or partner-specific), each with its own redemption limits, expiry, and eligibility rules.
## Methods [#methods]
```ts
easy.createPromotionCode(body); // POST /promotion-codes
easy.listPromotionCodes(params?); // GET /promotion-codes
easy.getPromotionCode(promotionCodeId); // GET /promotion-codes/:id
easy.updatePromotionCode(promotionCodeId, body); // PATCH /promotion-codes/:id
easy.deletePromotionCode(promotionCodeId); // DELETE /promotion-codes/:id
easy.validatePromotionCode(body); // POST /promotion-codes/validate
```
```ts
const promo = await easy.createPromotionCode({
coupon_id: coupon.data.id,
code: "SPRING25",
active: true,
max_redemptions: 500,
valid_until: "2026-06-30T23:59:59Z",
first_time_only: true,
minimum_amount: 5000, // require $50 minimum cart
metadata: { campaign: "spring_2026" },
});
```
`UpdatePromotionCode` is narrow: only `active` and `metadata` are mutable. To change the coupon, code, or redemption rules, create a new promotion code.
## Validation [#validation]
`validatePromotionCode` previews whether a code would apply for a given customer / amount before you let them apply it. Returns `{ valid, promotion_code?, coupon?, discount_preview?, reason? }`:
```ts
const { data } = await easy.validatePromotionCode({
code: userInput,
identity_id: customer.id, // optional
amount: cartTotalCents, // optional
});
if (!data.valid) {
throw new Error(data.reason ?? "Invalid promo code");
}
console.log(data.discount_preview); // { amount_off } or { percent_off }
```
The corresponding redemption happens when you call `applySubscriptionDiscount(subId, { promotion_code: "SPRING25" })`.
## Object shape [#object-shape]
`PromotionCodeData`:
| Field | Type | Notes |
| ----------------- | ------------------------- | ------------------------------------------------------------ |
| `id` | `string` | |
| `coupon_id` | `string` | The coupon this code resolves to. |
| `code` | `string` | The buyer-facing string. |
| `active` | `boolean` | |
| `max_redemptions` | `number \| null` | Per-code cap. |
| `times_redeemed` | `number` | |
| `valid_until` | `string \| null` | ISO timestamp. |
| `first_time_only` | `boolean` | If true, only redeemable on a customer's first subscription. |
| `minimum_amount` | `number \| null` | Smallest currency unit. |
| `metadata` | `Record` | |
## Examples [#examples]
### Validate-then-apply at checkout [#validate-then-apply-at-checkout]
```ts
const validation = await easy.validatePromotionCode({
code: input,
identity_id: customer.id,
amount: cart.totalCents,
});
if (!validation.data.valid) return reply.code(400).send({ error: validation.data.reason });
await easy.applySubscriptionDiscount(subId, { promotion_code: input });
```
### Disable a code mid-campaign [#disable-a-code-mid-campaign]
```ts
await easy.updatePromotionCode(promoId, { active: false });
```
### Per-partner unique codes [#per-partner-unique-codes]
```ts
const codes = ["PARTNERA", "PARTNERB", "PARTNERC"];
for (const code of codes) {
await easy.createPromotionCode({
coupon_id: couponId,
code,
metadata: { partner: code.toLowerCase() },
});
}
```
# Refunds (/docs/sdks/node/resources/refunds)
A refund is a reversal of an existing transfer. The Easy API models it as a child transfer (`type: "REVERSAL"`, `parent_transfer` pointing at the original) created against the original transfer's `/reversals` collection.
## Methods [#methods]
### `createRefund(transferId, body)` [#createrefundtransferid-body]
`POST /transfer/:id/reversals`. Returns `ApiResponse` — the reversal transfer itself.
```ts
// Full refund
await easy.createRefund(transferId, { refund_amount: originalAmount });
// Partial refund
await easy.createRefund(transferId, { refund_amount: 500 });
// With tags for reconciliation
await easy.createRefund(transferId, {
refund_amount: 500,
tags: { reason: "shipping_delay", approved_by: "agent_42" },
});
```
`refund_amount` is in the smallest currency unit (cents for USD). The API enforces the partial-refund accumulation limit — issuing more than the original captured amount returns 422.
There is no dedicated "list refunds" method. List transfers and filter by `type === "REVERSAL"` and `parent_transfer === originalTransferId`, or follow the `_links.reversals.href` on the parent transfer.
## Object shape [#object-shape]
The returned `TransferData` carries:
* `type: "REVERSAL"` — distinguishes it from forward charges.
* `parent_transfer: string` — the ID of the transfer being reversed.
* `parent_transfer_trace_id: string | null` — convenience trace.
* `amount` — the refunded amount in smallest currency units.
* `state` — `PENDING` initially, transitioning to `SUCCEEDED` when the funds clear back.
* `tags` — anything you passed in `body.tags`.
Refer to [Transfers](./transfers) for the rest of the field list.
## Examples [#examples]
### Refund-on-cancel [#refund-on-cancel]
```ts
async function cancelAndRefund(orderId: string, reason: string) {
const { data: order } = await easy.getOrder(orderId);
if (!order.transfer) throw new Error("Order has no captured transfer.");
return easy.createRefund(order.transfer.id, {
refund_amount: order.transfer.amount,
tags: { reason, source: "support_console" },
});
}
```
### Partial refund driven by a webhook [#partial-refund-driven-by-a-webhook]
When you receive a `dispute.created` event you may want to refund preemptively rather than fight:
```ts
async function refundDisputedTransfer(transferId: string) {
const { data: t } = await easy.getTransfer(transferId);
return easy.createRefund(transferId, {
refund_amount: t.amount,
tags: { reason: "dispute_received" },
});
}
```
### Reconcile the reversal back to your ledger [#reconcile-the-reversal-back-to-your-ledger]
```ts
const reversal = await easy.createRefund(transferId, { refund_amount: 250 });
console.log({
original: reversal.data.parent_transfer,
reversal: reversal.data.id,
state: reversal.data.state,
});
```
# Settlements (/docs/sdks/node/resources/settlements)
A settlement bundles a batch of transfers into a single payout to your bank. The SDK exposes read-only listing plus a manual `closeSettlement` action used by platforms that drive their own batch boundaries.
## Methods [#methods]
```ts
easy.getSettlements(params?); // GET /settlements
easy.getSettlement(settlementId); // GET /settlements/:id
easy.closeSettlement(settlementId); // PATCH /settlements/:id
```
```ts
const recent = await easy.getSettlements({ limit: 25 });
const { data: settlement } = await easy.getSettlement(recent.data[0].id);
// Manually close an open settlement for a same-day payout:
await easy.closeSettlement(settlement.id);
```
## Object shape [#object-shape]
`SettlementData` mirrors the underlying processor record (snake\_case fields, `_links`, batch-level totals and counts). Refer to the API reference for the canonical schema; the SDK re-exports the type as `SettlementData` from `@easylabs/node`.
## Examples [#examples]
### Reconcile a settlement against your ledger [#reconcile-a-settlement-against-your-ledger]
```ts
const { data } = await easy.getSettlement(settlementId);
// Compare data.total_amount / data.fee_amount / data.transfer_count to your records.
```
### Sweep recently closed settlements [#sweep-recently-closed-settlements]
```ts
const settlements = await easy.getSettlements({ limit: 100 });
for (const s of settlements.data) {
// post each to your accounting system using s.id as the idempotency key
}
```
# Subscriptions (/docs/sdks/node/resources/subscriptions)
A subscription is a recurring billing relationship between a customer and one or more prices. The SDK exposes the full lifecycle — create, update items, pause/resume, apply discounts, report metered usage, preview proration, cancel — plus one-time charges that ride along on the next invoice.
## Methods [#methods]
### Lifecycle [#lifecycle]
```ts
easy.createSubscription(body); // POST /subscriptions
easy.getSubscription(id); // GET /subscriptions/:id
easy.getSubscriptions(params?); // GET /subscriptions
easy.updateSubscription(id, body); // PATCH /subscriptions/:id
easy.cancelSubscription(id, { at_period_end? }); // DELETE /subscriptions/:id
easy.pauseSubscription(id, { behavior, resumes_at? }); // POST /subscriptions/:id/pause
easy.resumeSubscription(id); // POST /subscriptions/:id/resume
easy.getSubscriptionProrationPreview(id, params?); // GET /subscriptions/:id/proration-preview
```
```ts
const { data: sub } = await easy.createSubscription({
identity_id: customer.id,
items: [{ price_id: "PR_monthly", quantity: 1 }],
instrument_id: paymentInstrumentId,
proration_behavior: "create_prorations",
trial_period_days: 14,
metadata: { plan: "pro_monthly" },
});
// Schedule cancellation at period end:
await easy.cancelSubscription(sub.id, { at_period_end: true });
// Pause until a date:
await easy.pauseSubscription(sub.id, {
behavior: "mark_uncollectible",
resumes_at: "2026-08-01T00:00:00Z",
});
await easy.resumeSubscription(sub.id);
```
`pauseSubscription`'s `behavior` is `"void" | "keep_as_draft" | "mark_uncollectible"` — controls how invoices generated during the pause are handled.
### Items [#items]
```ts
easy.addSubscriptionItem(subId, { price_id, quantity? });
easy.updateSubscriptionItem(subId, itemId, { quantity });
easy.removeSubscriptionItem(subId, itemId);
```
### Discounts [#discounts]
```ts
easy.applySubscriptionDiscount(subId, body); // POST /subscriptions/:id/discounts
easy.listSubscriptionDiscounts(subId); // GET /subscriptions/:id/discounts
easy.removeSubscriptionDiscount(subId, discountId); // DELETE
```
`body` is exactly one of:
```ts
{ coupon_id: string; subscription_item_id?: string }
| { promotion_code: string; subscription_item_id?: string }
```
### One-time charges [#one-time-charges]
```ts
await easy.createOneTimeCharge(subId, {
price_id: "PR_setup_fee",
quantity: 1,
description: "Implementation fee",
due_date: "2026-06-01",
});
```
The charge attaches to the subscription's next invoice without changing recurring items.
### Metered usage [#metered-usage]
```ts
await easy.reportSubscriptionUsage(subId, {
subscription_item_id: itemId,
quantity: 42,
action: "increment", // or "set"
timestamp: new Date().toISOString(),
idempotency_key: deliveryEventId,
});
await easy.getSubscriptionUsageSummary(subId, {
subscription_item_id: itemId,
from: "2026-04-01",
to: "2026-04-30",
});
await easy.getSubscriptionUsageReconciliation(subId, {
subscription_item_id: itemId,
period_start: "2026-04-01",
period_end: "2026-04-30",
});
```
### Proration preview [#proration-preview]
Run a dry-run before applying item changes:
```ts
const preview = await easy.getSubscriptionProrationPreview(subId, {
items: [{ price_id: "PR_higher_tier", quantity: 1 }],
remove_items: [oldItemId],
proration_date: new Date().toISOString(),
});
```
## Object shape [#object-shape]
`SubscriptionData`:
| Field | Type | Notes |
| ----------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `id` | `string` | |
| `identity_id` / `instrument_id` | `string \| string \| null` | |
| `status` | `SubscriptionStatus` | `TRIALING`, `ACTIVE`, `PAST_DUE`, `CANCELED`, `UNPAID`, `PAUSED`, `INCOMPLETE`, `INCOMPLETE_EXPIRED`. |
| `items` | `SubscriptionItem[]` | Each with `price_id`, optional `price`, `quantity`, `metadata`. |
| `current_period_start` / `current_period_end` | `string \| null` | ISO timestamps. |
| `trial_start` / `trial_end` | `string \| null` | |
| `billing_cycle_anchor` | `string \| null` | |
| `cancel_at` / `cancel_at_period_end` / `canceled_at` / `ended_at` | various | Cancellation state. |
| `pause_collection` | `{ behavior, resumes_at } \| null` | |
| `latest_invoice_id` | `string \| null` | |
| `proration_behavior` | `ProrationBehavior \| null` | |
| `credit_balance` | `number` | |
| `pending_update` | `SubscriptionPendingUpdate \| null` | |
## Examples [#examples]
### Upgrade with proration preview [#upgrade-with-proration-preview]
```ts
const previewed = await easy.getSubscriptionProrationPreview(subId, {
items: [{ price_id: "PR_pro_yearly", quantity: 1 }],
remove_items: [currentMonthlyItemId],
});
// Show previewed.data to the user, then commit:
await easy.updateSubscription(subId, {
items: [{ price_id: "PR_pro_yearly", quantity: 1 }],
remove_items: [currentMonthlyItemId],
proration_behavior: "create_prorations",
});
```
### Apply a 3-month coupon [#apply-a-3-month-coupon]
```ts
const coupon = await easy.createCoupon({
duration: "repeating",
duration_in_months: 3,
percent_off: 25,
name: "Q2 promo",
});
await easy.applySubscriptionDiscount(subId, { coupon_id: coupon.data.id });
```
### Idempotent metered usage from a webhook [#idempotent-metered-usage-from-a-webhook]
```ts
await easy.reportSubscriptionUsage(subId, {
subscription_item_id: itemId,
quantity: 1,
action: "increment",
idempotency_key: webhookDeliveryId, // repeat-safe
});
```
# Transfers (/docs/sdks/node/resources/transfers)
A transfer is the money-movement record produced by a charge. Most transfers are created indirectly (by `checkout`, `payInvoice`, subscription billing, or `captureAuthorization`); this resource lets you create raw transfers, list and read them, update tags, and reverse them via [refunds](./refunds).
## Methods [#methods]
### `createTransfer(data)` [#createtransferdata]
`POST /transfer`.
```ts
const { data: transfer } = await easy.createTransfer({
amount: 4999, // smallest currency unit (cents for USD)
currency: "USD",
source: instrumentId, // payment-instrument ID to debit
tags: { internal_order_id: "ord_42" },
});
```
### `updateTransfer(transferId, data)` [#updatetransfertransferid-data]
`PATCH /transfer/:id`. The body is a free-form `Record` — currently used to attach or replace `tags`.
```ts
await easy.updateTransfer(transfer.id, { tags: { fulfilment: "shipped" } });
```
### `getTransfer(transferId)` [#gettransfertransferid]
`GET /transfer/:id`.
### `getTransfers(params?)` [#gettransfersparams]
`GET /transfer`. Standard pagination.
### `createRefund(transferId, body)` [#createrefundtransferid-body]
See [refunds](./refunds) — under the hood this issues a `POST /transfer/:id/reversals` and the response is itself a `TransferData` with `type: "REVERSAL"`.
## Object shape [#object-shape]
`TransferData` is the underlying processor record. The fields you'll touch most:
| Field | Type | Notes |
| ---------------------------------- | ------------------------ | -------------------------------------------------------------- |
| `id` | `string` | Transfer ID. |
| `amount` / `amount_requested` | `number` | In smallest currency units. |
| `currency` | `string` | ISO 4217. |
| `source` / `destination` | `string` \| `null` | Payment-instrument IDs. |
| `state` | `TransferState` | `PENDING`, `SUCCEEDED`, `FAILED`, `CANCELED`, `DISPUTED`, etc. |
| `type` | `TransferType` | `DEBIT`, `CREDIT`, `REVERSAL`, `FEE`, `DISPUTE`. |
| `subtype` | `TransferSubtype` | E.g. `API`, `PUSH`, `PLATFORM`. |
| `failure_code` / `failure_message` | `string` \| `null` | Populated on `FAILED`. |
| `fee` / `supplemental_fee` | `number` | Processor + platform fees. |
| `parent_transfer` | `string` \| `null` | Set on reversals to point at the original. |
| `tags` | `Record` | Application metadata. |
| `_links` | object | HATEOAS links — `reversals`, `disputes`, `fees`, etc. |
## Examples [#examples]
### Find every refund issued against a transfer [#find-every-refund-issued-against-a-transfer]
```ts
const refunds = (await easy.getTransfers({ limit: 100 })).data.filter(
(t) => t.type === "REVERSAL" && t.parent_transfer === originalId,
);
```
### Tag a charge with your internal order number [#tag-a-charge-with-your-internal-order-number]
```ts
await easy.updateTransfer(transferId, {
tags: { internal_order_id: "ord_42", channel: "web" },
});
```
### Read failure details [#read-failure-details]
```ts
const { data: t } = await easy.getTransfer(transferId);
if (t.state === "FAILED") {
console.error(t.failure_code, t.failure_message);
}
```
# Treasury (/docs/sdks/node/resources/treasury)
Treasury is Easy Labs' send / withdraw / move-money toolkit, sitting alongside the standard payment flows. It handles outbound payouts to recipients (ACH, wire, RTP, same-day ACH), inter-account transfers, deposits via wire instructions or bank pulls, recurring payouts, security rules, multi-step approvals, and 1099 / W-9 collection.
The SDK groups Treasury into eleven sub-areas. Methods all live on the same client — `easy.treasurySend(...)`, `easy.listTreasuryRecipients(...)`, etc. — but most response payloads are currently typed loosely (`{ id: string; [key: string]: unknown }`) pending schema lock-down.
## Methods [#methods]
### Send [#send]
```ts
easy.treasurySend(body); // POST /treasury/send
easy.confirmTreasurySend(body); // POST /treasury/send/confirm
easy.cancelTreasurySend(body); // POST /treasury/send/cancel
```
```ts
const tx = await easy.treasurySend({
recipient_id: recipient.id,
amount: 250000, // smallest currency unit
source_account_id: bankAccount.id,
method: "ach", // "wire" | "ach" | "same_day_ach" | "rtp"
memo: "May retainer",
});
await easy.confirmTreasurySend({
transaction_id: tx.data.id,
security_code: "123456", // when a security rule requires it
});
```
### Internal transfer (between owned accounts) [#internal-transfer-between-owned-accounts]
```ts
easy.treasuryTransfer(body); // POST /treasury/transfer
easy.confirmTreasuryTransfer(body); // POST /treasury/transfer/confirm
```
### Deposit [#deposit]
```ts
easy.getTreasuryWireInstructions(); // GET /treasury/deposit/wire-instructions
easy.treasuryDepositBankPull(body); // POST /treasury/deposit/bank-pull
```
### Withdraw [#withdraw]
```ts
easy.treasuryWithdraw(body); // POST /treasury/withdraw
easy.confirmTreasuryWithdraw(body); // POST /treasury/withdraw/confirm
```
### Transactions [#transactions]
```ts
easy.listTreasuryTransactions(params?); // GET /treasury/transactions
easy.getTreasuryTransaction(id); // GET /treasury/transactions/:id
easy.updateTreasuryTransaction(id, body); // PATCH /treasury/transactions/:id
easy.getTreasuryTransactionSettlement(id); // GET /treasury/transactions/:id/settlement
easy.exportTreasuryTransactions(params?); // GET /treasury/transactions/export
```
### Dashboard [#dashboard]
```ts
easy.getTreasuryDashboardSummary(); // GET /treasury/dashboard/summary
```
### Recipients [#recipients]
```ts
easy.listTreasuryRecipients(params?);
easy.createTreasuryRecipient(body);
easy.getTreasuryRecipient(id);
easy.updateTreasuryRecipient(id, body);
easy.deleteTreasuryRecipient(id);
easy.addTreasuryRecipientPaymentMethod(id, body);
easy.inviteTreasuryRecipient(id);
easy.setTreasuryRecipientAutoPay(id, body);
easy.requestTreasuryRecipientW9(id);
easy.getTreasuryRecipientTaxInfo(id);
easy.setTreasuryRecipientTaxInfo(id, body);
easy.listTreasuryRecipientInvitations();
easy.acceptTreasuryRecipientInvite(body);
easy.importTreasuryRecipients(body?);
easy.getTreasuryRecipientTaxReport();
easy.getTreasuryRecipientPlaidLinkToken(body);
easy.exchangeTreasuryRecipientPlaidToken(body);
```
```ts
const recipient = await easy.createTreasuryRecipient({
name: "Ada Lovelace",
email: "ada@example.com",
type: "person",
payment_methods: [
{ method_type: "ach", account_type: "checking", routing_number: "021000021", account_number: "1234567890" },
],
metadata: { internal_id: "vendor_42" },
});
await easy.setTreasuryRecipientAutoPay(recipient.data.id, {
amount: 500000,
frequency: "monthly",
start_date: "2026-06-01",
source_account_id: bankAccountId,
method: "ach",
memo: "Monthly retainer",
});
```
### Bank accounts [#bank-accounts]
```ts
easy.listTreasuryBankAccounts();
easy.getTreasuryBankAccountLinkToken();
easy.linkTreasuryBankAccount(body);
easy.deleteTreasuryBankAccount(id);
```
### Recurring payments [#recurring-payments]
```ts
easy.listTreasuryRecurringPayments(params?);
easy.createTreasuryRecurringPayment(body);
easy.getTreasuryRecurringPayment(id);
easy.updateTreasuryRecurringPayment(id, body);
easy.deleteTreasuryRecurringPayment(id);
```
### Auto-transfer rules [#auto-transfer-rules]
```ts
easy.listTreasuryAutoTransferRules();
easy.createTreasuryAutoTransferRule(body);
easy.updateTreasuryAutoTransferRule(id, body);
easy.deleteTreasuryAutoTransferRule(id);
```
### Security rules + approvals [#security-rules--approvals]
```ts
easy.listTreasurySecurityRules();
easy.createTreasurySecurityRule(body);
easy.updateTreasurySecurityRule(id, body);
easy.deleteTreasurySecurityRule(id);
easy.createTreasuryApprovalRequest(body);
easy.resolveTreasuryApprovalRequest(id, { action: "approve" /* or "deny" */, reason });
```
### Payout links + categories [#payout-links--categories]
```ts
easy.listTreasuryPayoutLinks(params?);
easy.generateTreasuryPayoutLink(body);
easy.getTreasuryPayoutLink(token);
easy.submitTreasuryPayoutLink(token, body);
easy.listTreasuryCategories();
easy.createTreasuryCategory({ name, color? });
easy.deleteTreasuryCategory(id);
easy.getTreasuryCategoryUsage(id);
```
## Object shape [#object-shape]
The Treasury request types are tightly typed (see `CreateTreasuryRecipientParams`, `TreasurySendParams`, etc.); response payloads are intentionally permissive in this SDK release:
```ts
type TreasuryTransactionData = { id: string; [key: string]: unknown };
type TreasuryRecipientData = { id: string; name: string; email: string; type: "person" | "business"; [key: string]: unknown };
type TreasuryBankAccountData = { id: string; [key: string]: unknown };
// ...etc
```
Refer to the API reference for the canonical Treasury schemas. {/* TODO: tighten the TreasuryDashboardSummary, TreasurySecurityRuleData, TreasuryAutoTransferRuleData, etc. shapes in a follow-up release. */}
## Examples [#examples]
### Send + confirm with a security code [#send--confirm-with-a-security-code]
```ts
const send = await easy.treasurySend({
recipient_id, amount: 1500000, source_account_id, method: "wire",
});
// Out of band, your approver retrieves a code, then:
await easy.confirmTreasurySend({ transaction_id: send.data.id, security_code: "123456" });
```
### Plaid-linked recipient onboarding [#plaid-linked-recipient-onboarding]
```ts
const linkToken = await easy.getTreasuryRecipientPlaidLinkToken({ invitation_token });
// Hand linkToken.data.link_token to Plaid Link in the recipient's browser.
// On success, exchange the public token:
await easy.exchangeTreasuryRecipientPlaidToken({
invitation_token,
public_token,
account_id,
institution_name,
account_name,
account_mask,
});
```
### Approval flow [#approval-flow]
```ts
const tx = await easy.treasurySend({ /* … */ });
const approval = await easy.createTreasuryApprovalRequest({
transaction_id: tx.data.id,
security_rule_id: ruleId,
});
// In the approver's UI:
await easy.resolveTreasuryApprovalRequest(approval.data.id, { action: "approve" });
```
# Wallet Checkout (/docs/sdks/node/resources/wallet-checkout)
{/* TODO: One-paragraph description of the resource. */}
## Methods [#methods]
{/* TODO: list / retrieve / create / update / delete — code blocks per method. */}
## Object shape [#object-shape]
{/* TODO: Table of fields with types; link to API reference for the canonical schema. */}
## Examples [#examples]
{/* TODO: Two or three realistic snippets (e.g. listing with filters, idempotent create, partial update). */}
# Webhook Endpoints (/docs/sdks/node/resources/webhook-endpoints)
Webhook endpoints are HTTPS URLs that receive event deliveries. Register up to 5 active endpoints per company, each subscribed to all events (`["*"]`) or a specific subset of `EASY_EVENT_TYPES`. The signing secret is returned **only on creation** — store it immediately.
For verifying inbound deliveries, see the top-level [Webhooks](../webhooks) page.
## Methods [#methods]
```ts
easy.registerWebhookEndpoint(body); // POST /webhooks
easy.listWebhookEndpoints(); // GET /webhooks
easy.updateWebhookEndpoint(endpointId, body); // PATCH /webhooks/:id
easy.deleteWebhookEndpoint(endpointId); // DELETE /webhooks/:id
easy.listWebhookDeliveries(query?); // GET /webhooks/deliveries
easy.listEndpointDeliveries(endpointId, query?); // GET /webhooks/:id/deliveries
```
### Register [#register]
```ts
const ep = await easy.registerWebhookEndpoint({
url: "https://api.example.com/webhooks/easy",
events: ["payment.created", "subscription.updated", "invoice.paid"], // or ["*"]
});
console.log(ep.data.id);
console.log(ep.data.secret); // <-- store this NOW; it's never returned again
```
The URL must be HTTPS and cannot resolve to private / loopback addresses. The maximum is 5 active endpoints per company.
### List, update, delete [#list-update-delete]
```ts
const all = await easy.listWebhookEndpoints();
await easy.updateWebhookEndpoint(ep.data.id, { active: false });
await easy.deleteWebhookEndpoint(ep.data.id);
```
### Inspect deliveries [#inspect-deliveries]
`listWebhookDeliveries` is the cross-endpoint delivery log. `listEndpointDeliveries(endpointId, query)` is the same shape scoped to one endpoint (no `endpoint_id` or `include_counts` parameters there).
```ts
const deliveries = await easy.listWebhookDeliveries({
event_type: "invoice.payment_failed",
success: false,
created_after: "2026-04-01T00:00:00Z",
limit: 100,
include_counts: true,
});
console.log(deliveries.data.total, deliveries.data.event_counts, deliveries.data.failed_count);
```
## Object shape [#object-shape]
`RegisteredWebhookEndpoint extends WebhookEndpoint`:
| Field | Type | Notes |
| ---------------------- | ----------------------------------- | ------------------------------------------ |
| `id` | `string` | |
| `url` | `string` | |
| `events` | `string[]` | `["*"]` or specific event types. |
| `active` | `boolean` | |
| `status` | `"enabled" \| "disabled" \| string` | |
| `consecutive_failures` | `number` | Endpoints auto-disable after repeated 5xx. |
| `last_triggered_at` | `string \| null` | |
| `secret` | `string` | **Only on the create response.** |
`WebhookDelivery`: `id`, `endpoint_id`, `event_type`, `payload` (`WebhookEvent`), `attempt`, `attempt_number`, `status_code`, `response_code`, `response_body`, `response_body_hash`, `response_time`, `error`, `success`, `created_at`, `next_retry_at`.
`WebhookDeliveriesListQuery`: `event_type?`, `success?`, `attempt?`, `attempt_number?`, `created_after?`, `created_before?`, `endpoint_id?`, `limit?`, `offset?`, `include_counts?`.
## Examples [#examples]
### Bootstrap a fresh endpoint at deploy time [#bootstrap-a-fresh-endpoint-at-deploy-time]
```ts
const all = await easy.listWebhookEndpoints();
const expectedUrl = `${process.env.PUBLIC_URL}/webhooks/easy`;
const existing = all.data.find((e) => e.url === expectedUrl);
if (!existing) {
const created = await easy.registerWebhookEndpoint({
url: expectedUrl,
events: ["*"],
});
// Persist created.data.secret in a secret manager — you can't read it back later.
process.env.EASY_WEBHOOK_SECRET = created.data.secret;
}
```
### Replay a failed delivery's payload locally [#replay-a-failed-deliverys-payload-locally]
```ts
const failed = await easy.listEndpointDeliveries(endpointId, {
success: false,
limit: 1,
});
const delivery = failed.data.deliveries[0];
console.log(delivery.payload); // typed WebhookEvent
```
### Rotate an endpoint's URL [#rotate-an-endpoints-url]
```ts
await easy.updateWebhookEndpoint(endpointId, {
url: "https://api.example.com/v2/webhooks/easy",
});
```
### Rotate the signing secret [#rotate-the-signing-secret]
There is no rotate-secret endpoint. Delete the old endpoint and register a new one to issue a fresh secret:
```ts
const replacement = await easy.registerWebhookEndpoint({
url: existing.url,
events: existing.events as any,
});
await easy.deleteWebhookEndpoint(existing.id);
// store replacement.data.secret
```
# E-commerce Flow (/docs/sdks/python/examples/ecommerce-flow)
An end-to-end recipe for a typical e-commerce order: tokenize a card
on the front-end, save it as a payment instrument, charge it once, and
fulfill the order from a webhook.
## Goal [#goal]
You're selling discrete goods, not subscriptions. The flow you want is:
1. Customer enters card details — tokenized client-side via Basis Theory
(the `client.basis_theory_public_api_key` is what your front-end
uses).
2. Your server creates a `Customer`, attaches the card as a
`PaymentInstrument`, and charges the cart in one shot via
`client.checkout.create(...)`.
3. The `payment.created` webhook fires; your handler marks the order
paid and triggers fulfillment.
## Implementation [#implementation]
### 1. Server: tokenize → checkout in one call [#1-server-tokenize--checkout-in-one-call]
```python
import os
import uuid
from easylabs import Client, EasyError, RateLimitError
client = Client(api_key=os.environ["EASY_API_KEY"])
def place_order(*, user, cart, card_token):
"""Charge the user once for `cart`. Idempotent on cart.id."""
try:
result = client.checkout.create(
customer={
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
},
payment_instrument={
"type": "PAYMENT_CARD",
"name": user.full_name,
"token_id": card_token, # from Basis Theory in the browser
},
items=[
{"price_id": item.price_id, "quantity": item.quantity}
for item in cart.items
],
idempotency_key=f"order-{cart.id}",
)
except RateLimitError as e:
raise RetryAfter(e.retry_after_seconds or 5)
except EasyError as e:
raise OrderFailed(e.code, e.message, e.details)
return {
"order_id": result.order_id,
"transfer_id": result.transfer.id if result.transfer else None,
}
```
### 2. Webhook handler: mark paid, fulfill [#2-webhook-handler-mark-paid-fulfill]
```python
from easylabs import Webhooks, InvalidRequestError
SECRET = os.environ["EASY_WEBHOOK_SECRET"]
def receive_webhook(request):
try:
event = Webhooks.construct_event(
payload=request.body,
signature=request.headers.get("x-easy-webhook-signature", ""),
secret=SECRET,
)
except InvalidRequestError:
return ("invalid", 400)
# Idempotent: skip if we've already processed this event.
if WebhookSeen.exists(event.id):
return ("", 204)
WebhookSeen.record(event.id)
if event.type == "payment.created":
transfer = event.data
order_id = (transfer.get("tags") or {}).get("order_id")
if order_id:
mark_order_paid(order_id, transfer["id"])
enqueue_fulfillment(order_id)
return ("", 204)
```
### 3. Refund path [#3-refund-path]
```python
def refund_order(order):
return client.transfers.create_refund(
order.transfer_id,
refund_amount=order.total_cents,
tags={"reason": "customer_request"},
idempotency_key=f"refund-{order.id}-full",
)
```
## Tradeoffs [#tradeoffs]
* **Idempotency keys are non-negotiable.** Without them, a retry on a
flaky network can charge the customer twice. The keys above are
deterministic per cart / order.
* **Webhooks must be idempotent on `event.id`.** Easy Labs retries on
non-2xx responses; the same event will arrive more than once if your
handler is slow or the network blips.
* **Don't poll.** If you find yourself calling
`client.transfers.retrieve(...)` in a loop, you're papering over
webhook plumbing that's failing somewhere.
* **For two-step "auth then capture" flows** (e.g. ship-then-charge),
swap `client.checkout.create(...)` for an authorization-only checkout
and use [`client.authorizations`](../resources/authorizations).
# Refunds (/docs/sdks/python/examples/refunds)
An end-to-end recipe for issuing refunds — full and partial — including
idempotency, webhook follow-up, and retry handling.
## Goal [#goal]
Refunds are reversals on transfers (see the
[Refunds resource page](../resources/refunds)). The SDK exposes them as
`client.transfers.create_refund(...)`, not a separate `client.refunds`
namespace. This recipe covers:
* Full refund of a single transfer.
* Partial refund with metadata for ops tooling.
* Retry-safe webhook flow that reacts to `refund.updated`.
## Implementation [#implementation]
### 1. Full refund [#1-full-refund]
```python
import os
from easylabs import Client
client = Client(api_key=os.environ["EASY_API_KEY"])
def full_refund(transfer_id):
transfer = client.transfers.retrieve(transfer_id)
return client.transfers.create_refund(
transfer.id,
refund_amount=transfer.amount,
tags={"reason": "customer_request", "kind": "full"},
idempotency_key=f"refund-{transfer.id}-full",
)
```
### 2. Partial refund with reason tracking [#2-partial-refund-with-reason-tracking]
```python
def partial_refund(*, transfer_id, amount_cents, reason, ticket_id):
return client.transfers.create_refund(
transfer_id,
refund_amount=amount_cents,
tags={
"reason": reason,
"ticket_id": ticket_id,
"kind": "partial",
},
idempotency_key=f"refund-{transfer_id}-{ticket_id}",
)
```
### 3. Retry-safe call wrapper [#3-retry-safe-call-wrapper]
```python
import time
from easylabs import RateLimitError, ServerError, EasyError
def with_retries(fn, attempts=5):
for i in range(attempts):
try:
return fn()
except RateLimitError as e:
time.sleep(e.retry_after_seconds or 1)
except ServerError:
time.sleep(min(2 ** i, 30))
return fn() # last attempt; let it raise
```
Wrap any of the refund calls in `with_retries(...)` to survive transient
issues without losing idempotency.
### 4. React to refund webhooks [#4-react-to-refund-webhooks]
```python
from easylabs import Webhooks, InvalidRequestError
SECRET = os.environ["EASY_WEBHOOK_SECRET"]
def receive(request):
try:
event = Webhooks.construct_event(
payload=request.body,
signature=request.headers.get("x-easy-webhook-signature", ""),
secret=SECRET,
)
except InvalidRequestError:
return ("invalid", 400)
if event.type == "refund.updated":
refund = event.data
if refund.get("state") == "SUCCEEDED":
mark_refund_completed(refund["id"], refund["amount"])
elif refund.get("state") == "FAILED":
alert_ops(refund["id"], refund.get("failure_message"))
return ("", 204)
```
## Tradeoffs [#tradeoffs]
* **Always pass `idempotency_key`.** A network blip during refund
creation that retries without an idempotency key will refund the
customer twice — and chargebacks for double refunds are the worst.
* **Don't poll for completion.** Refunds may take seconds (cards) to
several days (ACH). Wait for the `refund.updated` webhook to flip
to `SUCCEEDED`.
* **Reverse-then-resell is rarely what you want.** If the customer
bought the wrong item, refund + create a new order rather than
trying to mutate the original.
* **Disputes ≠ refunds.** If the cardholder has already opened a
chargeback, manage it via [`client.disputes`](../resources/disputes)
— issuing a refund on a disputed transfer can leave you double-paying.
# Subscription System (/docs/sdks/python/examples/subscription-system)
An end-to-end recipe for a SaaS subscription system: catalog setup,
sign-up, mid-cycle upgrades with proration, pause, and cancel-at-period-
end. All using the Python SDK.
## Goal [#goal]
You're building a SaaS app with monthly + annual plans, optional
add-on seats, and a self-service "Manage subscription" page.
The data model:
* **Products** — one per plan tier ("Free", "Pro", "Team").
* **Prices** — recurring monthly / annual price per tier, plus a
per-seat add-on price.
* **Subscriptions** — one per customer; items reference prices.
## Implementation [#implementation]
### 1. Bootstrap the catalog (one-time) [#1-bootstrap-the-catalog-one-time]
```python
import os
from easylabs import Client
client = Client(api_key=os.environ["EASY_API_KEY"])
pro = client.products.create(name="Pro", description="Pro tier")
team = client.products.create(name="Team", description="Team tier")
pro_monthly = client.product_prices.create(
product_id=pro.id,
currency="USD",
unit_amount=2900,
recurring=True, interval="month", interval_count=1,
)
pro_annual = client.product_prices.create(
product_id=pro.id,
currency="USD",
unit_amount=29000,
recurring=True, interval="year", interval_count=1,
)
seat_addon = client.product_prices.create(
product_id=team.id,
currency="USD",
unit_amount=1500,
recurring=True, interval="month", interval_count=1,
)
```
### 2. Sign-up flow — checkout + start subscription atomically [#2-sign-up-flow--checkout--start-subscription-atomically]
```python
def signup(*, user, plan_price_id, card_token):
return client.checkout.create(
customer={
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
},
payment_instrument={
"type": "PAYMENT_CARD",
"name": user.full_name,
"token_id": card_token,
},
subscriptions=[{"items": [{"price_id": plan_price_id, "quantity": 1}]}],
idempotency_key=f"signup-{user.id}",
)
```
Persist `result.subscriptions[0]["id"]` against the user record so you
can manage the subscription later.
### 3. Mid-cycle plan change with proration preview [#3-mid-cycle-plan-change-with-proration-preview]
```python
def preview_upgrade(*, sub_id, new_price_id, old_item_id):
return client.subscriptions.proration_preview(
sub_id,
items=[{"price_id": new_price_id, "quantity": 1}],
remove_items=[old_item_id],
)
def apply_upgrade(*, sub_id, new_price_id, old_item_id):
client.subscriptions.add_item(
sub_id, price_id=new_price_id,
idempotency_key=f"upgrade-{sub_id}-add-{new_price_id}",
)
client.subscriptions.remove_item(
sub_id, old_item_id,
idempotency_key=f"upgrade-{sub_id}-remove-{old_item_id}",
)
```
### 4. Add seats to a Team subscription [#4-add-seats-to-a-team-subscription]
```python
def set_seat_count(*, sub_id, seat_item_id, seats):
client.subscriptions.update_item(
sub_id, seat_item_id,
quantity=seats,
idempotency_key=f"seats-{sub_id}-{seats}",
)
```
### 5. Apply a coupon [#5-apply-a-coupon]
```python
def apply_coupon(*, sub_id, coupon_id):
client.subscriptions.apply_discount(
sub_id,
coupon_id=coupon_id,
idempotency_key=f"discount-{sub_id}-{coupon_id}",
)
```
### 6. Pause / resume / cancel [#6-pause--resume--cancel]
```python
def pause(sub_id, resumes_at):
client.subscriptions.pause(
sub_id, behavior="mark_uncollectible", resumes_at=resumes_at,
)
def resume(sub_id):
client.subscriptions.resume(sub_id)
def cancel(sub_id, immediate=False):
client.subscriptions.cancel(
sub_id,
at_period_end=not immediate,
)
```
### 7. React to lifecycle webhooks [#7-react-to-lifecycle-webhooks]
```python
def on_event(event):
if event.type == "subscription.trial_will_end":
send_trial_ending_email(event.data["identity_id"])
elif event.type == "invoice.payment_failed":
notify_dunning(event.data["customer_id"], event.data["id"])
elif event.type == "subscription.deleted":
downgrade_account(event.data["identity_id"])
```
## Tradeoffs [#tradeoffs]
* **Always preview proration before applying.** Show the user the exact
amount they'll be charged today; surprise charges drive disputes.
* **Use `cancel(at_period_end=True)` for self-service cancellations.**
It avoids refund accounting, leaves the customer with paid access
until the cycle ends, and gives you a window to send a save-attempt
email.
* **Configure dunning before going live.** See the
[Dunning](../resources/dunning) page — without it, failed renewals
silently retry once and then cancel.
* **One subscription per customer is simplest.** Multiple parallel
subscriptions per customer are supported but make the UI and
invoicing materially harder.
# Analytics (/docs/sdks/python/resources/analytics)
Read aggregated metrics for your account. All five methods take the
same shape (`period`, `start_date`, `end_date`) and return the raw
`data` payload as a `dict` — the response shape varies per metric, so
the SDK doesn't strongly type it.
Namespace: `client.analytics`.
| Method | Underlying endpoint |
| ----------------------- | --------------------------------- |
| `transactions(...)` | `GET /analytics/transactions` |
| `disputes(...)` | `GET /analytics/disputes` |
| `settlements(...)` | `GET /analytics/settlements` |
| `revenue(...)` | `GET /analytics/revenue` |
| `revenue_recovery(...)` | `GET /analytics/revenue-recovery` |
## Methods [#methods]
Every method has the same signature:
```python
client.analytics.transactions(
period=None, # e.g. "7d", "30d", "month"
start_date=None, # ISO-8601
end_date=None, # ISO-8601
)
```
Pass `period=` for a relative window or `start_date=` + `end_date=` for
an explicit range. Returns a `dict` whose shape depends on the metric.
### `transactions` [#transactions]
```python
data = client.analytics.transactions(period="30d")
print(data["total_count"], data["total_volume_cents"])
```
### `disputes` [#disputes]
```python
data = client.analytics.disputes(start_date="2026-01-01", end_date="2026-04-01")
```
### `settlements` [#settlements]
```python
data = client.analytics.settlements(period="7d")
```
### `revenue` [#revenue]
```python
data = client.analytics.revenue(period="month")
```
### `revenue_recovery` [#revenue_recovery]
```python
data = client.analytics.revenue_recovery(period="90d")
```
## Object shape [#object-shape]
Responses come back as `dict[str, Any]` rather than typed models —
analytics endpoints intentionally have flexible, metric-specific
shapes. See the
[Easy Labs API reference](https://docs.itseasy.co/api) for the
canonical schema per endpoint.
## Examples [#examples]
### Build a daily revenue dashboard widget [#build-a-daily-revenue-dashboard-widget]
```python
revenue = client.analytics.revenue(period="30d")
print(f"30-day revenue: ${revenue['total_cents'] / 100:,.2f}")
```
### Compare disputes month over month [#compare-disputes-month-over-month]
```python
this_month = client.analytics.disputes(period="month")
last_month = client.analytics.disputes(
start_date="2026-04-01",
end_date="2026-04-30",
)
delta = this_month["count"] - last_month["count"]
log.info("disputes vs. last month: %+d", delta)
```
### Audit recovery effectiveness [#audit-recovery-effectiveness]
```python
recovery = client.analytics.revenue_recovery(period="90d")
print(recovery)
```
# Authorizations (/docs/sdks/python/resources/authorizations)
An `Authorization` represents a hold on a customer's payment instrument
that hasn't been captured yet — useful for two-step "auth then capture"
flows (rentals, marketplaces, pre-orders).
Namespace: `client.authorizations`. Authorizations are typically
*created* via the checkout flow with `auth_only=True`; this resource
exposes the read + capture/void endpoints.
## Methods [#methods]
### `list` [#list]
```python
auths = client.authorizations.list(limit=50, offset=0)
batch = client.authorizations.list(ids=["auth_1", "auth_2"])
```
Returns `list[Authorization]`.
### `retrieve` [#retrieve]
```python
auth = client.authorizations.retrieve("auth_123")
```
### `capture` [#capture]
```python
captured = client.authorizations.capture(
"auth_123",
amount=2000, # ≤ original auth amount
idempotency_key="capture-auth_123-1",
)
```
Captures funds against the existing hold. `amount` is required and is
the amount you want to actually charge (in the smallest currency unit).
### `void` [#void]
```python
voided = client.authorizations.void(
"auth_123",
idempotency_key="void-auth_123-1",
)
```
Releases the hold without charging.
## Object shape [#object-shape]
`Authorization`:
| Field | Type |
| -------------------------- | ------------------------ |
| `id` | `str` |
| `state` | `str \| None` |
| `amount` | `int \| None` |
| `currency` | `str \| None` |
| `captured_amount` | `int \| None` |
| `voided` | `bool \| None` |
| `transfer_id` | `str \| None` |
| `payment_instrument_id` | `str \| None` |
| `expires_at` | `str \| None` |
| `tags` | `dict[str, Any] \| None` |
| `created_at`, `updated_at` | `str \| None` |
## Examples [#examples]
### Auth on order, capture on ship [#auth-on-order-capture-on-ship]
```python
# 1. (Checkout flow elsewhere creates the auth — auth_id returned)
# 2. When the order ships, capture the actual amount
client.authorizations.capture(
auth_id,
amount=order.shipped_total_cents,
idempotency_key=f"capture-{auth_id}",
)
```
### Void a hold after the customer cancels [#void-a-hold-after-the-customer-cancels]
```python
client.authorizations.void(
auth_id,
idempotency_key=f"void-{auth_id}",
)
```
### Sweep expiring authorizations [#sweep-expiring-authorizations]
```python
import datetime as dt
soon = dt.datetime.now(dt.timezone.utc) + dt.timedelta(days=1)
for a in client.authorizations.list(limit=200):
if a.expires_at and a.expires_at <= soon.isoformat():
log.warning("auth %s expires at %s", a.id, a.expires_at)
```
# Checkout (/docs/sdks/python/resources/checkout)
The `client.checkout` resource creates a one-shot, server-side checkout
that combines a payment, an order, and (optionally) subscriptions in a
single API call. Use it when you already have a tokenized payment
instrument and want to charge + record + subscribe atomically.
Namespace: `client.checkout`.
Other checkout surfaces live next door:
* [Payment Links](./payment-links) — share a hosted URL.
* [Embedded Checkout](./embedded-checkout) — render a session in your own UI.
## Methods [#methods]
### `create` [#create]
```python
result = client.checkout.create(
customer={
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
},
payment_instrument={
"type": "PAYMENT_CARD",
"name": "Ada Lovelace",
"token_id": "tok_abc",
},
items=[
{"price_id": "price_widget", "quantity": 2},
],
idempotency_key="checkout-cart-2025-01-01-001",
)
print(result.transfer.id if result.transfer else None)
print(result.order_id)
```
Returns: `CheckoutResponse`. All fields are accepted via `**kwargs`;
see the [Easy Labs API reference](https://docs.itseasy.co/api) for the
full request schema.
## Object shape [#object-shape]
`CheckoutResponse`:
| Field | Type | Notes |
| --------------- | ------------------------------- | ------------------------------------------------------------- |
| `transfer` | `Transfer \| None` | The charge created by the call. |
| `order_id` | `str \| None` (alias `orderId`) | The `Order` produced (look up with `client.orders.retrieve`). |
| `subscriptions` | `list[dict[str, Any]] \| None` | Any subscriptions started in the same call. |
| `order` | `dict[str, Any] \| None` | Full order payload when expanded. |
| `customer` | `dict[str, Any] \| None` | The customer that was created or matched. |
## Examples [#examples]
### Charge a returning customer with a saved instrument [#charge-a-returning-customer-with-a-saved-instrument]
```python
result = client.checkout.create(
identity_id="ident_123",
payment_instrument_id="pi_abc",
items=[{"price_id": "price_widget", "quantity": 1}],
idempotency_key=f"order-{order_id}",
)
if result.transfer and result.transfer.state == "SUCCEEDED":
fulfill_order(result.order_id)
```
### Combine a one-time charge with a subscription start [#combine-a-one-time-charge-with-a-subscription-start]
```python
result = client.checkout.create(
customer={"first_name": "Ada", "last_name": "L.", "email": "a@b.c"},
payment_instrument={"type": "PAYMENT_CARD", "name": "Ada", "token_id": "tok_abc"},
items=[{"price_id": "price_setup_fee", "quantity": 1}],
subscriptions=[{"items": [{"price_id": "price_monthly"}]}],
idempotency_key="signup-ada-2025-01-01",
)
setup_charge = result.transfer
new_subs = result.subscriptions or []
```
# Compliance Forms (/docs/sdks/python/resources/compliance-forms)
Compliance forms are the regulatory documents (W-9, terms of service,
processor agreements, etc.) the operator is asked to sign before, or
during, doing business on the Easy Labs platform.
Namespace: `client.compliance_forms`.
## Methods [#methods]
### `list` [#list]
```python
forms = client.compliance_forms.list()
```
Returns `list[ComplianceForm]`. No pagination — the full list is
returned in one call.
### `retrieve` [#retrieve]
```python
form = client.compliance_forms.retrieve("form_123")
```
### `sign` [#sign]
```python
signed = client.compliance_forms.sign(
"form_123",
signer_name="Ada Lovelace",
signer_title="CEO",
signed_at="2026-05-01T15:30:00Z",
idempotency_key="sign-form_123-2026-05-01",
)
```
PUT-based — accepts arbitrary fields via `**kwargs` for processor-
specific metadata. Returns the updated `ComplianceForm`.
## Object shape [#object-shape]
`ComplianceForm` follows the canonical API shape. Common fields:
| Field | Notes |
| -------------------------- | ------------------------------------- |
| `id` | Always present. |
| `type` | Form type (`"w9"`, `"terms"`, etc.). |
| `status` | `"pending"`, `"signed"`, `"expired"`. |
| `signed_at` | ISO-8601 once signed. |
| `created_at`, `updated_at` | Standard. |
## Examples [#examples]
### Render a "things you still need to sign" panel [#render-a-things-you-still-need-to-sign-panel]
```python
forms = client.compliance_forms.list()
pending = [f for f in forms if (f.status or "").lower() == "pending"]
for f in pending:
print(f.id, f.type)
```
### Sign during onboarding [#sign-during-onboarding]
```python
client.compliance_forms.sign(
form_id,
signer_name=user.full_name,
signer_title=user.title or "Owner",
signed_at=dt.datetime.now(dt.timezone.utc).isoformat(),
idempotency_key=f"sign-{form_id}-{user.id}",
)
```
# Coupons (/docs/sdks/python/resources/coupons)
A `Coupon` is a reusable discount template — a flat amount or a
percentage off, optionally limited by duration, redemption count, or a
redeem-by date. Coupons are applied to subscriptions either directly
(`subscription.apply_discount(coupon_id=...)`) or wrapped in a
[Promotion Code](./promotion-codes) for end-user-facing flows.
Namespace: `client.coupons`.
## Methods [#methods]
### `create` [#create]
```python
# Percentage off
coupon = client.coupons.create(
name="25% off",
percent_off=25,
duration="forever",
idempotency_key="coupon-25-forever",
)
# Flat-amount off (one currency)
coupon = client.coupons.create(
name="$10 off",
amount_off=1000,
currency="USD",
duration="once",
)
# Time-boxed
coupon = client.coupons.create(
name="Spring promo",
percent_off=15,
duration="repeating",
duration_in_months=3,
redeem_by="2026-06-01T00:00:00Z",
max_redemptions=500,
)
```
Pass exactly one of `percent_off` or `amount_off`. Returns: `Coupon`.
### `list` [#list]
```python
coupons = client.coupons.list(limit=50, offset=0)
```
### `retrieve` [#retrieve]
```python
coupon = client.coupons.retrieve("cou_123")
```
### `update` [#update]
```python
coupon = client.coupons.update("cou_123", name="25% off (legacy)")
```
Most fields are immutable on the server — typically you'll only update
the display `name` and `metadata`.
### `delete` [#delete]
```python
client.coupons.delete("cou_123")
```
## Object shape [#object-shape]
`Coupon`:
| Field | Type |
| -------------------------------------- | ---------------------------------------------------- |
| `id` | `str` |
| `name`, `code` | `str \| None` |
| `duration` | `str \| None` (`"once"`, `"forever"`, `"repeating"`) |
| `duration_in_months` | `int \| None` |
| `amount_off` | `int \| None` |
| `percent_off` | `float \| None` |
| `currency` | `str \| None` |
| `max_redemptions` | `int \| None` |
| `times_redeemed` | `int \| None` |
| `redeem_by` | `str \| None` (ISO-8601) |
| `valid` | `bool \| None` |
| `metadata`, `created_at`, `updated_at` | Standard. |
## Examples [#examples]
### Apply a coupon to an existing subscription [#apply-a-coupon-to-an-existing-subscription]
```python
discount = client.subscriptions.apply_discount(
"sub_123",
coupon_id="cou_25_off",
)
```
### Time-boxed campaign that auto-expires [#time-boxed-campaign-that-auto-expires]
```python
client.coupons.create(
name="Black Friday 2026",
percent_off=30,
duration="once",
redeem_by="2026-12-01T08:00:00Z",
max_redemptions=1000,
)
```
# Customers (/docs/sdks/python/resources/customers)
A `Customer` represents one end-user of your business — the person you
charge, subscribe, refund, or invoice. Customers are the parent of
payment instruments, orders, subscriptions, and wallets, all of which
are reachable as helper methods on this resource.
Namespace: `client.customers`.
## Methods [#methods]
### `create` [#create]
```python
customer = client.customers.create(
first_name="Ada",
last_name="Lovelace",
email="ada@example.com",
phone="+15551234567",
personal_address={
"line1": "1 Babbage St",
"city": "London",
"country": "GB",
},
tags={"plan": "founder"},
idempotency_key="customer-ada-2025-01-01",
)
```
Returns: `Customer`. Only `first_name` and `last_name` are required.
### `update` [#update]
```python
customer = client.customers.update(
"cus_123",
phone="+15559999999",
tags={"plan": "growth"},
)
```
Accepts arbitrary fields via `**kwargs` and PATCHes them. Returns
the updated `Customer`.
### `retrieve` [#retrieve]
```python
customer = client.customers.retrieve("cus_123")
```
### `list` [#list]
```python
customers = client.customers.list(limit=50, offset=0)
# Look up specific customers by ID
batch = client.customers.list(ids=["cus_1", "cus_2"])
```
Returns `list[Customer]`. See [Pagination](../pagination) for offset
walking patterns.
### `payment_instruments` [#payment_instruments]
```python
instruments = client.customers.payment_instruments("cus_123")
```
Returns `list[PaymentInstrument]` for the customer.
### `orders` [#orders]
```python
orders = client.customers.orders("cus_123", limit=20)
```
Returns `list[Order]`.
### `subscriptions` [#subscriptions]
```python
page = client.customers.subscriptions("cus_123", status="active", limit=20)
print(page["total"])
for sub in page["data"]: # list[Subscription]
print(sub.id, sub.status)
```
Returns the paginated envelope `{ data, total, limit, offset }` as a
plain `dict` — `data` contains `Subscription` models.
### `wallets` [#wallets]
```python
wallets = client.customers.wallets("cus_123")
```
Returns `list[Wallet]`.
## Object shape [#object-shape]
The `Customer` model surfaces the most common fields with type hints;
unknown fields are still accessible because `EasyModel` allows extras.
| Field | Type | Notes |
| ---------------- | ------------------------ | ------------------------------------- |
| `id` | `str` | Always present. |
| `created_at` | `str \| None` | ISO-8601. |
| `updated_at` | `str \| None` | ISO-8601. |
| `type` | `str \| None` | Identity type (e.g. `"PERSONAL"`). |
| `application` | `str \| None` | Easy Labs application ID. |
| `identity_roles` | `list[str] \| None` | |
| `entity` | `dict[str, Any] \| None` | Structured personal/business profile. |
| `tags` | `dict[str, Any] \| None` | Free-form metadata you set. |
The full canonical shape lives in the
[Easy Labs API reference](https://docs.itseasy.co/api).
## Examples [#examples]
### Idempotent create from a webhook handler [#idempotent-create-from-a-webhook-handler]
```python
def on_signup(user, raw_request):
customer = client.customers.create(
first_name=user.first_name,
last_name=user.last_name,
email=user.email,
idempotency_key=f"signup-{user.id}", # safe to retry
)
user.update(easy_customer_id=customer.id)
```
### List + walk every customer [#list--walk-every-customer]
```python
PAGE = 100
offset = 0
while True:
page = client.customers.list(limit=PAGE, offset=offset)
if not page:
break
for c in page:
print(c.id, c.email)
if len(page) < PAGE:
break
offset += PAGE
```
### Aggregate a customer's payments [#aggregate-a-customers-payments]
```python
instruments = client.customers.payment_instruments("cus_123")
orders = client.customers.orders("cus_123")
total_paid = sum(o.total or 0 for o in orders)
print(f"{len(instruments)} cards, ${total_paid / 100:.2f} lifetime")
```
# Disputes (/docs/sdks/python/resources/disputes)
A `Dispute` is a chargeback raised by a cardholder. The Python SDK
exposes the read endpoints, the tag-only update, the lifecycle actions
(`accept`, `submit`), plus evidence upload + listing.
Namespace: `client.disputes`.
## Methods [#methods]
### `retrieve` [#retrieve]
```python
dispute = client.disputes.retrieve("dp_123")
```
### `list` [#list]
```python
disputes = client.disputes.list(limit=50, offset=0)
```
Returns `list[Dispute]`.
### `update` [#update]
```python
dispute = client.disputes.update(
"dp_123",
tags={"reviewed_by": "ops@example.com"},
)
```
Only `tags` is mutable from the API.
### `accept` [#accept]
```python
result = client.disputes.accept(
"dp_123",
idempotency_key="dispute-accept-dp_123",
)
```
Concedes the dispute. Returns the raw `data` payload — shape varies
by processor.
### `submit` [#submit]
```python
client.disputes.submit("dp_123", idempotency_key="dispute-submit-dp_123")
```
Submits previously-uploaded evidence for review. Call once you've
finished `upload_evidence(...)` for every supporting document.
### `upload_evidence` [#upload_evidence]
```python
with open("receipt.pdf", "rb") as f:
client.disputes.upload_evidence(
"dp_123",
file=f,
filename="receipt.pdf",
content_type="application/pdf",
idempotency_key="dispute-evidence-receipt",
)
```
Multipart upload. The API accepts JPEG / PNG / PDF up to 1 MB per file.
Call once per file, then call `submit(...)` to finalize.
### `list_evidence` [#list_evidence]
```python
files = client.disputes.list_evidence("dp_123")
```
Returns the raw `data` payload (list of evidence file objects).
## Object shape [#object-shape]
`Dispute` follows the canonical API shape. The common fields:
| Field | Notes |
| -------------------------- | --------------------------------------------------- |
| `id` | Always present. |
| `state` | `"NEEDS_RESPONSE"`, `"PENDING"`, `"WON"`, `"LOST"`. |
| `amount` | Integer in the smallest currency unit. |
| `currency` | Three-letter ISO. |
| `transfer_id` | The disputed transfer. |
| `due_at` | Deadline for evidence submission. |
| `tags` | Free-form metadata; mutable via `update`. |
| `created_at`, `updated_at` | Standard. |
## Examples [#examples]
### React to a `dispute.created` webhook [#react-to-a-disputecreated-webhook]
```python
if event.type == "dispute.created":
dispute = event.data
notify_ops(dispute["id"], dispute.get("amount"), dispute.get("due_at"))
```
### Build, upload, and submit evidence [#build-upload-and-submit-evidence]
```python
for path, mime in [
("receipt.pdf", "application/pdf"),
("delivery.jpg", "image/jpeg"),
("emails.pdf", "application/pdf"),
]:
with open(path, "rb") as f:
client.disputes.upload_evidence(
"dp_123",
file=f,
filename=path,
content_type=mime,
idempotency_key=f"evidence-{path}",
)
client.disputes.submit("dp_123", idempotency_key="submit-dp_123-1")
```
### Concede when the cost of contesting outweighs the chargeback [#concede-when-the-cost-of-contesting-outweighs-the-chargeback]
```python
client.disputes.accept("dp_123", idempotency_key="accept-dp_123-1")
```
# Dunning (/docs/sdks/python/resources/dunning)
Dunning is the policy your account uses to retry failed subscription
payments and recover revenue. The Python SDK exposes two surfaces:
* `client.dunning_config` — the single, account-wide retry/recovery policy.
* `client.revenue_recovery_automations` — per-event automations layered on top.
## `client.dunning_config` [#clientdunning_config]
There is exactly one dunning config per account. The first call to
`create_or_replace` initializes it; subsequent calls overwrite.
### `create_or_replace` [#create_or_replace]
```python
config = client.dunning_config.create_or_replace(
retry_schedule=[1, 3, 7, 14], # days after failure to retry
final_action="cancel", # or "mark_uncollectible"
send_customer_emails=True,
idempotency_key="dunning-2026-q2",
)
```
Returns: `DunningConfig`. All fields are accepted via `**kwargs`.
### `retrieve` [#retrieve]
```python
config = client.dunning_config.retrieve()
```
### `update` [#update]
```python
config = client.dunning_config.update(send_customer_emails=False)
```
PATCH semantics — only the fields you pass are changed.
## `client.revenue_recovery_automations` [#clientrevenue_recovery_automations]
Automations run alongside (or instead of) the built-in retry schedule —
e.g. "on second failure, send a custom email and create a follow-up
task."
### `list` [#list]
```python
automations = client.revenue_recovery_automations.list()
```
Returns `list[RevenueRecoveryAutomation]`.
### `create` [#create]
```python
automation = client.revenue_recovery_automations.create(
name="Notify CSM on second failure",
trigger="payment_failed",
conditions={"attempt": 2},
actions=[{"type": "webhook", "url": "https://example.com/csm-alert"}],
idempotency_key="auto-csm-alert-v1",
)
```
### `update` [#update-1]
```python
client.revenue_recovery_automations.update("auto_123", enabled=False)
```
### `delete` [#delete]
```python
client.revenue_recovery_automations.delete("auto_123")
```
### `list_runs` [#list_runs]
```python
runs = client.revenue_recovery_automations.list_runs("auto_123")
for r in runs:
print(r.id, r.status, r.created_at)
```
Returns `list[RevenueRecoveryAutomationRun]`.
## Object shape [#object-shape]
`DunningConfig` mirrors the configured policy. Common fields:
| Field | Notes |
| ---------------------- | ------------------------------------------ |
| `retry_schedule` | List of day offsets, e.g. `[1, 3, 7, 14]`. |
| `final_action` | What to do once retries exhaust. |
| `send_customer_emails` | Whether to email the customer. |
`RevenueRecoveryAutomation` and `RevenueRecoveryAutomationRun` carry
`id`, `status`, `created_at`, plus the per-trigger configuration.
## Examples [#examples]
### Replace a stale dunning policy [#replace-a-stale-dunning-policy]
```python
client.dunning_config.create_or_replace(
retry_schedule=[1, 3, 5],
final_action="cancel",
send_customer_emails=True,
idempotency_key="dunning-2026-q3-rev1",
)
```
### Audit recent automation runs [#audit-recent-automation-runs]
```python
for auto in client.revenue_recovery_automations.list():
runs = client.revenue_recovery_automations.list_runs(auto.id)
failed = [r for r in runs if (r.status or "").lower() == "failed"]
if failed:
log.warning("automation %s has %d failed runs", auto.id, len(failed))
```
### React to recovery in your webhook handler [#react-to-recovery-in-your-webhook-handler]
```python
if event.type == "revenue_recovery.action_completed":
log.info("recovery action ran: %s", event.data)
```
# Embedded Checkout (/docs/sdks/python/resources/embedded-checkout)
Embedded Checkout lets you render a fully-managed checkout flow in an
iframe inside your own UI. Your server creates a session via the
`x-easy-api-key`, hands the resulting `client_secret` to the browser,
and the iframe takes care of the rest.
Namespace: `client.embedded_checkout`.
Two of the methods below (`validate`, `confirm`) are **client-side
methods** that authenticate via the session's `client_secret` instead
of the SDK's API key. They're included on the Python SDK for parity
with the JS SDK's surface and for server-rendered flows that need to
proxy these calls.
## Methods [#methods]
### `create_session` [#create_session]
```python
session = client.embedded_checkout.create_session(
items=[{"price_id": "price_abc", "quantity": 1}],
return_url="https://example.com/checkout/return",
idempotency_key="ecs-cart-2025-01-01-001",
)
print(session.id, session.client_secret)
```
Returns: `EmbeddedCheckoutSession`. Hand `client_secret` to the
browser; do not log it.
### `retrieve_session` [#retrieve_session]
```python
status = client.embedded_checkout.retrieve_session("ecs_123")
print(status.status, status.payment_status)
```
Returns: `EmbeddedCheckoutSessionStatus`.
### `crypto_status` [#crypto_status]
```python
crypto = client.embedded_checkout.crypto_status("ecs_123")
print(crypto.status, crypto.tx_signature)
```
Returns: `CryptoPaymentStatus` — for sessions paid via on-chain assets.
### `validate` (client-side) [#validate-client-side]
```python
ok = client.embedded_checkout.validate(
client_secret="ecs_secret_xyz",
parent_origin="https://example.com",
)
print(ok.valid)
```
Returns: `ValidateEmbeddedCheckoutSessionResponse`. **Sends no API key**
— authenticates via `client_secret`. Use server-side only when proxying
the iframe call.
### `confirm` (client-side) [#confirm-client-side]
```python
result = client.embedded_checkout.confirm(
client_secret="ecs_secret_xyz",
payment_method={"type": "PAYMENT_CARD", "token_id": "tok_abc"},
idempotency_key="confirm-ecs_123-1",
)
```
Returns the raw confirmation payload (`dict`). Same authentication
note as `validate`.
### `get_config` [#get_config]
```python
config = client.embedded_checkout.get_config()
print(config.allowed_origins)
```
Returns: `EmbeddedCheckoutConfig`.
### `update_config` [#update_config]
```python
config = client.embedded_checkout.update_config(
allowed_origins=["https://example.com", "https://staging.example.com"],
idempotency_key="ecs-config-2025-01-01",
)
```
Returns the updated `EmbeddedCheckoutConfig`.
## Object shape [#object-shape]
`EmbeddedCheckoutSession`:
| Field | Type |
| -------------------------- | ------------- |
| `id` | `str` |
| `client_secret` | `str \| None` |
| `status` | `str \| None` |
| `expires_at` | `str \| None` |
| `return_url` | `str \| None` |
| `created_at`, `updated_at` | `str \| None` |
`EmbeddedCheckoutSessionStatus`: `id`, `status`, `payment_status`.
`CryptoPaymentStatus`: `status`, `tx_signature`, `payment_address`,
`chain`, `asset`, `amount` — all `str | None`.
`EmbeddedCheckoutConfig`: `allowed_origins`, `updated_at`.
## Examples [#examples]
### Server endpoint that mints a session for the browser [#server-endpoint-that-mints-a-session-for-the-browser]
```python
@app.post("/api/checkout/session")
def create_checkout_session(request):
cart = build_cart_from(request.user)
session = client.embedded_checkout.create_session(
items=cart.items,
return_url=f"https://example.com/checkout/{cart.id}/return",
idempotency_key=f"ecs-{cart.id}",
)
return {"client_secret": session.client_secret}
```
### Poll session status server-side after redirect [#poll-session-status-server-side-after-redirect]
```python
status = client.embedded_checkout.retrieve_session(session_id)
if status.payment_status == "succeeded":
fulfill(session_id)
```
# Invoices (/docs/sdks/python/resources/invoices)
An `Invoice` is a billed line-item document — produced automatically by
subscription cycles or created ad-hoc for one-off billing. The Python
SDK exposes the full lifecycle: draft, finalize-and-send, pay, remind,
void, plus PDF retrieval.
Namespace: `client.invoices`.
## Methods [#methods]
### `create` [#create]
```python
invoice = client.invoices.create(
customer_id="cus_123",
currency="USD",
collection_method="send_invoice", # or "charge_automatically"
items=[
{"description": "Onboarding", "amount": 50000, "quantity": 1},
],
due_date="2026-06-01T00:00:00Z",
metadata={"po": "PO-1234"},
idempotency_key="inv-cus_123-2026-05-onboarding",
)
```
Returns: `Invoice`.
### `list` [#list]
```python
invoices = client.invoices.list(
limit=50,
offset=0,
status="open",
collection_method="send_invoice",
due_date_from="2026-05-01T00:00:00Z",
due_date_to="2026-06-01T00:00:00Z",
)
```
Returns `list[Invoice]`. All filter args are optional.
### `retrieve` [#retrieve]
```python
invoice = client.invoices.retrieve("inv_123")
```
### `update` [#update]
```python
invoice = client.invoices.update(
"inv_123",
description="Updated description",
metadata={"po": "PO-5678"},
)
```
### `send_invoice` [#send_invoice]
```python
invoice = client.invoices.send_invoice("inv_123")
```
Sends the invoice email. Named `send_invoice` (not `send`) to avoid
the coroutine / `socket.send` association in Python.
### `pay` [#pay]
```python
invoice = client.invoices.pay(
"inv_123",
payment_instrument_id="pi_abc", # optional override
idempotency_key="pay-inv_123-1",
)
```
Charges the invoice immediately. With `collection_method="charge_automatically"`
the API runs this on its own at the due date.
### `remind` [#remind]
```python
client.invoices.remind("inv_123")
```
Sends a reminder email for an overdue invoice.
### `void` [#void]
```python
client.invoices.void("inv_123")
```
Cancels the invoice. Use this instead of deleting; deletion isn't
exposed for audit reasons.
### `pdf_data` [#pdf_data]
```python
pdf = client.invoices.pdf_data("inv_123")
# pdf is the raw `data` payload from the API — typically a hosted URL.
```
## Object shape [#object-shape]
`Invoice`:
| Field | Notes |
| ------------------------------------------------- | ----------------------------------------------------------- |
| `id` | Always present. |
| `status` | `"draft"`, `"open"`, `"paid"`, `"void"`, `"uncollectible"`. |
| `customer_id`, `subscription_id` | Parent linkage. |
| `collection_method` | `"send_invoice"` or `"charge_automatically"`. |
| `currency` | Three-letter ISO. |
| `amount_due` / `amount_paid` / `amount_remaining` | Integer cents. |
| `subtotal` / `total` | Integer cents. |
| `items` | List of line-item dicts. |
| `custom_fields`, `tax_ids` | Display + tax metadata. |
| `due_date` | ISO-8601. |
| `period_start` / `period_end` | Subscription billing window if applicable. |
| `description`, `metadata` | Free-form. |
| `hosted_invoice_url`, `invoice_pdf_url` | Public URLs for the hosted view + PDF. |
| `created_at`, `updated_at` | Standard. |
## Examples [#examples]
### Manual invoicing — create, finalize, and email [#manual-invoicing--create-finalize-and-email]
```python
inv = client.invoices.create(
customer_id="cus_123",
currency="USD",
collection_method="send_invoice",
items=[{"description": "Consulting", "amount": 250000, "quantity": 1}],
due_date="2026-06-01T00:00:00Z",
idempotency_key="inv-consult-2026-06",
)
client.invoices.send_invoice(inv.id)
```
### Pay an invoice immediately with a saved card [#pay-an-invoice-immediately-with-a-saved-card]
```python
client.invoices.pay(
"inv_123",
payment_instrument_id="pi_abc",
idempotency_key="pay-inv_123-attempt-1",
)
```
### Nightly chase job [#nightly-chase-job]
```python
overdue = client.invoices.list(
status="open",
due_date_to="2026-05-01T00:00:00Z",
)
for inv in overdue:
client.invoices.remind(inv.id)
```
# Orders (/docs/sdks/python/resources/orders)
An `Order` is the line-item record produced by checkout. It groups one
or more priced items into a single transaction so you can render
receipts, attach metadata, and reconcile against transfers.
Namespace: `client.orders`. Orders are created automatically by checkout
flows; the SDK exposes read endpoints plus a tag-only update.
## Methods [#methods]
### `retrieve` [#retrieve]
```python
order = client.orders.retrieve("ord_123")
```
### `list` [#list]
```python
orders = client.orders.list(limit=50, offset=0)
batch = client.orders.list(ids=["ord_1", "ord_2"])
```
Returns `list[Order]`. See [Pagination](../pagination).
### `update_tags` [#update_tags]
```python
order = client.orders.update_tags(
"ord_123",
tags={"reconciled": True, "warehouse": "us-east"},
)
```
PATCHes only the `tags` map — the rest of the order is immutable from
the API.
## Object shape [#object-shape]
`Order` mirrors the API's order document. The most-used fields:
| Field | Notes |
| -------------------------- | ---------------------------------------------- |
| `id` | Always present. |
| `customer_id` | The customer who placed the order. |
| `items` | Line items (`price_id`, `quantity`, etc.). |
| `total` / `subtotal` | Integer amounts in the smallest currency unit. |
| `currency` | Three-letter ISO code. |
| `tags` | Free-form metadata; mutable via `update_tags`. |
| `created_at`, `updated_at` | ISO-8601 timestamps. |
`EasyModel` allows extras, so any new server fields appear on the
object even before the SDK ships explicit annotations for them.
## Examples [#examples]
### Look up the order behind a checkout session [#look-up-the-order-behind-a-checkout-session]
```python
session = client.embedded_checkout.retrieve_session("ecs_123")
# (`session.payment_status` and other status fields drive UI here)
```
### Tag and walk recent orders [#tag-and-walk-recent-orders]
```python
PAGE = 100
offset = 0
while True:
page = client.orders.list(limit=PAGE, offset=offset)
if not page:
break
for o in page:
client.orders.update_tags(o.id, tags={"synced": True})
if len(page) < PAGE:
break
offset += PAGE
```
# Payment Instruments (/docs/sdks/python/resources/payment-instruments)
A `PaymentInstrument` is a saved payment method (card or bank account)
attached to a customer. The SDK surfaces the two server-side mutation
endpoints; reads come through `client.customers.payment_instruments(...)`.
Namespace: `client.payment_instruments`.
> Cards must be tokenized client-side first (via Basis Theory) — the
> server only ever receives the resulting `token_id`. The tokenization
> step happens in the browser, not in the Python SDK.
## Methods [#methods]
### `create` [#create]
```python
instrument = client.payment_instruments.create(
type="PAYMENT_CARD", # or "BANK_ACCOUNT"
name="Ada Lovelace",
identity_id="ident_123",
token_id="tok_abc", # required
address={
"line1": "1 Babbage St",
"city": "London",
"country": "GB",
},
idempotency_key="inst-ada-2025-01-01",
)
```
Returns: `FinixPaymentInstrument`. Pass `type="BANK_ACCOUNT"` for ACH
instead, with `account_type="checking"` (or `"savings"`) and an
`attempt_bank_account_validation_check=True/False`. Any extra keyword
arguments are forwarded as-is to the API.
`token_id` is required — the SDK raises `ValueError` if you pass an
empty string.
### `update` [#update]
```python
instrument = client.payment_instruments.update(
"pi_123",
enabled=False,
tags={"deprecated": True},
)
```
PATCHes any combination of writable fields. Returns the updated
`PaymentInstrument`.
## Object shape [#object-shape]
`PaymentInstrument` (returned by reads):
| Field | Type |
| ---------------------------------------------------- | ------------------------ |
| `id` | `str` |
| `identity_id` | `str \| None` |
| `type` | `str \| None` |
| `enabled` | `bool \| None` |
| `currency` | `str \| None` |
| `last_four` | `str \| None` |
| `brand` / `card_type` | `str \| None` |
| `expiration_month` / `expiration_year` | `int \| None` |
| `bank_code`, `masked_account_number`, `account_type` | `str \| None` |
| `name`, `fingerprint` | `str \| None` |
| `address`, `tags` | `dict[str, Any] \| None` |
| `created_at`, `updated_at` | `str \| None` |
`FinixPaymentInstrument` (returned by `create`) includes processor-
specific fields like `bin`, `issuer_country`, and `address_verification`.
## Examples [#examples]
### Save a card and immediately charge it [#save-a-card-and-immediately-charge-it]
```python
inst = client.payment_instruments.create(
type="PAYMENT_CARD",
name="Ada Lovelace",
identity_id="ident_123",
token_id="tok_abc",
)
transfer = client.transfers.create(
amount=2500,
currency="USD",
source=inst.id,
idempotency_key=f"order-{order_id}",
)
```
### Disable a card without deleting it [#disable-a-card-without-deleting-it]
```python
client.payment_instruments.update("pi_123", enabled=False)
```
# Payment Links (/docs/sdks/python/resources/payment-links)
Payment links are pre-configured, hosted checkout URLs you can share
over email, chat, or social — useful when you don't want to build a
checkout UI yourself. Each link can charge once or be reused, optionally
capped by `payment_limit`.
Namespace: `client.payment_links`.
## Methods [#methods]
### `create` [#create]
```python
link = client.payment_links.create(
items=[{"price_id": "price_abc", "quantity": 1}],
payment_limit=1,
branding_overrides={"primary_color": "#0a84ff"},
tags={"campaign": "newsletter-jan"},
idempotency_key="link-newsletter-jan",
)
print(link.id) # use this id to look up the full link
```
Returns: `CreatePaymentLinkResponse` (just `{ id }`). All other fields
are accepted via `**kwargs`.
### `list` [#list]
```python
links = client.payment_links.list(limit=50, offset=0)
```
Returns `list[PaymentLink]`.
### `retrieve` [#retrieve]
```python
link = client.payment_links.retrieve("plink_123")
```
### `update` [#update]
```python
link = client.payment_links.update(
"plink_123",
payment_limit=10,
branding_overrides={"primary_color": "#ff3b30"},
)
```
### `delete` [#delete]
```python
client.payment_links.delete("plink_123")
```
Returns `None` (HTTP 204).
### `list_payments` [#list_payments]
```python
payments = client.payment_links.list_payments("plink_123", limit=50)
```
Returns the raw `data` payload (a list of payment / transfer objects)
as a `dict` — passed through verbatim from the API.
## Object shape [#object-shape]
`PaymentLink`:
| Field | Type |
| -------------------- | ------------------------ |
| `id` | `str` |
| `state` | `str \| None` |
| `payment_count` | `int \| None` |
| `payment_limit` | `int \| None` |
| `branding_overrides` | `dict[str, Any] \| None` |
| `tags` | `dict[str, Any] \| None` |
| `created_at` | `str \| None` |
| `updated_at` | `str \| None` |
## Examples [#examples]
### Single-use checkout link [#single-use-checkout-link]
```python
link = client.payment_links.create(
items=[{"price_id": "price_abc", "quantity": 1}],
payment_limit=1,
)
share_url = f"https://pay.itseasy.co/{link.id}"
send_invoice_email(customer_email, url=share_url)
```
### Reusable promo link, then audit later [#reusable-promo-link-then-audit-later]
```python
link = client.payment_links.create(
items=[{"price_id": "price_promo", "quantity": 1}],
tags={"promo": "spring-2026"},
)
# Later — pull every payment that used it
payments = client.payment_links.list_payments(link.id, limit=200)
```
### Disable a link without deleting it [#disable-a-link-without-deleting-it]
```python
client.payment_links.update("plink_123", payment_limit=0)
```
# Products & pricing (/docs/sdks/python/resources/products-pricing)
Products and prices are the catalog you charge against. A `Product` is
the thing you sell ("Pro plan", "Widget"); a `ProductPrice` is the
amount + cadence ("$10/month", "$0.10 per unit usage"). Subscriptions
and checkouts both reference prices by ID.
Two namespaces:
* `client.products` — the product catalog.
* `client.product_prices` — prices attached to products.
## `client.products` [#clientproducts]
### `create` [#create]
```python
product = client.products.create(
name="Pro plan",
description="Everything in Free, plus priority support.",
metadata={"tier": "pro"},
idempotency_key="product-pro-2025-01-01",
)
```
All fields are accepted via `**kwargs`. Returns: `Product`.
### `update` [#update]
```python
product = client.products.update("prod_123", description="…")
```
### `retrieve` [#retrieve]
```python
product = client.products.retrieve("prod_123")
```
### `list` [#list]
```python
products = client.products.list(limit=50, offset=0)
batch = client.products.list(ids=["prod_1", "prod_2"])
```
Returns `list[Product]`.
### `archive` [#archive]
```python
client.products.archive("prod_123")
```
Soft-deletes the product. Existing subscriptions on its prices keep
running; you can no longer create new ones.
### `with_prices` [#with_prices]
```python
bundle = client.products.with_prices("prod_123")
# bundle is a dict with the product + its prices joined
```
### `with_price` [#with_price]
```python
bundle = client.products.with_price("prod_123", "price_abc")
```
## `client.product_prices` [#clientproduct_prices]
### `create` [#create-1]
```python
# One-time price
one_time = client.product_prices.create(
product_id="prod_123",
currency="USD",
unit_amount=2500,
recurring=False,
idempotency_key="price-onetime-prod_123",
)
# Recurring price
monthly = client.product_prices.create(
product_id="prod_123",
currency="USD",
unit_amount=1000,
recurring=True,
interval="month",
interval_count=1,
idempotency_key="price-monthly-prod_123",
)
```
The discriminated union (`recurring=True/False`) is enforced server-
side. Returns: `ProductPrice`.
### `update` [#update-1]
```python
price = client.product_prices.update("price_abc", metadata={"legacy": True})
```
### `retrieve` [#retrieve-1]
```python
price = client.product_prices.retrieve("price_abc")
```
### `list` [#list-1]
```python
prices = client.product_prices.list(limit=50)
```
### `archive` [#archive-1]
```python
client.product_prices.archive("price_abc")
```
## Object shape [#object-shape]
`Product` and `ProductPrice` follow the canonical shape from the
[API reference](https://docs.itseasy.co/api). `EasyModel` allows
extras, so new fields appear on the model automatically.
Common fields:
| Type | Field | Notes |
| -------------- | ------------------------------------------------- | ----------------- |
| `Product` | `id`, `name`, `description`, `active`, `metadata` | |
| `ProductPrice` | `id`, `product_id`, `currency`, `unit_amount` | Integer cents. |
| `ProductPrice` | `recurring`, `interval`, `interval_count` | Recurring config. |
## Examples [#examples]
### Bootstrap a SaaS plan (product + monthly price) [#bootstrap-a-saas-plan-product--monthly-price]
```python
product = client.products.create(name="Pro", description="Pro tier")
price = client.product_prices.create(
product_id=product.id,
currency="USD",
unit_amount=2900,
recurring=True,
interval="month",
interval_count=1,
)
print(price.id)
```
### Rotate a price (archive old, create new) [#rotate-a-price-archive-old-create-new]
```python
client.product_prices.archive("price_old")
new_price = client.product_prices.create(
product_id="prod_123",
currency="USD",
unit_amount=3500,
recurring=True,
interval="month",
interval_count=1,
)
```
### Render a product page [#render-a-product-page]
```python
bundle = client.products.with_prices("prod_123")
print(bundle["name"])
for p in bundle.get("prices", []):
print(p["id"], p["unit_amount"], p.get("interval"))
```
# Promotion Codes (/docs/sdks/python/resources/promotion-codes)
A `PromotionCode` is a customer-facing code (`SUMMER2026`) that wraps a
[Coupon](./coupons). Use coupons to model the discount; use promotion
codes to expose it to end users with code strings, expiry dates,
minimum order amounts, and redemption caps.
Namespace: `client.promotion_codes`.
## Methods [#methods]
### `create` [#create]
```python
promo = client.promotion_codes.create(
code="SUMMER2026",
coupon_id="cou_25_off",
max_redemptions=500,
expires_at="2026-09-01T00:00:00Z",
minimum_amount=2000,
minimum_amount_currency="USD",
first_time_transaction=True,
idempotency_key="promo-summer2026",
)
```
Returns: `PromotionCode`. `coupon_id` is required.
### `list` [#list]
```python
codes = client.promotion_codes.list(limit=50)
```
### `retrieve` [#retrieve]
```python
promo = client.promotion_codes.retrieve("promo_123")
```
### `update` [#update]
```python
promo = client.promotion_codes.update("promo_123", active=False)
```
### `delete` [#delete]
```python
client.promotion_codes.delete("promo_123")
```
### `validate` [#validate]
```python
result = client.promotion_codes.validate(
code="SUMMER2026",
customer_id="cus_123",
items=[{"price_id": "price_monthly", "quantity": 1}],
)
if result.valid:
show_discount(result.discount_preview)
else:
show_error(result.reason)
```
Returns: `ValidatePromotionCodeResponse` (`valid`, `promotion_code`,
`coupon`, `discount_preview`, `reason`). Use this on your checkout's
"Apply code" button before confirming the order.
## Object shape [#object-shape]
`PromotionCode`:
| Field | Type |
| -------------------------------------------- | ------------------------------------- |
| `id` | `str` |
| `code` | `str \| None` |
| `coupon_id` | `str \| None` |
| `active` | `bool \| None` |
| `max_redemptions` | `int \| None` |
| `times_redeemed` | `int \| None` |
| `expires_at` | `str \| None` |
| `customer_id` | `str \| None` (single-customer codes) |
| `minimum_amount` / `minimum_amount_currency` | `int \| None` / `str \| None` |
| `first_time_transaction` | `bool \| None` |
| `metadata`, `created_at`, `updated_at` | Standard. |
`ValidatePromotionCodeResponse`:
| Field | Type |
| ------------------ | ------------------------ |
| `valid` | `bool` |
| `promotion_code` | `PromotionCode \| None` |
| `coupon` | `dict[str, Any] \| None` |
| `discount_preview` | `dict[str, Any] \| None` |
| `reason` | `str \| None` |
## Examples [#examples]
### Self-service "apply code" handler [#self-service-apply-code-handler]
```python
@app.post("/api/cart/apply-code")
def apply_code(request):
result = client.promotion_codes.validate(
code=request.json["code"],
customer_id=request.user.easy_customer_id,
items=request.session["cart_items"],
)
return {
"valid": result.valid,
"preview": result.discount_preview,
"reason": result.reason,
}
```
### Apply the validated code to a subscription [#apply-the-validated-code-to-a-subscription]
```python
client.subscriptions.apply_discount(
sub_id,
promotion_code="SUMMER2026",
)
```
### Disable a code without deleting it [#disable-a-code-without-deleting-it]
```python
client.promotion_codes.update("promo_123", active=False)
```
# Refunds (/docs/sdks/python/resources/refunds)
Refunds are reversals on an existing transfer. The Python SDK does not
expose a top-level `client.refunds` namespace — refunds live on the
[Transfers](./transfers) resource as `client.transfers.create_refund(...)`.
> No standalone `client.refunds.*` namespace ships in `0.1.x`.
>
> {/* TODO: Add a top-level `client.refunds` namespace (list / retrieve)
> when the API exposes a queryable refund resource. */}
## Methods [#methods]
### `create_refund` (on `transfers`) [#create_refund-on-transfers]
```python
reversal = client.transfers.create_refund(
"tr_123",
refund_amount=500, # cents; partial allowed
tags={"reason": "broken_widget"},
idempotency_key="refund-tr_123-1",
)
```
Returns the reversal as a `Transfer` object (the API models reversals
as transfers in the opposite direction). Pass `refund_amount` equal to
the original transfer's `amount` for a full refund.
## Object shape [#object-shape]
The reversal is a `Transfer` — see [Transfers](./transfers#object-shape)
for the field list.
The most relevant fields on a reversal:
| Field | Notes |
| ---------------------------------- | ---------------------------------------------- |
| `id` | The reversal's own ID. |
| `type` | Distinguishes reversal vs. original transfer. |
| `state` | `"PENDING"`, `"SUCCEEDED"`, `"FAILED"`. |
| `amount` | The refunded amount (positive integer, cents). |
| `failure_code` / `failure_message` | Populated if the reversal fails. |
## Examples [#examples]
### Full refund on a transfer [#full-refund-on-a-transfer]
```python
original = client.transfers.retrieve("tr_123")
reversal = client.transfers.create_refund(
original.id,
refund_amount=original.amount,
idempotency_key=f"refund-{original.id}-full",
)
assert reversal.state in {"PENDING", "SUCCEEDED"}
```
### Partial refund with a reason tag [#partial-refund-with-a-reason-tag]
```python
client.transfers.create_refund(
"tr_123",
refund_amount=500,
tags={"reason": "shipping_damage", "ticket": "SUP-4421"},
idempotency_key="refund-tr_123-shipping",
)
```
### Idempotent refund from a webhook handler [#idempotent-refund-from-a-webhook-handler]
```python
def on_dispute_won(event):
dispute = event.data
client.transfers.create_refund(
dispute["transfer_id"],
refund_amount=dispute["amount"],
idempotency_key=f"refund-dispute-{dispute['id']}",
)
```
# Settlements (/docs/sdks/python/resources/settlements)
A `Settlement` is a batched payout — the rolled-up record of transfers
the processor has paid out (or will pay out) to your bank account. Use
this resource to reconcile your books against actual deposits.
Namespace: `client.settlements`.
## Methods [#methods]
### `retrieve` [#retrieve]
```python
settlement = client.settlements.retrieve("stl_123")
```
### `list` [#list]
```python
settlements = client.settlements.list(limit=50, offset=0)
batch = client.settlements.list(ids=["stl_1", "stl_2"])
```
Returns `list[Settlement]`. See [Pagination](../pagination).
### `close` [#close]
```python
settlement = client.settlements.close(
"stl_123",
idempotency_key="close-stl_123",
)
```
PATCHes the settlement to a closed state. Use when finalizing a manual
settlement run.
## Object shape [#object-shape]
`Settlement` follows the canonical API shape. Common fields:
| Field | Notes |
| -------------------------- | ----------------------------------------- |
| `id` | Always present. |
| `state` | `"PENDING"`, `"APPROVED"`, `"PAID"`, etc. |
| `amount` | Integer in the smallest currency unit. |
| `currency` | Three-letter ISO. |
| `created_at`, `updated_at` | ISO-8601 timestamps. |
`EasyModel` allows extras, so any additional fields the API returns
(processor identifiers, batch counts, etc.) come through on the model.
## Examples [#examples]
### Reconcile against your accounting ledger [#reconcile-against-your-accounting-ledger]
```python
batch = client.settlements.list(limit=100)
for s in batch:
record_settlement_in_ledger(
external_id=s.id,
state=s.state,
amount=s.amount,
currency=s.currency,
)
```
### Close a settlement after manual review [#close-a-settlement-after-manual-review]
```python
client.settlements.close("stl_123", idempotency_key="close-stl_123-final")
```
# Subscriptions (/docs/sdks/python/resources/subscriptions)
Subscriptions bill a customer on a cadence — monthly, annual, usage-
based, or any combination. The Python SDK exposes the full lifecycle:
create, pause, resume, change items, apply discounts, report usage, and
cancel.
Namespace: `client.subscriptions`. The list of methods is large because
the resource models a real-world billing system; the
[Subscription System](../examples/subscription-system) example shows
how the pieces fit together.
## Core CRUD [#core-crud]
### `create` [#create]
```python
sub = client.subscriptions.create(
identity_id="ident_123",
items=[{"price_id": "price_monthly", "quantity": 1}],
payment_instrument_id="pi_abc",
idempotency_key="sub-ident_123-2025-01-01",
)
```
Returns: `Subscription`. All fields go through `**kwargs`.
### `retrieve` [#retrieve]
```python
sub = client.subscriptions.retrieve("sub_123")
```
### `update` [#update]
```python
sub = client.subscriptions.update(
"sub_123",
proration_behavior="create_prorations",
metadata={"upgraded": True},
)
```
### `list` [#list]
```python
subs = client.subscriptions.list(limit=50, offset=0)
```
For per-customer lists, use
`client.customers.subscriptions(customer_id, status="active", ...)` —
that endpoint returns a paginated envelope (see
[Pagination](../pagination)).
### `cancel` [#cancel]
```python
# Cancel immediately
client.subscriptions.cancel("sub_123")
# Cancel at the end of the current billing period
client.subscriptions.cancel("sub_123", at_period_end=True)
```
## Pause, resume, preview [#pause-resume-preview]
### `pause` [#pause]
```python
client.subscriptions.pause(
"sub_123",
behavior="mark_uncollectible", # or "keep_as_draft" / "void"
resumes_at="2026-06-01T00:00:00Z",
)
```
### `resume` [#resume]
```python
client.subscriptions.resume("sub_123")
```
### `proration_preview` [#proration_preview]
```python
preview = client.subscriptions.proration_preview(
"sub_123",
items=[{"price_id": "price_pro", "quantity": 1}],
remove_items=["si_old"],
proration_date="2026-05-01T00:00:00Z",
)
```
Returns a `dict` with the calculated proration line items. Useful for
showing a "you'll be charged $X today" confirmation before applying
the change.
## Items [#items]
### `add_item` [#add_item]
```python
item = client.subscriptions.add_item(
"sub_123",
price_id="price_addon_seats",
quantity=5,
)
```
### `update_item` [#update_item]
```python
item = client.subscriptions.update_item("sub_123", "si_456", quantity=10)
```
### `remove_item` [#remove_item]
```python
client.subscriptions.remove_item("sub_123", "si_456")
```
## Discounts [#discounts]
### `apply_discount` [#apply_discount]
```python
discount = client.subscriptions.apply_discount(
"sub_123",
coupon_id="cou_25_off", # XOR with promotion_code
subscription_item_id="si_456", # optional scope
)
# Or via promotion code
discount = client.subscriptions.apply_discount(
"sub_123",
promotion_code="SUMMER2026",
)
```
Pass exactly one of `coupon_id` or `promotion_code`. Returns:
`SubscriptionDiscount`.
### `list_discounts` [#list_discounts]
```python
discounts = client.subscriptions.list_discounts("sub_123")
```
### `remove_discount` [#remove_discount]
```python
client.subscriptions.remove_discount("sub_123", "sd_789")
```
## One-time charges & usage [#one-time-charges--usage]
### `create_one_time_charge` [#create_one_time_charge]
```python
charge = client.subscriptions.create_one_time_charge(
"sub_123",
amount=500,
currency="USD",
description="Setup fee",
idempotency_key="otc-sub_123-setup",
)
```
Adds a one-off line item to the subscription's next invoice. Returns
the raw `data` dict.
### `report_usage` [#report_usage]
```python
client.subscriptions.report_usage(
"sub_123",
subscription_item_id="si_metered",
quantity=42,
timestamp="2026-05-01T12:00:00Z",
action="increment", # or "set"
idempotency_key="usage-sub_123-2026-05-01-12",
)
```
### `usage_summary` [#usage_summary]
```python
summary = client.subscriptions.usage_summary(
"sub_123",
subscription_item_id="si_metered",
from_="2026-05-01T00:00:00Z",
to="2026-06-01T00:00:00Z",
)
```
> The keyword is `from_` (with trailing underscore) because `from` is
> a Python reserved word; the SDK rewrites it to `from` on the wire.
### `usage_reconciliation` [#usage_reconciliation]
```python
recon = client.subscriptions.usage_reconciliation(
"sub_123",
period_start="2026-05-01T00:00:00Z",
period_end="2026-06-01T00:00:00Z",
)
```
## Object shape [#object-shape]
`Subscription`:
| Field | Notes |
| --------------------------------------------- | ---------------------------------------------- |
| `id` | Always present. |
| `identity_id` | The customer. |
| `status` | `"trialing"`, `"active"`, `"past_due"`, etc. |
| `items` | `list[SubscriptionItem] \| None` |
| `current_period_start` / `current_period_end` | ISO-8601. |
| `cancel_at_period_end` | `bool \| None` |
| `pause_collection` | `dict \| None` |
| `latest_invoice_id` | The invoice driving the current billing cycle. |
| `pending_update` | Scheduled changes not yet applied. |
| `trial_end` | ISO-8601 if in trial. |
| `metadata`, `created_at`, `updated_at` | Standard. |
Related models: `SubscriptionItem` (`id`, `price_id`, `quantity`),
`SubscriptionDiscount` (`id`, `coupon_id` or `promotion_code_id`,
`start`, `end`), `SubscriptionPlan` (legacy Finix engine).
## Examples [#examples]
See the [Subscription System](../examples/subscription-system) recipe
for an end-to-end walkthrough — checkout → create → upgrade with
proration → cancel at period end.
# Transfers (/docs/sdks/python/resources/transfers)
A `Transfer` is the SDK's term for a money movement — a charge against a
saved payment instrument. Refunds are reversals on a transfer and live
on this same resource (see [Refunds](./refunds) for the recipe).
Namespace: `client.transfers`.
> Amounts are integers in the smallest currency unit (e.g. cents for
> USD). `amount=2500` + `currency="USD"` charges $25.00.
## Methods [#methods]
### `create` [#create]
```python
transfer = client.transfers.create(
amount=2500,
currency="USD",
source="pi_abc", # payment instrument id
tags={"order_id": "ord_123"},
idempotency_key=f"charge-ord_123",
)
```
Returns: `Transfer`.
### `update` [#update]
```python
transfer = client.transfers.update(
"tr_123",
tags={"reconciled": True},
)
```
PATCHes any combination of writable fields (typically `tags`).
### `retrieve` [#retrieve]
```python
transfer = client.transfers.retrieve("tr_123")
```
### `list` [#list]
```python
recent = client.transfers.list(limit=50)
batch = client.transfers.list(ids=["tr_1", "tr_2"])
```
Returns `list[Transfer]`. See [Pagination](../pagination).
### `create_refund` [#create_refund]
```python
reversal = client.transfers.create_refund(
"tr_123",
refund_amount=500, # partial — leave full transfer for $20
tags={"reason": "broken_widget"},
idempotency_key="refund-tr_123-1",
)
```
Returns the reversal as a `Transfer` object. Pass `refund_amount` equal
to `transfer.amount` for a full refund. See the dedicated
[Refunds](./refunds) page for the full pattern.
## Object shape [#object-shape]
`Transfer`:
| Field | Type |
| ------------------ | ------------------------ |
| `id` | `str` |
| `type` | `str \| None` |
| `state` | `str \| None` |
| `amount` | `int \| None` |
| `amount_requested` | `int \| None` |
| `currency` | `str \| None` |
| `source` | `str \| None` |
| `destination` | `str \| None` |
| `fee` | `int \| None` |
| `failure_code` | `str \| None` |
| `failure_message` | `str \| None` |
| `tags` | `dict[str, Any] \| None` |
| `created_at` | `str \| None` |
| `updated_at` | `str \| None` |
## Examples [#examples]
### Idempotent charge per order [#idempotent-charge-per-order]
```python
import uuid
def charge_order(order):
return client.transfers.create(
amount=order.total_cents,
currency=order.currency,
source=order.payment_instrument_id,
tags={"order_id": order.id},
idempotency_key=f"order-{order.id}-charge",
)
```
### Pull the most recent transfers for reconciliation [#pull-the-most-recent-transfers-for-reconciliation]
```python
transfers = client.transfers.list(limit=100)
for t in transfers:
if t.state == "FAILED":
log.warning("failed: %s — %s", t.id, t.failure_message)
```
# Treasury (/docs/sdks/python/resources/treasury)
Treasury is the platform's money-movement layer for the *operator's*
funds — bank-linking, recipients, transfers, withdrawals, payout
links, recurring payouts, and approval/security rules. The Python SDK
exposes the full surface as flat methods on `client.treasury`.
Namespace: `client.treasury`. The method names are deliberately
verbose (`list_recipient_payment_methods`, `create_security_rule`) so
the resource works without nested classes — see the [client.treasury
source](https://github.com/itseasyco/easy-sdk-python/blob/main/src/easylabs/resources/treasury.py)
for the canonical list.
## Bank accounts [#bank-accounts]
```python
# List linked bank accounts
accounts = client.treasury.list_bank_accounts(limit=50, offset=0)
# Plaid Link token for the dashboard / app
link_token = client.treasury.get_bank_link_token()
# Link a bank account after Plaid Link succeeds
client.treasury.link_bank_account(
plaid_public_token="public-sandbox-…",
plaid_account_id="acc_…",
idempotency_key="bank-link-2026-05-01",
)
# Deactivate
client.treasury.delete_bank_account("ba_123")
```
## Categories [#categories]
```python
categories = client.treasury.list_categories(limit=50)
custom = client.treasury.create_category(
name="Marketing",
color="#0a84ff",
idempotency_key="cat-marketing",
)
client.treasury.delete_category(custom.id)
usage = client.treasury.get_category_usage(custom.id)
```
## Dashboard & deposits [#dashboard--deposits]
```python
summary = client.treasury.get_dashboard_summary()
wire = client.treasury.get_wire_instructions()
# Pull funds from a linked bank (ACH)
client.treasury.bank_pull(
bank_account_id="ba_123",
amount=100000,
idempotency_key="pull-2026-05-01",
)
```
## Recipients [#recipients]
```python
recipients = client.treasury.list_recipients(limit=50)
recipient = client.treasury.retrieve_recipient("rec_123")
new = client.treasury.create_recipient(
name="Vendor LLC",
email="vendor@example.com",
payment_methods=[{
"type": "BANK_ACCOUNT",
"routing_number": "...",
"account_number": "...",
"account_type": "checking",
}],
idempotency_key="recipient-vendor-llc",
)
client.treasury.update_recipient(new.id, notes="Net-30")
client.treasury.delete_recipient(new.id)
# Payment methods (sub-resource)
client.treasury.add_recipient_payment_method(
new.id, type="BANK_ACCOUNT", routing_number="...", account_number="...",
)
# Self-service flows
client.treasury.invite_recipient(new.id)
client.treasury.request_recipient_w9(new.id)
# Auto-pay schedule
client.treasury.create_recipient_auto_pay(
new.id, amount=100000, schedule="monthly_first",
)
# Tax info
tax = client.treasury.get_recipient_tax_info(new.id)
client.treasury.set_recipient_tax_info(new.id, tin="...", classification="LLC")
# Invitations
invites = client.treasury.list_recipient_invitations(limit=50)
# Bulk import (CSV)
with open("recipients.csv", "rb") as f:
client.treasury.import_recipients(file=f, filename="recipients.csv")
# Tax compliance report
report = client.treasury.get_recipient_tax_report()
# Plaid for recipient self-service
token = client.treasury.get_recipient_plaid_link_token(recipient_id=new.id)
client.treasury.exchange_recipient_plaid_token(
recipient_id=new.id,
public_token="public-sandbox-…",
)
```
> `list_recipient_payment_methods(recipient_id)` is exposed for forward
> compatibility but the API has no dedicated GET endpoint today —
> payment methods are returned inline on the recipient object.
## Transactions [#transactions]
```python
# Cursor-based pagination — pass next_cursor back as cursor=
page = client.treasury.list_transactions(limit=100)
while True:
for tx in page.get("data", []):
print(tx["id"], tx["amount"])
if not page.get("next_cursor"):
break
page = client.treasury.list_transactions(limit=100, cursor=page["next_cursor"])
tx = client.treasury.retrieve_transaction("ttx_123")
client.treasury.update_transaction("ttx_123", category_id="cat_marketing")
breakdown = client.treasury.get_transaction_settlement("ttx_123")
csv = client.treasury.export_transactions(start="2026-05-01", end="2026-05-31")
```
## Send / transfer / withdraw [#send--transfer--withdraw]
All three are two-step (initiate, then optional 2FA confirm) and accept
arbitrary fields via `**kwargs`. Each returns a raw `dict`.
```python
# Send money to a recipient
op = client.treasury.send(
recipient_id="rec_123",
amount=100000,
method="ach",
idempotency_key="send-rec_123-2026-05-01",
)
client.treasury.confirm_send(send_id=op["id"], two_factor_code="123456")
client.treasury.cancel_send(send_id=op["id"])
# Transfer between accounts
client.treasury.transfer(
from_account="ba_123", to_account="ba_456", amount=50000,
)
client.treasury.confirm_transfer(transfer_id="ttr_123", two_factor_code="123456")
# Withdraw to an external bank
client.treasury.withdraw(
bank_account_id="ba_123", amount=200000,
idempotency_key="withdraw-2026-05-01",
)
client.treasury.confirm_withdraw(withdraw_id="tw_123", two_factor_code="123456")
```
## Payout links [#payout-links]
```python
links = client.treasury.list_payout_links(limit=50)
link = client.treasury.generate_payout_link(amount=50000, currency="USD")
# Public endpoint — no auth needed; suitable for the recipient page
detail = client.treasury.get_payout_link("plk_token")
client.treasury.submit_payout_link(
"plk_token",
routing_number="...", account_number="...", account_type="checking",
)
```
## Recurring payments [#recurring-payments]
```python
recurring = client.treasury.list_recurring_payments(limit=50)
rp = client.treasury.retrieve_recurring_payment("trp_123")
new_rp = client.treasury.create_recurring_payment(
recipient_id="rec_123",
amount=100000,
schedule="monthly_first",
idempotency_key="recurring-vendor-monthly",
)
client.treasury.update_recurring_payment(new_rp.id, status="paused")
client.treasury.delete_recurring_payment(new_rp.id)
```
## Auto-transfer rules [#auto-transfer-rules]
```python
rules = client.treasury.list_auto_transfer_rules(limit=50)
rule = client.treasury.create_auto_transfer_rule(
threshold=500000,
target_account_id="ba_savings",
idempotency_key="atr-overflow-to-savings",
)
client.treasury.update_auto_transfer_rule(rule.id, enabled=False)
client.treasury.delete_auto_transfer_rule(rule.id)
```
## Security rules & approvals [#security-rules--approvals]
```python
rules = client.treasury.list_security_rules(limit=50)
rule = client.treasury.create_security_rule(
type="approval_required",
threshold=1000000,
approvers=["user_admin"],
idempotency_key="sec-large-tx-approval",
)
client.treasury.update_security_rule(rule.id, threshold=2000000)
client.treasury.delete_security_rule(rule.id)
# Approvals
req = client.treasury.create_approval_request(
transaction_id="ttx_pending",
reason="High-value transfer",
)
client.treasury.resolve_approval_request(
req.id, decision="approve", notes="OK from CFO",
)
```
## Object shapes [#object-shapes]
Treasury models live in `easylabs.models.treasury`:
`TreasuryBankAccount`, `TreasuryCategory`, `TreasuryRecipient`,
`TreasuryTransaction`, `TreasuryPayoutLink`, `TreasuryRecurringPayment`,
`TreasuryAutoTransferRule`, `TreasurySecurityRule`,
`TreasuryApprovalRequest`.
Each is a `pydantic` model on `EasyModel` with `extra="allow"`, so the
typed fields shown in source surface common attributes (`id`, `status`,
`amount`, `created_at`, ...) and the rest comes through dynamically.
The full per-model schema lives in the
[Easy Labs API reference](https://docs.itseasy.co/api).
## Examples [#examples]
### Walk all transactions for a CSV export [#walk-all-transactions-for-a-csv-export]
```python
all_rows = []
cursor = None
while True:
page = client.treasury.list_transactions(limit=200, cursor=cursor)
all_rows.extend(page.get("data", []))
cursor = page.get("next_cursor")
if not cursor:
break
```
### Categorize uncategorized transactions [#categorize-uncategorized-transactions]
```python
page = client.treasury.list_transactions(limit=100)
for tx in page.get("data", []):
if not tx.get("category_id"):
client.treasury.update_transaction(tx["id"], category_id="cat_uncategorized")
```
# Wallet Checkout (/docs/sdks/python/resources/wallet-checkout)
Coming soon.
The Python SDK does not currently expose a dedicated `client.wallet_checkout`
namespace. Wallet-funded checkouts go through the standard checkout
surfaces today:
* [Checkout](./checkout) — `client.checkout.create(...)` for one-shot,
server-side flows.
* [Embedded Checkout](./embedded-checkout) — `client.embedded_checkout.*`
for iframe-rendered flows that can include wallet-based payment
methods.
The `Wallet` *response model* exists (it's what
`client.customers.wallets(...)` returns) but there is no wallet-
specific checkout entry point in `0.1.x`.
{/* TODO: Document `client.wallet_checkout` once a dedicated namespace
ships. The current Apple Pay / Google Pay / crypto wallet flows are
covered by `client.checkout` and `client.embedded_checkout`. */}
# Webhook Endpoints (/docs/sdks/python/resources/webhook-endpoints)
Manage your webhook subscriptions and audit deliveries. The signature
verifier for *receiving* webhooks is documented separately on the
[Webhooks](../webhooks) page.
Namespace: `client.webhooks_management`.
## Methods [#methods]
### `register` [#register]
```python
endpoint = client.webhooks_management.register(
url="https://example.com/webhooks/easy",
events=["payment.created", "subscription.updated", "invoice.paid"],
idempotency_key="webhook-prod-2026-05",
)
# `endpoint.secret` is returned once — store it now; you cannot fetch
# it again.
SECRET = endpoint.secret
```
Returns: `RegisteredWebhookEndpoint`. Pass `events=None` (the default)
to subscribe to every supported event.
### `list` [#list]
```python
endpoints = client.webhooks_management.list()
```
Returns `list[WebhookEndpoint]`. The signing secret is **not** included
on subsequent reads — only on the original `register(...)` response.
### `update` [#update]
```python
endpoint = client.webhooks_management.update(
"we_123",
url="https://example.com/webhooks/easy/v2",
events=["payment.*"],
)
```
### `delete` [#delete]
```python
client.webhooks_management.delete("we_123")
```
### `list_deliveries` [#list_deliveries]
```python
page = client.webhooks_management.list_deliveries(
event_type="payment.created",
success=False,
created_after="2026-05-01T00:00:00Z",
created_before="2026-05-02T00:00:00Z",
limit=100,
offset=0,
include_counts=True,
)
print(page.get("total"))
for d in page.get("data", []):
print(d["id"], d["status_code"])
```
Returns the raw paginated `data` payload (`dict`). Use `endpoint_id=`
to scope to a single endpoint, or call `list_endpoint_deliveries` for
the same effect with the endpoint in the URL.
### `list_endpoint_deliveries` [#list_endpoint_deliveries]
```python
page = client.webhooks_management.list_endpoint_deliveries(
"we_123",
success=False,
limit=50,
)
```
## Object shape [#object-shape]
`WebhookEndpoint`:
| Field | Type |
| ------------ | ------------------- |
| `id` | `str` |
| `url` | `str \| None` |
| `status` | `str \| None` |
| `events` | `list[str] \| None` |
| `created_at` | `str \| None` |
| `updated_at` | `str \| None` |
`RegisteredWebhookEndpoint` extends the above with `secret: str | None`
— **only populated on creation**.
`WebhookDelivery`:
| Field | Type |
| ----------------------------- | -------------- |
| `id` | `str` |
| `endpoint_id` | `str \| None` |
| `event_type` | `str \| None` |
| `success` | `bool \| None` |
| `attempt` / `attempt_number` | `int \| None` |
| `status_code` | `int \| None` |
| `response_body` | `str \| None` |
| `created_at` / `delivered_at` | `str \| None` |
## Examples [#examples]
### Bootstrap an endpoint and store the secret [#bootstrap-an-endpoint-and-store-the-secret]
```python
endpoint = client.webhooks_management.register(
url=f"{PUBLIC_BASE_URL}/webhooks/easy",
events=["payment.created", "invoice.paid", "subscription.updated"],
idempotency_key=f"webhook-{ENV}-2026-05",
)
# Persist atomically — losing the secret means re-registering.
secrets_store.put("EASY_WEBHOOK_SECRET", endpoint.secret)
```
### Audit failed deliveries from the last 24 hours [#audit-failed-deliveries-from-the-last-24-hours]
```python
import datetime as dt
since = (dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1)).isoformat()
page = client.webhooks_management.list_deliveries(
success=False,
created_after=since,
limit=200,
)
for d in page.get("data", []):
print(d["event_type"], d["status_code"], d["response_body"])
```
### Rotate an endpoint URL [#rotate-an-endpoint-url]
```python
client.webhooks_management.update(
"we_123",
url="https://example.com/webhooks/easy/v2",
)
```
# Customer Management (/docs/sdks/react/examples/customer-management)
This recipe builds the data layer for a customer-facing account page: list the signed-in customer's saved payment methods, orders, and subscriptions, then patch their profile in place. Everything runs through `useEasy()` from inside React components — no extra REST plumbing.
## Goal [#goal]
We want a `` component that:
1. Loads the customer record on mount.
2. Loads payment instruments, orders, and subscriptions in parallel.
3. Renders three tabs (or sections) for each list.
4. Lets the user update name / email and persists with `updateCustomer`.
This mirrors [`examples/next-example/src/app/profile/page.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/app/profile/page.tsx), simplified.
## Implementation [#implementation]
### 1. Create the customer [#1-create-the-customer]
Customers are usually created at signup or at first checkout. For a standalone signup form:
```tsx
const { createCustomer } = useEasy();
const res = await createCustomer({
first_name: "Ada",
last_name: "Lovelace",
email: "ada@example.com",
});
const customerId = res.data.id;
```
Persist `customerId` somewhere your app can read it back — most apps store it on their own user row.
### 2. Load the dashboard data in parallel [#2-load-the-dashboard-data-in-parallel]
```tsx title="src/components/CustomerDashboard.tsx"
"use client";
import {
useEasy,
type CustomerData,
type OrderData,
type PaymentInstrumentData,
type SubscriptionData,
} from "@easylabs/react";
import { useEffect, useState } from "react";
type State = {
customer: CustomerData | null;
instruments: PaymentInstrumentData[];
orders: OrderData[];
subscriptions: SubscriptionData[];
loading: boolean;
error: string | null;
};
export function CustomerDashboard({ customerId }: { customerId: string }) {
const {
getCustomer,
getCustomerPaymentInstruments,
getCustomerOrders,
getCustomerSubscriptions,
updateCustomer,
} = useEasy();
const [state, setState] = useState({
customer: null,
instruments: [],
orders: [],
subscriptions: [],
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false;
(async () => {
try {
const [customer, instruments, orders, subscriptions] = await Promise.all([
getCustomer(customerId),
getCustomerPaymentInstruments(customerId),
getCustomerOrders(customerId),
getCustomerSubscriptions(customerId),
]);
if (cancelled) return;
setState({
customer: customer.success ? customer.data : null,
instruments: instruments.success ? instruments.data : [],
orders: orders.success ? orders.data : [],
subscriptions: subscriptions.success ? subscriptions.data.data : [],
loading: false,
error: customer.success ? null : customer.message,
});
} catch (err) {
if (cancelled) return;
setState((s) => ({
...s,
loading: false,
error: err instanceof Error ? err.message : "Failed to load.",
}));
}
})();
return () => {
cancelled = true;
};
}, [customerId, getCustomer, getCustomerPaymentInstruments, getCustomerOrders, getCustomerSubscriptions]);
async function saveProfile(patch: Partial) {
const res = await updateCustomer(customerId, patch);
if (res.success) {
setState((s) => ({ ...s, customer: res.data }));
}
}
if (state.loading) return Loading…
;
if (state.error || !state.customer) return Couldn't load account.
;
return (
<>
>
);
}
```
(`ProfileSection`, `PaymentMethodsSection`, etc. are plain presentational components that render the typed records. Use whatever UI kit you prefer.)
### 3. Cancel a subscription [#3-cancel-a-subscription]
Cancellation is a one-liner. Refetch the list afterward (or optimistically update local state).
```tsx
const { cancelSubscription, getCustomerSubscriptions } = useEasy();
async function onCancel(subscriptionId: string) {
await cancelSubscription(subscriptionId);
const fresh = await getCustomerSubscriptions(customerId);
if (fresh.success) setSubscriptions(fresh.data.data);
}
```
## Tradeoffs [#tradeoffs]
* **Client-side fetching.** Everything runs in the browser with the user's session. That's fine for a logged-in dashboard. For SEO-sensitive pages, fetch the same data server-side via `@easylabs/node` and pass it to a client component as props.
* **No built-in pagination.** `getCustomerOrders` and `getCustomerSubscriptions` accept pagination params (`limit`, `cursor`). For long histories, page them — see the [Node SDK reference](/sdk/node) for the exact param shape (the React types mirror the server's directly).
* **Stale data after mutation.** `updateCustomer` returns the patched record; reuse that instead of refetching. For lists (`getCustomerPaymentInstruments`, etc.) you'll want to refetch or optimistically update.
* **Saved-instrument selection at checkout.** Once you have `instruments` loaded, pass an `instrument.id` directly to `checkout` as `source: instrument.id`. This skips tokenization entirely — no `` needed for repeat purchases. The Next.js example shows the full pattern in [`src/app/checkout/page.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/app/checkout/page.tsx).
# Next.js integration (/docs/sdks/react/examples/nextjs)
A complete, runnable Next.js example lives at `easy-sdk/examples/next-example`. This page is a guided tour — it points to the most-relevant files in that example so you can see the SDK wired into a real project, not just isolated snippets.
## Source code [#source-code]
* **Repository:** [`itseasyco/easy-sdk`](https://github.com/itseasyco/easy-sdk)
* **Path:** [`examples/next-example`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example)
## What this example covers [#what-this-example-covers]
* Wiring `EasyProvider` into the App Router via a `"use client"` boundary, so server components above it stay server-side.
* A storefront landing page that fetches products through a server action and renders an interactive grid.
* A full checkout page (`/checkout`) that supports anonymous shoppers (creates a customer mid-flow), authenticated shoppers (uses a saved `identity_id`), and either a new card / bank account or a previously saved payment instrument.
* A user profile (`/profile`) that lists the signed-in customer's orders, payment methods, and subscriptions through `useEasy().getCustomerOrders / getCustomerPaymentInstruments / getCustomerSubscriptions`.
* An admin area (`/admin`) for products, prices, orders, subscriptions, and refunds — backed by Easy Labs server-side calls in Next.js Server Actions.
* Mixed usage of `@easylabs/react` (client-side, for tokenization) and `@easylabs/node` (server-side, in actions).
## Key files [#key-files]
| File | What it shows |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`src/components/Providers.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/components/Providers.tsx) | `"use client"` wrapper that renders `` from `process.env.NEXT_PUBLIC_EASY_API_KEY`. |
| [`src/app/layout.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/app/layout.tsx) | Root layout that mounts `` once for the whole app. |
| [`src/app/checkout/page.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/app/checkout/page.tsx) | Full client-side checkout flow with `useEasy().checkout`, branching on `customer_creation` vs. `identity_id`, plus existing-instrument selection. |
| [`src/app/profile/page.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/app/profile/page.tsx) | Customer dashboard fetching orders, payment instruments, and subscriptions via `useEasy()`. |
| [`src/app/admin/products/actions.ts`](https://github.com/itseasyco/easy-sdk/tree/main/examples/next-example/src/app/admin/products/actions.ts) | Server-side product CRUD in a Next.js Server Action using the Node SDK with React types imported for shape sharing. |
## Run it locally [#run-it-locally]
```bash
git clone https://github.com/itseasyco/easy-sdk
cd easy-sdk
pnpm install
pnpm --filter @easylabs/common build
pnpm --filter @easylabs/react build
pnpm --filter @easylabs/node build
cd examples/next-example
cp .env.example .env.local
# Set NEXT_PUBLIC_EASY_API_KEY (sk_test_…), EASY_API_KEY (server-side), and DATABASE_URL.
pnpm db:gen
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000).
The example uses Prisma for an embedded user / role / cart database — it is not part of the SDK and exists only to demonstrate the auth and admin flows in a realistic shape.
## Adapting it [#adapting-it]
* **App Router only.** `EasyProvider` must live inside a Client Component. The pattern in `src/components/Providers.tsx` is the recommended one — keep all client providers (theme, auth, cart, etc.) in a single file rendered from `app/layout.tsx`.
* **Pages Router.** Mount `` once in `pages/_app.tsx`. No `"use client"` needed.
* **Server vs. client split.** Use `@easylabs/react` only on the client (anywhere you need to tokenize a card or read live customer data with the user's session). Use `@easylabs/node` in server actions, route handlers, and webhooks where you need a server-side API key.
* **Strip auth.** The example bundles a Prisma-backed auth system to show authenticated checkout — if you already have your own auth, replace `useAuthContext()` with your equivalent and pass `user.customerId` into `checkout({ customer_creation: false, identity_id })`.
* **Webhooks.** Not in the example yet — see the [Node SDK webhooks guide](/sdk/node/webhooks) for the matching server side.
# Payment Form (/docs/sdks/react/examples/payment-form)
This recipe covers the most common Easy Labs React pattern: a checkout-style form that collects card or bank-account details with PCI-isolated elements and persists the result as a saved payment instrument against a customer you already have. It assumes you have an `EasyProvider` mounted somewhere above this form.
## Goal [#goal]
We want one form that:
1. Lets the user choose **card** or **bank account**.
2. Renders the corresponding PCI-isolated elements.
3. Submits to `useEasy().createPaymentInstrument`, which tokenizes the elements and saves a Finix-backed instrument against `customerId` in a single round trip.
4. Surfaces tokenization and API errors back to the user.
If you also want to charge the instrument in the same submission, swap `createPaymentInstrument` for `checkout` — see [Tradeoffs](#tradeoffs).
## Implementation [#implementation]
```tsx title="src/components/PaymentForm.tsx"
"use client";
import {
CardElement,
TextElement,
useEasy,
type AccountType,
type CreatePaymentInstrument,
type ICardElement,
type ITextElement,
} from "@easylabs/react";
import { useId, useRef, useState } from "react";
type Props = {
customerId: string;
onSaved?: (instrumentId: string) => void;
};
export function PaymentForm({ customerId, onSaved }: Props) {
const formId = useId();
const { createPaymentInstrument } = useEasy();
const [type, setType] = useState("PAYMENT_CARD");
const [name, setName] = useState("");
const [accountType, setAccountType] = useState("PERSONAL_CHECKING");
const [error, setError] = useState(null);
const [submitting, setSubmitting] = useState(false);
const cardRef = useRef(null);
const routingRef = useRef(null);
const accountRef = useRef(null);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setSubmitting(true);
try {
const res = await createPaymentInstrument(
type === "PAYMENT_CARD"
? {
type: "PAYMENT_CARD",
customerId,
name,
cardElement: cardRef,
}
: {
type: "BANK_ACCOUNT",
customerId,
name,
accountType,
routingElement: routingRef,
accountElement: accountRef,
},
);
if (res.success) {
onSaved?.(res.data.id);
} else {
setError(res.message ?? "Could not save payment method.");
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unexpected error.");
} finally {
setSubmitting(false);
}
}
return (
);
}
```
Three things make this work:
* The `cardElement` ref is a `RefObject`, not a DOM ref. The element is an iframe that exposes a typed API surface (`.month()`, `.year()`, `.value`).
* Each branch passes the matching set of refs into `createPaymentInstrument`. The SDK throws if the wrong combination is provided, so a strict discriminated union on `type` keeps it ergonomic.
* The form key is generated with `useId()` so multiple instances of `` on the same page don't collide on input IDs — important when you re-render after a save.
For a full version with TanStack Form, address fields, and validation, see [`examples/react/src/components/CreateInstrumentForm.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/components/CreateInstrumentForm.tsx).
## Tradeoffs [#tradeoffs]
* **`createPaymentInstrument` vs. `checkout`.** `createPaymentInstrument` saves a card without charging it — useful for "add a payment method" pages. If you want to save *and* charge in one step, call `checkout` with the same element refs in `source` (and `customer_creation: true` for new customers). The example app's `CheckoutForm.tsx` does this.
* **Split-card layout.** Replace `` with `` + `` + `` and pass all three refs. The SDK throws if you pass only some of them.
* **No raw token access.** This recipe uses `createPaymentInstrument` so you never see the raw token. If you need to mint a bare token (e.g. to stash for later), call `tokenizePaymentInstrument` directly. You give up the typed instrument record in exchange.
* **Customer must exist first.** `createPaymentInstrument` requires `customerId`. Create the customer with `createCustomer` (or use `checkout({ customer_creation: true })` to do both at once).
# Vite SPA integration (/docs/sdks/react/examples/vite-spa)
A complete, runnable Vite SPA example lives at `easy-sdk/examples/react`. This page is a guided tour — it points to the most-relevant files in that example so you can see the SDK wired into a real project, not just isolated snippets.
## Source code [#source-code]
* **Repository:** [`itseasyco/easy-sdk`](https://github.com/itseasyco/easy-sdk)
* **Path:** [`examples/react`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react)
## What this example covers [#what-this-example-covers]
* Mounting `EasyProvider` in `main.tsx` and reading API config from Vite env vars.
* Using `useEasy()` from inside form components.
* Standalone customer creation (`createCustomer`).
* Saving a card or bank account against an existing customer (`createPaymentInstrument`) using both the combined `CardElement` and the `TextElement` pair for ACH.
* One-off transfers against a saved instrument (`createTransfer`).
* An end-to-end product → cart → checkout flow that lists products with `getProducts`, builds a `line_items` payload, and runs `checkout` with `customer_creation: true`.
* TanStack Form + Zod for client-side validation, shadcn-style UI primitives for the visual layer.
## Key files [#key-files]
| File | What it shows |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [`src/main.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/main.tsx) | `EasyProvider` mounted at the root with `apiKey` from `VITE_EASY_API_KEY`. |
| [`src/App.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/App.tsx) | Form picker that swaps in each demo component. Good entry point for reading top-down. |
| [`src/components/CreateCustomerForm.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/components/CreateCustomerForm.tsx) | Minimal `useEasy().createCustomer` flow with validation. |
| [`src/components/CreateInstrumentForm.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/components/CreateInstrumentForm.tsx) | `CardElement` and `TextElement` refs passed into `createPaymentInstrument`. |
| [`src/components/CreateTransferForm.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/components/CreateTransferForm.tsx) | Charging a saved instrument with `createTransfer`. |
| [`src/components/CheckoutForm.tsx`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react/src/components/CheckoutForm.tsx) | Full product browse → cart → `checkout({ customer_creation: true, line_items, source })`. |
## Run it locally [#run-it-locally]
```bash
git clone https://github.com/itseasyco/easy-sdk
cd easy-sdk
pnpm install
pnpm --filter @easylabs/common build
pnpm --filter @easylabs/react build
cd examples/react
cp .env.example .env # then fill in VITE_EASY_API_KEY
pnpm dev
```
Open [http://localhost:5173](http://localhost:5173). You can use any `sk_test_…` key from the [Easy Labs dashboard](https://dashboard.itseasy.co); it will route to the sandbox API automatically.
## Adapting it [#adapting-it]
* **Drop the form picker.** `App.tsx` is just a router for the demos. Lift any single component (e.g. `CheckoutForm`) into your own app.
* **Replace TanStack Form.** The SDK doesn't care which form library you use — react-hook-form, Formik, or hand-rolled state all work.
* **Swap the UI kit.** Components depend on shadcn-style primitives in `src/components/ui/*`. Replace those with your own design system; the SDK code is in the `useEasy()` calls and element refs, not the styling.
* **Move tokenization to a separate step.** The example calls `checkout` (which tokenizes + charges in one shot). If you want to save the card before charging, swap to `createPaymentInstrument` followed by `createTransfer`.
# Customer Management (/docs/sdks/react-native/examples/customer-management)
A native screen for listing, creating, and editing customers — the data plane that complements the [Payment Form](./payment-form) recipe. No card data is involved here, so this pattern is independent of Basis Theory and runs as soon as `EasyProvider` mounts.
## Goal [#goal]
Build a screen that:
* Loads existing customers with `getCustomers`.
* Creates new customers with `createCustomer`.
* Edits an existing customer with `updateCustomer`.
* Uses a single form for both create and edit, toggled by selection state.
This is the same shape as the `ProfileScreen` in the [Expo example](./expo) — adapted here as a standalone recipe.
## Implementation [#implementation]
```tsx title="src/screens/CustomersScreen.tsx"
import type { CustomerData } from '@easylabs/react-native';
import { useEasy } from '@easylabs/react-native';
import { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
export default function CustomersScreen() {
const { getCustomers, createCustomer, updateCustomer } = useEasy();
const [customers, setCustomers] = useState([]);
const [selected, setSelected] = useState(null);
const [editing, setEditing] = useState(false);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({ firstName: '', lastName: '', email: '', phone: '' });
const fillFormFrom = useCallback((c: CustomerData) => {
setForm({
firstName: c.entity.first_name,
lastName: c.entity.last_name,
email: c.entity.email ?? '',
phone: c.entity.phone ?? '',
});
}, []);
const load = useCallback(async () => {
setLoading(true);
try {
const result = await getCustomers({ limit: 20 });
if (result.success) {
setCustomers(result.data);
if (result.data.length > 0) {
setSelected(result.data[0]);
fillFormFrom(result.data[0]);
}
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to load');
} finally {
setLoading(false);
}
}, [getCustomers, fillFormFrom]);
useEffect(() => {
load();
}, [load]);
const handleCreate = async () => {
if (!form.firstName || !form.lastName) {
Alert.alert('Missing info', 'First and last name are required.');
return;
}
setLoading(true);
try {
const result = await createCustomer({
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
phone: form.phone,
});
if (result.success) {
Alert.alert('Created', `Customer ${result.data.id}`);
await load();
setForm({ firstName: '', lastName: '', email: '', phone: '' });
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to create');
} finally {
setLoading(false);
}
};
const handleUpdate = async () => {
if (!selected) return;
setLoading(true);
try {
const result = await updateCustomer(selected.id, {
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
phone: form.phone,
});
if (result.success) {
Alert.alert('Saved', 'Customer updated');
await load();
setEditing(false);
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to save');
} finally {
setLoading(false);
}
};
return (
{customers.length > 0 && (
Customers
{customers.map((c) => (
{
setSelected(c);
fillFormFrom(c);
setEditing(false);
}}
>
{c.entity.first_name} {c.entity.last_name}
))}
)}
{selected && !editing ? 'Customer details' : selected ? 'Edit customer' : 'New customer'}
First name
setForm({ ...form, firstName: v })}
editable={!loading && (!selected || editing)}
/>
Last name
setForm({ ...form, lastName: v })}
editable={!loading && (!selected || editing)}
/>
Email
setForm({ ...form, email: v })}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading && (!selected || editing)}
/>
Phone
setForm({ ...form, phone: v })}
keyboardType="phone-pad"
editable={!loading && (!selected || editing)}
/>
{selected && !editing ? (
setEditing(true)}>
Edit
) : selected && editing ? (
{loading ? : Save}
) : (
{loading ? : Create}
)}
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 8 },
sectionTitle: { fontSize: 18, fontWeight: '600', marginTop: 16, marginBottom: 8 },
chip: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: '#f0f0f0', marginRight: 8 },
chipSelected: { backgroundColor: '#007AFF' },
chipText: { color: '#333' },
chipTextSelected: { color: '#fff' },
label: { fontSize: 14, fontWeight: '500', marginTop: 12 },
input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, fontSize: 16 },
button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center', marginTop: 16 },
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
```
### Related calls [#related-calls]
`useEasy()` exposes the rest of the customer surface for follow-on screens:
```ts
const {
// Single record
getCustomer,
// Subresources
getCustomerPaymentInstruments,
getCustomerOrders,
getCustomerSubscriptions,
getCustomerWallets,
} = useEasy();
const { data: instruments } = await getCustomerPaymentInstruments('cust_…');
const { data: orders } = await getCustomerOrders('cust_…', { limit: 20 });
```
## Tradeoffs [#tradeoffs]
* **All customer reads happen client-side under the publishable key.** This is fine for a logged-in customer viewing their own profile or for admin tooling shipped behind your own auth, but it's not appropriate for surfacing other customers' data in a consumer app. For multi-tenant or operator-facing tooling, proxy customer reads through your server using the [backend SDK](/docs/backend/node).
* **`updateCustomer` is `PATCH`-style.** Only fields you pass are updated; omitted fields keep their current value. To clear a field, pass an explicit `null` rather than omitting it.
* **No client-side pagination state.** The example loads 20 records and stops. For large lists, hold an `offset` in component state and bump it on a "Load more" button — `getCustomers({ limit, offset })` is paginated.
* **Tag schemas are unenforced.** `tags` is `Record` at the API level. Define your tag shape in your own type and cast at the call site (see [TypeScript → Module augmentation](../typescript#module-augmentation)).
# Expo integration (/docs/sdks/react-native/examples/expo)
A complete, runnable Expo example lives at `easy-sdk/examples/react-native-example`. This page is a guided tour — it points to the most-relevant files in that example so you can see the SDK wired into a real project, not just isolated snippets.
## Source code [#source-code]
* **Repository:** [`itseasyco/easy-sdk`](https://github.com/itseasyco/easy-sdk)
* **Path:** [`examples/react-native-example`](https://github.com/itseasyco/easy-sdk/tree/main/examples/react-native-example)
* **README:** [`examples/react-native-example/README.md`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/README.md)
## What this example covers [#what-this-example-covers]
* Mounting `EasyProvider` once at the app root, above React Navigation.
* A two-screen native stack (`@react-navigation/native-stack`) — Home, Checkout, Profile.
* A full card checkout using the three separate native elements (`CardNumberElement`, `CardExpirationDateElement`, `CardVerificationCodeElement`) and `useEasy().checkout()`.
* Customer CRUD — list with `getCustomers`, create with `createCustomer`, update with `updateCustomer`.
* Reading API keys from Expo public env vars (`EXPO_PUBLIC_EASY_API_KEY`).
* Expo `newArchEnabled: true` configuration with the `expo-build-properties` plugin pinning Android to `arm64-v8a`.
## Key files [#key-files]
| File | What it shows |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| [`App.tsx`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/App.tsx) | `EasyProvider` mounted at the root, wrapping `NavigationContainer` and the stack navigator. |
| [`src/screens/CheckoutScreen.tsx`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/src/screens/CheckoutScreen.tsx) | Full card capture + `checkout()` flow with separate elements, billing address, and loading state. |
| [`src/screens/ProfileScreen.tsx`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/src/screens/ProfileScreen.tsx) | Customer list / create / edit with `getCustomers`, `createCustomer`, `updateCustomer`. |
| [`src/screens/HomeScreen.tsx`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/src/screens/HomeScreen.tsx) | Plain navigation surface — useful as a template for screens that don't touch the SDK directly. |
| [`app.json`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/app.json) | Expo config: `newArchEnabled: true`, `expo-build-properties` for Android arch pinning. |
| [`package.json`](https://github.com/itseasyco/easy-sdk/blob/main/examples/react-native-example/package.json) | Pinned versions: Expo `~54.0.22`, React Native `0.81.5`, React `19.1.0`. |
## Run it locally [#run-it-locally]
From the monorepo root:
```sh
pnpm install
pnpm run build:packages
```
In `examples/react-native-example`, create `.env.local`:
```bash
EXPO_PUBLIC_EASY_API_KEY=sk_test_…
```
Then start Metro:
```sh
cd examples/react-native-example
pnpm start
```
Press `i` for iOS, `a` for Android. iOS requires Xcode and an iOS Simulator; Android requires Android Studio with an emulator or a connected device.
If you hit hook errors after editing the SDK itself, clear Metro's cache:
```sh
npx expo start --clear
```
## Adapting it [#adapting-it]
Common modifications teams make when starting from this example:
* **Replace the `price_id`** in `CheckoutScreen.tsx` with one of your own product price IDs from the dashboard.
* **Swap separate elements for a single combined input** by replacing the three card refs with a different layout — the SDK supports either pattern through `tokenizePaymentInstrument`.
* **Add bank account support** by mounting two `TextElement` components for routing and account numbers and switching `source.type` to `BANK_ACCOUNT`. See the [Payment Form](./payment-form) recipe.
* **Move the API key off `EXPO_PUBLIC_*`** for bare-RN builds — use `react-native-config` or your own native config and pass the resolved string into `EasyProvider`.
* **Swap React Navigation for Expo Router** — `EasyProvider` doesn't depend on either; mount it inside `app/_layout.tsx` for Expo Router instead of in `App.tsx`.
# Payment Form (/docs/sdks/react-native/examples/payment-form)
A reusable native checkout screen that collects customer details, captures card data inside Basis Theory's PCI-scoped elements, and runs `useEasy().checkout()` to create a customer, tokenize the card, and place an order in one call.
## Goal [#goal]
Ship a card payment screen that:
* Captures the PAN, expiration, and CVC inside native inputs (no card data ever in your component state).
* Creates a new customer and order in a single round-trip via `checkout()`.
* Surfaces validation, network, and tokenization errors back to the user.
* Works the same way on iOS and Android with no platform-specific branching.
The same pattern adapts to bank account (ACH) capture by swapping the element types and `source.type`.
## Implementation [#implementation]
```tsx title="src/screens/PaymentFormScreen.tsx"
import {
type BTDateRef,
type BTRef,
CardExpirationDateElement,
CardNumberElement,
CardVerificationCodeElement,
useEasy,
} from '@easylabs/react-native';
import { useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
const PRICE_ID = 'price_…'; // from your Easy dashboard
export default function PaymentFormScreen() {
const { checkout } = useEasy();
const cardNumberRef = useRef(null);
const cardExpirationRef = useRef(null);
const cardCvcRef = useRef(null);
const [form, setForm] = useState({ firstName: '', lastName: '', email: '' });
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (!form.firstName || !form.lastName || !form.email) {
Alert.alert('Missing info', 'First name, last name, and email are required.');
return;
}
setLoading(true);
try {
const result = await checkout({
customer_creation: true,
customer_details: {
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
},
source: {
type: 'PAYMENT_CARD',
name: `${form.firstName} ${form.lastName}`,
cardNumberElement: cardNumberRef,
cardExpirationDateElement: cardExpirationRef,
cardVerificationCodeElement: cardCvcRef,
},
line_items: [{ price_id: PRICE_ID, quantity: 1 }],
metadata: { source: 'react-native-app' },
});
if (result.success) {
Alert.alert('Paid', `Order ${result.data.orderId}`);
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Payment failed');
} finally {
setLoading(false);
}
};
return (
First name
setForm({ ...form, firstName: v })}
editable={!loading}
/>
Last name
setForm({ ...form, lastName: v })}
editable={!loading}
/>
Email
setForm({ ...form, email: v })}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
Card number
Expiration
CVC
{loading ? : Pay}
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 8 },
label: { fontSize: 14, fontWeight: '500', marginTop: 12 },
input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, fontSize: 16 },
element: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, minHeight: 50 },
row: { flexDirection: 'row', gap: 12 },
flex: { flex: 1 },
button: {
backgroundColor: '#007AFF',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginTop: 24,
},
buttonDisabled: { backgroundColor: '#ccc' },
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
```
### Variations [#variations]
**Existing customer.** Skip `customer_creation` and pass an `identity_id`:
```ts
await checkout({
customer_creation: false,
identity_id: 'cust_…',
source: {
type: 'PAYMENT_CARD',
name: 'Jane Doe',
cardNumberElement: cardNumberRef,
cardExpirationDateElement: cardExpirationRef,
cardVerificationCodeElement: cardCvcRef,
},
line_items: [{ price_id: PRICE_ID, quantity: 1 }],
});
```
**Stored payment instrument.** Pass the instrument id as `source` directly — no element refs needed:
```ts
await checkout({
customer_creation: false,
identity_id: 'cust_…',
source: 'pi_…',
line_items: [{ price_id: PRICE_ID, quantity: 1 }],
});
```
**Bank account (ACH).** Replace card refs with two `TextElement` refs and switch `source.type`:
```tsx
import { TextElement, type BTRef } from '@easylabs/react-native';
const routingRef = useRef(null);
const accountRef = useRef(null);
await checkout({
customer_creation: true,
customer_details: { first_name: 'Jane', last_name: 'Doe' },
source: {
type: 'BANK_ACCOUNT',
accountType: 'CHECKING',
name: 'Jane Doe',
routingElement: routingRef,
accountElement: accountRef,
},
line_items: [{ price_id: PRICE_ID, quantity: 1 }],
});
// Inputs:
```
## Tradeoffs [#tradeoffs]
* **Element refs must be mounted at submit time.** Don't conditionally hide a card element behind a tab and call `checkout()` from a different tab — the ref will be `null` and tokenization throws `"Card element reference is not set"`. If you need multi-step UI, mount the element off-screen or guard the submit on a known-good screen.
* **No on-device validation gating before submit.** The element exposes `onChange` events with `complete` / `valid` flags (see [Elements → Validation](../elements#validation)) — wire a disabled state into the submit button if you want to block submission until the form is complete locally.
* **`checkout()` is a single transaction.** Customer creation, tokenization, instrument creation, and the order all share one promise. If you need to retry only the order portion (e.g. after a 3DS retry), call `createPaymentInstrument` first to get a stable instrument ID, then call `checkout()` with that ID as the `source`.
* **Don't use this for Apple Pay or Google Pay.** Wallet flows have their own native sheet — see [Wallet Checkout](../wallet-checkout). Only fall back to manual card entry when the wallet is unavailable.
# E-commerce Flow (/docs/sdks/ruby/examples/ecommerce-flow)
End-to-end recipe for a typical e-commerce checkout: capture a tokenised
card from the browser, charge it, persist the order, and react to the
post-charge webhook.
## Goal [#goal]
Take a customer through a one-shot card payment with a server-side
charge:
1. Browser tokenises the card with BasisTheory and posts the token to
our server.
2. Server creates (or retrieves) a customer.
3. Server attaches the tokenised card as a payment instrument.
4. Server creates a transfer for the order amount.
5. Server records the order locally.
6. Webhook handler confirms the payment when `payment.created` arrives.
## Implementation [#implementation]
```ruby
require "easy_sdk"
client = EasyLabs::Client.new(api_key: ENV.fetch("EASY_API_KEY"))
# 1. Browser POST: { token: "tok_…", email:, first_name:, last_name:, amount_cents: }
def checkout(client, params)
customer = client.customers.create(
first_name: params[:first_name],
last_name: params[:last_name],
email: params[:email]
)
card = client.payment_instruments.create(
tokenId: params[:token],
identityId: customer[:id],
type: "PAYMENT_CARD",
name: "Card on file"
)
transfer = client.transfers.create(
amount: params[:amount_cents],
currency: "USD",
source: card[:id],
tags: { order_id: SecureRandom.uuid }
)
Order.create!(
customer_id: customer[:id],
transfer_id: transfer[:id],
amount: params[:amount_cents],
status: "pending"
)
{ transfer_id: transfer[:id], status: "pending" }
rescue EasyLabs::InvalidRequestError => e
# Bad input from the browser (e.g. expired token).
{ error: e.message, code: e.code }
rescue EasyLabs::Error => e
Rails.logger.error("[easy] #{e.status} #{e.code}: #{e.message}")
raise
end
# Webhook receiver
post "/webhooks/easy" do
event = EasyLabs::Webhooks.construct_event(
payload: request.body.read,
signature: request.headers["X-Easy-Webhook-Signature"],
secret: ENV.fetch("EASY_WEBHOOK_SECRET")
)
case event[:type]
when "payment.created"
transfer_id = event.dig(:data, :id)
Order.where(transfer_id: transfer_id).update_all(status: "paid")
end
status 204
end
```
## Tradeoffs [#tradeoffs]
* **Server-side `transfers.create` vs. checkout sessions.** This recipe
charges the customer synchronously. For a hosted page, swap steps
2-4 for [Payment Links](../resources/payment-links). For an embedded
widget, use [Embedded Checkout](../resources/embedded-checkout) — both
return a URL/secret you hand to the browser instead of charging
inline.
* **Customer dedup.** This recipe always creates a new customer.
Real-world apps should look up by email first and reuse the existing
identity to avoid duplicate `cust_…` rows.
* **Idempotency.** The transfer is fire-and-forget. If the request
times out, you may double-charge. Wrap the call in your own retry
layer keyed on a stable `order_id` until the SDK exposes
per-resource `idempotency_key:` arguments.
* **Webhook latency.** Don't render "thanks for your order" off the
webhook — render it off the synchronous `transfers.create` response
and use the webhook only for downstream effects (fulfillment,
receipts, accounting).
# Refunds (/docs/sdks/ruby/examples/refunds)
Practical recipe for issuing refunds from your support tooling and
keeping local state consistent with refund webhooks.
## Goal [#goal]
A support agent can:
1. Look up a charge by ID.
2. Issue a full or partial refund with a reason tag.
3. Trust that the refund eventually settles, with `refund.updated`
webhooks keeping your DB in sync.
## Implementation [#implementation]
### Issue the refund [#issue-the-refund]
The Easy API expresses refunds as **reversals** on the original
transfer — there is no top-level `Refunds.create`. The SDK exposes them
via `client.transfers.create_refund`.
```ruby
require "easy_sdk"
client = EasyLabs::Client.new(api_key: ENV.fetch("EASY_API_KEY"))
def refund_charge(client, transfer_id:, amount_cents: nil, reason:)
charge = client.transfers.retrieve(transfer_id)
amount = amount_cents || charge[:amount] # default to full refund
if amount > charge[:amount]
raise ArgumentError, "Refund amount exceeds charge amount"
end
client.transfers.create_refund(
charge[:id],
refund_amount: amount,
tags: {
reason: reason,
issued_by: Current.agent.email,
issued_at: Time.now.iso8601
}
)
rescue EasyLabs::InvalidRequestError => e
# 400/422 — typically because the charge isn't refundable yet.
{ error: e.message, code: e.code }
end
```
### Read refund history [#read-refund-history]
The parent transfer carries the full reversals collection — no separate
list call needed.
```ruby
charge = client.transfers.retrieve("tr_…")
charge[:reversals]&.each do |refund|
puts "#{refund[:id]} — #{refund[:amount]} #{refund[:state]}"
end
```
### Sync from webhooks [#sync-from-webhooks]
```ruby
post "/webhooks/easy" do
event = EasyLabs::Webhooks.construct_event(
payload: request.body.read,
signature: request.headers["X-Easy-Webhook-Signature"],
secret: ENV.fetch("EASY_WEBHOOK_SECRET")
)
case event[:type]
when "refund.created", "refund.updated"
Refund.upsert_by_easy_id(event[:data])
end
status 204
end
```
## Tradeoffs [#tradeoffs]
* **Full vs. partial refunds.** `refund_amount` is in minor units —
always pass the full charge amount for a full refund rather than
relying on a "missing → full" default. Explicit is safer in support
workflows.
* **Multiple partial refunds.** You can issue multiple partial refunds
against the same transfer until the cumulative amount matches the
original charge. The API rejects further refunds beyond that.
* **Tags as audit trail.** Stash the agent identifier and reason in
`tags` — they're immutable on the refund record and showing them in
your support tool gives you a free audit trail.
* **Webhook latency.** `transfers.create_refund` returns a
`PENDING`-shaped refund; final settlement (and chargeback resolution)
comes through `refund.updated`. Don't tell the customer "refund
complete" off the create call.
* **Subscription / invoice refunds.** For refunding a subscription
invoice, refund the underlying transfer here — there is no separate
"void invoice + refund" combo on the SDK.
# Subscription System (/docs/sdks/ruby/examples/subscription-system)
Recipe for standing up a recurring-billing system on top of
`client.products`, `client.product_prices`, and `client.subscriptions` —
catalog setup, customer signup, mid-cycle upgrades, and cancellation.
## Goal [#goal]
Run a SaaS-style subscription where:
1. Catalog is defined once (one `Product`, multiple `Prices`).
2. Customers sign up to a price; payment instrument is on file.
3. Mid-cycle upgrades use proration preview before applying.
4. Cancellations honor "end of period" by default.
5. Lifecycle webhooks keep your local state in sync.
## Implementation [#implementation]
### 1. Define the catalog (one-time) [#1-define-the-catalog-one-time]
```ruby
client = EasyLabs::Client.new(api_key: ENV.fetch("EASY_API_KEY"))
product = client.products.create(name: "Pro plan")
monthly = client.product_prices.create(
product_id: product[:id],
active: true,
recurring: true,
currency: "USD",
unit_amount: 4_900,
interval: "month",
interval_count: 1,
tax_behavior: "exclusive"
)
annual = client.product_prices.create(
product_id: product[:id],
active: true,
recurring: true,
currency: "USD",
unit_amount: 49_900,
interval: "year",
interval_count: 1,
tax_behavior: "exclusive"
)
```
### 2. Sign up a customer [#2-sign-up-a-customer]
```ruby
customer = client.customers.create(
first_name: "Ada", last_name: "Lovelace", email: "ada@example.com"
)
card = client.payment_instruments.create(
tokenId: token_from_browser,
identityId: customer[:id],
type: "PAYMENT_CARD",
name: "Personal card"
)
sub = client.subscriptions.create(
identity_id: customer[:id],
items: [{ price_id: monthly[:id] }],
instrument_id: card[:id]
)
```
### 3. Preview, then upgrade [#3-preview-then-upgrade]
```ruby
preview = client.subscriptions.proration_preview(
sub[:id],
items: [{ price_id: annual[:id], quantity: 1 }]
)
if preview[:total] <= max_charge_cents
client.subscriptions.update(sub[:id], items: [{ price_id: annual[:id] }])
end
```
### 4. Cancel at period end [#4-cancel-at-period-end]
```ruby
client.subscriptions.cancel(sub[:id], at_period_end: true)
```
### 5. Sync state from webhooks [#5-sync-state-from-webhooks]
```ruby
post "/webhooks/easy" do
event = EasyLabs::Webhooks.construct_event(
payload: request.body.read,
signature: request.headers["X-Easy-Webhook-Signature"],
secret: ENV.fetch("EASY_WEBHOOK_SECRET")
)
data = event[:data]
case event[:type]
when "subscription.created", "subscription.updated"
Subscription.upsert_by_easy_id(data)
when "subscription.deleted"
Subscription.find_by(easy_id: data[:id])&.update!(state: "canceled")
when "subscription.paused"
Subscription.find_by(easy_id: data[:id])&.update!(state: "paused")
when "subscription.resumed"
Subscription.find_by(easy_id: data[:id])&.update!(state: "active")
when "invoice.paid"
Invoice.upsert_by_easy_id(data)
when "invoice.payment_failed"
DunningMailer.with(invoice: data).failed.deliver_later
end
status 204
end
```
## Tradeoffs [#tradeoffs]
* **Always preview prorations.** `proration_preview` is cheap and lets
you show the customer (and verify against your business rules) what
an upgrade actually costs before you charge them.
* **Cancel at period end vs. immediately.** Defaulting to
`at_period_end: true` is friendlier and matches most customers'
expectations — passing `false` cancels immediately and forfeits the
remainder of the period.
* **Source of truth.** Treat the Easy API as the source of truth for
subscription state and use webhooks to keep your local cache fresh.
Avoid recording subscription state synchronously from the create call
— the create response and a subsequent webhook can race.
* **Metered billing.** For usage-based pricing, layer
`report_usage` calls into your existing event pipeline rather than
doing it inline at request time. See [Subscriptions › Metered
usage](../resources/subscriptions#metered-usage).
# Analytics (/docs/sdks/ruby/resources/analytics)
Aggregator endpoints for dashboards and reporting. Each method returns
a pre-aggregated payload over a time window — no need to walk every
underlying record yourself.
Every method accepts the same query shape: `period:`, `start_date:`,
`end_date:` (plus per-endpoint extras the API supports).
Accessed via `client.analytics`.
## Methods [#methods]
### `transactions(**query)` [#transactionsquery]
`GET /analytics/transactions`.
```ruby
client.analytics.transactions(period: "month")
client.analytics.transactions(start_date: "2026-04-01", end_date: "2026-04-30")
```
### `disputes(**query)` [#disputesquery]
`GET /analytics/disputes`.
```ruby
client.analytics.disputes(period: "quarter")
```
### `settlements(**query)` [#settlementsquery]
`GET /analytics/settlements`.
```ruby
client.analytics.settlements(period: "month")
```
### `revenue(**query)` [#revenuequery]
`GET /analytics/revenue`.
```ruby
client.analytics.revenue(period: "year")
```
### `revenue_recovery(**query)` [#revenue_recoveryquery]
`GET /analytics/revenue-recovery`. Aggregates the dunning + recovery
funnel — recovered amounts, retry success rate, automation hits.
```ruby
client.analytics.revenue_recovery(period: "month")
```
## Object shape [#object-shape]
Each endpoint returns a per-bucket aggregation: `:buckets` keyed by
period, each with `:gross`, `:net`, `:count`, `:currency`, … plus
top-line totals.
{/* TODO: enumerate the per-endpoint analytics schemas. */}
## Examples [#examples]
### Build a monthly revenue chart [#build-a-monthly-revenue-chart]
```ruby
data = client.analytics.revenue(period: "month")
data[:buckets].each do |bucket|
puts "#{bucket[:period_start]}: #{bucket[:gross]} #{bucket[:currency]}"
end
```
### Surface dispute-rate spikes [#surface-dispute-rate-spikes]
```ruby
disputes = client.analytics.disputes(period: "month")
txns = client.analytics.transactions(period: "month")
rate = disputes[:total_count].to_f / txns[:total_count]
alert! if rate > 0.01
```
# Authorizations (/docs/sdks/ruby/resources/authorizations)
An authorization holds funds on a card without capturing them. Use them
for delayed-capture flows: pre-orders, ride-hailing, hotels — anywhere
the final amount isn't known when the customer pays.
Authorizations are created implicitly through `checkout` /
`embedded_checkout` / `transfers` (when the appropriate flag is set).
The SDK exposes the read + capture/void surface on the resulting
authorization object.
Accessed via `client.authorizations`.
## Methods [#methods]
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /authorizations`.
```ruby
client.authorizations.list(limit: 50)
```
### `retrieve(id)` [#retrieveid]
`GET /authorizations/:id`.
### `capture(id, amount:)` [#captureid-amount]
`POST /authorizations/:id/capture`. `amount` is in minor units and may
be less than or equal to the authorized amount.
```ruby
client.authorizations.capture("auth_…", amount: 1_000)
```
### `void(id)` [#voidid]
`POST /authorizations/:id/void`. Release the held funds without
capturing.
```ruby
client.authorizations.void("auth_…")
```
## Object shape [#object-shape]
`:id`, `:state` (`SUCCEEDED`, `CAPTURED`, `VOIDED`, …), `:amount`,
`:captured_amount`, `:currency`, `:source`, `:expires_at`, `:created_at`, …
{/* TODO: enumerate the full authorization schema. */}
## Webhook events [#webhook-events]
Subscribe to `authorization.created`, `authorization.updated`, and
`authorization.voided`.
## Examples [#examples]
### Capture less than the authorized amount [#capture-less-than-the-authorized-amount]
```ruby
auth = client.authorizations.retrieve("auth_…")
client.authorizations.capture(auth[:id], amount: auth[:amount] - 500)
```
### Void on cancelled order [#void-on-cancelled-order]
```ruby
client.authorizations.void("auth_…")
```
# Checkout (/docs/sdks/ruby/resources/checkout)
One-shot, server-driven checkout. Use this when you've collected
payment-instrument data via tokenisation and want to create the customer
* instrument + charge in a single API call. For a hosted page, use
[Payment Links](./payment-links). For an iframe-embeddable widget, use
[Embedded Checkout](./embedded-checkout).
Accessed via `client.checkout`. Maps to a single endpoint.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /checkout`. Accepts both card/bank and wallet (crypto) variants —
the API discriminates on the body shape. Pass `source` for a tokenised
payment instrument or wallet details for a wallet-checkout flow.
```ruby
session = client.checkout.create(
customer_creation: false,
identity_id: customer[:id],
source: token_id,
line_items: [
{ name: "Pro plan", quantity: 1, unit_price: 4_900, currency: "USD" }
]
)
```
## Object shape [#object-shape]
The response includes the resulting `:order`, `:transfer` (if a charge
was made), and `:customer` records. Wallet-checkout responses additionally
include the on-chain transaction reference.
{/* TODO: enumerate the full checkout response payload, including the wallet-variant fields. */}
## Examples [#examples]
### Charge a tokenised card on a guest checkout [#charge-a-tokenised-card-on-a-guest-checkout]
```ruby
client.checkout.create(
customer_creation: true,
customer_details: { first_name: "Ada", last_name: "Lovelace", email: "ada@example.com" },
source: token_from_browser,
line_items: [
{ name: "Annual plan", quantity: 1, unit_price: 19_900, currency: "USD" }
]
)
```
### Wallet (crypto) checkout [#wallet-crypto-checkout]
{/* TODO: document the wallet-checkout request body once the canonical shape is finalised — it currently shares the `/checkout` endpoint via body discrimination. */}
# Compliance Forms (/docs/sdks/ruby/resources/compliance-forms)
Compliance forms are the merchant agreements (terms of service,
processing agreements, etc.) that need an authorized signer's name and
title before certain platform features unlock.
Accessed via `client.compliance_forms`.
## Methods [#methods]
### `list` [#list]
`GET /compliance-forms`. Every form your account currently needs.
```ruby
client.compliance_forms.list
```
### `retrieve(id)` [#retrieveid]
`GET /compliance-forms/:id`.
```ruby
client.compliance_forms.retrieve("cf_…")
```
### `sign(id, name:, title:)` [#signid-name-title]
`PUT /compliance-forms/:id/sign`. Records the signer's name and title
against the form.
```ruby
client.compliance_forms.sign("cf_…", name: "Ada Lovelace", title: "CEO")
```
## Object shape [#object-shape]
`:id`, `:type`, `:state` (`UNSIGNED`, `SIGNED`), `:document_url`,
`:signed_by`, `:signed_at`, `:created_at`, …
{/* TODO: enumerate the full compliance-form schema. */}
## Examples [#examples]
### Sign every outstanding form [#sign-every-outstanding-form]
```ruby
client.compliance_forms.list[:data].each do |form|
next if form[:state] == "SIGNED"
client.compliance_forms.sign(form[:id], name: "Ada Lovelace", title: "CEO")
end
```
# Coupons (/docs/sdks/ruby/resources/coupons)
A coupon is a reusable discount template — percent-off or amount-off,
applied once or recurring across N billing periods. Promotion codes are
the customer-facing names you hand out; coupons are the underlying
discount.
Accessed via `client.coupons`.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /coupons`.
```ruby
client.coupons.create(percent_off: 25, duration: "once")
client.coupons.create(amount_off: 1_000, currency: "USD", duration: "repeating", duration_in_months: 3)
```
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /coupons`.
### `retrieve(id)` [#retrieveid]
`GET /coupons/:id`.
### `update(id, **body)` [#updateid-body]
`PATCH /coupons/:id`. Coupons are mostly immutable — the metadata and
`active` flag are the practical update targets.
```ruby
client.coupons.update("cpn_…", active: false)
```
### `delete(id)` [#deleteid]
`DELETE /coupons/:id`.
## Object shape [#object-shape]
`:id`, `:percent_off`, `:amount_off`, `:currency`, `:duration` (`once`,
`forever`, `repeating`), `:duration_in_months`, `:max_redemptions`,
`:redeem_by`, `:active`, `:created_at`, …
{/* TODO: link to canonical coupon schema. */}
## Examples [#examples]
### Create a one-time 25% off coupon [#create-a-one-time-25-off-coupon]
```ruby
coupon = client.coupons.create(percent_off: 25, duration: "once")
```
### Wrap a coupon in a customer-facing promotion code [#wrap-a-coupon-in-a-customer-facing-promotion-code]
```ruby
coupon = client.coupons.create(percent_off: 50, duration: "once")
client.promotion_codes.create(coupon_id: coupon[:id], code: "LAUNCH50", max_redemptions: 100)
```
### Retire a coupon without deleting it [#retire-a-coupon-without-deleting-it]
```ruby
client.coupons.update("cpn_…", active: false)
```
# Customers (/docs/sdks/ruby/resources/customers)
A `Customer` is the identity Easy Labs uses to associate payment
instruments, orders, subscriptions, and wallets. Most resources accept
either a customer-shaped payload inline or an `identity_id` referencing a
previously created customer.
Accessed via `client.customers`.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /customer`. Body is a hash of customer fields — at minimum `email`,
`first_name`, `last_name`.
```ruby
customer = client.customers.create(
first_name: "Ada",
last_name: "Lovelace",
email: "ada@example.com"
)
```
### `update(id, **body)` [#updateid-body]
`PATCH /customer/:id`. Pass only the fields you want to change.
```ruby
client.customers.update(customer[:id], phone: "+15555550100")
```
### `retrieve(id)` [#retrieveid]
`GET /customer/:id`.
```ruby
client.customers.retrieve("cust_…")
```
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /customer`. Standard pagination keywords (see [Pagination](../pagination)).
```ruby
page = client.customers.list(limit: 50)
page = client.customers.list(ids: ["cust_a", "cust_b"])
```
### `payment_instruments(id)` [#payment_instrumentsid]
`GET /customer/:id/instruments`. Every payment instrument tokenised
against this customer.
```ruby
client.customers.payment_instruments("cust_…")
```
### `orders(id, limit: nil, offset: nil, ids: nil)` [#ordersid-limit-nil-offset-nil-ids-nil]
`GET /customer/:id/orders`.
```ruby
client.customers.orders("cust_…", limit: 25)
```
### `subscriptions(id, status: nil, limit: nil, offset: nil, ids: nil)` [#subscriptionsid-status-nil-limit-nil-offset-nil-ids-nil]
`GET /customer/:id/subscriptions`. Optional `status:` filter
(`active`, `paused`, `canceled`, …).
```ruby
client.customers.subscriptions("cust_…", status: "active")
```
### `wallets(id, limit: nil, offset: nil, ids: nil)` [#walletsid-limit-nil-offset-nil-ids-nil]
`GET /customer/:id/wallets`. Crypto wallet records associated with the
customer.
```ruby
client.customers.wallets("cust_…")
```
## Object shape [#object-shape]
The response is a Ruby `Hash` with symbol keys mirroring the API
payload — `:id`, `:first_name`, `:last_name`, `:email`, `:phone`,
`:created_at`, `:updated_at`, `:tags`, …
See the [API reference](https://api-docs.itseasy.co) for the canonical
schema; the SDK passes payloads through as-is.
## Examples [#examples]
### Create or update by email [#create-or-update-by-email]
```ruby
def upsert_customer(client, email:, **fields)
existing = client.customers.list(limit: 1).dig(:data, 0) # filter on email server-side when supported
if existing && existing[:email] == email
client.customers.update(existing[:id], **fields)
else
client.customers.create(email: email, **fields)
end
end
```
### List active subscriptions for a customer [#list-active-subscriptions-for-a-customer]
```ruby
client.customers
.subscriptions("cust_…", status: "active")
.fetch(:data, [])
.each { |sub| puts "#{sub[:id]} — #{sub[:plan_name]}" }
```
### Walk every customer [#walk-every-customer]
```ruby
offset = 0
loop do
page = client.customers.list(limit: 100, offset: offset)
rows = page[:data] || []
rows.each { |c| puts c[:email] }
break if rows.size < 100
offset += rows.size
end
```
# Disputes (/docs/sdks/ruby/resources/disputes)
A dispute (a.k.a. chargeback) is a customer's challenge to a transfer.
The SDK lets you list and retrieve disputes, mutate their tags, and
drive the lifecycle: accept the dispute, upload evidence, or submit
your response.
Accessed via `client.disputes`.
## Methods [#methods]
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /disputes`.
```ruby
client.disputes.list(limit: 25)
```
### `retrieve(id)` [#retrieveid]
`GET /disputes/:id`.
### `update(id, tags:)` [#updateid-tags]
`PATCH /disputes/:id`. Mutates `tags` only.
```ruby
client.disputes.update("dp_…", tags: { reason: "duplicate", agent: "support@example.com" })
```
## Lifecycle actions [#lifecycle-actions]
### `accept(id)` [#acceptid]
`POST /disputes/:id/accept`. Concede the dispute.
```ruby
client.disputes.accept("dp_…")
```
### `submit(id)` [#submitid]
`POST /disputes/:id/submit`. Submit your evidence package for review.
```ruby
client.disputes.submit("dp_…")
```
### `upload_evidence(id, **parts)` [#upload_evidenceid-parts]
`POST /disputes/:id/evidence`. Multipart/form-data upload — pass scalar
fields as keyword args and the file as a `Net::HTTP::UploadIO` (or any
IO-like object accepted by the [`multipart-post`](https://rubygems.org/gems/multipart-post) gem).
```ruby
require "net/http/post/multipart"
client.disputes.upload_evidence(
"dp_…",
evidence_type: "receipt",
file: UploadIO.new(File.open("receipt.pdf"), "application/pdf", "receipt.pdf")
)
```
### `list_evidence(id)` [#list_evidenceid]
`GET /disputes/:id/evidence`. Every evidence item attached to the
dispute.
```ruby
client.disputes.list_evidence("dp_…")
```
## Object shape [#object-shape]
`:id`, `:transfer_id`, `:reason`, `:state` (`NEEDS_RESPONSE`,
`UNDER_REVIEW`, `WON`, `LOST`, `ACCEPTED`, …), `:amount`, `:currency`,
`:respond_by`, `:evidence`, `:tags`, `:created_at`, …
{/* TODO: enumerate the full dispute schema. */}
## Webhook events [#webhook-events]
Subscribe to `dispute.created` and `dispute.updated`.
## Examples [#examples]
### Build an evidence package and submit it [#build-an-evidence-package-and-submit-it]
```ruby
require "net/http/post/multipart"
File.open("invoice.pdf") do |io|
client.disputes.upload_evidence(
"dp_…",
evidence_type: "invoice",
file: UploadIO.new(io, "application/pdf", "invoice.pdf")
)
end
File.open("shipping_label.pdf") do |io|
client.disputes.upload_evidence(
"dp_…",
evidence_type: "shipping_documentation",
file: UploadIO.new(io, "application/pdf", "shipping_label.pdf")
)
end
client.disputes.submit("dp_…")
```
### Concede a clearly-fraudulent dispute [#concede-a-clearly-fraudulent-dispute]
```ruby
client.disputes.accept("dp_…")
```
# Dunning (/docs/sdks/ruby/resources/dunning)
Dunning is the recovery flow that runs when an invoice or subscription
payment fails — retry schedules, reminder emails, and recovery
automations. The SDK exposes two related resources:
* `client.dunning_config` — the singleton config that controls retry
behaviour and customer-facing emails.
* `client.revenue_recovery_automations` — programmable rules that fire
in response to recovery events (e.g. "if invoice still unpaid after 7
days, cancel the subscription").
## Dunning config [#dunning-config]
A single config per merchant. `create_or_replace` POSTs the full config;
`update` PATCHes the fields you pass; `retrieve` reads the current state.
### `client.dunning_config.create_or_replace(**body)` [#clientdunning_configcreate_or_replacebody]
`POST /dunning-config`.
```ruby
client.dunning_config.create_or_replace(
retry_mode: "smart",
smart_retry_attempts: 4,
payment_failed_email_enabled: true
)
```
### `client.dunning_config.retrieve` [#clientdunning_configretrieve]
`GET /dunning-config`.
### `client.dunning_config.update(**body)` [#clientdunning_configupdatebody]
`PATCH /dunning-config`.
```ruby
client.dunning_config.update(payment_failed_email_enabled: false)
```
## Revenue recovery automations [#revenue-recovery-automations]
Rule-based actions that fire on recovery events. Each rule has a
`trigger_type:` and an action body the API interprets.
### `client.revenue_recovery_automations.list` [#clientrevenue_recovery_automationslist]
`GET /revenue-recovery-automations`.
### `client.revenue_recovery_automations.create(**body)` [#clientrevenue_recovery_automationscreatebody]
`POST /revenue-recovery-automations`.
```ruby
client.revenue_recovery_automations.create(
trigger_type: "invoice_overdue",
conditions: { days_overdue: 7 },
actions: [{ type: "cancel_subscription" }]
)
```
### `client.revenue_recovery_automations.update(id, **body)` [#clientrevenue_recovery_automationsupdateid-body]
`PATCH /revenue-recovery-automations/:id`.
```ruby
client.revenue_recovery_automations.update("auto_…", active: false)
```
### `client.revenue_recovery_automations.delete(id)` [#clientrevenue_recovery_automationsdeleteid]
`DELETE /revenue-recovery-automations/:id`.
### `client.revenue_recovery_automations.runs(id)` [#clientrevenue_recovery_automationsrunsid]
`GET /revenue-recovery-automations/:id/runs`. Audit trail of every time
this automation fired.
```ruby
client.revenue_recovery_automations.runs("auto_…")
```
## Object shapes [#object-shapes]
`DunningConfig` — `:retry_mode` (`smart` | `manual` | `off`),
`:smart_retry_attempts`, `:retry_schedule`,
`:payment_failed_email_enabled`, `:reminder_emails`, …
`RevenueRecoveryAutomation` — `:id`, `:trigger_type`, `:conditions`,
`:actions`, `:active`, `:created_at`, …
{/* TODO: enumerate the full config + automation schemas. */}
## Webhook events [#webhook-events]
Subscribe to `invoice.payment_failed` to react when dunning starts and
`revenue_recovery.action_completed` when an automation runs.
## Examples [#examples]
### Tighten dunning policy [#tighten-dunning-policy]
```ruby
client.dunning_config.update(retry_mode: "smart", smart_retry_attempts: 6)
```
### Auto-cancel after a week of failed retries [#auto-cancel-after-a-week-of-failed-retries]
```ruby
client.revenue_recovery_automations.create(
trigger_type: "invoice_overdue",
conditions: { days_overdue: 7 },
actions: [{ type: "cancel_subscription", at_period_end: false }]
)
```
# Embedded Checkout (/docs/sdks/ruby/resources/embedded-checkout)
An embedded-checkout session is the server-side counterpart to the
`@easylabs/embedded-checkout` browser widget. Your server creates the
session and returns the `client_secret` to the browser; the browser
calls `validate` and `confirm` directly with that secret (no API key
needed in client code).
Accessed via `client.embedded_checkout`.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /embedded-checkout`. Server-side. Returns the session including
the `client_secret`.
```ruby
session = client.embedded_checkout.create(
line_items: [{ price_id: "price_…", quantity: 1 }]
)
return_to_browser(session[:client_secret])
```
### `retrieve(id)` [#retrieveid]
`GET /embedded-checkout/:id`. Server-side session lookup.
```ruby
client.embedded_checkout.retrieve("sess_…")
```
### `crypto_status(id)` [#crypto_statusid]
`GET /embedded-checkout/:id/crypto-status`. Polls the on-chain status
for crypto sessions.
```ruby
client.embedded_checkout.crypto_status("sess_…")
```
### `validate(client_secret:, parent_origin: nil)` [#validateclient_secret-parent_origin-nil]
`POST /embedded-checkout/validate`. **Public endpoint** — authenticates
via the session's `client_secret` and skips the `X-Easy-Api-Key` header
entirely, so it's safe to call from a browser-like context. The SDK still
exposes it for completeness (e.g. server-side smoke tests).
```ruby
client.embedded_checkout.validate(
client_secret: "cs_…",
parent_origin: "https://shop.example.com"
)
```
### `confirm(client_secret:, source:, customer_details:)` [#confirmclient_secret-source-customer_details]
`POST /embedded-checkout/confirm`. **Public endpoint** — same auth
behaviour as `validate`.
```ruby
client.embedded_checkout.confirm(
client_secret: "cs_…",
source: { type: "PAYMENT_CARD", tokenId: "tok_…" },
customer_details: { first_name: "Ada", last_name: "Lovelace", email: "ada@example.com" }
)
```
### `config` [#config]
`GET /embedded-checkout/config`. Returns the merchant-level embedded
config (e.g. `allowed_origins`).
```ruby
client.embedded_checkout.config
```
### `update_config(allowed_origins: nil)` [#update_configallowed_origins-nil]
`PATCH /embedded-checkout/config`.
```ruby
client.embedded_checkout.update_config(allowed_origins: ["https://shop.example.com"])
```
## Object shape [#object-shape]
A session response contains `:id`, `:client_secret`, `:status`,
`:line_items`, `:amount`, `:currency`, `:expires_at`, …
{/* TODO: enumerate the full embedded-checkout session schema. */}
## Examples [#examples]
### Create a session and pass the secret to the browser [#create-a-session-and-pass-the-secret-to-the-browser]
```ruby
def create_checkout_session(price_id)
session = client.embedded_checkout.create(
line_items: [{ price_id: price_id, quantity: 1 }]
)
{ client_secret: session[:client_secret] }
end
```
### Lock down `allowed_origins` [#lock-down-allowed_origins]
```ruby
client.embedded_checkout.update_config(
allowed_origins: ["https://shop.example.com", "https://staging.shop.example.com"]
)
```
# Invoices (/docs/sdks/ruby/resources/invoices)
Invoices represent both the recurring invoices automatically generated
by the subscription engine and ad-hoc invoices created via the API for
one-off billing.
Accessed via `client.invoices`.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /invoices`. Create a manual invoice.
```ruby
invoice = client.invoices.create(
to_email: "client@example.com",
due_date: "2026-05-15",
items: [
{ description: "Consulting — April", quantity: 10, unit_price: 25_000 },
{ description: "Travel reimbursement", quantity: 1, unit_price: 12_500 }
]
)
```
### `list(**query)` [#listquery]
`GET /invoices`. Accepts arbitrary query params — `status:`, `customer_id:`,
plus the usual `limit:` / `offset:`.
```ruby
client.invoices.list(status: "OPEN", limit: 50)
```
### `retrieve(id)` [#retrieveid]
`GET /invoices/:id`.
### `update(id, **body)` [#updateid-body]
`PATCH /invoices/:id`. Mutate notes, line items, or due date on an
unfinalised invoice.
```ruby
client.invoices.update("inv_…", notes: "Net 15 on this one — thanks Ada.")
```
### `send_invoice(id, **body)` [#send_invoiceid-body]
`POST /invoices/:id/send`. Renamed from `send` to avoid collision with
`Object#send`.
```ruby
client.invoices.send_invoice("inv_…")
```
### `pay(id, **body)` [#payid-body]
`POST /invoices/:id/pay`. Charge an invoice immediately using a saved
instrument.
```ruby
client.invoices.pay("inv_…", instrument_id: card[:id])
```
### `remind(id)` [#remindid]
`POST /invoices/:id/remind`. Trigger a reminder email to the customer.
```ruby
client.invoices.remind("inv_…")
```
### `void(id)` [#voidid]
`POST /invoices/:id/void`.
```ruby
client.invoices.void("inv_…")
```
### `pdf_data(id)` [#pdf_dataid]
`GET /invoices/:id/pdf`. Returns a JSON payload shaped for client-side
PDF rendering rather than a binary PDF blob.
```ruby
client.invoices.pdf_data("inv_…")
```
## Object shape [#object-shape]
`:id`, `:status` (`DRAFT`, `OPEN`, `PAID`, `VOID`, `UNCOLLECTIBLE`),
`:customer`, `:items`, `:subtotal`, `:total`, `:amount_due`, `:due_date`,
`:hosted_invoice_url`, `:created_at`, …
{/* TODO: enumerate the full invoice schema. */}
## Examples [#examples]
### Create + send in one go [#create--send-in-one-go]
```ruby
invoice = client.invoices.create(
to_email: "client@example.com",
due_date: 14.days.from_now.to_date.iso8601,
items: [{ description: "Plan", quantity: 1, unit_price: 4_900 }]
)
client.invoices.send_invoice(invoice[:id])
```
### Auto-charge on creation [#auto-charge-on-creation]
```ruby
invoice = client.invoices.create(...)
client.invoices.pay(invoice[:id], instrument_id: customer_default_card_id)
```
### Render a custom PDF client-side [#render-a-custom-pdf-client-side]
```ruby
data = client.invoices.pdf_data("inv_…")
MyPdfRenderer.render(data)
```
# Orders (/docs/sdks/ruby/resources/orders)
An order represents a discrete transaction in your system — typically
created server-side after a checkout completes. The SDK exposes a
read-only orders list plus a tag-update for reconciliation metadata.
Order creation happens implicitly through checkout, payment links, and
invoices.
Accessed via `client.orders`.
## Methods [#methods]
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /orders`.
```ruby
client.orders.list(limit: 50)
```
### `retrieve(id)` [#retrieveid]
`GET /orders/:id`.
```ruby
client.orders.retrieve("ord_…")
```
### `update_tags(id, tags:)` [#update_tagsid-tags]
`PATCH /orders/:id` with `{ tags }`. The legacy `/orders/:id/tags`
suffix returns 404 on the current API and is not used by the SDK.
```ruby
client.orders.update_tags("ord_…", tags: { region: "us-east", source: "shopify" })
```
## Object shape [#object-shape]
`:id`, `:state`, `:amount`, `:currency`, `:customer`, `:line_items`,
`:tags`, `:created_at`, … plus references to the originating checkout
session, payment link, or invoice.
{/* TODO: enumerate the full order schema once the public reference is finalised. */}
## Examples [#examples]
### Tag an order with internal reconciliation IDs [#tag-an-order-with-internal-reconciliation-ids]
```ruby
client.orders.update_tags(
"ord_…",
tags: { netsuite_id: "12345", reconciled_at: Time.now.iso8601 }
)
```
### Pull recent orders for a CSV export [#pull-recent-orders-for-a-csv-export]
```ruby
rows = client.orders.list(limit: 100)[:data] || []
rows.each do |order|
puts [order[:id], order[:amount], order[:currency], order[:state]].join(",")
end
```
# Payment Instruments (/docs/sdks/ruby/resources/payment-instruments)
A payment instrument is a tokenised card or bank account attached to a
customer. The Easy API stores Finix-shaped payment-instrument objects;
the SDK exposes the two server-safe operations: `create` (with a token
from the front-end) and `update` (e.g. to disable an instrument).
Tokenisation itself happens client-side via BasisTheory — the
`basis_theory_public_api_key` returned at client construction is what you
hand to the browser SDK. Never send raw card data to the API.
Accessed via `client.payment_instruments`.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /payment`. Attach a tokenised instrument to a customer.
```ruby
client.payment_instruments.create(
tokenId: "tok_…", # from BasisTheory tokenisation
identityId: customer[:id],
type: "PAYMENT_CARD",
name: "Card on file"
)
```
### `update(id, **body)` [#updateid-body]
`PATCH /payment/:id`. Mutate metadata, disable, or set as default.
```ruby
client.payment_instruments.update("pi_…", enabled: false)
```
## Object shape [#object-shape]
Returned shape mirrors the Finix payment-instrument object — `:id`,
`:type`, `:name`, `:enabled`, `:identity`, `:tags`, `:bin`,
`:last_four`, `:brand`, …
{/* TODO: link to canonical API reference for the full Finix-shaped payload. */}
## Examples [#examples]
### Customer with a single card on file [#customer-with-a-single-card-on-file]
```ruby
customer = client.customers.create(
first_name: "Ada", last_name: "Lovelace", email: "ada@example.com"
)
card = client.payment_instruments.create(
tokenId: token_from_browser,
identityId: customer[:id],
type: "PAYMENT_CARD",
name: "Personal card"
)
client.transfers.create(amount: 1000, currency: "USD", source: card[:id])
```
### Disable an instrument without deleting it [#disable-an-instrument-without-deleting-it]
```ruby
client.payment_instruments.update(card[:id], enabled: false)
```
# Payment Links (/docs/sdks/ruby/resources/payment-links)
A payment link is a hosted, shareable URL that runs the Easy checkout
without any code on your site. Use them for invoicing, sales outreach,
and one-off requests for payment.
Accessed via `client.payment_links`. Maps to the `/v1/api/payment-link/*`
routes — branding endpoints under `/v1/auth/branding/payment-link/*` are
intentionally out of scope for this SDK.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /payment-link`.
```ruby
link = client.payment_links.create(
amount_type: "FIXED",
products: [
{ name: "Annual plan", quantity: 1, unit_price: 19_900, currency: "USD" }
]
)
puts link[:url]
```
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /payment-link/`.
```ruby
client.payment_links.list(limit: 25)
```
### `retrieve(id)` [#retrieveid]
`GET /payment-link/:id`.
```ruby
client.payment_links.retrieve("plink_…")
```
### `update(id, **body)` [#updateid-body]
`PATCH /payment-link/:id`. Adjust the active state, expiration, products,
or metadata.
```ruby
client.payment_links.update("plink_…", active: false)
```
### `delete(id)` [#deleteid]
`DELETE /payment-link/:id`.
```ruby
client.payment_links.delete("plink_…")
```
### `list_payments(id, limit: nil, offset: nil, ids: nil)` [#list_paymentsid-limit-nil-offset-nil-ids-nil]
`GET /payment-link/:id/payments`. Every payment captured against a link.
```ruby
client.payment_links.list_payments("plink_…", limit: 100)
```
## Object shape [#object-shape]
The response includes `:id`, `:url`, `:amount_type` (`FIXED` or
`CUSTOMER_DECIDES`), `:products`, `:active`, `:expires_at`, `:created_at`,
`:metadata`, …
{/* TODO: enumerate the full payment-link object once the public schema is finalised. */}
## Examples [#examples]
### Send a fixed-amount link via email [#send-a-fixed-amount-link-via-email]
```ruby
link = client.payment_links.create(
amount_type: "FIXED",
products: [{ name: "Consultation", quantity: 1, unit_price: 25_000, currency: "USD" }],
expires_at: (Time.now + 7 * 86_400).iso8601
)
CustomerMailer.with(to: "client@example.com", url: link[:url]).pay_now.deliver_later
```
### Reconcile payments captured by a link [#reconcile-payments-captured-by-a-link]
```ruby
client.payment_links
.list_payments("plink_…", limit: 100)
.fetch(:data, [])
.each { |p| puts "#{p[:id]} — #{p[:amount]} #{p[:currency]}" }
```
# Products Pricing (/docs/sdks/ruby/resources/products-pricing)
Products and prices are the primitives behind subscriptions and recurring
billing. A `Product` describes the thing being sold; a `Price` (a.k.a.
product-price) describes how it's billed — currency, amount, billing
interval, and tax behaviour.
The SDK splits these across two resources:
* `client.products` — product CRUD plus nested-price reads.
* `client.product_prices` — price CRUD.
## Products [#products]
### `client.products.list(limit: nil, offset: nil, ids: nil)` [#clientproductslistlimit-nil-offset-nil-ids-nil]
`GET /products`.
```ruby
client.products.list(limit: 25)
```
### `client.products.retrieve(id)` [#clientproductsretrieveid]
`GET /products/:id`.
### `client.products.create(**body)` [#clientproductscreatebody]
`POST /products`.
```ruby
product = client.products.create(name: "Pro plan", description: "Everything in Pro.")
```
### `client.products.update(id, **body)` [#clientproductsupdateid-body]
`PATCH /products/:id`.
```ruby
client.products.update("pr_…", name: "Pro plan v2")
```
### `client.products.archive(id)` [#clientproductsarchiveid]
`PATCH /products/:id/archive`.
```ruby
client.products.archive("pr_…")
```
### `client.products.with_prices(id)` [#clientproductswith_pricesid]
`GET /products/:id/prices`. Product object plus its full nested price
list — handy for catalog rendering.
### `client.products.with_price(id, price_id)` [#clientproductswith_priceid-price_id]
`GET /products/:id/prices/:price_id`. Product plus a single price.
## Product prices [#product-prices]
### `client.product_prices.list(limit: nil, offset: nil, ids: nil)` [#clientproduct_priceslistlimit-nil-offset-nil-ids-nil]
`GET /product-prices`.
### `client.product_prices.retrieve(id)` [#clientproduct_pricesretrieveid]
`GET /product-prices/:id`.
### `client.product_prices.create(**body)` [#clientproduct_pricescreatebody]
`POST /product-prices`. Body shape is a tagged union: pass either a
recurring price or a one-off. The API enforces the discriminator.
```ruby
client.product_prices.create(
product_id: "pr_…",
active: true,
recurring: true,
currency: "USD",
unit_amount: 5_000,
interval: "month",
interval_count: 1,
tax_behavior: "exclusive"
)
```
### `client.product_prices.update(id, **body)` [#clientproduct_pricesupdateid-body]
`PATCH /product-prices/:id`.
```ruby
client.product_prices.update("price_…", active: false)
```
### `client.product_prices.archive(id)` [#clientproduct_pricesarchiveid]
`PATCH /product-prices/:id/archive`.
## Object shapes [#object-shapes]
`Product` — `:id`, `:name`, `:description`, `:active`, `:metadata`,
`:created_at`, …
`Price` — `:id`, `:product_id`, `:currency`, `:unit_amount`,
`:recurring`, `:interval`, `:interval_count`, `:tax_behavior`, `:active`,
…
{/* TODO: enumerate the full price union (recurring vs. one-off, tiered, metered) once the canonical schema is published. */}
## Examples [#examples]
### Create a product with a monthly price [#create-a-product-with-a-monthly-price]
```ruby
product = client.products.create(name: "Pro plan")
price = client.product_prices.create(
product_id: product[:id],
active: true,
recurring: true,
currency: "USD",
unit_amount: 4_900,
interval: "month",
interval_count: 1,
tax_behavior: "exclusive"
)
client.subscriptions.create(
identity_id: customer[:id],
items: [{ price_id: price[:id] }],
instrument_id: card[:id]
)
```
### Render a catalog page [#render-a-catalog-page]
```ruby
products = client.products.list(limit: 100)[:data] || []
catalog = products.map { |p| client.products.with_prices(p[:id]) }
```
# Promotion Codes (/docs/sdks/ruby/resources/promotion-codes)
A promotion code is the customer-facing string (e.g. `LAUNCH50`) that
maps to a `Coupon`. Promotion codes can scope, limit, and gate the
underlying discount per identity.
Accessed via `client.promotion_codes`.
## Methods [#methods]
### `create(**body)` [#createbody]
`POST /promotion-codes`. Requires `coupon_id:`; `code:` is optional
(the API generates one if omitted).
```ruby
client.promotion_codes.create(
coupon_id: "cpn_…",
code: "SUMMER25",
max_redemptions: 1_000,
expires_at: "2026-09-01T00:00:00Z"
)
```
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /promotion-codes`.
### `retrieve(id)` [#retrieveid]
`GET /promotion-codes/:id`.
### `update(id, **body)` [#updateid-body]
`PATCH /promotion-codes/:id`.
```ruby
client.promotion_codes.update("pc_…", active: false)
```
### `delete(id)` [#deleteid]
`DELETE /promotion-codes/:id`.
### `validate(code:, identity_id: nil, amount: nil)` [#validatecode-identity_id-nil-amount-nil]
`POST /promotion-codes/validate`. Server-side validity check before
applying the code at checkout. Optionally scope the check to a specific
identity and/or order amount.
```ruby
result = client.promotion_codes.validate(
code: "SUMMER25",
identity_id: customer[:id],
amount: 4_900
)
```
## Object shape [#object-shape]
`:id`, `:code`, `:coupon_id`, `:active`, `:expires_at`, `:max_redemptions`,
`:times_redeemed`, `:restrictions`, …
{/* TODO: link to canonical promotion-code schema. */}
## Examples [#examples]
### Validate before applying [#validate-before-applying]
```ruby
result = client.promotion_codes.validate(code: params[:code], identity_id: customer[:id])
if result[:valid]
client.subscriptions.apply_discount(sub[:id], promotion_code: params[:code])
else
flash[:error] = "That code can't be used on this subscription."
end
```
### Generate a one-shot personalised code [#generate-a-one-shot-personalised-code]
```ruby
client.promotion_codes.create(
coupon_id: "cpn_winback",
max_redemptions: 1,
metadata: { customer_id: customer[:id] }
)
```
# Refunds (/docs/sdks/ruby/resources/refunds)
There is no top-level `Refunds` resource — refunds are issued against an
existing transfer via the `reversals` sub-resource. Use
`client.transfers.create_refund` to issue them.
For a top-level refunds index / retrieve API, **coming soon**.
{/* TODO: add a dedicated `client.refunds` resource (`list`, `retrieve`) once the API exposes a top-level `/refunds` index. */}
## Issuing a refund [#issuing-a-refund]
```ruby
# Full refund
charge = client.transfers.retrieve("tr_…")
client.transfers.create_refund(charge[:id], refund_amount: charge[:amount])
# Partial refund
client.transfers.create_refund("tr_…", refund_amount: 500)
# With reconciliation tags
client.transfers.create_refund(
"tr_…",
refund_amount: 1_000,
tags: { reason: "customer_request", agent: "support@example.com" }
)
```
See [Transfers › `create_refund`](./transfers#create_refundtransfer_id-refund_amount-tags-nil)
for the full method signature.
## Reading refund history [#reading-refund-history]
The `:reversals` field on the parent transfer contains every refund
issued against it:
```ruby
charge = client.transfers.retrieve("tr_…")
charge[:reversals]&.each do |refund|
puts "#{refund[:id]} — #{refund[:amount]}"
end
```
## Webhook events [#webhook-events]
Subscribe to `refund.created` and `refund.updated` to react to refund
state changes asynchronously. See [Webhooks](../webhooks) for the
verifier and the full event-type catalog.
# Settlements (/docs/sdks/ruby/resources/settlements)
A settlement is a batch of transfers (charges and refunds) grouped for
payout — the unit of reconciliation between Easy Labs and your bank.
Read-only on the SDK side, plus a `close` action.
Accessed via `client.settlements`.
## Methods [#methods]
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /settlements`.
```ruby
client.settlements.list(limit: 50)
```
### `retrieve(id)` [#retrieveid]
`GET /settlements/:id`.
```ruby
client.settlements.retrieve("st_…")
```
### `close(id)` [#closeid]
`PATCH /settlements/:id`. Closes an open settlement so the payout can
be initiated.
```ruby
client.settlements.close("st_…")
```
## Object shape [#object-shape]
`:id`, `:state` (`OPEN`, `CLOSED`, `PAID`, …), `:total_amount`,
`:net_amount`, `:fees`, `:transfers`, `:created_at`, `:closed_at`, …
{/* TODO: enumerate the full settlement schema. */}
## Webhook events [#webhook-events]
Subscribe to `settlement.created` to react when a new settlement opens.
## Examples [#examples]
### Daily reconciliation export [#daily-reconciliation-export]
```ruby
client.settlements
.list(limit: 100)[:data]
.select { |s| s[:state] == "CLOSED" }
.each { |s| ReconciliationJob.perform_later(s[:id]) }
```
### Manually close an open settlement [#manually-close-an-open-settlement]
```ruby
client.settlements.close("st_…")
```
# Subscriptions (/docs/sdks/ruby/resources/subscriptions)
The Easy-native subscription engine. Covers the full lifecycle:
create / list / retrieve, cancel / pause / resume, item CRUD, discount
application, one-time charges, metered usage reporting, and proration
preview.
Accessed via `client.subscriptions`. The largest resource in the SDK
(\~14 endpoints).
## Top-level methods [#top-level-methods]
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /subscriptions`.
```ruby
client.subscriptions.list(limit: 50)
```
### `retrieve(id)` [#retrieveid]
`GET /subscriptions/:id`.
### `create(**body)` [#createbody]
`POST /subscriptions`. Minimum required fields: `identity_id:`,
`items:`, `instrument_id:`.
```ruby
sub = client.subscriptions.create(
identity_id: customer[:id],
items: [{ price_id: "price_…" }],
instrument_id: card[:id]
)
```
### `update(id, **body)` [#updateid-body]
`PATCH /subscriptions/:id`. Update top-level fields like
`cancel_at_period_end:` or default payment method.
```ruby
client.subscriptions.update("sub_…", cancel_at_period_end: true)
```
### `cancel(id, at_period_end: nil)` [#cancelid-at_period_end-nil]
`DELETE /subscriptions/:id?at_period_end=…`. With no argument the
server's default applies; pass `true` / `false` explicitly to override.
```ruby
client.subscriptions.cancel("sub_…") # server default
client.subscriptions.cancel("sub_…", at_period_end: true) # cancel at end of period
client.subscriptions.cancel("sub_…", at_period_end: false) # cancel immediately
```
## Pause / resume [#pause--resume]
### `pause(id, behavior:, resumes_at: nil)` [#pauseid-behavior-resumes_at-nil]
`POST /subscriptions/:id/pause`. `behavior` controls how the API treats
unpaid invoices during the pause (`"void"`, `"keep_as_draft"`, …).
```ruby
client.subscriptions.pause("sub_…", behavior: "void", resumes_at: "2026-06-01T00:00:00Z")
```
### `resume(id)` [#resumeid]
`POST /subscriptions/:id/resume`.
## Items [#items]
### `add_item(id, price_id:, quantity: nil)` [#add_itemid-price_id-quantity-nil]
`POST /subscriptions/:id/items`.
```ruby
client.subscriptions.add_item("sub_…", price_id: "price_addon", quantity: 2)
```
### `update_item(id, item_id, quantity:)` [#update_itemid-item_id-quantity]
`PATCH /subscriptions/:id/items/:item_id`.
```ruby
client.subscriptions.update_item("sub_…", "si_…", quantity: 5)
```
### `remove_item(id, item_id)` [#remove_itemid-item_id]
`DELETE /subscriptions/:id/items/:item_id`.
## Discounts [#discounts]
### `apply_discount(id, **body)` [#apply_discountid-body]
`POST /subscriptions/:id/discounts`. Pass exactly one of `coupon_id:`
or `promotion_code:`; optionally scope to a single item with
`subscription_item_id:`. The XOR is enforced server-side.
```ruby
client.subscriptions.apply_discount("sub_…", promotion_code: "SUMMER25")
```
### `list_discounts(id)` [#list_discountsid]
`GET /subscriptions/:id/discounts`.
### `remove_discount(id, discount_id)` [#remove_discountid-discount_id]
`DELETE /subscriptions/:id/discounts/:discount_id`.
## One-time charges [#one-time-charges]
### `create_one_time_charge(id, **body)` [#create_one_time_chargeid-body]
`POST /subscriptions/:id/one-time-charges`. Adds a non-recurring line to
the next invoice — useful for usage overages, setup fees, etc.
```ruby
client.subscriptions.create_one_time_charge(
"sub_…", price_id: "price_setup_fee", quantity: 1
)
```
## Metered usage [#metered-usage]
### `report_usage(id, **body)` [#report_usageid-body]
`POST /subscriptions/:id/usage`. Report a usage delta against a metered
subscription item.
```ruby
client.subscriptions.report_usage("sub_…", subscription_item_id: "si_…", quantity: 100)
```
### `usage_summary(id, **query)` [#usage_summaryid-query]
`GET /subscriptions/:id/usage/summary`.
### `usage_reconciliation(id, **query)` [#usage_reconciliationid-query]
`GET /subscriptions/:id/usage/reconciliation`.
## Proration preview [#proration-preview]
### `proration_preview(id, items: nil, remove_items: nil, proration_date: nil)` [#proration_previewid-items-nil-remove_items-nil-proration_date-nil]
`GET /subscriptions/:id/proration-preview`. Preview the charge that
would result from a planned change before applying it.
`items:` accepts the same shape as `add_item` — array of
`{ price_id:, quantity: }` hashes. The SDK encodes them as the
comma-joined `price_id:quantity` shape the API expects.
```ruby
preview = client.subscriptions.proration_preview(
"sub_…",
items: [{ price_id: "price_addon", quantity: 2 }],
remove_items: ["si_old_addon"]
)
```
## Object shape [#object-shape]
`:id`, `:identity_id`, `:status` (`active`, `paused`, `canceled`,
`trialing`, …), `:items`, `:current_period_start`,
`:current_period_end`, `:cancel_at_period_end`, `:default_payment_method`,
`:metadata`, …
{/* TODO: enumerate the full subscription schema once the public reference is published. */}
## Examples [#examples]
### Create, add an item, then preview the proration [#create-add-an-item-then-preview-the-proration]
```ruby
sub = client.subscriptions.create(
identity_id: customer[:id],
items: [{ price_id: "price_base" }],
instrument_id: card[:id]
)
preview = client.subscriptions.proration_preview(
sub[:id],
items: [{ price_id: "price_addon", quantity: 2 }]
)
if preview[:total] <= max_acceptable_charge
client.subscriptions.add_item(sub[:id], price_id: "price_addon", quantity: 2)
end
```
### Cancel at the end of the current period [#cancel-at-the-end-of-the-current-period]
```ruby
client.subscriptions.cancel("sub_…", at_period_end: true)
```
### Apply a coupon, then remove it [#apply-a-coupon-then-remove-it]
```ruby
discount = client.subscriptions.apply_discount("sub_…", coupon_id: "cpn_holiday")
client.subscriptions.remove_discount("sub_…", discount[:id])
```
# Transfers (/docs/sdks/ruby/resources/transfers)
A transfer is a single charge against a payment instrument — the API
primitive behind one-off payments and the source of refunds via the
`reversals` sub-resource.
Accessed via `client.transfers`.
## Methods [#methods]
### `create(amount:, currency:, source:, tags: nil)` [#createamount-currency-source-tags-nil]
`POST /transfer`. `amount` is in minor units (cents). `source` is a
payment-instrument ID.
```ruby
transfer = client.transfers.create(
amount: 1_500,
currency: "USD",
source: "pi_…",
tags: { order_id: "ord_…" }
)
```
### `update(id, **body)` [#updateid-body]
`PATCH /transfer/:id`. Used primarily to mutate `tags`.
```ruby
client.transfers.update("tr_…", tags: { reconciled: true })
```
### `retrieve(id)` [#retrieveid]
`GET /transfer/:id`.
```ruby
client.transfers.retrieve("tr_…")
```
### `list(limit: nil, offset: nil, ids: nil)` [#listlimit-nil-offset-nil-ids-nil]
`GET /transfer`.
```ruby
client.transfers.list(limit: 100)
```
### `create_refund(transfer_id, refund_amount:, tags: nil)` [#create_refundtransfer_id-refund_amount-tags-nil]
`POST /transfer/:id/reversals`. Issues a refund against an existing
transfer. `refund_amount` is in minor units; pass the full transfer
amount for a complete refund or a smaller value for a partial refund.
```ruby
client.transfers.create_refund(
"tr_…",
refund_amount: 500,
tags: { reason: "duplicate_charge" }
)
```
## Object shape [#object-shape]
`:id`, `:amount`, `:currency`, `:source`, `:state`, `:created_at`,
`:tags`, … plus a `:reversals` collection that grows as refunds are
issued.
{/* TODO: link to canonical Finix transfer schema. */}
## Examples [#examples]
### Charge a customer's default card [#charge-a-customers-default-card]
```ruby
instruments = client.customers.payment_instruments(customer[:id])[:data] || []
default = instruments.find { |pi| pi[:default] } || instruments.first
raise "no instrument on file" unless default
client.transfers.create(amount: 2_500, currency: "USD", source: default[:id])
```
### Full refund [#full-refund]
```ruby
charge = client.transfers.retrieve("tr_…")
client.transfers.create_refund(charge[:id], refund_amount: charge[:amount])
```
### Partial refund [#partial-refund]
```ruby
client.transfers.create_refund("tr_…", refund_amount: 500)
```
# Treasury (/docs/sdks/ruby/resources/treasury)
Treasury is the money-movement layer: linked bank accounts, ACH /
wire deposits and withdrawals, recipients (1099 and otherwise), payout
links, recurring payments, security rules, and approval workflows.
The resource keeps a flat method layout — sub-resources (recipients,
transactions, etc.) are exposed as prefixed methods rather than nested
objects, matching the rest of the SDK's convention.
Accessed via `client.treasury`. \~40 endpoints; only the most frequent
are documented here — see the source for the complete list.
## Dashboard [#dashboard]
### `dashboard_summary` [#dashboard_summary]
`GET /treasury/dashboard/summary`.
```ruby
client.treasury.dashboard_summary
```
## Bank accounts [#bank-accounts]
```ruby
client.treasury.list_bank_accounts
client.treasury.bank_account_link_token # for Plaid Link
client.treasury.link_bank_account(public_token: "…")
client.treasury.delete_bank_account("ba_…")
```
## Deposit [#deposit]
```ruby
client.treasury.deposit_bank_pull(amount: 100_00, source_id: "ba_…")
client.treasury.deposit_wire_instructions
```
## Withdraw [#withdraw]
```ruby
client.treasury.withdraw(amount: 50_000, destination_id: "ba_…")
client.treasury.confirm_withdraw(withdrawal_id: "wd_…")
```
## Transfer (between own accounts) [#transfer-between-own-accounts]
```ruby
client.treasury.transfer(amount: 10_000, source_id: "ba_a", destination_id: "ba_b")
client.treasury.confirm_transfer(transfer_id: "tx_…")
```
## Send (to a recipient) [#send-to-a-recipient]
```ruby
client.treasury.send_payment(amount: 25_000, recipient_id: "rec_…")
client.treasury.confirm_send(send_id: "snd_…")
client.treasury.cancel_send(send_id: "snd_…")
```
## Transactions [#transactions]
```ruby
client.treasury.list_transactions(limit: 100)
client.treasury.retrieve_transaction("tx_…")
client.treasury.update_transaction("tx_…", category_id: "cat_…")
client.treasury.transaction_settlement("tx_…")
client.treasury.export_transactions(start_date: "2026-04-01", end_date: "2026-04-30")
```
## Recipients [#recipients]
Full CRUD plus invitations, auto-pay, payment-method attach, W-9
collection, and bulk import via CSV upload.
```ruby
client.treasury.list_recipients(limit: 50)
client.treasury.create_recipient(first_name: "Pat", last_name: "Lee", email: "pat@example.com")
client.treasury.retrieve_recipient("rec_…")
client.treasury.update_recipient("rec_…", email: "new@example.com")
client.treasury.delete_recipient("rec_…")
client.treasury.invite_recipient("rec_…")
client.treasury.set_recipient_auto_pay("rec_…", enabled: true)
client.treasury.create_recipient_payment_method("rec_…", type: "ACH", account_number: "…", routing_number: "…")
client.treasury.request_recipient_w9("rec_…")
client.treasury.recipient_tax_info("rec_…")
client.treasury.update_recipient_tax_info("rec_…", tin: "…", classification: "individual")
client.treasury.list_recipient_invitations
client.treasury.accept_recipient_invite(invite_token: "…")
client.treasury.recipients_tax_report(year: 2025)
# Plaid for recipient-side bank linking
client.treasury.recipient_plaid_link_token(recipient_id: "rec_…")
client.treasury.recipient_plaid_exchange(recipient_id: "rec_…", public_token: "…")
```
### Bulk import [#bulk-import]
`POST /treasury/recipients/import` is multipart/form-data. Pass `file:`
as a `Net::HTTP::UploadIO` (or any IO-like object the
[`multipart-post`](https://rubygems.org/gems/multipart-post) gem
accepts).
```ruby
require "net/http/post/multipart"
client.treasury.import_recipients(
file: UploadIO.new(File.open("recipients.csv"), "text/csv", "recipients.csv")
)
```
## Payout links [#payout-links]
```ruby
client.treasury.list_payout_links(limit: 50)
client.treasury.generate_payout_link(amount: 50_000, recipient_email: "pat@example.com")
client.treasury.retrieve_payout_link("pl_token_…")
client.treasury.submit_payout_link("pl_token_…", payment_method_id: "pm_…")
```
## Recurring payments [#recurring-payments]
```ruby
client.treasury.list_recurring_payments(limit: 50)
client.treasury.create_recurring_payment(
recipient_id: "rec_…", amount: 100_000, interval: "month", interval_count: 1
)
client.treasury.retrieve_recurring_payment("rp_…")
client.treasury.update_recurring_payment("rp_…", amount: 120_000)
client.treasury.delete_recurring_payment("rp_…")
```
## Categories [#categories]
```ruby
client.treasury.list_categories
client.treasury.create_category(name: "Software")
client.treasury.delete_category("cat_…")
client.treasury.category_usage("cat_…")
```
## Security rules [#security-rules]
```ruby
client.treasury.list_security_rules
client.treasury.create_security_rule(rule_type: "max_send_amount", value: 100_000)
client.treasury.update_security_rule("sec_…", value: 250_000)
client.treasury.delete_security_rule("sec_…")
```
## Auto-transfer rules [#auto-transfer-rules]
```ruby
client.treasury.list_auto_transfer_rules
client.treasury.create_auto_transfer_rule(source_id: "ba_a", destination_id: "ba_b", trigger_balance: 500_000)
client.treasury.update_auto_transfer_rule("atr_…", trigger_balance: 1_000_000)
client.treasury.delete_auto_transfer_rule("atr_…")
```
## Approval requests [#approval-requests]
```ruby
client.treasury.create_approval_request(operation: "send", payload: { recipient_id: "rec_…", amount: 100_000 })
client.treasury.resolve_approval_request("ar_…", decision: "approve")
```
## Object shapes [#object-shapes]
Treasury returns rich, varied shapes — bank accounts, transactions,
recipients, etc. all have distinct schemas.
{/* TODO: enumerate the per-resource schemas as the Treasury API reference
stabilises. */}
## Examples [#examples]
### Pay a contractor and confirm [#pay-a-contractor-and-confirm]
```ruby
recipient = client.treasury.create_recipient(
first_name: "Pat", last_name: "Lee", email: "pat@example.com"
)
send = client.treasury.send_payment(amount: 250_000, recipient_id: recipient[:id])
client.treasury.confirm_send(send_id: send[:id])
```
### Recurring monthly retainer [#recurring-monthly-retainer]
```ruby
client.treasury.create_recurring_payment(
recipient_id: "rec_…",
amount: 500_000,
interval: "month",
interval_count: 1,
start_date: "2026-06-01"
)
```
### Bulk-import recipients from a CSV [#bulk-import-recipients-from-a-csv]
```ruby
require "net/http/post/multipart"
File.open("vendors.csv") do |io|
client.treasury.import_recipients(
file: UploadIO.new(io, "text/csv", "vendors.csv")
)
end
```
# Wallet Checkout (/docs/sdks/ruby/resources/wallet-checkout)
Wallet (crypto) checkout is **not** a separate SDK resource — it shares
the `/checkout` endpoint with card/bank checkout, and the API
discriminates on the body shape. See [Checkout](./checkout) for the
method signature.
```ruby
client.checkout.create(
customer_creation: false,
identity_id: customer[:id],
# …wallet-specific source fields…
)
```
## Methods [#methods]
`client.checkout.create(**body)` — see [Checkout](./checkout).
## Object shape [#object-shape]
The wallet variant returns the same checkout response envelope plus the
on-chain transaction reference.
{/* TODO: document the wallet-checkout request body and response fields once the canonical schema for the crypto variant is published. */}
## Examples [#examples]
{/* TODO: add a runnable wallet-checkout snippet once the request shape stabilises. */}
# Webhook Endpoints (/docs/sdks/ruby/resources/webhook-endpoints)
Manages the webhook **endpoints** registered on your account — the URLs
Easy Labs POSTs events to. For verifying inbound deliveries see
[Webhooks](../webhooks); this resource is for endpoint CRUD plus
delivery-history reads.
Accessed via `client.webhooks`.
## Methods [#methods]
### `register(url:, events: ["*"])` [#registerurl-events-]
`POST /webhooks`. Returns the registered endpoint including
**`secret`** — the HMAC signing key. The secret is returned **only on
creation**; capture and store it immediately.
```ruby
endpoint = client.webhooks.register(
url: "https://example.com/webhooks/easy",
events: ["payment.created", "subscription.*"]
)
ENV["EASY_WEBHOOK_SECRET"] = endpoint[:secret] # store this securely
```
### `list` [#list]
`GET /webhooks`.
```ruby
client.webhooks.list
```
### `update(id, **body)` [#updateid-body]
`PATCH /webhooks/:id`. Common updates: `active:`, `events:`, `url:`.
```ruby
client.webhooks.update("wh_…", active: false)
```
### `delete(id)` [#deleteid]
`DELETE /webhooks/:id`.
```ruby
client.webhooks.delete("wh_…")
```
### `list_deliveries(**query)` [#list_deliveriesquery]
`GET /webhooks/deliveries`. Every delivery across all endpoints. Query
params (e.g. `success: false`, `limit: 50`) are passed through.
```ruby
client.webhooks.list_deliveries(success: false, limit: 50)
```
### `list_endpoint_deliveries(endpoint_id, **query)` [#list_endpoint_deliveriesendpoint_id-query]
`GET /webhooks/:id/deliveries`. Deliveries scoped to one endpoint.
```ruby
client.webhooks.list_endpoint_deliveries("wh_…", limit: 50)
```
## Object shape [#object-shape]
Endpoint — `:id`, `:url`, `:events`, `:active`, `:secret` (only on
create), `:created_at`, …
Delivery — `:id`, `:endpoint_id`, `:event_type`, `:status_code`,
`:success`, `:response_body`, `:attempts`, `:delivered_at`, …
{/* TODO: enumerate the full endpoint + delivery schemas. */}
## Examples [#examples]
### Register and persist the secret [#register-and-persist-the-secret]
```ruby
endpoint = client.webhooks.register(url: "https://example.com/webhooks/easy")
SecretStore.write(:easy_webhook_secret, endpoint[:secret])
```
### Triage failed deliveries [#triage-failed-deliveries]
```ruby
failed = client.webhooks.list_deliveries(success: false, limit: 100)[:data] || []
failed.each { |d| WebhookFailureMailer.alert(d).deliver_later }
```
### Disable an endpoint temporarily [#disable-an-endpoint-temporarily]
```ruby
client.webhooks.update("wh_…", active: false)
# …investigate / fix the receiver…
client.webhooks.update("wh_…", active: true)
```
# Get dispute metrics (/docs/reference/api/account-and-operations/analytics/list-disputes)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get revenue recovery analytics (/docs/reference/api/account-and-operations/analytics/list-revenue-recovery)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get revenue breakdown (/docs/reference/api/account-and-operations/analytics/list-revenue)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get settlement summary (/docs/reference/api/account-and-operations/analytics/list-settlements)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get transaction volume analytics (/docs/reference/api/account-and-operations/analytics/list-transactions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Preview checkout branding (/docs/reference/api/account-and-operations/branding/create-checkout-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Preview email branding (/docs/reference/api/account-and-operations/branding/create-email)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Preview invoice branding (/docs/reference/api/account-and-operations/branding/create-invoice)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Preview customer portal branding (/docs/reference/api/account-and-operations/branding/create-portal)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Reset branding to Easy defaults (/docs/reference/api/account-and-operations/branding/create-reset-to-defaults)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Reset branding to Easy defaults (deprecated alias) (/docs/reference/api/account-and-operations/branding/create-reset)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a branding test email (/docs/reference/api/account-and-operations/branding/create-test)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Validate branding color or logo URL (/docs/reference/api/account-and-operations/branding/create-validate-3)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Register custom email sending domain (/docs/reference/api/account-and-operations/branding/create-verify-domain)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Clear invoice branding override (/docs/reference/api/account-and-operations/branding/delete-invoice)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Clear payment link branding override (/docs/reference/api/account-and-operations/branding/delete-payment-link-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get invoice branding override (/docs/reference/api/account-and-operations/branding/get-invoice-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get payment link branding override (/docs/reference/api/account-and-operations/branding/get-payment-link-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get branding audit log (/docs/reference/api/account-and-operations/branding/list-audit)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get company branding (/docs/reference/api/account-and-operations/branding/list-branding)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List available fonts (/docs/reference/api/account-and-operations/branding/list-list)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get domain verification status (/docs/reference/api/account-and-operations/branding/list-verify-domain)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update company branding (/docs/reference/api/account-and-operations/branding/update-branding)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update invoice branding override (/docs/reference/api/account-and-operations/branding/update-invoice)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update payment link branding override (/docs/reference/api/account-and-operations/branding/update-payment-link-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a custom transaction category (/docs/reference/api/account-and-operations/categories/create-categories)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a custom transaction category (/docs/reference/api/account-and-operations/categories/delete-categories)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get usage count for a category (/docs/reference/api/account-and-operations/categories/get-usage)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List transaction categories (/docs/reference/api/account-and-operations/categories/list-categories)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get communication preferences (/docs/reference/api/account-and-operations/comm-prefs/list-comm-prefs)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update communication preferences (/docs/reference/api/account-and-operations/comm-prefs/update-comm-prefs)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new company (/docs/reference/api/account-and-operations/companies/create-companies)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Complete onboarding (/docs/reference/api/account-and-operations/companies/create-onboarding-complete-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Ensure company accounts are fully provisioned (/docs/reference/api/account-and-operations/companies/list-ensure-provisioned)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a custom domain (dashboard) (/docs/reference/api/account-and-operations/custom-domains-dashboard/create-custom-domains)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Verify custom domain (dashboard) (/docs/reference/api/account-and-operations/custom-domains-dashboard/create-verify)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete custom domain (dashboard) (/docs/reference/api/account-and-operations/custom-domains-dashboard/delete-custom-domains)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List custom domains (dashboard) (/docs/reference/api/account-and-operations/custom-domains-dashboard/list-custom-domains)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get dashboard home stats (JWT-only) (/docs/reference/api/account-and-operations/dashboard/list-dashboard-stats)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Upload file content (/docs/reference/api/account-and-operations/files/create-upload)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a file by ID (/docs/reference/api/account-and-operations/files/get-file)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List merchant files (/docs/reference/api/account-and-operations/files/list-files)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Revoke a specific login session (/docs/reference/api/account-and-operations/login-sessions/delete-login-sessions-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Sign out all other sessions (/docs/reference/api/account-and-operations/login-sessions/delete-login-sessions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List active login sessions (/docs/reference/api/account-and-operations/login-sessions/list-login-sessions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get the authenticated merchant (/docs/reference/api/account-and-operations/merchant/list-merchant)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an approval request for a gated transaction (/docs/reference/api/account-and-operations/security-rules/create-approval-requests)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Approve or deny an approval request (/docs/reference/api/account-and-operations/security-rules/create-resolve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a security rule (budget, approval, time restriction) (/docs/reference/api/account-and-operations/security-rules/create-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a security rule (/docs/reference/api/account-and-operations/security-rules/delete-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List security rules (/docs/reference/api/account-and-operations/security-rules/list-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a security rule (/docs/reference/api/account-and-operations/security-rules/update-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Register a webhook endpoint (/docs/reference/api/account-and-operations/webhooks/create-root)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a webhook endpoint (/docs/reference/api/account-and-operations/webhooks/delete-root)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List webhook deliveries (/docs/reference/api/account-and-operations/webhooks/get-deliverie)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List webhook deliveries (/docs/reference/api/account-and-operations/webhooks/list-deliveries)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List webhook endpoints (/docs/reference/api/account-and-operations/webhooks/list-root-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a webhook endpoint (/docs/reference/api/account-and-operations/webhooks/update-root)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create auto-pay schedule for a recipient (/docs/reference/api/billing/auto-pay/create-auto-pay)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create coupon (/docs/reference/api/billing/coupons/create-coupons)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete coupon (/docs/reference/api/billing/coupons/delete-coupons)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get coupon (/docs/reference/api/billing/coupons/get-coupon)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List coupons (/docs/reference/api/billing/coupons/list-coupons)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update coupon (/docs/reference/api/billing/coupons/update-coupons)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Consume customer portal magic link (/docs/reference/api/billing/customer-portal/create-consume)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a one-time handoff code for cross-domain session transfer (/docs/reference/api/billing/customer-portal/create-create)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exchange a handoff code for a session token (single use) (/docs/reference/api/billing/customer-portal/create-exchange)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a payment method from an active customer portal session (/docs/reference/api/billing/customer-portal/create-payment-methods)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Consume a revenue recovery link into a customer portal session (/docs/reference/api/billing/customer-portal/create-recovery)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Request customer portal magic link (/docs/reference/api/billing/customer-portal/create-request-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Sign out customer portal session (/docs/reference/api/billing/customer-portal/create-sign-out)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove a payment method from an active customer portal session (/docs/reference/api/billing/customer-portal/delete-payment-methods)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get active customer portal session (/docs/reference/api/billing/customer-portal/list-me)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get public customer portal context (/docs/reference/api/billing/customer-portal/list-public)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update billing information from an active customer portal session (/docs/reference/api/billing/customer-portal/update-billing-information)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get customer portal config (/docs/reference/api/billing/customer-portal-config/list-customer-portal-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Patch customer portal config (/docs/reference/api/billing/customer-portal-config/update-customer-portal-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new invoice (/docs/reference/api/billing/invoices/create-invoices-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new invoice (/docs/reference/api/billing/invoices/create-invoices)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pay an outstanding invoice (/docs/reference/api/billing/invoices/create-pay-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pay an outstanding invoice (/docs/reference/api/billing/invoices/create-pay)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send invoice reminder (/docs/reference/api/billing/invoices/create-remind-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send invoice reminder (/docs/reference/api/billing/invoices/create-remind)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send an invoice (/docs/reference/api/billing/invoices/create-send-3)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send an invoice (/docs/reference/api/billing/invoices/create-send)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Void an invoice (/docs/reference/api/billing/invoices/create-void-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Void an invoice (/docs/reference/api/billing/invoices/create-void-3)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get invoice by ID (/docs/reference/api/billing/invoices/get-invoice-3)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get invoice by ID (/docs/reference/api/billing/invoices/get-invoice)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get invoice data for PDF (/docs/reference/api/billing/invoices/get-pdf-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get invoice data for PDF (/docs/reference/api/billing/invoices/get-pdf)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# View invoice (public) (/docs/reference/api/billing/invoices/get-view-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# View invoice (public) (/docs/reference/api/billing/invoices/get-view)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Download invoice PDF (public) (/docs/reference/api/billing/invoices/list-download-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Download invoice PDF (public) (/docs/reference/api/billing/invoices/list-download)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List invoices (/docs/reference/api/billing/invoices/list-invoices-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List invoices (/docs/reference/api/billing/invoices/list-invoices)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a draft invoice (/docs/reference/api/billing/invoices/update-invoices-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a draft invoice (/docs/reference/api/billing/invoices/update-invoices)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Clone a product (/docs/reference/api/billing/product/create-clone)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new product price (/docs/reference/api/billing/product/create-product-prices)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new product (/docs/reference/api/billing/product/create-products)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a product (soft-delete) (/docs/reference/api/billing/product/delete-products)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a product price by ID (/docs/reference/api/billing/product/get-product-price)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a product by ID (/docs/reference/api/billing/product/get-product)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all product prices (/docs/reference/api/billing/product/list-product-prices)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all products (/docs/reference/api/billing/product/list-products)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a product (/docs/reference/api/billing/product/update-archive-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Archive a product price (/docs/reference/api/billing/product/update-archive)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a product price (/docs/reference/api/billing/product/update-product-prices)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a product (/docs/reference/api/billing/product/update-products)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Restore an archived product (/docs/reference/api/billing/product/update-restore)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Unarchive a product (atomic) (/docs/reference/api/billing/product/update-unarchive)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get product by product ID and price ID (/docs/reference/api/billing/products/get-price-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all prices for a product (/docs/reference/api/billing/products/get-price)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create promotion code (/docs/reference/api/billing/promotion-codes/create-promotion-codes)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Validate promotion code (/docs/reference/api/billing/promotion-codes/create-validate-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete promotion code (/docs/reference/api/billing/promotion-codes/delete-promotion-codes)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get promotion code (/docs/reference/api/billing/promotion-codes/get-promotion-code)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List promotion codes (/docs/reference/api/billing/promotion-codes/list-promotion-codes)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update promotion code (/docs/reference/api/billing/promotion-codes/update-promotion-codes)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a recurring payment (/docs/reference/api/billing/recurring-payments/create-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a recurring payment (/docs/reference/api/billing/recurring-payments/delete-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a recurring payment by ID (/docs/reference/api/billing/recurring-payments/get-recurring-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List recurring payments (/docs/reference/api/billing/recurring-payments/list-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a recurring payment (pause, resume, cancel, edit) (/docs/reference/api/billing/recurring-payments/update-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a subscription plan (legacy compatibility) (/docs/reference/api/billing/subscription-plan/create-subscription-plans)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a subscription plan by ID (legacy compatibility) (/docs/reference/api/billing/subscription-plan/get-subscription-plan)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get subscription plans (legacy compatibility) (/docs/reference/api/billing/subscription-plan/list-subscription-plans)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a subscription plan (legacy compatibility) (/docs/reference/api/billing/subscription-plan/update-subscription-plans)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Apply discount to subscription (/docs/reference/api/billing/subscriptions/create-discounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create or replace revenue recovery config (/docs/reference/api/billing/subscriptions/create-dunning-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add item to subscription (/docs/reference/api/billing/subscriptions/create-items)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create one-time charge for subscription (/docs/reference/api/billing/subscriptions/create-one-time-charges)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pause a subscription (/docs/reference/api/billing/subscriptions/create-pause)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Resume a subscription (/docs/reference/api/billing/subscriptions/create-resume)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create revenue recovery automation (/docs/reference/api/billing/subscriptions/create-revenue-recovery-automations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create or replace revenue recovery config (/docs/reference/api/billing/subscriptions/create-revenue-recovery-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a subscription (/docs/reference/api/billing/subscriptions/create-subscriptions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Report metered usage (/docs/reference/api/billing/subscriptions/create-usage)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove subscription discount (/docs/reference/api/billing/subscriptions/delete-discounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove subscription item (/docs/reference/api/billing/subscriptions/delete-items)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete revenue recovery automation (/docs/reference/api/billing/subscriptions/delete-revenue-recovery-automations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a subscription (/docs/reference/api/billing/subscriptions/delete-subscriptions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List subscription discounts (/docs/reference/api/billing/subscriptions/get-discount)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Preview proration for subscription updates (/docs/reference/api/billing/subscriptions/get-proration-preview)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List revenue recovery automation runs (/docs/reference/api/billing/subscriptions/get-run)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a subscription (/docs/reference/api/billing/subscriptions/get-subscription-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get revenue recovery config (/docs/reference/api/billing/subscriptions/list-dunning-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Reconcile usage billing for a period (/docs/reference/api/billing/subscriptions/list-reconciliation)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List revenue recovery automations (/docs/reference/api/billing/subscriptions/list-revenue-recovery-automations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get revenue recovery config (/docs/reference/api/billing/subscriptions/list-revenue-recovery-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List subscriptions (/docs/reference/api/billing/subscriptions/list-subscriptions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get usage summary (/docs/reference/api/billing/subscriptions/list-summary)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Patch revenue recovery config (/docs/reference/api/billing/subscriptions/update-dunning-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update subscription item quantity (/docs/reference/api/billing/subscriptions/update-items)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update revenue recovery automation (/docs/reference/api/billing/subscriptions/update-revenue-recovery-automations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Patch revenue recovery config (/docs/reference/api/billing/subscriptions/update-revenue-recovery-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a subscription (/docs/reference/api/billing/subscriptions/update-subscriptions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an authorization (/docs/reference/api/payments/authorization/create-authorizations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Capture an authorization (/docs/reference/api/payments/authorization/create-capture)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Void an authorization (/docs/reference/api/payments/authorization/create-void)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an authorization by ID (/docs/reference/api/payments/authorization/get-authorization)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List authorizations (/docs/reference/api/payments/authorization/list-authorizations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a checkout session (/docs/reference/api/payments/checkout/create-checkout)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get checkout slug (/docs/reference/api/payments/checkout-slug/list-checkout-slug)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update checkout slug (/docs/reference/api/payments/checkout-slug/update-checkout-slug)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new customer (/docs/reference/api/payments/customer/create-customer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a customer by ID (/docs/reference/api/payments/customer/get-customer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all payment instruments for a customer (/docs/reference/api/payments/customer/get-instrument)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all orders for a customer (/docs/reference/api/payments/customer/get-order)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all subscriptions for a customer (/docs/reference/api/payments/customer/get-subscription)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all wallets for a customer (/docs/reference/api/payments/customer/get-wallet)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all customers (/docs/reference/api/payments/customer/list-customer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a customer (/docs/reference/api/payments/customer/update-customer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Accept a dispute (/docs/reference/api/payments/dispute/create-accept)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Upload dispute evidence (/docs/reference/api/payments/dispute/create-evidence)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Submit dispute evidence (/docs/reference/api/payments/dispute/create-submit)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a dispute by ID (/docs/reference/api/payments/dispute/get-dispute)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List dispute evidence files (/docs/reference/api/payments/dispute/get-evidence)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get disputes (/docs/reference/api/payments/dispute/list-disputes-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update dispute tags (/docs/reference/api/payments/dispute/update-disputes)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Confirm payment for an embedded checkout session (/docs/reference/api/payments/embedded-checkout/create-confirm)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an embedded checkout session (/docs/reference/api/payments/embedded-checkout/create-embedded-checkout)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Validate an embedded checkout session (/docs/reference/api/payments/embedded-checkout/create-validate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get crypto payment status for a checkout session (/docs/reference/api/payments/embedded-checkout/get-crypto-statu)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Retrieve an embedded checkout session (/docs/reference/api/payments/embedded-checkout/get-embedded-checkout)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get embedded checkout configuration (/docs/reference/api/payments/embedded-checkout/list-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update embedded checkout configuration (/docs/reference/api/payments/embedded-checkout/update-config)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an order by ID (/docs/reference/api/payments/order/get-order-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get orders (/docs/reference/api/payments/order/list-orders)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update order tags (/docs/reference/api/payments/order/update-orders)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a payment instrument (Basis Theory proxy) (/docs/reference/api/payments/payment-instrument/create-payment-instruments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a payment instrument by ID (/docs/reference/api/payments/payment-instrument/get-payment-instrument)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payment instruments (/docs/reference/api/payments/payment-instrument/list-payment-instruments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a payment instrument (/docs/reference/api/payments/payment-instrument/update-payment-instruments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a payment link (/docs/reference/api/payments/payment-link/create-payment-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Deactivate a payment link (/docs/reference/api/payments/payment-link/delete-payment-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Retrieve a payment link (/docs/reference/api/payments/payment-link/get-payment-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payments for a payment link (/docs/reference/api/payments/payment-link/get-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payment links (/docs/reference/api/payments/payment-link/list-payment-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a payment link (/docs/reference/api/payments/payment-link/update-payment-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a session (/docs/reference/api/payments/sessions/create-sessions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a refund (reversal) (/docs/reference/api/payments/transfer/create-reversals)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a transfer (/docs/reference/api/payments/transfer/create-transfer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a transfer by ID (/docs/reference/api/payments/transfer/get-transfer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get transfers (/docs/reference/api/payments/transfer/list-transfer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update transfer tags (/docs/reference/api/payments/transfer/update-transfer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an auto-transfer rule (/docs/reference/api/treasury/auto-transfer-rules/create-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete an auto-transfer rule (/docs/reference/api/treasury/auto-transfer-rules/delete-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List auto-transfer rules (/docs/reference/api/treasury/auto-transfer-rules/list-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an auto-transfer rule (/docs/reference/api/treasury/auto-transfer-rules/update-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Link a bank account via Plaid (/docs/reference/api/treasury/bank-accounts/create-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Deactivate a linked bank account (/docs/reference/api/treasury/bank-accounts/delete-bank-accounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List linked bank accounts (/docs/reference/api/treasury/bank-accounts/list-bank-accounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Plaid Link token (/docs/reference/api/treasury/bank-accounts/list-link-token)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Generate a shareable payout link (/docs/reference/api/treasury/payout-link/create-generate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Submit banking details via payout link (public) (/docs/reference/api/treasury/payout-link/create-submit-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get payout link details (public) (/docs/reference/api/treasury/payout-link/get-payout-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payout links (/docs/reference/api/treasury/payout-link/list-payout-links)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exchange Plaid public token for recipient bank linking (/docs/reference/api/treasury/recipient-plaid/create-exchange-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Plaid Link token for recipient self-service (/docs/reference/api/treasury/recipient-plaid/create-link-token)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a settlement by ID (/docs/reference/api/treasury/settlement/get-settlement)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get settlements (/docs/reference/api/treasury/settlement/list-settlements-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Close a settlement (/docs/reference/api/treasury/settlement/update-settlements)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Calculate sales tax (/docs/reference/api/treasury/tax/create-calculate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a W-9 request email to a recipient (/docs/reference/api/treasury/tax-documents/create-request-w9)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update tax information for a recipient (/docs/reference/api/treasury/tax-documents/create-tax-info)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get tax information for a recipient (/docs/reference/api/treasury/tax-documents/get-tax-info)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get tax compliance report for all recipients (/docs/reference/api/treasury/tax-documents/list-tax-report)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Accept recipient invitation (public) (/docs/reference/api/treasury/treasury/create-accept-invite)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an approval request for a gated transaction (/docs/reference/api/treasury/treasury/create-approval-requests)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create auto-pay schedule for a recipient (/docs/reference/api/treasury/treasury/create-auto-pay)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create an auto-transfer rule (/docs/reference/api/treasury/treasury/create-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Pull funds from a linked bank account (ACH) (/docs/reference/api/treasury/treasury/create-bank-pull)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a pending send operation (/docs/reference/api/treasury/treasury/create-cancel)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a custom transaction category (/docs/reference/api/treasury/treasury/create-categories)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Confirm a send operation (with optional 2FA code) (/docs/reference/api/treasury/treasury/create-confirm-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Confirm an external transfer (with optional 2FA code) (/docs/reference/api/treasury/treasury/create-confirm-3)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Confirm a withdrawal (with optional 2FA code) (/docs/reference/api/treasury/treasury/create-confirm-4)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Exchange Plaid public token for recipient bank linking (/docs/reference/api/treasury/treasury/create-exchange-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Generate a shareable payout link (/docs/reference/api/treasury/treasury/create-generate)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk import recipients from CSV (/docs/reference/api/treasury/treasury/create-import)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send invitation email to a recipient (/docs/reference/api/treasury/treasury/create-invite)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Plaid Link token for recipient self-service (/docs/reference/api/treasury/treasury/create-link-token)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Link a bank account via Plaid (/docs/reference/api/treasury/treasury/create-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add a payment method to a recipient (/docs/reference/api/treasury/treasury/create-payment-methods-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a recipient with payment methods (/docs/reference/api/treasury/treasury/create-recipients)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a recurring payment (/docs/reference/api/treasury/treasury/create-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Send a W-9 request email to a recipient (/docs/reference/api/treasury/treasury/create-request-w9)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Approve or deny an approval request (/docs/reference/api/treasury/treasury/create-resolve)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a security rule (budget, approval, time restriction) (/docs/reference/api/treasury/treasury/create-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Initiate a send money operation (/docs/reference/api/treasury/treasury/create-send-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Submit banking details via payout link (public) (/docs/reference/api/treasury/treasury/create-submit-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update tax information for a recipient (/docs/reference/api/treasury/treasury/create-tax-info)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Initiate a fund transfer (internal or external) (/docs/reference/api/treasury/treasury/create-transfer-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Initiate a withdrawal to an external bank (/docs/reference/api/treasury/treasury/create-withdraw)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete an auto-transfer rule (/docs/reference/api/treasury/treasury/delete-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Deactivate a linked bank account (/docs/reference/api/treasury/treasury/delete-bank-accounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a custom transaction category (/docs/reference/api/treasury/treasury/delete-categories)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a recipient (/docs/reference/api/treasury/treasury/delete-recipients)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Cancel a recurring payment (/docs/reference/api/treasury/treasury/delete-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a security rule (/docs/reference/api/treasury/treasury/delete-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get payout link details (public) (/docs/reference/api/treasury/treasury/get-payout-link)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a recipient by ID (/docs/reference/api/treasury/treasury/get-recipient)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a recurring payment by ID (/docs/reference/api/treasury/treasury/get-recurring-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get settlement/fees breakdown for a transaction (/docs/reference/api/treasury/treasury/get-settlement-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get tax information for a recipient (/docs/reference/api/treasury/treasury/get-tax-info)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get full transaction detail with settlement breakdown (/docs/reference/api/treasury/treasury/get-transaction)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get usage count for a category (/docs/reference/api/treasury/treasury/get-usage)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List auto-transfer rules (/docs/reference/api/treasury/treasury/list-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List linked bank accounts (/docs/reference/api/treasury/treasury/list-bank-accounts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List transaction categories (/docs/reference/api/treasury/treasury/list-categories)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export transactions as CSV (/docs/reference/api/treasury/treasury/list-export)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List recipient invitations (/docs/reference/api/treasury/treasury/list-invitations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Plaid Link token (/docs/reference/api/treasury/treasury/list-link-token)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List payout links (/docs/reference/api/treasury/treasury/list-payout-links)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List recipients (/docs/reference/api/treasury/treasury/list-recipients)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List recurring payments (/docs/reference/api/treasury/treasury/list-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List security rules (/docs/reference/api/treasury/treasury/list-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get treasury dashboard summary (/docs/reference/api/treasury/treasury/list-summary-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get tax compliance report for all recipients (/docs/reference/api/treasury/treasury/list-tax-report)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List treasury transactions with cursor-based pagination (/docs/reference/api/treasury/treasury/list-transactions-2)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get wire transfer instructions for an Easy account (/docs/reference/api/treasury/treasury/list-wire-instructions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an auto-transfer rule (/docs/reference/api/treasury/treasury/update-auto-transfer-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a recipient (/docs/reference/api/treasury/treasury/update-recipients)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a recurring payment (pause, resume, cancel, edit) (/docs/reference/api/treasury/treasury/update-recurring-payments)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a security rule (/docs/reference/api/treasury/treasury/update-security-rules)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update transaction metadata (category, receipt) (/docs/reference/api/treasury/treasury/update-transactions)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}