Base 3:16
Guides

Reseller mode

Let the firms on your product charge their own clients on their own Stripe account, while you keep billing each firm wholesale on yours.

Your product has firms (parent organizations) that manage clients (their child organizations — see Managed tenants). An accounting firm runs the books for twenty businesses; an agency runs fifty brands. Without reseller mode you bill the firm, and the firm bills its clients however it likes, outside your product.

With reseller mode the firm is the merchant of record for its clients:

  • The firm connects its own Stripe account, through your product.
  • Every charge to one of the firm's clients — invoices, saved cards, pay links, credit packs, auto-recharge — is made on the firm's account. The money lands in the firm's Stripe balance, and disputes are the firm's.
  • You keep billing the firm wholesale, on your own account, as today. You take no application fee on the firm's client payments. Base316's own processing fee, where your Base316 plan has one, applies to them as it does to your own charges.

How the accounts fit together

Your environment's account is a Standard connected account of Base316's Stripe platform. Stripe does not let a connected account have connected accounts of its own, so a firm's account cannot sit under yours. It sits beside it: another Standard connected account of the same platform, owned by the firm.

Base316 platform
├── your environment's account      ← the firm pays you here (wholesale)
├── Ledger & Co's account           ← Ledger & Co's clients pay Ledger & Co here
└── Hartley Partners' account       ← Hartley's clients pay Hartley here

Who is charged where is decided by one rule:

CustomerCharged on
A client of a firm that has connected an accountThe firm's account
Everyone else, including the firm itselfYour environment's account

A client whose firm has connected is never charged on your account, even while the firm's onboarding is unfinished or Stripe has restricted it: the charge is refused (NO_PAYMENT_ACCOUNT) until the firm's account can take it. A card or Stripe customer the client had on your account before its firm connected is not used on the firm's; the client adds a card again.

Turn it on

In the dashboard: Settings → Payments & failed payments → Reseller mode. Or with the SDK:

await base316.reseller.update({
	enabled: true,
	// Optional: only firms whose plan grants this BOOLEAN feature may connect.
	featureKey: "resale",
});

Turning it off stops new firms connecting. A firm already connected keeps its account and keeps charging its clients on it, because their Stripe customers and cards live there.

Connect a firm

Onboarding is done by a person at the firm, on Stripe's hosted page, and they come back to your application. Build a "Connect Stripe" button in your firm settings that calls your server, which calls:

const { onboardingUrl, account } = await base316.reseller.connect(firmOrganizationId, {
	// Your own pages. Both must be allowed redirect URLs for the environment
	// (Authentication → Redirect URLs).
	returnUrl: "https://app.example.com/firm/settings/payments?stripe=return",
	refreshUrl: "https://app.example.com/firm/settings/payments?stripe=refresh",
	country: "US",
	email: "owner@ledger.example",
});

// Send the firm's person to `onboardingUrl`.
  • Only a parent organization can connect; a client is refused (409).
  • Calling connect again for a firm already connected returns a fresh link for the same account (200) rather than creating a second one (201). Stripe's links are single-use and expire; onboardingLink(firmId, { returnUrl, refreshUrl }) mints another, which is what your refreshUrl page should do.
  • paymentAccount(firmId) reports ready — whether Stripe will accept a charge on it now — and what Stripe still asks for (requirementsDue, currentDeadline). It is kept current by Stripe's account.updated; call refresh(firmId) if a webhook was missed.

Show the firm its Stripe account

Mount Stripe's Connect embedded components (the requirements banner, payments, payouts) in your firm settings with an Account Session for the firm's account:

// On your server, when the components mount:
const { clientSecret, publishableKey } = await base316.reseller.accountSession(firmOrganizationId);

Verification stays entirely on Stripe's side: the hosted onboarding and the banner component satisfy every requirement Stripe can raise.

Webhooks

  • payment_account.requirements_due, .charges_disabled and .verified fire for firms' accounts too, with organizationId set to the firm (null for your environment's own account).
  • payment.recorded and payment.failed carry resellerOrganizationId when the payment was taken on a firm's account.

