> ## Documentation Index
> Fetch the complete documentation index at: https://docs.segmnt.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Plans, payment and the paid-plan whitelist

> List the plans and prices in SAR, join the paid-plan whitelist while paid plans are not on sale, and see how a payment changes the plan when billing is on. Prices are per month, before VAT.

segmnt bills in SAR, per month. **Paid plans are not on sale yet**: every payment
gateway is blocked right now, so there is no checkout. Instead, a customer
**joins the paid-plan whitelist** and we contact them when paid plans open.

This page shows how to list the plans, how to join the whitelist, and how a
confirmed payment changes the plan when billing is on.

This guide is written in simple English.

## Before you start

* API in TEST: `https://test-api.segmnt.cloud`
* Every billing call on this page uses your **session cookie** (the dashboard
  session). An API key cannot call them.
* Prices are in **SAR per month, before VAT**.
* The plans are **Starter**, **Pro** and **Agency**, but they are **not on sale
  yet** (billing is paused). The free plan is the default. A customer joins the
  [paid-plan whitelist](#3-join-the-paid-plan-whitelist) to ask for a paid plan.

## 1. The plans

`GET /v1/billing/plans` returns the plans and their prices.

```bash theme={null}
curl "$BASE/v1/billing/plans" \
  -H "Cookie: segmnt_session=$SEGMNT_SESSION"
```

```json theme={null}
{
  "data": [
    {
      "id": "starter",
      "name": "Starter",
      "price": { "amount": 5900, "currency": "SAR", "interval": "month" },
      "limits": {
        "storedSeconds": 90000,
        "uploadSecondsPerPeriod": 18000,
        "watchedMinutesPerPeriod": 20000,
        "verifiedDomains": 1,
        "projects": 1,
        "maxQuality": 1080,
        "whiteLabel": false
      }
    },
    {
      "id": "pro",
      "name": "Pro",
      "price": { "amount": 34900, "currency": "SAR", "interval": "month" },
      "limits": {
        "storedSeconds": 360000,
        "uploadSecondsPerPeriod": 54000,
        "watchedMinutesPerPeriod": 250000,
        "verifiedDomains": 10,
        "projects": 3,
        "maxQuality": 2160,
        "whiteLabel": true
      }
    },
    {
      "id": "agency",
      "name": "Agency",
      "price": { "amount": 129900, "currency": "SAR", "interval": "month" },
      "limits": {
        "storedSeconds": 1440000,
        "uploadSecondsPerPeriod": 108000,
        "watchedMinutesPerPeriod": 1000000,
        "verifiedDomains": 50,
        "projects": 25,
        "maxQuality": 2160,
        "whiteLabel": true
      }
    }
  ]
}
```

The same prices, in SAR:

| Plan | `id` | Price (SAR / month) |
| - | - | - |
| Starter | `starter` | 59 |
| Pro | `pro` | 349 |
| Agency | `agency` | 1,299 |

Notes:

* `price.amount` is in **halalas** (1 SAR = 100 halalas). `5900` halalas is
  59 SAR. This is the same unit the API uses everywhere.
* `price.currency` is `SAR` and `price.interval` is `month`.
* `price` is **before VAT**. See [VAT](#vat) below.
* `limits` are the plan limits (stored hours, new uploads, watched minutes,
  domains, projects, quality and the logo). See
  [Usage and plan limits](/usage-and-limits) for what they mean.
* The prices and limits come from the plan catalog. They are the same
  numbers you see on the site.
* This call needs the session cookie. Without it you get `401 unauthorized`.

## 2. Start a checkout (paused)

Checkout is **paused**: no payment gateway is active, so this call cannot start
a payment and cannot change a plan. It answers `503 billing_paused`.

```bash theme={null}
curl -X POST "$BASE/v1/billing/checkout" \
  -H "Cookie: segmnt_session=$SEGMNT_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"planId":"pro"}'
```

Answer (`503`):

```json theme={null}
{
  "type": "https://docs.segmnt.cloud/errors/billing_paused",
  "title": "Paid plans are not on sale yet. Join the paid-plan whitelist and we will contact you.",
  "status": 503,
  "code": "billing_paused",
  "detail": "Paid plans are not on sale yet. Join the paid-plan whitelist and we will contact you."
}
```

Notes:

* **Nothing is charged and the plan does not change.** The call returns before it
  reaches any provider.
* Ask the customer to
  [join the paid-plan whitelist](#3-join-the-paid-plan-whitelist) instead of
  paying.
* When billing is on, checkout answers `201` with the provider's hosted page
  (`checkoutUrl`) and the customer pays with mada, Apple Pay, STC Pay or a card.
  `planId` is then checked (a free or unknown plan gets `400 invalid_request`),
  and `discountCode` is an optional code (see [Discount codes](#discount-codes)).
* `503 billing_unavailable` (no provider configured) and `502
  billing_provider_error` (the provider did not answer) apply again only after
  the board turns billing on.

## 3. Join the paid-plan whitelist

Paid plans are not on sale yet. To ask for one, record your interest with this
API. It **stores your intent only** — no payment, no plan change.

`POST /v1/billing/whitelist` (session cookie):

```bash theme={null}
curl -X POST "$BASE/v1/billing/whitelist" \
  -H "Cookie: segmnt_session=$SEGMNT_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"planId":"pro","email":"buyer@example.com"}'
```

Answer (`201`):

```json theme={null}
{
  "data": {
    "planId": "pro",
    "email": "buyer@example.com",
    "createdAt": "2026-10-14T20:00:00.000Z"
  }
}
```

Notes:

* `planId` must be one of the paid plans: `starter`, `pro` or `agency`. A free
  or unknown plan gets `400 invalid_request`.
* `email` is optional. When you omit it, we use the account owner's e-mail.
* The call is **idempotent per account and plan**: calling it again does not
  create a second entry, and the `createdAt` stays the first time. A new `email`
  updates the contact address.
* This never charges the customer and never changes the plan.

`GET /v1/billing/whitelist` (session cookie) returns the account's entries:

```bash theme={null}
curl "$BASE/v1/billing/whitelist" \
  -H "Cookie: segmnt_session=$SEGMNT_SESSION"
```

```json theme={null}
{
  "data": [
    { "planId": "pro", "email": "buyer@example.com", "createdAt": "2026-10-14T20:00:00.000Z" }
  ]
}
```

## Discount codes

Discount codes apply at checkout, so they are **not usable while billing is
paused**. This section is a reference for when the board turns billing on.

`POST /v1/billing/checkout` takes an optional `discountCode`. A code is applied
when it has been **issued to you**; creating a code is an internal segmnt action,
so this page does not show how to create one.

```bash theme={null}
curl -X POST "$BASE/v1/billing/checkout" \
  -H "Cookie: segmnt_session=$SEGMNT_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"planId":"pro","discountCode":"FOUNDING-30"}'
```

* A **founding** code gives **30% off** and a **referral** code gives **10% off**.
  The **total discount is capped at 40%**, so no code can take off more than that.
* Codes are **case-insensitive** and trimmed, so `founding-30` and `FOUNDING-30 `
  are the same code.
* The code is **checked before the payment provider is called**. An unknown,
  expired or used-up code answers `400 invalid_request` with a clear message, so
  the customer is never charged the full price by accident.
* The discount is applied to the plan price **before VAT**: VAT (15%) is charged
  on the discounted net, not on the full price.
* The 15% VAT invoice shows the plan line and, when a code was applied, a second
  **negative discount line**. The lines still add up to the invoice total.
* The code is **counted only when a payment is confirmed**. A provider retry does
  not count the same code twice.
* A confirmed payment stores the code on the subscription (`discount_code`).
* **The discount is kept on every renewal for the promised number of months.**
  A founding code or a referral for the referrer lasts **12 months**; a referral
  for the new customer lasts **3 months**. The first discounted payment locks the
  promise on the subscription, and each later hosted payment re-applies it. Send
  **no** `discountCode` on a renewal: the promise stored on the subscription is
  applied automatically. Once the window is used up, the renewal is at the
  **full price** again.
* The code's `used` counter counts a **customer once**, on the first discounted
  payment, not once per renewal. So `max_uses = 10` means **10 customers**, even
  though each of them may pay 12 discounted months.
* A locked promise is honored even when the code later expires or reaches its
  cap: the customer was promised the discount. A **new** customer using that same
  expired or used-up code is still refused with `400 invalid_request`.
* The 40% cap applies on every payment, including a renewal.

## 4. The payment that changes the plan

Billing is paused, so no provider calls this route today. A valid event that
arrived anyway is acknowledged with `204` and changes no plan. This section is a
reference for when the board turns billing on.

When the customer pays, Moyasar calls
`POST /v1/billing/providers/moyasar/webhook`. This route is for the provider,
not for customers. It has no segmnt session; it is checked with the provider's
own secret token.

What happens on every verified event:

1. The provider's secret token is checked with a constant-time compare.
2. The webhook body is **never trusted**. We read the merchant invoice back
   from the provider API.
3. The amount and currency are checked against the plan's fixed price
   (plus 15% VAT when VAT mode is on).
4. The payment, the 15% VAT invoice, the subscription and the account plan are
   written in **one batch**, so the event is applied once and cannot be applied
   half way.

This makes the webhook **idempotent**: a provider retries a webhook, so the
same event can arrive more than once. One provider invoice is recorded at most
once, a second delivery answers `204` and does not change the plan again or
extend the period.

A **refund** reverses an applied payment. When the provider reports a refunded
payment, the payment is recorded as `refunded`, the subscription is set to
`canceled`, and the account returns to the **free** plan. The invoice row is
**kept**: a refund is not a delete, and the fiscal sequence must stay gapless
(a ZATCA credit note is a later open point). A refund is idempotent too, so
a retried refund changes nothing.

Answers:

| Status | When |
| - | - |
| `204` | Applied, already applied, reversed by a refund, or not a paid payment. A refund is accepted so the provider does not retry. |
| `400` `billing_mismatch` | A verified payment that does not match what was ordered. No plan change. |
| `401` `invalid_signature` | The secret token is missing or wrong, or there is no invoice to reconcile. |
| `404` `not_found` | Unknown payment provider. |
| `502` `billing_provider_error` | The provider API could not be read. The provider will retry. |
| `503` `billing_unavailable` | Webhooks are not configured. |

A verified event that does not match is refused with `400` and changes no plan.
This covers a wrong amount, a currency that is not SAR, a missing plan or
account in the invoice, an unknown plan, and an account we do not have.

## 5. Check the plan after payment

After a confirmed payment, `GET /v1/billing/plan` returns the paid plan, an
active subscription period, the new limits and your usage. The call is
documented on the [Usage and plan limits](/usage-and-limits) page.

```bash theme={null}
curl "$BASE/v1/billing/plan" \
  -H "Cookie: segmnt_session=$SEGMNT_SESSION"
```

```json theme={null}
{
  "plan": { "id": "pro", "name": "Pro" },
  "status": "active",
  "currentPeriod": {
    "start": "2026-10-14T10:00:00.000Z",
    "end": "2026-11-14T10:00:00.000Z"
  },
  "limits": {
    "storedSeconds": 360000,
    "watchedMinutesPerPeriod": 250000,
    "verifiedDomains": 10,
    "maxQuality": 2160,
    "whiteLabel": true
  },
  "usage": { "storedSeconds": 5400, "watchedMinutes": 842, "verifiedDomains": 1 }
}
```

Notes:

* `limits` is the paid plan's limits (stored hours, watched minutes, domains,
  quality and the logo). In `limits`, `null` means **no limit**.
* `usage` is your account now: stored seconds, watched minutes in the current
  period, and verified domains.
* The new-upload and project limits are enforced, but they are not in this
  answer yet. See [Usage and plan limits](/usage-and-limits) for the full
  limits table.

The paid period is one calendar month from the payment, in UTC. A shorter month
is clamped, so 31 January becomes 28 or 29 February.

## VAT

VAT is not on yet. Today VAT is **off**:

* When VAT is off, the customer pays the plan price and the invoice shows
  `0` VAT.
* When VAT mode is **on**, the amount is the plan price **plus 15%**, and the
  invoice records the VAT.

The webhook checks the paid amount with the same rule, so a payment that does
not match the expected amount is refused. VAT is ZATCA's money, not segmnt
revenue.

The stored invoice **lines** carry the VAT: each line has `unitAmount` (net),
`vatAmount` and `amount` (gross, net + VAT). So the lines add up to
`invoices.total` when VAT mode is on.

## Not live yet

* **Paid plans.** They are not on sale yet: billing is paused. A customer joins
  the [paid-plan whitelist](#3-join-the-paid-plan-whitelist) and we contact them.
* **The annual plan.** Only the monthly plan is planned.
* **The watched-minute overage on the provider invoice.** Pro and Agency are
  never stopped at the watched-minutes limit. The overage is now recorded in
  TEST (see [Usage and plan limits](/usage-and-limits)); folding it into the
  payment provider's invoice is a later step.
* **The invoice PDF.** The 15% VAT invoice is written, but its PDF file is not
  built yet.

## Errors

Errors use `application/problem+json`. Use the `code` field in your code, not
the text. The `type` field links to `https://docs.segmnt.cloud/errors/{code}`.

| Status | `code` | Meaning | What to do |
| - | - | - | - |
| 400 | `invalid_request` | Unknown `planId` (checkout or whitelist), a bad whitelist `email`, or an unknown, expired or used-up `discountCode` | Send a plan on sale and a valid code or e-mail (or none). |
| 400 | `billing_mismatch` | Provider webhook: the payment does not match the order (amount, currency, plan or account) | Check the invoice in the provider dashboard. No plan change was made. |
| 401 | `unauthorized` | Missing, wrong or expired session | Sign in again. |
| 401 | `invalid_signature` | Provider webhook: the secret token is wrong, or there is no invoice | Check the provider's webhook secret. |
| 404 | `not_found` | Provider webhook: unknown payment provider | Use the provider's webhook URL (`/v1/billing/providers/moyasar/webhook`). |
| 405 | `method_not_allowed` | Wrong method for the URL | See the method in each section above. |
| 502 | `billing_provider_error` | The payment provider did not answer | Try again. For a webhook, the provider retries. |
| 503 | `billing_paused` | Paid plans are not on sale yet (billing is paused) | Join the paid-plan whitelist. Nothing was charged. |
| 503 | `billing_unavailable` | Online payment or webhooks are not configured | Use a manual invoice, or ask us to configure the provider. |

## Tested in TEST

Checked against `https://test-api.segmnt.cloud` on 14 Oct 2026:

* `POST /v1/billing/checkout` with a session -> `503 billing_paused` (no provider
  is active; nothing is charged and the plan does not change).
* `POST /v1/billing/whitelist` with a session -> `201`, and a second call ->
  `201` with the same entry (idempotent per plan).
* `GET /v1/billing/whitelist` with a session -> `200` with the account's rows.
* `GET /v1/billing/plans` with no session -> `401 unauthorized`.
* `POST /v1/billing/checkout` with no session -> `401 unauthorized`.
* `POST /v1/billing/checkout` with an unknown plan (billing on) -> `400
  invalid_request` (unit test).

The pause and the whitelist are proved by the API tests in
`api/src/billing.test.ts`: checkout answers `503 billing_paused` with **no**
provider call and no plan change, and the whitelist is idempotent per
account+plan and account-scoped. The same tests keep the retained payment path
covered for when the board turns billing on: a verified invoice applies the plan
and the 15% VAT invoice, sequential invoice numbers, idempotent replay, and every
`400` refusal path. The live Moyasar sandbox run needs sandbox keys, so the
end-to-end provider call is not run on TEST while billing is paused.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.