---
name: candi-billing
description: Operate a CANDI billing account — the Merchant-of-Record API for selling digital products, subscriptions, license keys and usage credits. Use when an agent must create products, take payments via hosted checkout, manage subscriptions, issue/validate license keys, grant or spend customer credits, read revenue stats, or wire webhooks. CANDI is the seller of record (charges land on CANDI's Stripe account; merchant balances are held by CANDI and paid out twice-monthly).
---

# CANDI — billing you can drive with an API

CANDI is a Merchant-of-Record (MoR) billing platform for SaaS and digital products:
hosted checkout, subscriptions, license keys, customer credits, discounts, HMAC-signed
webhooks, and twice-monthly payouts. Fee: **3.9% + $0.40** per successful transaction.
This SKILL is the whole public API in one page so an AI agent can onboard and operate an
account end to end.

## The fastest path (agents: use the MCP server)

The lowest-friction way to operate CANDI is the **CANDI MCP server** — it exposes every
capability below as typed tools. Configure it once and call `create_product`,
`create_checkout`, `activate_license`, `credit_account`, etc. directly:

```json
{ "mcpServers": { "candi": {
  "command": "npx", "args": ["-y", "candi-mcp"],
  "env": { "CANDI_API_KEY": "candi_test_your_key" }
} } }
```

Everything below is the underlying REST API the MCP server calls — use it directly when
you need raw HTTP.

## Authentication & base URLs

Every `/v1` request carries a secret API key in the `x-api-key` header. The key prefix
selects the data plane — there is no separate mode flag:

| Prefix | Plane | Base URL |
|---|---|---|
| `candi_live_…` | live (real money) | `https://api.usecandi.com/v1` |
| `candi_test_…` | test (sandbox) | `https://test-api.usecandi.com/v1` |

Keys are shown once at creation and stored only as a hash — treat them as passwords.
Start in **test mode** for every integration; live is a deliberate switch (a different key).

```bash
curl https://test-api.usecandi.com/v1/products -H "x-api-key: candi_test_xxx"
```

## Conventions

- **Amounts are integers** — minor units (cents for money, whole units for credits).
  `4900` = $49.00. Never floats.
- **Pagination**: `?page_number=1&page_size=50` on list endpoints.
- **Idempotency**: send an `Idempotency-Key` header on any POST to make a retry safe
  (24h replay window; a differing body under the same key is a 409). Credit writes
  ALSO take a durable `idempotency_key` in the body.
- **Errors** always return this envelope, so parse it rather than guessing on status:
  ```json
  { "trace_id": "…", "status": 403, "error": "activation_limit_reached",
    "message": ["license activation limit reached"], "timestamp": "…" }
  ```
  `error` is a stable machine slug; `message[]` is human-readable.

## Core objects

`product` (one-time or recurring) → `checkout` (a hosted payment page) → `order` +
`transaction` (a paid charge) → optionally a `subscription`, a `license`, or a `credit`
grant. `customer` ties them together. `webhook_endpoint` receives events. `discount`
reduces a checkout total.

---

## Flow 1 — sell something (product → checkout → webhook)

1. **Create a product.** One-time or recurring, USD or EUR.
   ```bash
   POST /v1/products
   { "name": "Pro plan", "price": 4900, "currency": "usd", "billing_type": "onetime" }
   ```
   Recurring: `"billing_type": "recurring", "billing_period": "every-month"`. Options
   include `trial_days`, `pay_what_you_want` (+ `suggested_price`), `tax_mode`.

2. **Create a checkout.** Returns a hosted URL on `checkout.usecandi.com` — send the
   buyer there; CANDI collects payment (PCI is CANDI's problem, not yours).
   ```bash
   POST /v1/checkouts
   { "product_id": "prod_…", "success_url": "https://you.com/thanks", "units": 1 }
   → { "id": "ch_…", "url": "https://checkout.usecandi.com/ch_…" }
   ```

3. **Receive the result via webhook** (recommended) — CANDI POSTs a `checkout.completed`
   event to your endpoint the instant payment succeeds (see Webhooks below). The event
   object carries the order, and — if the product grants them — the `license_key` and/or
   `credits_granted`.

4. **Return-URL signature (optional).** When the buyer is redirected to your
   `success_url`, CANDI appends query params plus a `signature`. Verify it:
   `signature = SHA256hex("k1=v1|k2=v2|…|salt={your_api_key}")` — params in **arrival
   order** (not sorted), null/empty excluded. This lets you trust the redirect without a
   round-trip, but the webhook is the source of truth.

## Flow 2 — subscriptions

A recurring product checkout creates a `subscription`. Manage it:

- `GET /v1/subscriptions/{id}` / `GET /v1/subscriptions?…` / `GET /v1/subscriptions/search`
- `POST /v1/subscriptions/{id}` — change seats/units (proration applies)
- `POST /v1/subscriptions/{id}/upgrade` — move to another product with a proration mode;
  `POST /v1/subscriptions/{id}/upgrade/preview` shows the immediate charge + credit first
- `POST /v1/subscriptions/{id}/cancel` — end now or at period end
- `POST /v1/subscriptions/{id}/pause` / `…/resume`

Status values: `trialing, active, past_due, paused, canceled, scheduled_cancel, expired`.
CANDI runs renewals + dunning (retry schedule +1d/+3d/+5d/+7d) automatically; you just
react to the `subscription.*` webhooks.

## Flow 3 — license keys

Attach a `licenseKey` feature to a product (config: `activation_limit`, `expiry_days`).
On purchase, CANDI mints a key `XXXXX-XXXXX-XXXXX-XXXXX` (crypto-random, unique) and puts
it on the `checkout.completed` event. Your software validates it against CANDI:

- `POST /v1/licenses/activate` `{ key, instance_name }` → creates a device instance.
  **403** `activation_limit_reached` when the limit is hit; **410** when the license is
  revoked or expired.
- `POST /v1/licenses/validate` `{ key, instance_id? }` → `{ valid, status, activation_limit,
  activation_count, instance }`. `status` ∈ `active|inactive|expired|disabled`.
- `POST /v1/licenses/deactivate` `{ key, instance_id }` → frees a seat.

## Flow 4 — customer credits (usage wallet)

A metered/prepaid balance per customer, backed by a double-entry ledger.

- `POST /v1/credits/accounts` `{ customer_id, unit_label }` — open a wallet
- `POST /v1/credits/accounts/{id}/credit` `{ amount, reference, idempotency_key }` — add
- `POST /v1/credits/accounts/{id}/debit`  `{ amount, reference, idempotency_key }` — spend
  (**409** `insufficient_funds` if it would overdraw; never goes below 0)
- `POST /v1/credits/accounts/{id}/reverse` `{ entry_id, idempotency_key }` — offsetting
  entry (never mutates the original)
- `GET  /v1/credits/accounts/{id}/balance?at=<rfc3339>` — current or historical balance
- `GET  /v1/credits/accounts/{id}/entries` — the ledger
- freeze/unfreeze/close: `POST …/{id}/{freeze,unfreeze,close}` (freeze blocks debits, not
  credits)

The mandatory `idempotency_key` per write is **durable** — the same key replays the same
entry (safe to retry a charge forever); a different payload under it is a 409 conflict.
Products with a `customerCredits` feature grant credits automatically on purchase.

## Flow 5 — customer self-service portal

`POST /v1/customers/billing` `{ customer_id | customer_email }` → returns a magic-link URL
to CANDI's hosted billing portal, where the customer manages their subscription, payment
methods and invoices. Deliver the link by email.

## Webhooks (how you learn about money)

Register an endpoint, then verify every delivery:

- `POST /v1/webhook-endpoints` `{ url, … }` → returns a `secret`.
- Each delivery has header **`candi-signature: <hex>`** = `HMAC_SHA256(rawRequestBody,
  endpoint.secret)`. Recompute over the RAW bytes and compare in constant time; reject on
  mismatch. Retries: 30s / 1m / 5m / 1h; each event has a stable `id` — **dedupe on it**.
- Manage + replay from `GET /v1/webhook-endpoints/{id}/deliveries` and
  `POST …/{delivery_id}/resend`.

Events: `checkout.completed`; `subscription.{active,paid,canceled,scheduled_cancel,
past_due,expired,trialing,paused,update}`; `refund.created`; `dispute.created`.

Node verification:
```js
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
  const mac = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(header));
}
```

## Stats

- `GET /v1/stats/summary` — revenue, transaction count, customer count.
- `GET /v1/metrics/summary` — MRR (cents) with a `periods[]` series.

## Money model (what "merchant of record" means for you)

Charges settle on CANDI's Stripe account; CANDI takes the 3.9% + $0.40 fee, holds your net
in a ledger balance (9-day risk hold, then available), and pays out on the 1st and 15th
(min $50). You never touch card data or hold a Stripe account — CANDI is the seller of
record on the receipt.

## Getting started checklist (test mode)

1. Get a `candi_test_` key from the dashboard (Developers → API keys).
2. `create_product` → `create_checkout` → open the returned URL → pay with a Stripe test
   card.
3. Register a webhook endpoint; confirm you receive + verify `checkout.completed`.
4. When it all works in test, swap to a `candi_live_` key. Nothing else changes.

Full interactive reference: **https://docs.usecandi.com** · Raw OpenAPI:
**https://docs.usecandi.com/candi.v1.yaml**
