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 hereWho is charged where is decided by one rule:
| Customer | Charged on |
|---|---|
| A client of a firm that has connected an account | The firm's account |
| Everyone else, including the firm itself | Your 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
connectagain 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 yourrefreshUrlpage should do. paymentAccount(firmId)reportsready— whether Stripe will accept a charge on it now — and what Stripe still asks for (requirementsDue,currentDeadline). It is kept current by Stripe'saccount.updated; callrefresh(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_disabledand.verifiedfire for firms' accounts too, withorganizationIdset to the firm (null for your environment's own account).payment.recordedandpayment.failedcarryresellerOrganizationIdwhen the payment was taken on a firm's account.
API
| Method and path | What it does |
|---|---|
GET /v1/reseller, PUT /v1/reseller | Read or change reseller mode |
GET /v1/reseller/accounts | Every firm's account, with its client count |
GET /v1/organizations/:id/payment-account | A firm's account and whether it is ready |
POST /v1/organizations/:id/payment-account | Connect (or resume onboarding) and get the URL |
POST /v1/organizations/:id/payment-account/onboarding-link | A fresh onboarding URL |
POST /v1/organizations/:id/payment-account/refresh | Pull the account state from Stripe |
POST /v1/organizations/:id/payment-account/session | An 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:
| Retail | Wholesale | |
|---|---|---|
| Rated against | the client's plan from the firm | the firm's plan from you |
| Usage | the client's events only | every client's and the firm's (the pool) |
| Credits drawn | packs the client bought from the firm | credits the firm holds |
| Invoice | the client's, on the firm's account | the 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 path | What it does |
|---|---|
GET /v1/reseller/catalogs | Every firm's plans and packs, read-only |
GET, POST /v1/organizations/:firm/client-plans | A firm's client plans |
PATCH /v1/organizations/:firm/client-plans/:planId | Rename, or publish new terms |
POST /v1/organizations/:firm/client-plans/:planId/archive | Take off sale |
GET, POST /v1/organizations/:firm/client-packs | A firm's client credit packs |
PATCH /v1/organizations/:firm/client-packs/:packId, …/archive | Change or take off sale |
GET, PATCH /v1/organizations/:firm/reseller-settings | Failed-payment setting |
GET /v1/organizations/:firm/margins | Per-client margins |
PUT /v1/organizations/:client/client-plan | Assign a firm plan to a client |
POST /v1/organizations/:client/client-checkout | Hosted checkout for a client |
POST /v1/organizations/:client/client-portal | Billing-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).