API

Method and pathWhat it does
GET /v1/reseller, PUT /v1/resellerRead or change reseller mode
GET /v1/reseller/accountsEvery firm's account, with its client count
GET /v1/organizations/:id/payment-accountA firm's account and whether it is ready
POST /v1/organizations/:id/payment-accountConnect (or resume onboarding) and get the URL
POST /v1/organizations/:id/payment-account/onboarding-linkA fresh onboarding URL
POST /v1/organizations/:id/payment-account/refreshPull the account state from Stripe
POST /v1/organizations/:id/payment-account/sessionAn Account Session for Stripe's embedded components

All of them need a secret key: an account's requirements name the verification a business still owes Stripe.

Sell to clients: the firm's own catalog

Once connected, a firm can sell its clients plans and credit packs of its own, priced in its account's currency and charged on its account. Your application builds the screens; the firm never sees Base316.

const plan = await base316.reseller.clientPlans.create(firmOrganizationId, {
	name: "Bookkeeping Plus",
	interval: "MONTH",
	entitlements: [{ featureKey: "ai_credits", limit: 500 }, { featureKey: "advanced_reports" }],
	prices: [
		{ model: "FLAT", amount: "79" }, // the monthly fee
		{ model: "PER_UNIT", featureKey: "ai_credits", amount: "0.05" }, // overage; omit for none
	],
});

await base316.reseller.clientPacks.create(firmOrganizationId, {
	name: "1,000 AI credits",
	featureKey: "ai_credits",
	units: 1000,
	amount: "25.00",
});

A firm sells only what it has. A client plan may include only features the firm's own plan grants, and no more of a metered or quantity feature than the firm's plan allows — unlimited only where the firm's is. A refusal names every offending feature (LIMIT_EXCEEDS_FIRM_PLAN, FEATURE_NOT_IN_FIRM_PLAN). New terms (clientPlans.update(firmId, planId, { terms })) publish a new version; clients already on the plan keep theirs.

A firm's plans and packs are its clients' only: they never appear in your own plan list, pricing table or checkout, another firm's client cannot buy them, and a client on one can move only between its firm's plans. Your own plans and packs stay refused to a firm's client (BILLED_BY_PARENT): its firm sells to it.

Put a client on a plan

// Hosted checkout, in the firm's brand, the card saved on the firm's account:
const { url } = await base316.reseller.clientCheckout(clientOrganizationId, {
	planId: plan.id,
	returnUrl: "https://app.example.com/billing?done=1",
});

// Or assign it directly (the firm invoices the client, or has its card):
await base316.reseller.assignPlan(clientOrganizationId, { planId: plan.id });

// The client's own billing page — its plan, the firm's packs, cards, invoices:
const portal = await base316.reseller.clientPortal(clientOrganizationId, { returnUrl });

A client on one of its firm's plans is then a payer in its own right on the firm's account — subscription, invoices, cards and credits — while its firm keeps being billed wholesale for it exactly as before: its usage still counts in the firm's pool, and the per-client add-on still counts it.

What a client may use: the smaller of two plans

A client on one of its firm's plans is decided twice on every check(): against its own plan (its allowance, then the packs it bought from the firm, then overage where the firm's plan for it prices it), and against the firm's plan (the shared pool, as before). It may go ahead only when both say yes.

const decision = await base316.check({ customerId: clientCustomerId, feature: "ai_credits" });

if (!decision.allowed && decision.limitedBy === "client") {
	// The client's own plan is used up: offer the firm's packs or an upgrade.
}
if (!decision.allowed && decision.limitedBy === "firm") {
	// The firm's pool is out: only the firm can fix it.
}

usage is the client's own snapshot, with remaining capped at what the firm has left; firmUsage is the firm's. Token claims (entitlementSnapshot) carry the same smaller answer. A client of a firm that sells it no plan is decided by the firm's plan alone, as before.

