# Billing

> Buy credits for your account. Sign in first. Generated from the API contract.

## POST `/v1/account/checkout`

Open a Stripe payment page that adds credits to this account.

Owner only. A pack tops up an active subscription, and only the packs
of your plan are on sale; without one the answer is 409 no_subscription.
The credits land on this account once
Stripe confirms the payment. It makes no keys. Open checkout_url in a
browser; the page expires after 30 minutes. ref_token and ref_code
are read on every checkout, but only the account's first paid
purchase binds a referral (MEG-504). A bad ref is dropped, never an
error. Stripe sends the buyer back to the dashboard: /buy?paid, or
/buy?cancelled.

### Parameters

| name | in | required | type or values | what it is |
| -- | -- | -- | -- | -- |
| `Idempotency-Key` | header | no | string | Your own id for this call, 1 to 128 printable ASCII characters. A second call with the same key from the same account key inside 24 hours answers 200 with the same checkout_url, and opens no new payment page. A call that failed frees its key. |


### Body

| field | required | type or values | what it is |
| -- | -- | -- | -- |
| `pack` | yes | string | The pack's public name, like starter. A name we do not sell is 404 not_found. |
| `campaign` | no | string | Where the buyer came from, like reddit-launch. Never a URL or an email. |
| `ref_token` | no | string | Added by MEG-237. The ref_token the /r/{code} link put on the page's address. Checked with our secret; a bad, forged or expired one (older than 60 days) is dropped without an error. Binds only on the account's first paid purchase. |
| `ref_code` | no | string | Added by MEG-237. A referral code typed by the buyer, 8 of a-z and 0-9. An unknown code is dropped without an error. ref_token wins when both are sent. |


### Answers

| status | what it means |
| -- | -- |
| `200` | The Stripe payment page. Open checkout_url in a browser. It expires after 30 minutes. |
| `400` | bad_parameter: pack is missing (field pack), campaign is wrong (field campaign), Idempotency-Key is not 1 to 128 printable ASCII characters (field Idempotency-Key), or a key this request does not take (field names it). bad_request: the body is not one JSON object. |
| `401` | bad_token. The key is missing, not valid, expired, revoked, of an unknown prefix or wrong length, or sent as the api_key query parameter, which no route accepts. |
| `403` | wrong_scope (a scrape key on a route that needs an account key, or an account key on a scraping or batches route) or owner_only (your role is member). |
| `404` | not_found. No pack with that name. |
| `413` | body_too_large. The body was larger than 4 KiB. |
| `415` | unsupported_media_type. Send Content-Type: application/json, even for an empty {} body. |
| `422` | idempotency_mismatch. This Idempotency-Key already opened a checkout for a different body. Send the same body, or a new key. |
| `429` | rate_limited. Too many requests to this endpoint right now. Wait Retry-After seconds, then send the same request again. |
| `500` | internal. Our bug. Try again in a minute; if it stays, report the X-Request-Id. |
| `503` | checkout_failed (Stripe did not open the payment page; nothing was charged, try again in a minute), checkout_closed (buying is off on this server) or busy (the account store is not configured, or a request with this Idempotency-Key is still opening its page; send it again in a few seconds). |


## POST `/v1/account/subscription`

Subscribe to a plan, or change it.

Owner only. With no subscription, opens a Stripe payment page for the
plan. With one, moves to the plan: a higher plan is charged in full now
and starts a new month with its full credits; a lower plan starts at
your next renewal. Two identical changes sent at once are charged once.
Credits come only with a paid invoice. Packs top up
a subscription (POST /v1/account/checkout).

### Body

| field | required | type or values | what it is |
| -- | -- | -- | -- |
| `plan` | yes | string |  |


### Answers

| status | what it means |
| -- | -- |
| `200` | New subscription: checkout_url, a Stripe payment page that expires after 30 minutes. Stripe sends you back to /plan?subscribed or /plan?cancelled. A plan change: change is upgrade (paid now, the month restarts, the new credits land when Stripe confirms) or downgrade (from your next renewal; no money moves now). |
| `400` | bad_parameter: plan is missing or not a plan (field plan). bad_request: the body is not one JSON object. |
| `401` | bad_token. The key is missing, not valid, expired, revoked, of an unknown prefix or wrong length, or sent as the api_key query parameter, which no route accepts. |
| `402` | payment_failed. The payment for the higher plan failed. Your plan did not change. |
| `403` | wrong_scope (a scrape key on a route that needs an account key, or an account key on a scraping or batches route) or owner_only (your role is member). |
| `409` | checkout_open (a payment page is still open), past_due (the last payment failed; update the card in the billing portal) or already_on_plan. |
| `413` | body_too_large. The body was larger than 4 KiB. |
| `415` | unsupported_media_type. Send Content-Type: application/json, even for an empty {} body. |
| `429` | rate_limited. Too many requests to this endpoint right now. Wait Retry-After seconds, then send the same request again. |
| `500` | internal. Our bug. Try again in a minute; if it stays, report the X-Request-Id. |
| `503` | checkout_failed (Stripe did not answer; nothing was charged, try again in a minute) or checkout_closed (buying is off on this server). |


## POST `/v1/account/billing-portal`

Open the Stripe billing portal.

Owner only. Your card, your invoices, and canceling.

### Answers

| status | what it means |
| -- | -- |
| `200` | portal_url, a Stripe page for your card, your invoices and canceling. A cancel takes effect at the end of the paid month; your credits stay until they expire. |
| `401` | bad_token. The key is missing, not valid, expired, revoked, of an unknown prefix or wrong length, or sent as the api_key query parameter, which no route accepts. |
| `403` | wrong_scope (a scrape key on a route that needs an account key, or an account key on a scraping or batches route) or owner_only (your role is member). |
| `404` | not_subscribed. This account has no subscription yet. |
| `429` | rate_limited. Too many requests to this endpoint right now. Wait Retry-After seconds, then send the same request again. |
| `500` | internal. Our bug. Try again in a minute; if it stays, report the X-Request-Id. |
| `503` | checkout_failed (Stripe did not answer) or checkout_closed (buying is off on this server). |


## GET `/v1/plans`

The free tier and the plans you can subscribe to.

No key. From the plan rows in Postgres; the price is the one Stripe charges.

### Answers

| status | what it means |
| -- | -- |
| `200` | The free tier first, then the plans you can subscribe to, cheapest first. price is per month, in currency; both are null on the free tier, which needs no subscription. |
| `500` | internal. Our bug. Try again in a minute; if it stays, report the X-Request-Id. |
| `503` | Error. The code is in the body. |
