---
title: Billing
description: Read your current plan and understand the billing API. Self-service checkout and the portal are not available; contact us for Pro access.
order: 11
---

# Billing

**Self-service checkout and the billing portal are not currently available.** Email [hello@robota.us](mailto:hello@robota.us) to arrange Pro access. `GET /v1/orgs/:org/billing` reports your current plan.

The checkout and portal examples below describe the API when billing is enabled. They do not establish a subscription or authorize a charge today.

Checkout and the portal use payment-provider pages. Docsary does not store card numbers; it records billing references and subscription status.

All three need an organization API key. The two `POST` calls need a
write-capable one; `GET /billing` works with a read-only key.

Pro is **$9 a month or $90 a year, per organization**. See
[Pricing and access](/access) for what that buys.

## Start a subscription

```
POST /v1/orgs/:org/billing/checkout
```

```bash
curl -s -X POST "$DOCSARY_API/v1/orgs/acme/billing/checkout" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{ "interval": "month" }'
```

```json
{
  "url": "https://checkout.stripe.com/c/pay/cs_example_SESSION",
  "session_id": "cs_example_SESSION",
  "interval": "month"
}
```

When enabled, `201` returns a payment URL. After payment, check `GET /billing` to confirm the plan update. No content push is required.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `interval` | string | no | `month` or `year`. Defaults to `month`. |
| `success_url` | string | no | Where the browser lands after paying. Defaults to `https://docsary.com/access?checkout=success`. |
| `cancel_url` | string | no | Where it lands if you back out. Defaults to `https://docsary.com/access?checkout=cancel`. |

Omitting the body selects monthly billing and the default return URLs.

### Return URLs

Return URLs must meet both rules:

- `https` only;
- the host must be `docsary.com` or `www.docsary.com`, or a hostname **your
  organization** has attached and verified — which includes any `docsary.com`
  address allocated to one of your sites at creation.

Other hosts return `400`. Example using a verified custom domain:

```bash
curl -s -X POST "$DOCSARY_API/v1/orgs/acme/billing/checkout" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{
    "interval": "year",
    "success_url": "https://docs.acme.com/thanks",
    "cancel_url": "https://docs.acme.com/pricing"
  }'
```

### Discount codes

The checkout page accepts promotion codes. If we have given you one, enter it
there rather than sending it through this call.

### Errors

| Response | Cause |
| --- | --- |
| `400 {"error":"interval must be 'month' or 'year'"}` | Anything else in `interval` |
| `400` containing `must be an https URL` | A return URL that is not `https` |
| `400` containing `attached and verified` | A return URL on a host that is not yours |
| `401 {"error":"missing bearer token"}` | No key |
| `403` containing `read-only` | A read-only key. Starting a subscription is a mutation |
| `404 {"error":"org not found"}` | Unknown organization — or somebody else's. The two answer identically on purpose |
| `409` containing `already has an active subscription` | You are already subscribed; use the portal below |
| `409` containing `already open` | You have a checkout session open. Finish it, or wait for it to expire |
| `502` containing `could not create a checkout session` | The payment provider refused the request or could not be reached. Retry once; if it repeats, email us rather than looping — a repeat means something needs fixing on our side, not on yours |
| `503 {"error":"billing is not configured"}` | Self-service checkout is not switched on. Email us and we will upgrade you by hand |

### One checkout at a time

Send checkout requests sequentially and reuse the returned link. Concurrent requests can create duplicate subscriptions. An already-recorded open checkout returns `409` with its session ID and expiry:

```json
{
  "error": "a checkout session is already open for this organization",
  "detail": "Finish or abandon the one you started. It expires by itself, and after that a new one can be created.",
  "session_id": "cs_example_SESSION",
  "expires_at": 1790001800000
}
```

`expires_at` uses Unix milliseconds; `current_period_end` uses Unix seconds. Use the returned expiry before requesting another session.

### Errors, continued

An existing subscription returns the portal endpoint:

```json
{
  "error": "this organization already has an active subscription",
  "detail": "Use POST /v1/orgs/acme/billing/portal to change the plan, switch between monthly and yearly, update the card, or cancel.",
  "billing_status": "active",
  "billing_interval": "month"
}
```

## Change or cancel

```
POST /v1/orgs/:org/billing/portal
```

