Zaroz
Zaroz Cloud Kernel Open API

List My Invoices

GET
/api/v2/billing/invoices

Lists invoices across every organization the caller may see, paginated, including invoices for cancelled or soft-deleted orders (design doc §44.2 -- an organization is a shared workspace, so members see each other's invoices). Every row carries its own organization_id, so the listing can be grouped by owner; for one organization's invoices alone, read /organizations/{id}/invoices instead. Each invoice's line items are not included here -- fetch one invoice by id for its lines. Each row carries due_at, the date an unpaid invoice must be paid by -- derived from the invoice's created_at plus the grace period of the subscription it bills, and null for an invoice that bills no subscription. A pending invoice whose due_at has passed is overdue; pending on its own does not say that.

List My Invoices › query Parameters

page
​integer · uint32 · min: 0 · style: form

Page number (1-indexed). Defaults to 1.

Page number (1-indexed). Defaults to 1.

Default: 1
page_size
​integer · uint32 · min: 0 · style: form

Number of items per page. Defaults to 20, max 100.

Number of items per page. Defaults to 20, max 100.

Default: 20

List My Invoices › Responses

Invoices

`/v2` DTOs are always named `*Request` / `*Response` (design doc `§43.4`) -- there is no `InvoiceView` here on purpose (`/v1`'s equivalent, `kernel_http_billing::controllers::invoices::InvoiceView`).
​InvoiceSummaryResponse[] · required
​PaginationMeta · required

Pagination metadata included in paginated responses


Get My Open Invoices Summary

GET
/api/v2/billing/invoices/open-summary

What the caller still owes across every organization they may see: how many invoices are open (pending, including a postpaid invoice before its due date), how many of those are overdue, and their total. One cheap read for a navigation indicator, so a client does not page the invoice list to decide whether to light a dot. total_cents and currency are null when nothing is open or when the open invoices span several currencies.

Get My Open Invoices Summary › Responses

Open invoices summary

What the caller still owes, without the invoices themselves (V-0579). The read behind the open-invoice attention dot on the Billing nav: a sidebar needs one number and one amount, not a page of rows. "Open" and "overdue" are decided in the domain (`OpenInvoicesSummary`); this is its wire shape.
count
​integer · uint64 · min: 0 · required

Invoices issued and not yet paid, including a postpaid invoice before its due date. 0 means nothing is owed.

overdue_count
​integer · uint64 · min: 0 · required

How many of count are past their due_at.

currency
​string | null

ISO 4217 code of total_cents; null exactly when total_cents is.

total_cents
​integer | null · int64

Sum of the open invoices' tax-inclusive totals, in cents. null when nothing is open, and also when the open invoices span more than one currency (an amount that adds two currencies is withheld, not guessed).


Get My Invoice

GET
/api/v2/billing/invoices/{id}

Gets one invoice by id, from any organization the caller may see (design doc §44.2), including its line items and, for a Full-series invoice, the billing-identity snapshot frozen onto it at issue. Each line names the project it was billed for (project_id/project_name), resolved server-side through the line's subscription so a client never needs a lookup per line; both are null for a line that names no subscription. The invoice itself carries due_at, the date an unpaid invoice must be paid by, derived from created_at plus the grace period of the subscription it bills and null when it bills none.

Get My Invoice › path Parameters

id
​string · uuid · required · style: simple

Get My Invoice › Responses

The invoice

One invoice with its line items and its frozen billing-identity snapshot. Program A2-S10 made invoices genuinely multi-line and organization-owned; `/v1`'s `InvoiceView` has never had a line-items field and, per the `/v1` freeze, never will (design doc `2026-08-26-v1-api-surface-changes.md` `§7`). This is that route. The `bill_to_*` fields are Program G `§2.2`'s frozen recipient snapshot, taken once at issue and never re-derived from a live billing identity -- last year's invoice keeps showing last year's address even after the organization's billing profile changes. All four are `None` for a `Simplified`-series invoice (which must structurally carry no personal data, see `Invoice::validate`) and for every invoice issued before Program G.
amount_cents
​integer · int64 · required
created_at
​string · date-time · required
currency
​string · required
id
​string · uuid · required
is_synthetic
​boolean · required
​InvoiceKindResponse · required
​InvoiceLineResponse[] · required
status
​InvoiceStatusResponse · enum · required

An invoice's status on the /v2 wire.

Deserialize as well as Serialize, because this is also the closed vocabulary of listOrganizationInvoices's status filter. Deliberately one type rather than a separate *Filter enum: the filter's accepted values and the values a row can come back carrying must be the same set, and a second enum is a second place for them to disagree. A generated client sees one schema for both.

Enum values:
pending
paid
failed
refunded
cancelled
tax_amount_cents
​integer · int64 · required
total_amount_cents
​integer · int64 · required
bill_to_address
​string | null
bill_to_country
​string | null
bill_to_name
​string | null
bill_to_tax_id
​string | null
due_at
​string | null · date-time

When an unpaid invoice must be paid by. Identical in meaning and derivation to InvoiceSummaryResponse::due_at -- see that field, including its postpaid settlement exception.

failed_at
​string | null · date-time
invoice_number
​string | null
​
paid_at
​string | null · date-time
refunded_at
​string | null · date-time
tax_label
​string | null
tax_rate
​number | null · double

The tax rate applied at issue, e.g. 0.21 for 21%. None when no tax rate was resolved for this invoice. Exactly the stored rate (numeric(6,4)), never a blurred one: a rate the JSON number cannot carry exactly fails the response instead (V-0699).


List Organization Invoices

GET
/api/v2/organizations/{id}/invoices

Lists one organization's invoices, paginated, including invoices for cancelled or soft-deleted orders. An organization is a shared workspace, so every member sees the organization's invoices, not only ones they placed themselves (design doc §44.2). Answers 404 for an organization the caller does not belong to, the same way reading that organization directly would. Line items are not included here -- fetch one invoice by id for its lines. Each row carries due_at, derived the same way listMyInvoices describes.

Narrow to one status with status. The filter is applied to the organization's whole invoice set, not to the page: ?status=failed answered with an empty page means this organization has no failed invoice at all, which is the question a past-due banner is really asking. A status that names none of the five invoice statuses is refused with invalid-field-value rather than ignored.

List Organization Invoices › path Parameters

id
​string · uuid · required · style: simple

List Organization Invoices › query Parameters

page
​integer · uint32 · min: 0 · style: form

Page number (1-indexed). Defaults to 1.

Page number (1-indexed). Defaults to 1.

Default: 1
page_size
​integer · uint32 · min: 0 · style: form

Number of items per page. Defaults to 20, max 100.

Number of items per page. Defaults to 20, max 100.

Default: 20
status
​style: form

Narrows the page to invoices in one status, e.g. failed.

Optional, and a pure narrowing: omitted, the listing is exactly what it has always been. status=failed answered with an empty page is kernel saying this organization has nothing past due -- an answer over the organization's whole invoice set, not over one page of it.

Typed as a String here and decoded by [InvoiceStatusResponse::parse_wire] so an unrecognised value can be refused with this surface's own invalid-field-value 400 rather than the query extractor's plain-text rejection. The emitted schema is the closed InvoiceStatusResponse enum regardless, so a generated client cannot spell one wrong in the first place.

Narrows the page to invoices in one status, e.g. failed.

Optional, and a pure narrowing: omitted, the listing is exactly what it has always been. status=failed answered with an empty page is kernel saying this organization has nothing past due -- an answer over the organization's whole invoice set, not over one page of it.

Typed as a String here and decoded by [InvoiceStatusResponse::parse_wire] so an unrecognised value can be refused with this surface's own invalid-field-value 400 rather than the query extractor's plain-text rejection. The emitted schema is the closed InvoiceStatusResponse enum regardless, so a generated client cannot spell one wrong in the first place.

Default: null

List Organization Invoices › Responses

Invoices

`/v2` DTOs are always named `*Request` / `*Response` (design doc `§43.4`) -- there is no `InvoiceView` here on purpose (`/v1`'s equivalent, `kernel_http_billing::controllers::invoices::InvoiceView`).
​InvoiceSummaryResponse[] · required
​PaginationMeta · required

Pagination metadata included in paginated responses


Get Organization Open Invoices Summary

GET
/api/v2/organizations/{id}/invoices/open-summary

What one organization still owes: its open invoice count, how many are overdue, and their total, as getMyOpenInvoicesSummary describes, scoped to the organization. Every member sees it (an organization is a shared workspace, design doc §44.2). Answers 404 for an organization the caller does not belong to.

Get Organization Open Invoices Summary › path Parameters

id
​string · uuid · required · style: simple

Get Organization Open Invoices Summary › Responses

Open invoices summary

What the caller still owes, without the invoices themselves (V-0579). The read behind the open-invoice attention dot on the Billing nav: a sidebar needs one number and one amount, not a page of rows. "Open" and "overdue" are decided in the domain (`OpenInvoicesSummary`); this is its wire shape.
count
​integer · uint64 · min: 0 · required

Invoices issued and not yet paid, including a postpaid invoice before its due date. 0 means nothing is owed.

overdue_count
​integer · uint64 · min: 0 · required

How many of count are past their due_at.

currency
​string | null

ISO 4217 code of total_cents; null exactly when total_cents is.

total_cents
​integer | null · int64

Sum of the open invoices' tax-inclusive totals, in cents. null when nothing is open, and also when the open invoices span more than one currency (an amount that adds two currencies is withheld, not guessed).


Checkout

POST
/api/v2/checkout

Creates a pending Subscription for one product/plan/region combination, along with the Initial invoice that establishes it. Which hardware profile inside the region runs the workload is resolved server-side; customers never name one. region_id is required for every product except a licence, which is placed nowhere and must omit it. A VPS also requires operating_system_id, chosen from GET /api/v2/catalog/products/{id}/operating-systems. Bills the organization named by organization_id, which requires the Owner or Admin role there (or being its personal owner); omit organization_id to bill the caller's own personal organization. Files the subscription under project_id, which must belong to the buying organization; omit it to use (and, on first purchase, create) that organization's default project. A personal organization's games project belongs to the gaming dashboard and is refused here (403 project-gaming-managed); the gaming dashboard buys through POST /api/v2/checkout/gaming. Creates no Order -- V2 has no order concept at all. Pay the returned invoice through the existing invoice-id-keyed payment endpoints (for example POST /api/v1/invoices/{id}/checkout/mollie); once paid, the subscription activates and provisioning is attempted automatically.

Checkout › Request Body

`POST /api/v2/console/checkout`'s request. A customer picks a **region** and a plan; which hardware profile inside that region runs the workload is internal placement, resolved by `CreateSubscriptionCheckoutServiceImpl::resolve_region_hardware_profile` (`crates/application/subscriptions/src/application/services/orders/create_subscription_checkout_service.rs`). This used to carry a `hardware_profile_id` instead, which no client could ever fill because nothing on `/v2` enumerated hardware profiles. `plan_id` and `region_id` are both `i32` (`impl_int_id!`), not UUIDs: the same ids `GET /catalog/products/{id}/plans` and `GET /catalog/regions` hand back as `PlanResponse.id` and `RegionResponse.id`.
name
​string · required

The customer's name for the service being bought.

The subscription is a purchase of a product and carries no name; this names the ServiceInstance the initial payment creates -- the VPS or container the customer actually refers to.

plan_id
​integer · int32 · required
product_id
​string · uuid · required
slug
​string · required

The URL segment addressing that service, unique within its project. Supplied rather than derived here: the console suggests one from the name as the customer types and lets them edit it.

cadence_months
​integer | null · int16 · min: -32768 · max: 32767

The billing term to buy, in months: 1, 3, 6 or 12 (V-0717).

Omitting it buys the monthly term, which is what every client written before term choice sends. The plan's offered terms, with each one's price and saving, are its terms on the product plan listing. A term the plan is not sold at is refused (400), never sold monthly instead; the subscription renews at the same term and Price.

Default: null
discount_code
​string | null

An optional discount code, exactly as the customer typed it.

A code that cannot be used is refused, not ignored: a customer who entered one and was charged full price anyway has been overcharged as far as they are concerned.

Public ports to open on the service once it runs. Omitting this (or sending none) buys exactly what every earlier request bought.

The customer names only the container port and the protocol(s); Zaroz assigns the public address and port. Each port is billed by the hour it is held, monthly in arrears: it is never part of the invoice this request creates, and the quote lists each one as a line of amount zero with the estimate in metered_estimate. A port that cannot be opened right after payment never fails the purchase: it is listed on the service as pending with the reason, and opened as soon as it can be. Refused for a product that offers none, past the plan's included ports when more are not priced, and above 10 per request.

hardware_profile_id
​integer | null · int32

The hardware tier inside region_id to run on, as RegionHardwareResponse.id from GET /api/v2/catalog/regions (decision-0347: a customer chooses between a region's tiers, each with its own Price). Omit it to let kernel place the service on the cheapest tier that fits. A tier that is not listed under region_id, or that the listing marks unavailable, is refused exactly as an unbuyable region is.

Default: null
operating_system_id
​string | null · uuid

The operating system to install, for a VPS purchase (ProductFamily::VirtualMachine). Required for that family, and refused if sent for any other -- nothing else on /v2 installs an operating system. GET /api/v2/catalog/products/{id}/operating-systems lists what can be named here.

Default: null
organization_id
​string | null · uuid

Which organization is buying, and therefore which one the subscription and its invoice bill to.

Optional, and omitting it means "my personal organization" -- the only thing /v2 checkout could bill before shared organizations became purchasable. Existing clients that send no organization_id keep the exact behaviour they had.

Sending one requires the caller to be entitled to spend that organization's money: an Owner or Admin membership, or its personal owner. A Member or Billing role is refused (403), never silently redirected to the caller's own organization. An organization the caller has no relationship to answers 404, the same as an unknown id, so ids cannot be enumerated by their refusal.

project_id
​string | null · uuid

Which project the purchased service belongs to (§34.1's Organization -> Project -> Subscription -> Resource).

Optional, and omitting it means the buying organization's default project -- created on first use if it does not exist yet, the same resolution /v1 order creation has always applied for an organization with no project concept of its own. This is what makes checkout reachable without first showing the caller an organization: §11's invisibility on organization_id is worthless if project_id still forces one into view.

When supplied, it must name a project the buying organization owns; one belonging to another organization is refused rather than ignored, because a subscription filed under someone else's project misattributes that spend in every read that joins through it.

region_id
​integer | null · int32

Where the service should run. Must be one of the regions GET /api/v2/console/catalog/regions returns for this product_id/plan_id: that read applies the same access and capacity rules checkout does, so anything it lists is buyable and anything it omits would be refused here.

Required for every purchasable product except a licence (ProductFamily::License), which is placed nowhere at all and is refused if this is sent. Omit it entirely for a licence purchase.

Default: null
​

The size to buy, when the customer sized it themselves.

Omitting this buys the plan exactly as authored, which is what every existing client does and what every existing request means. This is an override, not a replacement: plan_id is still required and still decides which hardware profile runs the workload and which Price rates it, because those are placement and money rather than size.

A plan is one convenient point inside the range products.minimum_*/maximum_* describes, and a customer sizing their own instance is choosing a different point in the same range. Anything outside it is refused by Product::admits rather than clamped to fit: silently resizing somebody's order and then charging them for the result is the worse of the two answers.

This is also what makes persistent storage opt-in. A product whose minimum_storage_gb is zero admits a shape with no disk, that shape prices at zero against the storage rate, and the provisioner builds no volume for it.

Checkout › Responses

The created subscription and its establishing invoice

`/v2` DTOs are always named `*Request` / `*Response` (design doc `§43.4`). There is no `order` field anywhere on this type or its `subscription` nested value -- design constraint 1 of this checkout slice: V2 buys without orders, and no wire contract may name one even in spirit.
available_cents
​integer · int64 · required
​object · required

The establishing invoice, in the same shape GET /billing/invoices/{id} already returns (InvoiceResponse) -- a client that just checked out pays this invoice through the existing invoice-id-keyed payment endpoints (POST /api/v1/invoices/{id}/checkout/mollie or .../paypal, both of which already resolve by InvoiceId alone).

required_cents
​integer · int64 · required

ZC-1324 org trust S5 (§8.4). See [QuoteResponse::required_cents]'s own doc comment -- identical meaning, read AFTER this checkout's own effects (its reservation, if it wrote one) rather than before.

​CheckoutSubscriptionResponse · required
​

V-0449.04: postpaid disclosure, present only for the OWNER of an organization with a credit limit. See crate::contracts::standing::PostpaidDisclosureResponse.

​

Gaming Checkout

POST
/api/v2/checkout/gaming

The gaming dashboard's checkout: the same purchase as Checkout, filed into the caller's personal organization's games project, which it creates on the first purchase and reuses afterwards. It is the only way anything is added to that project. Sells only products the gaming store sells (any other is 404 like an unknown product). Takes the Checkout body; organization_id may only name the caller's own personal organization and project_id must be omitted, or the purchase is refused with 422 gaming-checkout-target-fixed.

Gaming Checkout › Request Body

`POST /api/v2/console/checkout`'s request. A customer picks a **region** and a plan; which hardware profile inside that region runs the workload is internal placement, resolved by `CreateSubscriptionCheckoutServiceImpl::resolve_region_hardware_profile` (`crates/application/subscriptions/src/application/services/orders/create_subscription_checkout_service.rs`). This used to carry a `hardware_profile_id` instead, which no client could ever fill because nothing on `/v2` enumerated hardware profiles. `plan_id` and `region_id` are both `i32` (`impl_int_id!`), not UUIDs: the same ids `GET /catalog/products/{id}/plans` and `GET /catalog/regions` hand back as `PlanResponse.id` and `RegionResponse.id`.
name
​string · required

The customer's name for the service being bought.

The subscription is a purchase of a product and carries no name; this names the ServiceInstance the initial payment creates -- the VPS or container the customer actually refers to.

plan_id
​integer · int32 · required
product_id
​string · uuid · required
slug
​string · required

The URL segment addressing that service, unique within its project. Supplied rather than derived here: the console suggests one from the name as the customer types and lets them edit it.

cadence_months
​integer | null · int16 · min: -32768 · max: 32767

The billing term to buy, in months: 1, 3, 6 or 12 (V-0717).

Omitting it buys the monthly term, which is what every client written before term choice sends. The plan's offered terms, with each one's price and saving, are its terms on the product plan listing. A term the plan is not sold at is refused (400), never sold monthly instead; the subscription renews at the same term and Price.

Default: null
discount_code
​string | null

An optional discount code, exactly as the customer typed it.

A code that cannot be used is refused, not ignored: a customer who entered one and was charged full price anyway has been overcharged as far as they are concerned.

Public ports to open on the service once it runs. Omitting this (or sending none) buys exactly what every earlier request bought.

The customer names only the container port and the protocol(s); Zaroz assigns the public address and port. Each port is billed by the hour it is held, monthly in arrears: it is never part of the invoice this request creates, and the quote lists each one as a line of amount zero with the estimate in metered_estimate. A port that cannot be opened right after payment never fails the purchase: it is listed on the service as pending with the reason, and opened as soon as it can be. Refused for a product that offers none, past the plan's included ports when more are not priced, and above 10 per request.

hardware_profile_id
​integer | null · int32

The hardware tier inside region_id to run on, as RegionHardwareResponse.id from GET /api/v2/catalog/regions (decision-0347: a customer chooses between a region's tiers, each with its own Price). Omit it to let kernel place the service on the cheapest tier that fits. A tier that is not listed under region_id, or that the listing marks unavailable, is refused exactly as an unbuyable region is.

Default: null
operating_system_id
​string | null · uuid

The operating system to install, for a VPS purchase (ProductFamily::VirtualMachine). Required for that family, and refused if sent for any other -- nothing else on /v2 installs an operating system. GET /api/v2/catalog/products/{id}/operating-systems lists what can be named here.

Default: null
organization_id
​string | null · uuid

Which organization is buying, and therefore which one the subscription and its invoice bill to.

Optional, and omitting it means "my personal organization" -- the only thing /v2 checkout could bill before shared organizations became purchasable. Existing clients that send no organization_id keep the exact behaviour they had.

Sending one requires the caller to be entitled to spend that organization's money: an Owner or Admin membership, or its personal owner. A Member or Billing role is refused (403), never silently redirected to the caller's own organization. An organization the caller has no relationship to answers 404, the same as an unknown id, so ids cannot be enumerated by their refusal.

project_id
​string | null · uuid

Which project the purchased service belongs to (§34.1's Organization -> Project -> Subscription -> Resource).

Optional, and omitting it means the buying organization's default project -- created on first use if it does not exist yet, the same resolution /v1 order creation has always applied for an organization with no project concept of its own. This is what makes checkout reachable without first showing the caller an organization: §11's invisibility on organization_id is worthless if project_id still forces one into view.

When supplied, it must name a project the buying organization owns; one belonging to another organization is refused rather than ignored, because a subscription filed under someone else's project misattributes that spend in every read that joins through it.

region_id
​integer | null · int32

Where the service should run. Must be one of the regions GET /api/v2/console/catalog/regions returns for this product_id/plan_id: that read applies the same access and capacity rules checkout does, so anything it lists is buyable and anything it omits would be refused here.

Required for every purchasable product except a licence (ProductFamily::License), which is placed nowhere at all and is refused if this is sent. Omit it entirely for a licence purchase.

Default: null
​

The size to buy, when the customer sized it themselves.

Omitting this buys the plan exactly as authored, which is what every existing client does and what every existing request means. This is an override, not a replacement: plan_id is still required and still decides which hardware profile runs the workload and which Price rates it, because those are placement and money rather than size.

A plan is one convenient point inside the range products.minimum_*/maximum_* describes, and a customer sizing their own instance is choosing a different point in the same range. Anything outside it is refused by Product::admits rather than clamped to fit: silently resizing somebody's order and then charging them for the result is the worse of the two answers.

This is also what makes persistent storage opt-in. A product whose minimum_storage_gb is zero admits a shape with no disk, that shape prices at zero against the storage rate, and the provisioner builds no volume for it.

Gaming Checkout › Responses

The created subscription and its establishing invoice

`/v2` DTOs are always named `*Request` / `*Response` (design doc `§43.4`). There is no `order` field anywhere on this type or its `subscription` nested value -- design constraint 1 of this checkout slice: V2 buys without orders, and no wire contract may name one even in spirit.
available_cents
​integer · int64 · required
​object · required

The establishing invoice, in the same shape GET /billing/invoices/{id} already returns (InvoiceResponse) -- a client that just checked out pays this invoice through the existing invoice-id-keyed payment endpoints (POST /api/v1/invoices/{id}/checkout/mollie or .../paypal, both of which already resolve by InvoiceId alone).

required_cents
​integer · int64 · required

ZC-1324 org trust S5 (§8.4). See [QuoteResponse::required_cents]'s own doc comment -- identical meaning, read AFTER this checkout's own effects (its reservation, if it wrote one) rather than before.

​CheckoutSubscriptionResponse · required
​

V-0449.04: postpaid disclosure, present only for the OWNER of an organization with a credit limit. See crate::contracts::standing::PostpaidDisclosureResponse.

​

Quote a checkout

POST
/api/v2/checkout/quote

Prices one product/plan/region/size combination without buying it (region_id and operating_system_id follow the same rules as /checkout): the same resolution, the same placement, the same price, the same discount and tax rules POST /checkout would apply, so the total quoted here is the total that endpoint invoices. Writes nothing -- no subscription, no invoice, and a discount code is validated but not redeemed. Send the same body you would send to /checkout, minus name and slug. Refusals match /checkout's exactly, which is the point: a size, region or code that cannot be bought says so here rather than on submit.

Quote a checkout › Request Body

`POST /api/v2/console/checkout/quote`'s request. [`CheckoutRequest`] minus the two fields that only describe a purchase that is actually happening: `name` and `slug`. A customer moving a sizing control has not named anything yet, and requiring a name before showing a price would put the price last -- after the decision it is supposed to inform. Every remaining field is here because it changes the number or decides which number applies, and each one means exactly what it means on `CheckoutRequest`. Send the same body to both endpoints (plus a name and slug) and the total quoted is the total invoiced.
plan_id
​integer · int32 · required
product_id
​string · uuid · required
cadence_months
​integer | null · int16 · min: -32768 · max: 32767

The billing term to quote. See [CheckoutRequest::cadence_months]: omitted is the monthly term, and the quote's total is one whole term.

Default: null
discount_code
​string | null

An optional discount code, exactly as the customer typed it. A code that cannot be used is refused here exactly as it would be at checkout, rather than quietly quoting the undiscounted price.

Public ports to price. See CheckoutRequest::endpoints: the same rules refuse the same requests here. Each comes back as an informational line of amount zero, and metered_estimate carries the worst monthly cost. total_cents never includes them.

hardware_profile_id
​integer | null · int32

See CheckoutRequest::hardware_profile_id's own doc comment.

Default: null
operating_system_id
​string | null · uuid

See CheckoutRequest::operating_system_id's own doc comment.

Default: null
organization_id
​string | null · uuid

Which organization would be buying. Decides the reseller price multiplier and the tax jurisdiction, so it is quote input rather than bookkeeping.

project_id
​string | null · uuid

Which project the purchase would be filed under.

Optional, mirroring [CheckoutRequest::project_id]. When supplied it is still checked against the buying organization -- nothing is written, but a quote is an answer about a purchase this caller could make, so naming somebody else's project is refused here too. When absent, nothing is checked and nothing is resolved: unlike checkout, a quote never creates the buying organization's default project, because a quote is documented to write nothing at all.

region_id
​integer | null · int32

Required for every family except a licence. See CheckoutRequest::region_id's own doc comment.

Default: null
​

The size to price. Omitting it quotes the plan as authored.

Quote a checkout › Responses

What the purchase would cost, broken down

What a purchase would cost, in cents, itemized. Cents and `i64`, matching `InvoiceResponse` exactly (see that type's own note on why `/v2` does not put decimal strings on the wire): the figures a customer compares are the quote and the invoice, so they are quoted in the same unit and arrive as the same kind of number. **Every figure here is a field, and none is derived from another.** A console that had to work out what a code took off, what tax was charged, or what the line under the total was for would be reimplementing the pricing rules in TypeScript, and its answer would start disagreeing with the invoice the first time kernel prorates, clamps a discount or prices a region differently. The breakdown exists so nobody is ever tempted. The customer-facing ticket adds up, in this order and no other: ```text subtotal_cents (== the sum of `lines`) + discount.amount_cents (negative, or the field is absent) + tax.amount_cents (only when `tax.included_in_lines` is false) = total_cents ``` On a plan authored `tax_behavior: inclusive`, `tax.included_in_lines` is true, every line above already contains the tax, and the chain collapses to `subtotal_cents + discount.amount_cents = total_cents` with nothing added for tax. The tax figure is then a statement about what is inside the price ("of which VAT"), not something to add. That is the only thing that flag changes: the sentence, never this arithmetic. `amount_cents` is not a term of that chain. It is always the establishing invoice's `amount` -- the net, pre-tax figure `TaxBehavior::split` derives -- regardless of `tax_behavior`. On an `Exclusive` plan the two happen to coincide (`subtotal_cents + discount.amount_cents == amount_cents`, because every figure above is already net), but on an `Inclusive` one `amount_cents` is less than `total_cents` by the tax the advertised price already contains, precisely because the lines, subtotal and discount above are gross there and `amount_cents` is not.
amount_cents
​integer · int64 · required

After the discount, in the invoice's own terms: always the net, pre-tax figure, never the plan's advertised (possibly tax-inclusive) price. This is the establishing invoice's amount, and the figure the two can be reconciled on, for both tax behaviours. See this struct's own doc comment for why it is not a term of the customer-facing additive chain on an Inclusive plan.

available_cents
​integer · int64 · required

balance - required_cents. Compared against required_cents (not against this purchase's own total) by the caller: kernel does not know here whether this purchase itself will reserve anything, only what the organization's standing already looks like.

cadence_label
​string · required

How to read the figures aloud, e.g. "per month, billed monthly". Kernel's own words: it is a claim about a billing term, and a client assembling one from a month count would be deciding what that count means.

cadence_months
​integer · int16 · min: -32768 · max: 32767 · required

The billing term quoted, in months (1, 3, 6 or 12): the one asked for, or 1 when none was. Every figure below is one whole term (V-0717).

currency
​string · required

The currency every figure below is in.

due_now_cents
​integer · int64 · required

What the customer pays now, which is not always total_cents: a setup fee, a term paid up front or a prorated first period all move it. Rendered, never derived. Equal to total_cents for every purchase this endpoint can price today, and a field anyway, because the day it stops being equal must not be the day a console starts lying.

​QuoteLineResponse[] · required

What is being bought, described the way the invoice will describe it.

Exactly one line today, because a /v2 checkout issues exactly one invoice line (InvoiceLineItemKind::BaseSubscription). It is a list rather than a single field so that a setup fee, a term bundle or a second priced component does not have to reshape the contract, and so a client renders whatever arrives instead of a fixed row.

required_cents
​integer · int64 · required

ZC-1324 org trust S5 (§8.4): max(organization_floor, already-reserved) -- the buying organization's own standing, not this purchase's price. Present on every quote, not only a Measurable one: an unmeasurable product still explains the organization's floor this way (§8.2's closing paragraph).

​object · required

The shape that was priced -- the customer's when they sent one, the plan's when they did not. Echoed so a console showing "2 vCPU, 4 GiB, no disk -- EUR 12.40" is reading both halves from the same answer rather than pairing a server price with its own local idea of the size.

subtotal_cents
​integer · int64 · required

The sum of lines, before any discount code.

​QuoteTaxResponse · required

Tax, as data rather than as something to work out.

The client must never derive any of this from a billing country: which country applies, whether a business is reverse-charged, and what the rate is on the day are all kernel's decisions, taken by TaxResolutionService::resolve.

total_cents
​integer · int64 · required

What the customer pays for one billing cycle.

​

The code that was applied, what it does, and what it took off. None when no code was sent; a code that was sent and cannot be used is a refusal, never a silently absent discount.

​

What the requested public ports can cost, present only when the request named some. Ports are billed by the hour held, monthly in arrears, so this is a worst case (every priced port held a whole month), shown beside the ticket and never added to total_cents or due_now_cents.

​

V-0449.04: postpaid disclosure, present only for the OWNER of an organization with a credit limit. See crate::contracts::standing::PostpaidDisclosureResponse.

​

The organization's own current restriction, if any -- None when its standing is Good. Kernel supplies the reason and the numbers; the frontend supplies the words (§8.4).


Pay Invoice With Wallet

POST
/api/v2/billing/invoices/{id}/pay-with-wallet

Pays one pending invoice from the wallet of the organization it is billed to, and returns the wallet entry the money moved through. Requires an Owner, Admin or Billing role in that organization (or being its personal owner); an API token needs the organizations.wallet.spend grant. A plain Member may read the invoice and may not pay it.

Send an Idempotency-Key header. Paying is the one write in this API where a retry after a timeout can cost the customer real money twice, and the header is what lets a client repeat the identical request safely. It is optional only because a request without one is still better than a customer who cannot pay at all.

A wallet that cannot cover the invoice is a 422 naming the balance and the amount needed, not a 402: nothing was attempted and nothing is owed to a third party. An invoice that is already paid, void or draft is a 422 as well, because the invoice is real and the request is simply not applicable to it. An invoice belonging to somebody else answers exactly as one that does not exist. A card/PayPal settlement or a staff reconciliation already transitioning this invoice answers 409; nothing was charged, and retrying finds either success or the invoice already paid. An active provider checkout also answers 409 because it must be finished or resolved before the wallet can safely own payment.

Pay Invoice With Wallet › path Parameters

id
​string · uuid · required · style: simple

Pay Invoice With Wallet › Responses

The wallet entry the payment moved through

What a successful payment produces: the ledger entry the money moved through, so a client can link straight to it rather than paging the wallet to find what it just did.
entry_id
​string · uuid · required

Start Mollie Checkout For Invoice

POST
/api/v2/billing/invoices/{id}/checkout/mollie

Starts a Mollie payment for one pending invoice and returns the Mollie checkout page to send the customer's browser to. Mollie returns the customer to the invoice on the dashboard when they finish.

Requires an Owner, Admin or Billing role in the organization the invoice is billed to (or being its personal owner); an API token needs the organizations.purchase grant. Paying an invoice by card moves no wallet money, so the wallet grants do not apply.

Starting a checkout pays nothing: the invoice is paid when the provider settles, and is then booked as a card receipt. Calling this again for the same invoice returns the checkout already started. An invoice belonging to an organization the caller cannot see answers exactly as one that does not exist. An invoice that is already paid, failed, refunded or cancelled is a 422, as is one with nothing to pay. A checkout or settlement already in progress is a 409; one active provider must be finished or resolved before choosing another. A 503 means the provider is not available right now.

Start Mollie Checkout For Invoice › path Parameters

id
​string · uuid · required · style: simple

Start Mollie Checkout For Invoice › Responses

The Mollie checkout page

Where to send the customer's browser to pay. One shape for both providers. `/v1` answered Mollie with `checkout_url` and PayPal with `approve_url` for the same idea, so every client carried a branch that existed only to read a differently named field.
checkout_url
​string · required

The provider-hosted page the customer completes the payment on. Open it in the browser; the invoice is paid when the provider settles, not when this URL is issued.


Start PayPal Checkout For Invoice

POST
/api/v2/billing/invoices/{id}/checkout/paypal

Starts a PayPal order for one pending invoice and returns the PayPal approval page to send the customer's browser to. Once the customer approves, PayPal sends the browser back to GET /api/v2/public/billing/invoices/{id}/checkout/paypal/capture, which captures the payment and redirects to the invoice on the dashboard.

Requires an Owner, Admin or Billing role in the organization the invoice is billed to (or being its personal owner); an API token needs the organizations.purchase grant. Paying an invoice by card moves no wallet money, so the wallet grants do not apply.

Starting a checkout pays nothing: the invoice is paid when the provider settles, and is then booked as a card receipt. Calling this again for the same invoice returns the checkout already started. An invoice belonging to an organization the caller cannot see answers exactly as one that does not exist. An invoice that is already paid, failed, refunded or cancelled is a 422, as is one with nothing to pay. A checkout or settlement already in progress is a 409; one active provider must be finished or resolved before choosing another. A 503 means the provider is not available right now.

Start PayPal Checkout For Invoice › path Parameters

id
​string · uuid · required · style: simple

Start PayPal Checkout For Invoice › Responses

The PayPal approval page

Where to send the customer's browser to pay. One shape for both providers. `/v1` answered Mollie with `checkout_url` and PayPal with `approve_url` for the same idea, so every client carried a branch that existed only to read a differently named field.
checkout_url
​string · required

The provider-hosted page the customer completes the payment on. Open it in the browser; the invoice is paid when the provider settles, not when this URL is issued.


Get Organization Wallet

GET
/api/v2/organizations/{id}/wallet

Gets one organization's wallet: its credit balance, one row per currency, in integer cents. The organization owns the wallet (kernel-v2 §19 Q89), so every member of a shared organization reads the same balance -- this is not the caller's personal wallet unless the caller named their personal organization.

Only non-zero balances are returned: the balance is summed from the ledger, so an organization that has never been credited, and one whose movements net to exactly zero, both answer with an empty balances list. Treat a currency that is absent as zero.

The balance is a separate read rather than a field on the organization on purpose. A client's scope layout reads the organization on every request; folding the ledger there would put a sum over every wallet entry in front of every navigation. Ask for the balance on the page that shows money.

No threshold is applied and no "low balance" verdict is served. If one is ever wanted it is kernel's to define, not something a client may work out from this number.

Answers 404 for an organization the caller does not belong to, the same way reading that organization -- or /organizations/{id}/invoices -- would; an organization that does not exist is not distinguishable from one that is not yours.

Get Organization Wallet › path Parameters

id
​string · uuid · required · style: simple

Get Organization Wallet › Responses

The organization's wallet balances

One organization's wallet. The balance lives here rather than on `OrganizationResponse` on purpose. Console's scope layout reads the organization on every request, under the sidebar and every page below it; a running total folded out of the ledger on that path would put a `SUM` over `wallet_entries` in front of every navigation. A balance is one operation, asked for when a page actually shows money.
​WalletBalanceResponse[] · required

One row per currency the organization holds, ordered by currency code.

A currency with no money in it has no row. The balance is summed from the ledger and only non-zero sums come back, so an organization that has never been credited answers with an empty list, and so does one whose movements happen to net to exactly zero. Treat a missing currency as zero; do not treat an empty list as "no wallet".

organization_id
​string · uuid · required

The organization whose wallet this is -- echoed back so a client that fans several of these out cannot mis-file an answer.


List Organization Wallet Entries

GET
/api/v2/organizations/{id}/wallet/entries

Lists one organization's wallet ledger, newest first, paginated the same way the invoice listing is: a { data, pagination } envelope whose total_items counts every entry the filter matches, not the ones on this page. (/v1's /me/wallet/entries answers with a bare array and a timestamp cursor, with no total and no next cursor, so nothing there can build a pager.)

Each row is one signed movement in integer cents: positive credits the organization, negative debits it. Rows carry no running balance, and a client must not add them up to produce one -- a total over a page agrees with the wallet only when the page is the entire ledger, so it would read as correct and be wrong. Read /organizations/{id}/wallet for the balance.

Narrow to one currency with currency (e.g. EUR). A wallet holding two currencies has two independent balances, so an unfiltered page is a history rather than an account. A currency that is not a three-letter uppercase ISO 4217 code is refused with invalid-field-value rather than ignored.

The page is cut on a total order -- time descending, with the entry id breaking ties -- so walking consecutive pages sees each entry exactly once.

Answers 404 for an organization the caller does not belong to, identically to an organization that does not exist.

List Organization Wallet Entries › path Parameters

id
​string · uuid · required · style: simple

List Organization Wallet Entries › query Parameters

currency
​string | null · style: form

Narrows the page to one currency, e.g. EUR.

Optional, and a pure narrowing: omitted, the page carries every currency the organization has ever moved, newest first. A wallet with two currencies has two independent balances, so a page that mixes them is a history and not an account -- a client showing one balance should ask for that currency by name.

A value that is not three uppercase letters is refused with invalid-field-value rather than ignored, for the reason listOrganizationInvoices refuses an unknown status: serving the unfiltered listing would answer a question the caller did not ask.

Narrows the page to one currency, e.g. EUR.

Optional, and a pure narrowing: omitted, the page carries every currency the organization has ever moved, newest first. A wallet with two currencies has two independent balances, so a page that mixes them is a history and not an account -- a client showing one balance should ask for that currency by name.

A value that is not three uppercase letters is refused with invalid-field-value rather than ignored, for the reason listOrganizationInvoices refuses an unknown status: serving the unfiltered listing would answer a question the caller did not ask.

Default: null
page
​integer · uint32 · min: 0 · style: form

Page number (1-indexed). Defaults to 1.

Page number (1-indexed). Defaults to 1.

Default: 1
page_size
​integer · uint32 · min: 0 · style: form

Number of items per page. Defaults to 20, max 100.

Number of items per page. Defaults to 20, max 100.

Default: 20

List Organization Wallet Entries › Responses

One page of the organization's wallet ledger

One page of one organization's ledger. The same `{ data, pagination }` envelope `ListInvoicesResponse` uses, and for the same reason: `/v1`'s `/me/wallet/entries` answers with a bare array paged on a timestamp cursor, with no total and no next cursor, so a client can render kernel's default page and has nothing to build a pager out of. **No row carries a running balance, and that is a decision, not an oversight.** `balance_after` is well-defined only against a total order over the whole ledger, folded from one end. This page is a window: it can be the second page, it can be narrowed to one currency, and a wallet holding two currencies has two independent balances that a mixed page interleaves. Any value computed from the rows in hand would be a running total of *this page*, which agrees with the wallet only when the page is the entire ledger -- so it would read as right and be wrong. Kernel could serve a true one (the order in `WalletRepository::list_entries_page` is total, which is the hard half), but it needs a window-function query and a ruling on what the value means on a filtered page. Until then no balance per row is served, and a client must not add the movements up itself.
​WalletEntryResponse[] · required
​PaginationMeta · required

Pagination metadata included in paginated responses


List Subscription Invoices

GET
/api/v2/subscriptions/{id}/invoices

Lists one subscription's invoices, paginated: every invoice with at least one line item billed against this subscription, newest first. Built for a per-service billing tab that needs one subscription's history rather than an organization's whole one (/organizations/{id}/invoices for that). Each row carries due_at, derived the same way listMyInvoices describes. Line items are not included here -- fetch one invoice by id for its lines.

Answers 404 for a subscription in an organization the caller cannot see, identically to one that does not exist -- the same rule reading that subscription directly already follows.

List Subscription Invoices › path Parameters

id
​string · uuid · required · style: simple

List Subscription Invoices › query Parameters

page
​integer · uint32 · min: 0 · style: form

Page number (1-indexed). Defaults to 1.

Page number (1-indexed). Defaults to 1.

Default: 1
page_size
​integer · uint32 · min: 0 · style: form

Number of items per page. Defaults to 20, max 100.

Number of items per page. Defaults to 20, max 100.

Default: 20

List Subscription Invoices › Responses

Invoices

`/v2` DTOs are always named `*Request` / `*Response` (design doc `§43.4`) -- there is no `InvoiceView` here on purpose (`/v1`'s equivalent, `kernel_http_billing::controllers::invoices::InvoiceView`).
​InvoiceSummaryResponse[] · required
​PaginationMeta · required

Pagination metadata included in paginated responses


Get My Wallet

GET
/api/v2/me/wallet

Gets the caller's own wallet: their personal organization's credit balance, one row per currency, in integer cents.

Only non-zero balances are returned: the balance is summed from the ledger, so a wallet that has never been credited, and one whose movements net to exactly zero, both answer with an empty balances list. Treat a currency that is absent as zero -- an empty list is a normal 200, not a sign the wallet does not exist.

The organization behind this balance is never named: it is always the caller's own personal organization, resolved from their token. Reading a shared organization's wallet needs GET /api/v2/organizations/{id}/wallet instead.

Get My Wallet › Responses

The caller's own wallet balances

The caller's own wallet: their personal organization's balance, one row per currency. No `organization_id` field, unlike `OrganizationWalletResponse` -- dashboard never learns which organization backs this balance, only that it is "mine".
​WalletBalanceResponse[] · required

One row per currency the caller's personal organization holds.

A currency with no money in it has no row. An organization that has never been credited, or one whose movements net to exactly zero, answers with an empty list -- this is a normal, fully-formed 200, not a sign that the wallet is missing. Treat an absent currency as zero.


List My Wallet Entries

GET
/api/v2/me/wallet/entries

Lists the caller's own wallet ledger (their personal organization's), newest first, paginated as a { data, pagination } envelope whose total_items counts every entry the filter matches, not only the ones on this page.

Each row is one signed movement in integer cents: positive credits the wallet, negative debits it. Rows carry no running balance; read GET /api/v2/me/wallet for that.

Narrow to one currency with currency (e.g. EUR). A value that is not a three-letter uppercase ISO 4217 code is refused with invalid-field-value rather than ignored.

The organization behind this ledger is never named: it is always the caller's own personal organization. Reading a shared organization's ledger needs GET /api/v2/organizations/{id}/wallet/entries instead.

List My Wallet Entries › query Parameters

currency
​string | null · style: form

Narrows the page to one currency, e.g. EUR.

Optional, and a pure narrowing: omitted, the page carries every currency the organization has ever moved, newest first. A wallet with two currencies has two independent balances, so a page that mixes them is a history and not an account -- a client showing one balance should ask for that currency by name.

A value that is not three uppercase letters is refused with invalid-field-value rather than ignored, for the reason listOrganizationInvoices refuses an unknown status: serving the unfiltered listing would answer a question the caller did not ask.

Narrows the page to one currency, e.g. EUR.

Optional, and a pure narrowing: omitted, the page carries every currency the organization has ever moved, newest first. A wallet with two currencies has two independent balances, so a page that mixes them is a history and not an account -- a client showing one balance should ask for that currency by name.

A value that is not three uppercase letters is refused with invalid-field-value rather than ignored, for the reason listOrganizationInvoices refuses an unknown status: serving the unfiltered listing would answer a question the caller did not ask.

Default: null
page
​integer · uint32 · min: 0 · style: form

Page number (1-indexed). Defaults to 1.

Page number (1-indexed). Defaults to 1.

Default: 1
page_size
​integer · uint32 · min: 0 · style: form

Number of items per page. Defaults to 20, max 100.

Number of items per page. Defaults to 20, max 100.

Default: 20

List My Wallet Entries › Responses

One page of the caller's own wallet ledger

One page of one organization's ledger. The same `{ data, pagination }` envelope `ListInvoicesResponse` uses, and for the same reason: `/v1`'s `/me/wallet/entries` answers with a bare array paged on a timestamp cursor, with no total and no next cursor, so a client can render kernel's default page and has nothing to build a pager out of. **No row carries a running balance, and that is a decision, not an oversight.** `balance_after` is well-defined only against a total order over the whole ledger, folded from one end. This page is a window: it can be the second page, it can be narrowed to one currency, and a wallet holding two currencies has two independent balances that a mixed page interleaves. Any value computed from the rows in hand would be a running total of *this page*, which agrees with the wallet only when the page is the entire ledger -- so it would read as right and be wrong. Kernel could serve a true one (the order in `WalletRepository::list_entries_page` is total, which is the hard half), but it needs a window-function query and a ruling on what the value means on a filtered page. Until then no balance per row is served, and a client must not add the movements up itself.
​WalletEntryResponse[] · required
​PaginationMeta · required

Pagination metadata included in paginated responses


Start My Wallet Top-Up

POST
/api/v2/me/wallet/top-ups

Starts a top-up of the caller's own wallet: creates a hosted checkout at the chosen provider and returns it. Send the customer to checkout_url.

Nothing is credited by this call. The wallet is credited once, when the provider confirms the payment to the kernel; poll GET /api/v2/me/wallet/top-ups/{id} until status is credited (or a terminal failure). Retrying this request after a timeout starts a second, separate checkout -- neither moves money until it is paid.

amount_cents is one of the fixed EUR presets (2500, 5000, 10000, 25000, 100000); anything else is invalid-field-value. A provider that is not configured, or that does not answer, is a 503 and nothing was charged. The wallet is always the caller's personal organization's and cannot be named. Requires the top-up authority on that wallet; a restricted API token needs organizations.wallet.top_up.

Start My Wallet Top-Up › Request Body

Start a top-up of the caller's own wallet.
amount_cents
​integer · int64 · required

Integer euro cents, and one of the fixed presets: 2500, 5000, 10000, 25000 or 100000. Any other amount is refused with invalid-field-value; top-ups are always EUR.

provider
​TopUpProviderRequest · enum · required

A provider a top-up can be started with. Mollie is deliberately absent: a Mollie top-up has no checkout and no settlement path in the kernel.

Enum values:
paypal
nowpayments

Start My Wallet Top-Up › Responses

The new top-up, waiting for payment at checkout_url

One of the caller's own top-ups.
amount_cents
​integer · int64 · required

Integer cents of currency.

created_at
​string · date-time · required
currency
​string · required

ISO 4217. Always EUR today.

expires_at
​string · date-time · required

After this the checkout lapses and the top-up becomes expired.

id
​string · uuid · required
provider
​string · required

paypal or nowpayments. Older rows may say mollie.

​TopUpStatusResponse · required

Where a top-up is. Only credited means money reached the wallet.

checkout_url
​string | null

The provider's hosted page to send the customer to. Present once the checkout exists; absent while creating and on a failed start.


Get Wallet Top-Up Availability

GET
/api/v2/me/wallet/top-ups/availability

Says which providers can take a wallet top-up right now. A provider answering false would refuse POST /api/v2/me/wallet/top-ups with a 503; offer only the ones that answer true, and show no top-up option at all when both are false.

Get Wallet Top-Up Availability › Responses

Which providers are configured and enabled

Which providers can take a top-up right now.
nowpayments
​boolean · required
paypal
​boolean · required

Get My Wallet Top-Up

GET
/api/v2/me/wallet/top-ups/{id}

Gets one of the caller's own top-ups, to poll while the customer pays. credited is the only status that means money reached the wallet; expired, failed and review_required are terminal too.

A top-up belonging to any other account answers 404, exactly as an id that does not exist does.

Get My Wallet Top-Up › path Parameters

id
​string · uuid · required · style: simple

Get My Wallet Top-Up › Responses

The top-up

One of the caller's own top-ups.
amount_cents
​integer · int64 · required

Integer cents of currency.

created_at
​string · date-time · required
currency
​string · required

ISO 4217. Always EUR today.

expires_at
​string · date-time · required

After this the checkout lapses and the top-up becomes expired.

id
​string · uuid · required
provider
​string · required

paypal or nowpayments. Older rows may say mollie.

​TopUpStatusResponse · required

Where a top-up is. Only credited means money reached the wallet.

checkout_url
​string | null

The provider's hosted page to send the customer to. Present once the checkout exists; absent while creating and on a failed start.


Redeem Wallet Code

POST
/api/v2/me/wallet/codes/redeem

Redeems a wallet code into the caller's own wallet and returns the credit it produced.

Send an Idempotency-Key header. Repeating the identical request with the same key answers with the first credit instead of a refusal, so a client that timed out can find out what happened without guessing.

A code credits exactly one wallet, once. Every way a code can fail to redeem -- it does not exist, was already redeemed by anyone, was revoked, has expired, or its group's per-caller limit is reached -- answers with the same invalid-field-value refusal. Distinguishing them would let any signed-in customer walk the wallet-code namespace and learn which codes exist and which giveaway campaigns are still live, the same oracle ZC-0183 closed on the discount-code quote. Requires the top-up authority on the caller's wallet; a restricted API token needs organizations.wallet.top_up.

Redeem Wallet Code › Request Body

code
​string · required

The code as the customer typed it. Case and separators are normalised server-side.

Redeem Wallet Code › Responses

The wallet credit the code produced

The credit a redeemed code produced.
amount_cents
​integer · int64 · required
currency
​string · required
entry_id
​string · uuid · required

The wallet entry the value landed in.


Get My Wallet Preferences

GET
/api/v2/me/wallet/preferences

Gets the caller's own wallet preferences: whether invoices are collected from the wallet automatically, and the wallet's currency. A wallet that has never been configured answers with the defaults (autopay on, EUR).

Get My Wallet Preferences › Responses

The caller's wallet preferences

autopay_enabled
​boolean · required

Whether invoices are collected from the wallet automatically when they fall due.

preferred_currency
​string · required

ISO 4217. Read-only: wallets are EUR-only, so there is nothing to choose.


Update My Wallet Preferences

PATCH
/api/v2/me/wallet/preferences

Turns automatic invoice collection from the caller's own wallet on or off. autopay_enabled is required and is the only writable field; any other field is refused rather than ignored.

Switching autopay on authorizes the platform to spend the wallet on every renewal, so it needs the spending authority (Owner or Admin), not the top-up authority; a restricted API token needs organizations.wallet.spend.

Update My Wallet Preferences › Request Body

The one writable preference. Required, not optional: a PATCH that changes nothing and answers 200 would tell the customer their autopay choice was saved when it was not.
autopay_enabled
​boolean · required

Update My Wallet Preferences › Responses

The saved preferences

autopay_enabled
​boolean · required

Whether invoices are collected from the wallet automatically when they fall due.

preferred_currency
​string · required

ISO 4217. Read-only: wallets are EUR-only, so there is nothing to choose.


List My Free Trials

GET
/api/v2/me/trials

Every free trial the caller's storefront offers, each with whether they may claim it right now.

Refused trials are listed, not hidden. A trial the caller has already used comes back with claimable: false and refusal.code = already_claimed, so the product's trial does not silently vanish from the page once it has been taken. The three refusal codes are already_claimed (permanent), capacity_reached (try later) and not_available (the trial is disabled, expired, or not fully configured). Trials belonging to another storefront are absent entirely -- they were never this caller's.

region_is_fixed says the trial pins its own hardware: do not show a region picker for that row, because the claim will ignore whatever it is sent. Writes nothing, and needs no captcha.

List My Free Trials › Responses

The trials this caller's storefront offers, with a verdict on each


Claim A Free Trial

POST
/api/v2/me/trials

Claims one free trial from GET /api/v2/me/trials. Creates a Subscription in the caller's own personal organization -- the request cannot name an organization -- together with a zero-amount Initial invoice that is settled immediately, which activates the subscription and provisions the service. Nothing is charged and no payment method is required.

auto_renew starts false. A customer who does nothing is never billed: at trial_ends_at the subscription is suspended instead. Turning renewal on through PUT /api/v2/subscriptions/{id}/renewal before that instant is what converts the trial, and billing then starts at trial_ends_at rather than at the claim.

plan, size and price all come from the trial declaration; only name, slug, project_id and region_id are the customer's. project_id may not name a personal organization's games project (403 project-gaming-managed); the gaming dashboard claims through POST /api/v2/me/trials/gaming. region_id is ignored when the trial reports region_is_fixed, and omitting it otherwise places the service server-side.

One trial per product, per account, permanently -- cancelling one does not free the slot. A second attempt answers business-rule-violation with already_claimed. A trial that is not on the caller's storefront answers 404, exactly as an unknown id does. Requires a captcha token when the host has captcha configured for free_trial_claim.

Claim A Free Trial › Request Body

`POST /api/v2/me/trials`'s request, and `POST /api/v2/me/trials/gaming`'s. Deliberately tiny next to `CheckoutRequest`: no organization (always the caller's own), no product or plan (the trial declares them), no size (the plan as authored), no discount code (there is nothing to discount).
name
​string · required

The customer's name for the service they will receive.

slug
​string · required

The URL segment addressing it, unique within its project. Same rules as checkout's: 3-63 characters of lowercase letters, numbers and single hyphens.

trial_price_id
​string · uuid · required

The id from GET /api/v2/me/trials.

project_id
​string | null · uuid

Which project to file it under. Omit for the personal organization's default project, created on first use. Must be omitted on POST /api/v2/me/trials/gaming, which always files into the games project.

region_id
​integer | null · int32

Where to run it.

Ignored when the trial reports region_is_fixed, and omitting it otherwise means "anywhere that fits, cheapest first" -- resolved server-side, never by the client.

Claim A Free Trial › Responses

The running trial subscription and its settled zero invoice

What a claim produced: a running trial and the zero invoice that records it.
​object · required

The zero-amount establishing invoice, in the same shape GET /billing/invoices/{id} returns. Already paid; it is returned so the customer's invoice list and this response cannot describe the same row differently.

​ClaimedTrialSubscriptionResponse · required

Claim A Free Trial For Gaming

POST
/api/v2/me/trials/gaming

The gaming dashboard's trial claim: the same claim as Claim A Free Trial, filed into the caller's personal organization's games project, which it creates on first use and reuses afterwards, beside purchases made through POST /api/v2/checkout/gaming. Claims only trials on products the gaming store sells (any other answers 404, exactly as an unknown id does). Takes the same body; project_id must be omitted, or the claim is refused with 422 gaming-checkout-target-fixed. Requires a captcha token when the host has captcha configured for free_trial_claim.

Claim A Free Trial For Gaming › Request Body

`POST /api/v2/me/trials`'s request, and `POST /api/v2/me/trials/gaming`'s. Deliberately tiny next to `CheckoutRequest`: no organization (always the caller's own), no product or plan (the trial declares them), no size (the plan as authored), no discount code (there is nothing to discount).
name
​string · required

The customer's name for the service they will receive.

slug
​string · required

The URL segment addressing it, unique within its project. Same rules as checkout's: 3-63 characters of lowercase letters, numbers and single hyphens.

trial_price_id
​string · uuid · required

The id from GET /api/v2/me/trials.

project_id
​string | null · uuid

Which project to file it under. Omit for the personal organization's default project, created on first use. Must be omitted on POST /api/v2/me/trials/gaming, which always files into the games project.

region_id
​integer | null · int32

Where to run it.

Ignored when the trial reports region_is_fixed, and omitting it otherwise means "anywhere that fits, cheapest first" -- resolved server-side, never by the client.

Claim A Free Trial For Gaming › Responses

The running trial subscription and its settled zero invoice

What a claim produced: a running trial and the zero invoice that records it.
​object · required

The zero-amount establishing invoice, in the same shape GET /billing/invoices/{id} returns. Already paid; it is returned so the customer's invoice list and this response cannot describe the same row differently.

​ClaimedTrialSubscriptionResponse · required

Get My Standing

GET
/api/v2/me/standing

Gets the caller's own standing (ZC-1324 org trust): whether their personal organization is currently restricted, and why, with the full numeric breakdown -- balance, open reservations, floor, required and available cents, credit limit, open receivables. Always the full detail: the caller reading this route is always their own personal organization's owner. The organization behind it is never named, matching every other /api/v2/me route.

Get My Standing › Responses

The caller's own personal organization's current standing

The full detail view of one organization's standing (`Q121`: the owner, and staff). No `organization_id` field -- every caller of this type already knows which organization it is about from the route it called.
available_cents
​integer · int64 · required
balance_cents
​integer · int64 · required
credit_limit_cents
​integer · int64 · required
currency
​string · required
floor_cents
​integer · int64 · required
open_receivable_cents
​integer · int64 · required
required_cents
​integer · int64 · required
reserved_cents
​integer · int64 · required
restricted
​boolean · required
​

Preview Wallet Code

GET
/api/v2/public/wallet/codes/{code}/preview

Returns the face value, currency and expiry of a redeemable wallet code. Requires no authentication: a claim link is opened by somebody who may not have an account yet, and the page has to be able to say what the code is worth before asking them to make one. The code is normalized (trimmed, upper-cased) exactly as redemption normalizes it. A code that is expired, already redeemed, revoked, or simply does not exist is reported as not found, with no way to tell those cases apart. Rate limited more tightly than ordinary reads, because an endpoint that turns a code into a value is an enumeration oracle.

Preview Wallet Code › path Parameters

code
​string · required · style: simple

The code as printed, in any case and with or without surrounding whitespace. Normalized server side before lookup, the same way redemption normalizes it, so a preview can never say "valid" about a spelling redemption would then reject.

The code as printed, in any case and with or without surrounding whitespace. Normalized server side before lookup, the same way redemption normalizes it, so a preview can never say "valid" about a spelling redemption would then reject.

Preview Wallet Code › Responses

The code's value and expiry

What a visitor holding a code is told about it before signing in. Deliberately three fields. It says what the code is worth and until when, because that is what the claim page and its OG image render; it says nothing about who issued it, who it was issued for, whether anyone has tried it, or what it is attached to. An inactive, redeemed, revoked, or expired code is not described at all -- it is a 404, indistinguishable from a code that never existed.
amount_cents
​integer · int64 · required

Face value in minor units of currency

currency
​string · required
expires_at
​string | null · date-time

When this code stops being redeemable, or null if it does not expire. A code already past this instant is reported as not found rather than returned with a past date.


Capture PayPal Invoice Checkout

GET
/api/v2/public/billing/invoices/{id}/checkout/paypal/capture

PayPal's return URL for an invoice checkout started with POST /api/v2/billing/invoices/{id}/checkout/paypal. PayPal redirects the customer's browser here with the order id as token once they approve; this captures the payment and redirects the browser to the invoice on the dashboard with payment=success, payment=processing (not settled yet; the invoice updates when it does) or payment=failed.

Unauthenticated by necessity: a provider redirect carries no credentials. It captures only the PayPal order stored on that invoice when the checkout was started, and pays the invoice only when PayPal reports the capture completed. An invoice that does not exist and an order that is not the invoice's own are the same 404. Safe to repeat: a refreshed or replayed return pays the invoice once and redirects to success.

Capture PayPal Invoice Checkout › path Parameters

id
​string · uuid · required · style: simple

Capture PayPal Invoice Checkout › query Parameters

token
​string · required · style: form

The PayPal order id, as PayPal names it on the return URL.

The PayPal order id, as PayPal names it on the return URL.

Capture PayPal Invoice Checkout › Responses

Redirects the browser to the invoice on the dashboard, with the outcome in payment

No data returned