API documentation

Consuming the VellumCharter API

VellumCharter is the layer between “they paid” and “they can use it.” Your app calls one fast endpoint to check what a customer is allowed to do; the rest of the API runs checkout, subscriptions, billing, provisioning, and prepaid credits. Every endpoint below has a JavaScript, Python, and raw-HTTP example — pick a language once and it applies to the whole page.

Base URL https://api.vellumcharter.com

These are server-side APIs. Your API key carries a secret — keep it on your backend, never ship it to a browser or mobile client.

Examples in

Authentication

Pass your key in the x-api-key header on every request. Keys look like vlm_<tenantId>.<secret> and are created in your dashboard. A key is scoped to one tenant — requests for any other tenant’s path get a 403. Python also ships an AsyncVellumCharterClient with the same methods.

import { VellumCharterClient } from '@vellumcharter/sdk';

const vellum = new VellumCharterClient({
  apiKey: process.env.VELLUM_API_KEY!, // vlm_<tenantId>.<secret>
  tenantId: 'acme',
  // baseUrl defaults to https://api.vellumcharter.com
  // cacheTtlMs defaults to 30000 (entitlement reads); 0 disables caching
});

Conventions

Requests and responses are JSON. Path parameters (tenantId, accountId, customerId, …) are your own identifiers. Errors come back as { "error": "..." } with a matching status code.

StatusMeaning
400Invalid request — bad or missing parameters, malformed JSON.
403Bad API key, or the key’s tenant doesn’t match the path.
404Not found. For entitlements this just means “no access”.
409Conflict — e.g. insufficient credits on a consume. Nothing changed.
500Server error.

A few “not an error” cases worth knowing: a customer with no entitlements returns 404 (the SDKs hand you an empty, no-access set — no null check needed); a never-purchased credit balance reads as 0, not 404; and a repeated credit consume with the same idempotencyKey returns 200 with duplicate: true rather than deducting twice.

Entitlements

The hot path. One fast, cached lookup answers “what can this customer do right now?” — already resolved from what they pay for. This is the call your app makes on (almost) every request.

GET /tenants/{tenantId}/customers/{customerId}/entitlements

Read a customer’s entitlements

Returns the pre-flattened entitlement blob: which modules are unlocked and the plan config. A customer with no record returns 404 — treat that as “no access”. The SDKs turn it into an empty set so you never null-check.

// One call, instant yes/no — cached ~30s by default.
if (await vellum.can('cust_123', 'exports')) {
  // ...let them export
}

// Or read the whole resolved set:
const ent = await vellum.getEntitlements('cust_123');
ent.isActive();              // true while active/trialing
ent.has('reports');          // module check
ent.config('max_users');     // a plan config value

// After a webhook tells you it changed, drop the cache:
vellum.invalidate('cust_123');

Response

{
  "accountId": "acme",
  "planId": "pro",
  "planVersion": 3,
  "status": "active",
  "currentPeriodEnd": "2026-07-01T00:00:00.000Z",
  "modules": ["exports", "reports"],
  "config": { "max_users": 50 },
  "billingSubscriptionId": "sub_1a2b3c",
  "updatedAt": "2026-06-01T12:00:00.000Z"
}

404 — no entitlements (normal; means no access)

{ "tenantId": "acme", "customerId": "cust_123", "error": "no entitlements" }

Checkout & subscriptions

Turn a customer into a paying one and manage their subscription. Hosted checkout returns a URL you redirect to; the server-side variants act directly when the account already has a saved card.

POST /tenants/{tenantId}/accounts/{accountId}/checkout

Start hosted checkout

Creates a hosted subscription-checkout session. Redirect the browser to the returned URL; the customer enters payment there. Their entitlements are resolved automatically once the payment completes.

  • customerIdrequired The member who gets the seat.
  • planId / planVersionrequired The specific plan version to subscribe to.
  • seats Number of seats (default 1).
  • successUrl / cancelUrlrequired Where the customer is returned after checkout.
const { url } = await vellum.createCheckout({
  accountId: 'acme',
  customerId: 'cust_123',
  planId: 'pro',
  planVersion: 3,
  seats: 2,
  successUrl: 'https://app.example.com/billing/ok',
  cancelUrl: 'https://app.example.com/billing/cancel',
});
// redirect the browser to `url`

Response

{ "url": "https://checkout.example.com/pay/abc123" }
POST /tenants/{tenantId}/accounts/{accountId}/customers/{customerId}/subscribe

Subscribe a customer directly

Creates the subscription server-side, with no hosted page. The account must already have a saved payment method (see Billing or setup checkout). Honors the plan’s trial period if configured.

  • planId / planVersionrequired The specific plan version.
  • seats Number of seats (default 1).
const { subscriptionId } = await vellum.createSubscription({
  accountId: 'acme',
  customerId: 'cust_123',
  planId: 'pro',
  planVersion: 3,
  seats: 2,
});