```bash
curl -s -X POST "$DOCSARY_API/v1/orgs/acme/billing/portal" \
  -H "authorization: Bearer $DOCSARY_KEY"
```

```json
{
  "url": "https://billing.stripe.com/p/session/bps_example_SESSION",
  "session_id": "bps_example_SESSION"
}
```

When enabled, `201` returns a portal URL for changing billing interval, updating payment details, downloading invoices, or cancelling.

`503 billing is not configured` means the portal is unavailable. Contact us about your paid arrangement.

For a subscription managed through this portal, cancellation keeps Pro through the paid period, then applies Free limits. Downgrading does not delete content. See [downgrade behavior](#what-a-downgrade-does-not-do).

| Response | Cause |
| --- | --- |
| `401` / `403` / `404` | As above |
| `404` containing `no billing customer` | This organization has never bought anything. Start with `checkout` |
| `502` containing `could not create a portal session` | The payment provider was unreachable. Retry |
| `503 {"error":"billing is not configured"}` | As above |

## What am I on

```
GET /v1/orgs/:org/billing
```

```bash
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/orgs/acme/billing"
```

```json
{
  "org": "acme",
  "plan": "pro",
  "plan_source": "stripe",
  "billing_status": "active",
  "billing_interval": "year",
  "current_period_end": 1790000000,
  "current_period_end_unit": "unix_seconds",
  "cancel_at_period_end": false,
  "customer": "…7f21",
  "subscription": "…c904"
}
```

This endpoint reads the recorded plan without contacting the payment provider.

| Field | Meaning |
| --- | --- |
| `plan` | The effective plan used for [quotas](/access#the-ceilings): `free` or `pro` for customers. `owner` is reserved for the service operator and is not a customer subscription |
| `plan_source` | `stripe` if a subscription is paying for it, `manual` if a human set it, `none` once a subscription has ended and nothing is paying for the plan |
| `billing_status` | The subscription's status, verbatim: `active`, `trialing`, `past_due`, `canceled`, `unpaid`, `incomplete`, `incomplete_expired`, `paused`, or `null` if you have never subscribed |
| `billing_interval` | `month` or `year` |
| `current_period_end` | When the period you have **paid for** ends, **in unix seconds** — not milliseconds, unlike every other timestamp in this API. `current_period_end_unit` says so in the response. It moves when a renewal is actually paid, so for the hour or so between a renewal being raised and its invoice clearing, it still shows the period before |
| `cancel_at_period_end` | `true` once you have cancelled and are running out the period you paid for |
| `customer`, `subscription` | The last four characters of the provider's ids, enough to match against an invoice in a support email. The full ids are not returned |

`GET /v1/whoami` also reports the same effective plan, without the billing detail.

### The statuses that keep you on Pro

For a subscription-backed plan, `active` and `trialing` qualify for Pro unless payment collection is paused. `past_due` qualifies only within its grace period and while the subscription has not ended.

The grace cutoff uses `current_period_end` plus the configured period, normally 14 days. When applicable, `past_due_grace_ends` reports that cutoff in milliseconds. Earlier subscription termination can end Pro before that date. Successful payment restores Pro without another content push.

Paused subscriptions do not qualify for Pro. Check the returned effective `plan`; status alone is not sufficient.

## What happens after a payment

When billing is enabled, successful payment updates the plan. A paid renewal advances `current_period_end`; failed payment can start the grace period described above. Portal cancellation takes effect at the paid period’s end, while nonpayment can end the subscription earlier.

Confirm changes with `GET /billing` rather than assuming that opening or returning from a payment page completed them.

### What a downgrade does not do

A downgrade does not delete pages, versions, redirects, or assets. Existing custom hostnames keep serving. Free limits can prevent new sites, new custom domains, and additional storage.

`GET /billing` lists limits your organization exceeds:

```json
{ "plan": "free", "over_plan": ["sites", "custom_domains"] }
```

This list does not schedule content deletion. A manually assigned plan is not revoked by subscription events; contact us about changes to a separate paid arrangement.

## If checkout is not available

`503 {"error":"billing is not configured"}` is the current availability response for self-service billing. Retrying will not enable it. Email [hello@robota.us](mailto:hello@robota.us) with the organization you want upgraded.
