Metadata-Version: 2.4
Name: vellumcharter
Version: 1.1.0
Summary: Thin, dependency-free server-side client for the VellumCharter entitlements + billing API
License-Expression: MIT
Project-URL: Homepage, https://vellumcharter.com
Project-URL: Repository, https://github.com/tdesposito/VellumCharter
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# vellumcharter

Thin, dependency-free (stdlib-only) server-side client for the VellumCharter
entitlements + billing API. **Server-side only** — it holds a secret API key.

## Install

```bash
pip install vellumcharter
```

Requires Python 3.11+.

## Usage

```python
from vellumcharter import VellumCharterClient

vellum = VellumCharterClient(
    api_key="vlm_sam.xxxxx",   # keep server-side, never ship to a browser
    tenant_id="sam",
    # base_url defaults to https://api.vellumcharter.com
)

# Gate a feature (cached with a short TTL):
if vellum.can("unit_1", "imports"):
    ...

ent = vellum.get_entitlements("unit_1")
ent.has_access()           # server-authoritative; for reporting, not gating
ent.has("imports")         # gate on THIS
ent.config("trial_days")
ent.found                  # False if the customer has no record (no exception)
```

A 404 returns an empty `EntitlementSet` (no exception), so gating never needs a
null check.

## Async

`AsyncVellumCharterClient` is the awaitable counterpart — same constructor and
methods, each `await`-ed. Use it from async frameworks (e.g. FastAPI):

```python
from vellumcharter import AsyncVellumCharterClient

vellum = AsyncVellumCharterClient(api_key="vlm_sam.xxxxx", tenant_id="sam")

if await vellum.can("unit_1", "imports"):
    ...

ent = await vellum.get_entitlements("unit_1")
vellum.invalidate("unit_1")   # cache op is synchronous (no await)
```

## What it covers

- **Entitlements:** `get_entitlements`, `can`, opt-in TTL cache + `invalidate`.
- **Checkout / subscriptions:** `create_checkout`, `create_setup_checkout`,
  `create_subscription`, `cancel_subscription`, `get_subscription`.
- **Provisioning:** `create_account`, `create_customer`, `set_account_status`,
  `set_customer_status`.
- **Billing:** `list_payment_methods`, `set_default_payment_method`,
  `remove_payment_method`, `set_subscription_payment_method`, `list_invoices`,
  `invoice_pdf_url`.
- **Webhook verification:** `verify_push_signature(raw_body, header, secret)` —
  validates a push and returns the parsed event (raises on a bad signature).

Billing reads return dicts in the server's camelCase shape:

- `get_subscription` → `id` (deprecated, removed in 1.0.0), `status`,
  `cancelAtPeriodEnd`, `seats`, `amount` (per-seat price, cents), `currency`,
  `interval` (`month`/`year`), `currentPeriodStart`, `currentPeriodEnd`,
  `trialEnd`, `canceledAt`, `defaultPaymentMethod` (deprecated, removed in
  1.0.0), `defaultPaymentMethodRef` (an opaque handle, byte-identical to the
  matching payment method's `ref`; omitted if it could not be minted — fall
  back to `defaultPaymentMethod`; all timestamps ISO 8601).
- `list_payment_methods` → items of `id` (deprecated, removed in 1.0.0), `ref`
  (opaque, account-bound — send this to the methods below; omitted if it
  could not be minted, fall back to `id`), `brand`, `last4`, `expMonth`,
  `expYear`, `name` (cardholder), `isDefault`.
- `list_invoices` → items of `id` (deprecated, removed in 1.0.0), `ref`
  (opaque, account-bound — send this to `invoice_pdf_url`; omitted if it
  could not be minted, fall back to `id`), `number`, `status`, `amountDue`,
  `amountPaid`, `subtotal`, `total`, `tax`, `paid`, `currency`, `created`
  (ISO), `hostedInvoiceUrl`, `invoicePdf` (deprecated, removed in 1.0.0 — use
  `invoice_pdf_url`, which redirects to the hosted PDF after an ownership
  check).

`set_default_payment_method`, `remove_payment_method`,
`set_subscription_payment_method`, and `invoice_pdf_url` each take a `ref`
from the calls above; a raw billing-provider id is still accepted in the same
argument but is deprecated and removed in 1.0.0.

`payment.*` push events additionally carry `seats` and an `invoice` block
(`id`, `number`, `status`, `amountDue`, `amountPaid`, `currency`,
`hostedInvoiceUrl`, `invoicePdf`) so a dunning notice needs no follow-up read.
`invoice.id` and `invoice.invoicePdf` are deprecated the same way as their
pull-API counterparts, but the push payload has no `ref` to migrate onto yet,
so their removal is tracked separately (D40, TODO.md) rather than tied to the
SDK's 1.0.0.

## Errors

`VellumCharterAuthError` on 401/403; `VellumCharterApiError` (with `.status` /
`.body`) on other non-2xx; `VellumCharterSignatureError` on a bad push signature.
The API key is sent as `x-api-key` on every request.

## API reference

Full endpoint + webhook contract: https://api.vellumcharter.com/openapi.json

## Upgrading to 1.0.0

Breaking. Every raw billing-provider id is gone from the contract; objects that
need a handle carry an opaque, account-bound ``ref`` instead.

| Removed | Use instead |
| --- | --- |
| `PaymentMethod.id` | `PaymentMethod.ref` |
| `Invoice.id` | `Invoice.ref` |
| `Invoice.invoicePdf` | `invoice_pdf_url(account_id=..., invoice_id=<ref>)` — redirects to the hosted PDF after an ownership check |
| `SubscriptionView.id` | address the subscription by `(account_id, customer_id)` |
| `SubscriptionView.defaultPaymentMethod` | `defaultPaymentMethodRef` |
| `SubscribeResponse.subscriptionId` | address the subscription by `(account_id, customer_id)` |
| `CreateAccountResponse.billingCustomerId` | address the account by `account_id` |
| `Entitlements.billingSubscriptionId` | address the subscription by `(account_id, customer_id)` |

**Raw ids are no longer accepted on input.** `set_default_payment_method`,
`remove_payment_method`, `set_subscription_payment_method` and
`invoice_pdf_url` take a `ref`; anything else returns **404**, deliberately — a
403 would confirm the object exists.

`set_subscription_payment_method` now returns the updated subscription view
rather than `{"subscription", "default"}`, matching every other subscription
mutation.

`ref` is always present. Through 0.10.0 it could be omitted if the server failed
to mint one and you fell back to `id`; with `id` gone the server fails closed
instead, so a response either has a usable ref or is an error.

Two things did **not** change: `Invoice.hostedInvoiceUrl` (a Stripe-hosted
payment page, not an identifier) and the inbound `billing_customer_id` on
`create_account`, which adopts a customer that predates Vellum.

Both clients are affected identically — `VellumCharterClient` and the async
`AsyncVellumCharterClient` stay in step.