201 Created

{ "subscriptionId": "sub_1a2b3c" }
POST /tenants/{tenantId}/accounts/{accountId}/billing/setup-checkout

Capture a payment method

Creates a setup-mode hosted session to save or update a card without charging it. Redirect to the returned URL — useful before a server-side subscribe, or to fix an expired card.

  • successUrl / cancelUrlrequired Where the customer is returned after checkout.
const { url } = await vellum.createSetupCheckout({
  accountId: 'acme',
  successUrl: 'https://app.example.com/billing/ok',
  cancelUrl: 'https://app.example.com/billing/cancel',
});
// redirect to `url` to capture a card (no charge)

Response

{ "url": "https://checkout.example.com/pay/abc123" }
GET /tenants/{tenantId}/accounts/{accountId}/customers/{customerId}/subscription

Get a customer’s subscription

The current subscription details — status, seats, price, and period — for building a billing screen. Returns 404 if the customer has no subscription.

const sub = await vellum.getSubscription('acme', 'cust_123');

Response

{
  "id": "sub_1a2b3c",
  "status": "active",
  "cancelAtPeriodEnd": false,
  "seats": 2,
  "amount": 1200,
  "currency": "usd",
  "interval": "month",
  "currentPeriodStart": "2026-06-01T00:00:00.000Z",
  "currentPeriodEnd": "2026-07-01T00:00:00.000Z",
  "defaultPaymentMethod": "pm_1a2b3c"
}
DELETE /tenants/{tenantId}/accounts/{accountId}/customers/{customerId}/subscription

Cancel a subscription

Cancels immediately, or at the end of the current period with ?atPeriodEnd=true (the customer keeps access until then).

  • atPeriodEnd Query param; true to cancel at period end (default false).
await vellum.cancelSubscription('acme', 'cust_123', true); // at period end

Response

{ "canceled": "cust_123", "atPeriodEnd": true }

Billing

Read-and-manage helpers over the account’s cards and invoices — enough to build a self-serve billing page. The underlying payment platform still owns invoicing, proration, tax, and dunning; these are a thin, camelCase view.

GET /tenants/{tenantId}/accounts/{accountId}/payment-methods

List payment methods

The account’s saved cards, with which one is the default.

const methods = await vellum.listPaymentMethods('acme');

Response

{
  "methods": [
    { "id": "pm_1a2b3c", "brand": "visa", "last4": "4242",
      "expMonth": 12, "expYear": 2027, "name": "Ada Lovelace", "isDefault": true }
  ]
}
PATCH /tenants/{tenantId}/accounts/{accountId}/payment-methods/{methodId}

Set the default card

Makes a saved card the account’s default for future invoices.

await vellum.setDefaultPaymentMethod('acme', 'pm_1a2b3c');

Response

{ "default": "pm_1a2b3c" }
DELETE /tenants/{tenantId}/accounts/{accountId}/payment-methods/{methodId}

Remove a card

Detaches a saved card from the account.

await vellum.removePaymentMethod('acme', 'pm_1a2b3c');

Response

{ "removed": "pm_1a2b3c" }
PATCH /tenants/{tenantId}/accounts/{accountId}/customers/{customerId}/payment-method

Set a subscription’s card

Overrides the default payment method for one customer’s subscription (when it should differ from the account default).

  • methodIdrequired The payment method to use.
await vellum.setSubscriptionPaymentMethod('acme', 'cust_123', 'pm_1a2b3c');

Response

{ "subscription": "sub_1a2b3c", "default": "pm_1a2b3c" }
GET /tenants/{tenantId}/accounts/{accountId}/invoices

List invoices

Invoices for the account, or scoped to one customer’s subscription with ?customerId=. Amounts are in the smallest currency unit (e.g. cents).

  • customerId Query param; filter to one customer’s subscription.
const invoices = await vellum.listInvoices('acme', 'cust_123');

Response

{
  "invoices": [
    {
      "id": "in_1a2b3c", "number": "ACME-0001", "status": "paid",
      "amountDue": 2400, "amountPaid": 2400, "subtotal": 2400,
      "total": 2400, "tax": 0, "paid": true, "currency": "usd",
      "created": "2026-06-01T00:00:00.000Z",
      "hostedInvoiceUrl": "https://invoice.example.com/i/...",
      "invoicePdf": "https://.../invoice.pdf"
    }
  ]
}
GET /tenants/{tenantId}/accounts/{accountId}/invoices/{invoiceId}/pdf

Get an invoice PDF

Responds with a 302 redirect to the hosted PDF. The SDKs follow the Location header and return the URL.

const pdfUrl = await vellum.invoicePdfUrl('acme', 'in_1a2b3c');

302 Found

Location: https://.../invoice.pdf

Provisioning

Register the accounts (orgs) and customers (members) your billing hangs off, and update their lifecycle status. Create the account first, then its customers, then subscribe.