Two prices for every usage event

Every event a client sends is stored once — against the firm, with the client as its source — and rated twice:

RetailWholesale
Rated againstthe client's plan from the firmthe firm's plan from you
Usagethe client's events onlyevery client's and the firm's (the pool)
Credits drawnpacks the client bought from the firmcredits the firm holds
Invoicethe client's, on the firm's accountthe firm's, on yours

The two ledgers never meet: a pack a client buys never raises the firm's pool, and the firm's credits never cover a client's retail overage.

A MATRIX price — a unit price per AI model or region — rates the same way on each side: a firm's client plan may use one ({ model: "MATRIX", featureKey, rows: [{ dimensions: { model: "opus" }, unitAmount: "0.10" }] }), and its rows are applied to the client's own events, while your plan's rows are applied to the firm's whole pool.

Margins

const margins = await base316.reseller.margins(firmOrganizationId);
// margins.clients: [{ name, plan, retail, wholesale: { usage, clientFee, total }, margin }]

Per client, what the firm charges it against what you charge the firm for it. basis: "current" (the default) rates this period as the next invoices will; basis: "invoiced" with from/to totals invoices issued in the window. A client's wholesale share is its part of the firm's usage charges, by usage, plus an even share of the per-client add-on. Before tax and discounts.

Branding and failed payments

A client's invoices, credit notes, pay page, hosted checkout, billing portal and dunning emails carry the firm's tenant branding (see Managed tenants), because the firm is the merchant.

When a client's payment to its firm fails, the firm decides what happens:

await base316.reseller.updateSettings(firmOrganizationId, {
	clientPaymentFailure: "pause_ai", // or "pause_client", or "none" (the default)
});

While the client's subscription is past due, its check() carries clientPaymentEffect: "pause_ai" | "pause_client". Base316 does not enforce it — only your application knows which features are AI — so act on it there. payment.recorded and payment.failed for a client's charge carry resellerOrganizationId, clientOrganizationId and customerId.

Refunds and tax stay on the firm's account. A credit note that refunds a client's card payment sends the refund through the account the payment was taken on — the firm's — and the money comes out of the firm's Stripe balance. With Stripe Tax on, a client's invoices are taxed on the firm's account, under the firm's own registrations, because the firm is the seller: the firm needs Stripe Tax set up in its own Stripe dashboard, or its clients' invoices stay drafts with the reason until it is.

Fees

You take no cut of a firm's client payments; you bill the firm wholesale. What Base316 itself takes from a charge on a firm's account is shown on GET /v1/reseller as applicationFeeMode: inherit (your plan's Base316 fee, as on your own payments — the default), exempt, or custom. Base316 sets it.

API

Method and pathWhat it does
GET /v1/reseller/catalogsEvery firm's plans and packs, read-only
GET, POST /v1/organizations/:firm/client-plansA firm's client plans
PATCH /v1/organizations/:firm/client-plans/:planIdRename, or publish new terms
POST /v1/organizations/:firm/client-plans/:planId/archiveTake off sale
GET, POST /v1/organizations/:firm/client-packsA firm's client credit packs
PATCH /v1/organizations/:firm/client-packs/:packId, …/archiveChange or take off sale
GET, PATCH /v1/organizations/:firm/reseller-settingsFailed-payment setting
GET /v1/organizations/:firm/marginsPer-client margins
PUT /v1/organizations/:client/client-planAssign a firm plan to a client
POST /v1/organizations/:client/client-checkoutHosted checkout for a client
POST /v1/organizations/:client/client-portalBilling-portal link for a client

All need a secret key.

Not yet

  • Auto-recharge for a firm's clients (they buy the firm's packs by hand).
  • Links on the firm's own verified domain: the checkout and portal URLs are on Base316's host, in the firm's brand.
  • Connecting a firm's existing Stripe account (OAuth) rather than creating a new one.
  • Environments that bring their own Stripe credential: reseller mode is refused there (OWN_PROCESSOR).

On this page