]` | Forward this environment's events to a local URL, as a webhook would |
A customer can be named by Base316 id (`cus_…`) or by your own id — the prefix
decides which.
## Options [#options]
| | |
| -------- | ------------------------------------------------ |
| `--json` | Print the raw API response, for piping into `jq` |
| `--live` | Allow a write against a live key |
| `--help` | The command list |
## Writes are guarded on a live key [#writes-are-guarded-on-a-live-key]
Every command marked as a write is refused on a **live** key unless `--live` is
passed, before any request is made:
```
$ base316 usage cus_abc123 api_call
Refused: `usage` would change live data.
This key is a live key. Re-run with --live if you mean it, or export a sandbox
key instead.
```
A CLI is run from shell history and from scripts, and the gap between seeding a
sandbox and metering ten thousand events against production is one environment
variable somebody forgot to change. The band is read from the key's own prefix,
so the check cannot fail open when the network does.
## Environment [#environment]
| | |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `BASE316_SECRET_KEY` | A secret key (`b316_sk_…`), or read from `.env.local` / `.env`. A publishable key is refused — it cannot read customers or invoices. |
| `BASE316_API_URL` | Defaults to `https://api.base316.com`. Point it at `http://localhost:3110` for a local stack. |
| `BASE316_APP_URL` | The dashboard `login` and `init` talk to. Defaults to `https://app.base316.com`. |
| `BASE316_CONFIG_DIR` | Where `login` keeps its session. Defaults to `~/.config/base316`. |
## Debugging an entitlement [#debugging-an-entitlement]
The command worth knowing about:
```
$ base316 check cus_abc123 api_calls
DENIED
Reason USAGE_LIMIT_EXCEEDED
Limit 1000
Used 1000
Remaining 0
```
A bare "false" sends you to the dashboard. A reason sends you to the fix.
# Components
`@base316/react` renders Base316's screens inside your own pages rather than on
a page your users are sent away to.
```bash
npm install @base316/react @base316/sdk
```
## Wrap your app once [#wrap-your-app-once]
```tsx
import { Base316Provider } from "@base316/react";
export default function App({ children }) {
return (
{children}
);
}
```
The provider holds one client and the signed-in person's session, and loads
the branding you set in the dashboard — so the components look like your
hosted pages without configuring your brand twice.
## What there is [#what-there-is]
| Area | Components |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Signing in | `SignIn` · `SignUp` · `ForgotPassword` · `ResetPassword` · `VerifyEmail` · `DeviceApproval` |
| The account | `UserProfile` · `PasskeyManager` · `TwoFactorManager` · `ActiveSessions` · `PrivacyControls` |
| Teams | `TeamMembers` · `AcceptInvitation` |
| Billing | `BillingPortal` · `PricingTable` · `PlanChangeReview` · `SubscriptionManager` · `UpcomingInvoice` · `SeatManager` · `PaymentMethod` · `Invoices` · `PayInvoice` · `usePlans` |
| Your data | `DataTable` · `Kanban` · `Chart` · `RecordDetail` · `RecordForm` · `RelationPicker` |
| Brand | `Branding` |
| For your own | `useBase316` · `useSession` · `useCollection` |
Every component and prop is also described in machine-readable form —
`MANIFEST`, exported from the package — which is what an agent building your
screens reads to choose between them.
## White-label for a tenant [#white-label-for-a-tenant]
When your product is run by one business on behalf of others — a firm and its
client companies, an agency and its brands — the screens a client sees should
carry the firm's brand, not yours. Give the provider the tenant and every
component under it follows:
```tsx
```
* Name the tenant by Base316's id (`organizationId`) or your own
(`organizationExternalId`).
* Its branding is resolved field by field: the tenant's own, then its parent's,
then your environment's. So a firm's clients show the firm's brand with
nothing set on each client — and a client that sets its own colour keeps the
firm's name and logo.
* Change `tenant` when the person switches team and the components restyle
without a reload.
* A tenant id that does not exist in the environment falls back to your own
branding rather than failing, so the page never renders unbranded.
The same lookup is available without React: `billing.tenantBranding({
organizationId })` on `Base316Browser`, or `GET /v1/public/branding?organizationId=…`
with a publishable key.
Branding is set per tenant in the dashboard (**Branding** on the tenant's row)
or with `organizations.setBranding` — see
[Managed tenants](/guides/managed-tenants#the-firms-brand-on-the-firms-domain).
### A whole billing page [#a-whole-billing-page]
`BillingPortal` is the billing screen in one component: plan and status, the
next invoice, a scheduled change with its undo, a plan picker whose choice opens
`PlanChangeReview` (gained and lost features, what it costs now or when it
waits), seats on a per-seat plan, the card on file (`PaymentMethod`, which
replaces it on a Stripe-hosted page), invoices with PDF download and **Pay**, and
cancel and resume. Pass `billingDetails` to put your own address or tax-ID form
in its Billing details section. The same page is also hosted by Base316 for apps
that do not use React — see [Billing portal](/guides/billing-portal).
### A downgrade scheduled for the renewal [#a-downgrade-scheduled-for-the-renewal]
Picking a cheaper plan in `PricingTable` schedules it for the end of the period
rather than switching at once: the current plan stays marked current, and the
table says when the new one starts. `usePlans().selectPlan` returns
`{ status: "scheduled", effectiveAt }` for it. `SubscriptionManager` then shows
"Moves to Basic on 1 Nov" with a **Keep Pro** button that withdraws the change.
### Billing screens for a firm's clients [#billing-screens-for-a-firms-clients]
When the firm pays for a client, `SubscriptionManager` shows the firm's plan
with "Provided by Hartley & Co" and no cancel button, and `PricingTable` names
the firm above the plans and disables every button — a plan bought there would
grant the client nothing. `usePlans()` exposes the same as `managedBy`.
### Teams reached through a firm [#teams-reached-through-a-firm]
A firm's staff reach its clients' teams without being members of them. In
`TeamMembers`, such a team says where the access comes from — "You have access
through Hartley & Co" — and offers nothing to leave, because there is no
membership to leave: the access ends when the firm's role mapping or the
client's link to the firm does.
### What a manager can do to other roles [#what-a-manager-can-do-to-other-roles]
`team.manage` decides who may invite and remove people at all. What they may
do to somebody else's role is bounded by their own: a member can invite people
only to a role whose permissions they hold every one of, and can remove only a
member whose role is within theirs. An admin cannot invite an owner, withdraw
the owner's invitation, or remove the owner; an owner can do all three to
another owner. `TeamMembers` invites to the environment's default role, so if
that is a role holding `*`, only a member holding `*` can invite from it — call
`teams.invite` with a `roleId` for anybody else.
Leaving is always allowed, with one exception: a member holding `team.manage`
cannot leave while nobody remaining holds a role covering theirs. That is the
last owner — they give the role to somebody first. Your server, with a secret
key, is not bounded by either rule.
The member list carries addresses only for somebody holding `team.manage`;
everybody else sees names and roles, with `email` null.
## Styling [#styling]
Three ways, from least to most invasive:
1. **The dashboard.** Brand colour, background and logo arrive automatically —
your environment's, or the tenant's when the provider is given one.
2. **`appearance` tokens** on the provider: `{ colorPrimary, borderRadius, … }`.
These win over the dashboard's values.
3. **Your own CSS**, against the `b316-*` class names or a `className` on any
component. Every colour, radius and font is a `--b316-*` custom property.
## Reading the session [#reading-the-session]
```tsx
import { useSession } from "@base316/react";
function Greeting() {
const { session, loading } = useSession();
if (loading) return null;
return session ? Hello {session.user.email}
: Sign in;
}
```
# MCP server
A Model Context Protocol server for Base316. It lets an agent read your catalog,
ask whether a customer may use a feature, and record that they did.
Everything goes through the public REST API with your own secret key, so the
server has no privileged access of its own — it can do exactly what that key
can do and nothing more.
## Configure it [#configure-it]
Claude Code, in a project where `npx @base316/cli init` has run:
```bash
claude mcp add base316 -- npx -y @base316/mcp
```
The server starts in the project directory and reads `BASE316_SECRET_KEY` and
`BASE316_API_URL` from its `.env.local` or `.env`, so no key goes into the
client's configuration. See [Build with an agent](/getting-started/agents).
Anywhere else, pass the key explicitly:
```bash
claude mcp add base316 --env BASE316_SECRET_KEY=b316_sk_sandbox_... -- npx -y @base316/mcp
```
Claude Desktop (`claude_desktop_config.json`) and Cursor (`~/.cursor/mcp.json`,
or `.cursor/mcp.json` in a project) take the same block:
```json
{
"mcpServers": {
"base316": {
"command": "npx",
"args": ["-y", "@base316/mcp"],
"env": { "BASE316_SECRET_KEY": "b316_sk_sandbox_..." }
}
}
}
```
| Variable | Required | Meaning |
| -------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BASE316_SECRET_KEY` | yes | A secret key (`b316_sk_…`), set here or in the project's `.env.local` / `.env`. A publishable key is refused: it cannot read customers or invoices. |
| `BASE316_API_URL` | no | Defaults to `https://api.base316.com`. Point it at `http://localhost:3110` to work against a local stack. |
| `BASE316_MCP_ALLOW_WRITES` | no | Set to exactly `true` to allow writes on a **live** key. |
## Writes are gated on the key [#writes-are-gated-on-the-key]
Reads are always available. Anything that changes something — `record_event`,
`create_customer`, `create_subscription`, and any write reached through
`call_operation` — can put charges on a real customer's invoice, so on a **live**
key it is refused unless `BASE316_MCP_ALLOW_WRITES=true` is set deliberately.
Sandbox keys write freely, which is what a sandbox is for.
A disabled tool says so in its own description, so a model can plan around it
rather than discovering the refusal by calling it.
## Tools [#tools]
**Reads**
| Tool | What it answers |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `whoami` | Which project and environment this key belongs to, and whether it is sandbox or live |
| `list_products` | The products this environment sells |
| `list_plans` | Plans and versions, with prices and the features each grants |
| `list_features` | The features gated on, and the meter behind each metered one |
| `list_customers` / `get_customer` | Accounts, with currency, terms and collection method |
| `list_subscriptions` | Plan, status and current billing period |
| `list_invoices` | Invoices, including what is still owed rather than only the total |
| `preview_upcoming_invoice` | What a subscription will be charged next, and when — computed the way the billing run will, with nothing stored |
| `preview_quantity_change` | What changing a subscription's quantity would charge now and each period after, and when it applies — nothing is changed |
| `preview_plan_change` | What moving a subscription to another plan would do now and at the period end: entitlements, proration and the next renewal |
| `check_entitlement` | **The core one.** May this customer use this feature, and how much allowance is left — counting plan limits and prepaid credits |
| `get_customer_entitlements` | Every feature for one customer, allowed or denied and why, with the plan version, credit grants and paying account behind each |
**Writes**
| Tool | What it does |
| --------------------- | ------------------------------------------------------------------------------------------------- |
| `record_event` | Records one usage event. Requires an `idempotencyKey`, so a retry after a timeout records it once |
| `create_customer` | Opens a billable account |
| `create_subscription` | Starts billing |
**Everything else**
The tools above are the hot path. Behind them, three more reach the rest of the
API — every endpoint this deployment serves, not a curated subset.
| Tool | What it does |
| -------------------- | --------------------------------------------------------------------------- |
| `list_operations` | Search everything the API can do. Filter by `search`, `tag` or `writesOnly` |
| `describe_operation` | One operation's parameters and JSON body schema, with its real constraints |
| `call_operation` | Call it, by `operationId`, with `pathParams`, `query` and `body` |
They are driven by [the OpenAPI document](https://api.base316.com/openapi.json),
fetched from whichever deployment your key points at — so the reachable surface
is the one actually running, not whatever was true when this package was
published.
**Why not one tool per endpoint?** That would be about two hundred tools. A model choosing between
two hundred tools chooses worse than one choosing between fifteen, and the list is context you pay
for on every turn. Discovery costs one call and keeps the choice small.
`call_operation` obeys the same gate as everything else, decided per call from
the operation you name: reads go through on a live key, writes do not. An
`operationId` it cannot identify is treated as a write, because guessing
"harmless" for something unrecognised is the wrong way round.
## Working against a local stack [#working-against-a-local-stack]
```bash
BASE316_API_URL=http://localhost:3110 \
BASE316_SECRET_KEY=b316_sk_sandbox_... \
pnpm --filter @base316/mcp start
```
Diagnostics go to stderr; stdout carries the protocol.
# SDKs
Base316 has a client for seven languages. Each one depends on nothing beyond
its standard library — the TypeScript one uses global `fetch`, the .NET one the
BCL — so adding Base316 never changes your dependency graph.
| Language | Where |
| ----------------------- | ------------------------------------------- |
| TypeScript / JavaScript | `npm install @base316/sdk` |
| Python 3.9+ | `sdks/python` — the `base316` package |
| Go 1.22+ | `sdks/go` — `github.com/base316/base316-go` |
| Ruby 2.6+ | `sdks/ruby` — `lib/base316.rb` |
| Java 17+ | `sdks/java` — `com.base316` |
| C# / .NET 8+ | `sdks/csharp` — the `Base316` project |
| PHP 8.1+ | `sdks/php` — `base316/sdk` |
Only the TypeScript SDK is published to a registry today. The other six are single-file clients in
the Base316 repository: copy the source into your project, or vendor the directory. Each one is a
handful of files with no dependencies, which is what makes that practical.
The TypeScript package covers the whole API — every endpoint has a method — and
includes `Base316Browser`, the client for a publishable key in a browser. The
other six cover what an application's server actually touches, listed below;
anything beyond that is one HTTPS call with your secret key.
For drop-in screens rather than calls, see [Components](/tools/components).
## What every SDK covers [#what-every-sdk-covers]
| Area | What |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Keys | Parse a key and refuse one that cannot do the job, at construction |
| Sign-in | Build the hosted sign-in URL; exchange the one-time code for a session |
| Sessions | Resolve and revoke |
| Access | `checkAccess` — entitlement, permission and relationship in one call |
| Usage | `recordEvent`, with a required idempotency key |
| Customers | Create, read, list; the accounts a customer pays for; every feature evaluated (`customers.entitlements` · `get_customer_entitlements` · `GetCustomerEntitlements` · `getCustomerEntitlements` · `GetCustomerEntitlementsAsync`) |
| Subscriptions | Create, with an optional `trialDays` for a no-card trial; preview the next invoice (`subscriptions.upcomingInvoice` · `get_upcoming_invoice` · `GetUpcomingInvoice` · `getUpcomingInvoice` · `GetUpcomingInvoiceAsync`); change the quantity, prorated, after previewing it (`previewQuantity` / `setQuantity` · `preview_quantity_change` / `change_subscription_quantity` · `PreviewQuantityChange` / `ChangeSubscriptionQuantity`); preview, make or schedule a plan change and undo a scheduled one (`previewPlanChange` · `changePlan` · `cancelScheduledChange`, `preview_plan_change` · `change_plan` · `cancel_scheduled_change` in Python and Ruby). Ramps: `subscriptions.setPhases` in TypeScript |
| Retention | The retention offer and its report; start a cancellation, then accept the offer or confirm (`retention.start` / `accept` / `confirm` · `start_cancellation` / `accept_retention_offer` / `confirm_cancellation` · `StartCancellation` / `AcceptRetentionOffer` / `ConfirmCancellation` · `startCancellation` · `StartCancellationAsync`). See [Retention offers](/guides/retention-offers) |
| Organizations | List, create (`parentId` for a managed tenant), children, detach, members |
| Managed tenants | Read and replace the inherited-role mapping |
| White-label | Read and set a tenant's branding; claim, verify and remove its sign-in domain |
| Webhooks | Verify a signature in constant time, with a timestamp tolerance; parse the event |
## Managed tenants, by language [#managed-tenants-by-language]
The same calls, named the way each language names things. TypeScript groups
them under `organizations` and `customers`.
| Call | TypeScript | Python / Ruby | Go | Java · PHP · C# |
| ----------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------- | ------------------------------------------------ |
| Create a client tenant | `organizations.create({ parentId })` | `create_organization(parent_id=)` | `CreateOrganization` | `createOrganization` · `CreateOrganizationAsync` |
| List a parent's clients | `organizations.children(id)` | `list_child_organizations` | `ListChildOrganizations` | `listChildOrganizations` |
| Detach | `organizations.detach(id)` | `detach_organization` | `DetachOrganization` | `detachOrganization` |
| Read the role mapping | `organizations.inheritedRoles(id)` | `get_inherited_roles` | `GetInheritedRoles` | `getInheritedRoles` |
| Replace it | `organizations.setInheritedRoles(id, …)` | `set_inherited_roles` | `SetInheritedRoles` | `setInheritedRoles` |
| What a firm pays for | `customers.billed(id)` | `list_billed_customers` | `ListBilledCustomers` | `listBilledCustomers` |
| A tenant's branding | `organizations.branding` · `setBranding` | `get_` · `set_organization_branding` | `Get` · `SetOrganizationBranding` | `get` · `setOrganizationBranding` |
| A tenant's domain | `organizations.domain` · `addDomain` · `verifyDomain` · `removeDomain` | `get_` · `add_` · `verify_` · `remove_organization_domain` | `…OrganizationDomain` | `…OrganizationDomain` |
C# methods end in `Async` and take a `CancellationToken`. Setting branding is a
patch in every language: leave a key out to keep it, pass an explicit null to
clear it so the tenant falls back to its parent and then the environment.
See [Managed tenants](/guides/managed-tenants) for what these do.
## Three things they do that hand-rolled code gets wrong [#three-things-they-do-that-hand-rolled-code-gets-wrong]
**A secret key in a browser throws.** The TypeScript client refuses to construct
in a browser with a `b316_sk_` key — the one mistake that leaks an entire
environment and stays invisible afterwards, because the code keeps working.
**Webhook comparison is constant time, and the timestamp is checked.** A plain
`==` leaks how much of a forged signature was right; a missing timestamp check
lets one captured delivery be replayed for ever.
**Retries are selective.** A 429 or a 5xx is retried with jittered backoff,
honouring `Retry-After`. A 4xx is not — it will fail the same way every time.
## Errors [#errors]
Every client raises a typed error carrying `code`, `status` and `requestId`.
The request id is the one value support can find a specific call by, so log it.