> For the complete documentation index, see [llms.txt](https://docs.osmosworks.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.osmosworks.com/business-modules/payments-and-billing.md).

# Payments and billing

This module covers invoices, payments, subscriptions, and payout accounts.

## Invoices

An invoice represents an amount owed by a customer. Invoices can be created:

* Automatically when a payment stage becomes current.
* Manually by staff through `POST /api/v1/finance/invoices`.

Invoice statuses:

* `OPEN` — unpaid and payable.
* `PAID` — fully settled.
* `VOID` — cancelled; cannot be paid.

An invoice has a tenant-unique reference (for example `INV-XXXXXX`), currency, amount due, amount paid, and line items (`InvoiceItem`) describing each charge.

### Creating an ad-hoc invoice

Staff can create an invoice without a service by providing:

* `amount` (positive)
* `currency`
* `description`
* optional `partyId` and `dueAt`

### Creating a service-priced invoice

When `serviceId` is provided, `InvoiceService` calls `PricingService.priceBreakdown` to compute the total from the service's charges and current answers.

### Voiding an invoice

`POST /api/v1/finance/invoices/{invoiceId}/void` marks an open invoice as `VOID`. Paid invoices cannot be voided.

## Payments

A payment is one checkout attempt against an invoice. Payments are processed through Paystack.

Payment statuses:

* `PENDING` — checkout started, awaiting customer completion.
* `SUCCESSFUL` — provider confirmed payment.
* `FAILED` — provider reported failure.
* `ABANDONED` — older than the configured expiry threshold.

### Starting a payment

Staff can start a payment for an invoice:

`POST /api/v1/finance/invoices/{invoiceId}/payments`

The request body requires the customer's `email` and may include a `callbackUrl`. The backend:

1. Verifies the invoice is `OPEN` and has a positive balance.
2. Creates a `Payment` record with a unique reference such as `PAY-XXXXXX`.
3. Calls Paystack to initialize a checkout, passing the tenant's verified subaccount code if available.
4. Returns the Paystack `authorizationUrl` and `accessCode`.

Customers can also start payments publicly without authentication:

`POST /api/v1/public/payments/invoices/{invoiceReference}/checkout`

### Reconciling payments

```mermaid
flowchart TD
    Start([Customer completes Paystack checkout]) --> Choice{Notification Vector}

    Choice -->|Vector 1: Real-time| Webhook[POST /api/v1/payments/webhook]
    Choice -->|Vector 2: Background Poller| Cron[Scheduled Job: Pending Payment Reconcile]
    Choice -->|Vector 3: Manual Intervention| Staff[POST /api/v1/finance/payments/{ref}/reconcile]

    Webhook --> Verify[Verify Signature & Query Paystack API]
    Cron --> Verify
    Staff --> Verify

    Verify --> Check{Payment Successful & Amount Matches?}
    Check -->|Yes| Fulfill[1. Mark Payment SUCCESSFUL<br/>2. Mark Invoice PAID<br/>3. Advance linked Application Stage]
    Check -->|Mismatch| Error[Record Payment Mismatch Error]
    Check -->|Pending| Wait[Remain PENDING until next retry]
    Check -->|Older than 24h| Expire[Mark Payment ABANDONED]
```

Payments are reconciled by:

* The Paystack webhook posting to `POST /api/v1/payments/webhook`.
* A scheduled job that checks pending payments older than `PAYMENT_PENDING_RECONCILE_MINUTES`.
* Manual reconciliation: `POST /api/v1/finance/payments/{reference}/reconcile`.

Reconciliation verifies the payment with Paystack and, if successful, marks the invoice `PAID`, records the paid amount, and advances any linked payment stage.

### Payment mismatch

If the verified amount or currency does not match the invoice, the payment is not fulfilled and an error is recorded.

## Subscriptions

Tenants subscribe to platform billing plans. The subscription lifecycle is managed through `/api/v1/billing` endpoints.

Subscription statuses:

* `ACTIVE`
* `CANCELED`
* `PAST_DUE`
* others as defined by the implementation

### Checkout

`POST /api/v1/billing/checkout` starts a Paystack subscription checkout for a paid plan. The backend creates or fetches a Paystack customer code and returns an authorization URL.

### Change plan

`PUT /api/v1/billing/subscription/plan` changes the tenant's plan. If a Paystack subscription exists, its plan is updated through Paystack.

### Cancel

`POST /api/v1/billing/subscription/cancel` disables the Paystack subscription and sets the local subscription to `CANCELED`. Paid access continues until the end of the current period.

### Card update

`POST /api/v1/billing/subscription/card-update` requests a Paystack manage-subscription link sent to the billing email.

## Payout accounts

A tenant can register a payout account so customer payments settle into the tenant's bank account through Paystack subaccounts. The account is verified against Paystack's bank list.

* `GET /api/v1/organizations/payment-account/banks` lists supported banks.
* `POST /api/v1/organizations/payment-account` sets up the payout account.

## Billing plans

Platform administrators define billing plans with:

* `code` (unique)
* `name`
* `price` and `currency`
* `interval` (monthly, yearly)
* `paystackPlanCode`
* limits (staff seats, services, customers, and so on)

Plans are managed through `/api/v1/platform/billing-plans`.

## Subscription limits and TierGuard

The backend protects platform resources through `TierGuard`. Every consuming write path (such as activating staff or creating new applications) is checked against the active plan's limits:

* **Staff seats limit (`maxStaff`)**: `ensureStaffSeatsAvailable` verifies seats prior to invitation. During member activation, `consumeStaffSeat` bumps the `TenantSubscription` optimistic lock version to serialize concurrent activations and re-checks the count. If exceeded, the transaction is rolled back.
* **Applications per billing cycle (`maxApplicationsPerCycle`)**: `ensureApplicationsAvailable` ensures the tenant has not exceeded their quota for the current period.

When a limit is reached, `TierGuard` throws `TierLimitExceededException` (HTTP 403 Forbidden) with details:

```json
{
  "status": "Failed",
  "message": "Plan limit exceeded",
  "errorCode": "TIER_LIMIT_EXCEEDED",
  "data": {
    "limitKind": "APPLICATIONS_PER_CYCLE",
    "used": 100,
    "max": 100
  }
}
```

The current effective limits and usage snapshot are returned by `GET /api/v1/billing/subscription/tier` (cached per tenant and evicted on billing status changes).

## Permissions

* `invoice.read`, `invoice.manage` — invoice operations.
* `payment.read`, `payment.reconcile` — payment operations.
* `billing.read`, `billing.manage` — tenant subscription operations.
* `platform:tenant:manage`, `platform:admin:manage` — platform billing and tenant management.

## Integration notes

* Paystack webhooks must be configured to post to `/api/v1/payments/webhook`.
* Cloudinary webhooks must post to `/api/v1/files/webhooks/cloudinary`.
* The backend uses the Paystack test keys by default; replace them with live keys in production.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.osmosworks.com/business-modules/payments-and-billing.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