POST /tenants/{tenantId}/accounts

Create an account

Creates a billing account. Optionally create a new billing customer for it (createBillingCustomer) or adopt an existing one (billingCustomerId).

  • accountIdrequired Your identifier for the org.
  • name / email Optional billing contact details.
  • createBillingCustomer Create a new billing customer (default false).
  • billingCustomerId Adopt an existing billing customer instead.
const { accountId, billingCustomerId } = await vellum.createAccount({
  accountId: 'acme',
  name: 'Acme, Inc.',
  email: 'billing@acme.test',
  createBillingCustomer: true,
});

201 Created

{ "accountId": "acme", "billingCustomerId": "cus_1a2b3c" }
PATCH /tenants/{tenantId}/accounts/{accountId}

Update account status

Set the account’s operational status (e.g. active, suspended).

  • statusrequired The new status string.
await vellum.setAccountStatus('acme', 'suspended');

Response

{ "accountId": "acme", "status": "suspended" }
POST /tenants/{tenantId}/accounts/{accountId}/customers

Create a customer

Adds a customer (member) under an account.

  • customerIdrequired Your identifier for the member.
  • email Optional contact email.
await vellum.createCustomer({
  accountId: 'acme',
  customerId: 'cust_123',
  email: 'ada@acme.test',
});

201 Created

{ "customerId": "cust_123" }
PATCH /tenants/{tenantId}/accounts/{accountId}/customers/{customerId}

Update customer status

Set the customer’s operational status (e.g. active, disabled).

  • statusrequired The new status string.
await vellum.setCustomerStatus('acme', 'cust_123', 'disabled');

Response

{ "customerId": "cust_123", "status": "disabled" }

Credits

Prepaid, pay-as-you-go balances that sit alongside subscriptions. A customer buys a pack with a one-time payment, and you consume credits as a feature is used. Consumption is atomic, all-or-nothing, and idempotent.

POST /tenants/{tenantId}/accounts/{accountId}/credit-checkout

Buy a credit pack

Creates a one-time (not recurring) hosted Checkout session to buy a prepaid pack. Redirect to the URL; the credits land in the customer’s wallet once payment completes. Buying quantity N grants the pack amount × N.

  • customerIdrequired Whose wallet receives the credits.
  • creditTypeIdrequired Which credit type to buy.
  • packIdrequired The pack SKU.
  • quantity Number of packs (default 1).
  • successUrl / cancelUrlrequired Where the customer is returned after checkout.
const { url } = await vellum.createCreditCheckout({
  accountId: 'acme',
  customerId: 'cust_123',
  creditTypeId: 'render_minutes',
  packId: 'pack_1000',
  quantity: 2,
  successUrl: 'https://app.example.com/credits/ok',
  cancelUrl: 'https://app.example.com/credits/cancel',
});

Response

{ "url": "https://checkout.example.com/pay/abc123" }
GET /tenants/{tenantId}/customers/{customerId}/credits

List credit balances

Every credit type the customer holds, with its balance.

const credits = await vellum.listCredits('cust_123');

Response

{ "credits": [ { "creditTypeId": "render_minutes", "balance": 2000 } ] }
GET /tenants/{tenantId}/customers/{customerId}/credits/{creditTypeId}

Get one balance

A single balance. A customer who never bought this type reads as 0 (not a 404), so a gating check has one uniform shape.

const bal = await vellum.getCreditBalance('cust_123', 'render_minutes');
// bal.balance

Response

{ "creditTypeId": "render_minutes", "balance": 2000 }
POST /tenants/{tenantId}/customers/{customerId}/credits/{creditTypeId}/consume

Consume credits

Atomically deducts credits as a feature is used. Pass a stable idempotencyKey (e.g. the job id) so a retry is a safe no-op — it returns the already-applied balance with duplicate: true. If the balance is insufficient, nothing is deducted and you get a 409.

  • amountrequired Credits to deduct (integer ≥ 1).
  • idempotencyKeyrequired Stable key per logical operation.
try {
  const r = await vellum.consumeCredits('cust_123', {
    creditTypeId: 'render_minutes',
    amount: 30,
    idempotencyKey: 'render:job_42',
  });
  // r.balance, r.duplicate
} catch (e) {
  if (e instanceof VellumCharterApiError && e.status === 409) {
    // insufficient balance — nothing was deducted
  }
}

Response

{ "creditTypeId": "render_minutes", "consumed": 30, "balance": 1970, "duplicate": false }

409 — insufficient credits (nothing deducted)

{ "error": "insufficient credits", "balance": 10 }

SDKs & next steps

The thin, dependency-free SDKs wrap every endpoint above (server-side only). Or call the API directly — it’s plain JSON over HTTPS.

JavaScript / TypeScript

npm install @vellumcharter/sdk — Node 18+.

Python

pip install vellumcharter — Python 3.11+, sync & async.