Billing
The billing API lets you manage subscription plans, view invoices, track voucher usage, and handle payments. Payments are processed through Stripe. All endpoints require authentication and access to the specified client account.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /billing/status | Get billing status |
| GET | /billing/usage | Get voucher usage data |
| GET | /billing/items | List available plans and add-ons |
| GET | /billing/orders | Get current plan selection |
| PUT | /billing/orders | Update plan selection |
| GET | /billing/documents | List billing documents |
| GET | /billing/documents/{id} | Get a billing document |
| GET | /billing/documents/{id}/pdf | Download document PDF |
| POST | /billing/documents/{id}/pay | Pay a single document |
| POST | /billing/pay | Pay multiple documents |
| GET | /billing/payment-methods | List payment methods |
| POST | /billing/payment-methods/setup | Add a payment method |
| POST | /billing/payment-methods/{id}/default | Set default payment method |
| DELETE | /billing/payment-methods/{id} | Remove a payment method |
Status & Usage
Get Billing Status
GET /api/v2/billing/status
Returns the current billing status for a client account, including plan limits and payment method availability.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
{
"has_payment_method": true,
"status": "ACTIVE",
"can_process": true,
"outstanding_balance": 0.00,
"outstanding_balance_currency": "NOK",
"vouchers_used": 45,
"vouchers_included": 100,
"over_limit": false,
"price_per_voucher": 2.50,
"overage_included": 200,
"usage_hard_limit": 300
}
price_per_voucher, overage_included, and usage_hard_limit are null if the account has no active plan. overage_included and usage_hard_limit are derived from vouchers_included (currently 2x and 3x, respectively). outstanding_balance is the chargeable AR balance (including unposted invoices) minus any in-flight Stripe charges, floored at zero — the same amount POST /billing/pay charges when called without document_ids. outstanding_balance_currency is the billing account’s accounting currency.
Status Values
| Value | Description |
|---|---|
ACTIVE |
Account is active and can process normally |
NO_PLAN |
No active subscription (trial ended, not re-subscribed) |
OVER_USAGE |
Over the included voucher quota but within the overage ceiling; billable, still processes (can_process is true) |
USAGE_LIMIT_REACHED |
Past the overage ceiling (usage_hard_limit); processing is blocked |
PAYMENT_OVERDUE |
Outstanding payment is overdue; processing is blocked |
Billing accounts (accounts other client accounts bill through, via billing_client_account_id) and provider accounts (accounting/auditing firms, which are never billed directly) always report ACTIVE regardless of plan or usage. A balance below Stripe’s per-currency minimum never triggers PAYMENT_OVERDUE — it rolls forward, immaterial, until a later invoice pushes the total over the threshold.
Get Voucher Usage
GET /api/v2/billing/usage
Returns voucher usage counts over a time period, useful for charting and analytics.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
| granularity | string | No | daily (default) or monthly |
| from_date | date | No | Start date (ISO format). Defaults to first day of current month for daily, or 11 months ago for monthly |
| to_date | date | No | End date (ISO format). Defaults to today |
Response
{
"from_date": "2026-04-01",
"to_date": "2026-04-30",
"total": 145,
"vouchers_included": 200,
"data": [
{
"period": "2026-04-01",
"count": 5
},
{
"period": "2026-04-02",
"count": 3
}
]
}
The vouchers_included field is null if the account has no active plan.
Error Responses
| Status | Description |
|---|---|
| 400 | Invalid granularity value, or from_date is after to_date |
| 403 | No access to the client account |
Plans & Orders
List Available Items
GET /api/v2/billing/items
Returns all available billing items (plans and add-ons) for a client account.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
Returns an array of item objects, each including a billing_metadata field with plan/add-on details.
Get Current Orders
GET /api/v2/billing/orders
Returns the currently selected plans and add-ons for a client account.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
Returns an array of item collection entry objects, each containing item details with billing_metadata.
Update Orders
PUT /api/v2/billing/orders
Sets the billing plan and add-ons for a client account. Replaces the current selection.
An explicit empty array ([]) is a full cancellation: every active subscription entry is ended as of today. Cancelling or downgrading away the plan (ending every plan, or replacing a paid plan with only free items) requires an admin role (SA/AA/CA) on the client account, or an admin-role user of a provider account with an approved contract — providers with a contract manage their clients’ plans. Upgrades and first-time subscriptions are open to every account member.
If the update drops an active BANK_INTEGRATION add-on, active bank integrations are disabled as part of it (including provider-side credential cleanup where supported). Add-on entries that merely lapse by date (e.g. trial expiry) keep their grace through the paid day and are disconnected by a nightly reconcile instead.
Re-submitting an identical selection the same day it was cancelled revives the billing entries — but not the bank integrations: disabled connections (and revoked provider credentials, e.g. Folio API keys) must be reconnected manually under Settings → Integrations.
An order may contain at most one plan, and quantity must be a positive number when provided.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Request Body
A single object or an array of objects:
| Field | Type | Required | Description |
|---|---|---|---|
| item_id | integer | Yes | The ID of the billing item |
| quantity | integer | No | Quantity (defaults to 1) |
Example Request
[
{ "item_id": 10, "quantity": 1 },
{ "item_id": 15, "quantity": 2 }
]
Response
Returns the updated item collection entries with status 201 Created.
Error Responses
| Status | Description |
|---|---|
| 400 | Missing or malformed request body, invalid item ID, item has no price defined, or the account cannot manage billing (e.g. provider accounts) |
| 403 | No access to the client account, or cancellation/downgrade attempted without an admin role or a provider contract |
Billing Documents
List Documents
GET /api/v2/billing/documents
Returns a paginated list of billing documents (invoices and credit notes).
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
| document_type | string | No | Filter by type: ARInvoice, ARCreditNote |
| status | string | No | Filter by status: OPEN, PAID, PART PAID |
| from_date | date | No | Filter documents from this date |
| to_date | date | No | Filter documents up to this date |
| page | integer | No | Page number (default: 1) |
| per_page | integer | No | Items per page (default: 50) |
Response
{
"data": [
{
"id": 42,
"document_type": "ARInvoice",
"document_number": "SB-2026-0042",
"document_date": "2026-04-01",
"due_date": "2026-04-15",
"document_status": "OPEN",
"payment_status": "NONE",
"gross_amount": 1250.00,
"open_amount": 1250.00,
"currency_code": "NOK",
"pdf_url": "https://..."
}
],
"meta": {
"page": 1,
"pages": 3,
"per_page": 50,
"records": 125
}
}
The pdf_url field is null if no PDF is available for the document.
Get Document
GET /api/v2/billing/documents/{id}
Returns a single billing document.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
Returns a single document object (same shape as the list response items).
Error Responses
| Status | Description |
|---|---|
| 403 | No access to the client account |
| 404 | Document not found |
Download Document PDF
GET /api/v2/billing/documents/{id}/pdf
Redirects to a presigned URL for downloading the document PDF.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
Returns a 302 Found redirect to the presigned S3 download URL.
Error Responses
| Status | Description |
|---|---|
| 403 | No access to the client account |
| 404 | Document not found or PDF not available |
Pay a Document
POST /api/v2/billing/documents/{id}/pay
Initiates payment for a single billing document. If a saved payment method exists, the charge is processed directly. Otherwise, a Stripe checkout URL is returned for redirect.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| success_url | string | Yes | URL to redirect to after successful payment |
| cancel_url | string | Yes | URL to redirect to if payment is cancelled |
Example Request
{
"success_url": "https://app.snapbooks.no/billing/success",
"cancel_url": "https://app.snapbooks.no/billing"
}
Response (Direct Charge)
{
"status": "processing",
"payment_request_id": 123,
"amount": 1250.00,
"currency": "NOK"
}
Response (Redirect Required)
{
"status": "pending_redirect",
"payment_request_id": 123,
"amount": 1250.00,
"currency": "NOK",
"checkout_url": "https://checkout.stripe.com/..."
}
Pay Multiple Documents
POST /api/v2/billing/pay
Pays either a specific set of billing documents, or the full outstanding balance, depending on whether document_ids is present in the request body.
- Document mode (
document_idspresent): creates a checkout session for paying the listed documents in a single transaction. - Balance mode (
document_idsomitted entirely): pays the full outstanding balance — the same amount returned asoutstanding_balancebyGET /billing/status. The amount is derived from the AR ledger, not document statuses, so stale-OPENinvoices can never be charged twice. If a payment method is saved, the balance is charged directly; otherwise a Stripe checkout URL is returned for redirect. A present-but-emptydocument_idsarray ([]) is rejected rather than treated as “charge everything.”
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| document_ids | array | No | Array of document IDs to pay. Omit entirely to pay the full outstanding balance instead |
| success_url | string | Yes | URL to redirect to after successful payment |
| cancel_url | string | Yes | URL to redirect to if payment is cancelled |
Example Request (Document Mode)
{
"document_ids": [42, 43],
"success_url": "https://app.snapbooks.no/billing/success",
"cancel_url": "https://app.snapbooks.no/billing"
}
Response (Document Mode)
{
"checkout_url": "https://checkout.stripe.com/...",
"total_amount": 2500.00,
"payment_request_id": 789
}
Example Request (Balance Mode)
{
"success_url": "https://app.snapbooks.no/billing/success",
"cancel_url": "https://app.snapbooks.no/billing"
}
Response (Balance Mode, Direct Charge)
{
"status": "processing",
"amount": 750.00,
"currency": "NOK"
}
Response (Balance Mode, Redirect Required)
{
"status": "pending_redirect",
"checkout_url": "https://checkout.stripe.com/...",
"payment_request_id": 790,
"amount": 750.00,
"currency": "NOK"
}
Error Responses
| Status | Description |
|---|---|
| 400 | Present-but-empty document_ids, missing URLs, payment provider not configured, nothing owed, outstanding balance below Stripe’s per-currency minimum, or the direct charge failed (balance mode without a redirect) |
| 403 | No access to the client account |
Payment Methods
List Payment Methods
GET /api/v2/billing/payment-methods
Returns saved payment methods (cards) for a client account.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
[
{
"id": "pm_1234567890",
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2025,
"is_default": true
}
]
Returns an empty array if no payment methods are saved.
Add Payment Method
POST /api/v2/billing/payment-methods/setup
Creates a Stripe checkout session for securely adding a new payment method.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| success_url | string | Yes | URL to redirect to after card is saved |
| cancel_url | string | Yes | URL to redirect to if setup is cancelled |
Response
{
"checkout_url": "https://checkout.stripe.com/..."
}
Redirect the user to checkout_url to complete the card setup.
Error Responses
| Status | Description |
|---|---|
| 400 | Missing URLs or payment provider not configured |
| 403 | No access to the client account |
Set Default Payment Method
POST /api/v2/billing/payment-methods/{payment_method_id}/default
Sets a payment method as the default for the client account.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
{
"status": "ok"
}
Error Responses
| Status | Description |
|---|---|
| 400 | Operation failed |
| 403 | Payment method does not belong to the client account |
Remove Payment Method
DELETE /api/v2/billing/payment-methods/{payment_method_id}
Removes a saved payment method from the client account.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
Response
{
"status": "ok"
}
Error Responses
| Status | Description |
|---|---|
| 400 | Failed to detach payment method from Stripe |
| 403 | Payment method does not belong to the client account |
Attributes
Billing Document
| Attribute | Type | Description |
|---|---|---|
| id | integer | Unique identifier (read-only) |
| document_type | string | Document type: ARInvoice or ARCreditNote |
| document_number | string | Human-readable document number |
| document_date | date | Date the document was issued |
| due_date | date | Payment due date |
| document_status | string | Processing status: OPEN, PAID, PART PAID, CLOSED, DELETED |
| payment_status | string | Payment status based on reconciliation: NONE, PARTIALLY_PAID, COMPLETED |
| gross_amount | decimal | Total amount including tax |
| open_amount | decimal | Remaining unpaid amount |
| currency_code | string | Currency code (e.g. NOK) |
| pdf_url | string | Presigned URL for PDF download (null if unavailable) |
Payment Method
| Attribute | Type | Description |
|---|---|---|
| id | string | Stripe payment method ID |
| brand | string | Card brand (e.g. visa, mastercard) |
| last4 | string | Last four digits of the card |
| exp_month | integer | Card expiration month |
| exp_year | integer | Card expiration year |
| is_default | boolean | Whether this is the default payment method |
Related Resources
- Client Accounts — the accounts that billing applies to