From ece77da8ab60aa13354096a18107b76e56adcdad Mon Sep 17 00:00:00 2001 From: Alexandre Bergere Date: Sun, 1 Nov 2026 21:00:59 +0100 Subject: [PATCH] feat: the billing release, against Kaiten 26.10.8 The SDK now models everything Kaiten 26.10.8 publishes: native billing, keyed usage reports and the usage history, entitlement exceptions, and all 117 webhook events, up from 55. - openapi/ is synced from kaitencloud/kaiten@f685ceb8, tag 26.10.8 and kaiten main, by task sync:openapi, and the models regenerated. - The generator names the enums the billing schemas share by property and values, renders nullable and scalar unions, keeps the 1.0.0 names of the webhook payloads the contract renamed ...Delivered, and makes a payload that is a resource plus who wrote the change a subclass of that resource's model. No name 1.0.0 published is gone, and an event's data is still an instance of the class it was. - client.billing, a namespace per resource after sdk-go v0.2.0's naming: subscriptions, invoices, handoff, license_prices, addons, instance_addons, vouchers, instance_vouchers, customers, publishable_keys, customer_sessions and checkout_sessions. public and session reach the routes under /public; the transport checks their credential (pk_, ksc_, kst_) by path, and sends a publishable key in X-Kaiten-Publishable-Key, never as a bearer token. - instances: report_usage_detailed() under an Idempotency-Key, with TransactionIdReusedError; the usage history, walked by afterSeq and exported; entitlement exceptions. license_families.update(), license commercial terms, a customer's billing e-mail, archived metadata fields, and platform.billing.close_periods(). - coverage.yaml maps 210 of the 228 Core operations and all 11 Platform ones. The twelve user and role operations are excluded as console authoring, as kaiten's own coverage test excludes them. - Tests: a contract case for every new operation, the credential checked where the operation's security scheme puts it, nested namespaces in the coverage and README checks, and test_billing.py for what the contract table cannot see. - README, CHANGELOG and version 1.1.0. Signed-off-by: Alexandre Bergere --- CHANGELOG.md | 50 +- README.md | 265 +- openapi/coverage.yaml | 156 +- openapi/openapi.yaml | 40956 +++++++++++++--- openapi/platform-openapi.yaml | 163 +- openapi/source.yaml | 6 +- pyproject.toml | 2 + scripts/generate_models.py | 429 +- src/kaitencloud/__init__.py | 5 +- src/kaitencloud/_async/_http.py | 73 +- src/kaitencloud/_async/client.py | 12 +- src/kaitencloud/_async/resources/__init__.py | 2 + .../_async/resources/billing/__init__.py | 216 + .../_async/resources/billing/addons.py | 349 + .../_async/resources/billing/customers.py | 102 + .../_async/resources/billing/invoices.py | 379 + .../resources/billing/license_prices.py | 192 + .../_async/resources/billing/public.py | 330 + .../_async/resources/billing/sessions.py | 184 + .../_async/resources/billing/subscriptions.py | 346 + .../_async/resources/billing/vouchers.py | 321 + src/kaitencloud/_async/resources/customers.py | 17 +- src/kaitencloud/_async/resources/instances.py | 302 +- .../_async/resources/license_families.py | 39 +- src/kaitencloud/_async/resources/licenses.py | 38 +- .../_async/resources/metadata_fields.py | 13 +- .../_async/resources/platform/__init__.py | 2 + .../_async/resources/platform/billing.py | 39 + src/kaitencloud/_constants.py | 6 + src/kaitencloud/_exceptions.py | 16 +- src/kaitencloud/_models.py | 27 +- src/kaitencloud/_sync/_http.py | 73 +- src/kaitencloud/_sync/client.py | 12 +- src/kaitencloud/_sync/resources/__init__.py | 2 + .../_sync/resources/billing/__init__.py | 218 + .../_sync/resources/billing/addons.py | 347 + .../_sync/resources/billing/customers.py | 104 + .../_sync/resources/billing/invoices.py | 379 + .../_sync/resources/billing/license_prices.py | 194 + .../_sync/resources/billing/public.py | 324 + .../_sync/resources/billing/sessions.py | 186 + .../_sync/resources/billing/subscriptions.py | 340 + .../_sync/resources/billing/vouchers.py | 321 + src/kaitencloud/_sync/resources/customers.py | 17 +- src/kaitencloud/_sync/resources/instances.py | 302 +- .../_sync/resources/license_families.py | 39 +- src/kaitencloud/_sync/resources/licenses.py | 38 +- .../_sync/resources/metadata_fields.py | 13 +- .../_sync/resources/platform/__init__.py | 2 + .../_sync/resources/platform/billing.py | 41 + src/kaitencloud/_utils.py | 73 +- src/kaitencloud/_version.py | 2 +- src/kaitencloud/types/__init__.py | 466 + src/kaitencloud/types/_core.py | 6535 ++- src/kaitencloud/types/_params.py | 38 + src/kaitencloud/types/_webhooks.py | 1779 +- tests/contract.py | 11 +- tests/test_billing.py | 200 + tests/test_contract.py | 766 +- tests/test_coverage.py | 16 +- tests/test_models.py | 1 + tests/test_readme.py | 31 +- 62 files changed, 49468 insertions(+), 8439 deletions(-) create mode 100644 src/kaitencloud/_async/resources/billing/__init__.py create mode 100644 src/kaitencloud/_async/resources/billing/addons.py create mode 100644 src/kaitencloud/_async/resources/billing/customers.py create mode 100644 src/kaitencloud/_async/resources/billing/invoices.py create mode 100644 src/kaitencloud/_async/resources/billing/license_prices.py create mode 100644 src/kaitencloud/_async/resources/billing/public.py create mode 100644 src/kaitencloud/_async/resources/billing/sessions.py create mode 100644 src/kaitencloud/_async/resources/billing/subscriptions.py create mode 100644 src/kaitencloud/_async/resources/billing/vouchers.py create mode 100644 src/kaitencloud/_async/resources/platform/billing.py create mode 100644 src/kaitencloud/_sync/resources/billing/__init__.py create mode 100644 src/kaitencloud/_sync/resources/billing/addons.py create mode 100644 src/kaitencloud/_sync/resources/billing/customers.py create mode 100644 src/kaitencloud/_sync/resources/billing/invoices.py create mode 100644 src/kaitencloud/_sync/resources/billing/license_prices.py create mode 100644 src/kaitencloud/_sync/resources/billing/public.py create mode 100644 src/kaitencloud/_sync/resources/billing/sessions.py create mode 100644 src/kaitencloud/_sync/resources/billing/subscriptions.py create mode 100644 src/kaitencloud/_sync/resources/billing/vouchers.py create mode 100644 src/kaitencloud/_sync/resources/platform/billing.py create mode 100644 tests/test_billing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ca053cd..f5261c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,62 @@ All notable changes to this project are documented here. The format follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): only a major version changes the public API, a minor version adds to it, and a patch version fixes it. -## [Unreleased] +## [1.1.0] - 2026-11-01 + +Generated and tested against the contract of Kaiten 26.10.8, +[`kaitencloud/kaiten@f685ceb8`](https://github.com/kaitencloud/kaiten/commit/f685ceb86bfeb5f6110663662fb955336ce1a1a4): +use it with Kaiten 26.10.8 or later. Nothing 1.0.0 published was removed or renamed: methods +only gained optional arguments. ### Added +- `client.billing`, a namespace per billing resource: the organization's settings, capabilities, + health, provider sync and period close; `subscriptions` -- subscribe, change the provider and + terms, cancel, reactivate, schedule and drop a plan change, the upcoming invoice, an instance's + invoices; `invoices` -- list, export, void, recompose, mark paid, write off, release a hold, + retry a push, sync, and a line's usage reports; the accounting `handoff`, claimed and + acknowledged; `license_prices` and the invoice preview; `addons`, with their families, grants, + prices and the license families they fit; `instance_addons`; `vouchers`, their redemptions and + lookup, and `instance_vouchers`; `customers` -- a customer's billing, payment method and + provider portal; `publishable_keys`, `customer_sessions` and `checkout_sessions`. +- `client.billing.public` and `client.billing.session`: what a publishable key (`pk_...`), an + anonymous checkout's token (`ksc_...`) and a customer session (`kst_...`) reach. A route under + `/public` takes one of them and refuses any other; the SDK refuses the mix-up before sending, + and sends a publishable key in `X-Kaiten-Publishable-Key`, never as a bearer token. +- `instances.report_usage_detailed()`: a usage report under an idempotency key, + `transaction_id`, which Kaiten applies at most once. It returns a `UsageReportResult`: the + usage, whether the report was a replay, and whether its metadata was too large to store. A key + spent on another report raises `TransactionIdReusedError`. +- The usage history: `instances.list_usage_reports()`, `instances.export_usage_reports()` and + `instances.export_organization_usage_reports()`. +- Entitlement exceptions: `instances.list_entitlement_exceptions()`, + `instances.create_entitlement_exception()` and `instances.revoke_entitlement_exception()`. +- `license_families.update()`: whether a family is public, its fallback family, and how its + trials end. +- A license version's commercial terms on `licenses.create()` and `licenses.update()`: + `pricing_type`, `trial_period_days`, `requires_payment_method` and `self_serve_cta_url`. A + customer's `billing_email` on `customers.create()` and `customers.update()`. + `metadata_fields.list(include_archived=True)`. +- `platform.billing.close_periods()`, the Platform API's eleventh operation. +- Models for every new response, and for the 62 webhook events published since 1.0.0: all 117 + are typed. An event carries the delivery's trace `headers`, and its `data` who wrote the change + (`via`, `checkout_session_id`, `customer_session_id`); that `data` is still an instance of the + class 1.0.0 typed it with. - `feature_flags.targeting_context()`, `feature_flags.lint_rule()` and `feature_flags.test_rule()`: the facts a targeting rule can read, the check the API runs before accepting one, and a dry run of a rule against a customer or an instance. - `feature_flags.update(..., slug=...)` renames a flag. +### Changed + +- The models require what Kaiten 26.10.8 always sends and older deployments do not: + `EntitlementUsage.source`, `AuditTrail.actor`, `ConnectorSettings.configured`, the trial and + catalog settings of `LicenseFamilyView`, and the period rollover event's `reason`. From an older + Kaiten, those reads raise `APIResponseValidationError`, and that event `WebhookPayloadError`. +- `ResetAnchor` gains `BILLING_PERIOD`, and `Scope` the `read:` and `write:` scopes of `addons`, + `billing`, `customer_sessions`, `profile`, `publishable_keys`, `users`, `voucher_redemptions` and + `vouchers`. + ### Fixed - `feature_flags.update` sends the flag's slug. Without it, Kaiten blanked the slug and the flag @@ -66,4 +113,5 @@ The first release of the Kaiten Python SDK, generated and tested against the con - Flag evaluation. Kaiten implements OFREP, so flags are evaluated with an OpenFeature SDK and its generic OFREP provider (`openfeature-provider-ofrep`); the README shows how. +[1.1.0]: https://github.com/kaitencloud/sdk-python/releases/tag/v1.1.0 [1.0.0]: https://github.com/kaitencloud/sdk-python/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 57b58b6..ef17b82 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Kaiten Python SDK **The official Python client for [Kaiten](https://kaiten.sh), the control plane of your SaaS:
-customers, instances, licenses, entitlements, usage metering, feature flags and releases.** +customers, instances, licenses, entitlements, usage metering, billing, feature flags and releases.** [![PyPI](https://img.shields.io/pypi/v/kaitencloud.svg?label=pypi&color=4c1)](https://pypi.org/project/kaitencloud/) [![Python](https://img.shields.io/pypi/pyversions/kaitencloud.svg)](https://pypi.org/project/kaitencloud/) @@ -42,10 +42,10 @@ client.instances.report_usage("acme-production", "seats", 1) problem: `code`, `detail`, `error_id`. - **Both APIs.** The Core API your product talks to, and the Platform API that provisions organizations and mints their tokens. -- **Webhooks included.** Svix signatures verified, all 55 event types parsed into models. +- **Webhooks included.** Svix signatures verified, all 117 event types parsed into models. - **Flags through OpenFeature.** Kaiten speaks OFREP, so flags are defined here and evaluated with the [OpenFeature SDK](#feature-flags) — a vendor-neutral API, not ours. -- **Contract-tested.** All 111 operations the SDK wraps are exercised against the contract, and +- **Contract-tested.** All 221 operations the SDK wraps are exercised against the contract, and every request body is validated against the API's own schema. ## Contents @@ -54,7 +54,7 @@ client.instances.report_usage("acme-production", "seats", 1) - [Quick start](#quick-start) - [Configuration](#configuration) - [Guide](#guide) - - [Catalog and licenses](#catalog-and-licenses) · [Customers and instances](#customers-and-instances) · [Usage metering](#usage-metering) + - [Catalog and licenses](#catalog-and-licenses) · [Customers and instances](#customers-and-instances) · [Usage metering](#usage-metering) · [Billing](#billing) - [Feature flags](#feature-flags) · [Releases and deployment zones](#releases-and-deployment-zones) · [Webhooks](#webhooks) - [The Platform API](#the-platform-api) - [The playbook](#the-playbook) @@ -131,6 +131,7 @@ Organization you: a SaaS vendor using Kaiten ├── License client.licenses the contract it runs under, granting values │ client.entitlements ...of the entitlements in your catalog ├── Usage client.instances.report_usage() what it consumed + ├── Subscription client.billing.subscriptions how it is billed, invoice after invoice └── Zone client.deployment_zones where it runs, and the release deployed there client.releases → client.components ``` @@ -160,8 +161,15 @@ anything when one ends up in the wrong place. | Organization token | `ksh_...` | `KaitenClient` | | Identity-provider JWT | `eyJ...` | `KaitenClient` | | Platform credential | `ksm_...` | `KaitenPlatformClient` | +| Publishable key | `pk_...` | `KaitenClient`, for `client.billing.public` | +| Customer session | `kst_...` | `KaitenClient`, for `client.billing.session` | +| Anonymous checkout session | `ksc_...` | `KaitenClient`, to read or confirm that checkout | | Webhook signing secret | `whsec_...` | `kaitencloud.webhooks` | +The routes under `/public` — a pricing page's, a customer portal's — each take one of the three +public credentials and refuse any other, so the SDK refuses the mix-up before sending. It sends a +publishable key in `X-Kaiten-Publishable-Key`, as the API requires, never as a bearer token. + A credential that rotates can be supplied as a callable — it is read before every request, so there is no client to rebuild: @@ -264,6 +272,29 @@ except ThresholdExceededError as error: `behavior="append"` (the default) folds the value into the total through the entitlement's aggregation method; `behavior="set"` replaces the total, which is also how to correct it. +A report retried after a lost response would count twice, so the SDK never retries one. To make +resending safe — after a timeout, or from a queue that delivers at least once — report under +the id of the business event it counts. Kaiten applies a report at most once per key, instance +and entitlement within its idempotency window (35 days by default): + +```python +result = client.instances.report_usage_detailed( + "acme-production", "tokens", 1200, transaction_id="llm-call-9f2c:tokens" +) +if result.replayed: + print("Counted already: nothing was added") +``` + +The same key with another value raises `TransactionIdReusedError`. Every accepted report is kept +in the usage history, with the counter before and after it: + +```python +for report in client.instances.list_usage_reports("acme-production", "tokens", limit=100): + print(report.report_seq, report.reported_at, report.delta, report.value_after) + +csv = client.instances.export_usage_reports("acme-production", "tokens", format="csv") +``` + `kaitencloud.usage` computes the same boundaries the server enforces, to render a meter or check a report before sending it: @@ -279,6 +310,55 @@ usage.remaining(included, used, grant.limit_cap_exceeded_overage_percent) # lef usage.percentage_used(included, used) # passes 100 once the allowance is in use ``` +### Billing + +`client.billing` composes invoices from what you sell and what your customers used. Prices belong +to license versions; a subscription pins an instance to one of them, and each period closes into +an invoice. Most of it requires billing to be enabled for the organization: +`client.billing.get_capabilities()` says whether it is, and why not. + +```python +price = client.billing.license_prices.create( + "growth-v2", + billing_model="FLAT_FEE", + currency="EUR", + unit_amount_decimal="4900", # minor units: 49.00 EUR + billing_period="MONTHLY", + is_default=True, +) + +client.billing.subscriptions.subscribe("acme-production", base_price_id=price.id) +client.billing.instance_addons.attach("acme-production", "extra-seats-v1", quantity=2) +client.billing.instance_vouchers.redeem("acme-production", "WELCOME-2026") +``` + +Each subscription says who issues and collects its invoices: `NOOP`, your organization itself, or +a connected payment provider. Invoices of your own reach your accounting system through the +handoff queue, claimed in leased batches and acknowledged one by one — a lost acknowledgement is +claimed again, so book each invoice once by its id: + +```python +claim = client.billing.handoff.claim(limit=25) +for invoice in claim.invoices: + number = book(invoice) # your accounting system + client.billing.handoff.ack(invoice.id, lease_id=claim.lease_id, external_reference=number) +``` + +A pricing page reads the public catalog with a publishable key, and a customer's own billing page +acts with a customer session your backend mints for it: + +```python +session = client.billing.customer_sessions.create( + customer_slug="acme", instance_slug="acme-production" +) +hand_to_the_page(session.token) # kst_..., shown this once + +storefront = KaitenClient(token="pk_...") +catalog = storefront.billing.public.get_catalog() +``` + +Amounts are decimal strings in minor units, exactly as Kaiten stores them. + ### Feature flags Define flags with the builders in `kaitencloud.targeting`. Targeting rules are CEL expressions, @@ -493,7 +573,8 @@ Every `list()` returns the **whole** collection: the SDK walks the cursor at the size the API accepts. If the API ever reports more rows without a usable cursor, `list()` raises `PaginationError` rather than return a list it knows is partial. -The audit trail grows without bound, so it takes a ceiling instead: +Collections that grow without bound — the audit trail, invoices, the usage history — also +take a ceiling: ```python entries = client.instances.list_audit_trails( @@ -516,7 +597,8 @@ KaitenError │ ├── PermissionDeniedError 403 a scope is missing │ ├── NotFoundError 404 │ ├── ConflictError 409 -│ │ └── ThresholdExceededError a usage report refused at the cap +│ │ ├── ThresholdExceededError a usage report refused at the cap +│ │ └── TransactionIdReusedError a usage report key spent on another report │ ├── UnprocessableEntityError 422 │ ├── RateLimitError 429 │ └── InternalServerError 5xx @@ -551,8 +633,8 @@ backoff and jitter, honouring `Retry-After` — but only when repeating it is sa | Request | Retried | | --- | --- | | `GET`, `PUT`, `DELETE` | ✓ | -| Instance status and lifecycle updates, metadata dry runs and reorders, `organizations.ensure()`, `connectors.register()` | ✓ — they converge | -| Any other `POST` or `PATCH`: creations, usage reports, upserts | only if the connection never opened | +| Instance status and lifecycle updates, add-on quantities, metadata dry runs and reorders, invoice previews, voucher lookups and checks, invoice voids and write-offs, handoff acknowledgements, payment-method confirmations, revocations, `organizations.ensure()`, `connectors.register()` | ✓ — they converge | +| Any other `POST` or `PATCH`: creations, usage reports — keyed or not — handoff claims, upserts | only if the connection never opened | | A `401` or `403` | never | Tune it per client, or per call with `with_options()`, which shares the connection pool: @@ -595,10 +677,158 @@ logging.getLogger("kaitencloud").setLevel(logging.INFO) ## API reference -The Core API clients cover 101 of its 107 operations. Of the six left out, four are a signed-in -person's own notification feed and preferences — which an API token has no equivalent of — and -two are the OFREP evaluation endpoints, which an [OpenFeature provider](#feature-flags) calls. The Platform API clients cover -all 10 of its operations. The async clients have the same methods. +The Core API clients cover 210 of its 228 operations. Of the eighteen left out, twelve administer +the people of an organization and their roles, which the console and the operational CLI do; four +are a signed-in person's own notification feed and preferences — which an API token has no +equivalent of — and two are the OFREP evaluation endpoints, which an +[OpenFeature provider](#feature-flags) calls. The Platform API clients cover all 11 of its +operations. The async clients have the same methods. + +
+client.billing · client.billing.subscriptions · client.billing.invoices · client.billing.handoff + +| Method | Operation | +| --- | --- | +| `client.billing.get_settings()` | `GET /billing/settings` | +| `client.billing.get_capabilities()` | `GET /billing/capabilities` | +| `client.billing.get_health()` | `GET /billing/health` | +| `client.billing.update_settings()` | `PUT /billing/settings` | +| `client.billing.sync()` | `POST /billing/sync` | +| `client.billing.close_periods()` | `POST /billing/close-periods` | +| `client.billing.subscriptions.get()` | `GET /instances/{instanceSlug}/billing` | +| `client.billing.subscriptions.subscribe()` | `POST /instances/{instanceSlug}/billing` | +| `client.billing.subscriptions.update()` | `PATCH /instances/{instanceSlug}/billing` | +| `client.billing.subscriptions.cancel()` | `POST /instances/{instanceSlug}/billing/cancel` | +| `client.billing.subscriptions.reactivate()` | `POST /instances/{instanceSlug}/billing/reactivate` | +| `client.billing.subscriptions.schedule_plan_change()` | `PUT /instances/{instanceSlug}/billing/scheduled-change` | +| `client.billing.subscriptions.cancel_plan_change()` | `DELETE /instances/{instanceSlug}/billing/scheduled-change` | +| `client.billing.subscriptions.get_upcoming_invoice()` | `GET /instances/{instanceSlug}/billing/upcoming-invoice` | +| `client.billing.subscriptions.list_invoices()` | `GET /instances/{instanceSlug}/invoices` | +| `client.billing.invoices.list()` | `GET /invoices` | +| `client.billing.invoices.get()` | `GET /invoices/{invoiceId}` | +| `client.billing.invoices.export()` | `GET /invoices/export` | +| `client.billing.invoices.void()` | `POST /invoices/{invoiceId}/void` | +| `client.billing.invoices.recompose()` | `POST /invoices/{invoiceId}/recompose` | +| `client.billing.invoices.mark_paid()` | `POST /invoices/{invoiceId}/mark-paid` | +| `client.billing.invoices.write_off()` | `POST /invoices/{invoiceId}/write-off` | +| `client.billing.invoices.release_hold()` | `POST /invoices/{invoiceId}/release-hold` | +| `client.billing.invoices.retry_push()` | `POST /invoices/{invoiceId}/retry-push` | +| `client.billing.invoices.sync()` | `POST /invoices/{invoiceId}/sync` | +| `client.billing.invoices.list_line_reports()` | `GET /invoices/{invoiceId}/lines/{lineId}/reports` | +| `client.billing.handoff.list()` | `GET /billing/handoff` | +| `client.billing.handoff.claim()` | `POST /billing/handoff/claim` | +| `client.billing.handoff.ack()` | `POST /billing/handoff/{invoiceId}/ack` | + +
+ +
+client.billing.license_prices · client.billing.addons + +| Method | Operation | +| --- | --- | +| `client.billing.license_prices.list()` | `GET /licenses/{licenseSlug}/prices` | +| `client.billing.license_prices.get()` | `GET /licenses/{licenseSlug}/prices/{priceId}` | +| `client.billing.license_prices.create()` | `POST /licenses/{licenseSlug}/prices` | +| `client.billing.license_prices.update()` | `PATCH /licenses/{licenseSlug}/prices/{priceId}` | +| `client.billing.license_prices.deprecate()` | `POST /licenses/{licenseSlug}/prices/{priceId}/deprecate` | +| `client.billing.license_prices.preview_invoice()` | `POST /licenses/{licenseSlug}/invoice-preview` | +| `client.billing.addons.list()` | `GET /addons` | +| `client.billing.addons.get()` | `GET /addons/{addonSlug}` | +| `client.billing.addons.create()` | `POST /addons` | +| `client.billing.addons.update()` | `PUT /addons/{addonSlug}` | +| `client.billing.addons.delete()` | `DELETE /addons/{addonSlug}` | +| `client.billing.addons.publish()` | `POST /addons/{addonSlug}/publish` | +| `client.billing.addons.archive()` | `POST /addons/{addonSlug}/archive` | +| `client.billing.addons.unarchive()` | `POST /addons/{addonSlug}/unarchive` | +| `client.billing.addons.list_compatibility()` | `GET /addons/{addonSlug}/compatible-license-families` | +| `client.billing.addons.set_compatibility()` | `PUT /addons/{addonSlug}/compatible-license-families/{familySlug}` | +| `client.billing.addons.remove_compatibility()` | `DELETE /addons/{addonSlug}/compatible-license-families/{familySlug}` | +| `client.billing.addons.list_entitlements()` | `GET /addons/{addonSlug}/entitlements` | +| `client.billing.addons.get_entitlement()` | `GET /addons/{addonSlug}/entitlements/{entitlementSlug}` | +| `client.billing.addons.assign_entitlement()` | `POST /addons/{addonSlug}/entitlements` | +| `client.billing.addons.update_entitlement()` | `PUT /addons/{addonSlug}/entitlements/{entitlementSlug}` | +| `client.billing.addons.unassign_entitlement()` | `DELETE /addons/{addonSlug}/entitlements/{entitlementSlug}` | +| `client.billing.addons.list_prices()` | `GET /addons/{addonSlug}/prices` | +| `client.billing.addons.create_price()` | `POST /addons/{addonSlug}/prices` | +| `client.billing.addons.deprecate_price()` | `POST /addons/{addonSlug}/prices/{priceId}/deprecate` | +| `client.billing.addons.list_families()` | `GET /addon-families` | +| `client.billing.addons.get_family()` | `GET /addon-families/{familySlug}` | +| `client.billing.addons.update_family()` | `PATCH /addon-families/{familySlug}` | + +
+ +
+client.billing.instance_addons · client.billing.instance_vouchers · client.billing.vouchers + +| Method | Operation | +| --- | --- | +| `client.billing.instance_addons.list()` | `GET /instances/{instanceSlug}/addons` | +| `client.billing.instance_addons.attach()` | `POST /instances/{instanceSlug}/addons` | +| `client.billing.instance_addons.set_quantity()` | `PATCH /instances/{instanceSlug}/addons/{addonSlug}` | +| `client.billing.instance_addons.detach()` | `DELETE /instances/{instanceSlug}/addons/{addonSlug}` | +| `client.billing.instance_vouchers.list()` | `GET /instances/{instanceSlug}/vouchers` | +| `client.billing.instance_vouchers.redeem()` | `POST /instances/{instanceSlug}/vouchers/redeem` | +| `client.billing.instance_vouchers.revoke()` | `POST /instances/{instanceSlug}/vouchers/{instanceVoucherId}/revoke` | +| `client.billing.vouchers.list()` | `GET /vouchers` | +| `client.billing.vouchers.get()` | `GET /vouchers/{voucherId}` | +| `client.billing.vouchers.create()` | `POST /vouchers` | +| `client.billing.vouchers.update()` | `PUT /vouchers/{voucherId}` | +| `client.billing.vouchers.delete()` | `DELETE /vouchers/{voucherId}` | +| `client.billing.vouchers.publish()` | `POST /vouchers/{voucherId}/publish` | +| `client.billing.vouchers.archive()` | `POST /vouchers/{voucherId}/archive` | +| `client.billing.vouchers.list_redemptions()` | `GET /vouchers/{voucherId}/redemptions` | +| `client.billing.vouchers.lookup()` | `POST /vouchers/lookup` | +| `client.billing.vouchers.validate()` | `POST /vouchers/validate` | + +
+ +
+client.billing.customers · client.billing.customer_sessions · client.billing.publishable_keys · client.billing.checkout_sessions + +| Method | Operation | +| --- | --- | +| `client.billing.customers.get()` | `GET /customers/{customerSlug}/billing` | +| `client.billing.customers.detach_payment_method()` | `DELETE /customers/{customerSlug}/billing/payment-method` | +| `client.billing.customers.create_payment_method_session()` | `POST /customers/{customerSlug}/billing/payment-method-session` | +| `client.billing.customers.complete_payment_method_session()` | `POST /customers/{customerSlug}/billing/payment-method-session/{sessionId}/complete` | +| `client.billing.customers.create_portal_session()` | `POST /customers/{customerSlug}/billing/portal-session` | +| `client.billing.customer_sessions.create()` | `POST /customer-sessions` | +| `client.billing.customer_sessions.revoke()` | `POST /customer-sessions/{sessionId}/revoke` | +| `client.billing.publishable_keys.list()` | `GET /publishable-keys` | +| `client.billing.publishable_keys.create()` | `POST /publishable-keys` | +| `client.billing.publishable_keys.update()` | `PATCH /publishable-keys/{keyId}` | +| `client.billing.publishable_keys.revoke()` | `POST /publishable-keys/{keyId}/revoke` | +| `client.billing.checkout_sessions.list()` | `GET /checkout-sessions` | +| `client.billing.checkout_sessions.get()` | `GET /checkout-sessions/{sessionId}` | + +
+ +
+client.billing.public · client.billing.session — with a public credential + +`client.billing.public` takes a publishable key (`pk_...`), and an anonymous checkout's own +session token (`ksc_...`) to read or confirm it; `client.billing.session` takes a customer +session (`kst_...`). Build the client with that token. + +| Method | Operation | +| --- | --- | +| `client.billing.public.get_catalog()` | `GET /public/catalog` | +| `client.billing.public.create_checkout_session()` | `POST /public/checkout-sessions` | +| `client.billing.public.get_checkout_session()` | `GET /public/checkout-sessions/{sessionId}` | +| `client.billing.public.confirm_checkout_session()` | `POST /public/checkout-sessions/{sessionId}/confirm` | +| `client.billing.session.get_portal()` | `GET /public/session/portal` | +| `client.billing.session.list_invoices()` | `GET /public/session/invoices` | +| `client.billing.session.checkout()` | `POST /public/session/checkout` | +| `client.billing.session.cancel()` | `POST /public/session/billing/cancel` | +| `client.billing.session.reactivate()` | `POST /public/session/billing/reactivate` | +| `client.billing.session.set_addon_quantity()` | `PUT /public/session/addons/{addonSlug}` | +| `client.billing.session.create_payment_method_session()` | `POST /public/session/billing/payment-method-session` | +| `client.billing.session.complete_payment_method_session()` | `POST /public/session/billing/payment-method-session/{sessionId}/complete` | +| `client.billing.session.create_portal_session()` | `POST /public/session/billing/portal-session` | +| `client.billing.session.validate_voucher()` | `POST /public/session/vouchers/validate` | +| `client.billing.session.redeem_voucher()` | `POST /public/session/vouchers/redeem` | + +
client.components · client.connectors · client.customers @@ -690,6 +920,13 @@ all 10 of its operations. The async clients have the same methods. | `client.instances.list_usage()` | `GET /instances/{instanceSlug}/entitlements/usage` | | `client.instances.get_usage()` | `GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage` | | `client.instances.report_usage()` | `POST /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage` | +| `client.instances.report_usage_detailed()` | `POST /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage` | +| `client.instances.list_usage_reports()` | `GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage/reports` | +| `client.instances.export_usage_reports()` | `GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage/reports/export` | +| `client.instances.export_organization_usage_reports()` | `GET /usage/reports/export` | +| `client.instances.list_entitlement_exceptions()` | `GET /instances/{instanceSlug}/entitlement-exceptions` | +| `client.instances.create_entitlement_exception()` | `POST /instances/{instanceSlug}/entitlement-exceptions` | +| `client.instances.revoke_entitlement_exception()` | `DELETE /instances/{instanceSlug}/entitlement-exceptions/{exceptionId}` | | `client.instances.get_integration()` | `GET /instances/{instanceSlug}/integrations/{integrationName}` | | `client.instances.create_integration()` | `POST /instances/{instanceSlug}/integrations/{integrationName}` | | `client.instances.update_integration()` | `PUT /instances/{instanceSlug}/integrations/{integrationName}` | @@ -702,12 +939,13 @@ all 10 of its operations. The async clients have the same methods.
-client.licenses · client.metadata_fields +client.license_families · client.licenses · client.metadata_fields | Method | Operation | | --- | --- | | `client.license_families.list()` | `GET /license-families` | | `client.license_families.get()` | `GET /license-families/{familySlug}` | +| `client.license_families.update()` | `PATCH /license-families/{familySlug}` | | `client.licenses.list()` | `GET /licenses` | | `client.licenses.get()` | `GET /licenses/{licenseSlug}` | | `client.licenses.create()` | `POST /licenses` | @@ -756,6 +994,7 @@ all 10 of its operations. The async clients have the same methods. | Method | Operation | | --- | --- | | `platform.me()` | `GET /platform/me` | +| `platform.billing.close_periods()` | `POST /platform/organizations/{orgId}/billing/close-periods` | | `platform.connectors.register()` | `POST /platform/connectors` | | `platform.organizations.ensure()` | `POST /platform/organizations` | | `platform.organizations.get()` | `GET /platform/organizations/{orgId}` | diff --git a/openapi/coverage.yaml b/openapi/coverage.yaml index 273d5e0..1367a8a 100644 --- a/openapi/coverage.yaml +++ b/openapi/coverage.yaml @@ -11,6 +11,132 @@ core: operations: + # billing: settings, capabilities and the payment providers + getBillingSettings: billing.get_settings + updateBillingSettings: billing.update_settings + getBillingCapabilities: billing.get_capabilities + getBillingHealth: billing.get_health + syncBillingProvider: billing.sync + closeBillingPeriods: billing.close_periods + + # billing: add-ons, their families, grants, prices and compatibility + listAddons: billing.addons.list + getAddon: billing.addons.get + createAddon: billing.addons.create + updateAddon: billing.addons.update + deleteAddon: billing.addons.delete + publishAddon: billing.addons.publish + archiveAddon: billing.addons.archive + unarchiveAddon: billing.addons.unarchive + listAddonCompatibility: billing.addons.list_compatibility + setAddonCompatibility: billing.addons.set_compatibility + removeAddonCompatibility: billing.addons.remove_compatibility + listAddonEntitlements: billing.addons.list_entitlements + getAddonEntitlement: billing.addons.get_entitlement + assignAddonEntitlement: billing.addons.assign_entitlement + updateAddonEntitlement: billing.addons.update_entitlement + unassignAddonEntitlement: billing.addons.unassign_entitlement + listAddonPrices: billing.addons.list_prices + createAddonPrice: billing.addons.create_price + deprecateAddonPrice: billing.addons.deprecate_price + listAddonFamilies: billing.addons.list_families + getAddonFamily: billing.addons.get_family + updateAddonFamily: billing.addons.update_family + + # billing: anonymous checkouts, customer sessions and publishable keys + listCheckoutSessions: billing.checkout_sessions.list + getCheckoutSessionAdmin: billing.checkout_sessions.get + createCustomerSession: billing.customer_sessions.create + revokeCustomerSession: billing.customer_sessions.revoke + listPublishableKeys: billing.publishable_keys.list + createPublishableKey: billing.publishable_keys.create + updatePublishableKey: billing.publishable_keys.update + revokePublishableKey: billing.publishable_keys.revoke + + # billing: a customer's payment method and provider portal + getCustomerBilling: billing.customers.get + detachPaymentMethod: billing.customers.detach_payment_method + createPaymentMethodSession: billing.customers.create_payment_method_session + completePaymentMethodSession: billing.customers.complete_payment_method_session + createPortalSession: billing.customers.create_portal_session + + # billing: the accounting handoff + listHandoff: billing.handoff.list + claimHandoff: billing.handoff.claim + ackHandoff: billing.handoff.ack + + # billing: an instance's add-ons and redeemed vouchers + listInstanceAddons: billing.instance_addons.list + attachInstanceAddon: billing.instance_addons.attach + setInstanceAddonQuantity: billing.instance_addons.set_quantity + detachInstanceAddon: billing.instance_addons.detach + listInstanceVouchers: billing.instance_vouchers.list + redeemVoucher: billing.instance_vouchers.redeem + revokeInstanceVoucher: billing.instance_vouchers.revoke + + # billing: invoices + listInvoices: billing.invoices.list + getInvoice: billing.invoices.get + exportInvoices: billing.invoices.export + voidInvoice: billing.invoices.void + recomposeInvoice: billing.invoices.recompose + markInvoicePaid: billing.invoices.mark_paid + writeOffInvoice: billing.invoices.write_off + releaseInvoiceHold: billing.invoices.release_hold + retryInvoicePush: billing.invoices.retry_push + syncInvoice: billing.invoices.sync + listInvoiceLineReports: billing.invoices.list_line_reports + + # billing: the prices of license versions + listLicensePrices: billing.license_prices.list + getLicensePrice: billing.license_prices.get + createLicensePrice: billing.license_prices.create + updateLicensePrice: billing.license_prices.update + deprecateLicensePrice: billing.license_prices.deprecate + previewLicenseInvoice: billing.license_prices.preview_invoice + + # billing: what a publishable key (pk_) and an anonymous checkout's token (ksc_) reach + getPublicCatalog: billing.public.get_catalog + createCheckoutSession: billing.public.create_checkout_session + getCheckoutSession: billing.public.get_checkout_session + confirmCheckoutSession: billing.public.confirm_checkout_session + + # billing: what a customer session (kst_) reaches + getSessionPortal: billing.session.get_portal + listSessionInvoices: billing.session.list_invoices + createSessionCheckout: billing.session.checkout + cancelSessionSubscription: billing.session.cancel + reactivateSessionSubscription: billing.session.reactivate + setSessionAddonQuantity: billing.session.set_addon_quantity + createSessionPaymentMethodSession: billing.session.create_payment_method_session + completeSessionPaymentMethodSession: billing.session.complete_payment_method_session + createSessionPortalSession: billing.session.create_portal_session + validateSessionVoucher: billing.session.validate_voucher + redeemSessionVoucher: billing.session.redeem_voucher + + # billing: an instance's subscription + getInstanceBilling: billing.subscriptions.get + subscribeInstance: billing.subscriptions.subscribe + updateInstanceBilling: billing.subscriptions.update + cancelSubscription: billing.subscriptions.cancel + reactivateSubscription: billing.subscriptions.reactivate + schedulePlanChange: billing.subscriptions.schedule_plan_change + cancelPlanChange: billing.subscriptions.cancel_plan_change + getUpcomingInvoice: billing.subscriptions.get_upcoming_invoice + listInstanceInvoices: billing.subscriptions.list_invoices + + # billing: vouchers + listVouchers: billing.vouchers.list + getVoucher: billing.vouchers.get + createVoucher: billing.vouchers.create + updateVoucher: billing.vouchers.update + deleteVoucher: billing.vouchers.delete + publishVoucher: billing.vouchers.publish + archiveVoucher: billing.vouchers.archive + listVoucherRedemptions: billing.vouchers.list_redemptions + lookupVoucher: billing.vouchers.lookup + validateVoucher: billing.vouchers.validate + # components list-components: components.list get-component: components.get @@ -86,7 +212,13 @@ core: getAuditTrails: instances.list_audit_trails getEntitlementsUsageMetrics: instances.list_usage getEntitlementUsageMetrics: instances.get_usage - reportEntitlementUsageMetric: instances.report_usage + reportEntitlementUsageMetric: [instances.report_usage, instances.report_usage_detailed] + listUsageReports: instances.list_usage_reports + exportUsageReports: instances.export_usage_reports + exportOrganizationUsageReports: instances.export_organization_usage_reports + listEntitlementExceptions: instances.list_entitlement_exceptions + createEntitlementException: instances.create_entitlement_exception + revokeEntitlementException: instances.revoke_entitlement_exception get-instance-integration: instances.get_integration create-instance-integration: instances.create_integration update-instance-integration: instances.update_integration @@ -116,6 +248,7 @@ core: # license families: the products whose versions the licenses above are list-license-families: license_families.list get-license-family: license_families.get + updateLicenseFamily: license_families.update # metadata fields list-metadata-fields: metadata_fields.list @@ -163,9 +296,30 @@ core: every flag at once. (sdk-js does keep a bulk call, but only to feed the licensing snapshot its React components render; nothing in a Python SDK needs that.) + GetMe: >- + The signed-in person's own profile and access. A machine client holding an API token has + no human profile. kaiten's own operation-coverage test exempts this operation and the + eleven below as console authoring, for the same reasons. + GetAuthorization: >- + Request-time identity and grant introspection, which Kaiten's SaaS middleware consumes; + not product automation. + ListUsers: The human member directory, administered in the console and the operational CLI. + GetUser: An administrator's view of a human member's profile. + UpdateUser: An administrator's edit of a human identity, not machine account automation. + PutUserAuthorization: >- + An administrator's replacement of a person's roles and permissions -- never a service + token's. + SetUserAssignments: The console's role and direct-scope editor for a person. + SetUserEnabled: A person's membership access lifecycle, administered by a human. + ListRoles: The role directory of the console and the operational CLI. + CreateRole: Scope-group authoring by a human administrator. + UpdateRole: Scope-group authoring by a human administrator. + DeleteRole: Scope-group lifecycle, administered by a human. + platform: operations: get-platform-me: me + platform-close-billing-periods: billing.close_periods register-connector: connectors.register ensure-organization: organizations.ensure get-organization: organizations.get diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index e09e19c..a28362d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,1796 +1,2277 @@ components: schemas: - AuditTrail: + Addon: additionalProperties: false properties: - eventName: - description: Event name (e.g. ENTITLEMENT_VALUE_GET) - type: string - eventType: - description: Event type identifier (e.g. com.kaiten.instance.entitlement.v1.value_get) - type: string - id: - description: Unique audit trail entry ID + createdAt: + format: date-time + readOnly: true type: string - instanceId: - description: Instance UUID (present for instance-scoped events) + description: type: string - instanceSlug: - description: Instance slug + familySlug: + description: The family the version belongs to + examples: + - extra-seats type: string - payload: - description: Event payload (JSON) - timestamp: - description: When the event occurred - format: date-time + id: + readOnly: true type: string - required: - - id - - instanceSlug - - eventName - - eventType - - timestamp - type: object - Basic: - additionalProperties: false - properties: - type: + increaseBilling: + description: "The family's: how a mid-period increase or attachment of an ADVANCE add-on is billed. TRUE_UP adds a true-up line for the elapsed period at the next renewal; NEXT_BOUNDARY bills from the next boundary only" enum: - - basic + - TRUE_UP + - NEXT_BOUNDARY + readOnly: true type: string - value: + isDefault: + description: The version the family resolves to. Only a PUBLISHED version. + type: boolean + lifecycleState: + description: A DRAFT can be attached to try it; an ARCHIVED version stays on the instances holding it but cannot be attached again. + enum: + - DRAFT + - PUBLISHED + - ARCHIVED type: string - required: - - type - - value - type: object - BasicTargeting: - additionalProperties: false - properties: + maxQuantity: + description: The largest quantity an instance can hold; unbounded when null + examples: + - 10 + format: int32 + type: + - integer + - "null" name: examples: - - Targeting Rule 1 + - Extra seats type: string - rule: - description: CEL expression for basicTargeting evaluation + pricingType: + enum: + - FREE + - PAID + - CUSTOM + type: string + slug: + description: The version's slug, unique in the organization examples: - - user.group == 'beta' - maxLength: 10000 + - extra-seats-v1 type: string - type: - enum: - - basic + updatedAt: + format: date-time + readOnly: true type: string - variant: + version: + description: The version number in its family, assigned on create + examples: + - 1 + format: int32 + readOnly: true + type: integer + versionName: + examples: + - Version - 1 type: string required: - - type + - id + - slug + - familySlug - name - - variant - - rule + - description + - version + - versionName + - isDefault + - lifecycleState + - pricingType + - maxQuantity + - increaseBilling + - createdAt + - updatedAt type: object - BooleanEntitlementValue: + AddonAmount: additionalProperties: false properties: - type: - description: Value type discriminator - enum: - - boolean + addonSlug: examples: - - boolean + - extra-seats-v1 type: string - value: - description: Boolean value + quantity: examples: - - true - type: boolean + - 3 + format: int32 + type: integer required: - - type - - value + - addonSlug + - quantity type: object - BulkEvaluationFailure: + AddonChanges: additionalProperties: false properties: - errorCode: - description: Error code specific to the bulk evaluation error + description: type: string - errorDetails: - description: Optional error details description + isDefault: + type: boolean + maxQuantity: + format: int32 + type: integer + name: + examples: + - Extra seats + minLength: 1 type: string - required: - - errorCode - type: object - BulkEvaluationRequest: - additionalProperties: false - properties: - context: - additionalProperties: {} - type: object - type: object - Command: - additionalProperties: false - properties: - entitlementSlug: + versionName: type: string required: - - entitlementSlug + - name + - description + - isDefault type: object - Component: + AddonDelivered: additionalProperties: false properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string createdAt: - description: Timestamp when the component was created - examples: - - "2023-10-01T12:00:00Z" format: date-time readOnly: true type: string - createdBy: - $ref: "#/components/schemas/User" - description: User who created this component - readOnly: true + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string description: - description: Description of the component + type: string + familySlug: + description: The family the version belongs to examples: - - Main API gateway component - maxLength: 500 + - extra-seats type: string id: - description: Unique identifier for the component - examples: - - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string + increaseBilling: + description: "The family's: how a mid-period increase or attachment of an ADVANCE add-on is billed. TRUE_UP adds a true-up line for the elapsed period at the next renewal; NEXT_BOUNDARY bills from the next boundary only" + enum: + - TRUE_UP + - NEXT_BOUNDARY + readOnly: true + type: string + isDefault: + description: The version the family resolves to. Only a PUBLISHED version. + type: boolean + lifecycleState: + description: A DRAFT can be attached to try it; an ARCHIVED version stays on the instances holding it but cannot be attached again. + enum: + - DRAFT + - PUBLISHED + - ARCHIVED + type: string + maxQuantity: + description: The largest quantity an instance can hold; unbounded when null + examples: + - 10 + format: int32 + type: + - integer + - "null" name: - description: Name of the component examples: - - api-gateway - maxLength: 100 - minLength: 1 + - Extra seats type: string - previousComponentId: - description: Reference to the previous version of this component - examples: - - 123e4567-e89b-12d3-a456-426614174000 + pricingType: + enum: + - FREE + - PAID + - CUSTOM type: string slug: - description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + description: The version's slug, unique in the organization examples: - - api-gateway-v1-2-3 - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + - extra-seats-v1 + type: string + updatedAt: + format: date-time + readOnly: true type: string version: - description: Version of the component + description: The version number in its family, assigned on create examples: - - v1.2.3 - maxLength: 100 - minLength: 1 + - 1 + format: int32 + readOnly: true + type: integer + versionName: + examples: + - Version - 1 + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - id + - slug + - familySlug - name + - description - version - - createdBy + - versionName + - isDefault + - lifecycleState + - pricingType + - maxQuantity + - increaseBilling - createdAt + - updatedAt type: object - ConfigEntitlementValue: + x-kaiten-payload: "#/components/schemas/Addon" + AddonEntitlement: additionalProperties: false properties: - type: - description: Value type discriminator - enum: - - object + entitlementSlug: examples: - - object + - seats + type: string + entitlementType: + enum: + - BOOLEAN + - NUMBER + - CONFIG + - NUMBER_AI_CREDIT + type: string + id: + readOnly: true + type: string + limitCapExceededOveragePercent: + description: The overage the add-on allows; null inherits the licence grant's + format: int32 + type: + - integer + - "null" + overrideBehavior: + description: "How a number grant combines with the licence's: ADD sums value × quantity, OVERRIDE replaces it with the latest attached add-on's, MAX keeps the larger. A boolean always ORs, a config always overrides." + enum: + - ADD + - OVERRIDE + - MAX type: string value: additionalProperties: {} - description: Object configuration value + description: "{type, value}, shaped like a licence grant. A number counts once per unit of quantity." type: object required: - - type + - id + - entitlementSlug + - entitlementType - value + - overrideBehavior + - limitCapExceededOveragePercent type: object - Connector: + AddonFamily: additionalProperties: false properties: - created_at: - description: Timestamp when connector was first registered - format: date-time + currentVersion: + description: The default version, else the highest PUBLISHED one; null when none is published + oneOf: + - $ref: "#/components/schemas/Addon" + - enum: + - null + type: "null" + id: + readOnly: true type: string - entitlement_slug: - description: Slug of the BOOLEAN entitlement a license must grant for an organization to activate this connector. Absent means the connector is ungated. - examples: - - connector-attio + increaseBilling: + description: How a mid-period increase or attachment of the family's ADVANCE add-ons is billed (V31-70) + enum: + - TRUE_UP + - NEXT_BOUNDARY type: string - name: - description: Stable connector identifier - examples: - - kaiten.integration.crm.attio - type: string - settings_schema: - additionalProperties: {} - description: JSON schema used to validate connector settings payloads - type: object - updated_at: - description: Timestamp when connector registration was last updated - format: date-time - type: string - version: - description: Registered connector version + isPublic: + description: Whether the family is listed in the public catalogue + type: boolean + lastVersion: + description: The highest version number the family has assigned + format: int32 + type: integer + slug: examples: - - 1.0.0 + - extra-seats type: string + versions: + description: Every version, newest first + items: + $ref: "#/components/schemas/Addon" + type: + - array + - "null" required: - - name - - version - - settings_schema - - created_at - - updated_at + - id + - slug + - isPublic + - increaseBilling + - lastVersion + - currentVersion + - versions type: object - ConnectorActivation: + AddonFamilyVisibility: additionalProperties: false properties: - activatedAt: - description: When this organization first activated the connector - format: date-time - type: string - connectorName: - description: Stable connector identifier - examples: - - kaiten.integration.crm.attio + increaseBilling: + description: "How a mid-period increase or attachment of the family's ADVANCE add-ons is billed, from the next composition: TRUE_UP (a true-up line for the elapsed period at the next renewal) or NEXT_BOUNDARY (from the next boundary only); another value answers 422 .InvalidIncreaseBilling" type: string - required: - - connectorName - - activatedAt + isPublic: + description: List the family's default PUBLISHED version, with its ACTIVE prices, in the public catalogue + type: boolean type: object - ConnectorSettings: + AddonGrant: additionalProperties: false properties: - connector_name: - description: Stable connector identifier - examples: - - kaiten.integration.crm.attio - readOnly: true + limitCapExceededOveragePercent: + description: "The overage the add-on allows: -1 with an unlimited value, 0 or more otherwise. Absent inherits the licence grant's." + format: int32 + type: integer + overrideBehavior: + default: MAX + description: How a number grant combines with the licence's + enum: + - ADD + - OVERRIDE + - MAX type: string - settings: + value: additionalProperties: {} - description: Connector settings payload + description: "{type, value}, shaped like a licence grant: a number counts once per unit of quantity" + examples: + - type: number + value: 5 type: object required: - - connector_name - - settings + - value type: object - ConnectorSettingsSchema: + AddonGrantUpdateDelivered: additionalProperties: false properties: - connector_name: - description: Stable connector identifier + addonSlug: + description: The add-on version the grant belongs to + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlementSlug: examples: - - kaiten.integration.crm.attio + - seats type: string - schema: + entitlementType: + enum: + - BOOLEAN + - NUMBER + - CONFIG + - NUMBER_AI_CREDIT + type: string + id: + readOnly: true + type: string + limitCapExceededOveragePercent: + description: The overage the add-on allows; null inherits the licence grant's + format: int32 + type: + - integer + - "null" + overrideBehavior: + description: "How a number grant combines with the licence's: ADD sums value × quantity, OVERRIDE replaces it with the latest attached add-on's, MAX keeps the larger. A boolean always ORs, a config always overrides." + enum: + - ADD + - OVERRIDE + - MAX + type: string + value: additionalProperties: {} - description: JSON schema describing supported connector settings payload + description: "{type, value}, shaped like a licence grant. A number counts once per unit of quantity." type: object - version: - description: Registered connector version - examples: - - 1.0.0 + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - - connector_name - - version - - schema + - addonSlug + - id + - entitlementSlug + - entitlementType + - value + - overrideBehavior + - limitCapExceededOveragePercent type: object - ConnectorState: + x-kaiten-payload: "#/components/schemas/AddonGrantUpdate" + AddonPriceUpdateDelivered: additionalProperties: false properties: - activated: - description: Whether this organization has turned the connector on - type: boolean - activatedAt: - description: When this organization activated the connector. Absent when it has not. - format: date-time + addonSlug: + description: The add-on version the price belongs to type: string - available: - description: Whether this deployment has the connector registered - type: boolean - connectorName: - description: Stable connector identifier - examples: - - kaiten.integration.crm.attio + billingModel: + description: "FLAT_FEE: an amount per billing period. USAGE_BASED: an amount per sale unit of the metered entitlement's usage. OVERAGE: an amount per sale unit of the usage above the licence's limit." + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE type: string - entitled: - description: Whether this organization's licence includes the connector. True for an ungated connector. - type: boolean - version: - description: Registered connector version. Absent when the connector is not available. - examples: - - 1.0.0 + billingPeriod: + description: The period a FLAT_FEE price is charged for. Null on metered prices, which bill on the subscription's period. + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + - null + type: + - string + - "null" + billingTiming: + description: ADVANCE bills the period that starts; ARREARS the period that ended. Metered prices are always ARREARS. + enum: + - ADVANCE + - ARREARS + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - required: - - connectorName - - available - - entitled - - activated - type: object - Customer: - additionalProperties: false - properties: createdAt: - description: Timestamp when the customer was created - examples: - - "2023-10-01T12:00:00Z" format: date-time readOnly: true type: string - createdBy: - $ref: "#/components/schemas/User" - description: User who created this customer - readOnly: true - domain: - description: Optional customer domain name + currency: + description: ISO 4217 code, upper case. One currency per licence version. examples: - - example.tld - pattern: ^[a-z0-9_-]+(\.[a-z0-9_-]+)+$ + - EUR type: string - externalCustomerId: - description: Optional identifier for the customer in an external system + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + deprecatedAt: + description: When the price was deprecated + format: date-time + type: + - string + - "null" + displayLabel: + description: The invoice line's label; a default is derived when it is null. examples: - - external-customer-id-12345 + - Pro plan type: - string - "null" - id: - description: Unique identifier for the customer + displayOrder: + description: Order among the version's prices examples: - - 123e4567-e89b-12d3-a456-426614174000 + - 0 + format: int32 + type: integer + id: + description: Unique identifier of the price readOnly: true type: string - integrations: - additionalProperties: - $ref: "#/components/schemas/CustomerIntegration" - description: Integrations grouped by adapter name - type: object - name: - description: Name of the customer - examples: - - Awesome Customer - minLength: 1 + isDefault: + description: The FLAT_FEE price picked for its billing period when none is named. At most one per period; only an ACTIVE FLAT_FEE price. + type: boolean + metered: + description: What a metered price measures. Null on FLAT_FEE prices. + oneOf: + - $ref: "#/components/schemas/PriceMeter" + - enum: + - null + type: "null" + status: + description: A DEPRECATED price keeps billing what is already pinned to it but is no longer offered. + enum: + - ACTIVE + - DEPRECATED type: string - slug: - description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + unitAmount: + description: unitAmountDecimal as an integer number of minor units, when it is one; null otherwise. examples: - - awesome-customer - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + - 2900 + format: int64 + type: + - integer + - "null" + unitAmountDecimal: + description: "Amount in the currency's minor units (cents for EUR): per period for FLAT_FEE, per sale unit for metered prices. Up to 12 decimal places." + examples: + - "2900" type: string updatedAt: - description: Timestamp when the customer was last updated - examples: - - "2023-10-01T12:00:00Z" format: date-time readOnly: true type: string - updatedBy: - $ref: "#/components/schemas/User" - description: User who last updated this customer - readOnly: true + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string required: + - addonSlug - id - - name - - createdBy + - billingModel + - billingTiming + - billingPeriod + - currency + - unitAmount + - unitAmountDecimal + - metered + - displayLabel + - displayOrder + - isDefault + - status + - deprecatedAt - createdAt - - updatedBy - updatedAt type: object - CustomerBody: + x-kaiten-payload: "#/components/schemas/AddonPriceUpdate" + AddonQuantity: additionalProperties: false properties: - domain: - description: Optional customer domain name + quantity: examples: - - example.tld - pattern: ^[a-z0-9_-]+(\.[a-z0-9_-]+)+$ + - 3 + format: int32 + minimum: 1 + type: integer + required: + - quantity + type: object + AddonUpdateDelivered: + additionalProperties: false + properties: + changedFields: + description: The members the change touched, as the API names them + items: + type: string + type: + - array + - "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - error: - description: Optional integration error message + compatibleLicenseFamilies: + description: The licence families the version fits after the change + items: + type: string + type: + - array + - "null" + createdAt: + format: date-time + readOnly: true type: string - integrationMetadata: - additionalProperties: {} - description: Adapter-specific integration metadata - type: object - name: - description: Customer name - examples: - - Awesome Customer - minLength: 1 + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - slug: - description: Customer slug - examples: - - awesome-customer - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + description: type: string - webUrl: - description: Absolute http(s) link to the record in the third-party system + familySlug: + description: The family the version belongs to examples: - - https://app.attio.com/w/acme/record/rec_12345 + - extra-seats type: string - type: object - CustomerCreationRejected: - additionalProperties: false - properties: - name: + id: + readOnly: true type: string - reason: + increaseBilling: + description: "The family's: how a mid-period increase or attachment of an ADVANCE add-on is billed. TRUE_UP adds a true-up line for the elapsed period at the next renewal; NEXT_BOUNDARY bills from the next boundary only" + enum: + - TRUE_UP + - NEXT_BOUNDARY + readOnly: true type: string - slug: + isDefault: + description: The version the family resolves to. Only a PUBLISHED version. + type: boolean + lifecycleState: + description: A DRAFT can be attached to try it; an ARCHIVED version stays on the instances holding it but cannot be attached again. + enum: + - DRAFT + - PUBLISHED + - ARCHIVED type: string - required: - - name - - slug - - reason - type: object - CustomerIntegration: - additionalProperties: false - properties: - external_id: - description: External identifier in the third-party adapter + maxQuantity: + description: The largest quantity an instance can hold; unbounded when null examples: - - rec_12345 + - 10 + format: int32 + type: + - integer + - "null" + name: + examples: + - Extra seats type: string - last_error: - description: Last synchronization error message, if any + pricingType: + enum: + - FREE + - PAID + - CUSTOM type: string - metadata: - additionalProperties: {} - description: Adapter-specific integration metadata - type: object - synced_at: - description: Timestamp of the last synchronization with the adapter + slug: + description: The version's slug, unique in the organization + examples: + - extra-seats-v1 + type: string + updatedAt: format: date-time readOnly: true type: string - web_url: - description: Absolute http(s) link to the record in the third-party system + version: + description: The version number in its family, assigned on create examples: - - https://app.attio.com/w/acme/company/rec_12345 + - 1 + format: int32 + readOnly: true + type: integer + versionName: + examples: + - Version - 1 + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - - external_id - - synced_at + - changedFields + - compatibleLicenseFamilies + - id + - slug + - familySlug + - name + - description + - version + - versionName + - isDefault + - lifecycleState + - pricingType + - maxQuantity + - increaseBilling + - createdAt + - updatedAt type: object - CustomerIntegrationResource: + x-kaiten-payload: "#/components/schemas/AddonUpdate" + AuditActor: additionalProperties: false properties: - domain: - description: Optional customer domain name - examples: - - example.tld + id: type: string - error: - description: Last synchronization error for this integration, if any + kind: + description: "USER: a person. SERVICE: a service account. SYSTEM: Kaiten itself -- jobs, and the anonymous checkouts no one of the organization makes" + enum: + - USER + - SERVICE + - SYSTEM type: string - integrationMetadata: - additionalProperties: {} - description: Adapter-specific customer integration metadata - type: object name: - description: Customer name - examples: - - Awesome Customer - type: string - slug: - description: Customer slug - examples: - - awesome-customer - type: string - webUrl: - description: Absolute http(s) link to the record in the third-party system - examples: - - https://app.attio.com/w/acme/company/rec_12345 + description: The user's name; "Kaiten (system)" for the work Kaiten does itself type: string required: - - integrationMetadata + - id - name - - slug + - kind type: object - DefaultVariant: - discriminator: - mapping: - basic: "#/components/schemas/Basic" - rollout_date: "#/components/schemas/RolloutDate" - rollout_percentage: "#/components/schemas/RolloutPercentage" - propertyName: type - oneOf: - - $ref: "#/components/schemas/Basic" - - $ref: "#/components/schemas/RolloutDate" - - $ref: "#/components/schemas/RolloutPercentage" - Deployment: + AuditTrail: additionalProperties: false properties: - createdAt: - description: Timestamp when the deployment was recorded - examples: - - "2023-10-01T12:00:00Z" - format: date-time + actor: + description: Who the event is recorded under; null for an entry recorded before actors were (E-27), or whose user is gone + oneOf: + - $ref: "#/components/schemas/AuditActor" + - enum: + - null + type: "null" + eventName: + description: Event name (e.g. ENTITLEMENT_VALUE_GET) type: string - createdBy: - $ref: "#/components/schemas/User" - description: User who recorded this deployment - deploymentZoneId: - description: Deployment zone the release was deployed to - examples: - - 123e4567-e89b-12d3-a456-426614174000 + eventType: + description: Event type identifier (e.g. com.kaiten.instance.entitlement.v1.value_get) type: string id: - description: Unique identifier for this deployment event - examples: - - 123e4567-e89b-12d3-a456-426614174000 + description: Unique audit trail entry ID type: string - releaseId: - description: Release that was deployed - examples: - - 123e4567-e89b-12d3-a456-426614174000 + instanceId: + description: Instance UUID (present for instance-scoped events) + type: string + instanceSlug: + description: Instance slug + type: string + payload: + description: Event payload (JSON) + timestamp: + description: When the event occurred + format: date-time type: string required: - id - - deploymentZoneId - - releaseId - - createdBy - - createdAt + - instanceSlug + - eventName + - eventType + - timestamp + - actor type: object - DeploymentZone: + Authorization: additionalProperties: false properties: - createdAt: - description: Timestamp when the deployment zone was created - examples: - - "2023-10-01T12:00:00Z" - format: date-time - readOnly: true - type: string - createdBy: - $ref: "#/components/schemas/User" - description: User who created this deployment zone - readOnly: true - description: - description: Description of the deployment zone - examples: - - "2023-10-01T12:00:00Z" + externalOrganizationId: type: string - id: - description: Unique identifier for the deployment zone - examples: - - 123e4567-e89b-12d3-a456-426614174000 - readOnly: true + kind: type: string - metadata: - additionalProperties: {} - description: Typed metadata for the deployment zone, validated against the org-scoped MetadataField schema - examples: - - region: eu-west-1 - type: object - name: - description: Name of the deployment zone - examples: - - AWS us-west-2 - minLength: 1 + organizationId: type: string - releaseId: - description: Currently deployed release ID - examples: - - 123e4567-e89b-12d3-a456-426614174000 + scopes: + items: + type: string + type: array + subject: type: string - slug: - description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. - examples: - - aws-us-west-2 - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + userId: type: string + required: + - kind + - userId + - organizationId + - externalOrganizationId + - subject + - scopes + type: object + Basic: + additionalProperties: false + properties: type: - description: "Environment class of the deployment zone. Free-form: any value is stored as sent, but production, staging and development are the ones the console labels, and feature-flag targeting rules match on this field as __kaiten.deploymentZone.type." - examples: - - production + enum: + - basic type: string - updatedAt: - description: Timestamp when the deployment zone was last updated - examples: - - "2023-10-01T12:00:00Z" - format: date-time - readOnly: true + value: type: string - updatedBy: - $ref: "#/components/schemas/User" - description: User who last updated the deployment zone - readOnly: true required: - - id - - name - type - - description - - createdBy - - createdAt - - updatedBy - - updatedAt + - value type: object - Entitlement: + BasicTargeting: additionalProperties: false properties: - aggregationMethod: - description: Method used to fold each reported usage value into the stored total (NUMBER only). Defaults to SUM. - enum: - - COUNT - - SUM - - AVERAGE - - MIN - - MAX - - LATEST + name: examples: - - SUM + - Targeting Rule 1 type: string - createdAt: - description: Timestamp when this entitlement was created + rule: + description: CEL expression for basicTargeting evaluation examples: - - "2023-10-01T12:00:00Z" - format: date-time - readOnly: true + - user.group == 'beta' + maxLength: 10000 type: string - description: - description: Description of the entitlement - examples: - - Basic subscription plan with limited features + type: + enum: + - basic + type: string + variant: + type: string + required: + - type + - name + - variant + - rule + type: object + BillingCapabilities: + additionalProperties: false + properties: + disabledReason: + description: "Why they do not: billing is off on this deployment, or the organization's plan does not include it" + enum: + - DEPLOYMENT_DISABLED + - NOT_ENTITLED + - null type: - string - "null" - displayOrder: - description: Sort order of this entitlement in customer-facing components (ascending). Defaults to 0. - examples: - - 10 - format: int32 - minimum: 0 - type: integer - entitlementGroups: - description: Groups associated with this entitlement + enabled: + description: Whether the billing routes answer for this organization + type: boolean + features: + $ref: "#/components/schemas/BillingFeatures" + description: Which parts of billing this release ships + providers: + description: Who can collect invoices items: - $ref: "#/components/schemas/EntitlementGroupSummary" - readOnly: true + $ref: "#/components/schemas/BillingProvider" + type: array + publicSurface: + $ref: "#/components/schemas/PublicSurface" + usageHistoryRetentionMonths: + description: How long usage reports are kept, in months; null when they are kept forever or it cannot be told + format: int64 type: - - array + - integer - "null" - groupSlugs: - description: Groups associated with this entitlement, identified by slug + usageHistoryRetentionSource: + description: "Where the retention comes from: KBK, the organization's licence or an add-on; ENV, the deployment's KAITEN_USAGE_LEDGER_RETENTION_MONTHS; UNKNOWN, the licensing authority could not tell. A null usageHistoryRetentionMonths reads as kept forever with KBK or ENV, and as unknown with UNKNOWN (V31-11)." + enum: + - KBK + - ENV + - UNKNOWN + type: string + usageIdempotencyWindowDays: + description: How long a usage report's transactionId is remembered, in days + format: int64 + type: integer + required: + - enabled + - disabledReason + - providers + - publicSurface + - usageHistoryRetentionMonths + - usageHistoryRetentionSource + - usageIdempotencyWindowDays + - features + type: object + BillingFeatures: + additionalProperties: false + properties: + addons: + type: boolean + chargeAutomatically: + type: boolean + lifecycle: + description: Cancellation, reactivation, plan changes and overdue tracking + type: boolean + publicSurface: + type: boolean + stripe: + type: boolean + trials: + type: boolean + vouchers: + type: boolean + required: + - stripe + - lifecycle + - trials + - addons + - vouchers + - chargeAutomatically + - publicSurface + type: object + BillingHealth: + additionalProperties: false + properties: + closeBacklog: + $ref: "#/components/schemas/CloseBacklog" + description: Subscriptions whose period ended more than KAITEN_BILLING_CLOSE_GRACE ago and has not closed yet + handoff: + $ref: "#/components/schemas/HandoffBacklog" + heldInvoices: + $ref: "#/components/schemas/HeldInvoices" + overdueInvoices: + format: int64 + type: integer + pastDueSubscriptions: + format: int64 + type: integer + paymentMethodsExpiring30d: + description: ACTIVE payment methods whose card expires in the next 30 days + format: int64 + type: integer + providerSync: items: - type: string + $ref: "#/components/schemas/ProviderSync" + type: array + pushFailures: + $ref: "#/components/schemas/PushFailures" + description: Invoices whose push failed at least as many times as the alert threshold + reconciliationMismatches30d: + description: Invoices whose provider's amounts differed from Kaiten's, in the last 30 days + format: int64 + type: integer + usageLedger: + $ref: "#/components/schemas/UsageLedger" + required: + - closeBacklog + - heldInvoices + - pushFailures + - reconciliationMismatches30d + - providerSync + - pastDueSubscriptions + - overdueInvoices + - handoff + - paymentMethodsExpiring30d + - usageLedger + type: object + BillingProvider: + additionalProperties: false + properties: + available: + description: Whether the organization may connect it on this deployment + type: boolean + capabilities: + $ref: "#/components/schemas/ProviderCapabilities" + connected: + description: Whether it is set up for the organization; NOOP always is + type: boolean + kind: + enum: + - NOOP + - STRIPE + type: string + livemode: + description: Whether the connection reaches the provider's live account rather than a test one; null when not connected, and for NOOP type: - - array + - boolean - "null" - writeOnly: true - icon: - description: Optional icon, stored as a provider-namespaced token (provider:name). Decoupled from any specific icon library. - examples: - - lucide:rocket - maxLength: 64 - pattern: ^[a-z0-9]+:[a-z0-9]+(-[a-z0-9]+)*$ - type: string - id: - description: Unique identifier for the entitlement + unavailableReason: + description: Why it is not available; null when it is, and when the licensing authority cannot answer + enum: + - NOT_ENTITLED + - VAULT_NOT_CONFIGURED + - null + type: + - string + - "null" + required: + - kind + - available + - unavailableReason + - connected + - livemode + - capabilities + type: object + BillingSettings: + additionalProperties: false + properties: + defaultCollectionMethod: + description: "How an invoice is collected when its subscription does not say: SEND_INVOICE sends it for payment; CHARGE_AUTOMATICALLY charges the customer's payment method, on a provider that can. A NOOP subscription sends its invoices whatever the default." + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY examples: - - 123e4567-e89b-12d3-a456-426614174000 - readOnly: true + - SEND_INVOICE type: string - name: - description: Name of the entitlement + defaultDaysUntilDue: + description: Days an invoice is due after it is issued, when its subscription does not say; 0 to 365. examples: - - Basic Plan - minLength: 1 + - 30 + format: int32 + type: integer + handoffStripeInvoices: + description: Whether invoices a payment provider issues also enter the handoff queue, for an accounting system that wants every invoice. Invoices without a provider always do. + type: boolean + required: + - defaultCollectionMethod + - defaultDaysUntilDue + - handoffStripeInvoices + type: object + BillingStartedDelivered: + additionalProperties: false + properties: + anchorAt: + description: The instant its periods are counted from + format: date-time type: string - resetAnchor: - description: "Phase of the periodic usage window: CALENDAR (UTC calendar-aligned) or LICENSE_START (phased off the instance's license start date). Required exactly when resetPeriod is set; defaults to CALENDAR when omitted. Immutable once set." + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: enum: - - CALENDAR - - LICENSE_START - examples: - - CALENDAR + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL type: string - resetPeriod: - description: Cadence at which usage automatically resets for this NUMBER entitlement. NULL means a lifetime counter (default). Immutable once set -- a full-replace PUT must echo back the stored value. + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time + type: + - string + - "null" + canceledAt: + format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" enum: - - HOUR - - DAY - - WEEK - - MONTH - - YEAR - examples: - - MONTH + - SEND_INVOICE + - CHARGE_AUTOMATICALLY type: string - saleUnitFactor: - description: Number of base units one sale unit represents (1 sale unit = N base units). - examples: - - 3 - exclusiveMinimum: 0 - format: double - type: number - saleUnitPlural: - description: Plural label of the unit this entitlement is sold in, when it differs from the base unit. - examples: - - packs - maxLength: 100 - minLength: 1 + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's + type: + - string + - "null" + createdAt: + format: date-time type: string - saleUnitSingular: - description: Singular label of the unit this entitlement is sold in, when it differs from the base unit. Requires the base unit labels and the full sale unit trio (saleUnitSingular, saleUnitPlural, saleUnitFactor). + currency: examples: - - pack - maxLength: 100 - minLength: 1 + - EUR type: string - slug: - description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. - examples: - - basic-plan - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time type: string - type: - description: Type of the entitlement - enum: - - BOOLEAN - - NUMBER - - CONFIG - - NUMBER_AI_CREDIT - examples: - - NUMBER + currentPeriodStart: + format: date-time type: string - unitPlural: - description: Plural label of the base unit this entitlement is measured in (NUMBER type only). Must be provided together with unitSingular. - examples: - - seats - maxLength: 100 - minLength: 1 + customerName: type: string - unitSingular: - description: Singular label of the base unit this entitlement is measured in (NUMBER type only). Must be provided together with unitPlural. - examples: - - seat - maxLength: 100 - minLength: 1 + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - updatedAt: - description: Timestamp when this entitlement was last updated - examples: - - "2023-10-02T12:00:00Z" - format: date-time - readOnly: true + customerSlug: type: string - userFacing: - description: Whether this entitlement is displayed in customer-facing components (e.g. plan and pricing pages). Defaults to false. - examples: - - true - type: boolean - warningThresholdPercent: - description: Percentage of the cap at which an early-warning event fires before the cap is reached. 0 disables the warning (default). - examples: - - 80 + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" format: int32 - maximum: 100 - minimum: 0 type: integer - required: - - id - - name - - description - - createdAt - - updatedAt - type: object - EntitlementGroup: - additionalProperties: false - properties: - description: - description: Description of the entitlement group - examples: - - All entitlements related to AI usage quotas + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 type: - - string + - integer - "null" id: - description: Unique identifier for the entitlement group - examples: - - 123e4567-e89b-12d3-a456-426614174000 - readOnly: true type: string - name: - description: Name of the entitlement group - examples: - - AI Quotas - minLength: 1 + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" + format: date-time + type: + - string + - "null" + instanceName: type: string - slug: - description: URL-friendly identifier, unique per organization. Auto-generated if not provided. - examples: - - ai-quotas - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + instanceSlug: + description: As it was at the subscription's last write type: string - required: - - id - - name - - description - type: object - EntitlementGroupSummary: - additionalProperties: false - properties: - id: - description: Unique identifier for the entitlement group - examples: - - 123e4567-e89b-12d3-a456-426614174000 - type: string - name: - description: Name of the entitlement group - examples: - - AI Quotas - type: string - slug: - description: URL-friendly identifier for the entitlement group - examples: - - ai-quotas - type: string - required: - - id - - name - - slug - type: object - EntitlementGroupUsage: - additionalProperties: false - properties: - currentPeriodEnd: - description: End of the current usage window (exclusive). Null for a lifetime entitlement. + pastDueSince: format: date-time + type: + - string + - "null" + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE type: string - currentPeriodStart: - description: Start of the current usage window (inclusive). Null for a lifetime entitlement. + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + resubscribed: + description: Set when a CANCELED subscription was subscribed again + type: boolean + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" format: date-time type: string - entitlementId: - description: Entitlement ID - type: string - entitlementName: - description: Entitlement name + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED type: string - entitlementSlug: - description: Entitlement slug + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" + updatedAt: + format: date-time type: string - entitlementType: - description: Entitlement type + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string - licenseValue: - description: License limit value, discriminated by the 'type' field. Null when the license grants no value. - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type - oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" - usageValue: - description: Current usage value, discriminated by the 'type' field. Null when nothing has been reported yet. - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type - oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" required: - - entitlementId - - entitlementSlug - - entitlementName - - entitlementType + - resubscribed + - id + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince + - createdAt + - updatedAt type: object - EntitlementUsage: + x-kaiten-payload: "#/components/schemas/BillingStarted" + BooleanEntitlementValue: additionalProperties: false properties: - currentPeriodEnd: - description: End of the current usage window (exclusive). Null for a lifetime entitlement (no configured reset period). - examples: - - "2026-04-01T00:00:00Z" - format: date-time - type: string - currentPeriodStart: - description: Start of the current usage window (inclusive). Null for a lifetime entitlement (no configured reset period). - examples: - - "2026-03-01T00:00:00Z" - format: date-time - type: string - entitlementId: - description: Unique identifier for the entitlement - examples: - - 123e4567-e89b-12d3-a456-426614174000 - type: string - entitlementSlug: - description: URL-friendly identifier of the entitlement - examples: - - premium-support - type: string - licenseId: - description: Unique identifier for the license to which this entitlement usage belongs - examples: - - 123e4567-e89b-12d3-a456-426614174001 - type: string - licenseSlug: - description: URL-friendly identifier of the license + type: + description: Value type discriminator + enum: + - boolean examples: - - enterprise-license + - boolean type: string - limit: - description: License grant this usage is measured against, discriminated by the 'type' field. Null when the license grants no value; -1 means unlimited for a NUMBER entitlement. - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type - oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" value: - description: Resolved entitlement value, discriminated by the 'type' field - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type - oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" + description: Boolean value + examples: + - true + type: boolean required: - - entitlementId - - entitlementSlug - - licenseId - - licenseSlug + - type - value type: object - ErrorDetail: - additionalProperties: false - properties: - location: - description: Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' - type: string - message: - description: Error message text - type: string - value: - description: The value at the given location - type: object - EvaluationFailure: + BulkEvaluationFailure: additionalProperties: false properties: errorCode: - description: OpenFeature compatible error code + description: Error code specific to the bulk evaluation error type: string errorDetails: - description: Error description - type: string - key: - description: Feature flag key + description: Optional error details description type: string - metadata: - additionalProperties: {} - description: Arbitrary metadata for the flag - type: object required: - - key - errorCode type: object - EvaluationRequest: + BulkEvaluationRequest: additionalProperties: false properties: context: additionalProperties: {} type: object type: object - EvaluationSuccess: + CanceledSubscription: additionalProperties: false properties: - key: - description: Feature flag key + anchorAt: + description: The instant its periods are counted from + format: date-time type: string - metadata: - additionalProperties: {} - description: Arbitrary metadata for the flag - type: object - reason: - description: OpenFeature reason for the evaluation - examples: - - STATIC + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL type: string - value: - description: Flag evaluation result - variant: - description: string of the evaluated flag value + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time + type: + - string + - "null" + canceledAt: + format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY type: string - required: - - key - type: object - FeatureFlag: - additionalProperties: false - properties: - default_variant: - $ref: "#/components/schemas/DefaultVariant" - description: - examples: - - This is a new feature flag + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's type: - string - "null" - enabled: - examples: - - true - type: boolean - event_name: - examples: - - feature_flag_event + createdAt: + format: date-time type: string - id: + currency: examples: - - 123e4567-e89b-12d3-a456-426614174000 - readOnly: true + - EUR type: string - metadata: - additionalProperties: {} - type: object - name: - examples: - - New Feature - minLength: 1 + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time type: string - slug: - examples: - - new-feature - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + currentPeriodStart: + format: date-time type: string - targetings: - $ref: "#/components/schemas/Targetings" - type: - enum: - - boolean - - string - - number - - object - examples: - - boolean + customerName: type: string - variants: - items: - $ref: "#/components/schemas/Variant" + customerSlug: + type: string + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" + format: int32 + type: integer + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 type: - - array + - integer - "null" - required: - - id - - type - - variants - - targetings - - name - - description - - default_variant - - metadata - - enabled - - event_name - type: object - FeatureFlagEvaluated: - additionalProperties: false - properties: - error_code: - description: Set when the evaluation failed and the default was returned + finalInvoice: + $ref: "#/components/schemas/InvoiceSummary" + description: The FINAL invoice of an immediate cancellation + id: type: string - error_message: - description: Human-readable detail accompanying error_code + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" + format: date-time + type: + - string + - "null" + instanceName: type: string - flag_id: - description: Unique identifier of the evaluated flag + instanceSlug: + description: As it was at the subscription's last write type: string - flag_slug: - description: URL-friendly identifier of the evaluated flag + pastDueSince: + format: date-time + type: + - string + - "null" + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE type: string - matched_rule_name: - description: Name of the targeting rule that matched, when the reason is TARGETING_MATCH + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" + format: date-time type: string - reason: - description: Why the value resolved as it did + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED type: string - variant: - description: Name of the resolved variant, when the flag has one + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" + updatedAt: + format: date-time type: string required: - - flag_slug - - flag_id - - reason + - id + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince + - createdAt + - updatedAt type: object - Impact: + CancellationChangeDelivered: additionalProperties: false properties: - count: - description: Number of resources whose stored value for this field would no longer satisfy the candidate schema - format: int64 - type: integer - samples: - description: Up to 5 impacted resources, for display - items: - $ref: "#/components/schemas/ImpactSample" + anchorAt: + description: The instant its periods are counted from + format: date-time + type: string + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time type: - - array + - string - "null" - required: - - count - - samples - type: object - ImpactSample: - additionalProperties: false - properties: - name: - description: Resource display name + canceledAt: + format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - slug: - description: Resource slug + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY type: string - required: - - name - - slug - type: object - Instance: - additionalProperties: false - properties: + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's + type: + - string + - "null" createdAt: - description: Timestamp when the instance was created - examples: - - "2023-10-01T12:00:00Z" format: date-time - readOnly: true type: string - createdBy: - $ref: "#/components/schemas/User" - description: User who created this instance - readOnly: true - customerId: - description: ID of the customer associated with this instance + currency: examples: - - 123e4567-e89b-12d3-a456-426614174003 + - EUR type: string - customerSlug: - description: Slug of the customer associated with this instance - examples: - - acme-corp - readOnly: true + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time type: string - deploymentZoneId: - description: ID of the deployment zone associated with this instance (optional) - examples: - - 123e4567-e89b-12d3-a456-426614174005 + currentPeriodStart: + format: date-time type: string - deploymentZoneSlug: - description: Slug of the deployment zone associated with this instance (optional) - examples: - - aws-us-west-2 - readOnly: true + customerName: type: string - description: - description: Brief description of the instance - examples: - - This is a sample instance description + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - endLicenseDate: - description: End date of the license for this instance - examples: - - "2024-10-01T12:00:00Z" + customerSlug: + type: string + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" + format: int32 + type: integer + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 + type: + - integer + - "null" + effectiveAt: + description: The boundary the cancellation takes effect at format: date-time type: string id: - description: Unique identifier for the instance - examples: - - 123e4567-e89b-12d3-a2456-426614174000 - readOnly: true - type: string - integrations: - additionalProperties: - $ref: "#/components/schemas/InstanceIntegration" - description: Integrations grouped by adapter name - type: object - licenseId: - description: ID of the license version this instance is pinned to. An ARCHIVED version cannot be assigned, on create or by changing the license (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); an instance already on a version that is archived later keeps it. A DRAFT version can be assigned, to try it before publishing. - examples: - - 123e4567-e89b-12d3-a456-426614174004 - type: string - licenseSlug: - description: Slug of the license associated with this instance - examples: - - my-license - readOnly: true type: string - lifecycleStage: - description: "Commercial lifecycle stage of the instance (free-form; suggested: TRIAL, ACTIVE, AT_RISK, CHURNED)" - examples: - - ACTIVE - readOnly: true + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" + format: date-time + type: + - string + - "null" + instanceName: type: string - metadata: - additionalProperties: {} - description: Metadata for the instance - examples: - - key: value - type: object - name: - description: Name of the instance - examples: - - My Instance - minLength: 1 + instanceSlug: + description: As it was at the subscription's last write type: string - slug: - description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. - examples: - - my-instance - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + pastDueSince: + format: date-time + type: + - string + - "null" + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE type: string - startLicenseDate: - description: Start date of the license for this instance - examples: - - "2023-10-01T12:00:00Z" + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" format: date-time type: string status: - description: Operational status of the instance enum: - - HEALTHY - - DEGRADED - - INCIDENT - - MAINTENANCE - examples: - - HEALTHY - readOnly: true + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED type: string + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" updatedAt: - description: Timestamp when the instance was last updated - examples: - - "2023-10-01T12:00:00Z" format: date-time - readOnly: true type: string - updatedBy: - $ref: "#/components/schemas/User" - description: User who last updated this instance - readOnly: true + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string required: + - effectiveAt - id - - createdBy + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince - createdAt - - updatedBy - updatedAt - - name - - description - - customerId - - customerSlug - - licenseId - - licenseSlug - - startLicenseDate - - endLicenseDate type: object - InstanceBody: + x-kaiten-payload: "#/components/schemas/CancellationChange" + CancellationDelivered: additionalProperties: false properties: - customerExternalId: - description: External identifier of the related customer in the same adapter - examples: - - rec_customer_123 - type: string - deploymentZoneId: - description: Deployment zone ID - examples: - - 123e4567-e89b-12d3-a456-426614174005 + anchorAt: + description: The instant its periods are counted from + format: date-time type: string - description: - description: Instance description - examples: - - This is a sample instance description + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL type: string - endLicenseDate: - description: License end date - examples: - - "2024-10-01T12:00:00Z" + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time + type: + - string + - "null" + canceledAt: format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - error: - description: Optional integration error message + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY type: string - integrationMetadata: - additionalProperties: {} - description: Adapter-specific integration metadata - type: object - licenseId: - description: License ID - examples: - - 123e4567-e89b-12d3-a456-426614174004 + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's + type: + - string + - "null" + createdAt: + format: date-time type: string - metadata: - additionalProperties: {} - description: Instance metadata - type: object - name: - description: Instance name + currency: examples: - - My Instance - minLength: 1 + - EUR type: string - slug: - description: Instance slug - examples: - - my-instance - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time type: string - startLicenseDate: - description: License start date - examples: - - "2023-10-01T12:00:00Z" + currentPeriodStart: format: date-time type: string - webUrl: - description: Absolute http(s) link to the record in the third-party system - examples: - - https://app.attio.com/w/acme/record/rec_12345 + customerName: type: string - type: object - InstanceEntitlementCapExceeded: - additionalProperties: false - properties: - entitlement_slug: - description: URL-friendly identifier of the entitlement + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - overage: - description: Amount by which usage exceeds the cap - format: double - type: number - threshold: - description: The entitlement cap - format: double - type: number - value: - description: Current usage value, including overage - format: double - type: number - required: - - entitlement_slug - - threshold - - value - - overage - type: object - InstanceEntitlementUsagePeriodRolledOver: - additionalProperties: false - properties: - closed_period_end: + customerSlug: + type: string + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" + format: int32 + type: integer + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 + type: + - integer + - "null" + finalInvoiceId: + description: The FINAL invoice; null when the cancellation issued none (a trial) + type: + - string + - "null" + id: + type: string + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" format: date-time type: - string - "null" - closed_period_start: + instanceName: + type: string + instanceSlug: + description: As it was at the subscription's last write + type: string + mode: + description: How it was canceled; a trial is canceled at once in either mode + enum: + - AT_PERIOD_END + - IMMEDIATE + type: string + pastDueSince: format: date-time type: - string - "null" - entitlement_id: + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE type: string - entitlement_slug: + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" + format: date-time type: string - event_count: - format: int32 - type: integer - instance_id: + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED type: string - is_synthetic: - type: boolean - new_period_start: + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" + updatedAt: format: date-time type: string - value: - format: double - type: number + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string required: - - entitlement_id - - entitlement_slug - - instance_id - - value - - event_count - - closed_period_start - - closed_period_end - - new_period_start - - is_synthetic + - mode + - finalInvoiceId + - id + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince + - createdAt + - updatedAt type: object - InstanceEntitlementUsageReportAccepted: + x-kaiten-payload: "#/components/schemas/Cancellation" + CheckoutConfirmation: additionalProperties: false properties: - behavior: - description: "Report behavior: append folds the value into the stored total, set overwrites it" - enum: - - append - - set + session: + $ref: "#/components/schemas/CheckoutSession" + setupExpiresAt: + description: requires_payment_method only + format: date-time type: string - entitlement_slug: - description: URL-friendly identifier of the entitlement the usage was reported against + setupUrl: + description: "requires_payment_method only: the hosted page that saves the payment method" type: string - event_count: - description: Number of reports folded into the current usage window, including this one - format: int32 - type: integer status: - description: Whether the report was persisted enum: - - ACCEPTED - - REJECTED + - requires_payment_method + - awaiting_payment + - completed type: string - value: - description: Value carried by the report - format: double - type: number required: - - entitlement_slug - - value - - behavior - - event_count - status + - session type: object - InstanceEntitlementUsageWarningThresholdReached: + CheckoutPayment: additionalProperties: false properties: - boundary: - description: Usage value at which the warning fires (cap * warningThresholdPercent / 100) - format: double - type: number - entitlement_slug: - description: URL-friendly identifier of the entitlement + hostedInvoiceUrl: + type: + - string + - "null" + status: + enum: + - paid + - requires_action + - failed + - processing + - not_required type: string - threshold: - description: The entitlement cap - format: double - type: number - value: - description: Current usage value - format: double - type: number required: - - entitlement_slug - - threshold - - boundary - - value + - status + - hostedInvoiceUrl type: object - InstanceEntitlementValueGet: + CheckoutSession: additionalProperties: false properties: - entitlement_id: - description: Unique identifier for the entitlement - type: string - entitlement_slug: - description: URL-friendly identifier of the entitlement - type: string - instance_id: - description: Unique identifier for the instance the value was read for + activationInvoice: + description: Once an ACTIVATION was issued + oneOf: + - $ref: "#/components/schemas/SessionInvoice" + - enum: + - null + type: "null" + addOns: + items: + $ref: "#/components/schemas/SessionAddonSelection" + type: array + billingPeriod: type: string - license_id: - description: Unique identifier for the license carrying the entitlement + clientReferenceId: + type: + - string + - "null" + completedAt: + format: date-time + type: + - string + - "null" + createdAt: + format: date-time type: string - organization_id: - description: Unique identifier for the owning organization + customerSlug: + description: Set by the completion + type: + - string + - "null" + dueToday: + $ref: "#/components/schemas/DueToday" + description: The voucher's discount shows as a line; the code itself is never echoed + email: type: string - timestamp: - description: When the value was read + expiresAt: format: date-time type: string - type: - description: Discriminator of the resolved entitlement value - examples: - - number + instanceSlug: + description: Set by the completion + type: + - string + - "null" + licenseSlug: type: string - value: - description: Resolved entitlement value at read time - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type + payment: oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" + - $ref: "#/components/schemas/CheckoutPayment" + - enum: + - null + type: "null" + priceId: + type: string + requiresPaymentMethod: + type: boolean + sessionId: + type: string + status: + description: A session still OPEN after expiresAt reads ABANDONED + enum: + - OPEN + - AWAITING_PAYMENT + - COMPLETED + - ABANDONED + - EXPIRED + type: string + trialEndsAt: + format: date-time + type: + - string + - "null" + trialRefusedReason: + type: + - string + - "null" required: - - entitlement_id - - instance_id - - license_id - - organization_id - - type - - value - - timestamp - - entitlement_slug + - sessionId + - status + - email + - clientReferenceId + - licenseSlug + - billingPeriod + - priceId + - addOns + - dueToday + - trialEndsAt + - trialRefusedReason + - requiresPaymentMethod + - expiresAt + - customerSlug + - instanceSlug + - activationInvoice + - payment + - createdAt + - completedAt type: object - InstanceIntegration: + CheckoutSessionAdmin: additionalProperties: false properties: - external_id: - description: External identifier in the third-party adapter - examples: - - rec_12345 + addOns: + items: + $ref: "#/components/schemas/SessionAddonSelection" + type: array + billingPeriod: type: string - last_error: - description: Last synchronization error message, if any + clientReferenceId: + type: + - string + - "null" + closedAt: + description: When it became COMPLETED, ABANDONED or EXPIRED; null while OPEN or AWAITING_PAYMENT + format: date-time + type: + - string + - "null" + completedAt: + format: date-time + type: + - string + - "null" + createdAt: + format: date-time type: string - metadata: - additionalProperties: {} - description: Adapter-specific integration metadata - type: object - synced_at: - description: Timestamp of the last synchronization with the adapter + customerSlug: + description: Set by the completion + type: + - string + - "null" + dueToday: + $ref: "#/components/schemas/DueToday" + email: + description: As the visitor gave it. Personal data + type: string + expiresAt: format: date-time - readOnly: true type: string - web_url: - description: Absolute http(s) link to the record in the third-party system - examples: - - https://app.attio.com/w/acme/workspace/rec_12345 + instanceSlug: + description: Set by the completion + type: + - string + - "null" + licenseSlug: + type: string + priceId: + type: string + publishableKeyId: + description: The key whose page started it type: string + sessionId: + type: string + status: + description: A session still OPEN after expiresAt reads ABANDONED + enum: + - OPEN + - AWAITING_PAYMENT + - COMPLETED + - ABANDONED + - EXPIRED + type: string + trialEndsAt: + format: date-time + type: + - string + - "null" + trialRefusedReason: + type: + - string + - "null" required: - - external_id - - synced_at + - sessionId + - publishableKeyId + - status + - email + - clientReferenceId + - licenseSlug + - billingPeriod + - priceId + - addOns + - dueToday + - trialEndsAt + - trialRefusedReason + - customerSlug + - instanceSlug + - createdAt + - expiresAt + - completedAt + - closedAt type: object - InstanceIntegrationResource: + CheckoutSessionDraft: additionalProperties: false properties: - customerExternalId: - description: External identifier of the related customer in the same adapter - examples: - - rec_customer_123 - type: string - deploymentZoneId: - description: Deployment zone ID - examples: - - 123e4567-e89b-12d3-a456-426614174005 - type: string - description: - description: Instance description - examples: - - This is a sample instance description + addOns: + items: + $ref: "#/components/schemas/SubscriptionAddon" + maxItems: 50 + type: + - array + - "null" + billingPeriod: + description: MONTHLY, QUARTERLY, SEMI_ANNUAL or ANNUAL, with licenseSlug type: string - endLicenseDate: - description: License end date - examples: - - "2024-10-01T12:00:00Z" - format: date-time + clientReferenceId: + description: The vendor's opaque reference, echoed on the checkout_session events + maxLength: 255 + minLength: 1 type: string - error: - description: Last synchronization error for this integration, if any + dryRun: + description: Compute what would be due today, and write nothing + type: boolean + email: + description: The buyer's e-mail. It becomes the new customer's billing e-mail at completion. Required unless dryRun. + maxLength: 254 type: string - integrationMetadata: - additionalProperties: {} - description: Adapter-specific instance integration metadata - type: object - licenseId: - description: License ID - examples: - - 123e4567-e89b-12d3-a456-426614174004 + licenseSlug: + description: "A public plan's version, with billingPeriod: its default ACTIVE FLAT_FEE price of that period" + maxLength: 255 type: string - name: - description: Instance name - examples: - - My Instance + priceId: + description: A FLAT_FEE price of a public plan. Not with licenseSlug and billingPeriod. type: string - slug: - description: Instance slug - examples: - - my-instance + returnUrl: + description: "Where the payment-method setup page sends the visitor back, with kaiten_checkout_session= appended: https, or http on localhost, on an origin the key allows. Required unless dryRun." + maxLength: 2048 type: string - startLicenseDate: - description: License start date - examples: - - "2023-10-01T12:00:00Z" - format: date-time + trial: + description: false declines the licence's trial + type: boolean + turnstileToken: + description: "The token the Turnstile widget gave the page. Single-use: a dryRun spends one too." + maxLength: 2048 + minLength: 1 type: string - webUrl: - description: Absolute http(s) link to the record in the third-party system - examples: - - https://app.attio.com/w/acme/workspace/rec_12345 + voucherCode: + description: "A code to apply, checked against the selection, with no customer yet: a voucher restricted to a customer never applies, and one without a PRICE part only when nothing is due today. Its discount counts in dueToday; the completion, or the payment, records its redemption. Any refusal answers 422 .VoucherInvalid" + maxLength: 64 type: string required: - - description - - endLicenseDate - - licenseId - - integrationMetadata - - name - - slug - - startLicenseDate + - turnstileToken type: object - InstanceLifecycleStageChanged: + CheckoutSessionStart: additionalProperties: false properties: - changed_by: - format: uuid + dueToday: + $ref: "#/components/schemas/DueToday" + expiresAt: + description: "open only: 24 h after the start" + format: date-time type: string - instance_id: - format: uuid + requiresPaymentMethod: + description: Whether the confirm will send the visitor to a payment-method setup page + type: boolean + sessionId: + description: open only type: string - new_lifecycle_stage: + status: + enum: + - preview + - open + type: string + token: + description: "open only: the session's ksc_, returned once. Keep it in memory and sessionStorage, never in a URL." + type: string + trialEndsAt: + description: The end of the trial the subscription would start with; null without one + format: date-time type: - string - "null" - previous_lifecycle_stage: + trialRefusedReason: + description: "ALREADY_CONSUMED: a customer with this e-mail already had a trial of the family, so none is given; null otherwise" type: - string - "null" required: - - instance_id - - previous_lifecycle_stage - - new_lifecycle_stage - - changed_by + - status + - dueToday + - trialEndsAt + - trialRefusedReason + - requiresPaymentMethod type: object - InstanceStatusChanged: + CloseBacklog: additionalProperties: false properties: - changed_by: - format: uuid - type: string - instance_id: - format: uuid - type: string - new_status: - enum: - - HEALTHY - - DEGRADED - - INCIDENT - - MAINTENANCE - type: string - previous_status: - enum: - - HEALTHY - - DEGRADED - - INCIDENT - - MAINTENANCE - type: string + count: + format: int64 + type: integer + oldestDueAt: + format: date-time + type: + - string + - "null" required: - - instance_id - - previous_status - - new_status - - changed_by + - count + - oldestDueAt type: object - License: + ClosePeriodsReport: additionalProperties: false properties: - createdAt: - description: Timestamp when this license was created - examples: - - "2023-10-01T12:00:00Z" - format: date-time - readOnly: true - type: string - description: - description: Detailed description of the license capabilities and restrictions - examples: - - This license provides access to all enterprise features including advanced analytics, priority support, and unlimited users. - maxLength: 500 + closed: + description: Periods closed into an invoice + format: int64 + type: integer + examined: + description: Subscriptions the batch looked at + format: int64 + type: integer + hasMore: + description: Set when due subscriptions are left beyond the batch + type: boolean + held: + description: Of those, invoices held as DRAFTs for their usage journal + format: int64 + type: integer + invoices: + items: + $ref: "#/components/schemas/ClosedInvoice" + type: array + skipped: + description: "Subscriptions left for later: held by another transaction, their meters' clock not at the boundary, or failed (logged)" + format: int64 + type: integer + required: + - examined + - closed + - held + - skipped + - hasMore + - invoices + type: object + ClosePeriodsScope: + additionalProperties: false + properties: + instanceSlug: + description: Close only this instance's subscription type: string - familyId: - description: "Identifier of the license family this license is a version of. Stable across renames and new versions: two licenses sharing it are the same product. Always present in responses. On create, optional: names the family this license is a new version of, as familySlug does by slug; omit both to create a new family. On update, optional and must be the license's own family: a version cannot move between families (UpdateLicense.FamilyNotReassignable)." + type: object + ClosedInvoice: + additionalProperties: false + properties: + boundaryAt: + format: date-time + type: string + held: + description: Set when the invoice is a held DRAFT + type: boolean + id: + type: string + instanceSlug: + type: string + kind: + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + status: + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + required: + - id + - instanceSlug + - kind + - boundaryAt + - status + - held + type: object + Command: + additionalProperties: false + properties: + entitlementSlug: + type: string + required: + - entitlementSlug + type: object + CompatibleLicenseFamilies: + additionalProperties: false + properties: + familySlugs: examples: - - 123e4567-e89b-12d3-a456-426614174000 + - - pro + items: + type: string + type: array + required: + - familySlugs + type: object + CompletedPaymentMethodSession: + additionalProperties: false + properties: + paymentMethod: + $ref: "#/components/schemas/PaymentMethodLabels" + required: + - paymentMethod + type: object + Component: + additionalProperties: false + properties: + createdAt: + description: Timestamp when the component was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true type: string - familySlug: - description: "Write-only on create: slug of the family this license is a new version of. familyId names the same family by identifier; sent together they must agree. Omit both to create a new family, of which this license becomes version 1. Never returned -- see familyId. Refused on update: a version cannot move between families." + createdBy: + $ref: "#/components/schemas/User" + description: User who created this component + readOnly: true + description: + description: Description of the component examples: - - enterprise-license - maxLength: 100 - minLength: 2 - pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + - Main API gateway component + maxLength: 500 type: string - writeOnly: true id: - description: Unique identifier for the license + description: Unique identifier for the component examples: - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string - isDefault: - description: "Whether this version is the one its family puts forward: at most one version per family, and only a PUBLISHED one (CreateLicense.DefaultMustBePublished, UpdateLicense.DefaultMustBePublished). Resolution hands it out ahead of the highest-numbered published version, so it keeps being served when a newer version is published. Setting it takes the flag from the family's previous default, which gets a LICENSE_UPDATED event of its own. On update, sending false unsets it; the default cannot be archived until another version takes its place or the flag is unset (ArchiveLicense.DefaultMustBePublished)." - examples: - - false - type: boolean - lifecycleState: - description: Whether this version may be served. Always present in responses. Optional on create, where it is DRAFT or PUBLISHED and PUBLISHED when omitted; ARCHIVED is refused (CreateLicense.LifecycleStateNotSettable), since a version is archived by withdrawing it from sale. After that it changes only through publish-license (DRAFT to PUBLISHED), archive-license (PUBLISHED to ARCHIVED) and unarchive-license (ARCHIVED to PUBLISHED); update accepts the stored state and refuses any other (UpdateLicense.LifecycleStateNotSettable). Only a PUBLISHED version can be the family's default. An ARCHIVED version cannot be assigned to an instance (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); instances already on it keep it. - enum: - - DRAFT - - PUBLISHED - - ARCHIVED - examples: - - PUBLISHED - type: string name: - description: Display name of the license + description: Name of the component examples: - - Enterprise License + - api-gateway maxLength: 100 minLength: 1 type: string + previousComponentId: + description: Reference to the previous version of this component + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string slug: - description: Optional URL-friendly identifier, unique per organization. When omitted, a new version of an existing family takes {familySlug}-v{version}, and any other license gets one generated from its name. A license that opens a new family gives the family its slug. + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. examples: - - enterprise-license + - api-gateway-v1-2-3 maxLength: 100 minLength: 2 pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string - type: - description: Type of the license determining its capabilities and restrictions - enum: - - DEVELOPMENT - - TRIAL - - PAID - - COMMUNITY - examples: - - paid - type: string - updatedAt: - description: Timestamp when this license was last updated - examples: - - "2023-10-02T12:00:00Z" - format: date-time - readOnly: true - type: string version: - description: "Version of the license within its family, assigned by the server when the version is created and never changed afterwards: the version's derived slug ({familySlug}-v{version}, see slug) is built from it. A number is never reused within a family, even after its version is deleted. Create refuses any value (CreateLicense.VersionNotSettable); update accepts only the stored one, so a representation can be written back as it was read (UpdateLicense.VersionNotSettable)." - examples: - - "1" - readOnly: true - type: string - versionName: - description: Optional human-readable name for the license version. Defaults to "Version - ". + description: Version of the component examples: - - Winter 2024 Release - maxLength: 50 + - v1.2.3 + maxLength: 100 + minLength: 1 type: string required: - id - name - - description - - type - version - - isDefault + - createdBy - createdAt - - updatedAt type: object - LicenseEntitlement: + ComponentDelivered: additionalProperties: false properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string createdAt: - description: Timestamp when this entitlement was created + description: Timestamp when the component was created examples: - "2023-10-01T12:00:00Z" format: date-time @@ -1798,1396 +2279,14270 @@ components: type: string createdBy: $ref: "#/components/schemas/User" - description: User who created this entitlement - readOnly: true - entitlementGroups: - description: Groups associated with this entitlement - items: - $ref: "#/components/schemas/EntitlementGroupSummary" - readOnly: true - type: - - array - - "null" - entitlementName: - description: Name of the entitlement feature or service - examples: - - Premium Support + description: User who created this component readOnly: true + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - entitlementSlug: - description: URL-friendly identifier for the entitlement + description: + description: Description of the component examples: - - premium-support - format: string + - Main API gateway component + maxLength: 500 type: string - entitlementType: - description: Type of entitlement (BOOLEAN, NUMBER, or CONFIG) - enum: - - BOOLEAN - - NUMBER - - CONFIG + id: + description: Unique identifier for the component examples: - - NUMBER + - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string - licenseId: - description: Unique identifier for the license this entitlement belongs to + name: + description: Name of the component examples: - - a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6 - readOnly: true + - api-gateway + maxLength: 100 + minLength: 1 type: string - licenseSlug: - description: URL-friendly identifier for the license this entitlement belongs to + previousComponentId: + description: Reference to the previous version of this component examples: - - enterprise-license - readOnly: true + - 123e4567-e89b-12d3-a456-426614174000 type: string - limitCapExceededOveragePercent: - description: "Derives this grant's enforcement from its own numeric value: -1 when the value is unlimited (no cap, no overage); otherwise 0 rejects usage above the value (hard limit) and a positive percentage allows usage to exceed it by that much before being rejected (soft limit). Null for non-numeric (BOOLEAN/CONFIG) grants." + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. examples: - - 10 - format: int32 - type: integer - updatedAt: - description: Timestamp when this entitlement was last updated + - api-gateway-v1-2-3 + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + version: + description: Version of the component examples: - - "2023-10-02T12:00:00Z" - format: date-time - readOnly: true + - v1.2.3 + maxLength: 100 + minLength: 1 + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string - updatedBy: - $ref: "#/components/schemas/User" - description: User who last updated this entitlement - readOnly: true - value: - description: "Typed entitlement value. Supported shapes: {type:number,value:number}, {type:boolean,value:boolean}, {type:object,value:{...}}" - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type - oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" required: - - entitlementName - - licenseId - - licenseSlug + - id + - name + - version - createdBy - createdAt - - updatedBy - - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/Component" + ConfigEntitlementValue: + additionalProperties: false + properties: + type: + description: Value type discriminator + enum: + - object + examples: + - object + type: string + value: + additionalProperties: {} + description: Object configuration value + type: object + required: + - type - value type: object - LicenseFamilyView: + ConnectionDelivered: additionalProperties: false properties: - createdAt: - description: Timestamp when this family was created + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + connectorName: + description: The connector that configures it examples: - - "2023-10-01T12:00:00Z" + - kaiten.integration.billing.stripe + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + livemode: + description: Whether the connection reaches the provider's live account rather than a test one + type: boolean + providerKind: + description: The payment provider + enum: + - STRIPE + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - providerKind + - connectorName + - livemode + type: object + x-kaiten-payload: "#/components/schemas/Connection" + Connector: + additionalProperties: false + properties: + created_at: + description: Timestamp when connector was first registered format: date-time - readOnly: true type: string - currentVersion: - $ref: "#/components/schemas/License" - description: "The version this read is about. By default the version the family resolves to: its default version if it has one, otherwise its highest-numbered PUBLISHED version. Under ?version=N it is the version asked for instead, whatever its lifecycle state. Absent when the family has no PUBLISHED version, which only a list or an include=versions read returns." - id: - description: Unique identifier for the license family + entitlement_slug: + description: Slug of the BOOLEAN entitlement a license must grant for an organization to activate this connector. Absent means the connector is ungated. examples: - - 123e4567-e89b-12d3-a456-426614174000 - readOnly: true + - connector-attio type: string - slug: - description: URL-friendly identifier of the family, unique per organization and stable across renames and new versions. Deleting the family's last version deletes the family and frees the slug. + name: + description: Stable connector identifier examples: - - enterprise-license + - kaiten.integration.crm.attio type: string - updatedAt: - description: Timestamp when a version was last added to this family, or when the family was created. Renames, lifecycle moves and default changes update the versions' own updatedAt, not this one. - examples: - - "2023-10-02T12:00:00Z" + settings_schema: + additionalProperties: {} + description: JSON schema used to validate connector settings payloads + type: object + updated_at: + description: Timestamp when connector registration was last updated format: date-time - readOnly: true type: string - versionCount: - description: Number of versions in this family, whatever their lifecycle state + version: + description: Registered connector version examples: - - 3 - format: int32 - type: integer - versions: - description: "Every version of the family, oldest first. Only returned when explicitly requested with include=versions, and then never null: a family always has at least one version." - items: - $ref: "#/components/schemas/License" - type: array + - 1.0.0 + type: string required: - - id - - slug - - versionCount - - createdAt - - updatedAt + - name + - version + - settings_schema + - created_at + - updated_at type: object - List: + ConnectorActivation: additionalProperties: false properties: - data: - description: The page, newest first - items: - $ref: "#/components/schemas/Notification" - type: array - nextCursor: - description: Opaque cursor for the next page; null on the last page - type: - - string - - "null" - unreadCount: - description: Unread notifications, capped (see MaxUnreadCount) - format: int64 - type: integer + activatedAt: + description: When this organization first activated the connector + format: date-time + type: string + connectorName: + description: Stable connector identifier + examples: + - kaiten.integration.crm.attio + type: string required: - - data - - nextCursor - - unreadCount + - connectorName + - activatedAt type: object - ManifestEnvelope: + ConnectorSettings: additionalProperties: false properties: - flags: - items: - $ref: "#/components/schemas/ManifestFlag" - type: - - array - - "null" + configured: + description: Whether settings were ever stored for this connector. A connector never configured reads false, with empty settings, rather than 404. + readOnly: true + type: boolean + connector_name: + description: Stable connector identifier + examples: + - kaiten.integration.crm.attio + readOnly: true + type: string + settings: + additionalProperties: {} + description: Connector settings payload + type: object required: - - flags + - connector_name + - settings + - configured type: object - ManifestFlag: + ConnectorSettingsSchema: additionalProperties: false properties: - defaultValue: {} - description: + connector_name: + description: Stable connector identifier + examples: + - kaiten.integration.crm.attio type: string - key: + schema: + additionalProperties: {} + description: JSON schema describing supported connector settings payload + type: object + version: + description: Registered connector version + examples: + - 1.0.0 type: string - name: + required: + - connector_name + - version + - schema + type: object + ConnectorState: + additionalProperties: false + properties: + activated: + description: Whether this organization has turned the connector on + type: boolean + activatedAt: + description: When this organization activated the connector. Absent when it has not. + format: date-time type: string - type: + available: + description: Whether this installation has the connector registered + type: boolean + connectorName: + description: Stable connector identifier + examples: + - kaiten.integration.crm.attio + type: string + entitled: + description: Whether this organization's license includes the connector. True for an ungated connector. + type: boolean + version: + description: Registered connector version. Absent when the connector is not available. + examples: + - 1.0.0 type: string required: - - key - - type - - defaultValue + - connectorName + - available + - entitled + - activated type: object - MarkReadResult: + CreateRole: additionalProperties: false properties: - unreadCount: - description: Unread notifications after the change - format: int64 - type: integer - updated: - description: Notifications newly marked read - format: int64 - type: integer + description: + maxLength: 1000 + type: string + name: + maxLength: 200 + minLength: 1 + type: string + permissions: + items: + type: string + type: array + slug: + maxLength: 64 + minLength: 1 + pattern: ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ + type: string required: - - updated - - unreadCount + - slug + - name + - description + - permissions type: object - MetadataField: + CreatedCustomerSession: additionalProperties: false properties: - archivedAt: - description: Timestamp when the field was archived (soft delete). Absent if active. + customerSlug: + type: string + expiresAt: format: date-time - readOnly: true type: string + id: + description: Revoke it with POST /customer-sessions/{id}/revoke + type: string + instanceSlug: + type: string + token: + description: "The kst_ token: hand it to the customer's browser, which sends it as Authorization: Bearer on /api/public/session routes. Shown once; only its digest is stored." + examples: + - kst_3q2-7wEXAMPLEtokenBodyOfFortyThreeCharactersX + type: string + required: + - id + - token + - expiresAt + - customerSlug + type: object + Customer: + additionalProperties: false + properties: + billingEmail: + description: "Where invoices sent to this customer are delivered, at most 254 characters (CreateCustomer.InvalidBillingEmail). Absent when there is none. On update, omit it or send null to keep the stored value, or send an empty string to remove it. Personal data: never included in event payloads." + examples: + - billing@example.tld + type: + - string + - "null" createdAt: - description: Timestamp when the field was created + description: Timestamp when the customer was created examples: - - "2026-05-26T12:00:00Z" + - "2023-10-01T12:00:00Z" format: date-time readOnly: true type: string createdBy: $ref: "#/components/schemas/User" - description: User who created this field + description: User who created this customer readOnly: true - displayOrder: - description: Sort order in the admin UI; ties are broken by createdAt ASC. Settable only through POST /metadata-fields/reorder; never echoed back on update. + domain: + description: Optional customer domain name examples: - - 0 - format: int32 - type: integer + - example.tld + pattern: ^[a-z0-9_-]+(\.[a-z0-9_-]+)+$ + type: string + externalCustomerId: + description: Optional identifier for the customer in an external system + examples: + - external-customer-id-12345 + type: + - string + - "null" id: - description: Unique identifier + description: Unique identifier for the customer examples: - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string - jsonSchema: - additionalProperties: {} - description: "JSON Schema 2020-12 document describing the value space. Example: {\"type\":\"string\",\"enum\":[\"a\",\"b\"]}" + integrations: + additionalProperties: + $ref: "#/components/schemas/CustomerIntegration" + description: Integrations grouped by adapter name type: object - key: - description: Stable identifier, unique per (organization, resource type) on non-archived rows. Immutable after creation; update must echo back the stored value. + name: + description: Name of the customer examples: - - region - maxLength: 100 + - Awesome Customer minLength: 1 type: string - label: - description: Human-readable label shown in the admin UI + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. examples: - - Region + - awesome-customer maxLength: 100 - minLength: 1 - type: string - resourceType: - description: Resource type this field applies to. Immutable after creation; update must echo back the stored value. - enum: - - DEPLOYMENT_ZONE - - INSTANCE + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string updatedAt: - description: Timestamp when the field was last updated + description: Timestamp when the customer was last updated examples: - - "2026-05-26T12:00:00Z" + - "2023-10-01T12:00:00Z" format: date-time readOnly: true type: string updatedBy: $ref: "#/components/schemas/User" - description: User who last updated this field + description: User who last updated this customer readOnly: true required: - id - - resourceType - - key - - label - - jsonSchema + - name - createdBy - createdAt - updatedBy - updatedAt type: object - MetadataFieldReordered: + CustomerBilling: additionalProperties: false properties: - ids: - description: Metadata field ids in their new display order - items: - type: string + billingEmail: + description: Where the customer's invoices are sent. Personal data type: - - array + - string - "null" + providers: + description: The customer in each payment provider; none for NOOP + items: + $ref: "#/components/schemas/CustomerInProvider" + type: array required: - - ids + - billingEmail + - providers type: object - MetadataFieldSchemaProposal: + CustomerBody: additionalProperties: false properties: - jsonSchema: + domain: + description: Optional customer domain name + examples: + - example.tld + pattern: ^[a-z0-9_-]+(\.[a-z0-9_-]+)+$ + type: string + error: + description: Optional integration error message + type: string + integrationMetadata: additionalProperties: {} - description: Candidate JSON Schema 2020-12 document to evaluate existing values against. + description: Adapter-specific integration metadata type: object - required: - - jsonSchema + name: + description: Customer name + examples: + - Awesome Customer + minLength: 1 + type: string + slug: + description: Customer slug + examples: + - awesome-customer + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + webUrl: + description: Absolute http(s) link to the record in the third-party system + examples: + - https://app.attio.com/w/acme/record/rec_12345 + type: string type: object - Notification: + CustomerCreationRejectedDelivered: additionalProperties: false properties: - actionUrl: - description: Where clicking the notification goes + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - body: - description: Rendered detail line, when the event has one + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + name: + type: string + reason: + type: string + slug: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - name + - slug + - reason + type: object + x-kaiten-payload: "#/components/schemas/CustomerCreationRejected" + CustomerEventDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string createdAt: - description: When the event occurred + description: Timestamp when the customer was created + examples: + - "2023-10-01T12:00:00Z" format: date-time type: string - eventName: - description: Event this notification reports - examples: - - INSTANCE_DEPLOYED + createdBy: + $ref: "#/components/schemas/User" + description: User who created this customer + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - eventType: - description: CloudEvents type of the same event -- the key it is published under in this document's webhooks section, so a client can line a notification up with the payload schema and the description already published there + domain: + description: Customer domain name examples: - - com.kaiten.instance.v1.deployed + - example.tld type: string + externalCustomerId: + description: Identifier for the customer in an external system, null when there is none + examples: + - external-customer-id-12345 + type: + - string + - "null" id: - description: Identifier of the underlying audit trail entry + description: Unique identifier for the customer + examples: + - 123e4567-e89b-12d3-a456-426614174000 type: string - objectType: - description: What the notification is about -- the kind of page actionUrl opens, and what the list's objectType filter matches + integrations: + additionalProperties: + $ref: "#/components/schemas/CustomerIntegration" + description: Integrations grouped by adapter name + type: object + name: + description: Name of the customer examples: - - instance + - Awesome Customer type: string - readAt: - description: When this user read it; absent means unread + slug: + description: URL-friendly identifier, unique per organization + examples: + - awesome-customer + type: string + updatedAt: + description: Timestamp when the customer was last updated + examples: + - "2023-10-01T12:00:00Z" format: date-time type: string - title: - description: Rendered headline + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this customer + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - id - - eventName - - eventType - - objectType - - title + - name + - externalCustomerId + - createdBy - createdAt + - updatedBy + - updatedAt type: object - NumberEntitlementValue: + x-kaiten-payload: "#/components/schemas/CustomerEvent" + CustomerInProvider: additionalProperties: false properties: - event_count: - description: Number of events accumulated (server-computed) - examples: - - 2 - format: int32 - readOnly: true - type: integer - type: - description: Value type discriminator + externalCustomerId: + description: The customer's id in the provider + type: string + paymentMethod: + description: Its default payment method; null when there is none + oneOf: + - $ref: "#/components/schemas/PaymentMethodLabels" + - enum: + - null + type: "null" + providerKind: enum: - - number - examples: - - number + - STRIPE type: string - value: - description: Numeric value - examples: - - 42.5 - format: double - type: number + syncedAt: + format: date-time + type: + - string + - "null" + webUrl: + description: The customer's page in the provider's dashboard + type: + - string + - "null" required: - - type - - value + - providerKind + - externalCustomerId + - webUrl + - syncedAt + - paymentMethod type: object - PageAuditTrail: + CustomerIntegration: additionalProperties: false properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/AuditTrail" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + external_id: + description: External identifier in the third-party adapter + examples: + - rec_12345 type: string - required: - - items - - hasMore - type: object - PageComponent: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/Component" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + last_error: + description: Last synchronization error message, if any type: string - required: - - items - - hasMore - type: object - PageCustomer: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/Customer" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + metadata: + additionalProperties: {} + description: Adapter-specific integration metadata + type: object + synced_at: + description: Timestamp of the last synchronization with the adapter + format: date-time + readOnly: true type: string - required: - - items - - hasMore - type: object - PageDeploymentZone: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/DeploymentZone" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + web_url: + description: Absolute http(s) link to the record in the third-party system + examples: + - https://app.attio.com/w/acme/company/rec_12345 type: string required: - - items - - hasMore + - external_id + - synced_at type: object - PageEntitlement: + CustomerIntegrationResource: additionalProperties: false properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/Entitlement" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + domain: + description: Optional customer domain name + examples: + - example.tld type: string - required: - - items - - hasMore - type: object - PageEntitlementGroup: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/EntitlementGroup" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + error: + description: Last synchronization error for this integration, if any type: string - required: - - items - - hasMore - type: object - PageFeatureFlag: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/FeatureFlag" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + integrationMetadata: + additionalProperties: {} + description: Adapter-specific customer integration metadata + type: object + name: + description: Customer name + examples: + - Awesome Customer type: string - required: - - items - - hasMore - type: object - PageInstance: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/Instance" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + slug: + description: Customer slug + examples: + - awesome-customer type: string - required: - - items - - hasMore - type: object - PageLicense: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/License" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + webUrl: + description: Absolute http(s) link to the record in the third-party system + examples: + - https://app.attio.com/w/acme/company/rec_12345 type: string required: - - items - - hasMore + - integrationMetadata + - name + - slug type: object - PageLicenseEntitlement: + CustomerSessionDraft: additionalProperties: false properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/LicenseEntitlement" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + customerSlug: + description: The customer the session acts for + examples: + - acme type: string + instanceSlug: + description: "Binds the session to one of the customer's instances; checkout, cancel and the other subscription routes need it. Omitted: the session reads the whole customer." + examples: + - acme-prod + type: string + ttlSeconds: + description: "How long the session lives, in seconds: 300 to 3600 (CreateCustomerSession.InvalidTtl otherwise). Default 30 minutes; mint a new one before it ends." + examples: + - 1800 + format: int32 + type: integer required: - - items - - hasMore + - customerSlug type: object - PageLicenseFamilyView: + DefaultVariant: + discriminator: + mapping: + basic: "#/components/schemas/Basic" + rollout_date: "#/components/schemas/RolloutDate" + rollout_percentage: "#/components/schemas/RolloutPercentage" + propertyName: type + oneOf: + - $ref: "#/components/schemas/Basic" + - $ref: "#/components/schemas/RolloutDate" + - $ref: "#/components/schemas/RolloutPercentage" + DeletedAddonDelivered: additionalProperties: false properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/LicenseFamilyView" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + familySlug: + type: string + id: + type: string + slug: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - - items - - hasMore + - id + - slug + - familySlug type: object - PageMetadataField: + x-kaiten-payload: "#/components/schemas/DeletedAddon" + DeploymentDelivered: additionalProperties: false properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/MetadataField" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when the deployment was recorded + examples: + - "2023-10-01T12:00:00Z" + format: date-time + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who recorded this deployment + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + deploymentZoneId: + description: Deployment zone the release was deployed to + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + id: + description: Unique identifier for this deployment event + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + releaseId: + description: Release that was deployed + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - - items - - hasMore + - id + - deploymentZoneId + - releaseId + - createdBy + - createdAt type: object - PageRelease: + x-kaiten-payload: "#/components/schemas/Deployment" + DeploymentZone: additionalProperties: false properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/Release" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + createdAt: + description: Timestamp when the deployment zone was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true type: string - required: - - items - - hasMore - type: object - PageServiceAccount: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/ServiceAccount" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + createdBy: + $ref: "#/components/schemas/User" + description: User who created this deployment zone + readOnly: true + description: + description: Description of the deployment zone + examples: + - "2023-10-01T12:00:00Z" type: string - required: - - items - - hasMore - type: object - PageToken: - additionalProperties: false - properties: - hasMore: - description: Whether more rows exist beyond this page. - type: boolean - items: - items: - $ref: "#/components/schemas/Token" - type: array - nextCursor: - description: Opaque cursor for the next page. Absent when hasMore is false. + id: + description: Unique identifier for the deployment zone + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true type: string - required: - - items - - hasMore - type: object - PatchInstanceBody: - additionalProperties: false - properties: - lifecycleStage: - description: "Commercial lifecycle stage (free-form; suggested: TRIAL, ACTIVE, AT_RISK, CHURNED). When omitted, the current stage is preserved; clearing it back to unset is not supported." + metadata: + additionalProperties: {} + description: Typed metadata for the deployment zone, validated against the org-scoped MetadataField schema examples: - - AT_RISK + - region: eu-west-1 + type: object + name: + description: Name of the deployment zone + examples: + - AWS us-west-2 minLength: 1 type: string - status: - description: Operational status of the instance - enum: - - HEALTHY - - DEGRADED - - INCIDENT - - MAINTENANCE + releaseId: + description: Currently deployed release ID examples: - - INCIDENT + - 123e4567-e89b-12d3-a456-426614174000 + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - aws-us-west-2 + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + type: + description: "Environment class of the deployment zone. Free-form: any value is stored as sent, but production, staging and development are the ones the console labels, and feature-flag targeting rules match on this field as __kaiten.deploymentZone.type." + examples: + - production + type: string + updatedAt: + description: Timestamp when the deployment zone was last updated + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated the deployment zone + readOnly: true + required: + - id + - name + - type + - description + - createdBy + - createdAt + - updatedBy + - updatedAt type: object - PlainToken: + DeploymentZoneDelivered: additionalProperties: false properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string createdAt: + description: Timestamp when the deployment zone was created + examples: + - "2023-10-01T12:00:00Z" format: date-time readOnly: true type: string createdBy: $ref: "#/components/schemas/User" + description: User who created this deployment zone readOnly: true - expiresAt: - format: date-time + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + description: + description: Description of the deployment zone + examples: + - "2023-10-01T12:00:00Z" type: string id: + description: Unique identifier for the deployment zone + examples: + - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string + metadata: + additionalProperties: {} + description: Typed metadata for the deployment zone, validated against the org-scoped MetadataField schema + examples: + - region: eu-west-1 + type: object name: + description: Name of the deployment zone + examples: + - AWS us-west-2 + minLength: 1 type: string - revokedAt: - format: date-time - readOnly: true - type: string - revokedBy: - $ref: "#/components/schemas/User" - readOnly: true - scopes: - items: - type: string - type: - - array - - "null" - serviceAccountId: - readOnly: true + releaseId: + description: Currently deployed release ID + examples: + - 123e4567-e89b-12d3-a456-426614174000 type: string slug: - description: Optional URL-friendly identifier, unique per service account. Auto-generated if not provided. + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - aws-us-west-2 maxLength: 100 minLength: 2 pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string - token: + type: + description: "Environment class of the deployment zone. Free-form: any value is stored as sent, but production, staging and development are the ones the console labels, and feature-flag targeting rules match on this field as __kaiten.deploymentZone.type." + examples: + - production + type: string + updatedAt: + description: Timestamp when the deployment zone was last updated + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated the deployment zone readOnly: true + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: + - id - name - - scopes - - createdAt + - type + - description - createdBy + - createdAt + - updatedBy + - updatedAt type: object - PreferenceChoices: + x-kaiten-payload: "#/components/schemas/DeploymentZone" + DiscountAllocation: additionalProperties: false properties: - events: - description: Events to set; events left out keep their current value - items: - $ref: "#/components/schemas/PreferenceUpdate" - type: - - array - - "null" + amount: + description: In minor units, positive + format: int64 + type: integer + targetSeq: + description: The target line + format: int64 + type: integer required: - - events + - targetSeq + - amount type: object - PreferenceEvent: + DiscountDifference: additionalProperties: false properties: - channels: - additionalProperties: - type: boolean - description: Effective value per channel - type: object - eventName: - examples: - - INSTANCE_DEPLOYED + couponId: + description: The provider's discount, empty when none was recorded type: string - eventType: - description: CloudEvents type of the same event; see Notification.eventType - examples: - - com.kaiten.instance.v1.deployed + kaitenAmount: + format: int64 + type: integer + lineId: + description: The DISCOUNT line type: string - group: - description: Catalogue group the client renders this under - examples: - - deployments + providerAmount: + description: 0 when the provider did not apply it + format: int64 + type: integer + seq: + format: int64 + type: integer + targetSeq: + format: int64 + type: integer + required: + - lineId + - seq + - targetSeq + - kaitenAmount + - providerAmount + - couponId + type: object + DueDateDifference: + additionalProperties: false + properties: + kaitenDueAt: + format: date-time type: string - label: - description: Human-readable event name + providerDueAt: + format: date-time type: string required: - - eventName - - eventType - - label - - group - - channels + - kaitenDueAt + - providerDueAt type: object - PreferenceMatrix: + DueToday: additionalProperties: false properties: - channels: - description: Channels this deployment serves, in display order + currency: + examples: + - EUR + type: string + discountTotal: + description: Minor units + format: int64 + type: integer + lines: items: - type: string + $ref: "#/components/schemas/SessionLine" type: array - events: - description: Every notifiable event - items: - $ref: "#/components/schemas/PreferenceEvent" - type: array - required: - - channels - - events - type: object - PreferenceUpdate: - additionalProperties: false - properties: - channels: - additionalProperties: - type: boolean - type: object - eventName: - type: string + subtotal: + description: Minor units + format: int64 + type: integer + total: + description: Minor units + format: int64 + type: integer required: - - eventName - - channels + - currency + - subtotal + - discountTotal + - total + - lines type: object - Problem: + Entitlement: additionalProperties: false properties: - code: - description: Stable, machine-readable error code. + aggregationMethod: + description: Method used to fold each reported usage value into the stored total (NUMBER only). Defaults to SUM. + enum: + - COUNT + - SUM + - AVERAGE + - MIN + - MAX + - LATEST examples: - - License.NotFound + - SUM type: string - detail: - description: A human-readable explanation specific to this occurrence of the problem. + createdAt: + description: Timestamp when this entitlement was created examples: - - license not found + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true type: string - errorId: - description: Correlation id for the server-side log entry describing the withheld cause. + description: + description: Description of the entitlement examples: - - 3f6b1a2c-7c1e-4d0b-9d5f-2b0a1c4e8d31 - type: string - errors: - description: Optional list of individual error details - items: - $ref: "#/components/schemas/ErrorDetail" + - Basic subscription plan with limited features type: - - array + - string - "null" - instance: - description: A URI reference that identifies the specific occurrence of the problem. - examples: - - /api/licenses/enterprise - type: string - status: - description: HTTP status code + displayOrder: + description: Sort order of this entitlement in customer-facing components (ascending). Defaults to 0. examples: - - 404 - format: int64 + - 10 + format: int32 + minimum: 0 type: integer - title: - description: A short, human-readable summary of the problem type. This value should not change between occurrences of the error. - examples: - - Not Found - type: string - type: - default: about:blank - description: A URI reference to human-readable documentation for the error. - examples: - - https://tools.ietf.org/html/rfc7231#section-6.5.4 - format: uri - type: string - type: object - ReadSelection: - additionalProperties: false - properties: - all: - description: Mark every notification read, up to now - type: boolean - ids: - description: Notifications to mark read + entitlementGroups: + description: Groups associated with this entitlement items: - type: string - maxItems: 200 + $ref: "#/components/schemas/EntitlementGroupSummary" + readOnly: true type: - array - "null" - type: object - Release: - additionalProperties: false - properties: - componentIds: - description: List of component IDs to link to this release - examples: - - - 123e4567-e89b-12d3-a456-426614174000 + groupSlugs: + description: Groups associated with this entitlement, identified by slug items: type: string type: - array - "null" writeOnly: true - components: - description: List of components included in this release, fixed at creation - items: - $ref: "#/components/schemas/Component" - readOnly: true - type: - - array - - "null" - createdAt: - description: Timestamp when the release was created - examples: - - "2023-10-01T12:00:00Z" - format: date-time - readOnly: true - type: string - createdBy: - $ref: "#/components/schemas/User" - description: User who created this release - readOnly: true - description: - description: Description of the release + icon: + description: Optional icon, stored as a provider-namespaced token (provider:name). Decoupled from any specific icon library. examples: - - "This release fixes everything. CHANGELOG: stuff was done" - maxLength: 500 + - lucide:rocket + maxLength: 64 + pattern: ^[a-z0-9]+:[a-z0-9]+(-[a-z0-9]+)*$ type: string id: - description: Unique identifier for the release + description: Unique identifier for the entitlement examples: - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string + name: + description: Name of the entitlement + examples: + - Basic Plan + minLength: 1 + type: string + resetAnchor: + description: "Phase of the periodic usage window: CALENDAR (UTC calendar-aligned), LICENSE_START (phased off the instance's license start date) or BILLING_PERIOD (phased off the instance's live subscription's billing anchor, LICENSE_START without one; with resetPeriod MONTH or YEAR only). Required exactly when resetPeriod is set; defaults to CALENDAR when omitted. Immutable once set." + enum: + - CALENDAR + - LICENSE_START + - BILLING_PERIOD + examples: + - CALENDAR + type: string + resetPeriod: + description: Cadence at which usage automatically resets for this NUMBER entitlement. NULL means a lifetime counter (default). Immutable once set -- a full-replace PUT must echo back the stored value. + enum: + - HOUR + - DAY + - WEEK + - MONTH + - YEAR + examples: + - MONTH + type: string + saleUnitFactor: + description: Number of base units one sale unit represents (1 sale unit = N base units). + examples: + - 3 + exclusiveMinimum: 0 + format: double + type: number + saleUnitPlural: + description: Plural label of the unit this entitlement is sold in, when it differs from the base unit. + examples: + - packs + maxLength: 100 + minLength: 1 + type: string + saleUnitSingular: + description: Singular label of the unit this entitlement is sold in, when it differs from the base unit. Requires the base unit labels and the full sale unit trio (saleUnitSingular, saleUnitPlural, saleUnitFactor). + examples: + - pack + maxLength: 100 + minLength: 1 + type: string slug: description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. examples: - - v1-0-0 + - basic-plan maxLength: 100 minLength: 2 pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string - version: - description: Version of the release + type: + description: Type of the entitlement + enum: + - BOOLEAN + - NUMBER + - CONFIG + - NUMBER_AI_CREDIT examples: - - v1.0.0 + - NUMBER + type: string + unitPlural: + description: Plural label of the base unit this entitlement is measured in (NUMBER type only). Must be provided together with unitSingular. + examples: + - seats + maxLength: 100 + minLength: 1 + type: string + unitSingular: + description: Singular label of the base unit this entitlement is measured in (NUMBER type only). Must be provided together with unitPlural. + examples: + - seat maxLength: 100 minLength: 1 type: string + updatedAt: + description: Timestamp when this entitlement was last updated + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + userFacing: + description: Whether this entitlement is displayed in customer-facing components (e.g. plan and pricing pages). Defaults to false. + examples: + - true + type: boolean + warningThresholdPercent: + description: Percentage of the cap at which an early-warning event fires before the cap is reached. 0 disables the warning (default). + examples: + - 80 + format: int32 + maximum: 100 + minimum: 0 + type: integer required: - id - - version - - createdBy + - name + - description - createdAt + - updatedAt type: object - ReorderMetadataFieldsInput: - additionalProperties: false - properties: - ids: - items: - type: string - type: - - array - - "null" - required: - - ids - type: object - ReportEntitlementUsageBody: + EntitlementDelivered: additionalProperties: false properties: - behavior: - description: "Report behavior: append folds the value into the stored total through the aggregation method; set overwrites it" + aggregationMethod: + description: Method used to fold each reported usage value into the stored total (NUMBER only). Defaults to SUM. enum: - - append - - set - type: string - metadata: - additionalProperties: {} - description: Optional metadata for the usage report - type: object - value: - description: Reported entitlement value, discriminated by the 'type' field. Usage reporting accepts the number variant only. - discriminator: - mapping: - boolean: "#/components/schemas/BooleanEntitlementValue" - number: "#/components/schemas/NumberEntitlementValue" - object: "#/components/schemas/ConfigEntitlementValue" - propertyName: type - oneOf: - - $ref: "#/components/schemas/NumberEntitlementValue" - - $ref: "#/components/schemas/BooleanEntitlementValue" - - $ref: "#/components/schemas/ConfigEntitlementValue" - required: - - value - type: object - RolloutDate: - additionalProperties: false - properties: - end: - $ref: "#/components/schemas/RolloutStep" - start: - $ref: "#/components/schemas/RolloutStep" - type: - enum: - - rollout_date + - COUNT + - SUM + - AVERAGE + - MIN + - MAX + - LATEST + examples: + - SUM type: string - required: - - type - - start - - end - type: object - RolloutDateTargeting: - additionalProperties: false - properties: - end: - $ref: "#/components/schemas/RolloutStep" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when this entitlement was created examples: - - date: "2024-01-31T23:59:59Z" - percentage: 100 - variant: variant_a - name: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + description: + description: Description of the entitlement examples: - - Targeting Rule 1 + - Basic subscription plan with limited features + type: + - string + - "null" + displayOrder: + description: Sort order of this entitlement in customer-facing components (ascending). Defaults to 0. + examples: + - 10 + format: int32 + minimum: 0 + type: integer + entitlementGroups: + description: Groups associated with this entitlement + items: + $ref: "#/components/schemas/EntitlementGroupSummary" + readOnly: true + type: + - array + - "null" + groupSlugs: + description: Groups associated with this entitlement, identified by slug + items: + type: string + type: + - array + - "null" + writeOnly: true + icon: + description: Optional icon, stored as a provider-namespaced token (provider:name). Decoupled from any specific icon library. + examples: + - lucide:rocket + maxLength: 64 + pattern: ^[a-z0-9]+:[a-z0-9]+(-[a-z0-9]+)*$ type: string - rule: - description: CEL expression for basicTargeting evaluation + id: + description: Unique identifier for the entitlement examples: - - user.group == 'beta' - maxLength: 10000 + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true type: string - start: - $ref: "#/components/schemas/RolloutStep" + name: + description: Name of the entitlement examples: - - date: "2024-01-01T00:00:00Z" - percentage: 0 - variant: variant_a - type: + - Basic Plan + minLength: 1 + type: string + resetAnchor: + description: "Phase of the periodic usage window: CALENDAR (UTC calendar-aligned), LICENSE_START (phased off the instance's license start date) or BILLING_PERIOD (phased off the instance's live subscription's billing anchor, LICENSE_START without one; with resetPeriod MONTH or YEAR only). Required exactly when resetPeriod is set; defaults to CALENDAR when omitted. Immutable once set." enum: - - rollout_date + - CALENDAR + - LICENSE_START + - BILLING_PERIOD + examples: + - CALENDAR type: string - required: - - type - - name - - rule - - start - - end - type: object - RolloutPercentage: - additionalProperties: false - properties: - distribution: - additionalProperties: - format: int64 - type: integer - type: object - type: + resetPeriod: + description: Cadence at which usage automatically resets for this NUMBER entitlement. NULL means a lifetime counter (default). Immutable once set -- a full-replace PUT must echo back the stored value. enum: - - rollout_percentage + - HOUR + - DAY + - WEEK + - MONTH + - YEAR + examples: + - MONTH type: string - required: - - type - - distribution - type: object - RolloutPercentageTargeting: - additionalProperties: false - properties: - distribution: - additionalProperties: - format: int64 - type: integer + saleUnitFactor: + description: Number of base units one sale unit represents (1 sale unit = N base units). examples: - - variant_a: 70 - variant_b: 30 - type: object - name: + - 3 + exclusiveMinimum: 0 + format: double + type: number + saleUnitPlural: + description: Plural label of the unit this entitlement is sold in, when it differs from the base unit. examples: - - Targeting Rule 1234 + - packs + maxLength: 100 + minLength: 1 type: string - rule: - description: CEL expression for basicTargeting evaluation + saleUnitSingular: + description: Singular label of the unit this entitlement is sold in, when it differs from the base unit. Requires the base unit labels and the full sale unit trio (saleUnitSingular, saleUnitPlural, saleUnitFactor). examples: - - user.group == 'beta' - maxLength: 10000 + - pack + maxLength: 100 + minLength: 1 + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - basic-plan + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string type: + description: Type of the entitlement enum: - - rollout_percentage + - BOOLEAN + - NUMBER + - CONFIG + - NUMBER_AI_CREDIT + examples: + - NUMBER type: string - required: - - type - - name - - rule - - distribution - type: object - RolloutStep: - additionalProperties: false - properties: - date: + unitPlural: + description: Plural label of the base unit this entitlement is measured in (NUMBER type only). Must be provided together with unitSingular. examples: - - "2024-01-15T10:00:00Z" - format: date-time + - seats + maxLength: 100 + minLength: 1 type: string - percentage: + unitSingular: + description: Singular label of the base unit this entitlement is measured in (NUMBER type only). Must be provided together with unitPlural. examples: - - 50 - format: int64 - type: integer - variant: + - seat + maxLength: 100 + minLength: 1 + type: string + updatedAt: + description: Timestamp when this entitlement was last updated examples: - - variant_b + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + userFacing: + description: Whether this entitlement is displayed in customer-facing components (e.g. plan and pricing pages). Defaults to false. + examples: + - true + type: boolean + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string + warningThresholdPercent: + description: Percentage of the cap at which an early-warning event fires before the cap is reached. 0 disables the warning (default). + examples: + - 80 + format: int32 + maximum: 100 + minimum: 0 + type: integer required: - - variant - - percentage - - date + - id + - name + - description + - createdAt + - updatedAt type: object - ServiceAccount: + x-kaiten-payload: "#/components/schemas/Entitlement" + EntitlementException: additionalProperties: false properties: createdAt: format: date-time - readOnly: true type: string - createdBy: - $ref: "#/components/schemas/User" - readOnly: true - externalId: - readOnly: true + endsAt: + description: "Null: no end" + format: date-time + type: + - string + - "null" + entitlementSlug: + type: string + entitlementType: + enum: + - NUMBER + - NUMBER_AI_CREDIT + - BOOLEAN + - CONFIG + type: string + id: + type: string + instanceSlug: + type: string + kind: + enum: + - SET + - ADD + - UNLIMITED + type: string + reason: + type: string + revokedAt: + format: date-time + type: + - string + - "null" + startsAt: + format: date-time + type: string + status: + description: "At the read: SCHEDULED before startsAt, ACTIVE in its window, EXPIRED after endsAt, REVOKED once revoked" + enum: + - SCHEDULED + - ACTIVE + - EXPIRED + - REVOKED type: string + value: + anyOf: + - type: number + - type: boolean + - additionalProperties: true + type: object + - type: "null" + description: A number for NUMBER and NUMBER_AI_CREDIT, a boolean for BOOLEAN, the CONFIG document as an object; null for UNLIMITED + required: + - id + - instanceSlug + - entitlementSlug + - entitlementType + - kind + - value + - startsAt + - endsAt + - reason + - status + - createdAt + - revokedAt + type: object + EntitlementGroup: + additionalProperties: false + properties: + description: + description: Description of the entitlement group + examples: + - All entitlements related to AI usage quotas + type: + - string + - "null" id: + description: Unique identifier for the entitlement group + examples: + - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string name: + description: Name of the entitlement group + examples: + - AI Quotas minLength: 1 type: string slug: - description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + description: URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - ai-quotas maxLength: 100 minLength: 2 pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string - tokens: - items: - $ref: "#/components/schemas/Token" - readOnly: true - type: - - array - - "null" required: + - id - name - - tokens + - description type: object - SystemTokenIssuance: + EntitlementGroupDelivered: additionalProperties: false properties: - credentialKind: - description: Credential class that performed the mint; "platform" is the system:kaiten identity - examples: - - platform - type: string - expiresAt: - description: Absent when the minted credential does not expire - format: date-time + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - issuedAt: - description: When the credential was minted - format: date-time + customerSessionId: + description: "With via customer_session: the session" + format: uuid type: string - issuedTokenId: - description: The credential that was minted + description: + description: Description of the entitlement group + examples: + - All entitlements related to AI usage quotas + type: + - string + - "null" + id: + description: Unique identifier for the entitlement group + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true type: string name: - description: Human-readable label the caller gave the minted credential + description: Name of the entitlement group examples: - - ci-deploy - type: string - platformTokenId: - description: Platform credential that performed the mint; revoking it revokes everything it issued + - AI Quotas + minLength: 1 type: string - scopes: - description: Scopes granted to the minted credential - examples: - - - read:customers - items: - type: string - type: - - array - - "null" slug: - description: Server-assigned identifier of the minted credential + description: URL-friendly identifier, unique per organization. Auto-generated if not provided. examples: - - system-kaiten-9f2c1a + - ai-quotas + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - - credentialKind - - platformTokenId - - issuedTokenId + - id - name - - slug - - scopes - - issuedAt - type: object - TargetingContext: - additionalProperties: false - properties: - roots: - description: "Identifiers the server guarantees, and what is reachable below each. Not a closed set: a host may target on its own attributes too" - items: - $ref: "#/components/schemas/TargetingContextNode" - type: array - required: - - roots + - description type: object - TargetingContextNode: + x-kaiten-payload: "#/components/schemas/EntitlementGroup" + EntitlementGroupSummary: additionalProperties: false properties: - description: - description: What this value means, shown by the editor + id: + description: Unique identifier for the entitlement group + examples: + - 123e4567-e89b-12d3-a456-426614174000 type: string - fields: - description: Named children, for an object - items: - $ref: "#/components/schemas/TargetingContextNode" - type: array - knownKeys: - description: Keys this map currently has, for completion. Not a closed set - items: - type: string - type: array name: - description: Name as it is written in a rule + description: Name of the entitlement group + examples: + - AI Quotas type: string - optional: - description: Whether this value may be absent, in which case has() is worth using - type: boolean - type: - description: Shape of this node's value - enum: - - string - - number - - boolean - - object - - map - - dyn + slug: + description: URL-friendly identifier for the entitlement group + examples: + - ai-quotas type: string - values: - $ref: "#/components/schemas/TargetingContextNode" - description: Shape shared by every entry, for a map required: + - id - name - - type + - slug type: object - TargetingRuleDraft: + EntitlementGroupUsage: additionalProperties: false properties: - rule: - description: The CEL targeting rule to check. May be blank, which is itself a mistake this reports - examples: - - __kaiten.license.familySlug == 'scale' - maxLength: 10000 + currentPeriodEnd: + description: End of the current usage window (exclusive). Null for a lifetime entitlement. + format: date-time + type: string + currentPeriodStart: + description: Start of the current usage window (inclusive). Null for a lifetime entitlement. + format: date-time + type: string + entitlementId: + description: Entitlement ID + type: string + entitlementName: + description: Entitlement name + type: string + entitlementSlug: + description: Entitlement slug type: string + entitlementType: + description: Entitlement type + type: string + licenseValue: + description: License limit value, discriminated by the 'type' field. Null when the license grants no value. + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + usageValue: + description: Current usage value, discriminated by the 'type' field. Null when nothing has been reported yet. + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" required: - - rule + - entitlementId + - entitlementSlug + - entitlementName + - entitlementType type: object - TargetingRuleIssue: + EntitlementUsage: additionalProperties: false properties: - column: - description: 1-based column the problem starts on, or 0 when it has no position - format: int64 - type: integer - endColumn: - description: 1-based column just past the problem, or 0 when only a point is known - format: int64 - type: integer - endLine: - description: 1-based line the problem ends on, or 0 when only a point is known - format: int64 + currentPeriodEnd: + description: End of the current usage window (exclusive). Null for a lifetime entitlement (no configured reset period). + examples: + - "2026-04-01T00:00:00Z" + format: date-time + type: string + currentPeriodStart: + description: Start of the current usage window (inclusive). Null for a lifetime entitlement (no configured reset period). + examples: + - "2026-03-01T00:00:00Z" + format: date-time + type: string + entitlementId: + description: Unique identifier for the entitlement + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + entitlementSlug: + description: URL-friendly identifier of the entitlement + examples: + - premium-support + type: string + licenseId: + description: Unique identifier for the license to which this entitlement usage belongs + examples: + - 123e4567-e89b-12d3-a456-426614174001 + type: string + licenseSlug: + description: URL-friendly identifier of the license + examples: + - enterprise-license + type: string + limit: + description: The effective value this usage is measured against (the licence's grant, with the instance's add-ons and boosts applied; source and provenance say which), discriminated by the 'type' field. Absent when nothing grants the entitlement a value; -1 means unlimited for a NUMBER entitlement. + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + limitCapExceededOveragePercent: + description: "How far above limit usage is still accepted, in percent: -1 unlimited, 0 a hard limit. Absent for BOOLEAN and CONFIG." + format: int32 type: integer - line: - description: 1-based line the problem starts on, or 0 when it has no position - format: int64 + provenance: + $ref: "#/components/schemas/Provenance" + description: What the licence, the add-ons and the boosts each contribute + source: + description: "license: the licence grants it (add-ons, boosts and an exception may change it); addon: an add-on attached does; exception: only the instance's exception does" + enum: + - license + - addon + - exception + type: string + value: + description: Resolved entitlement value, discriminated by the 'type' field + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + required: + - entitlementId + - entitlementSlug + - licenseId + - licenseSlug + - value + - source + type: object + EntitlementUsageDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + currentPeriodEnd: + description: End of the current usage window (exclusive). Null for a lifetime entitlement (no configured reset period). + examples: + - "2026-04-01T00:00:00Z" + format: date-time + type: string + currentPeriodStart: + description: Start of the current usage window (inclusive). Null for a lifetime entitlement (no configured reset period). + examples: + - "2026-03-01T00:00:00Z" + format: date-time + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlementId: + description: Unique identifier for the entitlement + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + entitlementSlug: + description: URL-friendly identifier of the entitlement + examples: + - premium-support + type: string + licenseId: + description: Unique identifier for the license to which this entitlement usage belongs + examples: + - 123e4567-e89b-12d3-a456-426614174001 + type: string + licenseSlug: + description: URL-friendly identifier of the license + examples: + - enterprise-license + type: string + limit: + description: The effective value this usage is measured against (the licence's grant, with the instance's add-ons and boosts applied; source and provenance say which), discriminated by the 'type' field. Absent when nothing grants the entitlement a value; -1 means unlimited for a NUMBER entitlement. + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + limitCapExceededOveragePercent: + description: "How far above limit usage is still accepted, in percent: -1 unlimited, 0 a hard limit. Absent for BOOLEAN and CONFIG." + format: int32 type: integer - message: - description: What is wrong, in the same words the write path refuses it with + provenance: + $ref: "#/components/schemas/Provenance" + description: What the licence, the add-ons and the boosts each contribute + source: + description: "license: the licence grants it (add-ons, boosts and an exception may change it); addon: an add-on attached does; exception: only the instance's exception does" + enum: + - license + - addon + - exception + type: string + value: + description: Resolved entitlement value, discriminated by the 'type' field + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - - message - - line - - column + - entitlementId + - entitlementSlug + - licenseId + - licenseSlug + - value + - source type: object - TargetingRuleRehearsal: + x-kaiten-payload: "#/components/schemas/EntitlementUsage" + ErrorDetail: additionalProperties: false properties: - evaluationError: - description: Why the engine could not answer, when it could not — e.g. the rule reads a host attribute this context does not carry. At a real evaluation the rule would simply not match + location: + description: Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' type: string - facts: + message: + description: Error message text + type: string + value: + description: The value at the given location + type: object + EvaluationFailure: + additionalProperties: false + properties: + errorCode: + description: OpenFeature compatible error code + type: string + errorDetails: + description: Error description + type: string + key: + description: Feature flag key + type: string + metadata: additionalProperties: {} - description: The __kaiten namespace as enriched for this context — what a rule actually reads. Empty sub-roots mean the context resolved nothing for them + description: Arbitrary metadata for the flag type: object - issues: - description: Problems the lint found. The rule is only run when there are none - items: - $ref: "#/components/schemas/TargetingRuleIssue" - type: array - matched: - description: Whether the rule matched this context. Meaningful only when valid and evaluationError is empty - type: boolean - valid: - description: Whether the rule lints clean - type: boolean required: - - facts - - issues - - valid - - matched + - key + - errorCode type: object - TargetingRuleTrial: + EvaluationRequest: additionalProperties: false properties: context: additionalProperties: {} - description: The evaluation context to rehearse against, as a client would send it. Anything under __kaiten is discarded, exactly as at a real evaluation type: object - rule: - description: The CEL targeting rule to rehearse + type: object + EvaluationSuccess: + additionalProperties: false + properties: + key: + description: Feature flag key + type: string + metadata: + additionalProperties: {} + description: Arbitrary metadata for the flag + type: object + reason: + description: OpenFeature reason for the evaluation examples: - - __kaiten.license.familySlug == 'scale' - maxLength: 10000 + - STATIC type: string - targetingKey: - description: The key the evaluation would be bucketed on. Resolves license and entitlement facts when it names a customer slug + value: + description: Flag evaluation result + variant: + description: string of the evaluated flag value type: string required: - - rule + - key type: object - TargetingRuleVerdict: - additionalProperties: false + EventHeaders: + additionalProperties: true properties: - issues: - description: Every problem found, positioned where it is written. Empty when the rule is accepted - items: - $ref: "#/components/schemas/TargetingRuleIssue" - type: array - valid: - description: Whether the write path would accept this rule - type: boolean - required: - - issues - - valid + baggage: + description: W3C baggage of that request, when it carried any + type: string + instance_id: + description: The instance the event concerns, set on the events of one instance. The usage events' payloads do not name it. + format: uuid + type: string + traceparent: + description: W3C trace context of the request that produced the event + type: string + tracestate: + description: W3C trace state of that request, when it carried one + type: string type: object - Targetings: - description: Polymorphic list of targeting rules (Basic, RolloutDate, RolloutPercentage) - items: - anyOf: - - $ref: "#/components/schemas/BasicTargeting" - - $ref: "#/components/schemas/RolloutDateTargeting" - - $ref: "#/components/schemas/RolloutPercentageTargeting" - type: - - array - - "null" - Token: + ExceptionEventDelivered: additionalProperties: false properties: - createdAt: - format: date-time - readOnly: true + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - createdBy: - $ref: "#/components/schemas/User" - readOnly: true - expiresAt: + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + endsAt: + description: "Null: no end" format: date-time + type: + - string + - "null" + entitlementSlug: type: string id: - readOnly: true type: string - name: + instanceSlug: + type: string + kind: + enum: + - SET + - ADD + - UNLIMITED + type: string + reason: type: string revokedAt: format: date-time - readOnly: true + type: + - string + - "null" + startsAt: + format: date-time type: string - revokedBy: - $ref: "#/components/schemas/User" - readOnly: true - scopes: - items: - type: string + value: + anyOf: + - type: number + - type: boolean + - additionalProperties: true + type: object + - type: "null" + description: A number for NUMBER and NUMBER_AI_CREDIT, a boolean for BOOLEAN, the CONFIG document as an object; null for UNLIMITED + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - instanceSlug + - entitlementSlug + - kind + - value + - startsAt + - endsAt + - reason + - revokedAt + type: object + x-kaiten-payload: "#/components/schemas/ExceptionEvent" + ExhaustedEventDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + id: + type: string + maxRedemptions: + format: int32 + type: integer + name: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - name + type: object + x-kaiten-payload: "#/components/schemas/ExhaustedEvent" + ExpiredEventDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + expiresAt: + format: date-time + type: string + id: + type: string + name: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - name + - expiresAt + type: object + x-kaiten-payload: "#/components/schemas/ExpiredEvent" + ExtraDiscount: + additionalProperties: false + properties: + amount: + format: int64 + type: integer + discountId: + type: string + externalLineId: + type: string + required: + - externalLineId + - discountId + - amount + type: object + FeatureFlag: + additionalProperties: false + properties: + default_variant: + $ref: "#/components/schemas/DefaultVariant" + description: + examples: + - This is a new feature flag type: - - array + - string - "null" - serviceAccountId: + enabled: + examples: + - true + type: boolean + event_name: + examples: + - feature_flag_event + type: string + id: + examples: + - 123e4567-e89b-12d3-a456-426614174000 readOnly: true type: string + metadata: + additionalProperties: {} + type: object + name: + examples: + - New Feature + minLength: 1 + type: string slug: - description: Optional URL-friendly identifier, unique per service account. Auto-generated if not provided. + examples: + - new-feature maxLength: 100 minLength: 2 pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ type: string + targetings: + $ref: "#/components/schemas/Targetings" + type: + enum: + - boolean + - string + - number + - object + examples: + - boolean + type: string + variants: + items: + $ref: "#/components/schemas/Variant" + type: + - array + - "null" required: + - id + - type + - variants + - targetings - name - - scopes - - createdAt - - createdBy + - description + - default_variant + - metadata + - enabled + - event_name type: object - User: + FeatureFlagDelivered: additionalProperties: false properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + default_variant: + $ref: "#/components/schemas/DefaultVariant" + description: + examples: + - This is a new feature flag + type: + - string + - "null" + enabled: + examples: + - true + type: boolean + event_name: + examples: + - feature_flag_event + type: string id: - description: ID of the user examples: - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true type: string + metadata: + additionalProperties: {} + type: object name: - description: Name of the user examples: - - John Doe + - New Feature + minLength: 1 + type: string + slug: + examples: + - new-feature + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + targetings: + $ref: "#/components/schemas/Targetings" + type: + enum: + - boolean + - string + - number + - object + examples: + - boolean + type: string + variants: + items: + $ref: "#/components/schemas/Variant" + type: + - array + - "null" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string required: - id + - type + - variants + - targetings + - name + - description + - default_variant + - metadata + - enabled + - event_name type: object - Variant: + x-kaiten-payload: "#/components/schemas/FeatureFlag" + FeatureFlagEvaluatedDelivered: additionalProperties: false properties: - description: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid type: string - name: + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + error_code: + description: Set when the evaluation failed and the default was returned + type: string + error_message: + description: Human-readable detail accompanying error_code + type: string + flag_id: + description: Unique identifier of the evaluated flag + type: string + flag_slug: + description: URL-friendly identifier of the evaluated flag + type: string + matched_rule_name: + description: Name of the targeting rule that matched, when the reason is TARGETING_MATCH + type: string + reason: + description: Why the value resolved as it did + type: string + variant: + description: Name of the resolved variant, when the flag has one + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session type: string - value: {} required: - - name - - description - - value + - flag_slug + - flag_id + - reason type: object - securitySchemes: - bearerAuth: - bearerFormat: JWT - description: "A JWT verified by the proxy in front of this API, or a Kaiten access token (`ksh_...`) — send either as `Authorization: Bearer `. The scopes listed on each operation are the ones the token must carry; `write:` implies `read:`, and `read:*` / `write:*` are wildcards over every module. `x-kaiten-scopes` lists every scope such a token can carry, including those enforced by a service in front of this API rather than by one of its operations." - scheme: bearer - type: http - x-kaiten-scopes: - - read:components - - read:customers + x-kaiten-payload: "#/components/schemas/FeatureFlagEvaluated" + FlagGrant: + additionalProperties: false + properties: + flagSlug: + examples: + - new-dashboard + type: string + variant: + description: The name of one of the flag's variants + examples: + - "on" + maxLength: 255 + minLength: 1 + type: string + required: + - flagSlug + - variant + type: object + Grant: + additionalProperties: false + properties: + entitlementSlug: + examples: + - tokens + type: string + modifierType: + description: SET replaces the value, ADD adds to it, MULTIPLY multiplies it, UNLIMITED lifts the limit. A BOOLEAN or CONFIG entitlement takes SET only + enum: + - SET + - ADD + - MULTIPLY + - UNLIMITED + type: string + modifierValue: + description: "A NUMBER grant's decimal: >= 0 for SET, > 0 for ADD and MULTIPLY, null for UNLIMITED; null on a BOOLEAN or CONFIG grant. Optional on write." + examples: + - "2" + type: + - string + - "null" + value: + anyOf: + - type: boolean + - additionalProperties: true + type: object + - type: "null" + description: "What a BOOLEAN or CONFIG SET grant sets: true or false, or the CONFIG document as an object; null on a NUMBER grant. Optional on write." + required: + - entitlementSlug + - modifierType + type: object + HandoffAcknowledgementDelivered: + additionalProperties: false + properties: + acknowledgedBy: + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + externalReference: + description: The invoice's number in the organization's accounting system + type: string + invoiceId: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - invoiceId + - acknowledgedBy + type: object + x-kaiten-payload: "#/components/schemas/HandoffAcknowledgement" + HandoffBacklog: + additionalProperties: false + properties: + oldestPendingIssuedAt: + format: date-time + type: + - string + - "null" + pending: + format: int64 + type: integer + reclaimedLast24h: + description: "Invoices claimed again after a lease expired, in the last 24 hours: a consumer that claims and does not acknowledge" + format: int64 + type: integer + required: + - pending + - oldestPendingIssuedAt + - reclaimedLast24h + type: object + HandoffBooking: + additionalProperties: false + properties: + externalReference: + description: The invoice's number in the accounting system, 1 to 255 characters + type: string + leaseId: + description: The claim's lease. When given, an invoice another claim took since is refused (AckHandoff.LeaseMismatch) + type: string + type: object + HandoffClaim: + additionalProperties: false + properties: + invoices: + description: "The leased invoices, oldest issue first, with their billing e-mail: what the accounting system needs to book them" + items: + $ref: "#/components/schemas/Invoice" + type: array + leaseId: + description: Pass it back when acknowledging an invoice of this claim + type: string + leasedUntil: + description: When the lease expires and the invoices are claimable again + format: date-time + type: string + required: + - leaseId + - leasedUntil + - invoices + type: object + HandoffClaimSize: + additionalProperties: false + properties: + leaseSeconds: + description: Lease length, 60 to 3600 seconds; 900 when omitted + format: int32 + type: integer + limit: + description: Invoices to lease, 1 to 100; 25 when omitted + format: int32 + type: integer + type: object + HeldInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdDetail: + $ref: "#/components/schemas/HoldDetail" + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - holdDetail + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/HeldInvoice" + HeldInvoices: + additionalProperties: false + properties: + byReason: + additionalProperties: + format: int64 + type: integer + type: object + count: + format: int64 + type: integer + required: + - count + - byReason + type: object + HeldPair: + additionalProperties: false + properties: + counterReportSeq: + format: int64 + type: + - integer + - "null" + detail: + type: string + entitlementId: + type: string + expected: + type: + - string + - "null" + firstSeq: + format: int64 + type: + - integer + - "null" + found: + type: + - string + - "null" + instanceId: + type: string + invariant: + type: string + lastSeq: + format: int64 + type: + - integer + - "null" + reportSeq: + description: The report the failure is at, when one is + format: int64 + type: + - integer + - "null" + required: + - instanceId + - entitlementId + - invariant + - expected + - found + - detail + - reportSeq + - firstSeq + - lastSeq + - counterReportSeq + type: object + HoldDetail: + additionalProperties: false + properties: + itemLimit: + $ref: "#/components/schemas/ProviderLineLimit" + description: "PROVIDER_ITEM_LIMIT only: the provider's line limit and how many items the invoice needs" + pairs: + items: + $ref: "#/components/schemas/HeldPair" + type: array + required: + - pairs + type: object + HoldRecord: + additionalProperties: false + properties: + heldAt: + format: date-time + type: + - string + - "null" + releaseReason: + type: + - string + - "null" + releasedAt: + format: date-time + type: + - string + - "null" + releasedBy: + description: The user who released it; null when it was released by a later check + type: + - string + - "null" + required: + - heldAt + - releasedAt + - releasedBy + - releaseReason + type: object + HoldRelease: + additionalProperties: false + properties: + reason: + description: 1 to 500 characters; .ReasonRequired otherwise + maxLength: 500 + minLength: 1 + type: string + required: + - reason + type: object + Impact: + additionalProperties: false + properties: + count: + description: Number of resources whose stored value for this field would no longer satisfy the candidate schema + format: int64 + type: integer + samples: + description: Up to 5 impacted resources, for display + items: + $ref: "#/components/schemas/ImpactSample" + type: + - array + - "null" + required: + - count + - samples + type: object + ImpactSample: + additionalProperties: false + properties: + name: + description: Resource display name + type: string + slug: + description: Resource slug + type: string + required: + - name + - slug + type: object + Instance: + additionalProperties: false + properties: + createdAt: + description: Timestamp when the instance was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this instance + readOnly: true + customerId: + description: ID of the customer associated with this instance + examples: + - 123e4567-e89b-12d3-a456-426614174003 + type: string + customerSlug: + description: Slug of the customer associated with this instance + examples: + - acme-corp + readOnly: true + type: string + deploymentZoneId: + description: ID of the deployment zone associated with this instance (optional) + examples: + - 123e4567-e89b-12d3-a456-426614174005 + type: string + deploymentZoneSlug: + description: Slug of the deployment zone associated with this instance (optional) + examples: + - aws-us-west-2 + readOnly: true + type: string + description: + description: Brief description of the instance + examples: + - This is a sample instance description + type: string + endLicenseDate: + description: End date of the license for this instance + examples: + - "2024-10-01T12:00:00Z" + format: date-time + type: string + id: + description: Unique identifier for the instance + examples: + - 123e4567-e89b-12d3-a2456-426614174000 + readOnly: true + type: string + integrations: + additionalProperties: + $ref: "#/components/schemas/InstanceIntegration" + description: Integrations grouped by adapter name + type: object + licenseId: + description: ID of the license version this instance is pinned to. An ARCHIVED version cannot be assigned, on create or by changing the license (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); an instance already on a version that is archived later keeps it. A DRAFT version can be assigned, to try it before publishing. + examples: + - 123e4567-e89b-12d3-a456-426614174004 + type: string + licenseSlug: + description: Slug of the license associated with this instance + examples: + - my-license + readOnly: true + type: string + lifecycleStage: + description: "Commercial lifecycle stage of the instance (free-form; suggested: TRIAL, ACTIVE, AT_RISK, CHURNED)" + examples: + - ACTIVE + readOnly: true + type: string + metadata: + additionalProperties: {} + description: Metadata for the instance + examples: + - key: value + type: object + name: + description: Name of the instance + examples: + - My Instance + minLength: 1 + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - my-instance + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + startLicenseDate: + description: Start date of the license for this instance + examples: + - "2023-10-01T12:00:00Z" + format: date-time + type: string + status: + description: Operational status of the instance + enum: + - HEALTHY + - DEGRADED + - INCIDENT + - MAINTENANCE + examples: + - HEALTHY + readOnly: true + type: string + updatedAt: + description: Timestamp when the instance was last updated + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this instance + readOnly: true + required: + - id + - createdBy + - createdAt + - updatedBy + - updatedAt + - name + - description + - customerId + - customerSlug + - licenseId + - licenseSlug + - startLicenseDate + - endLicenseDate + type: object + InstanceAddon: + additionalProperties: false + properties: + addonId: + type: string + addonSlug: + examples: + - extra-seats-v1 + type: string + attachedAt: + format: date-time + type: string + familySlug: + examples: + - extra-seats + type: string + id: + readOnly: true + type: string + maxQuantity: + format: int32 + type: + - integer + - "null" + name: + description: The add-on version's name + examples: + - Extra seats + type: string + pendingQuantity: + description: A decrease scheduled for the next boundary, 0 for a removal; the current quantity holds until then. Null when nothing is scheduled (V31-67) + format: int32 + type: + - integer + - "null" + prices: + description: "What the instance's subscription bills for it: the default FLAT_FEE price of the subscription's period and the ACTIVE metered prices. Empty when the instance is not billed." + items: + $ref: "#/components/schemas/Price" + type: array + quantity: + examples: + - 3 + format: int32 + type: integer + removedAt: + format: date-time + type: + - string + - "null" + required: + - id + - addonId + - addonSlug + - familySlug + - name + - quantity + - maxQuantity + - pendingQuantity + - attachedAt + - removedAt + - prices + type: object + InstanceAddonUpdateDelivered: + additionalProperties: false + properties: + addonId: + type: string + addonSlug: + examples: + - extra-seats-v1 + type: string + attachedAt: + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + familySlug: + examples: + - extra-seats + type: string + id: + readOnly: true + type: string + instanceSlug: + type: string + maxQuantity: + format: int32 + type: + - integer + - "null" + name: + description: The add-on version's name + examples: + - Extra seats + type: string + pendingQuantity: + description: A decrease scheduled for the next boundary, 0 for a removal; the current quantity holds until then. Null when nothing is scheduled (V31-67) + format: int32 + type: + - integer + - "null" + previousQuantity: + description: The quantity before a quantity change + format: int32 + type: integer + prices: + description: "What the instance's subscription bills for it: the default FLAT_FEE price of the subscription's period and the ACTIVE metered prices. Empty when the instance is not billed." + items: + $ref: "#/components/schemas/Price" + type: array + quantity: + examples: + - 3 + format: int32 + type: integer + removedAt: + format: date-time + type: + - string + - "null" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instanceSlug + - id + - addonId + - addonSlug + - familySlug + - name + - quantity + - maxQuantity + - pendingQuantity + - attachedAt + - removedAt + - prices + type: object + x-kaiten-payload: "#/components/schemas/InstanceAddonUpdate" + InstanceBilling: + additionalProperties: false + properties: + anchorAt: + description: The instant its periods are counted from + format: date-time + type: string + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time + type: + - string + - "null" + canceledAt: + format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's + type: + - string + - "null" + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time + type: string + currentPeriodStart: + format: date-time + type: string + customerName: + type: string + customerSlug: + type: string + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" + format: int32 + type: integer + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 + type: + - integer + - "null" + id: + type: string + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" + format: date-time + type: + - string + - "null" + instanceName: + type: string + instanceSlug: + description: As it was at the subscription's last write + type: string + pastDueSince: + format: date-time + type: + - string + - "null" + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" + format: date-time + type: string + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + type: string + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" + updatedAt: + format: date-time + type: string + required: + - id + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince + - createdAt + - updatedAt + type: object + InstanceBody: + additionalProperties: false + properties: + customerExternalId: + description: External identifier of the related customer in the same adapter + examples: + - rec_customer_123 + type: string + deploymentZoneId: + description: Deployment zone ID + examples: + - 123e4567-e89b-12d3-a456-426614174005 + type: string + description: + description: Instance description + examples: + - This is a sample instance description + type: string + endLicenseDate: + description: License end date + examples: + - "2024-10-01T12:00:00Z" + format: date-time + type: string + error: + description: Optional integration error message + type: string + integrationMetadata: + additionalProperties: {} + description: Adapter-specific integration metadata + type: object + licenseId: + description: License ID + examples: + - 123e4567-e89b-12d3-a456-426614174004 + type: string + metadata: + additionalProperties: {} + description: Instance metadata + type: object + name: + description: Instance name + examples: + - My Instance + minLength: 1 + type: string + slug: + description: Instance slug + examples: + - my-instance + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + startLicenseDate: + description: License start date + examples: + - "2023-10-01T12:00:00Z" + format: date-time + type: string + webUrl: + description: Absolute http(s) link to the record in the third-party system + examples: + - https://app.attio.com/w/acme/record/rec_12345 + type: string + type: object + InstanceDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when the instance was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this instance + readOnly: true + customerId: + description: ID of the customer associated with this instance + examples: + - 123e4567-e89b-12d3-a456-426614174003 + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: Slug of the customer associated with this instance + examples: + - acme-corp + readOnly: true + type: string + deploymentZoneId: + description: ID of the deployment zone associated with this instance (optional) + examples: + - 123e4567-e89b-12d3-a456-426614174005 + type: string + deploymentZoneSlug: + description: Slug of the deployment zone associated with this instance (optional) + examples: + - aws-us-west-2 + readOnly: true + type: string + description: + description: Brief description of the instance + examples: + - This is a sample instance description + type: string + endLicenseDate: + description: End date of the license for this instance + examples: + - "2024-10-01T12:00:00Z" + format: date-time + type: string + id: + description: Unique identifier for the instance + examples: + - 123e4567-e89b-12d3-a2456-426614174000 + readOnly: true + type: string + integrations: + additionalProperties: + $ref: "#/components/schemas/InstanceIntegration" + description: Integrations grouped by adapter name + type: object + licenseId: + description: ID of the license version this instance is pinned to. An ARCHIVED version cannot be assigned, on create or by changing the license (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); an instance already on a version that is archived later keeps it. A DRAFT version can be assigned, to try it before publishing. + examples: + - 123e4567-e89b-12d3-a456-426614174004 + type: string + licenseSlug: + description: Slug of the license associated with this instance + examples: + - my-license + readOnly: true + type: string + lifecycleStage: + description: "Commercial lifecycle stage of the instance (free-form; suggested: TRIAL, ACTIVE, AT_RISK, CHURNED)" + examples: + - ACTIVE + readOnly: true + type: string + metadata: + additionalProperties: {} + description: Metadata for the instance + examples: + - key: value + type: object + name: + description: Name of the instance + examples: + - My Instance + minLength: 1 + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - my-instance + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + startLicenseDate: + description: Start date of the license for this instance + examples: + - "2023-10-01T12:00:00Z" + format: date-time + type: string + status: + description: Operational status of the instance + enum: + - HEALTHY + - DEGRADED + - INCIDENT + - MAINTENANCE + examples: + - HEALTHY + readOnly: true + type: string + updatedAt: + description: Timestamp when the instance was last updated + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this instance + readOnly: true + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - createdBy + - createdAt + - updatedBy + - updatedAt + - name + - description + - customerId + - customerSlug + - licenseId + - licenseSlug + - startLicenseDate + - endLicenseDate + type: object + x-kaiten-payload: "#/components/schemas/Instance" + InstanceEntitlementCapExceededDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlement_slug: + description: URL-friendly identifier of the entitlement + type: string + overage: + description: Amount by which usage exceeds the cap + format: double + type: number + threshold: + description: The entitlement cap + format: double + type: number + value: + description: Current usage value, including overage + format: double + type: number + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - entitlement_slug + - threshold + - value + - overage + type: object + x-kaiten-payload: "#/components/schemas/InstanceEntitlementCapExceeded" + InstanceEntitlementUsagePeriodRolledOverDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + closed_period_end: + format: date-time + type: + - string + - "null" + closed_period_start: + format: date-time + type: + - string + - "null" + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlement_id: + type: string + entitlement_slug: + type: string + event_count: + format: int32 + type: integer + instance_id: + type: string + is_synthetic: + type: boolean + new_period_start: + format: date-time + type: string + reason: + enum: + - PERIOD_ENDED + - ANCHOR_CHANGED + type: string + value: + format: double + type: number + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - entitlement_id + - entitlement_slug + - instance_id + - value + - event_count + - closed_period_start + - closed_period_end + - new_period_start + - is_synthetic + - reason + type: object + x-kaiten-payload: "#/components/schemas/InstanceEntitlementUsagePeriodRolledOver" + InstanceEntitlementUsageReportAcceptedDelivered: + additionalProperties: false + properties: + behavior: + description: "Report behavior: append folds the value into the stored total, set overwrites it" + enum: + - append + - set + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlement_slug: + description: URL-friendly identifier of the entitlement the usage was reported against + type: string + event_count: + description: Number of reports folded into the current usage window, including this one + format: int32 + type: integer + status: + description: Whether the report was persisted + enum: + - ACCEPTED + - REJECTED + type: string + value: + description: Value carried by the report + format: double + type: number + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - entitlement_slug + - value + - behavior + - event_count + - status + type: object + x-kaiten-payload: "#/components/schemas/InstanceEntitlementUsageReportAccepted" + InstanceEntitlementUsageWarningThresholdReachedDelivered: + additionalProperties: false + properties: + boundary: + description: Usage value at which the warning fires (cap * warningThresholdPercent / 100) + format: double + type: number + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlement_slug: + description: URL-friendly identifier of the entitlement + type: string + threshold: + description: The entitlement cap + format: double + type: number + value: + description: Current usage value + format: double + type: number + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - entitlement_slug + - threshold + - boundary + - value + type: object + x-kaiten-payload: "#/components/schemas/InstanceEntitlementUsageWarningThresholdReached" + InstanceEntitlementValueGetDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlement_id: + description: Unique identifier for the entitlement + type: string + entitlement_slug: + description: URL-friendly identifier of the entitlement + type: string + instance_id: + description: Unique identifier for the instance the value was read for + type: string + license_id: + description: Unique identifier for the license carrying the entitlement + type: string + organization_id: + description: Unique identifier for the owning organization + type: string + timestamp: + description: When the value was read + format: date-time + type: string + type: + description: Discriminator of the resolved entitlement value + examples: + - number + type: string + value: + description: Resolved entitlement value at read time + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - entitlement_id + - instance_id + - license_id + - organization_id + - type + - value + - timestamp + - entitlement_slug + type: object + x-kaiten-payload: "#/components/schemas/InstanceEntitlementValueGet" + InstanceIntegration: + additionalProperties: false + properties: + external_id: + description: External identifier in the third-party adapter + examples: + - rec_12345 + type: string + last_error: + description: Last synchronization error message, if any + type: string + metadata: + additionalProperties: {} + description: Adapter-specific integration metadata + type: object + synced_at: + description: Timestamp of the last synchronization with the adapter + format: date-time + readOnly: true + type: string + web_url: + description: Absolute http(s) link to the record in the third-party system + examples: + - https://app.attio.com/w/acme/workspace/rec_12345 + type: string + required: + - external_id + - synced_at + type: object + InstanceIntegrationResource: + additionalProperties: false + properties: + customerExternalId: + description: External identifier of the related customer in the same adapter + examples: + - rec_customer_123 + type: string + deploymentZoneId: + description: Deployment zone ID + examples: + - 123e4567-e89b-12d3-a456-426614174005 + type: string + description: + description: Instance description + examples: + - This is a sample instance description + type: string + endLicenseDate: + description: License end date + examples: + - "2024-10-01T12:00:00Z" + format: date-time + type: string + error: + description: Last synchronization error for this integration, if any + type: string + integrationMetadata: + additionalProperties: {} + description: Adapter-specific instance integration metadata + type: object + licenseId: + description: License ID + examples: + - 123e4567-e89b-12d3-a456-426614174004 + type: string + name: + description: Instance name + examples: + - My Instance + type: string + slug: + description: Instance slug + examples: + - my-instance + type: string + startLicenseDate: + description: License start date + examples: + - "2023-10-01T12:00:00Z" + format: date-time + type: string + webUrl: + description: Absolute http(s) link to the record in the third-party system + examples: + - https://app.attio.com/w/acme/workspace/rec_12345 + type: string + required: + - description + - endLicenseDate + - licenseId + - integrationMetadata + - name + - slug + - startLicenseDate + type: object + InstanceLifecycleStageChangedDelivered: + additionalProperties: false + properties: + changed_by: + format: uuid + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + instance_id: + format: uuid + type: string + new_lifecycle_stage: + type: + - string + - "null" + previous_lifecycle_stage: + type: + - string + - "null" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instance_id + - previous_lifecycle_stage + - new_lifecycle_stage + - changed_by + type: object + x-kaiten-payload: "#/components/schemas/InstanceLifecycleStageChanged" + InstanceStatusChangedDelivered: + additionalProperties: false + properties: + changed_by: + format: uuid + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + instance_id: + format: uuid + type: string + new_status: + enum: + - HEALTHY + - DEGRADED + - INCIDENT + - MAINTENANCE + type: string + previous_status: + enum: + - HEALTHY + - DEGRADED + - INCIDENT + - MAINTENANCE + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instance_id + - previous_status + - new_status + - changed_by + type: object + x-kaiten-payload: "#/components/schemas/InstanceStatusChanged" + InstanceUpdateDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when the instance was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this instance + readOnly: true + customerId: + description: ID of the customer associated with this instance + examples: + - 123e4567-e89b-12d3-a456-426614174003 + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: Slug of the customer associated with this instance + examples: + - acme-corp + readOnly: true + type: string + deploymentZoneId: + description: ID of the deployment zone associated with this instance (optional) + examples: + - 123e4567-e89b-12d3-a456-426614174005 + type: string + deploymentZoneSlug: + description: Slug of the deployment zone associated with this instance (optional) + examples: + - aws-us-west-2 + readOnly: true + type: string + description: + description: Brief description of the instance + examples: + - This is a sample instance description + type: string + endLicenseDate: + description: End date of the license for this instance + examples: + - "2024-10-01T12:00:00Z" + format: date-time + type: string + id: + description: Unique identifier for the instance + examples: + - 123e4567-e89b-12d3-a2456-426614174000 + readOnly: true + type: string + integrations: + additionalProperties: + $ref: "#/components/schemas/InstanceIntegration" + description: Integrations grouped by adapter name + type: object + licenseChangeReason: + description: "BILLING_FALLBACK: the subscription ended and the instance moved to its family's fallback (V31-66). Null for any other change, the move a paid checkout applies included" + enum: + - BILLING_FALLBACK + type: + - string + - "null" + licenseId: + description: ID of the license version this instance is pinned to. An ARCHIVED version cannot be assigned, on create or by changing the license (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); an instance already on a version that is archived later keeps it. A DRAFT version can be assigned, to try it before publishing. + examples: + - 123e4567-e89b-12d3-a456-426614174004 + type: string + licenseSlug: + description: Slug of the license associated with this instance + examples: + - my-license + readOnly: true + type: string + lifecycleStage: + description: "Commercial lifecycle stage of the instance (free-form; suggested: TRIAL, ACTIVE, AT_RISK, CHURNED)" + examples: + - ACTIVE + readOnly: true + type: string + metadata: + additionalProperties: {} + description: Metadata for the instance + examples: + - key: value + type: object + name: + description: Name of the instance + examples: + - My Instance + minLength: 1 + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - my-instance + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + startLicenseDate: + description: Start date of the license for this instance + examples: + - "2023-10-01T12:00:00Z" + format: date-time + type: string + status: + description: Operational status of the instance + enum: + - HEALTHY + - DEGRADED + - INCIDENT + - MAINTENANCE + examples: + - HEALTHY + readOnly: true + type: string + updatedAt: + description: Timestamp when the instance was last updated + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this instance + readOnly: true + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - licenseChangeReason + - id + - createdBy + - createdAt + - updatedBy + - updatedAt + - name + - description + - customerId + - customerSlug + - licenseId + - licenseSlug + - startLicenseDate + - endLicenseDate + type: object + x-kaiten-payload: "#/components/schemas/InstanceUpdate" + Invoice: + additionalProperties: false + properties: + billingEmail: + description: "The address the invoice is for, as it was when composed. Personal data: it appears in no event" + type: + - string + - "null" + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoff: + $ref: "#/components/schemas/InvoiceHandoff" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + hold: + description: When the invoice was held and released + oneOf: + - $ref: "#/components/schemas/HoldRecord" + - enum: + - null + type: "null" + holdDetail: + description: Every meter whose journal failed a check + oneOf: + - $ref: "#/components/schemas/HoldDetail" + - enum: + - null + type: "null" + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseId: + type: string + licenseSlug: + type: string + lines: + items: + $ref: "#/components/schemas/InvoiceLine" + type: array + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + provider: + description: The invoice in its payment provider; null for NOOP + oneOf: + - $ref: "#/components/schemas/ProviderRecord" + - enum: + - null + type: "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + replacedByInvoiceId: + description: The invoice recomposed from this VOID one + type: + - string + - "null" + replacesInvoiceId: + description: The VOID invoice this one was recomposed from + type: + - string + - "null" + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + uncollectibleAt: + format: date-time + type: + - string + - "null" + updatedAt: + format: date-time + type: string + voidReason: + type: + - string + - "null" + voidedAt: + format: date-time + type: + - string + - "null" + required: + - licenseId + - lines + - billingEmail + - holdDetail + - hold + - handoff + - uncollectibleAt + - voidedAt + - voidReason + - replacesInvoiceId + - replacedByInvoiceId + - provider + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + InvoiceHandoff: + additionalProperties: false + properties: + acknowledgedAt: + format: date-time + type: + - string + - "null" + claimCount: + format: int32 + type: integer + externalReference: + description: The invoice's number in the organization's accounting system + type: + - string + - "null" + leaseId: + description: The claim currently holding it + type: + - string + - "null" + leasedUntil: + format: date-time + type: + - string + - "null" + status: + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + required: + - status + - leaseId + - leasedUntil + - claimCount + - acknowledgedAt + - externalReference + type: object + InvoiceHold: + additionalProperties: false + properties: + entitlementId: + type: string + invariant: + description: The check that failed + type: string + required: + - entitlementId + - invariant + type: object + InvoiceLine: + additionalProperties: true + properties: + addonId: + description: The add-on version an ADDON line bills + type: + - string + - "null" + addonPriceId: + description: The add-on price an ADDON line bills + type: + - string + - "null" + amount: + description: round_half_up(quantity × unitAmountDecimal), in minor units; negative on a DISCOUNT line + examples: + - 2440 + format: int64 + type: integer + billingModel: + description: The price's billing model; absent on a DISCOUNT line + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + - null + type: + - string + - "null" + billingTiming: + description: ADVANCE lines bill the period that starts at the boundary, ARREARS lines the one that ends there; absent on a DISCOUNT line + enum: + - ADVANCE + - ARREARS + - null + type: + - string + - "null" + capped: + description: "Set on a preview line whose sample exceeded what the licence accepts: reports above that are rejected, so the excess is not billed" + type: boolean + description: + description: The arithmetic of the line + examples: + - 3.05 × 8.00 EUR (per 10k tokens); 130,500 used; 30,500 above the applied limit (100,000) + type: string + discount: + description: How a DISCOUNT line was computed + oneOf: + - $ref: "#/components/schemas/InvoiceLineDiscount" + - enum: + - null + type: "null" + entitlementId: + description: The metered entitlement, on USAGE and OVERAGE lines + type: + - string + - "null" + entitlementSlug: + description: Its slug, as it was when the line was composed + type: + - string + - "null" + id: + description: The line's identifier, stable for the invoice's life; absent on a preview + type: string + instanceAddonId: + description: The instance's attachment an ADDON line bills + type: + - string + - "null" + instanceVoucherId: + description: The redemption a DISCOUNT line applies + type: + - string + - "null" + label: + description: The price's display label, else a derived one + examples: + - Tokens — overage + type: string + licensePriceId: + description: The licence price the line bills; null on an ADDON line + type: + - string + - "null" + metering: + description: How a USAGE or OVERAGE line's quantity was measured + oneOf: + - $ref: "#/components/schemas/InvoiceLineMetering" + - enum: + - null + type: "null" + overage: + description: The arithmetic of an OVERAGE line + oneOf: + - $ref: "#/components/schemas/InvoiceLineOverage" + - enum: + - null + type: "null" + provider: + description: The line in the payment provider, once pushed + oneOf: + - $ref: "#/components/schemas/InvoiceLineProvider" + - enum: + - null + type: "null" + quantity: + description: "In sale units, a decimal string: 1 on a BASE line, the quantity held on an ADDON line" + examples: + - "3.05" + type: string + seq: + description: Position on the invoice, from 1 + format: int64 + type: integer + serviceFrom: + description: Start of the period the line bills (inclusive) + format: date-time + type: string + serviceTo: + description: End of the period the line bills (exclusive) + format: date-time + type: string + trueUp: + description: "On the true-up line of an ADVANCE add-on of a TRUE_UP family: the highest quantity held in the elapsed period and the quantity billed in advance for it (V31-70)" + oneOf: + - $ref: "#/components/schemas/InvoiceLineTrueUp" + - enum: + - null + type: "null" + type: + description: "BASE: the subscription's FLAT_FEE price. ADDON: an add-on's FLAT_FEE price times the quantity held at the boundary. USAGE: a USAGE_BASED price's metered usage. OVERAGE: an OVERAGE price's usage above the licence's limit. DISCOUNT: a PRICE voucher's discount, negative." + enum: + - BASE + - ADDON + - USAGE + - OVERAGE + - DISCOUNT + type: string + unitAmountDecimal: + description: The price's unit amount in minor units; absent on a DISCOUNT line + examples: + - "800" + type: + - string + - "null" + voucherId: + description: The voucher a DISCOUNT line applies + type: + - string + - "null" + required: + - seq + - type + - billingModel + - billingTiming + - licensePriceId + - addonPriceId + - addonId + - instanceAddonId + - voucherId + - instanceVoucherId + - entitlementId + - entitlementSlug + - label + - description + - serviceFrom + - serviceTo + - quantity + - unitAmountDecimal + - amount + - metering + - overage + - trueUp + - discount + - provider + type: object + InvoiceLineDiscount: + additionalProperties: false + properties: + allocations: + description: "The part of the line each target bears, by ascending targetSeq: they sum to the line's magnitude, and no target is discounted below 0. A payment provider applies each one to its target line" + items: + $ref: "#/components/schemas/DiscountAllocation" + readOnly: true + type: array + application: + description: Which invoice of the redemption this is, from 1 + format: int32 + type: integer + applicationsMax: + description: How many invoices the redemption discounts; null for FOREVER + format: int32 + type: + - integer + - "null" + appliesTo: + enum: + - LICENSE_BASE + - ADDONS + - BOTH + - SELECTED_PRICES + type: string + base: + description: What its targets still amounted to before it, exact, in minor units + type: string + currency: + type: + - string + - "null" + discountType: + enum: + - PERCENTAGE + - FIXED_AMOUNT + type: string + discountValue: + description: The percentage, or the amount in minor units + type: string + targetSeqs: + description: The lines it discounts + items: + format: int64 + type: integer + type: + - array + - "null" + required: + - discountType + - discountValue + - currency + - appliesTo + - targetSeqs + - base + - application + - applicationsMax + - allocations + type: object + InvoiceLineLedger: + additionalProperties: false + properties: + firstSeq: + description: First report of the period, null when it has none + format: int64 + type: + - integer + - "null" + instanceId: + description: "The instance whose reports these are: with the line's entitlement, the pair the journal is kept by. It outlives the instance" + type: + - string + - "null" + lastSeq: + description: Last report of the period, null when it has none + format: int64 + type: + - integer + - "null" + rows: + description: Reports in the period + format: int64 + type: integer + sumDelta: + description: Sum of the reports' movements, before any floor + type: string + sumOverage: + description: Sum of their movements above the limit, before any floor + type: + - string + - "null" + required: + - instanceId + - firstSeq + - lastSeq + - rows + - sumDelta + - sumOverage + type: object + InvoiceLineMetering: + additionalProperties: false + properties: + ledger: + description: The usage journal rows the line was measured from + oneOf: + - $ref: "#/components/schemas/InvoiceLineLedger" + - enum: + - null + type: "null" + measuredQuantity: + description: "In measured units: the usage for a USAGE line, the overage for an OVERAGE line" + examples: + - "30500" + type: string + negativeSegmentsFloored: + description: Windows whose net movement was negative and counted as 0 + examples: + - 0 + format: int64 + type: integer + saleUnitFactor: + description: Measured units in one sale unit + examples: + - "10000" + type: string + windows: + description: Reset windows the period spans + examples: + - 1 + format: int64 + type: integer + required: + - saleUnitFactor + - measuredQuantity + - windows + - negativeSegmentsFloored + - ledger + type: object + InvoiceLineOverage: + additionalProperties: false + properties: + limits: + description: The limits applied, in the order they first applied + items: + $ref: "#/components/schemas/OverageLimit" + type: array + overageMeasured: + description: Usage above the applied limit + examples: + - "30500" + type: string + usageMeasured: + description: Usage in measured units + examples: + - "130500" + type: string + required: + - usageMeasured + - overageMeasured + - limits + type: object + InvoiceLineProvider: + additionalProperties: false + properties: + amount: + description: The provider's amount, read back for reconciliation + format: int64 + type: integer + couponIds: + description: "On a DISCOUNT line: the provider's discount (a Stripe coupon) of each allocation, in the allocations' order" + items: + type: string + type: + - array + - "null" + externalLineId: + description: The provider's id of the line; absent on a DISCOUNT line, which the provider holds as discounts on its targets + type: string + type: object + InvoiceLineTrueUp: + additionalProperties: false + properties: + billedQuantity: + description: The quantity its ADVANCE line billed at the previous boundary; 0 when it started after it + format: int32 + type: integer + maxQuantity: + description: The highest quantity the attachment held in the elapsed period + format: int32 + type: integer + required: + - maxQuantity + - billedQuantity + type: object + InvoicePayment: + additionalProperties: false + properties: + externalReference: + description: The invoice's number in the organization's accounting system, 1 to 255 characters. It also acknowledges an invoice still waiting in the handoff queue + type: string + note: + description: A note, such as the transfer's reference, kept only in the paid event + maxLength: 1000 + type: string + paidAt: + description: When it was paid, now or earlier; now when omitted + format: date-time + type: string + type: object + InvoicePreview: + additionalProperties: false + properties: + asOf: + description: When the preview was composed + format: date-time + type: string + boundaryAt: + description: The boundary the invoice bills + format: date-time + type: string + currency: + description: ISO 4217 code + examples: + - EUR + type: string + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + kind: + description: The boundary previewed + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + description: The licence version previewed + type: string + lines: + items: + $ref: "#/components/schemas/InvoiceLine" + type: array + serviceFrom: + description: Earliest start of the lines' service periods; absent without lines + format: date-time + type: string + serviceTo: + description: Latest end of the lines' service periods; absent without lines + format: date-time + type: string + status: + description: Always PREVIEW + enum: + - PREVIEW + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + wouldHold: + description: Meters whose usage journal fails a consistency check, which would hold the invoice. Always empty for a sample. + items: + $ref: "#/components/schemas/InvoiceHold" + type: array + required: + - status + - kind + - asOf + - boundaryAt + - licenseSlug + - currency + - subtotal + - discountTotal + - total + - lines + - wouldHold + type: object + InvoicePreviewScenario: + additionalProperties: false + properties: + addOns: + description: "Add-on versions priced in: their fees for the base's period and their metered prices, and their grants for the sample (PreviewLicenseInvoice.AddonNotFound; .InvalidAddOns for a repeated one or a quantity below 1)." + items: + $ref: "#/components/schemas/AddonAmount" + type: + - array + - "null" + basePriceId: + description: The FLAT_FEE price billed as the base (PreviewLicenseInvoice.PriceNotFound). Defaults to the version's default FLAT_FEE price (PreviewLicenseInvoice.NoBasePrice when it has none). + type: string + instanceSlug: + description: "Rate this instance's own usage, read from its journal over [P0, now): P0 of its live subscription, else one billing period back (PreviewLicenseInvoice.InstanceNotFound; .OutsideRetention when P0 is before the usage history). Not with sampleUsage." + type: string + sampleUsage: + description: Usage of the metered entitlements over the period that ends, one entry per entitlement, rated against the version's grant and the listed add-ons'. An entitlement left out used nothing. + items: + $ref: "#/components/schemas/SampleUsageAmount" + type: + - array + - "null" + voucherCode: + description: A PRICE voucher's discount, applied as a redemption would, consuming nothing (PreviewLicenseInvoice.VoucherNotFound; .VoucherInvalid when it is not an ACTIVE PRICE voucher, nor an ACTIVE COMPOSITE with a PRICE part). + type: string + type: object + InvoiceSummary: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + required: + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + InvoiceVoid: + additionalProperties: false + properties: + reason: + description: 1 to 500 characters; .ReasonRequired otherwise + maxLength: 500 + minLength: 1 + type: string + required: + - reason + type: object + InvoiceWriteOff: + additionalProperties: false + properties: + reason: + description: 1 to 500 characters; .ReasonRequired otherwise + maxLength: 500 + minLength: 1 + type: string + required: + - reason + type: object + IssuedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseId: + type: string + licenseSlug: + type: string + lines: + items: + $ref: "#/components/schemas/InvoiceLine" + type: array + linesTruncated: + description: "Set when the lines did not fit the event: read the invoice for all of them" + type: boolean + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - licenseId + - lines + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/IssuedInvoice" + License: + additionalProperties: false + properties: + createdAt: + description: Timestamp when this license was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + description: + description: Detailed description of the license capabilities and restrictions + examples: + - This license provides access to all enterprise features including advanced analytics, priority support, and unlimited users. + maxLength: 500 + type: string + familyId: + description: "Identifier of the license family this license is a version of. Stable across renames and new versions: two licenses sharing it are the same product. Always present in responses. On create, optional: names the family this license is a new version of, as familySlug does by slug; omit both to create a new family. On update, optional and must be the license's own family: a version cannot move between families (UpdateLicense.FamilyNotReassignable)." + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + familySlug: + description: "Write-only on create: slug of the family this license is a new version of. familyId names the same family by identifier; sent together they must agree. Omit both to create a new family, of which this license becomes version 1. Never returned -- see familyId. Refused on update: a version cannot move between families." + examples: + - enterprise-license + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + writeOnly: true + id: + description: Unique identifier for the license + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + isDefault: + description: "Whether this version is the one its family puts forward: at most one version per family, and only a PUBLISHED one (CreateLicense.DefaultMustBePublished, UpdateLicense.DefaultMustBePublished). Resolution hands it out ahead of the highest-numbered published version, so it keeps being served when a newer version is published. Setting it takes the flag from the family's previous default, which gets a LICENSE_UPDATED event of its own. On update, sending false unsets it; the default cannot be archived until another version takes its place or the flag is unset (ArchiveLicense.DefaultMustBePublished)." + examples: + - false + type: boolean + lifecycleState: + description: Whether this version may be served. Always present in responses. Optional on create, where it is DRAFT or PUBLISHED and PUBLISHED when omitted; ARCHIVED is refused (CreateLicense.LifecycleStateNotSettable), since a version is archived by withdrawing it from sale. After that it changes only through publish-license (DRAFT to PUBLISHED), archive-license (PUBLISHED to ARCHIVED) and unarchive-license (ARCHIVED to PUBLISHED); update accepts the stored state and refuses any other (UpdateLicense.LifecycleStateNotSettable). Only a PUBLISHED version can be the family's default. An ARCHIVED version cannot be assigned to an instance (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); instances already on it keep it. + enum: + - DRAFT + - PUBLISHED + - ARCHIVED + examples: + - PUBLISHED + type: string + name: + description: Display name of the license + examples: + - Enterprise License + maxLength: 100 + minLength: 1 + type: string + pricingType: + description: How this version is sold. FREE and PAID can be bought self-serve; CUSTOM sends a buyer to selfServeCtaUrl or a conversation. Always present in responses. On create it defaults to CUSTOM; on update, omit it or send null to keep the stored value. + enum: + - FREE + - PAID + - CUSTOM + examples: + - PAID + type: + - string + - "null" + requiresPaymentMethod: + description: Whether self-serve signup captures a payment method before activation. Always present in responses; false when omitted on create. On update, omit it or send null to keep the stored value. + examples: + - false + type: + - boolean + - "null" + selfServeCtaUrl: + description: "Where a buyer is sent when this version cannot be bought self-serve: an http(s) URL of at most 2048 characters (CreateLicense.InvalidSelfServeCtaUrl). Null when there is none. On update, omit it or send null to keep the stored value, or send an empty string to remove it." + examples: + - https://example.com/contact-sales + maxLength: 2048 + type: + - string + - "null" + slug: + description: Optional URL-friendly identifier, unique per organization. When omitted, a new version of an existing family takes {familySlug}-v{version}, and any other license gets one generated from its name. A license that opens a new family gives the family its slug. + examples: + - enterprise-license + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + trialPeriodDays: + description: Trial length, in days, a subscription to this version starts with by default. Null when there is none. From 1 to 365 (CreateLicense.InvalidTrialDays, UpdateLicense.InvalidTrialDays). On update, omit it or send null to keep the stored value, or send 0 to remove the trial. + examples: + - 14 + format: int32 + maximum: 365 + minimum: 0 + type: + - integer + - "null" + type: + description: Type of the license determining its capabilities and restrictions + enum: + - DEVELOPMENT + - TRIAL + - PAID + - COMMUNITY + examples: + - paid + type: string + updatedAt: + description: Timestamp when this license was last updated + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + version: + description: "Version of the license within its family, assigned by the server when the version is created and never changed afterwards: the version's derived slug ({familySlug}-v{version}, see slug) is built from it. A number is never reused within a family, even after its version is deleted. Create refuses any value (CreateLicense.VersionNotSettable); update accepts only the stored one, so a representation can be written back as it was read (UpdateLicense.VersionNotSettable)." + examples: + - "1" + readOnly: true + type: string + versionName: + description: Optional human-readable name for the license version. Defaults to "Version - ". + examples: + - Winter 2024 Release + maxLength: 50 + type: string + required: + - id + - name + - description + - type + - version + - isDefault + - createdAt + - updatedAt + type: object + LicenseDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when this license was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + description: + description: Detailed description of the license capabilities and restrictions + examples: + - This license provides access to all enterprise features including advanced analytics, priority support, and unlimited users. + maxLength: 500 + type: string + familyId: + description: "Identifier of the license family this license is a version of. Stable across renames and new versions: two licenses sharing it are the same product. Always present in responses. On create, optional: names the family this license is a new version of, as familySlug does by slug; omit both to create a new family. On update, optional and must be the license's own family: a version cannot move between families (UpdateLicense.FamilyNotReassignable)." + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + familySlug: + description: "Write-only on create: slug of the family this license is a new version of. familyId names the same family by identifier; sent together they must agree. Omit both to create a new family, of which this license becomes version 1. Never returned -- see familyId. Refused on update: a version cannot move between families." + examples: + - enterprise-license + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + writeOnly: true + id: + description: Unique identifier for the license + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + isDefault: + description: "Whether this version is the one its family puts forward: at most one version per family, and only a PUBLISHED one (CreateLicense.DefaultMustBePublished, UpdateLicense.DefaultMustBePublished). Resolution hands it out ahead of the highest-numbered published version, so it keeps being served when a newer version is published. Setting it takes the flag from the family's previous default, which gets a LICENSE_UPDATED event of its own. On update, sending false unsets it; the default cannot be archived until another version takes its place or the flag is unset (ArchiveLicense.DefaultMustBePublished)." + examples: + - false + type: boolean + lifecycleState: + description: Whether this version may be served. Always present in responses. Optional on create, where it is DRAFT or PUBLISHED and PUBLISHED when omitted; ARCHIVED is refused (CreateLicense.LifecycleStateNotSettable), since a version is archived by withdrawing it from sale. After that it changes only through publish-license (DRAFT to PUBLISHED), archive-license (PUBLISHED to ARCHIVED) and unarchive-license (ARCHIVED to PUBLISHED); update accepts the stored state and refuses any other (UpdateLicense.LifecycleStateNotSettable). Only a PUBLISHED version can be the family's default. An ARCHIVED version cannot be assigned to an instance (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); instances already on it keep it. + enum: + - DRAFT + - PUBLISHED + - ARCHIVED + examples: + - PUBLISHED + type: string + name: + description: Display name of the license + examples: + - Enterprise License + maxLength: 100 + minLength: 1 + type: string + pricingType: + description: How this version is sold. FREE and PAID can be bought self-serve; CUSTOM sends a buyer to selfServeCtaUrl or a conversation. Always present in responses. On create it defaults to CUSTOM; on update, omit it or send null to keep the stored value. + enum: + - FREE + - PAID + - CUSTOM + examples: + - PAID + type: + - string + - "null" + requiresPaymentMethod: + description: Whether self-serve signup captures a payment method before activation. Always present in responses; false when omitted on create. On update, omit it or send null to keep the stored value. + examples: + - false + type: + - boolean + - "null" + selfServeCtaUrl: + description: "Where a buyer is sent when this version cannot be bought self-serve: an http(s) URL of at most 2048 characters (CreateLicense.InvalidSelfServeCtaUrl). Null when there is none. On update, omit it or send null to keep the stored value, or send an empty string to remove it." + examples: + - https://example.com/contact-sales + maxLength: 2048 + type: + - string + - "null" + slug: + description: Optional URL-friendly identifier, unique per organization. When omitted, a new version of an existing family takes {familySlug}-v{version}, and any other license gets one generated from its name. A license that opens a new family gives the family its slug. + examples: + - enterprise-license + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + trialPeriodDays: + description: Trial length, in days, a subscription to this version starts with by default. Null when there is none. From 1 to 365 (CreateLicense.InvalidTrialDays, UpdateLicense.InvalidTrialDays). On update, omit it or send null to keep the stored value, or send 0 to remove the trial. + examples: + - 14 + format: int32 + maximum: 365 + minimum: 0 + type: + - integer + - "null" + type: + description: Type of the license determining its capabilities and restrictions + enum: + - DEVELOPMENT + - TRIAL + - PAID + - COMMUNITY + examples: + - paid + type: string + updatedAt: + description: Timestamp when this license was last updated + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + version: + description: "Version of the license within its family, assigned by the server when the version is created and never changed afterwards: the version's derived slug ({familySlug}-v{version}, see slug) is built from it. A number is never reused within a family, even after its version is deleted. Create refuses any value (CreateLicense.VersionNotSettable); update accepts only the stored one, so a representation can be written back as it was read (UpdateLicense.VersionNotSettable)." + examples: + - "1" + readOnly: true + type: string + versionName: + description: Optional human-readable name for the license version. Defaults to "Version - ". + examples: + - Winter 2024 Release + maxLength: 50 + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - name + - description + - type + - version + - isDefault + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/License" + LicenseEntitlement: + additionalProperties: false + properties: + createdAt: + description: Timestamp when this entitlement was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this entitlement + readOnly: true + entitlementGroups: + description: Groups associated with this entitlement + items: + $ref: "#/components/schemas/EntitlementGroupSummary" + readOnly: true + type: + - array + - "null" + entitlementName: + description: Name of the entitlement feature or service + examples: + - Premium Support + readOnly: true + type: string + entitlementSlug: + description: URL-friendly identifier for the entitlement + examples: + - premium-support + format: string + type: string + entitlementType: + description: Type of entitlement (BOOLEAN, NUMBER, or CONFIG) + enum: + - BOOLEAN + - NUMBER + - CONFIG + examples: + - NUMBER + readOnly: true + type: string + licenseId: + description: Unique identifier for the license this entitlement belongs to + examples: + - a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6 + readOnly: true + type: string + licenseSlug: + description: URL-friendly identifier for the license this entitlement belongs to + examples: + - enterprise-license + readOnly: true + type: string + limitCapExceededOveragePercent: + description: "Derives this grant's enforcement from its own numeric value: -1 when the value is unlimited (no cap, no overage); otherwise 0 rejects usage above the value (hard limit) and a positive percentage allows usage to exceed it by that much before being rejected (soft limit). Null for non-numeric (BOOLEAN/CONFIG) grants." + examples: + - 10 + format: int32 + type: integer + updatedAt: + description: Timestamp when this entitlement was last updated + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this entitlement + readOnly: true + value: + description: "Typed entitlement value. Supported shapes: {type:number,value:number}, {type:boolean,value:boolean}, {type:object,value:{...}}" + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + required: + - entitlementName + - licenseId + - licenseSlug + - createdBy + - createdAt + - updatedBy + - updatedAt + - value + type: object + LicenseEntitlementDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when this entitlement was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this entitlement + readOnly: true + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + entitlementGroups: + description: Groups associated with this entitlement + items: + $ref: "#/components/schemas/EntitlementGroupSummary" + readOnly: true + type: + - array + - "null" + entitlementName: + description: Name of the entitlement feature or service + examples: + - Premium Support + readOnly: true + type: string + entitlementSlug: + description: URL-friendly identifier for the entitlement + examples: + - premium-support + format: string + type: string + entitlementType: + description: Type of entitlement (BOOLEAN, NUMBER, or CONFIG) + enum: + - BOOLEAN + - NUMBER + - CONFIG + examples: + - NUMBER + readOnly: true + type: string + licenseId: + description: Unique identifier for the license this entitlement belongs to + examples: + - a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6 + readOnly: true + type: string + licenseSlug: + description: URL-friendly identifier for the license this entitlement belongs to + examples: + - enterprise-license + readOnly: true + type: string + limitCapExceededOveragePercent: + description: "Derives this grant's enforcement from its own numeric value: -1 when the value is unlimited (no cap, no overage); otherwise 0 rejects usage above the value (hard limit) and a positive percentage allows usage to exceed it by that much before being rejected (soft limit). Null for non-numeric (BOOLEAN/CONFIG) grants." + examples: + - 10 + format: int32 + type: integer + updatedAt: + description: Timestamp when this entitlement was last updated + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this entitlement + readOnly: true + value: + description: "Typed entitlement value. Supported shapes: {type:number,value:number}, {type:boolean,value:boolean}, {type:object,value:{...}}" + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - entitlementName + - licenseId + - licenseSlug + - createdBy + - createdAt + - updatedBy + - updatedAt + - value + type: object + x-kaiten-payload: "#/components/schemas/LicenseEntitlement" + LicenseFamilyUpdate: + additionalProperties: false + properties: + fallbackLicenseFamilySlug: + description: Another FREE family of the organization (every version of it priced FREE), whose default PUBLISHED version an instance moves to when its subscription ends. "" removes the fallback. Anything else answers 422 .InvalidFallback, an unknown family included. + maxLength: 255 + type: + - string + - "null" + isPublic: + description: List the family's default PUBLISHED version, with its ACTIVE prices, in the public catalogue + type: + - boolean + - "null" + trialEndWithoutPaymentMethod: + description: "CANCEL or CONVERT: what a trial charged automatically with a total above 0 becomes at its end when the customer has no ACTIVE payment method. CANCEL cancels it; CONVERT makes it ACTIVE, whose first charge then fails. Any other value answers 422 .InvalidTrialEndBehavior." + type: + - string + - "null" + trialReminderDays: + description: "How many days before a trial's end trial_will_end fires: an integer from 1 to 30 (else 422 .InvalidTrialReminderDays), for the trials started after the change" + format: int32 + type: + - integer + - "null" + type: object + LicenseFamilyView: + additionalProperties: false + properties: + createdAt: + description: Timestamp when this family was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + currentVersion: + $ref: "#/components/schemas/License" + description: "The version this read is about. By default the version the family resolves to: its default version if it has one, otherwise its highest-numbered PUBLISHED version. Under ?version=N it is the version asked for instead, whatever its lifecycle state. Absent when the family has no PUBLISHED version, which only a list or an include=versions read returns." + fallbackLicenseFamilySlug: + description: The FREE family whose default PUBLISHED version an instance of this family moves to when its subscription ends; null without one + type: + - string + - "null" + id: + description: Unique identifier for the license family + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + isPublic: + description: Whether the family's default PUBLISHED version is listed in the public catalogue. Private by default. + type: boolean + slug: + description: URL-friendly identifier of the family, unique per organization and stable across renames and new versions. Deleting the family's last version deletes the family and frees the slug. + examples: + - enterprise-license + type: string + trialEndWithoutPaymentMethod: + description: "What a trial charged automatically with a total above 0 becomes at its end when the customer has no ACTIVE payment method: CANCEL (the default) or CONVERT" + enum: + - CANCEL + - CONVERT + type: string + trialReminderDays: + description: How many days before a trial's end trial_will_end fires, for the trials started after it was set. 3 by default. + examples: + - 3 + format: int32 + maximum: 30 + minimum: 1 + type: integer + updatedAt: + description: Timestamp when a version was last added to this family, or when the family was created. Renames, lifecycle moves and default changes update the versions' own updatedAt, not this one. + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + versionCount: + description: Number of versions in this family, whatever their lifecycle state + examples: + - 3 + format: int32 + type: integer + versions: + description: "Every version of the family, oldest first. Only returned when explicitly requested with include=versions, and then never null: a family always has at least one version." + items: + $ref: "#/components/schemas/License" + type: array + required: + - id + - slug + - versionCount + - isPublic + - fallbackLicenseFamilySlug + - trialEndWithoutPaymentMethod + - trialReminderDays + - createdAt + - updatedAt + type: object + LicenseFamilyViewDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when this family was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + currentVersion: + $ref: "#/components/schemas/License" + description: "The version this read is about. By default the version the family resolves to: its default version if it has one, otherwise its highest-numbered PUBLISHED version. Under ?version=N it is the version asked for instead, whatever its lifecycle state. Absent when the family has no PUBLISHED version, which only a list or an include=versions read returns." + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + fallbackLicenseFamilySlug: + description: The FREE family whose default PUBLISHED version an instance of this family moves to when its subscription ends; null without one + type: + - string + - "null" + id: + description: Unique identifier for the license family + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + isPublic: + description: Whether the family's default PUBLISHED version is listed in the public catalogue. Private by default. + type: boolean + slug: + description: URL-friendly identifier of the family, unique per organization and stable across renames and new versions. Deleting the family's last version deletes the family and frees the slug. + examples: + - enterprise-license + type: string + trialEndWithoutPaymentMethod: + description: "What a trial charged automatically with a total above 0 becomes at its end when the customer has no ACTIVE payment method: CANCEL (the default) or CONVERT" + enum: + - CANCEL + - CONVERT + type: string + trialReminderDays: + description: How many days before a trial's end trial_will_end fires, for the trials started after it was set. 3 by default. + examples: + - 3 + format: int32 + maximum: 30 + minimum: 1 + type: integer + updatedAt: + description: Timestamp when a version was last added to this family, or when the family was created. Renames, lifecycle moves and default changes update the versions' own updatedAt, not this one. + examples: + - "2023-10-02T12:00:00Z" + format: date-time + readOnly: true + type: string + versionCount: + description: Number of versions in this family, whatever their lifecycle state + examples: + - 3 + format: int32 + type: integer + versions: + description: "Every version of the family, oldest first. Only returned when explicitly requested with include=versions, and then never null: a family always has at least one version." + items: + $ref: "#/components/schemas/License" + type: array + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - slug + - versionCount + - isPublic + - fallbackLicenseFamilySlug + - trialEndWithoutPaymentMethod + - trialReminderDays + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/LicenseFamilyView" + LicensePriceChanges: + additionalProperties: false + properties: + billingPeriod: + description: See createLicensePrice. + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + billingTiming: + description: See createLicensePrice. + enum: + - ADVANCE + - ARREARS + type: string + displayLabel: + maxLength: 200 + type: string + displayOrder: + format: int32 + minimum: 0 + type: integer + isDefault: + type: boolean + meteredEntitlementSlug: + description: See createLicensePrice. + type: string + unitAmountDecimal: + description: See createLicensePrice. + type: string + type: object + LicensePriceEventDelivered: + additionalProperties: false + properties: + billingModel: + description: "FLAT_FEE: an amount per billing period. USAGE_BASED: an amount per sale unit of the metered entitlement's usage. OVERAGE: an amount per sale unit of the usage above the licence's limit." + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + billingPeriod: + description: The period a FLAT_FEE price is charged for. Null on metered prices, which bill on the subscription's period. + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + - null + type: + - string + - "null" + billingTiming: + description: ADVANCE bills the period that starts; ARREARS the period that ended. Metered prices are always ARREARS. + enum: + - ADVANCE + - ARREARS + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + format: date-time + readOnly: true + type: string + currency: + description: ISO 4217 code, upper case. One currency per licence version. + examples: + - EUR + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + deprecatedAt: + description: When the price was deprecated + format: date-time + type: + - string + - "null" + displayLabel: + description: The invoice line's label; a default is derived when it is null. + examples: + - Pro plan + type: + - string + - "null" + displayOrder: + description: Order among the version's prices + examples: + - 0 + format: int32 + type: integer + familySlug: + description: The family of that version + type: string + id: + description: Unique identifier of the price + readOnly: true + type: string + isDefault: + description: The FLAT_FEE price picked for its billing period when none is named. At most one per period; only an ACTIVE FLAT_FEE price. + type: boolean + licenseSlug: + description: The licence version the price belongs to + type: string + metered: + description: What a metered price measures. Null on FLAT_FEE prices. + oneOf: + - $ref: "#/components/schemas/PriceMeter" + - enum: + - null + type: "null" + status: + description: A DEPRECATED price keeps billing what is already pinned to it but is no longer offered. + enum: + - ACTIVE + - DEPRECATED + type: string + unitAmount: + description: unitAmountDecimal as an integer number of minor units, when it is one; null otherwise. + examples: + - 2900 + format: int64 + type: + - integer + - "null" + unitAmountDecimal: + description: "Amount in the currency's minor units (cents for EUR): per period for FLAT_FEE, per sale unit for metered prices. Up to 12 decimal places." + examples: + - "2900" + type: string + updatedAt: + format: date-time + readOnly: true + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - licenseSlug + - familySlug + - id + - billingModel + - billingTiming + - billingPeriod + - currency + - unitAmount + - unitAmountDecimal + - metered + - displayLabel + - displayOrder + - isDefault + - status + - deprecatedAt + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/LicensePriceEvent" + LicensePriceUpdatedEventDelivered: + additionalProperties: false + properties: + billingModel: + description: "FLAT_FEE: an amount per billing period. USAGE_BASED: an amount per sale unit of the metered entitlement's usage. OVERAGE: an amount per sale unit of the usage above the licence's limit." + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + billingPeriod: + description: The period a FLAT_FEE price is charged for. Null on metered prices, which bill on the subscription's period. + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + - null + type: + - string + - "null" + billingTiming: + description: ADVANCE bills the period that starts; ARREARS the period that ended. Metered prices are always ARREARS. + enum: + - ADVANCE + - ARREARS + type: string + changedFields: + description: The members the update changed, as the API names them + items: + type: string + type: + - array + - "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + format: date-time + readOnly: true + type: string + currency: + description: ISO 4217 code, upper case. One currency per licence version. + examples: + - EUR + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + deprecatedAt: + description: When the price was deprecated + format: date-time + type: + - string + - "null" + displayLabel: + description: The invoice line's label; a default is derived when it is null. + examples: + - Pro plan + type: + - string + - "null" + displayOrder: + description: Order among the version's prices + examples: + - 0 + format: int32 + type: integer + familySlug: + description: The family of that version + type: string + id: + description: Unique identifier of the price + readOnly: true + type: string + isDefault: + description: The FLAT_FEE price picked for its billing period when none is named. At most one per period; only an ACTIVE FLAT_FEE price. + type: boolean + licenseSlug: + description: The licence version the price belongs to + type: string + metered: + description: What a metered price measures. Null on FLAT_FEE prices. + oneOf: + - $ref: "#/components/schemas/PriceMeter" + - enum: + - null + type: "null" + status: + description: A DEPRECATED price keeps billing what is already pinned to it but is no longer offered. + enum: + - ACTIVE + - DEPRECATED + type: string + unitAmount: + description: unitAmountDecimal as an integer number of minor units, when it is one; null otherwise. + examples: + - 2900 + format: int64 + type: + - integer + - "null" + unitAmountDecimal: + description: "Amount in the currency's minor units (cents for EUR): per period for FLAT_FEE, per sale unit for metered prices. Up to 12 decimal places." + examples: + - "2900" + type: string + updatedAt: + format: date-time + readOnly: true + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - changedFields + - licenseSlug + - familySlug + - id + - billingModel + - billingTiming + - billingPeriod + - currency + - unitAmount + - unitAmountDecimal + - metered + - displayLabel + - displayOrder + - isDefault + - status + - deprecatedAt + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/LicensePriceUpdatedEvent" + LineDifference: + additionalProperties: false + properties: + externalLineId: + type: string + kaitenAmount: + format: int64 + type: integer + lineId: + type: string + providerAmount: + format: int64 + type: integer + seq: + format: int64 + type: integer + required: + - lineId + - seq + - kaitenAmount + - providerAmount + - externalLineId + type: object + LineReportPage: + additionalProperties: false + properties: + items: + description: The reports, in reportSeq order + items: + $ref: "#/components/schemas/UsageReport" + type: array + nextAfterSeq: + description: Pass as afterSeq to read the next page. Absent on the last page. + format: int64 + type: integer + required: + - items + type: object + List: + additionalProperties: false + properties: + data: + description: The page, newest first + items: + $ref: "#/components/schemas/Notification" + type: array + nextCursor: + description: Opaque cursor for the next page; null on the last page + type: + - string + - "null" + unreadCount: + description: Unread notifications, capped (see MaxUnreadCount) + format: int64 + type: integer + required: + - data + - nextCursor + - unreadCount + type: object + ManagedUser: + additionalProperties: false + properties: + authorization: + $ref: "#/components/schemas/UserAuthorization" + createdAt: + format: date-time + type: string + email: + type: + - string + - "null" + externalId: + type: string + id: + type: string + name: + type: string + required: + - id + - externalId + - name + - email + - createdAt + - authorization + type: object + ManifestEnvelope: + additionalProperties: false + properties: + flags: + items: + $ref: "#/components/schemas/ManifestFlag" + type: + - array + - "null" + required: + - flags + type: object + ManifestFlag: + additionalProperties: false + properties: + defaultValue: {} + description: + type: string + key: + type: string + name: + type: string + type: + type: string + required: + - key + - type + - defaultValue + type: object + MarkReadResult: + additionalProperties: false + properties: + unreadCount: + description: Unread notifications after the change + format: int64 + type: integer + updated: + description: Notifications newly marked read + format: int64 + type: integer + required: + - updated + - unreadCount + type: object + Me: + additionalProperties: false + properties: + availablePermissions: + items: + type: string + type: array + availableRoles: + items: + $ref: "#/components/schemas/Role" + type: array + organizationId: + type: string + scopes: + items: + type: string + type: array + user: + $ref: "#/components/schemas/ManagedUser" + required: + - user + - organizationId + - scopes + - availableRoles + - availablePermissions + type: object + MetadataField: + additionalProperties: false + properties: + archivedAt: + description: Timestamp when the field was archived (soft delete). Absent if active. + format: date-time + readOnly: true + type: string + createdAt: + description: Timestamp when the field was created + examples: + - "2026-05-26T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this field + readOnly: true + displayOrder: + description: Sort order in the admin UI; ties are broken by createdAt ASC. Settable only through POST /metadata-fields/reorder; never echoed back on update. + examples: + - 0 + format: int32 + type: integer + id: + description: Unique identifier + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + jsonSchema: + additionalProperties: {} + description: "JSON Schema 2020-12 document describing the value space. Example: {\"type\":\"string\",\"enum\":[\"a\",\"b\"]}" + type: object + key: + description: Stable identifier, unique per (organization, resource type) on non-archived rows. Immutable after creation; update must echo back the stored value. + examples: + - region + maxLength: 100 + minLength: 1 + type: string + label: + description: Human-readable label shown in the admin UI + examples: + - Region + maxLength: 100 + minLength: 1 + type: string + resourceType: + description: Resource type this field applies to. Immutable after creation; update must echo back the stored value. + enum: + - DEPLOYMENT_ZONE + - INSTANCE + type: string + updatedAt: + description: Timestamp when the field was last updated + examples: + - "2026-05-26T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this field + readOnly: true + required: + - id + - resourceType + - key + - label + - jsonSchema + - createdBy + - createdAt + - updatedBy + - updatedAt + type: object + MetadataFieldDelivered: + additionalProperties: false + properties: + archivedAt: + description: Timestamp when the field was archived (soft delete). Absent if active. + format: date-time + readOnly: true + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + createdAt: + description: Timestamp when the field was created + examples: + - "2026-05-26T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this field + readOnly: true + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + displayOrder: + description: Sort order in the admin UI; ties are broken by createdAt ASC. Settable only through POST /metadata-fields/reorder; never echoed back on update. + examples: + - 0 + format: int32 + type: integer + id: + description: Unique identifier + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + jsonSchema: + additionalProperties: {} + description: "JSON Schema 2020-12 document describing the value space. Example: {\"type\":\"string\",\"enum\":[\"a\",\"b\"]}" + type: object + key: + description: Stable identifier, unique per (organization, resource type) on non-archived rows. Immutable after creation; update must echo back the stored value. + examples: + - region + maxLength: 100 + minLength: 1 + type: string + label: + description: Human-readable label shown in the admin UI + examples: + - Region + maxLength: 100 + minLength: 1 + type: string + resourceType: + description: Resource type this field applies to. Immutable after creation; update must echo back the stored value. + enum: + - DEPLOYMENT_ZONE + - INSTANCE + type: string + updatedAt: + description: Timestamp when the field was last updated + examples: + - "2026-05-26T12:00:00Z" + format: date-time + readOnly: true + type: string + updatedBy: + $ref: "#/components/schemas/User" + description: User who last updated this field + readOnly: true + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - resourceType + - key + - label + - jsonSchema + - createdBy + - createdAt + - updatedBy + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/MetadataField" + MetadataFieldReorderedDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + ids: + description: Metadata field ids in their new display order + items: + type: string + type: + - array + - "null" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - ids + type: object + x-kaiten-payload: "#/components/schemas/MetadataFieldReordered" + MetadataFieldSchemaProposal: + additionalProperties: false + properties: + jsonSchema: + additionalProperties: {} + description: Candidate JSON Schema 2020-12 document to evaluate existing values against. + type: object + required: + - jsonSchema + type: object + MinimumAmount: + additionalProperties: false + properties: + currency: + examples: + - EUR + type: string + unitAmountDecimal: + description: In minor units, above 0 + examples: + - "2900" + type: string + required: + - currency + - unitAmountDecimal + type: object + MismatchedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + reconciliationDetail: + $ref: "#/components/schemas/Reconciliation" + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - reconciliationDetail + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/MismatchedInvoice" + NewAddon: + additionalProperties: false + properties: + description: + examples: + - Five more seats per unit + type: string + familyId: + description: Make this a new version of this family + type: string + familySlug: + description: Make this a new version of this family + examples: + - extra-seats + type: string + increaseBilling: + description: "Read only when the call opens a new family: how a mid-period increase of its ADVANCE add-ons is billed, TRUE_UP (the default) or NEXT_BOUNDARY; another value answers 422 .InvalidIncreaseBilling" + type: string + isDefault: + description: Make this version its family's default; the previous default loses it + type: boolean + lifecycleState: + default: PUBLISHED + enum: + - DRAFT + - PUBLISHED + type: string + maxQuantity: + description: The largest quantity an instance can hold + examples: + - 10 + format: int32 + type: integer + name: + examples: + - Extra seats + minLength: 1 + type: string + pricingType: + enum: + - FREE + - PAID + - CUSTOM + type: string + slug: + description: "The version's slug. Default: {familySlug}-v{n} for a new version of a family, a slug generated from the name for a new family." + examples: + - extra-seats-v1 + type: string + versionName: + description: "Default: Version - {n}" + type: string + required: + - name + - description + - pricingType + type: object + NewAddonGrant: + additionalProperties: false + properties: + entitlementSlug: + examples: + - seats + type: string + limitCapExceededOveragePercent: + description: "The overage the add-on allows: -1 with an unlimited value, 0 or more otherwise. Absent inherits the licence grant's." + format: int32 + type: integer + overrideBehavior: + default: MAX + description: How a number grant combines with the licence's + enum: + - ADD + - OVERRIDE + - MAX + type: string + value: + additionalProperties: {} + description: "{type, value}, shaped like a licence grant: a number counts once per unit of quantity" + examples: + - type: number + value: 5 + type: object + required: + - entitlementSlug + - value + type: object + NewAddonPrice: + additionalProperties: false + properties: + billingModel: + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + billingPeriod: + description: "Required on a FLAT_FEE price: it bills subscriptions of that period. Refused on a metered one." + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + billingTiming: + description: Defaults to ADVANCE for a FLAT_FEE price and ARREARS for a metered one, which must be ARREARS. + enum: + - ADVANCE + - ARREARS + type: string + currency: + description: ISO 4217 code, upper case. One currency per add-on version. + examples: + - EUR + type: string + displayLabel: + maxLength: 200 + type: string + displayOrder: + format: int32 + minimum: 0 + type: integer + isDefault: + description: Makes this FLAT_FEE price the one that bills its period; the previous default of the period loses it. + type: boolean + meteredEntitlementSlug: + description: "The entitlement a metered price measures: granted by the add-on version, with a reset period, SUM or COUNT, NUMBER or NUMBER_AI_CREDIT." + examples: + - tokens + type: string + unitAmountDecimal: + description: "Amount in minor units: per unit and period for FLAT_FEE, per sale unit for metered prices." + examples: + - "900" + type: string + required: + - billingModel + - currency + - unitAmountDecimal + type: object + NewEntitlementException: + additionalProperties: false + properties: + endsAt: + description: "After startsAt and after now; absent or null: no end" + format: date-time + type: + - string + - "null" + entitlementSlug: + description: The entitlement it overrides; the instance's licence and add-ons need not grant it + examples: + - sso + type: string + kind: + description: SET works on every type; ADD and UNLIMITED on NUMBER and NUMBER_AI_CREDIT only + enum: + - SET + - ADD + - UNLIMITED + type: string + reason: + description: Why, 1 to 500 characters, not blank; shown to the vendor only + type: string + startsAt: + description: Now when absent; a past instant is taken as given, a future one schedules the exception + format: date-time + type: string + value: + anyOf: + - type: number + - type: boolean + - additionalProperties: true + type: object + - type: "null" + description: For SET, a number of 0 or more on the NUMBER types, a boolean on BOOLEAN, an object on CONFIG; for ADD, a number above 0; for UNLIMITED, absent or null + required: + - entitlementSlug + - kind + - reason + type: object + NewInstanceAddon: + additionalProperties: false + properties: + addonSlug: + description: The add-on version to attach + examples: + - extra-seats-v1 + type: string + quantity: + default: 1 + examples: + - 3 + format: int32 + minimum: 1 + type: integer + required: + - addonSlug + type: object + NewLicensePrice: + additionalProperties: false + properties: + billingModel: + description: "FLAT_FEE: an amount per billing period. USAGE_BASED: per sale unit of usage. OVERAGE: per sale unit of usage above the licence's limit." + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + billingPeriod: + description: Required on a FLAT_FEE price, refused on a metered one (CreateLicensePrice.InvalidPeriod). + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + billingTiming: + description: Defaults to ADVANCE for a FLAT_FEE price and ARREARS for a metered one, which must be ARREARS (CreateLicensePrice.InvalidTiming). + enum: + - ADVANCE + - ARREARS + type: string + currency: + description: ISO 4217 code, upper case (CreateLicensePrice.InvalidCurrency). One currency per licence version (CreateLicensePrice.CurrencyMismatch). + examples: + - EUR + type: string + displayLabel: + description: The invoice line's label. + maxLength: 200 + type: string + displayOrder: + description: Order among the version's prices. + format: int32 + minimum: 0 + type: integer + isDefault: + description: Makes this FLAT_FEE price the default of its billing period (CreateLicensePrice.DefaultConflict when another is). + type: boolean + meteredEntitlementSlug: + description: "The entitlement a metered price measures: granted by the version, with a reset period, SUM or COUNT, NUMBER or NUMBER_AI_CREDIT, and metered by no other active price of the version." + examples: + - tokens + type: string + unitAmountDecimal: + description: "Amount in minor units: per period for FLAT_FEE, per sale unit for metered prices. A non-negative decimal of at most 12 integer digits and 12 decimal places (CreateLicensePrice.InvalidAmount)." + examples: + - "2900" + type: string + required: + - billingModel + - currency + - unitAmountDecimal + type: object + NewPaymentMethodSession: + additionalProperties: false + properties: + currency: + description: The currency the payment method is set up in; required when the customer has no live subscription, whose currency is used otherwise + examples: + - EUR + type: string + returnUrl: + description: "Where the customer comes back to: https, or http on localhost. The session id is appended as kaiten_setup_session" + examples: + - https://app.example.test/billing + type: string + required: + - returnUrl + type: object + NewPortalSession: + additionalProperties: false + properties: + returnUrl: + description: https, or http on localhost + examples: + - https://app.example.test/billing + type: string + required: + - returnUrl + type: object + NewSessionPaymentMethodSession: + additionalProperties: false + properties: + currency: + description: The currency the payment method is set up in; required when the customer has no live subscription, whose currency is used otherwise + examples: + - EUR + type: string + returnUrl: + description: Where the setup page sends the customer back, with kaiten_setup_session appended. Its origin must be one the organization's publishable keys allow. + examples: + - https://app.example.test/billing + format: uri + type: string + required: + - returnUrl + type: object + NewSessionPortalSession: + additionalProperties: false + properties: + returnUrl: + description: Where the portal sends the customer back. Its origin must be one the organization's publishable keys allow. + examples: + - https://app.example.test/billing + format: uri + type: string + required: + - returnUrl + type: object + NewSubscription: + additionalProperties: false + properties: + addOns: + description: "Add-ons to attach with the subscription, under the rules of POST /instances/{instanceSlug}/addons; the ACTIVATION invoice bills them. One refused refuses the subscribe: 422 SubscribeInstance.AddonInvalid, the add-on's own code in errors[0].value.code." + items: + $ref: "#/components/schemas/SubscriptionAddon" + maxItems: 50 + type: + - array + - "null" + basePriceId: + description: An ACTIVE FLAT_FEE price of the instance's licence version, which must be PUBLISHED. The subscription stays pinned to it, even once deprecated. + type: string + collectionMethod: + description: "Omitted: the organization's default, read at each invoice. CHARGE_AUTOMATICALLY needs a provider that charges automatically (SubscribeInstance.CollectionMethodUnsupported)." + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + daysUntilDue: + description: "Payment terms in days, 0 to 365. Omitted: the organization's default, read at each invoice." + format: int32 + type: integer + providerKind: + description: "Who issues and collects the invoices. NOOP (the default): the organization itself, through the handoff queue. A provider must be connected (SubscribeInstance.ProviderNotConnected); one that pushes invoices needs the customer's billing e-mail for SEND_INVOICE (.BillingEmailMissing) and the price's currency (.UnsupportedCurrency), and registers the customer first." + enum: + - NOOP + - STRIPE + type: string + startAt: + description: "The anchor periods are counted from, truncated to the second: now when omitted, else at most one billing period ago. A period that has already ended closes on the next pass." + format: date-time + type: string + trialDays: + description: "Days of trial from the anchor, 1 to 365: nothing is billed, and no usage counts, until it ends; the period close then issues the first invoice. 0 is no trial; any other value is refused (SubscribeInstance.InvalidTrialDays). Omitted: the licence's trialPeriodDays, else no trial; none either when the customer already had a trial of the licence's family (one per customer and family)." + format: int32 + type: integer + voucherCode: + description: "A voucher to redeem with the subscription, once its add-ons are attached; a PRICE voucher applies from the ACTIVATION invoice. Refused: 422 SubscribeInstance.VoucherInvalid, the redemption's own code in errors[0].value.code." + maxLength: 64 + minLength: 1 + type: string + required: + - basePriceId + type: object + Notification: + additionalProperties: false + properties: + actionUrl: + description: Where clicking the notification goes + type: string + body: + description: Rendered detail line, when the event has one + type: string + createdAt: + description: When the event occurred + format: date-time + type: string + eventName: + description: Event this notification reports + examples: + - INSTANCE_DEPLOYED + type: string + eventType: + description: CloudEvents type of the same event -- the key it is published under in this document's webhooks section, so a client can line a notification up with the payload schema and the description already published there + examples: + - com.kaiten.instance.v1.deployed + type: string + id: + description: Identifier of the underlying audit trail entry + type: string + objectType: + description: What the notification is about -- the kind of page actionUrl opens, and what the list's objectType filter matches + examples: + - instance + type: string + readAt: + description: When this user read it; absent means unread + format: date-time + type: string + title: + description: Rendered headline + type: string + required: + - id + - eventName + - eventType + - objectType + - title + - createdAt + type: object + NumberEntitlementValue: + additionalProperties: false + properties: + event_count: + description: Number of events accumulated (server-computed) + examples: + - 2 + format: int32 + readOnly: true + type: integer + type: + description: Value type discriminator + enum: + - number + examples: + - number + type: string + value: + description: Numeric value + examples: + - 42.5 + format: double + type: number + required: + - type + - value + type: object + Outcome: + additionalProperties: false + properties: + applied: + description: Invoices read and applied + format: int64 + type: integer + error: + type: string + failed: + description: Invoices that could not be read or applied + format: int64 + type: integer + providerKind: + enum: + - NOOP + - STRIPE + type: string + status: + enum: + - SUCCESS + - PARTIAL + - FAILED + type: string + syncedAt: + format: date-time + type: string + required: + - providerKind + - status + - applied + - failed + - syncedAt + type: object + OverageLimit: + additionalProperties: false + properties: + limitValue: + description: The limit, null when unlimited + examples: + - "100000" + type: + - string + - "null" + overagePercent: + description: Usage accepted above the limit, as a percent of it + examples: + - 50 + format: int32 + type: integer + rows: + description: Usage reports measured against it; 0 for a sample + format: int64 + type: integer + required: + - limitValue + - overagePercent + - rows + type: object + PageAddon: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Addon" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageAuditTrail: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/AuditTrail" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageCheckoutSessionAdmin: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/CheckoutSessionAdmin" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageComponent: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Component" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageCustomer: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Customer" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageDeploymentZone: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/DeploymentZone" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageEntitlement: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Entitlement" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageEntitlementGroup: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/EntitlementGroup" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageFeatureFlag: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/FeatureFlag" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageInstance: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Instance" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageInvoiceSummary: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/InvoiceSummary" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageLicense: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/License" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageLicenseEntitlement: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/LicenseEntitlement" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageLicenseFamilyView: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/LicenseFamilyView" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageManagedUser: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/ManagedUser" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageMetadataField: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/MetadataField" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PagePublishableKey: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/PublishableKey" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageQueuedInvoice: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/QueuedInvoice" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageRedemption: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Redemption" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageRelease: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Release" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageServiceAccount: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/ServiceAccount" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageSessionInvoice: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/SessionInvoice" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageToken: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Token" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PageVoucher: + additionalProperties: false + properties: + hasMore: + description: Whether more rows exist beyond this page. + type: boolean + items: + items: + $ref: "#/components/schemas/Voucher" + type: array + nextCursor: + description: Opaque cursor for the next page. Absent when hasMore is false. + type: string + required: + - items + - hasMore + type: object + PaidInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + externalReference: + type: string + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + note: + description: The note given when it was marked paid, kept only here + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + source: + description: "MARK_PAID: recorded by the organization. ZERO_TOTAL: nothing was owed" + enum: + - MARK_PAID + - PROVIDER + - ZERO_TOTAL + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - source + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/PaidInvoice" + PatchInstanceBody: + additionalProperties: false + properties: + lifecycleStage: + description: "Commercial lifecycle stage (free-form; suggested: TRIAL, ACTIVE, AT_RISK, CHURNED). When omitted, the current stage is preserved; clearing it back to unset is not supported." + examples: + - AT_RISK + minLength: 1 + type: string + status: + description: Operational status of the instance + enum: + - HEALTHY + - DEGRADED + - INCIDENT + - MAINTENANCE + examples: + - INCIDENT + type: string + type: object + Payment: + additionalProperties: false + properties: + hostedInvoiceUrl: + type: string + status: + description: "paid. requires_action: the customer must confirm the payment on hostedInvoiceUrl. failed: the charge was refused; save another payment method, or pay on hostedInvoiceUrl. processing: the outcome is not known yet; poll GET /public/session/invoices. not_required: nothing is due today." + enum: + - paid + - requires_action + - failed + - processing + - not_required + type: string + required: + - status + type: object + PaymentFailedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + failureCode: + description: The provider's decline or failure code, such as card_declined, expired_card, authentication_required or no_payment_method + type: string + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + requiresAction: + description: The customer must authenticate the payment on the invoice's hosted page + type: boolean + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - failureCode + - requiresAction + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/PaymentFailedInvoice" + PaymentMethodAttachedDelivered: + additionalProperties: false + properties: + attachedAt: + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + type: string + providerKind: + enum: + - STRIPE + type: string + status: + enum: + - ACTIVE + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - customerSlug + - providerKind + - status + - attachedAt + type: object + x-kaiten-payload: "#/components/schemas/PaymentMethodAttached" + PaymentMethodDetachedDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + type: string + providerKind: + enum: + - STRIPE + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - customerSlug + - providerKind + type: object + x-kaiten-payload: "#/components/schemas/PaymentMethodDetached" + PaymentMethodExpiringDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + type: string + expiresAt: + description: The last instant of the payment method's expiry month + format: date-time + type: string + providerKind: + enum: + - STRIPE + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - customerSlug + - providerKind + - expiresAt + type: object + x-kaiten-payload: "#/components/schemas/PaymentMethodExpiring" + PaymentMethodLabels: + additionalProperties: false + properties: + attachedAt: + format: date-time + type: + - string + - "null" + brand: + examples: + - visa + type: + - string + - "null" + expMonth: + examples: + - 12 + format: int64 + type: + - integer + - "null" + expYear: + examples: + - 2030 + format: int64 + type: + - integer + - "null" + last4: + examples: + - "4242" + type: + - string + - "null" + status: + description: "ACTIVE: charged automatically. EXPIRED, FAILED: a charge said it is no longer usable; save another one" + enum: + - ACTIVE + - EXPIRED + - FAILED + type: string + required: + - status + - brand + - last4 + - expMonth + - expYear + - attachedAt + type: object + PaymentMethodSession: + additionalProperties: false + properties: + expiresAt: + format: date-time + type: string + sessionId: + description: Complete it with POST …/payment-method-session/{sessionId}/complete once the customer is back + type: string + url: + description: The provider's page; send the customer there + type: string + required: + - url + - sessionId + - expiresAt + type: object + PlainToken: + additionalProperties: false + properties: + createdAt: + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + readOnly: true + expiresAt: + format: date-time + type: string + id: + readOnly: true + type: string + name: + type: string + revokedAt: + format: date-time + readOnly: true + type: string + revokedBy: + $ref: "#/components/schemas/User" + readOnly: true + scopes: + items: + type: string + type: + - array + - "null" + serviceAccountId: + readOnly: true + type: string + slug: + description: Optional URL-friendly identifier, unique per service account. Auto-generated if not provided. + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + token: + readOnly: true + type: string + required: + - name + - scopes + - createdAt + - createdBy + type: object + PlanChangeDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + fromLicenseSlug: + type: string + fromPriceId: + type: string + instanceSlug: + type: string + invoiceId: + description: The RENEWAL that billed the old plan's arrears and the new plan's advance + type: string + toLicenseSlug: + type: string + toPriceId: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instanceSlug + - fromLicenseSlug + - toLicenseSlug + - fromPriceId + - toPriceId + - invoiceId + type: object + x-kaiten-payload: "#/components/schemas/PlanChange" + PlanChangeScheduleDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + effectiveAt: + format: date-time + type: string + fromPriceId: + type: string + instanceSlug: + type: string + toPriceId: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instanceSlug + - fromPriceId + - toPriceId + - effectiveAt + type: object + x-kaiten-payload: "#/components/schemas/PlanChangeSchedule" + PlanChangeTarget: + additionalProperties: false + properties: + licensePriceId: + description: An ACTIVE FLAT_FEE price of a PUBLISHED licence version, in the subscription's currency + type: string + required: + - licensePriceId + type: object + PortalCapabilities: + additionalProperties: false + properties: + checkout: + description: "POST /public/session/checkout: the instance has no live subscription and its plan can be bought alone, or its live subscription is free (a FREE licence family, no add-on price above 0) and the catalogue has a self-serve plan, which the checkout replaces it with" + type: boolean + invoices: + description: "Billing is on: GET /public/session/invoices lists the customer's" + type: boolean + paymentMethods: + description: "The vendor's provider captures payment methods: the payment-method and portal sessions work" + type: boolean + unsubscribe: + description: "The instance's subscription is live and not already ending: POST /public/session/billing/cancel" + type: boolean + required: + - paymentMethods + - invoices + - unsubscribe + - checkout + type: object + PortalCustomer: + additionalProperties: false + properties: + billingEmail: + description: Where its invoices are sent + type: + - string + - "null" + name: + examples: + - Acme + type: string + slug: + examples: + - acme + type: string + required: + - slug + - name + - billingEmail + type: object + PortalEntitlement: + additionalProperties: false + properties: + currentPeriodEnd: + description: The usage window's end; null for a lifetime quota + format: date-time + type: + - string + - "null" + currentPeriodStart: + description: The usage window's start; null for a lifetime quota + format: date-time + type: + - string + - "null" + currentValue: + description: A number's usage in its current window; null for BOOLEAN and CONFIG + format: double + type: + - number + - "null" + limitCapExceededOveragePercent: + description: "How far above the limit usage is still accepted, in percent: 0 a hard limit; null for BOOLEAN and CONFIG" + format: int32 + type: + - integer + - "null" + limitValue: + description: "The effective value: a number's limit (-1 unlimited), a BOOLEAN's flag, a CONFIG's object" + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + - enum: + - null + type: "null" + maximumAllowedUsage: + description: The most usage accepted, limit × (1 + percent ÷ 100); null when unlimited, and for BOOLEAN and CONFIG + format: double + type: + - number + - "null" + name: + examples: + - Seats + type: string + percentageUsed: + description: currentValue ÷ limit × 100; null when unlimited or the limit is 0, and for BOOLEAN and CONFIG + format: double + type: + - number + - "null" + provenance: + description: What the licence, the add-ons and the boosts each contribute + oneOf: + - $ref: "#/components/schemas/Provenance" + - enum: + - null + type: "null" + slug: + examples: + - seats + type: string + source: + description: "license: the licence grants it; addon: an add-on does; exception: only the vendor's exception for this instance does" + enum: + - license + - addon + - exception + type: string + type: + enum: + - NUMBER + - NUMBER_AI_CREDIT + - BOOLEAN + - CONFIG + type: string + unlimited: + type: boolean + required: + - slug + - name + - type + - limitValue + - currentValue + - unlimited + - limitCapExceededOveragePercent + - maximumAllowedUsage + - percentageUsed + - currentPeriodStart + - currentPeriodEnd + - source + - provenance + type: object + PortalInstance: + additionalProperties: false + properties: + licenseName: + examples: + - Pro + type: string + licenseSlug: + examples: + - pro-v2 + type: string + name: + examples: + - Acme prod + type: string + slug: + examples: + - acme-prod + type: string + required: + - slug + - name + - licenseSlug + - licenseName + type: object + PortalInstanceSummary: + additionalProperties: false + properties: + billingStatus: + description: Its subscription's status; null when it was never subscribed + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + - null + type: + - string + - "null" + licenseSlug: + examples: + - pro-v2 + type: string + name: + examples: + - Acme prod + type: string + slug: + examples: + - acme-prod + type: string + required: + - slug + - name + - licenseSlug + - billingStatus + type: object + PortalPaymentMethod: + additionalProperties: false + properties: + brand: + examples: + - visa + type: + - string + - "null" + expMonth: + examples: + - 12 + format: int64 + type: + - integer + - "null" + expYear: + examples: + - 2030 + format: int64 + type: + - integer + - "null" + last4: + examples: + - "4242" + type: + - string + - "null" + status: + description: "EXPIRED, FAILED: save another one" + enum: + - ACTIVE + - EXPIRED + - FAILED + type: string + required: + - status + - brand + - last4 + - expMonth + - expYear + type: object + PortalSession: + additionalProperties: false + properties: + url: + type: string + required: + - url + type: object + PortalTrial: + additionalProperties: false + properties: + charge: + description: What the conversion would charge, discounts included, before any tax the provider adds; null when nothing would be + oneOf: + - $ref: "#/components/schemas/PortalTrialCharge" + - enum: + - null + type: "null" + endsAt: + format: date-time + type: string + paymentMethod: + description: "The ACTIVE card the conversion would be charged to; null without one: ask for a card" + oneOf: + - $ref: "#/components/schemas/PortalTrialPayment" + - enum: + - null + type: "null" + reminderAt: + description: When trial_will_end is, or was, due + format: date-time + type: + - string + - "null" + required: + - endsAt + - reminderAt + - charge + - paymentMethod + type: object + PortalTrialCharge: + additionalProperties: false + properties: + amount: + format: int64 + type: integer + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + currency: + examples: + - EUR + type: string + required: + - amount + - currency + - billingPeriod + type: object + PortalTrialPayment: + additionalProperties: false + properties: + brand: + examples: + - visa + type: + - string + - "null" + last4: + examples: + - "4242" + type: + - string + - "null" + required: + - brand + - last4 + type: object + PortalUpcomingInvoice: + additionalProperties: false + properties: + boundaryAt: + description: When it is issued + format: date-time + type: string + currency: + examples: + - EUR + type: string + discountTotal: + description: Minor units + format: int64 + type: integer + subtotal: + description: Minor units + format: int64 + type: integer + total: + description: Minor units + format: int64 + type: integer + required: + - currency + - subtotal + - discountTotal + - total + - boundaryAt + type: object + PreferenceChoices: + additionalProperties: false + properties: + events: + description: Events to set; events left out keep their current value + items: + $ref: "#/components/schemas/PreferenceUpdate" + type: + - array + - "null" + required: + - events + type: object + PreferenceEvent: + additionalProperties: false + properties: + channels: + additionalProperties: + type: boolean + description: Effective value per channel + type: object + eventName: + examples: + - INSTANCE_DEPLOYED + type: string + eventType: + description: CloudEvents type of the same event; see Notification.eventType + examples: + - com.kaiten.instance.v1.deployed + type: string + group: + description: Catalog group the client renders this under + examples: + - deployments + type: string + label: + description: Human-readable event name + type: string + required: + - eventName + - eventType + - label + - group + - channels + type: object + PreferenceMatrix: + additionalProperties: false + properties: + channels: + description: Channels this installation serves, in display order + items: + type: string + type: array + events: + description: Every notifiable event + items: + $ref: "#/components/schemas/PreferenceEvent" + type: array + required: + - channels + - events + type: object + PreferenceUpdate: + additionalProperties: false + properties: + channels: + additionalProperties: + type: boolean + type: object + eventName: + type: string + required: + - eventName + - channels + type: object + Price: + additionalProperties: false + properties: + billingModel: + description: "FLAT_FEE: an amount per billing period. USAGE_BASED: an amount per sale unit of the metered entitlement's usage. OVERAGE: an amount per sale unit of the usage above the licence's limit." + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + billingPeriod: + description: The period a FLAT_FEE price is charged for. Null on metered prices, which bill on the subscription's period. + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + - null + type: + - string + - "null" + billingTiming: + description: ADVANCE bills the period that starts; ARREARS the period that ended. Metered prices are always ARREARS. + enum: + - ADVANCE + - ARREARS + type: string + createdAt: + format: date-time + readOnly: true + type: string + currency: + description: ISO 4217 code, upper case. One currency per licence version. + examples: + - EUR + type: string + deprecatedAt: + description: When the price was deprecated + format: date-time + type: + - string + - "null" + displayLabel: + description: The invoice line's label; a default is derived when it is null. + examples: + - Pro plan + type: + - string + - "null" + displayOrder: + description: Order among the version's prices + examples: + - 0 + format: int32 + type: integer + id: + description: Unique identifier of the price + readOnly: true + type: string + isDefault: + description: The FLAT_FEE price picked for its billing period when none is named. At most one per period; only an ACTIVE FLAT_FEE price. + type: boolean + metered: + description: What a metered price measures. Null on FLAT_FEE prices. + oneOf: + - $ref: "#/components/schemas/PriceMeter" + - enum: + - null + type: "null" + status: + description: A DEPRECATED price keeps billing what is already pinned to it but is no longer offered. + enum: + - ACTIVE + - DEPRECATED + type: string + unitAmount: + description: unitAmountDecimal as an integer number of minor units, when it is one; null otherwise. + examples: + - 2900 + format: int64 + type: + - integer + - "null" + unitAmountDecimal: + description: "Amount in the currency's minor units (cents for EUR): per period for FLAT_FEE, per sale unit for metered prices. Up to 12 decimal places." + examples: + - "2900" + type: string + updatedAt: + format: date-time + readOnly: true + type: string + required: + - id + - billingModel + - billingTiming + - billingPeriod + - currency + - unitAmount + - unitAmountDecimal + - metered + - displayLabel + - displayOrder + - isDefault + - status + - deprecatedAt + - createdAt + - updatedAt + type: object + PriceMeter: + additionalProperties: false + properties: + entitlementSlug: + description: The metered entitlement + examples: + - tokens + type: string + saleUnitFactor: + description: "Measured units in one sale unit, captured when the price was created (1 when the entitlement has no sale unit): 10000 bills per 10,000 tokens." + examples: + - "10000" + type: string + saleUnitPlural: + description: The sale unit's plural label; null when the entitlement has none + examples: + - 10k tokens + type: + - string + - "null" + saleUnitSingular: + description: The sale unit's singular label; null when the entitlement has none + examples: + - 10k tokens + type: + - string + - "null" + required: + - entitlementSlug + - saleUnitFactor + - saleUnitSingular + - saleUnitPlural + type: object + Problem: + additionalProperties: false + properties: + code: + description: Stable, machine-readable error code. + examples: + - License.NotFound + type: string + detail: + description: A human-readable explanation specific to this occurrence of the problem. + examples: + - license not found + type: string + errorId: + description: Correlation id for the server-side log entry describing the withheld cause. + examples: + - 3f6b1a2c-7c1e-4d0b-9d5f-2b0a1c4e8d31 + type: string + errors: + description: Optional list of individual error details + items: + $ref: "#/components/schemas/ErrorDetail" + type: + - array + - "null" + instance: + description: A URI reference that identifies the specific occurrence of the problem. + examples: + - /api/licenses/enterprise + type: string + status: + description: HTTP status code + examples: + - 404 + format: int64 + type: integer + title: + description: A short, human-readable summary of the problem type. This value should not change between occurrences of the error. + examples: + - Not Found + type: string + type: + default: about:blank + description: A URI reference to human-readable documentation for the error. + examples: + - https://tools.ietf.org/html/rfc7231#section-6.5.4 + format: uri + type: string + type: object + Profile: + additionalProperties: false + properties: + email: + format: email + maxLength: 320 + type: string + name: + maxLength: 200 + minLength: 1 + type: string + required: + - name + - email + type: object + Provenance: + additionalProperties: false + properties: + addons: + description: The active add-on grants, in attachment order + items: + $ref: "#/components/schemas/ProvenanceAddon" + type: array + boosts: + description: The boosts in their window, in redemption order + items: + $ref: "#/components/schemas/ProvenanceBoost" + type: array + exception: + description: The instance's exception in force at the read, the last layer; null without one. Never its reason or author + oneOf: + - $ref: "#/components/schemas/ProvenanceException" + - enum: + - null + type: "null" + license: + description: The licence's grant; null when only add-ons grant the entitlement + oneOf: + - $ref: "#/components/schemas/ProvenanceLicense" + - enum: + - null + type: "null" + number: + description: How a number was composed; null for BOOLEAN and CONFIG, and when no add-on, boost or exception changes it + oneOf: + - $ref: "#/components/schemas/ProvenanceNumber" + - enum: + - null + type: "null" + required: + - license + - addons + - boosts + - exception + - number + type: object + ProvenanceAddon: + additionalProperties: false + properties: + addonEntitlementId: + type: string + addonId: + type: string + attachedAt: + format: date-time + type: string + instanceAddonId: + type: string + limitCapExceededOveragePercent: + format: int32 + type: + - integer + - "null" + overrideBehavior: + enum: + - ADD + - OVERRIDE + - MAX + type: string + quantity: + format: int32 + type: integer + value: + description: One unit's grant + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + required: + - instanceAddonId + - addonId + - addonEntitlementId + - quantity + - overrideBehavior + - value + - limitCapExceededOveragePercent + - attachedAt + type: object + ProvenanceBoost: + additionalProperties: false + properties: + effectiveExpiresAt: + format: date-time + type: + - string + - "null" + effectiveStartsAt: + format: date-time + type: string + instanceVoucherId: + type: string + modifierType: + enum: + - SET + - ADD + - MULTIPLY + - UNLIMITED + type: string + modifierValue: + format: double + type: + - number + - "null" + redeemedAt: + format: date-time + type: string + value: + description: The value a BOOLEAN or CONFIG SET boost sets, in the {type, value} shape; null for the NUMBER modifiers + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + - enum: + - null + type: "null" + voucherEntitlementGrantId: + type: string + voucherId: + type: string + required: + - instanceVoucherId + - voucherId + - voucherEntitlementGrantId + - modifierType + - modifierValue + - value + - redeemedAt + - effectiveStartsAt + - effectiveExpiresAt + type: object + ProvenanceException: + additionalProperties: false + properties: + endsAt: + description: "Null: no end" + format: date-time + type: + - string + - "null" + instanceEntitlementExceptionId: + type: string + kind: + enum: + - SET + - ADD + - UNLIMITED + type: string + startsAt: + format: date-time + type: string + value: + description: The exception's value, in the {type, value} shape; null for UNLIMITED + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + - enum: + - null + type: "null" + required: + - instanceEntitlementExceptionId + - kind + - value + - startsAt + - endsAt + type: object + ProvenanceLicense: + additionalProperties: false + properties: + licenseEntitlementId: + type: string + limitCapExceededOveragePercent: + format: int32 + type: + - integer + - "null" + value: + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + required: + - licenseEntitlementId + - value + - limitCapExceededOveragePercent + type: object + ProvenanceNumber: + additionalProperties: false + properties: + afterAddons: + format: double + type: number + afterBoosts: + description: "The value before the exception: -1 when unlimited" + format: double + type: number + boostAdd: + format: double + type: + - number + - "null" + boostMultiply: + format: double + type: + - number + - "null" + boostSet: + format: double + type: + - number + - "null" + effective: + description: -1 when unlimited + format: double + type: number + license: + description: The licence's value; null when only add-ons or an exception grant it + format: double + type: + - number + - "null" + unlimited: + description: The licence, an add-on or a boost is unlimited; only an exception's SET makes it finite again + type: boolean + required: + - license + - afterAddons + - boostSet + - boostAdd + - boostMultiply + - unlimited + - afterBoosts + - effective + type: object + ProviderCapabilities: + additionalProperties: false + properties: + automaticCollection: + type: boolean + billingPortal: + type: boolean + paymentMethodCapture: + type: boolean + required: + - paymentMethodCapture + - billingPortal + - automaticCollection + type: object + ProviderChangeDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + effectiveFrom: + description: Invoices already composed keep their provider + enum: + - next_composition + type: string + from: + enum: + - NOOP + - STRIPE + type: string + instanceSlug: + type: string + to: + enum: + - NOOP + - STRIPE + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instanceSlug + - from + - to + - effectiveFrom + type: object + x-kaiten-payload: "#/components/schemas/ProviderChange" + ProviderLineLimit: + additionalProperties: false + properties: + items: + description: The provider items the invoice needs + format: int32 + type: integer + limit: + description: The most the provider takes on one invoice + format: int32 + type: integer + providerKind: + enum: + - STRIPE + type: string + required: + - providerKind + - items + - limit + type: object + ProviderRecord: + additionalProperties: false + properties: + externalCustomerId: + type: + - string + - "null" + externalInvoiceId: + type: + - string + - "null" + hostedInvoiceUrl: + type: + - string + - "null" + invoiceNumber: + description: The provider's invoice number + type: + - string + - "null" + invoicePdfUrl: + type: + - string + - "null" + lastPaymentError: + description: "Why the last automatic charge failed, as the provider coded it: authentication_required (the customer must confirm the payment on the hosted invoice page), card_declined, expired_card, no_payment_method..." + type: + - string + - "null" + lastPushError: + description: The provider's code and message of the last failed push step + type: + - string + - "null" + nextPushAt: + description: When the push queue tries it next; null when it waits for a human (finalization in the provider) or is pushed + format: date-time + type: + - string + - "null" + pushAttempts: + format: int32 + type: integer + pushedAt: + format: date-time + type: + - string + - "null" + reconciledAt: + format: date-time + type: + - string + - "null" + reconciliationDetail: + description: What differs, on a MISMATCH + oneOf: + - $ref: "#/components/schemas/Reconciliation" + - enum: + - null + type: "null" + reconciliationStatus: + enum: + - MATCHED + - MISMATCH + - null + type: + - string + - "null" + status: + description: The provider's status, mirrored verbatim + enum: + - draft + - open + - paid + - uncollectible + - void + - null + type: + - string + - "null" + syncedAt: + format: date-time + type: + - string + - "null" + totalExcludingTax: + description: The provider's total excluding tax, read back + format: int64 + type: + - integer + - "null" + required: + - externalCustomerId + - externalInvoiceId + - invoiceNumber + - status + - hostedInvoiceUrl + - invoicePdfUrl + - pushAttempts + - nextPushAt + - lastPushError + - lastPaymentError + - pushedAt + - syncedAt + - totalExcludingTax + - reconciliationStatus + - reconciledAt + - reconciliationDetail + type: object + ProviderSync: + additionalProperties: false + properties: + consecutiveFailures: + format: int32 + type: integer + lagSeconds: + description: Seconds since the last pass; null before the first + format: int64 + type: + - integer + - "null" + lastFullSweepAt: + format: date-time + type: + - string + - "null" + lastSuccessfulSyncAt: + description: The end of the last pass that ended SUCCESS; null before the first (V31-11) + format: date-time + type: + - string + - "null" + lastSyncError: + type: + - string + - "null" + lastSyncStatus: + enum: + - SUCCESS + - PARTIAL + - FAILED + - null + type: + - string + - "null" + lastSyncedAt: + description: The end of the last pass, whatever its status; null before the first + format: date-time + type: + - string + - "null" + providerKind: + enum: + - NOOP + - STRIPE + type: string + required: + - providerKind + - lastSyncedAt + - lastSuccessfulSyncAt + - lastSyncStatus + - lastSyncError + - consecutiveFailures + - lagSeconds + - lastFullSweepAt + type: object + PublicAddOn: + additionalProperties: false + properties: + addonId: + format: uuid + type: string + addonSlug: + examples: + - extra-seats-v1 + type: string + compatibleLicenseFamilies: + description: Public licence families this add-on fits + items: + type: string + type: array + description: + type: string + entitlements: + description: User-facing entitlements one unit grants + items: + $ref: "#/components/schemas/PublicAddOnEntitlement" + type: array + familySlug: + examples: + - extra-seats + type: string + maxQuantity: + description: Most units one instance may attach; null when unbounded + format: int32 + type: + - integer + - "null" + name: + type: string + prices: + items: + $ref: "#/components/schemas/PublicPrice" + type: array + pricingType: + enum: + - FREE + - PAID + - CUSTOM + type: string + required: + - familySlug + - addonSlug + - addonId + - name + - description + - pricingType + - maxQuantity + - prices + - entitlements + - compatibleLicenseFamilies + type: object + PublicAddOnEntitlement: + additionalProperties: false + properties: + limitCapExceededOveragePercent: + format: int32 + type: + - integer + - "null" + name: + type: string + overrideBehavior: + description: "How the grant combines with the plan's: added to it, replacing it, or the larger of the two" + enum: + - ADD + - OVERRIDE + - MAX + type: string + slug: + type: string + type: + enum: + - BOOLEAN + - NUMBER + - NUMBER_AI_CREDIT + - CONFIG + type: string + value: {} + required: + - slug + - name + - type + - value + - overrideBehavior + - limitCapExceededOveragePercent + type: object + PublicBranding: + additionalProperties: false + properties: + poweredByRemovable: + description: Whether the vendor's plan lets its storefront and portal hide "Powered by Kaiten" (the remove-powered-by entitlement). False while Kaiten's licensing authority cannot answer; always true self-hosted + type: boolean + required: + - poweredByRemovable + type: object + PublicCatalog: + additionalProperties: false + properties: + addOns: + description: The default version of each public add-on family; empty when include omits addons + items: + $ref: "#/components/schemas/PublicAddOn" + type: array + branding: + $ref: "#/components/schemas/PublicBranding" + capabilities: + $ref: "#/components/schemas/PublicCatalogCapabilities" + plans: + description: The default version of each public licence family, by family slug + items: + $ref: "#/components/schemas/PublicPlan" + type: array + required: + - plans + - addOns + - capabilities + - branding + type: object + PublicCatalogCapabilities: + additionalProperties: false + properties: + anonymousCheckout: + description: "Whether this publishable key starts anonymous checkouts (POST /public/checkout-sessions): on, with a Turnstile site key and secret" + type: boolean + checkout: + description: Whether a plan of the catalogue can be bought alone, through a customer session's checkout (POST /public/session/checkout) + type: boolean + paymentMethods: + description: Whether the organization's billing provider captures payment methods + type: boolean + turnstileSiteKey: + description: The Turnstile site key the page renders its widget with; null unless anonymousCheckout + type: + - string + - "null" + required: + - checkout + - paymentMethods + - anonymousCheckout + - turnstileSiteKey + type: object + PublicMeter: + additionalProperties: false + properties: + entitlementSlug: + examples: + - tokens + type: string + saleUnitFactor: + description: Measured units in one sale unit + examples: + - "10000" + type: string + saleUnitPlural: + description: The sale unit's plural label; null when the entitlement has none + examples: + - 10k tokens + type: + - string + - "null" + saleUnitSingular: + description: The sale unit's singular label; null when the entitlement has none + examples: + - 10k tokens + type: + - string + - "null" + required: + - entitlementSlug + - saleUnitFactor + - saleUnitSingular + - saleUnitPlural + type: object + PublicPlan: + additionalProperties: false + properties: + compatibleAddonFamilies: + description: Public add-on families whose listed version fits this plan + items: + type: string + type: array + currency: + description: ISO 4217 code of the plan's prices; null when it has none + examples: + - EUR + type: + - string + - "null" + description: + type: string + entitlements: + description: User-facing entitlements the plan grants, in display order + items: + $ref: "#/components/schemas/PublicPlanEntitlement" + type: array + familySlug: + examples: + - pro + type: string + licenseId: + format: uuid + type: string + licenseSlug: + description: The version's own slug + examples: + - pro-v3 + type: string + lifecycleState: + description: "Always PUBLISHED: only the default version is listed, and a default version is published" + enum: + - PUBLISHED + type: string + name: + examples: + - Pro + type: string + prices: + description: ACTIVE prices, in display order + items: + $ref: "#/components/schemas/PublicPrice" + type: array + pricingType: + description: "CUSTOM: priced on request; render selfServeCtaUrl" + enum: + - FREE + - PAID + - CUSTOM + type: string + requiresPaymentMethod: + description: A card is captured before subscribing, even when nothing is due + type: boolean + selfServe: + description: "Whether a customer can subscribe to this plan alone: not CUSTOM, with a default ACTIVE flat-fee price, and either FREE or sold through a provider that captures payment methods. Otherwise render selfServeCtaUrl." + type: boolean + selfServeCtaUrl: + description: Where to send a customer who cannot subscribe alone ("Contact us") + type: + - string + - "null" + trialPeriodDays: + description: Length of the free trial; null when the plan has none + format: int32 + type: + - integer + - "null" + required: + - familySlug + - licenseSlug + - licenseId + - name + - description + - lifecycleState + - pricingType + - trialPeriodDays + - requiresPaymentMethod + - selfServe + - selfServeCtaUrl + - currency + - prices + - compatibleAddonFamilies + - entitlements + type: object + PublicPlanEntitlement: + additionalProperties: false + properties: + description: + type: + - string + - "null" + displayOrder: + format: int32 + type: integer + icon: + type: + - string + - "null" + limitCapExceededOveragePercent: + description: "How far usage may exceed a NUMBER grant before it is refused: 0 is a hard limit, -1 is unlimited; null for other types" + format: int32 + type: + - integer + - "null" + name: + examples: + - Seats + type: string + saleUnitFactor: + type: + - string + - "null" + saleUnitPlural: + type: + - string + - "null" + saleUnitSingular: + type: + - string + - "null" + slug: + examples: + - seats + type: string + type: + enum: + - BOOLEAN + - NUMBER + - NUMBER_AI_CREDIT + - CONFIG + type: string + unitPlural: + examples: + - seats + type: + - string + - "null" + unitSingular: + examples: + - seat + type: + - string + - "null" + unlimited: + description: A NUMBER grant with no limit + type: boolean + value: + description: "The granted value: a boolean, a number, or a configuration object" + required: + - slug + - name + - description + - type + - value + - unlimited + - limitCapExceededOveragePercent + - icon + - unitSingular + - unitPlural + - saleUnitSingular + - saleUnitPlural + - saleUnitFactor + - displayOrder + type: object + PublicPrice: + additionalProperties: false + properties: + billingModel: + description: "FLAT_FEE: per billing period. USAGE_BASED: per sale unit of usage. OVERAGE: per sale unit of usage above the limit. Never sum metered prices into a headline." + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + billingPeriod: + description: The period a FLAT_FEE price is charged for; null on metered prices + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + - null + type: + - string + - "null" + billingTiming: + enum: + - ADVANCE + - ARREARS + type: string + currency: + examples: + - EUR + type: string + displayLabel: + description: The price's label; null when it has none + examples: + - Pro plan + type: + - string + - "null" + displayOrder: + format: int32 + type: integer + id: + format: uuid + type: string + isDefault: + description: The FLAT_FEE price shown for its billing period + type: boolean + metered: + description: What a metered price measures; null on FLAT_FEE prices + oneOf: + - $ref: "#/components/schemas/PublicMeter" + - enum: + - null + type: "null" + unitAmount: + description: The amount in minor units, when it is a whole number of them; null otherwise (render unitAmountDecimal) + examples: + - 2900 + format: int64 + type: + - integer + - "null" + unitAmountDecimal: + description: The amount in minor units, up to 12 decimal places + examples: + - "2900" + type: string + required: + - id + - billingModel + - billingTiming + - billingPeriod + - currency + - unitAmount + - unitAmountDecimal + - metered + - displayLabel + - displayOrder + - isDefault + type: object + PublicSurface: + additionalProperties: false + properties: + enabled: + description: "Whether publishable keys and customer sessions work for the organization: billing is on for it" + type: boolean + required: + - enabled + type: object + PublishableKey: + additionalProperties: false + properties: + allowedOrigins: + description: "Browser origins allowed to send the key. Empty: no browser origin may; requests without an Origin header (server-side rendering) still may." + examples: + - - https://www.example.com + items: + type: string + type: array + anonymousCheckout: + description: Whether the key may start an anonymous checkout (POST /public/checkout-sessions). Needs a Turnstile site key and secret. + type: boolean + createdAt: + format: date-time + readOnly: true + type: string + id: + description: Unique identifier of the key + readOnly: true + type: string + keyHint: + description: The last four characters of the key + examples: + - x9Qa + readOnly: true + type: string + label: + description: What the key is for, to tell keys apart + examples: + - Marketing site + type: string + lastUsedAt: + description: When the key last authenticated a request, to the minute; null before its first + format: date-time + readOnly: true + type: + - string + - "null" + revokedAt: + description: When the key was revoked; null while it is live + format: date-time + readOnly: true + type: + - string + - "null" + turnstileSecretConfigured: + description: Whether a Turnstile secret is stored for the key. The secret itself is write-only. + readOnly: true + type: boolean + turnstileSiteKey: + description: "The Turnstile site key the page renders its widget with; null without one. Public: the catalogue hands it to the page." + type: + - string + - "null" + updatedAt: + format: date-time + readOnly: true + type: string + required: + - id + - label + - keyHint + - allowedOrigins + - anonymousCheckout + - turnstileSiteKey + - turnstileSecretConfigured + - lastUsedAt + - createdAt + - updatedAt + - revokedAt + type: object + PublishableKeyCreated: + additionalProperties: false + properties: + allowedOrigins: + description: "Browser origins allowed to send the key. Empty: no browser origin may; requests without an Origin header (server-side rendering) still may." + examples: + - - https://www.example.com + items: + type: string + type: array + anonymousCheckout: + description: Whether the key may start an anonymous checkout (POST /public/checkout-sessions). Needs a Turnstile site key and secret. + type: boolean + createdAt: + format: date-time + readOnly: true + type: string + id: + description: Unique identifier of the key + readOnly: true + type: string + key: + description: "The key itself. Shown once: only its digest is stored." + examples: + - pk_3q2-7wEXAMPLEkeyBodyOfFortyThreeCharactersXyz + readOnly: true + type: string + keyHint: + description: The last four characters of the key + examples: + - x9Qa + readOnly: true + type: string + label: + description: What the key is for, to tell keys apart + examples: + - Marketing site + type: string + lastUsedAt: + description: When the key last authenticated a request, to the minute; null before its first + format: date-time + readOnly: true + type: + - string + - "null" + revokedAt: + description: When the key was revoked; null while it is live + format: date-time + readOnly: true + type: + - string + - "null" + turnstileSecretConfigured: + description: Whether a Turnstile secret is stored for the key. The secret itself is write-only. + readOnly: true + type: boolean + turnstileSiteKey: + description: "The Turnstile site key the page renders its widget with; null without one. Public: the catalogue hands it to the page." + type: + - string + - "null" + updatedAt: + format: date-time + readOnly: true + type: string + required: + - key + - id + - label + - keyHint + - allowedOrigins + - anonymousCheckout + - turnstileSiteKey + - turnstileSecretConfigured + - lastUsedAt + - createdAt + - updatedAt + - revokedAt + type: object + PublishableKeyDraft: + additionalProperties: false + properties: + allowedOrigins: + description: "Browser origins allowed to send the key, as scheme://host[:port]: https, or http for localhost. Empty: no browser origin may." + examples: + - - https://www.example.com + items: + type: string + maxItems: 50 + type: array + anonymousCheckout: + description: Open POST /public/checkout-sessions to the key. Needs turnstileSiteKey and turnstileSecret, else 422 .TurnstileNotConfigured. False by default. + type: boolean + label: + description: What the key is for + examples: + - Marketing site + maxLength: 100 + minLength: 1 + type: string + turnstileSecret: + description: "The Turnstile secret the server verifies tokens with. Stored in Vault, never returned: reads say turnstileSecretConfigured. 422 .VaultNotConfigured on a deployment without Vault." + maxLength: 255 + minLength: 1 + type: string + turnstileSiteKey: + description: The Turnstile site key the page renders its widget with + pattern: ^[A-Za-z0-9_-]{1,255}$ + type: string + required: + - label + - allowedOrigins + type: object + PublishableKeyEventDelivered: + additionalProperties: false + properties: + allowedOrigins: + items: + type: string + type: array + anonymousCheckout: + type: boolean + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + id: + type: string + keyHint: + description: The last four characters of the key + type: string + label: + type: string + revokedAt: + format: date-time + type: + - string + - "null" + turnstileSiteKey: + type: + - string + - "null" + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - label + - keyHint + - allowedOrigins + - anonymousCheckout + - turnstileSiteKey + - revokedAt + type: object + x-kaiten-payload: "#/components/schemas/PublishableKeyEvent" + PublishableKeyPatch: + additionalProperties: false + properties: + allowedOrigins: + description: Replaces the allowed browser origins. Takes effect on the next request. + items: + type: string + maxItems: 50 + type: array + anonymousCheckout: + description: Open or close POST /public/checkout-sessions to the key. On only with a Turnstile site key and secret, else 422 .TurnstileNotConfigured. + type: boolean + label: + description: What the key is for + maxLength: 100 + minLength: 1 + type: string + turnstileSecret: + description: The Turnstile secret, stored in Vault and never returned; "" deletes it. 422 .VaultNotConfigured on a deployment without Vault. + maxLength: 255 + type: string + turnstileSiteKey: + description: The Turnstile site key; "" clears it + pattern: ^[A-Za-z0-9_-]{0,255}$ + type: string + type: object + PushFailedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + lastPushError: + description: The provider's code and message, never a request body + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + pushAttempts: + format: int32 + type: integer + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - pushAttempts + - lastPushError + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/PushFailedInvoice" + PushFailures: + additionalProperties: false + properties: + count: + format: int64 + type: integer + oldestFailedAt: + format: date-time + type: + - string + - "null" + required: + - count + - oldestFailedAt + type: object + PushedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + externalInvoiceId: + type: string + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerInvoiceNumber: + type: string + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - externalInvoiceId + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/PushedInvoice" + QueuedInvoice: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoff: + $ref: "#/components/schemas/InvoiceHandoff" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + required: + - handoff + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + ReadSelection: + additionalProperties: false + properties: + all: + description: Mark every notification read, up to now + type: boolean + ids: + description: Notifications to mark read + items: + type: string + maxItems: 200 + type: + - array + - "null" + type: object + Reconciliation: + additionalProperties: false + properties: + discounts: + description: Allocations of DISCOUNT lines the provider applied with another amount, or not at all + items: + $ref: "#/components/schemas/DiscountDifference" + type: array + dueDate: + description: A SEND_INVOICE invoice the provider gives another due date than Kaiten's dueAt + oneOf: + - $ref: "#/components/schemas/DueDateDifference" + - enum: + - null + type: "null" + extraDiscounts: + description: Discounts the provider applied that Kaiten did not create, such as a coupon added in its dashboard + items: + $ref: "#/components/schemas/ExtraDiscount" + type: array + extraInProvider: + description: Provider lines Kaiten does not have + items: + type: string + type: array + inclusiveTax: + description: Whether the provider's subtotal was compared, its tax being included in the amounts + type: boolean + lines: + items: + $ref: "#/components/schemas/LineDifference" + type: array + missingInProvider: + description: Kaiten lines the provider does not have + items: + type: string + type: array + totals: + $ref: "#/components/schemas/TotalsDifference" + warnings: + description: What the comparison noticed without calling it a mismatch (§12.7, V31-60) + items: + type: string + type: + - array + - "null" + required: + - lines + - missingInProvider + - extraInProvider + - discounts + - extraDiscounts + - totals + - inclusiveTax + - dueDate + type: object + RedeemableCode: + additionalProperties: false + properties: + code: + examples: + - SUMMER-2026-LAUNCH + maxLength: 64 + minLength: 1 + type: string + required: + - code + type: object + Redemption: + additionalProperties: false + properties: + applicationsCount: + description: Invoices a PRICE voucher discounted + format: int32 + type: integer + applicationsMax: + description: 1 for ONE_TIME, durationInPeriods for REPEATING; null for FOREVER + format: int32 + type: + - integer + - "null" + codeHint: + type: string + effectiveExpiresAt: + description: When a boost stops applying; null for a PRICE voucher, counted in invoices, and for FOREVER + format: date-time + type: + - string + - "null" + effectiveStartsAt: + format: date-time + type: string + expiredAt: + format: date-time + type: + - string + - "null" + id: + readOnly: true + type: string + instanceSlug: + type: string + redeemedAt: + format: date-time + type: string + revokedAt: + format: date-time + type: + - string + - "null" + revokedReason: + type: + - string + - "null" + status: + enum: + - ACTIVE + - EXPIRED + - REVOKED + type: string + voucherId: + type: string + voucherName: + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - id + - voucherId + - voucherName + - voucherType + - codeHint + - instanceSlug + - redeemedAt + - effectiveStartsAt + - effectiveExpiresAt + - applicationsCount + - applicationsMax + - status + - expiredAt + - revokedAt + - revokedReason + type: object + RedemptionDelivered: + additionalProperties: false + properties: + applicationsCount: + description: Invoices a PRICE voucher discounted + format: int32 + type: integer + applicationsMax: + description: 1 for ONE_TIME, durationInPeriods for REPEATING; null for FOREVER + format: int32 + type: + - integer + - "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + codeHint: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + effectiveExpiresAt: + description: When a boost stops applying; null for a PRICE voucher, counted in invoices, and for FOREVER + format: date-time + type: + - string + - "null" + effectiveStartsAt: + format: date-time + type: string + expiredAt: + format: date-time + type: + - string + - "null" + id: + readOnly: true + type: string + instanceSlug: + type: string + redeemedAt: + format: date-time + type: string + revokedAt: + format: date-time + type: + - string + - "null" + revokedReason: + type: + - string + - "null" + status: + enum: + - ACTIVE + - EXPIRED + - REVOKED + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + voucherId: + type: string + voucherName: + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - id + - voucherId + - voucherName + - voucherType + - codeHint + - instanceSlug + - redeemedAt + - effectiveStartsAt + - effectiveExpiresAt + - applicationsCount + - applicationsMax + - status + - expiredAt + - revokedAt + - revokedReason + type: object + x-kaiten-payload: "#/components/schemas/Redemption" + RedemptionRules: + additionalProperties: false + properties: + annualOnly: + description: Only for an instance with a live ANNUAL subscription + type: boolean + firstTimeOnly: + description: Only for a customer none of whose instances has paid an invoice + type: boolean + minimumSubscriptionAmount: + $ref: "#/components/schemas/MinimumAmount" + description: Only for a live subscription whose base price is at least this. Strictly positive; omit it for no minimum (0 is refused with .InvalidRedemptionRules). + type: object + Release: + additionalProperties: false + properties: + componentIds: + description: List of component IDs to link to this release + examples: + - - 123e4567-e89b-12d3-a456-426614174000 + items: + type: string + type: + - array + - "null" + writeOnly: true + components: + description: List of components included in this release, fixed at creation + items: + $ref: "#/components/schemas/Component" + readOnly: true + type: + - array + - "null" + createdAt: + description: Timestamp when the release was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this release + readOnly: true + description: + description: Description of the release + examples: + - "This release fixes everything. CHANGELOG: stuff was done" + maxLength: 500 + type: string + id: + description: Unique identifier for the release + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - v1-0-0 + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + version: + description: Version of the release + examples: + - v1.0.0 + maxLength: 100 + minLength: 1 + type: string + required: + - id + - version + - createdBy + - createdAt + type: object + ReleaseDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + componentIds: + description: List of component IDs to link to this release + examples: + - - 123e4567-e89b-12d3-a456-426614174000 + items: + type: string + type: + - array + - "null" + writeOnly: true + components: + description: List of components included in this release, fixed at creation + items: + $ref: "#/components/schemas/Component" + readOnly: true + type: + - array + - "null" + createdAt: + description: Timestamp when the release was created + examples: + - "2023-10-01T12:00:00Z" + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + description: User who created this release + readOnly: true + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + description: + description: Description of the release + examples: + - "This release fixes everything. CHANGELOG: stuff was done" + maxLength: 500 + type: string + id: + description: Unique identifier for the release + examples: + - 123e4567-e89b-12d3-a456-426614174000 + readOnly: true + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + examples: + - v1-0-0 + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + version: + description: Version of the release + examples: + - v1.0.0 + maxLength: 100 + minLength: 1 + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - id + - version + - createdBy + - createdAt + type: object + x-kaiten-payload: "#/components/schemas/Release" + ReleasedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdDetail: + $ref: "#/components/schemas/HoldDetail" + description: The hold it left, which the invoice no longer shows + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + releaseReason: + type: string + releasedBy: + description: The user who released it, or system when a later check found the journal sound + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - releaseReason + - releasedBy + - holdDetail + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/ReleasedInvoice" + ReorderMetadataFieldsInput: + additionalProperties: false + properties: + ids: + items: + type: string + type: + - array + - "null" + required: + - ids + type: object + ReportEntitlementUsageBody: + additionalProperties: false + properties: + behavior: + description: "Report behavior: append folds the value into the stored total through the aggregation method; set overwrites it" + enum: + - append + - set + type: string + metadata: + additionalProperties: {} + description: "Optional metadata for the usage report, a JSON object stored with it in the usage history when its compact encoding is at most 4 KiB. Above that it is not stored, the report is still counted, and the response carries Kaiten-Metadata-Dropped: too_large. It must contain no personal data: anyone who can read the organization's instances can read it, for as long as the usage history is kept. Numbers are read as 64-bit floats, so send large identifiers as strings." + type: object + value: + description: Reported entitlement value, discriminated by the 'type' field. Usage reporting accepts the number variant only. + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + required: + - value + type: object + ReportedEntitlementUsage: + additionalProperties: false + properties: + currentPeriodEnd: + description: End of the current usage window (exclusive). Null for a lifetime entitlement (no configured reset period). + examples: + - "2026-04-01T00:00:00Z" + format: date-time + type: string + currentPeriodStart: + description: Start of the current usage window (inclusive). Null for a lifetime entitlement (no configured reset period). + examples: + - "2026-03-01T00:00:00Z" + format: date-time + type: string + entitlementId: + description: Unique identifier for the entitlement + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + entitlementSlug: + description: URL-friendly identifier of the entitlement + examples: + - premium-support + type: string + licenseId: + description: Unique identifier for the license to which this entitlement usage belongs + examples: + - 123e4567-e89b-12d3-a456-426614174001 + type: string + licenseSlug: + description: URL-friendly identifier of the license + examples: + - enterprise-license + type: string + limit: + description: The effective value this usage is measured against (the licence's grant, with the instance's add-ons and boosts applied; source and provenance say which), discriminated by the 'type' field. Absent when nothing grants the entitlement a value; -1 means unlimited for a NUMBER entitlement. + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + limitCapExceededOveragePercent: + description: "How far above limit usage is still accepted, in percent: -1 unlimited, 0 a hard limit. Absent for BOOLEAN and CONFIG." + format: int32 + type: integer + provenance: + description: "What the licence, the add-ons and the boosts each contribute. Null on a replay (Idempotent-Replayed): the journal does not store it." + oneOf: + - $ref: "#/components/schemas/Provenance" + - enum: + - null + type: "null" + source: + description: "license: the licence grants it (add-ons, boosts and an exception may change it); addon: an add-on attached does; exception: only the instance's exception does. Null on a replay (Idempotent-Replayed): the journal does not store it." + enum: + - license + - addon + - exception + - null + type: + - string + - "null" + transactionId: + description: The Idempotency-Key this report was accepted under, echoed on the first answer and on every replay; null when the request carried none. A client can tell from it that the server honours the key. + type: + - string + - "null" + value: + description: Resolved entitlement value, discriminated by the 'type' field + discriminator: + mapping: + boolean: "#/components/schemas/BooleanEntitlementValue" + number: "#/components/schemas/NumberEntitlementValue" + object: "#/components/schemas/ConfigEntitlementValue" + propertyName: type + oneOf: + - $ref: "#/components/schemas/NumberEntitlementValue" + - $ref: "#/components/schemas/BooleanEntitlementValue" + - $ref: "#/components/schemas/ConfigEntitlementValue" + required: + - source + - provenance + - transactionId + - entitlementId + - entitlementSlug + - licenseId + - licenseSlug + - value + type: object + RevocationReason: + additionalProperties: false + properties: + reason: + description: 1 to 500 characters; .ReasonRequired otherwise + examples: + - Granted by mistake + maxLength: 500 + minLength: 1 + type: string + required: + - reason + type: object + Role: + additionalProperties: false + properties: + builtIn: + type: boolean + description: + type: string + isAdministrator: + type: boolean + name: + type: string + permissions: + items: + type: string + type: array + slug: + type: string + required: + - slug + - name + - description + - permissions + - builtIn + - isAdministrator + type: object + RoleDefinition: + additionalProperties: false + properties: + description: + maxLength: 1000 + type: string + name: + maxLength: 200 + minLength: 1 + type: string + permissions: + items: + type: string + type: array + required: + - name + - description + - permissions + type: object + RolloutDate: + additionalProperties: false + properties: + end: + $ref: "#/components/schemas/RolloutStep" + start: + $ref: "#/components/schemas/RolloutStep" + type: + enum: + - rollout_date + type: string + required: + - type + - start + - end + type: object + RolloutDateTargeting: + additionalProperties: false + properties: + end: + $ref: "#/components/schemas/RolloutStep" + examples: + - date: "2024-01-31T23:59:59Z" + percentage: 100 + variant: variant_a + name: + examples: + - Targeting Rule 1 + type: string + rule: + description: CEL expression for basicTargeting evaluation + examples: + - user.group == 'beta' + maxLength: 10000 + type: string + start: + $ref: "#/components/schemas/RolloutStep" + examples: + - date: "2024-01-01T00:00:00Z" + percentage: 0 + variant: variant_a + type: + enum: + - rollout_date + type: string + required: + - type + - name + - rule + - start + - end + type: object + RolloutPercentage: + additionalProperties: false + properties: + distribution: + additionalProperties: + format: int64 + type: integer + type: object + type: + enum: + - rollout_percentage + type: string + required: + - type + - distribution + type: object + RolloutPercentageTargeting: + additionalProperties: false + properties: + distribution: + additionalProperties: + format: int64 + type: integer + examples: + - variant_a: 70 + variant_b: 30 + type: object + name: + examples: + - Targeting Rule 1234 + type: string + rule: + description: CEL expression for basicTargeting evaluation + examples: + - user.group == 'beta' + maxLength: 10000 + type: string + type: + enum: + - rollout_percentage + type: string + required: + - type + - name + - rule + - distribution + type: object + RolloutStep: + additionalProperties: false + properties: + date: + examples: + - "2024-01-15T10:00:00Z" + format: date-time + type: string + percentage: + examples: + - 50 + format: int64 + type: integer + variant: + examples: + - variant_b + type: string + required: + - variant + - percentage + - date + type: object + SampleUsageAmount: + additionalProperties: false + properties: + entitlementSlug: + description: An entitlement an ACTIVE price of the version meters + examples: + - tokens + type: string + quantity: + description: "Usage in the entitlement's measured units, a non-negative decimal string. Rated as one reset window against the version's grant: usage above limit × (1 + overage percent / 100) would be rejected, so it is capped, and the line says capped." + examples: + - "130500" + type: string + required: + - entitlementSlug + - quantity + type: object + ScheduledChange: + additionalProperties: false + properties: + effectiveAt: + description: "The boundary it applies at: the current period's end" + format: date-time + type: string + licenseSlug: + description: The licence version of that price + type: string + price: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription moves to + scheduledAt: + format: date-time + type: string + required: + - price + - licenseSlug + - scheduledAt + - effectiveAt + type: object + ServiceAccount: + additionalProperties: false + properties: + createdAt: + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + readOnly: true + externalId: + readOnly: true + type: string + id: + readOnly: true + type: string + name: + minLength: 1 + type: string + slug: + description: Optional URL-friendly identifier, unique per organization. Auto-generated if not provided. + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + tokens: + items: + $ref: "#/components/schemas/Token" + readOnly: true + type: + - array + - "null" + required: + - name + - tokens + type: object + SessionAddon: + additionalProperties: false + properties: + addonSlug: + examples: + - extra-seats-v1 + type: string + familySlug: + examples: + - extra-seats + type: string + maxQuantity: + description: The most one instance may hold; null when unbounded + format: int32 + type: + - integer + - "null" + name: + examples: + - Extra seats + type: string + pendingQuantity: + description: A decrease scheduled for the next renewal, 0 for a removal; null when nothing is scheduled + format: int32 + type: + - integer + - "null" + prices: + description: "What it is billed: the flat fee of the subscription's period and the metered prices. Quantity changes are billed from the next renewal, with no proration." + items: + $ref: "#/components/schemas/PublicPrice" + type: array + quantity: + description: 0 when the add-on is not held + examples: + - 3 + format: int32 + type: integer + required: + - addonSlug + - familySlug + - name + - quantity + - maxQuantity + - pendingQuantity + - prices + type: object + SessionAddonQuantity: + additionalProperties: false + properties: + quantity: + description: 0 removes the add-on; otherwise at most its maxQuantity + examples: + - 3 + format: int32 + minimum: 0 + type: integer + required: + - quantity + type: object + SessionAddonSelection: + additionalProperties: false + properties: + addonSlug: + type: string + quantity: + format: int32 + type: integer + required: + - addonSlug + - quantity + type: object + SessionBoost: + additionalProperties: false + properties: + entitlementSlug: + examples: + - tokens + type: string + modifierType: + enum: + - SET + - ADD + - MULTIPLY + - UNLIMITED + type: string + modifierValue: + description: Absent for UNLIMITED, and on a BOOLEAN or CONFIG boost + examples: + - "2" + type: string + value: + anyOf: + - type: boolean + - additionalProperties: true + type: object + - type: "null" + description: "A BOOLEAN or CONFIG boost's value: true or false, or the document" + required: + - entitlementSlug + - modifierType + type: object + SessionCancellation: + additionalProperties: false + properties: + reason: + description: Why the customer cancels, up to 500 characters; the vendor sees it on the subscription + type: string + type: object + SessionCheckout: + additionalProperties: false + properties: + activationInvoice: + $ref: "#/components/schemas/SessionInvoice" + description: "subscribed: the invoice of the first period; absent during a trial" + dueToday: + $ref: "#/components/schemas/DueToday" + description: "preview: what the checkout bills today; zero, with no lines, during a trial" + expiresAt: + description: "requires_payment_method: when the setup page expires" + format: date-time + type: string + payment: + $ref: "#/components/schemas/Payment" + description: "subscribed: how paying the activation invoice went" + setupSessionId: + description: "requires_payment_method: send it back as setupSessionId once the customer returns" + type: string + setupUrl: + description: "requires_payment_method: the provider's page where the customer saves a payment method" + type: string + status: + enum: + - preview + - requires_payment_method + - subscribed + type: string + subscription: + $ref: "#/components/schemas/SessionSubscription" + description: "subscribed: the subscription started" + trialEndsAt: + description: "preview: when the trial would end, if there is one" + format: date-time + type: string + required: + - status + type: object + SessionCheckoutOrder: + additionalProperties: false + properties: + addOns: + description: Public add-ons that fit the plan + items: + $ref: "#/components/schemas/SubscriptionAddon" + maxItems: 50 + type: + - array + - "null" + billingEmail: + description: Where invoices go. Required when the customer has none, except on a dryRun; stored on the customer when given. + maxLength: 254 + type: string + dryRun: + description: Answer what the checkout would bill today, and change nothing + type: boolean + licensePriceId: + description: An ACTIVE flat-fee price of a plan in the public catalogue. The instance is moved to that price's version when it is on another one. + type: string + returnUrl: + description: Where the setup page sends the customer back, with kaiten_setup_session appended. Required when a payment method has to be captured. Its origin must be one the organization's publishable keys allow. + format: uri + type: string + setupSessionId: + description: "The setupSessionId a requires_payment_method answer gave, once the customer is back from the setup page: the payment method saved there is checked with the provider and used." + type: string + trial: + description: "false skips the plan's trial. Omitted: the plan's trial, if it has one." + type: boolean + voucherCode: + description: Validated and redeemed in the checkout; any refusal answers .VoucherInvalid + maxLength: 64 + minLength: 1 + type: string + required: + - licensePriceId + type: object + SessionDiscount: + additionalProperties: false + properties: + appliesTo: + enum: + - LICENSE_BASE + - ADDONS + - BOTH + - SELECTED_PRICES + type: string + currency: + description: The currency of a FIXED_AMOUNT discount + examples: + - EUR + type: string + type: + enum: + - PERCENTAGE + - FIXED_AMOUNT + type: string + value: + description: A percentage in (0, 100], or an amount in minor units of currency + examples: + - "30" + type: string + required: + - type + - value + - appliesTo + type: object + SessionEventDelivered: + additionalProperties: false + properties: + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + clientReferenceId: + description: The vendor's opaque reference, as given at the start + type: + - string + - "null" + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: The customer the completion created; null before + type: + - string + - "null" + email: + description: The address the visitor gave at the start + type: string + instanceSlug: + description: The instance the completion created; null before + type: + - string + - "null" + licenseSlug: + type: string + sessionId: + type: string + status: + description: OPEN on started, COMPLETED on completed; on abandoned, ABANDONED (never completed, slugs null) or EXPIRED (its INCOMPLETE subscription expired or was canceled unpaid, slugs set) + enum: + - OPEN + - COMPLETED + - ABANDONED + - EXPIRED + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - sessionId + - email + - clientReferenceId + - licenseSlug + - billingPeriod + - customerSlug + - instanceSlug + - status + type: object + x-kaiten-payload: "#/components/schemas/SessionEvent" + SessionInvoice: + additionalProperties: false + properties: + currency: + examples: + - EUR + type: string + discountTotal: + description: Minor units + format: int64 + type: integer + dueAt: + format: date-time + type: + - string + - "null" + externalReference: + description: The vendor's own number for an invoice it collects itself + type: + - string + - "null" + hostedInvoiceUrl: + description: Where the customer pays the invoice, or confirms a payment that needs authenticating + type: + - string + - "null" + id: + format: uuid + type: string + invoicePdfUrl: + type: + - string + - "null" + issuedAt: + format: date-time + type: + - string + - "null" + kind: + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + lines: + items: + $ref: "#/components/schemas/SessionLine" + type: array + paidAt: + format: date-time + type: + - string + - "null" + providerInvoiceNumber: + type: + - string + - "null" + serviceFrom: + format: date-time + type: string + serviceTo: + format: date-time + type: string + status: + description: "PUSHED: issued and awaiting payment. PAYMENT_FAILED: a charge failed; pay on hostedInvoiceUrl. MANUAL: issued, the vendor collects it." + enum: + - MANUAL + - PUSHED + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Minor units + format: int64 + type: integer + total: + description: Minor units + format: int64 + type: integer + required: + - id + - kind + - status + - currency + - subtotal + - discountTotal + - total + - issuedAt + - dueAt + - paidAt + - serviceFrom + - serviceTo + - providerInvoiceNumber + - externalReference + - hostedInvoiceUrl + - invoicePdfUrl + - lines + type: object + SessionLine: + additionalProperties: false + properties: + amount: + description: Minor units; negative on a DISCOUNT line + format: int64 + type: integer + description: + type: string + label: + type: string + quantity: + description: In sale units, a decimal string + type: string + seq: + format: int64 + type: integer + serviceFrom: + format: date-time + type: string + serviceTo: + format: date-time + type: string + type: + enum: + - BASE + - ADDON + - USAGE + - OVERAGE + - DISCOUNT + type: string + unitAmountDecimal: + description: Minor units; null on a DISCOUNT line + type: + - string + - "null" + required: + - seq + - type + - label + - description + - quantity + - unitAmountDecimal + - amount + - serviceFrom + - serviceTo + type: object + SessionPortal: + additionalProperties: false + properties: + addOns: + description: The add-ons the instance holds + items: + $ref: "#/components/schemas/SessionAddon" + type: array + branding: + $ref: "#/components/schemas/PublicBranding" + capabilities: + $ref: "#/components/schemas/PortalCapabilities" + customer: + $ref: "#/components/schemas/PortalCustomer" + entitlements: + description: "The instance's effective entitlements: its licence's, raised or added by add-ons and boosts" + items: + $ref: "#/components/schemas/PortalEntitlement" + type: array + instance: + description: The session's instance; null for a session bound to the customer only + oneOf: + - $ref: "#/components/schemas/PortalInstance" + - enum: + - null + type: "null" + instances: + description: The customer's instances, for a session bound to the customer only; empty otherwise + items: + $ref: "#/components/schemas/PortalInstanceSummary" + type: array + paymentMethod: + description: The payment method the customer's invoices are charged to; null when none is saved + oneOf: + - $ref: "#/components/schemas/PortalPaymentMethod" + - enum: + - null + type: "null" + subscription: + description: The instance's subscription; null when it has none, or the session is bound to no instance + oneOf: + - $ref: "#/components/schemas/SessionSubscription" + - enum: + - null + type: "null" + trial: + description: "TRIAL only: when the trial ends, when its reminder is due, what the conversion would charge and to which card (V32-09); null otherwise" + oneOf: + - $ref: "#/components/schemas/PortalTrial" + - enum: + - null + type: "null" + upcomingInvoice: + description: What the next boundary will bill, on usage so far; null without a live subscription + oneOf: + - $ref: "#/components/schemas/PortalUpcomingInvoice" + - enum: + - null + type: "null" + updatedAt: + description: When this snapshot was read + format: date-time + type: string + vouchers: + description: The instance's ACTIVE redemptions. Never a code. + items: + $ref: "#/components/schemas/SessionRedemption" + type: array + required: + - customer + - instance + - instances + - subscription + - entitlements + - addOns + - vouchers + - paymentMethod + - upcomingInvoice + - trial + - capabilities + - branding + - updatedAt + type: object + SessionRedeem: + additionalProperties: false + properties: + code: + description: The code as the customer typed it; case and separators do not matter + examples: + - SUMMER-2026-LAUNCH + maxLength: 64 + minLength: 1 + type: string + dryRun: + description: Check, and answer the redemption the call would make, without making it; counts as one attempt + type: boolean + required: + - code + type: object + SessionRedemption: + additionalProperties: false + properties: + applicationsCount: + description: Invoices its PRICE part discounted; 0 without one + format: int32 + type: integer + applicationsMax: + description: "How many it discounts in all: 1 for ONE_TIME, its periods for REPEATING; null for FOREVER and without a PRICE part" + format: int32 + type: + - integer + - "null" + appliesFromInvoice: + description: "next: a PRICE part that has not discounted an invoice yet -- it applies from the next invoice composed, never to one already issued; null otherwise" + enum: + - next + - null + type: + - string + - "null" + effectiveExpiresAt: + description: When a boost or a flag grant stops applying; null when it does not expire, and for a PRICE voucher, counted in invoices + format: date-time + type: + - string + - "null" + effectiveStartsAt: + format: date-time + type: string + name: + examples: + - Summer launch + type: string + status: + enum: + - ACTIVE + - EXPIRED + - REVOKED + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - name + - voucherType + - status + - appliesFromInvoice + - applicationsCount + - applicationsMax + - effectiveStartsAt + - effectiveExpiresAt + type: object + SessionScheduledChange: + additionalProperties: false + properties: + effectiveAt: + description: "When it applies: the current period's end" + format: date-time + type: string + price: + $ref: "#/components/schemas/PublicPrice" + description: The price the subscription moves to + required: + - price + - effectiveAt + type: object + SessionSubscription: + additionalProperties: false + properties: + basePrice: + $ref: "#/components/schemas/PublicPrice" + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + cancelAtPeriodEnd: + description: The subscription ends at currentPeriodEnd; reactivate it before then to keep it + type: boolean + canceledAt: + description: When the subscription was canceled + format: date-time + type: + - string + - "null" + cancellationReason: + description: Why the system ended it; null when it did not, a vendor's own reason is never shown + enum: + - TRIAL_ENDED_WITHOUT_PAYMENT_METHOD + - INCOMPLETE_EXPIRED + - null + type: + - string + - "null" + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + currency: + examples: + - EUR + type: string + currentPeriodEnd: + description: "When the current period ends: the renewal date, or the trial's end" + format: date-time + type: string + currentPeriodStart: + format: date-time + type: string + incompleteExpiresAt: + description: "INCOMPLETE only: when the checkout ends unpaid" + format: date-time + type: + - string + - "null" + providerKind: + enum: + - NOOP + - STRIPE + type: string + scheduledChange: + description: A plan change the vendor scheduled for the next boundary + oneOf: + - $ref: "#/components/schemas/SessionScheduledChange" + - enum: + - null + type: "null" + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + type: string + trialEndsAt: + format: date-time + type: + - string + - "null" + required: + - status + - providerKind + - collectionMethod + - currency + - billingPeriod + - basePrice + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - incompleteExpiresAt + - cancelAtPeriodEnd + - canceledAt + - cancellationReason + - scheduledChange + type: object + SessionVoucher: + additionalProperties: false + properties: + boosts: + description: "ENTITLEMENT_BOOST: what it raises" + items: + $ref: "#/components/schemas/SessionBoost" + type: + - array + - "null" + discount: + $ref: "#/components/schemas/SessionDiscount" + description: "PRICE: what it takes off" + duration: + description: "PRICE: how many invoices it discounts (one, durationInPeriods, or every one). ENTITLEMENT_BOOST and FLAG_GRANT: how long it lasts, in billing periods. COMPOSITE: both, each for its part, which it carries each of." + enum: + - ONE_TIME + - REPEATING + - FOREVER + type: string + durationInPeriods: + format: int32 + type: integer + flagGrants: + description: "FLAG_GRANT: the flag variants it grants" + items: + $ref: "#/components/schemas/FlagGrant" + type: + - array + - "null" + name: + examples: + - Summer launch + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - name + - voucherType + - duration + type: object + SessionVoucherCheck: + additionalProperties: false + properties: + code: + description: The code as the customer typed it; case and separators do not matter + examples: + - SUMMER-2026-LAUNCH + maxLength: 64 + minLength: 1 + type: string + licensePriceId: + description: "The price the customer is about to check out: the checks that read the subscription read this price" + type: string + required: + - code + type: object + SessionVoucherValidity: + additionalProperties: false + properties: + valid: + type: boolean + voucher: + $ref: "#/components/schemas/SessionVoucher" + description: What the voucher gives, when valid. Never its code. + required: + - valid + type: object + StartedSubscription: + additionalProperties: false + properties: + activationInvoice: + $ref: "#/components/schemas/InvoiceSummary" + description: "The invoice of the first period, when the base price bills in advance; absent otherwise. Charged automatically, it is pushed and charged before this answer when the provider answers within 20 seconds: its status says how it went." + anchorAt: + description: The instant its periods are counted from + format: date-time + type: string + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time + type: + - string + - "null" + canceledAt: + format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's + type: + - string + - "null" + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time + type: string + currentPeriodStart: + format: date-time + type: string + customerName: + type: string + customerSlug: + type: string + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" + format: int32 + type: integer + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 + type: + - integer + - "null" + id: + type: string + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" + format: date-time + type: + - string + - "null" + instanceName: + type: string + instanceSlug: + description: As it was at the subscription's last write + type: string + pastDueSince: + format: date-time + type: + - string + - "null" + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" + format: date-time + type: string + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + type: string + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" + updatedAt: + format: date-time + type: string + required: + - id + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince + - createdAt + - updatedAt + type: object + StatusChangeDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + from: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + type: string + instanceBillingId: + type: string + instanceSlug: + type: string + reason: + enum: + - TRIAL_ENDED + - INVOICE_OVERDUE + - PAYMENT_FAILED + - SETTLED + type: string + to: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - instanceBillingId + - instanceSlug + - from + - to + - reason + type: object + x-kaiten-payload: "#/components/schemas/StatusChange" + SubscriptionAddon: + additionalProperties: false + properties: + addonSlug: + description: The add-on version to attach + examples: + - extra-seats-v1 + type: string + quantity: + default: 1 + examples: + - 3 + format: int32 + minimum: 1 + type: integer + required: + - addonSlug + type: object + SubscriptionCancellation: + additionalProperties: false + properties: + mode: + description: "AT_PERIOD_END (the default): the period paid for runs out, then the FINAL invoice is issued. IMMEDIATE: the FINAL invoice is issued now, flat fees billed in full and nothing refunded." + enum: + - AT_PERIOD_END + - IMMEDIATE + type: string + reason: + description: Up to 500 characters + type: string + type: object + SubscriptionTerms: + additionalProperties: false + properties: + collectionMethod: + description: "null: the organization's default. CHARGE_AUTOMATICALLY needs a provider that charges automatically (UpdateInstanceBilling.CollectionMethodUnsupported)." + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: + - string + - "null" + daysUntilDue: + description: "0 to 365; null: the organization's default" + format: int32 + type: + - integer + - "null" + providerKind: + description: "Who issues and collects the invoices, from the next one on. NOOP: the organization itself, through the handoff queue. A provider must be connected (UpdateInstanceBilling.ProviderNotConnected); one that pushes invoices needs the customer's billing e-mail for SEND_INVOICE (.BillingEmailMissing) and the subscription's currency (.UnsupportedCurrency)." + enum: + - NOOP + - STRIPE + type: string + type: object + SyncFailureDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + consecutiveFailures: + format: int32 + type: integer + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + lastSyncError: + type: string + lastSyncedAt: + format: date-time + type: string + providerKind: + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - providerKind + - consecutiveFailures + - lastSyncError + - lastSyncedAt + type: object + x-kaiten-payload: "#/components/schemas/SyncFailure" + SyncReport: + additionalProperties: false + properties: + providers: + items: + $ref: "#/components/schemas/Outcome" + type: array + required: + - providers + type: object + SystemTokenIssuanceDelivered: + additionalProperties: false + properties: + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + credentialKind: + description: Credential class that performed the mint; "platform" is the system:kaiten identity + examples: + - platform + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + expiresAt: + description: Absent when the minted credential does not expire + format: date-time + type: string + issuedAt: + description: When the credential was minted + format: date-time + type: string + issuedTokenId: + description: The credential that was minted + type: string + name: + description: Human-readable label the caller gave the minted credential + examples: + - ci-deploy + type: string + platformTokenId: + description: Platform credential that performed the mint; revoking it revokes everything it issued + type: string + scopes: + description: Scopes granted to the minted credential + examples: + - - read:customers + items: + type: string + type: + - array + - "null" + slug: + description: Server-assigned identifier of the minted credential + examples: + - system-kaiten-9f2c1a + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - credentialKind + - platformTokenId + - issuedTokenId + - name + - slug + - scopes + - issuedAt + type: object + x-kaiten-payload: "#/components/schemas/SystemTokenIssuance" + TargetingContext: + additionalProperties: false + properties: + roots: + description: "Identifiers the server guarantees, and what is reachable below each. Not a closed set: a host may target on its own attributes too" + items: + $ref: "#/components/schemas/TargetingContextNode" + type: array + required: + - roots + type: object + TargetingContextNode: + additionalProperties: false + properties: + description: + description: What this value means, shown by the editor + type: string + entries: + description: Shape of each known key, for a map whose entries differ. Named after the key + items: + $ref: "#/components/schemas/TargetingContextNode" + type: array + fields: + description: Named children, for an object + items: + $ref: "#/components/schemas/TargetingContextNode" + type: array + knownKeys: + description: Keys this map currently has, for completion. Not a closed set + items: + type: string + type: array + name: + description: Name as it is written in a rule + type: string + optional: + description: Whether this value may be absent, in which case has() is worth using + type: boolean + type: + description: Shape of this node's value + enum: + - string + - number + - boolean + - object + - map + - dyn + type: string + values: + $ref: "#/components/schemas/TargetingContextNode" + description: Shape shared by every entry, for a map + required: + - name + - type + type: object + TargetingRuleDraft: + additionalProperties: false + properties: + rule: + description: The CEL targeting rule to check. May be blank, which is itself a mistake this reports + examples: + - __kaiten.license.familySlug == 'scale' + maxLength: 10000 + type: string + required: + - rule + type: object + TargetingRuleIssue: + additionalProperties: false + properties: + column: + description: 1-based column the problem starts on, or 0 when it has no position + format: int64 + type: integer + endColumn: + description: 1-based column just past the problem, or 0 when only a point is known + format: int64 + type: integer + endLine: + description: 1-based line the problem ends on, or 0 when only a point is known + format: int64 + type: integer + line: + description: 1-based line the problem starts on, or 0 when it has no position + format: int64 + type: integer + message: + description: What is wrong, in the same words the write path refuses it with + type: string + required: + - message + - line + - column + type: object + TargetingRuleRehearsal: + additionalProperties: false + properties: + evaluationError: + description: Why the engine could not answer, when it could not — e.g. the rule reads a host attribute this context does not carry. At a real evaluation the rule would simply not match + type: string + facts: + additionalProperties: {} + description: The __kaiten namespace as enriched for this context — what a rule actually reads. Empty sub-roots mean the context resolved nothing for them + type: object + issues: + description: Problems the lint found. The rule is only run when there are none + items: + $ref: "#/components/schemas/TargetingRuleIssue" + type: array + matched: + description: Whether the rule matched this context. Meaningful only when valid and evaluationError is empty + type: boolean + valid: + description: Whether the rule lints clean + type: boolean + required: + - facts + - issues + - valid + - matched + type: object + TargetingRuleTrial: + additionalProperties: false + properties: + context: + additionalProperties: {} + description: The evaluation context to rehearse against, as a client would send it. Anything under __kaiten is discarded, exactly as at a real evaluation + type: object + rule: + description: The CEL targeting rule to rehearse + examples: + - __kaiten.license.familySlug == 'scale' + maxLength: 10000 + type: string + targetingKey: + description: The key the evaluation would be bucketed on. Resolves license and entitlement facts when it names a customer slug + type: string + required: + - rule + type: object + TargetingRuleVerdict: + additionalProperties: false + properties: + issues: + description: Every problem found, positioned where it is written. Empty when the rule is accepted + items: + $ref: "#/components/schemas/TargetingRuleIssue" + type: array + valid: + description: Whether the write path would accept this rule + type: boolean + required: + - issues + - valid + type: object + Targetings: + description: Polymorphic list of targeting rules (Basic, RolloutDate, RolloutPercentage) + items: + anyOf: + - $ref: "#/components/schemas/BasicTargeting" + - $ref: "#/components/schemas/RolloutDateTargeting" + - $ref: "#/components/schemas/RolloutPercentageTargeting" + type: + - array + - "null" + Token: + additionalProperties: false + properties: + createdAt: + format: date-time + readOnly: true + type: string + createdBy: + $ref: "#/components/schemas/User" + readOnly: true + expiresAt: + format: date-time + type: string + id: + readOnly: true + type: string + name: + type: string + revokedAt: + format: date-time + readOnly: true + type: string + revokedBy: + $ref: "#/components/schemas/User" + readOnly: true + scopes: + items: + type: string + type: + - array + - "null" + serviceAccountId: + readOnly: true + type: string + slug: + description: Optional URL-friendly identifier, unique per service account. Auto-generated if not provided. + maxLength: 100 + minLength: 2 + pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$ + type: string + required: + - name + - scopes + - createdAt + - createdBy + type: object + TotalsDifference: + additionalProperties: false + properties: + kaitenTotal: + format: int64 + type: integer + providerSubtotal: + format: int64 + type: integer + providerTotalDiscount: + description: "With inclusive tax: the provider's discounts, already taken off its subtotal" + format: int64 + type: integer + providerTotalExcludingTax: + format: int64 + type: integer + required: + - kaitenTotal + - providerTotalExcludingTax + type: object + TrialCharge: + additionalProperties: false + properties: + amount: + format: int64 + type: integer + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + currency: + examples: + - EUR + type: string + required: + - amount + - currency + - billingPeriod + type: object + TrialCustomer: + additionalProperties: false + properties: + billingEmail: + description: "Personal data: the second payload that carries an e-mail (D-56)" + type: + - string + - "null" + name: + type: string + slug: + type: string + required: + - slug + - name + - billingEmail + type: object + TrialInstance: + additionalProperties: false + properties: + name: + type: string + slug: + type: string + required: + - slug + - name + type: object + TrialPaymentMethod: + additionalProperties: false + properties: + brand: + examples: + - visa + type: + - string + - "null" + expMonth: + format: int64 + type: + - integer + - "null" + expYear: + format: int64 + type: + - integer + - "null" + last4: + examples: + - "4242" + type: + - string + - "null" + required: + - brand + - last4 + - expMonth + - expYear + type: object + TrialWillEndDelivered: + additionalProperties: false + properties: + anchorAt: + description: The instant its periods are counted from + format: date-time + type: string + basePrice: + $ref: "#/components/schemas/Price" + description: The FLAT_FEE price the subscription is pinned to, deprecated or not + billingPeriod: + enum: + - MONTHLY + - QUARTERLY + - SEMI_ANNUAL + - ANNUAL + type: string + cancelAtPeriodEnd: + type: boolean + cancelRequestedAt: + format: date-time + type: + - string + - "null" + canceledAt: + format: date-time + type: + - string + - "null" + cancellationReason: + type: + - string + - "null" + charge: + description: What the conversion would charge, as upcoming-invoice previews it, discounts included and before any tax the provider adds; null when nothing would be + oneOf: + - $ref: "#/components/schemas/TrialCharge" + - enum: + - null + type: "null" + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + description: "Effective: the subscription's own, else the organization's default" + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + collectionMethodOverride: + description: The subscription's own collection method; null when it takes the organization's + type: + - string + - "null" + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + currentPeriodEnd: + description: "The next boundary: the period closes, and its invoice is composed, after it" + format: date-time + type: string + currentPeriodStart: + format: date-time + type: string + customer: + $ref: "#/components/schemas/TrialCustomer" + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + type: string + daysLeft: + description: Whole days from the event to trialEndsAt, rounded up; 0 once it passed + format: int64 + type: integer + daysUntilDue: + description: "Effective: the subscription's own, else the organization's default" + format: int32 + type: integer + daysUntilDueOverride: + description: The subscription's own payment terms; null when it takes the organization's + format: int32 + type: + - integer + - "null" + familySlug: + description: Its family + type: string + id: + type: string + incompleteExpiresAt: + description: "When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription that carries one" + format: date-time + type: + - string + - "null" + instance: + $ref: "#/components/schemas/TrialInstance" + instanceName: + type: string + instanceSlug: + description: As it was at the subscription's last write + type: string + licenseName: + description: The licence version of the base price + type: string + pastDueSince: + format: date-time + type: + - string + - "null" + paymentMethod: + description: The ACTIVE card the conversion would be charged to; null without one, or on NOOP + oneOf: + - $ref: "#/components/schemas/TrialPaymentMethod" + - enum: + - null + type: "null" + pendingLicenseSlug: + description: The licence version a checkout waiting for its payment moves the instance to once paid + type: + - string + - "null" + providerKind: + description: "Who collects its invoices. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + reminderAt: + description: "When trial_will_end is (or was) emitted: trialEndsAt minus the family's trialReminderDays as it was when the trial started; set with trialEndsAt" + format: date-time + type: + - string + - "null" + scheduledChange: + description: A plan change waiting for the next boundary + oneOf: + - $ref: "#/components/schemas/ScheduledChange" + - enum: + - null + type: "null" + startedAt: + description: "When billing started: usage before it is never billed" + format: date-time + type: string + status: + enum: + - INCOMPLETE + - TRIAL + - ACTIVE + - PAST_DUE + - CANCELED + type: string + trialEndsAt: + description: "When the trial ends, or ended: usage before it is never billed" + format: date-time + type: + - string + - "null" + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - customer + - instance + - licenseName + - familySlug + - daysLeft + - charge + - paymentMethod + - id + - instanceSlug + - instanceName + - customerSlug + - customerName + - status + - providerKind + - collectionMethod + - collectionMethodOverride + - daysUntilDue + - daysUntilDueOverride + - basePrice + - billingPeriod + - currency + - anchorAt + - startedAt + - currentPeriodStart + - currentPeriodEnd + - trialEndsAt + - reminderAt + - pendingLicenseSlug + - incompleteExpiresAt + - scheduledChange + - cancelAtPeriodEnd + - cancelRequestedAt + - canceledAt + - cancellationReason + - pastDueSince + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/TrialWillEnd" + UncollectibleInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + reason: + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + required: + - reason + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/UncollectibleInvoice" + UsageLedger: + additionalProperties: false + properties: + retentionMonths: + description: How long usage reports are kept, in months; null when they are kept forever (ENV) or the retention is unknown (UNKNOWN) + format: int64 + type: + - integer + - "null" + retentionSource: + description: "KBK: the organization's licence or an add-on; ENV: KAITEN_USAGE_LEDGER_RETENTION_MONTHS, with no licensing authority; UNKNOWN: the licensing authority could not tell" + enum: + - KBK + - ENV + - UNKNOWN + type: string + required: + - retentionMonths + - retentionSource + type: object + UsageReport: + additionalProperties: false + properties: + aggregationMethod: + description: The entitlement's aggregation method when the report was accepted + examples: + - SUM + type: string + behavior: + description: append adds the value through the aggregation method; set overwrites the counter + enum: + - append + - set + type: string + delta: + description: valueAfter minus valueBefore; negative for a set that lowered the counter + examples: + - "600" + type: string + entitlementId: + description: The entitlement the report was made for. It may since have been deleted. + format: uuid + type: string + eventCountAfter: + description: Reports counted in the window after this one + examples: + - 1 + format: int32 + type: integer + instanceId: + description: The instance the report was made for. It may since have been deleted. + format: uuid + type: string + licenseId: + description: The instance's licence when the report was accepted + format: uuid + type: string + limitValue: + description: The limit in force when the report was accepted. Null when unlimited. + examples: + - "1000" + type: + - string + - "null" + overageDelta: + description: "How much the usage above limitValue moved: max(0, valueAfter - limitValue) - max(0, valueBefore - limitValue). 0 when unlimited." + examples: + - "0" + type: string + overagePercent: + description: The overage allowed above limitValue, in percent, when the report was accepted. Null when unlimited. + examples: + - 50 + format: int32 + type: + - integer + - "null" + properties: + additionalProperties: {} + description: The report's metadata; null when none was stored + type: + - object + - "null" + reportSeq: + description: Position of the report among the pair's accepted reports, from 1, without gaps + examples: + - 42 + format: int64 + type: integer + reportedAt: + description: When the server accepted the report (UTC, millisecond precision) + examples: + - "2026-10-05T08:00:00.000Z" + format: date-time + type: string + reportedValue: + description: "The value as sent: a delta for append, an absolute value for set" + examples: + - "600" + type: string + transactionId: + description: The report's idempotency key; null when it was sent without one + examples: + - llm-call-9f2c:tokens + type: + - string + - "null" + valueAfter: + description: The counter after the report + examples: + - "600" + type: string + valueBefore: + description: The counter before the report, after any window reset + examples: + - "0" + type: string + windowEnd: + description: End of the usage window the report counted in (exclusive). Null for a lifetime entitlement. + format: date-time + type: + - string + - "null" + windowStart: + description: Start of the usage window the report counted in (inclusive). Null for a lifetime entitlement. + format: date-time + type: + - string + - "null" + required: + - instanceId + - entitlementId + - reportSeq + - reportedAt + - behavior + - aggregationMethod + - reportedValue + - valueBefore + - valueAfter + - delta + - overageDelta + - eventCountAfter + - windowStart + - windowEnd + - limitValue + - overagePercent + - licenseId + - transactionId + - properties + type: object + UsageReportPage: + additionalProperties: false + properties: + items: + description: The reports, in reportSeq order + items: + $ref: "#/components/schemas/UsageReport" + type: array + nextAfterSeq: + description: Pass as afterSeq to read the next page. Absent on the last page. + examples: + - 100 + format: int64 + type: integer + required: + - items + type: object + User: + additionalProperties: false + properties: + id: + description: ID of the user + examples: + - 123e4567-e89b-12d3-a456-426614174000 + type: string + name: + description: Name of the user + examples: + - John Doe + type: string + required: + - id + type: object + UserAuthorization: + additionalProperties: false + properties: + enabled: + type: boolean + permissions: + items: + type: string + type: array + roles: + items: + type: string + type: array + required: + - enabled + - roles + - permissions + type: object + UserEnablement: + additionalProperties: false + properties: + enabled: + type: boolean + required: + - enabled + type: object + UserRoleAssignments: + additionalProperties: false + properties: + permissions: + items: + type: string + type: array + roles: + items: + type: string + type: array + required: + - roles + - permissions + type: object + Validity: + additionalProperties: false + properties: + reason: + description: "The first failing check. DISCOUNT_LIMIT_EXCEEDED: a line of the instance's invoices would bear more than 20 discounts" + enum: + - NOT_FOUND + - NOT_ACTIVE + - NOT_YET_VALID + - EXPIRED + - EXHAUSTED + - ALREADY_REDEEMED + - NOT_ELIGIBLE + - CURRENCY_MISMATCH + - DISCOUNT_LIMIT_EXCEEDED + type: string + rule: + description: "With NOT_ELIGIBLE: the eligibility rule" + enum: + - RESTRICTED_CUSTOMER + - LICENSE_NOT_APPLICABLE + - ADDON_NOT_APPLICABLE + - FIRST_TIME_ONLY + - ANNUAL_ONLY + - MINIMUM_SUBSCRIPTION_AMOUNT + - NOTHING_TO_BOOST + type: string + valid: + type: boolean + voucher: + $ref: "#/components/schemas/Voucher" + description: The voucher, without its code, when the code names one + required: + - valid + type: object + Variant: + additionalProperties: false + properties: + description: + type: string + name: + type: string + value: {} + required: + - name + - description + - value + type: object + VoidedInvoiceDelivered: + additionalProperties: false + properties: + boundaryAt: + description: The boundary the invoice bills. With the kind, it identifies the invoice within its subscription + format: date-time + type: string + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + collectionMethod: + enum: + - SEND_INVOICE + - CHARGE_AUTOMATICALLY + type: string + createdAt: + format: date-time + type: string + currency: + examples: + - EUR + type: string + customerName: + type: string + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + customerSlug: + description: As it was when the invoice was composed + type: string + daysUntilDue: + format: int32 + type: + - integer + - "null" + discountTotal: + description: Sum of the discounts, in minor units + format: int64 + type: integer + dueAt: + description: issuedAt + daysUntilDue + format: date-time + type: + - string + - "null" + handoffStatus: + description: "PENDING: waiting in the handoff queue for the organization's accounting system" + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + holdReason: + description: "Why a DRAFT is held: the usage journal it was measured from failed a consistency check (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT)" + enum: + - LEDGER_SEQUENCE_GAP + - LEDGER_CHAIN_BREAK + - LEDGER_COUNTER_MISMATCH + - PROVIDER_ITEM_LIMIT + - null + type: + - string + - "null" + id: + description: "The invoice's identifier: the key an accounting system deduplicates on" + type: string + instanceName: + type: string + instanceSlug: + type: string + issuedAt: + format: date-time + type: + - string + - "null" + kind: + description: ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the one that starts, FINAL what is left when a subscription ends + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + licenseSlug: + type: string + overdue: + description: "Whether the invoice is overdue when read, the grace period of automatic collection included: the predicate of ?overdue=true. Computed by the invoice list and the invoice read; false in the answers of writes and in event payloads." + type: boolean + paidAt: + format: date-time + type: + - string + - "null" + providerKind: + description: "Who collects it. NOOP: the organization itself, through the handoff queue" + enum: + - NOOP + - STRIPE + type: string + serviceFrom: + description: Earliest start of its lines' service periods + format: date-time + type: string + serviceTo: + description: Latest end of its lines' service periods + format: date-time + type: string + status: + description: "MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. DRAFT: held, waiting for a release or a recompose" + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + subtotal: + description: Sum of the lines, in minor units + format: int64 + type: integer + total: + description: subtotal − discountTotal, in minor units + format: int64 + type: integer + updatedAt: + format: date-time + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + voidReason: + type: string + required: + - voidReason + - id + - kind + - boundaryAt + - serviceFrom + - serviceTo + - status + - holdReason + - providerKind + - collectionMethod + - currency + - subtotal + - discountTotal + - total + - issuedAt + - daysUntilDue + - dueAt + - paidAt + - customerSlug + - customerName + - instanceSlug + - instanceName + - licenseSlug + - handoffStatus + - overdue + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/VoidedInvoice" + Voucher: + additionalProperties: false + properties: + applicableAddonIds: + description: Only instances holding one of these add-on versions can redeem it; any when empty + items: + type: string + type: array + applicableAddonPriceIds: + items: + type: string + type: array + applicableLicenseIds: + description: Only instances on these licence versions can redeem it; any when empty + items: + type: string + type: array + applicableLicensePriceIds: + items: + type: string + type: array + code: + description: The code to redeem. Returned to read:vouchers callers only; never in an event. + examples: + - SUMMER-2026-LAUNCH + type: string + codeHint: + description: The last four characters of the normalized code + examples: + - UNCH + type: string + createdAt: + format: date-time + readOnly: true + type: string + currency: + description: The currency of a FIXED_AMOUNT discount + examples: + - EUR + type: + - string + - "null" + description: + type: + - string + - "null" + duration: + description: "PRICE: how many invoices it discounts (one, durationInPeriods, or every one). BOOST and FLAG_GRANT: how long a redemption lasts, in the subscription's billing periods. COMPOSITE: both, each for its part." + enum: + - ONE_TIME + - REPEATING + - FOREVER + type: string + durationInPeriods: + format: int32 + type: + - integer + - "null" + expiresAt: + format: date-time + type: + - string + - "null" + flagGrants: + description: The flag variants a FLAG_GRANT grants + items: + $ref: "#/components/schemas/FlagGrant" + type: array + grants: + description: What an ENTITLEMENT_BOOST changes + items: + $ref: "#/components/schemas/Grant" + type: array + id: + readOnly: true + type: string + maxRedemptions: + format: int32 + type: + - integer + - "null" + name: + examples: + - Summer launch + type: string + priceAppliesTo: + enum: + - LICENSE_BASE + - ADDONS + - BOTH + - SELECTED_PRICES + - null + type: + - string + - "null" + priceDiscountType: + enum: + - PERCENTAGE + - FIXED_AMOUNT + - null + type: + - string + - "null" + priceDiscountValue: + description: A percentage in (0, 100], or an integer amount in minor units of currency + examples: + - "30" + type: + - string + - "null" + redemptionRules: + $ref: "#/components/schemas/RedemptionRules" + redemptionsCount: + format: int32 + type: integer + restrictedCustomerSlug: + description: Only this customer's instances can redeem it + type: + - string + - "null" + startsAt: + format: date-time + type: + - string + - "null" + status: + enum: + - DRAFT + - ACTIVE + - EXPIRED + - EXHAUSTED + - ARCHIVED + type: string + updatedAt: + format: date-time + readOnly: true + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - id + - codeHint + - name + - description + - voucherType + - status + - duration + - durationInPeriods + - maxRedemptions + - redemptionsCount + - startsAt + - expiresAt + - priceDiscountType + - priceDiscountValue + - currency + - priceAppliesTo + - applicableLicensePriceIds + - applicableAddonPriceIds + - grants + - flagGrants + - applicableLicenseIds + - applicableAddonIds + - restrictedCustomerSlug + - redemptionRules + - createdAt + - updatedAt + type: object + VoucherCheck: + additionalProperties: false + properties: + code: + examples: + - SUMMER-2026-LAUNCH + maxLength: 64 + minLength: 1 + type: string + instanceSlug: + description: Also run the checks that need the redeeming instance + type: string + licensePriceId: + description: "The flat-fee price a subscription would start on: the licence, period, amount and currency checks read it instead of the instance's live subscription" + type: string + required: + - code + type: object + VoucherCode: + additionalProperties: false + properties: + code: + examples: + - SUMMER-2026-LAUNCH + maxLength: 64 + minLength: 1 + type: string + required: + - code + type: object + VoucherDelivered: + additionalProperties: false + properties: + applicableAddonIds: + description: Only instances holding one of these add-on versions can redeem it; any when empty + items: + type: string + type: array + applicableAddonPriceIds: + items: + type: string + type: array + applicableLicenseIds: + description: Only instances on these licence versions can redeem it; any when empty + items: + type: string + type: array + applicableLicensePriceIds: + items: + type: string + type: array + checkoutSessionId: + description: "With via checkout_session: the session" + format: uuid + type: string + code: + description: The code to redeem. Returned to read:vouchers callers only; never in an event. + examples: + - SUMMER-2026-LAUNCH + type: string + codeHint: + description: The last four characters of the normalized code + examples: + - UNCH + type: string + createdAt: + format: date-time + readOnly: true + type: string + currency: + description: The currency of a FIXED_AMOUNT discount + examples: + - EUR + type: + - string + - "null" + customerSessionId: + description: "With via customer_session: the session" + format: uuid + type: string + description: + type: + - string + - "null" + duration: + description: "PRICE: how many invoices it discounts (one, durationInPeriods, or every one). BOOST and FLAG_GRANT: how long a redemption lasts, in the subscription's billing periods. COMPOSITE: both, each for its part." + enum: + - ONE_TIME + - REPEATING + - FOREVER + type: string + durationInPeriods: + format: int32 + type: + - integer + - "null" + expiresAt: + format: date-time + type: + - string + - "null" + flagGrants: + description: The flag variants a FLAG_GRANT grants + items: + $ref: "#/components/schemas/FlagGrant" + type: array + grants: + description: What an ENTITLEMENT_BOOST changes + items: + $ref: "#/components/schemas/Grant" + type: array + id: + readOnly: true + type: string + maxRedemptions: + format: int32 + type: + - integer + - "null" + name: + examples: + - Summer launch + type: string + priceAppliesTo: + enum: + - LICENSE_BASE + - ADDONS + - BOTH + - SELECTED_PRICES + - null + type: + - string + - "null" + priceDiscountType: + enum: + - PERCENTAGE + - FIXED_AMOUNT + - null + type: + - string + - "null" + priceDiscountValue: + description: A percentage in (0, 100], or an integer amount in minor units of currency + examples: + - "30" + type: + - string + - "null" + redemptionRules: + $ref: "#/components/schemas/RedemptionRules" + redemptionsCount: + format: int32 + type: integer + restrictedCustomerSlug: + description: Only this customer's instances can redeem it + type: + - string + - "null" + startsAt: + format: date-time + type: + - string + - "null" + status: + enum: + - DRAFT + - ACTIVE + - EXPIRED + - EXHAUSTED + - ARCHIVED + type: string + updatedAt: + format: date-time + readOnly: true + type: string + via: + description: "customer_session: written through a kst_ customer session, under the vendor principal that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. Absent otherwise" + enum: + - customer_session + - checkout_session + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - id + - codeHint + - name + - description + - voucherType + - status + - duration + - durationInPeriods + - maxRedemptions + - redemptionsCount + - startsAt + - expiresAt + - priceDiscountType + - priceDiscountValue + - currency + - priceAppliesTo + - applicableLicensePriceIds + - applicableAddonPriceIds + - grants + - flagGrants + - applicableLicenseIds + - applicableAddonIds + - restrictedCustomerSlug + - redemptionRules + - createdAt + - updatedAt + type: object + x-kaiten-payload: "#/components/schemas/Voucher" + VoucherDraft: + additionalProperties: false + properties: + applicableAddonIds: + items: + type: string + type: + - array + - "null" + applicableAddonPriceIds: + description: "With SELECTED_PRICES: the add-on prices it discounts" + items: + type: string + type: + - array + - "null" + applicableLicenseIds: + items: + type: string + type: + - array + - "null" + applicableLicensePriceIds: + description: "With SELECTED_PRICES: the licence prices it discounts" + items: + type: string + type: + - array + - "null" + code: + description: "8 to 64 of [A-Za-z0-9_-]. Generated when absent: 16 characters, 80 random bits. Matched without case or separators." + examples: + - SUMMER-2026-LAUNCH + type: string + currency: + description: Required with FIXED_AMOUNT, refused otherwise + examples: + - EUR + type: string + description: + maxLength: 2000 + type: string + duration: + enum: + - ONE_TIME + - REPEATING + - FOREVER + type: string + durationInPeriods: + description: Required with REPEATING, refused otherwise + format: int32 + type: integer + expiresAt: + format: date-time + type: string + flagGrants: + description: Required on a FLAG_GRANT, optional on a COMPOSITE, one per flag, refused otherwise + items: + $ref: "#/components/schemas/FlagGrant" + type: + - array + - "null" + grants: + description: Required on an ENTITLEMENT_BOOST, optional on a COMPOSITE, refused otherwise + items: + $ref: "#/components/schemas/Grant" + type: + - array + - "null" + maxRedemptions: + format: int32 + minimum: 1 + type: integer + name: + examples: + - Summer launch + maxLength: 200 + minLength: 1 + type: string + priceAppliesTo: + enum: + - LICENSE_BASE + - ADDONS + - BOTH + - SELECTED_PRICES + type: string + priceDiscountType: + enum: + - PERCENTAGE + - FIXED_AMOUNT + type: string + priceDiscountValue: + description: A percentage in (0, 100], or an integer amount in minor units + examples: + - "30" + type: string + redemptionRules: + $ref: "#/components/schemas/RedemptionRules" + restrictedCustomerSlug: + type: string + startsAt: + format: date-time + type: string + voucherType: + enum: + - PRICE + - ENTITLEMENT_BOOST + - FLAG_GRANT + - COMPOSITE + type: string + required: + - name + - voucherType + - duration + type: object + securitySchemes: + bearerAuth: + bearerFormat: JWT + description: "A JWT verified by the proxy in front of this API, or a Kaiten access token (`ksh_...`) — send either as `Authorization: Bearer `. The scopes listed on each operation are checked server-side against current stored user assignments or stored service-token permissions, never JWT claims; `write:` implies `read:`, and `read:*` / `write:*` are wildcards over every module. `x-kaiten-scopes` lists every scope such a token can carry, including those enforced by a service in front of this API rather than by one of its operations." + scheme: bearer + type: http + x-kaiten-scopes: + - read:addons + - read:billing + - read:components + - read:customer_sessions + - read:customers - read:deployment_zones - read:entitlements - read:feature_flags @@ -3196,10 +16551,18 @@ components: - read:metadata_fields - read:notifications - read:organizations + - read:profile + - read:publishable_keys - read:releases - read:tokens + - read:users + - read:voucher_redemptions + - read:vouchers - read:webhooks + - write:addons + - write:billing - write:components + - write:customer_sessions - write:customers - write:deployment_zones - write:entitlements @@ -3209,9 +16572,29 @@ components: - write:metadata_fields - write:notifications - write:organizations + - write:profile + - write:publishable_keys - write:releases - write:tokens + - write:users + - write:voucher_redemptions + - write:vouchers - write:webhooks + checkoutSession: + bearerFormat: ksc_ + description: "An anonymous checkout session (`ksc_...`), returned once by POST /public/checkout-sessions and sent as `Authorization: Bearer ksc_...`. It reads and confirms that one session, at /public/checkout-sessions/{sessionId}, and nothing else; another session's id answers 401. A browser request must come from an origin of the publishable key that started the session." + scheme: bearer + type: http + customerSession: + bearerFormat: kst_ + description: "A customer session (`kst_...`), minted by the vendor's backend with POST /customer-sessions and sent as `Authorization: Bearer kst_...`. It acts for one customer -- and one of its instances, when bound -- on the /public/session routes and nowhere else. A browser request must come from an origin one of the organization's publishable keys allows." + scheme: bearer + type: http + publishableKey: + description: "A Kaiten publishable key (`pk_...`). Not a secret: it ships in web pages, and authorizes reading the organization's public catalogue and nothing else. A browser request must come from one of the key's allowed origins. Never send it as `Authorization`; a request carrying `Authorization` is refused." + in: header + name: X-Kaiten-Publishable-Key + type: apiKey info: contact: email: support@kaiten.sh @@ -3221,34 +16604,13550 @@ info: version: 1.0.0 openapi: 3.1.0 paths: + /addon-families: + get: + description: Every add-on family of the organization with its versions, newest family first. Requires billing to be enabled for the organization. + operationId: listAddonFamilies + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/AddonFamily" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: List add-on families + tags: + - addons + /addon-families/{familySlug}: + get: + description: One add-on family with its versions and its current version. Requires billing to be enabled for the organization. + operationId: getAddonFamily + parameters: + - description: Add-on family slug + in: path + name: familySlug + required: true + schema: + description: Add-on family slug + examples: + - extra-seats + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AddonFamily" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: Get an add-on family + tags: + - addons + patch: + description: "Changes a family's own settings: whether the public catalogue lists it (families are private until made public), and how a mid-period increase of its ADVANCE add-ons is billed. An absent member keeps the stored value. Emits ADDON_UPDATED on each of its versions. Requires billing to be enabled for the organization. 404 .NotFound; 422 .InvalidIncreaseBilling." + operationId: updateAddonFamily + parameters: + - description: Add-on family slug + in: path + name: familySlug + required: true + schema: + description: Add-on family slug + examples: + - extra-seats + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddonFamilyVisibility" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AddonFamily" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Update an add-on family + tags: + - addons + /addons: + get: + description: The organization's add-on versions, newest first. Requires billing to be enabled for the organization. + operationId: listAddons + parameters: + - description: Only versions in this state + explode: false + in: query + name: lifecycleState + schema: + description: Only versions in this state + enum: + - DRAFT + - PUBLISHED + - ARCHIVED + type: string + - description: Only versions of this family + explode: false + in: query + name: familySlug + schema: + description: Only versions of this family + type: string + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageAddon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: List add-on versions + tags: + - addons + post: + description: "Creates an add-on version: the first of a new family, or the next version of the family familySlug (or familyId) names. Emits ADDON_CREATED. Requires billing to be enabled for the organization." + operationId: createAddon + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewAddon" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Addon" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Create an add-on version + tags: + - addons + /addons/{addonSlug}: + delete: + description: Deletes a version that was never attached and grants nothing (409 DeleteAddon.InUseConflict otherwise); its family goes with its last version. Emits ADDON_DELETED. Requires billing to be enabled for the organization. + operationId: deleteAddon + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Delete an add-on version + tags: + - addons + get: + description: One add-on version. Requires billing to be enabled for the organization. + operationId: getAddon + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Addon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: Get an add-on version + tags: + - addons + put: + description: Replaces a version's name, description, version name, default flag and maximum quantity. Emits ADDON_UPDATED. Requires billing to be enabled for the organization. + operationId: updateAddon + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddonChanges" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Addon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Update an add-on version + tags: + - addons + /addons/{addonSlug}/archive: + post: + description: "Withdraws a PUBLISHED version from sale: instances holding it keep it, none can attach it again (409 ArchiveAddon.NotPublished otherwise). Emits ADDON_ARCHIVED. Requires billing to be enabled for the organization." + operationId: archiveAddon + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Addon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Archive an add-on version + tags: + - addons + /addons/{addonSlug}/compatible-license-families: + get: + description: "The licence families an instance must be on to attach this add-on version. None: it cannot be attached. Requires billing to be enabled for the organization." + operationId: listAddonCompatibility + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CompatibleLicenseFamilies" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: List the licence families an add-on fits + tags: + - addons + /addons/{addonSlug}/compatible-license-families/{familySlug}: + delete: + description: Instances already holding the add-on keep it; no new attachment from the family. Idempotent. Emits ADDON_UPDATED. Requires billing to be enabled for the organization. + operationId: removeAddonCompatibility + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: License family slug + in: path + name: familySlug + required: true + schema: + description: License family slug + examples: + - pro + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Stop an add-on fitting a licence family + tags: + - addons + put: + description: Lets instances on any version of the licence family attach this add-on version. Idempotent. Emits ADDON_UPDATED. Requires billing to be enabled for the organization. + operationId: setAddonCompatibility + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: License family slug + in: path + name: familySlug + required: true + schema: + description: License family slug + examples: + - pro + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Make an add-on fit a licence family + tags: + - addons + /addons/{addonSlug}/entitlements: + get: + description: What one add-on version grants per unit of quantity. Requires billing to be enabled for the organization. + operationId: listAddonEntitlements + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/AddonEntitlement" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: List an add-on version's grants + tags: + - addons + post: + description: Grants an entitlement per unit of an add-on version. Refused while an instance with a live subscription holds the version (409 AssignAddonEntitlement.BillingActive). Emits ADDON_ENTITLEMENT_ASSIGNED. Requires billing to be enabled for the organization. + operationId: assignAddonEntitlement + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewAddonGrant" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/AddonEntitlement" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Grant an entitlement through an add-on + tags: + - addons + /addons/{addonSlug}/entitlements/{entitlementSlug}: + delete: + description: Removes a grant. Refused while an instance with a live subscription holds the version (409 UnassignAddonEntitlement.BillingActive) and while an ACTIVE price of the version meters it (409 UnassignAddonEntitlement.MeteredByPrice). Emits ADDON_ENTITLEMENT_UNASSIGNED. Requires billing to be enabled for the organization. + operationId: unassignAddonEntitlement + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - seats + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Remove an add-on grant + tags: + - addons + get: + description: One grant of an add-on version. Requires billing to be enabled for the organization. + operationId: getAddonEntitlement + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - seats + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AddonEntitlement" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: Get an add-on grant + tags: + - addons + put: + description: Replaces a grant's value, override behaviour and overage. Refused while an instance with a live subscription holds the version (409 UpdateAddonEntitlement.BillingActive). Emits ADDON_ENTITLEMENT_UPDATED. Requires billing to be enabled for the organization. + operationId: updateAddonEntitlement + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - seats + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddonGrant" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AddonEntitlement" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Update an add-on grant + tags: + - addons + /addons/{addonSlug}/prices: + get: + description: The prices of one add-on version, in display order. Requires billing to be enabled for the organization. + operationId: listAddonPrices + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: Only prices in this status; both when omitted + explode: false + in: query + name: status + schema: + description: Only prices in this status; both when omitted + enum: + - ACTIVE + - DEPRECATED + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Price" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:addons + summary: List an add-on version's prices + tags: + - addons + post: + description: "Adds a price to an add-on version, under the rules of a licence price. A price cannot change once created: create another and deprecate this one. Refused while an instance with a live subscription holds the version (409 CreateAddonPrice.BillingActive). Emits ADDON_PRICE_CREATED. Requires billing to be enabled for the organization." + operationId: createAddonPrice + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewAddonPrice" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Price" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Create an add-on price + tags: + - addons + /addons/{addonSlug}/prices/{priceId}/deprecate: + post: + description: "Stops offering a price. A default price cannot be deprecated (409 DeprecateAddonPrice.IsDefault): it is what bills every instance holding the version; retire it through a new version. Emits ADDON_PRICE_DEPRECATED. Requires billing to be enabled for the organization." + operationId: deprecateAddonPrice + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + - description: Price id + in: path + name: priceId + required: true + schema: + description: Price id + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Price" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Deprecate an add-on price + tags: + - addons + /addons/{addonSlug}/publish: + post: + description: Puts a DRAFT version on sale (409 PublishAddon.NotADraft otherwise). Emits ADDON_PUBLISHED. Requires billing to be enabled for the organization. + operationId: publishAddon + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Addon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Publish an add-on version + tags: + - addons + /addons/{addonSlug}/unarchive: + post: + description: Puts an ARCHIVED version back on sale (409 UnarchiveAddon.NotArchived otherwise). Emits ADDON_UNARCHIVED. Requires billing to be enabled for the organization. + operationId: unarchiveAddon + parameters: + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Addon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:addons + summary: Unarchive an add-on version + tags: + - addons + /authorization: + get: + description: Returns current stored grants for an active human or service-token caller, not JWT permissions. No permission is required. Credentials and token identifiers are never returned. Responses must not be cached. + operationId: GetAuthorization + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Authorization" + description: OK + headers: + Cache-Control: + schema: + type: string + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: [] + summary: Read current caller authorization + tags: + - identity + /billing/capabilities: + get: + description: Whether billing is enabled for the organization, and if not why; who can collect its invoices; and which parts of billing this release ships, so a client shows only what works. Answers even when billing is disabled. + operationId: getBillingCapabilities + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BillingCapabilities" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get billing capabilities + tags: + - billing + /billing/close-periods: + post: + description: "Closes the subscriptions whose period has ended now, rather than at the period-close job's next pass: each one's RENEWAL invoice is composed and issued (or held, when its usage journal fails a check) and its period advances. A subscription several periods behind closes one period at a time until it is current. Only what is due closes, at most the configured batch size per call. Requires billing to be enabled for the organization." + operationId: closeBillingPeriods + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ClosePeriodsScope" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ClosePeriodsReport" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Close due billing periods + tags: + - billing + /billing/handoff: + get: + description: "The invoices waiting for the organization's accounting system (PENDING), or already acknowledged by it, oldest issue first. Reading never leases: claim to take invoices. Requires billing to be enabled for the organization." + operationId: listHandoff + parameters: + - description: PENDING (the default) or ACKNOWLEDGED; .InvalidStatus otherwise + explode: false + in: query + name: status + schema: + description: PENDING (the default) or ACKNOWLEDGED; .InvalidStatus otherwise + enum: + - PENDING + - ACKNOWLEDGED + type: string + - description: Opaque cursor from the previous page's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque cursor from the previous page's nextCursor + type: string + - description: Page size, 50 by default, 200 at most + explode: false + in: query + name: limit + schema: + description: Page size, 50 by default, 200 at most + format: int32 + maximum: 200 + minimum: 0 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageQueuedInvoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: List the handoff queue + tags: + - billing + /billing/handoff/claim: + post: + description: "Leases the oldest invoices waiting for the organization's accounting system, under one lease. Two consumers claiming at once get different invoices. An invoice not acknowledged before its lease expires is claimed again: the queue delivers at least once, so a consumer deduplicates on the invoice id. A claim is not idempotent: a lost response leaves its invoices leased until leasedUntil. Requires billing to be enabled for the organization." + operationId: claimHandoff + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/HandoffClaimSize" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/HandoffClaim" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Claim invoices from the handoff queue + tags: + - billing + /billing/handoff/{invoiceId}/ack: + post: + description: "Records that the organization's accounting system booked an invoice from the handoff queue. Safe to repeat: the same acknowledgement again, or one that adds a reference the invoice did not have, answers 200. Another reference than the one it was acknowledged under is refused (AckHandoff.ReferenceMismatch), which a consumer can take as already handed off. Requires billing to be enabled for the organization." + operationId: ackHandoff + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/HandoffBooking" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Acknowledge a handed-off invoice + tags: + - billing + /billing/health: + get: + description: "What the organization's billing needs attention for, computed when read: periods waiting for their close, held invoices, invoices a payment provider keeps refusing, reconciliation mismatches, each provider's sync, overdue invoices and the handoff backlog. Requires billing to be enabled for the organization." + operationId: getBillingHealth + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BillingHealth" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get the billing health + tags: + - billing + /billing/settings: + get: + description: "The organization's billing defaults: the collection method and payment terms a subscription gets when it names none of its own. An organization that never wrote them has the defaults (SEND_INVOICE, 30 days)." + operationId: getBillingSettings + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BillingSettings" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get billing settings + tags: + - billing + put: + description: Replaces the organization's billing defaults. A subscription that names its own terms keeps them; the others take these at their next invoice. + operationId: updateBillingSettings + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/BillingSettings" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BillingSettings" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Update billing settings + tags: + - billing + /billing/sync: + post: + description: "Runs one sync pass now for each payment provider the organization has connected: payments, voids and finalizations done in the provider are mirrored, as the periodic pass does. Refused when none is connected (409 SyncProvider.NotConnected). Requires billing to be enabled for the organization." + operationId: syncBillingProvider + responses: + "202": + content: + application/json: + schema: + $ref: "#/components/schemas/SyncReport" + description: Accepted + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Sync with the payment providers now + tags: + - billing + /checkout-sessions: + get: + description: "The organization's anonymous checkouts, newest first: how a vendor that missed or receives no checkout_session events links a buyer to its own account. Never a ksc_ or a voucher code. Requires billing to be enabled for the organization. 400 CheckoutSessions.InvalidCursor." + operationId: listCheckoutSessions + parameters: + - description: Repeatable; a session OPEN past expiresAt is ABANDONED + explode: true + in: query + name: status + schema: + description: Repeatable; a session OPEN past expiresAt is ABANDONED + items: + enum: + - OPEN + - AWAITING_PAYMENT + - COMPLETED + - ABANDONED + - EXPIRED + type: string + type: + - array + - "null" + - description: The visitor's e-mail, in any letter case + explode: false + in: query + name: email + schema: + description: The visitor's e-mail, in any letter case + type: string + - description: The vendor's own reference, exactly + explode: false + in: query + name: clientReferenceId + schema: + description: The vendor's own reference, exactly + type: string + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageCheckoutSessionAdmin" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: List checkout sessions + tags: + - checkout-sessions + /checkout-sessions/{sessionId}: + get: + description: One anonymous checkout, as the vendor reads it. Never a ksc_ or a voucher code. Requires billing to be enabled for the organization. 404 GetCheckoutSessionAdmin.NotFound, also for a session deleted 90 days after it closed. + operationId: getCheckoutSessionAdmin + parameters: + - in: path + name: sessionId + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutSessionAdmin" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get a checkout session + tags: + - checkout-sessions /components: get: - description: Returns a cursor-paginated page of components for the current organization. - operationId: list-components + description: Returns a cursor-paginated page of components for the current organization. + operationId: list-components + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageComponent" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:components + summary: List components + tags: + - components + post: + description: Create a new standalone component. Optionally derive it from a previous component version. + operationId: create-component + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Component" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Component" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:components + summary: Create a new component + tags: + - components + /components/{componentSlug}: + delete: + description: Delete a component by slug if it is not linked to any release. + operationId: delete-component + parameters: + - description: Component slug + in: path + name: componentSlug + required: true + schema: + description: Component slug + examples: + - api-gateway-v1-2-3 + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:components + summary: Delete a component + tags: + - components + get: + description: Returns a single component by its slug. + operationId: get-component + parameters: + - description: Component slug + in: path + name: componentSlug + required: true + schema: + description: Component slug + examples: + - api-gateway-v1-2-3 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Component" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:components + summary: Get a component by slug + tags: + - components + put: + description: Update a component in place when unlinked, or create a new version when it is already linked to releases. + operationId: update-component + parameters: + - description: Component slug + in: path + name: componentSlug + required: true + schema: + description: Component slug + examples: + - api-gateway-v1-2-3 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Component" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Component" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:components + summary: Update a component + tags: + - components + /connectors: + get: + description: Returns every registered connector with its version and settings schema, ordered by name. The registry is platform-wide rather than per-organization and holds one row per connector, so the response is not paginated. + operationId: list-connectors + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Connector" + type: + - array + - "null" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:organizations + summary: List registered connectors + tags: + - connectors + /connectors/{connectorName}: + get: + description: Retrieves a registered connector resource including version and settings schema + operationId: get-connector + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Connector" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:organizations + summary: Get connector registration + tags: + - connectors + /connectors/{connectorName}/activation: + delete: + description: "Deactivates a connector for the authenticated organization. Idempotent, and never refused for an unregistered or unlicensed connector: an organization must always be able to stop using something." + operationId: deactivate-connector + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:organizations + summary: Deactivate connector + tags: + - connectors + put: + description: "Activates a registered connector for the authenticated organization. Idempotent: activating an already-active connector succeeds without changing when it was first activated. Refused when the organization's license does not include the connector." + operationId: activate-connector + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectorActivation" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:organizations + summary: Activate connector + tags: + - connectors + /connectors/{connectorName}/settings: + delete: + description: Delete connector settings for the authenticated organization + operationId: delete-connector-settings + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:organizations + summary: Delete connector settings + tags: + - connectors + get: + description: Get settings for a connector scoped to the authenticated organization. Write-only fields (flagged `writeOnly` in the connector settings schema) are returned redacted as `***`. + operationId: get-connector-settings + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectorSettings" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:organizations + summary: Get connector settings + tags: + - connectors + put: + description: Create or update connector settings for the authenticated organization. Write-only fields (flagged `writeOnly` in the connector settings schema) may be omitted or sent as `***` to keep the stored value; they are returned redacted. + operationId: update-connector-settings + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectorSettings" + description: Connector settings payload to upsert + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectorSettings" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:organizations + summary: Upsert connector settings + tags: + - connectors + /connectors/{connectorName}/settings/schema: + get: + description: Get the JSON schema used to validate connector settings payloads + operationId: get-connector-settings-schema + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectorSettingsSchema" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:organizations + summary: Get connector settings schema + tags: + - connectors + /connectors/{connectorName}/state: + get: + description: "Returns whether the connector is registered on this installation, whether the authenticated organization's license includes it, and whether the organization has activated it. Answers 200 with `available: false` for a connector this installation does not have." + operationId: get-connector-state + parameters: + - description: Stable connector name + in: path + name: connectorName + required: true + schema: + description: Stable connector name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectorState" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:organizations + summary: Get connector state for this organization + tags: + - connectors + /customer-sessions: + post: + description: "Mints a kst_ session for one of the organization's customers, optionally bound to one of its instances, for that customer's browser to call the /api/public/session routes: checkout, invoices. Call it from your backend, where the customer is signed in, and hand the token to the page; mint a new one before it expires. What the session writes is attributed to the caller. Limited to 50 sessions a second per organization. Requires billing to be enabled for the organization." + operationId: createCustomerSession + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerSessionDraft" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CreatedCustomerSession" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:customer_sessions + summary: Create a customer session + tags: + - customer-sessions + /customer-sessions/{sessionId}/revoke: + post: + description: "The session stops authenticating on the next request: on sign-out, or when the customer's access changes. Idempotent. Requires billing to be enabled for the organization." + operationId: revokeCustomerSession + parameters: + - description: Customer session id + in: path + name: sessionId + required: true + schema: + description: Customer session id + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:customer_sessions + summary: Revoke a customer session + tags: + - customer-sessions + /customers: + get: + description: Returns a cursor-paginated page of customers for the current organization. + operationId: list-customers + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageCustomer" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:customers + summary: List customers + tags: + - customers + post: + description: Create a new customer with the provided details. + operationId: create-customer + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Customer" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Customer" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:customers + summary: Create a new customer + tags: + - customers + /customers/{customerSlug}: + delete: + description: Delete a customer by their slug. + operationId: delete-customer + parameters: + - in: path + name: customerSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:customers + summary: Delete a customer + tags: + - customers + get: + description: Returns a single customer by their slug. + operationId: get-customer + parameters: + - in: path + name: customerSlug + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Customer" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:customers + summary: Get a customer by slug + tags: + - customers + put: + description: Update a customer's details. Slug and integrations are not renameable/settable through this endpoint -- omit them, or send the values already on record. + operationId: update-customer + parameters: + - in: path + name: customerSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Customer" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:customers + summary: Update a customer + tags: + - customers + /customers/{customerSlug}/billing: + get: + description: "The customer's billing e-mail and its side in each payment provider: its id there and the labels of its default payment method (never card data). 404 GetCustomerBilling.CustomerNotFound. Requires billing to be enabled for the organization." + operationId: getCustomerBilling + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerBilling" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get a customer's billing + tags: + - billing + /customers/{customerSlug}/billing/payment-method: + delete: + description: Removes the customer's payment method from the payment provider. 409 .InUseByAutomaticCollection while a live subscription of the customer is charged automatically, .NoPaymentMethod; 503 .ProviderUnavailable. Requires billing to be enabled for the organization. + operationId: detachPaymentMethod + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Detach a customer's payment method + tags: + - billing + /customers/{customerSlug}/billing/payment-method-session: + post: + description: Ensures the customer in the payment provider, then opens the provider's hosted page where the customer saves a payment method; Kaiten never sees card data. When the customer comes back to returnUrl, complete the session server-side. 404 .CustomerNotFound; 422 .ProviderNotConnected, .InvalidReturnUrl, .CurrencyRequired; 503 .ProviderUnavailable. Requires billing to be enabled for the organization. + operationId: createPaymentMethodSession + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewPaymentMethodSession" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PaymentMethodSession" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Open a page saving a customer's payment method + tags: + - billing + /customers/{customerSlug}/billing/payment-method-session/{sessionId}/complete: + post: + description: Checks the setup session with the payment provider (the redirect is never trusted) and makes the payment method saved there the one the customer's invoices are charged to. Idempotent. 404 .SessionNotFound (unknown, or another customer's); 409 .SessionNotComplete; 503 .ProviderUnavailable. Requires billing to be enabled for the organization. + operationId: completePaymentMethodSession + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + type: string + - description: The setup session id the customer came back with + in: path + name: sessionId + required: true + schema: + description: The setup session id the customer came back with + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CompletedPaymentMethodSession" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Complete a payment-method session + tags: + - billing + /customers/{customerSlug}/billing/portal-session: + post: + description: Opens the payment provider's hosted portal for the customer, where it manages payment methods and invoices; changes reach Kaiten through sync. 422 .ProviderNotConnected, .CustomerNotOnProvider, .InvalidReturnUrl; 503 .ProviderUnavailable. Requires billing to be enabled for the organization. + operationId: createPortalSession + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewPortalSession" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PortalSession" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Open a customer's billing portal + tags: + - billing + /customers/{customerSlug}/integrations/{integrationName}: + delete: + description: Delete a single integration for a customer + operationId: delete-customer-integration + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + examples: + - acme-corp + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:customers + summary: Delete a customer integration + tags: + - customers + get: + description: Retrieve a single integration for a customer + operationId: get-customer-integration + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + examples: + - acme-corp + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegration" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:customers + summary: Get a customer integration + tags: + - customers + post: + description: Create a single integration for a customer + operationId: create-customer-integration + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + examples: + - acme-corp + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegration" + description: Integration payload + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegration" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:customers + summary: Create a customer integration + tags: + - customers + put: + description: Update a single integration for a customer + operationId: update-customer-integration + parameters: + - description: Customer slug + in: path + name: customerSlug + required: true + schema: + description: Customer slug + examples: + - acme-corp + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegration" + description: Integration payload + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegration" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:customers + summary: Update a customer integration + tags: + - customers + /deployment-zones: + get: + description: Returns a cursor-paginated page of deployment zones for the current organization. A deployment zone is one place a customer runs a release -- a target they name and classify themselves, holding the release currently on it. It is not an installation of Kaiten; that sense of the term belongs to operations and is not reachable through this API. + operationId: list-deployment-zones + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageDeploymentZone" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:deployment_zones + summary: List deployment zones + tags: + - deploymentZones + post: + description: Create a new deployment zone with the provided details. Supplying releaseId records the zone's first deployment. A deployment zone is one place a customer runs a release -- a target they name and classify themselves, holding the release currently on it. It is not an installation of Kaiten; that sense of the term belongs to operations and is not reachable through this API. + operationId: create-deployment-zone + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DeploymentZone" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/DeploymentZone" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:deployment_zones + summary: Create a new deployment zone + tags: + - deploymentZones + /deployment-zones/{deploymentZoneSlug}: + delete: + description: Delete a deployment zone by their slug. + operationId: delete-deployment-zone + parameters: + - in: path + name: deploymentZoneSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:deployment_zones + summary: Delete a deployment zone + tags: + - deploymentZones + get: + description: Returns a single deployment zone by their slug. + operationId: get-deployment-zone-by-slug + parameters: + - in: path + name: deploymentZoneSlug + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/DeploymentZone" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:deployment_zones + summary: Get a deployment zone by slug + tags: + - deploymentZones + put: + description: "Update a deployment zone's details. Changing releaseId records a deployment: the zone's current release becomes the new one and the change is appended to its deployment history, including when the new release is one the zone already ran (a rollback). Omitting releaseId, or echoing back the current one, leaves the history untouched." + operationId: update-deploymentZone + parameters: + - in: path + name: deploymentZoneSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DeploymentZone" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:deployment_zones + summary: Update a deployment zone + tags: + - deploymentZones + /entitlement-groups: + get: + description: Returns a cursor-paginated page of entitlement groups for the current organization. + operationId: list-entitlement-groups + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageEntitlementGroup" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:entitlements + summary: List all entitlement groups + tags: + - entitlement-groups + post: + description: Create a new entitlement group to aggregate entitlements + operationId: create-entitlement-group + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EntitlementGroup" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/EntitlementGroup" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:entitlements + summary: Create a new entitlement group + tags: + - entitlement-groups + /entitlement-groups/{entitlementGroupSlug}: + delete: + description: Delete an entitlement group by its slug. This does not delete the member entitlements. + operationId: delete-entitlement-group + parameters: + - in: path + name: entitlementGroupSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:entitlements + summary: Delete an entitlement group + tags: + - entitlement-groups + get: + description: Retrieve details of a specific entitlement group by its slug + operationId: get-entitlement-group + parameters: + - in: path + name: entitlementGroupSlug + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EntitlementGroup" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:entitlements + summary: Get entitlement group by slug + tags: + - entitlement-groups + put: + description: Update an existing entitlement group with the provided details + operationId: update-entitlement-group + parameters: + - in: path + name: entitlementGroupSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EntitlementGroup" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:entitlements + summary: Update an entitlement group + tags: + - entitlement-groups + /entitlement-groups/{entitlementGroupSlug}/entitlements: + post: + description: Add an existing entitlement as a member of an entitlement group + operationId: add-entitlement-to-group + parameters: + - in: path + name: entitlementGroupSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Command" + required: true + responses: + "201": + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:entitlements + summary: Add an entitlement to a group + tags: + - entitlement-groups + /entitlement-groups/{entitlementGroupSlug}/entitlements/{entitlementSlug}: + delete: + description: Remove an entitlement from an entitlement group. The entitlement itself is not deleted. + operationId: remove-entitlement-from-group + parameters: + - in: path + name: entitlementGroupSlug + required: true + schema: + type: string + - in: path + name: entitlementSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:entitlements + summary: Remove an entitlement from a group + tags: + - entitlement-groups + /entitlement-groups/{entitlementGroupSlug}/usage: + get: + description: Retrieve the aggregated usage metrics of all member entitlements for a given instance + operationId: get-entitlement-group-usage + parameters: + - in: path + name: entitlementGroupSlug + required: true + schema: + type: string + - description: Instance slug to get usage for + explode: false + in: query + name: instance + required: true + schema: + description: Instance slug to get usage for + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/EntitlementGroupUsage" + type: + - array + - "null" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:entitlements + summary: Get aggregated usage for an entitlement group + tags: + - entitlement-groups + /entitlements: + get: + description: Returns a cursor-paginated page of entitlements for the current organization. + operationId: list-entitlements + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageEntitlement" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:entitlements + summary: List all entitlements + tags: + - entitlements + post: + description: Create a new entitlement with the provided details + operationId: create-entitlement + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Entitlement" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Entitlement" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:entitlements + summary: Create a new entitlement + tags: + - entitlements + /entitlements/{entitlementSlug}: + delete: + description: "Delete an entitlement by its slug. Its usage is deleted with it. An entitlement that a license or an add-on still grants, a license or an add-on price still meters, or a voucher boost still grants is refused with a 409 that counts each: remove the grants first. A price and a voucher boost are never deleted, so the entitlement they hold stays." + operationId: delete-entitlement + parameters: + - in: path + name: entitlementSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:entitlements + summary: Delete an entitlement + tags: + - entitlements + get: + description: Retrieve details of a specific entitlement by its slug + operationId: get-entitlement + parameters: + - in: path + name: entitlementSlug + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Entitlement" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:entitlements + summary: Get entitlement by slug + tags: + - entitlements + put: + description: Update an existing entitlement with the provided details + operationId: update-entitlement + parameters: + - in: path + name: entitlementSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Entitlement" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:entitlements + summary: Update an entitlement + tags: + - entitlements + /feature-flags: + get: + description: Returns a cursor-paginated page of feature flags for the given organization. + operationId: get-feature-flags + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageFeatureFlag" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:feature_flags + summary: Get a list of feature flags + tags: + - featureflags + post: + description: Create a new featureflag with the provided details + operationId: create-feature-flag + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/FeatureFlag" + description: Feature flag details + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/FeatureFlag" + description: Created feature flag + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:feature_flags + summary: Create a new featureflag + tags: + - featureflags + /feature-flags/targeting/context: + get: + description: "Returns the identifiers a targeting rule may read, with their types and descriptions, including the organization's own entitlement slugs. Intended for an editor: it is what autocomplete and hover documentation are built from. The set is not closed — a rule may also target on attributes the caller supplies in its own evaluation context." + operationId: get-targeting-context + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/TargetingContext" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:feature_flags + summary: Get the targeting context schema + tags: + - featureflags + /feature-flags/targeting/lint: + post: + description: "Runs the same check create and update run before accepting a targeting rule, and reports what it found without writing anything. A rule that references a fact or an entitlement that does not exist does not fail loudly at evaluation — it simply never matches — so this is what lets an editor say so while the rule is being written. A rejected rule is a 200 carrying `valid: false`, not a client error." + operationId: lint-targeting-rule + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TargetingRuleDraft" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/TargetingRuleVerdict" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:feature_flags + summary: Check a targeting rule + tags: + - featureflags + /feature-flags/targeting/test: + post: + description: "Runs a rule the way an evaluation would, without a flag and without writing anything: the context is enriched from the same sources (license, entitlements, instance, customer, deployment zone), anything the caller forged under __kaiten is discarded the same way, and the same engine answers. The response carries the enriched facts so an author can see exactly what the rule read — which is usually the answer to why it did or did not match. Nothing is metered or audited: this is authoring, not serving." + operationId: test-targeting-rule + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TargetingRuleTrial" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/TargetingRuleRehearsal" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:feature_flags + summary: Rehearse a targeting rule against a context + tags: + - featureflags + /feature-flags/{featureFlagSlug}: + delete: + description: Delete a featureflag with the provided details. A flag a voucher grants a variant of cannot be deleted (409 DeleteFeatureFlag.InUseConflict); it can be disabled. + operationId: delete-feature-flag + parameters: + - in: path + name: featureFlagSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:feature_flags + summary: Delete a featureflag + tags: + - featureflags + get: + description: Get a feature flag with the provided slug + operationId: get-feature-flag + parameters: + - in: path + name: featureFlagSlug + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/FeatureFlag" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:feature_flags + summary: Get a feature flag by slug + tags: + - featureflags + put: + description: Update a featureflag with the provided details. Sending a slug renames the flag; omitting it keeps the current one. + operationId: update-feature-flag + parameters: + - in: path + name: featureFlagSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/FeatureFlag" + description: Feature flag details + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:feature_flags + summary: Update a featureflag + tags: + - featureflags + /instances: + get: + description: Returns a cursor-paginated page of instances for the current organization. + operationId: getInstances + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageInstance" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: List all instances + tags: + - instances + post: + description: Create a new instance with the provided details + operationId: createInstance + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Create a new instance + tags: + - instances + /instances/{instanceSlug}: + delete: + description: Delete an instance with the provided slug + operationId: deleteInstance + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:instances + summary: Delete an instance + tags: + - instances + get: + description: Retrieve an instance by their unique slug + operationId: getInstance + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: Get an instance by slug + tags: + - instances + patch: + description: Partially update an instance. Currently supports the operational status and the commercial lifecycle stage. + operationId: patchInstance + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PatchInstanceBody" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:instances + summary: Patch an instance + tags: + - instances + put: + description: Update an instance with the provided details + operationId: updateInstance + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:instances + summary: Update an instance + tags: + - instances + /instances/{instanceSlug}/addons: + get: + description: The add-ons an instance holds, in attachment order, each with the prices its subscription bills. Requires billing to be enabled for the organization. + operationId: listInstanceAddons + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: Also list the add-ons removed from the instance + explode: false + in: query + name: includeRemoved + schema: + description: Also list the add-ons removed from the instance + type: boolean + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/InstanceAddon" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:instances + summary: List an instance's add-ons + tags: + - instances + post: + description: "Gives an instance an add-on version and a quantity. The entitlements apply at once; a subscription bills the quantity held at each boundary, without proration. One version per family: moving to another version is detach then attach. Emits INSTANCE_ADDON_ADDED. Requires billing to be enabled for the organization." + operationId: attachInstanceAddon + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewInstanceAddon" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceAddon" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Attach an add-on to an instance + tags: + - instances + /instances/{instanceSlug}/addons/{addonSlug}: + delete: + description: Removes an add-on from an instance, at once, a removal the customer scheduled included. The entitlements drop at once; nothing is refunded. The attachment stays readable with includeRemoved. Emits INSTANCE_ADDON_REMOVED. Requires billing to be enabled for the organization. + operationId: detachInstanceAddon + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Remove an add-on from an instance + tags: + - instances + patch: + description: Changes how many units of an add-on an instance holds, at once, a decrease included. The entitlements follow at once; the next invoice bills the new quantity. It cancels a decrease the customer scheduled (pendingQuantity), even when the quantity is the one held. Emits INSTANCE_ADDON_QUANTITY_CHANGED. Requires billing to be enabled for the organization. + operationId: setInstanceAddonQuantity + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: Add-on version slug + in: path + name: addonSlug + required: true + schema: + description: Add-on version slug + examples: + - extra-seats-v1 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddonQuantity" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceAddon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Change an instance add-on's quantity + tags: + - instances + /instances/{instanceSlug}/audit-trails: + get: + description: "Returns audit trail events for a given instance, optionally filtered by event name and time range. Results are cursor-paginated (default limit: 50, max: 200) — pass the previous response's nextCursor as the cursor parameter to fetch the next page." + operationId: getAuditTrails + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Filter by exact event name (e.g. ENTITLEMENT_VALUE_GET) + explode: false + in: query + name: event_name + schema: + description: Filter by exact event name (e.g. ENTITLEMENT_VALUE_GET) + type: string + - description: RFC3339 timestamp — include entries with timestamp ≥ this value + explode: false + in: query + name: after + schema: + description: RFC3339 timestamp — include entries with timestamp ≥ this value + type: string + - description: RFC3339 timestamp — include entries with timestamp ≤ this value + explode: false + in: query + name: before + schema: + description: RFC3339 timestamp — include entries with timestamp ≤ this value + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageAuditTrail" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: List audit trail entries for an instance + tags: + - instances + /instances/{instanceSlug}/billing: + get: + description: The instance's subscription, live or CANCELED; 404 when it was never subscribed. Requires billing to be enabled for the organization. + operationId: getInstanceBilling + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceBilling" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get an instance's subscription + tags: + - billing + patch: + description: Changes the subscription's payment provider, collection method and payment terms from its next invoice on; null restores the organization's default. Invoices already composed keep their own, and keep routing to the provider that issued them. Moving to a provider that pushes invoices registers the customer with it first (503 UpdateInstanceBilling.ProviderUnavailable when it cannot be reached). A provider change emits INSTANCE_BILLING_PROVIDER_CHANGED. Requires billing to be enabled for the organization. + operationId: updateInstanceBilling + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SubscriptionTerms" + application/octet-stream: + schema: + contentMediaType: application/octet-stream + format: binary + type: string + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceBilling" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Change a subscription's terms or provider + tags: + - billing + post: + description: Starts billing an instance on a FLAT_FEE price of its licence version. A base price that bills in advance issues the first period's invoice (ACTIVATION) at once; charged automatically, it is pushed and charged before the answer when the provider answers within 20 seconds. Each period then closes into a RENEWAL. Add-ons and a voucher can be started with it, in the same transaction. A CANCELED subscription is subscribed again. While the subscription is live the instance's customer and licence cannot change. Requires billing to be enabled for the organization. + operationId: subscribeInstance + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewSubscription" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/StartedSubscription" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Subscribe an instance + tags: + - billing + /instances/{instanceSlug}/billing/cancel: + post: + description: "Cancels an instance's subscription: a trial at once with no invoice; otherwise at the period's end (repeating it changes nothing), or immediately with a FINAL invoice. Cancellation changes billing only: entitlements and licence dates stay the vendor's to change. Requires billing to be enabled for the organization." + operationId: cancelSubscription + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SubscriptionCancellation" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CanceledSubscription" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Cancel a subscription + tags: + - billing + /instances/{instanceSlug}/billing/reactivate: + post: + description: Reverts a cancellation scheduled for the period end, before it. A canceled subscription is subscribed again instead. Requires billing to be enabled for the organization. + operationId: reactivateSubscription + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceBilling" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Reactivate a subscription + tags: + - billing + /instances/{instanceSlug}/billing/scheduled-change: + delete: + description: Drops the plan change scheduled for the next boundary. Requires billing to be enabled for the organization. + operationId: cancelPlanChange + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceBilling" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Cancel a scheduled plan change + tags: + - billing + put: + description: Schedules a move to another FLAT_FEE price, of any PUBLISHED licence version in the subscription's currency, for the next boundary. That boundary's RENEWAL bills the old plan's arrears and the new plan's advance, and moves the instance to the new version; there is no proration. Scheduling another target replaces the change, the same one changes nothing. A scheduled target's price cannot be deprecated, nor its version archived. Requires billing to be enabled for the organization. + operationId: schedulePlanChange + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PlanChangeTarget" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceBilling" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Schedule a plan change + tags: + - billing + /instances/{instanceSlug}/billing/upcoming-invoice: + get: + description: "The invoice the subscription's next boundary will issue, composed from its usage so far, without writing anything: lines carry the period they will bill, quantities what was measured until now. wouldHold lists the meters whose usage journal fails a check and would hold the invoice. Requires billing to be enabled for the organization." + operationId: getUpcomingInvoice + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InvoicePreview" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Preview an instance's upcoming invoice + tags: + - billing + /instances/{instanceSlug}/entitlement-exceptions: + get: + description: The instance's exceptions, latest start first, each with its status at the read. Works whether or not billing is enabled. + operationId: listEntitlementExceptions + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: "active: neither revoked nor ended, scheduled ones included; all: every exception of the instance" + explode: false + in: query + name: filter + schema: + default: active + description: "active: neither revoked nor ended, scheduled ones included; all: every exception of the instance" + enum: + - active + - all + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/EntitlementException" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:instances + summary: List an instance's entitlement exceptions + tags: + - instances + post: + description: "The vendor's explicit, dated decision on one entitlement of one instance, without a code: the last layer of the effective entitlements, after the licence, the add-ons and the boosts. It wins over them and may lower the value. One unrevoked exception per instance and entitlement at any instant. Emits INSTANCE_ENTITLEMENT_EXCEPTION_CREATED. Works whether or not billing is enabled." + operationId: createEntitlementException + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewEntitlementException" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/EntitlementException" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Set an exception on an instance's entitlement + tags: + - instances + /instances/{instanceSlug}/entitlement-exceptions/{exceptionId}: + delete: + description: "Revokes an exception and keeps it, readable with filter=all: a scheduled one never starts, an active one ends at once. Revoking one already revoked or ended changes nothing. Emits INSTANCE_ENTITLEMENT_EXCEPTION_REVOKED. Works whether or not billing is enabled." + operationId: revokeEntitlementException + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: Exception id + in: path + name: exceptionId + required: true + schema: + description: Exception id + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Revoke an entitlement exception + tags: + - instances + /instances/{instanceSlug}/entitlements/usage: + get: + description: Retrieve a list of all entitlements usage metrics for a specific instance + operationId: getEntitlementsUsageMetrics + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/EntitlementUsage" + type: + - array + - "null" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: List all entitlements usage metrics + tags: + - instances + /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage: + get: + description: Retrieve the usage metrics for a specific entitlement in a given instance + operationId: getEntitlementUsageMetrics + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - entitlement-slug + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EntitlementUsage" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: Get entitlement usage metrics for an instance + tags: + - instances + post: + description: "Report a usage metric for a specific entitlement in a given instance, with optional metadata. The server dates every report on receipt; the request carries no timestamp. Send an Idempotency-Key to make retries safe: without one, a report sent twice counts twice." + operationId: reportEntitlementUsageMetric + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - entitlement-slug + type: string + - description: "Optional idempotency key: the id of the business event this report counts, 1 to 128 characters of [A-Za-z0-9._:-], matched exactly and case-sensitively. A report sent again with the same key and the same behavior and value within KAITEN_USAGE_IDEMPOTENCY_WINDOW (35 days by default) is applied once: the retry answers 200 with the original response and the Idempotent-Replayed header, and changes nothing. The same key with another behavior or value answers 409 ReportEntitlementUsageMetric.TransactionIdReused. A rejected report does not consume its key. Scoped to the instance and entitlement: one business event may feed two meters under one key. Stored and read back as transactionId." + in: header + name: Idempotency-Key + schema: + description: "Optional idempotency key: the id of the business event this report counts, 1 to 128 characters of [A-Za-z0-9._:-], matched exactly and case-sensitively. A report sent again with the same key and the same behavior and value within KAITEN_USAGE_IDEMPOTENCY_WINDOW (35 days by default) is applied once: the retry answers 200 with the original response and the Idempotent-Replayed header, and changes nothing. The same key with another behavior or value answers 409 ReportEntitlementUsageMetric.TransactionIdReused. A rejected report does not consume its key. Scoped to the instance and entitlement: one business event may feed two meters under one key. Stored and read back as transactionId." + examples: + - llm-call-9f2c:tokens + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReportEntitlementUsageBody" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ReportedEntitlementUsage" + description: OK + headers: + Idempotent-Replayed: + schema: + description: "true when the report replays an earlier one sent with the same Idempotency-Key: the body is that report's original response and nothing was counted again" + type: string + Kaiten-Metadata-Dropped: + schema: + description: too_large when the report's metadata was above 4 KiB and was not stored; the report itself was counted + type: string + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Report entitlement usage metric for an instance + tags: + - instances + /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage/reports: + get: + description: "The usage history of one instance and entitlement: every accepted report in the range, with the counter before and after it and the limit in force, in reportSeq order, paged with afterSeq. Decimals are strings. Reports are kept for the organization's usage history retention." + operationId: listUsageReports + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - entitlement-slug + type: string + - description: Start of the range, inclusive (RFC 3339). Defaults to 30 days before to, moved up to the start of the organization's usage history when that is later. An explicit from before it answers 422 ListUsageReports.OutsideRetention. + explode: false + in: query + name: from + schema: + description: Start of the range, inclusive (RFC 3339). Defaults to 30 days before to, moved up to the start of the organization's usage history when that is later. An explicit from before it answers 422 ListUsageReports.OutsideRetention. + format: date-time + type: string + - description: End of the range, exclusive (RFC 3339). Defaults to now. + explode: false + in: query + name: to + schema: + description: End of the range, exclusive (RFC 3339). Defaults to now. + format: date-time + type: string + - description: "Return the reports after this reportSeq: the nextAfterSeq of the previous page" + explode: false + in: query + name: afterSeq + schema: + description: "Return the reports after this reportSeq: the nextAfterSeq of the previous page" + format: int64 + minimum: 0 + type: integer + - description: Maximum number of reports to return (default 100, max 500) + explode: false + in: query + name: limit + schema: + description: Maximum number of reports to return (default 100, max 500) + format: int32 + maximum: 500 + minimum: 1 + type: integer + - description: Only the report sent with this idempotency key + explode: false + in: query + name: transactionId + schema: + description: Only the report sent with this idempotency key + maxLength: 128 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/UsageReportPage" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: List an entitlement's usage reports for an instance + tags: + - instances + /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage/reports/export: + get: + description: Streams every report of one instance and entitlement in the range as CSV or NDJSON, in reportSeq order. Decimals are written exactly as the journal stores them. + operationId: exportUsageReports + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Entitlement slug + in: path + name: entitlementSlug + required: true + schema: + description: Entitlement slug + examples: + - entitlement-slug + type: string + - description: Start of the range, inclusive (RFC 3339). Defaults to 30 days before to, moved up to the start of the organization's usage history when that is later. An explicit from before it answers 422 ExportUsageReports.OutsideRetention. + explode: false + in: query + name: from + schema: + description: Start of the range, inclusive (RFC 3339). Defaults to 30 days before to, moved up to the start of the organization's usage history when that is later. An explicit from before it answers 422 ExportUsageReports.OutsideRetention. + format: date-time + type: string + - description: End of the range, exclusive (RFC 3339). Defaults to now. At most 366 days after from. + explode: false + in: query + name: to + schema: + description: End of the range, exclusive (RFC 3339). Defaults to now. At most 366 days after from. + format: date-time + type: string + - description: "csv (the default): RFC 4180 with a header row. json: NDJSON, one report per line, in the shape listUsageReports returns. Anything else answers 422 ExportUsageReports.InvalidFormat." + explode: false + in: query + name: format + schema: + description: "csv (the default): RFC 4180 with a header row. json: NDJSON, one report per line, in the shape listUsageReports returns. Anything else answers 422 ExportUsageReports.InvalidFormat." + examples: + - csv + type: string + responses: + "200": + content: + application/x-ndjson: + schema: + type: string + text/csv: + schema: + type: string + description: "The reports, as an attachment: CSV with a header row for format=csv, one JSON object per line for format=json" + headers: + Content-Disposition: + description: attachment, with a file name naming the range + schema: + type: string + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: Export an entitlement's usage reports for an instance + tags: + - instances + /instances/{instanceSlug}/integrations/{integrationName}: + delete: + description: Delete a single integration for an instance + operationId: delete-instance-integration + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:instances + summary: Delete an instance integration + tags: + - instances + get: + description: Retrieve a single integration for an instance + operationId: get-instance-integration + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegration" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: Get an instance integration + tags: + - instances + post: + description: Create a single integration for an instance + operationId: create-instance-integration + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegration" + description: Integration payload + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegration" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:instances + summary: Create an instance integration + tags: + - instances + put: + description: Update a single integration for an instance + operationId: update-instance-integration + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - instance-slug + type: string + - description: Integration adapter name + in: path + name: integrationName + required: true + schema: + description: Integration adapter name + examples: + - kaiten.integration.crm.attio + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegration" + description: Integration payload + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegration" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:instances + summary: Update an instance integration + tags: + - instances + /instances/{instanceSlug}/invoices: + get: + description: The invoices of one instance's subscription, across every time it was subscribed, with the filters of listInvoices. Requires billing to be enabled for the organization. + operationId: listInstanceInvoices + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: Only invoices in these statuses; repeat the parameter for several + explode: true + in: query + name: status + schema: + description: Only invoices in these statuses; repeat the parameter for several + items: + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + type: + - array + - "null" + - explode: false + in: query + name: kind + schema: + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + - explode: false + in: query + name: providerKind + schema: + enum: + - NOOP + - STRIPE + type: string + - description: "Only this customer's invoices: composed under this slug, or of the customer that has it now" + explode: false + in: query + name: customerSlug + schema: + description: "Only this customer's invoices: composed under this slug, or of the customer that has it now" + type: string + - description: Only issued invoices still unpaid past their due date + explode: false + in: query + name: overdue + schema: + description: Only issued invoices still unpaid past their due date + type: boolean + - description: Only DRAFTs held for their usage journal + explode: false + in: query + name: held + schema: + description: Only DRAFTs held for their usage journal + type: boolean + - explode: false + in: query + name: handoffStatus + schema: + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + - description: Issued at or after this instant (RFC 3339) + explode: false + in: query + name: issuedFrom + schema: + description: Issued at or after this instant (RFC 3339) + format: date-time + type: string + - description: Issued before this instant (RFC 3339) + explode: false + in: query + name: issuedTo + schema: + description: Issued before this instant (RFC 3339) + format: date-time + type: string + - description: Billing a boundary at or after this instant + explode: false + in: query + name: boundaryFrom + schema: + description: Billing a boundary at or after this instant + format: date-time + type: string + - description: Billing a boundary before this instant + explode: false + in: query + name: boundaryTo + schema: + description: Billing a boundary before this instant + format: date-time + type: string + - description: "Only invoices changed at or after this instant, oldest change first instead of newest invoice first: store the last updatedAt read and pass it next time to miss no change" + explode: false + in: query + name: updatedSince + schema: + description: "Only invoices changed at or after this instant, oldest change first instead of newest invoice first: store the last updatedAt read and pass it next time to miss no change" + format: date-time + type: string + - description: Opaque cursor from the previous page's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque cursor from the previous page's nextCursor + type: string + - description: Page size, 50 by default, 200 at most + explode: false + in: query + name: limit + schema: + description: Page size, 50 by default, 200 at most + format: int32 + maximum: 200 + minimum: 0 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageInvoiceSummary" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: List an instance's invoices + tags: + - billing + /instances/{instanceSlug}/vouchers: + get: + description: The vouchers an instance redeemed, newest first. Requires billing to be enabled for the organization. + operationId: listInstanceVouchers + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - explode: false + in: query + name: status + schema: + enum: + - ACTIVE + - EXPIRED + - REVOKED + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Redemption" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:voucher_redemptions + summary: List an instance's vouchers + tags: + - vouchers + /instances/{instanceSlug}/vouchers/redeem: + post: + description: Redeems a voucher for an instance. A boost applies from the next read or report, for its duration; a PRICE voucher discounts the subscription's next invoices. 422 .DiscountLimitExceeded when a line of the instance's invoices would bear more than 20 discounts, whatever the provider (a Stripe invoice item takes 20). Emits INSTANCE_VOUCHER_REDEEMED, and VOUCHER_EXHAUSTED with the last redemption. Requires billing to be enabled for the organization. + operationId: redeemVoucher + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RedeemableCode" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Redemption" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:voucher_redemptions + summary: Redeem a voucher + tags: + - vouchers + /instances/{instanceSlug}/vouchers/{instanceVoucherId}/revoke: + post: + description: "Stops a redemption from applying: a boost ends at once, a discount applies to no further invoice. Invoices already issued are untouched. Emits INSTANCE_VOUCHER_REVOKED. Requires billing to be enabled for the organization." + operationId: revokeInstanceVoucher + parameters: + - description: Instance slug + in: path + name: instanceSlug + required: true + schema: + description: Instance slug + examples: + - acme-prod + type: string + - description: Redemption id + in: path + name: instanceVoucherId + required: true + schema: + description: Redemption id + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RevocationReason" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Redemption" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:vouchers + summary: Revoke a redemption + tags: + - vouchers + /integration/{adapter}/customer/{externalId}: + get: + description: Resolve a customer through one of its integrations using adapter + external ID. + operationId: get-customer-integration-by-external-id + parameters: + - description: Integration adapter without the kaiten.integration. prefix + in: path + name: adapter + required: true + schema: + description: Integration adapter without the kaiten.integration. prefix + examples: + - crm.attio + type: string + - description: External identifier in the third-party system + in: path + name: externalId + required: true + schema: + description: External identifier in the third-party system + examples: + - rec_12345 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegrationResource" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:customers + summary: Get a customer by integration external ID + tags: + - integrations + - customers + patch: + description: Create or partially update a customer resolved through adapter + external ID. Missing fields are left unchanged. + operationId: upsert-customer-integration-by-external-id + parameters: + - description: Integration adapter without the kaiten.integration. prefix + in: path + name: adapter + required: true + schema: + description: Integration adapter without the kaiten.integration. prefix + examples: + - crm.attio + type: string + - description: External identifier in the third-party system + in: path + name: externalId + required: true + schema: + description: External identifier in the third-party system + examples: + - rec_12345 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerBody" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerIntegrationResource" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:customers + summary: Upsert a customer by integration external ID + tags: + - integrations + - customers + /integration/{adapter}/instance/{externalId}: + get: + description: Resolve an instance through one of its integrations using adapter + external ID. + operationId: get-instance-integration-by-external-id + parameters: + - description: Integration adapter without the kaiten.integration. prefix + in: path + name: adapter + required: true + schema: + description: Integration adapter without the kaiten.integration. prefix + examples: + - crm.attio + type: string + - description: External identifier in the third-party system + in: path + name: externalId + required: true + schema: + description: External identifier in the third-party system + examples: + - rec_12345 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegrationResource" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:instances + summary: Get an instance by integration external ID + tags: + - integrations + - instances + patch: + description: Create or partially update an instance resolved through adapter + external ID. Missing fields are left unchanged. + operationId: upsert-instance-integration-by-external-id + parameters: + - description: Integration adapter without the kaiten.integration. prefix + in: path + name: adapter + required: true + schema: + description: Integration adapter without the kaiten.integration. prefix + examples: + - crm.attio + type: string + - description: External identifier in the third-party system + in: path + name: externalId + required: true + schema: + description: External identifier in the third-party system + examples: + - rec_12345 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceBody" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InstanceIntegrationResource" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:instances + summary: Upsert an instance by integration external ID + tags: + - integrations + - instances + /invoices: + get: + description: The organization's invoices, newest first; with updatedSince, the ones changed since an instant, oldest change first, for incremental sync. Invoices outlive their instance and customer. Requires billing to be enabled for the organization. + operationId: listInvoices + parameters: + - description: Only invoices in these statuses; repeat the parameter for several + explode: true + in: query + name: status + schema: + description: Only invoices in these statuses; repeat the parameter for several + items: + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + type: + - array + - "null" + - explode: false + in: query + name: kind + schema: + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + - explode: false + in: query + name: providerKind + schema: + enum: + - NOOP + - STRIPE + type: string + - description: "Only this customer's invoices: composed under this slug, or of the customer that has it now" + explode: false + in: query + name: customerSlug + schema: + description: "Only this customer's invoices: composed under this slug, or of the customer that has it now" + type: string + - description: Only issued invoices still unpaid past their due date + explode: false + in: query + name: overdue + schema: + description: Only issued invoices still unpaid past their due date + type: boolean + - description: Only DRAFTs held for their usage journal + explode: false + in: query + name: held + schema: + description: Only DRAFTs held for their usage journal + type: boolean + - explode: false + in: query + name: handoffStatus + schema: + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + - description: Issued at or after this instant (RFC 3339) + explode: false + in: query + name: issuedFrom + schema: + description: Issued at or after this instant (RFC 3339) + format: date-time + type: string + - description: Issued before this instant (RFC 3339) + explode: false + in: query + name: issuedTo + schema: + description: Issued before this instant (RFC 3339) + format: date-time + type: string + - description: Billing a boundary at or after this instant + explode: false + in: query + name: boundaryFrom + schema: + description: Billing a boundary at or after this instant + format: date-time + type: string + - description: Billing a boundary before this instant + explode: false + in: query + name: boundaryTo + schema: + description: Billing a boundary before this instant + format: date-time + type: string + - description: "Only invoices changed at or after this instant, oldest change first instead of newest invoice first: store the last updatedAt read and pass it next time to miss no change" + explode: false + in: query + name: updatedSince + schema: + description: "Only invoices changed at or after this instant, oldest change first instead of newest invoice first: store the last updatedAt read and pass it next time to miss no change" + format: date-time + type: string + - description: Opaque cursor from the previous page's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque cursor from the previous page's nextCursor + type: string + - description: Page size, 50 by default, 200 at most + explode: false + in: query + name: limit + schema: + description: Page size, 50 by default, 200 at most + format: int32 + maximum: 200 + minimum: 0 + type: integer + - description: "Only this instance's invoices: composed under this slug, or of the instance that has it now" + explode: false + in: query + name: instanceSlug + schema: + description: "Only this instance's invoices: composed under this slug, or of the instance that has it now" + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageInvoiceSummary" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: List invoices + tags: + - billing + /invoices/export: + get: + description: "Streams the invoices the filters of listInvoices select, as CSV or NDJSON. Amounts are integer minor units with the currency's exponent beside them, so a spreadsheet never sees a float. The export carries billing e-mails: it is an authenticated read for the organization's accounting. Requires billing to be enabled for the organization." + operationId: exportInvoices + parameters: + - description: Only invoices in these statuses; repeat the parameter for several + explode: true + in: query + name: status + schema: + description: Only invoices in these statuses; repeat the parameter for several + items: + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + type: + - array + - "null" + - explode: false + in: query + name: kind + schema: + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + - explode: false + in: query + name: providerKind + schema: + enum: + - NOOP + - STRIPE + type: string + - description: "Only this customer's invoices: composed under this slug, or of the customer that has it now" + explode: false + in: query + name: customerSlug + schema: + description: "Only this customer's invoices: composed under this slug, or of the customer that has it now" + type: string + - description: Only issued invoices still unpaid past their due date + explode: false + in: query + name: overdue + schema: + description: Only issued invoices still unpaid past their due date + type: boolean + - description: Only DRAFTs held for their usage journal + explode: false + in: query + name: held + schema: + description: Only DRAFTs held for their usage journal + type: boolean + - explode: false + in: query + name: handoffStatus + schema: + enum: + - NOT_REQUIRED + - PENDING + - ACKNOWLEDGED + type: string + - description: Issued at or after this instant (RFC 3339) + explode: false + in: query + name: issuedFrom + schema: + description: Issued at or after this instant (RFC 3339) + format: date-time + type: string + - description: Issued before this instant (RFC 3339) + explode: false + in: query + name: issuedTo + schema: + description: Issued before this instant (RFC 3339) + format: date-time + type: string + - description: Billing a boundary at or after this instant + explode: false + in: query + name: boundaryFrom + schema: + description: Billing a boundary at or after this instant + format: date-time + type: string + - description: Billing a boundary before this instant + explode: false + in: query + name: boundaryTo + schema: + description: Billing a boundary before this instant + format: date-time + type: string + - description: "Only invoices changed at or after this instant, oldest change first instead of newest invoice first: store the last updatedAt read and pass it next time to miss no change" + explode: false + in: query + name: updatedSince + schema: + description: "Only invoices changed at or after this instant, oldest change first instead of newest invoice first: store the last updatedAt read and pass it next time to miss no change" + format: date-time + type: string + - description: Opaque cursor from the previous page's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque cursor from the previous page's nextCursor + type: string + - description: Page size, 50 by default, 200 at most + explode: false + in: query + name: limit + schema: + description: Page size, 50 by default, 200 at most + format: int32 + maximum: 200 + minimum: 0 + type: integer + - description: Only this instance's invoices + explode: false + in: query + name: instanceSlug + schema: + description: Only this instance's invoices + type: string + - description: "csv (the default): RFC 4180 with a header row. json: NDJSON, one invoice with its lines per line. Anything else answers 422 ExportInvoices.InvalidFormat." + explode: false + in: query + name: format + schema: + description: "csv (the default): RFC 4180 with a header row. json: NDJSON, one invoice with its lines per line. Anything else answers 422 ExportInvoices.InvalidFormat." + examples: + - csv + type: string + - description: "For csv: line (the default), one row per invoice line with the invoice's columns repeated; invoice, one row per invoice. Anything else answers 422 ExportInvoices.InvalidGranularity." + explode: false + in: query + name: granularity + schema: + description: "For csv: line (the default), one row per invoice line with the invoice's columns repeated; invoice, one row per invoice. Anything else answers 422 ExportInvoices.InvalidGranularity." + examples: + - line + type: string + responses: + "200": + content: + application/x-ndjson: + schema: + type: string + text/csv: + schema: + type: string + description: The invoices, as an attachment + headers: + Content-Disposition: + description: attachment, with a file name + schema: + type: string + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Export invoices + tags: + - billing + /invoices/{invoiceId}: + get: + description: "One invoice with its lines, the customer and instance as they were when it was composed, its hold and its handoff. Addressed by id at organization level: an invoice outlives its instance and customer. Requires billing to be enabled for the organization." + operationId: getInvoice + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: Get an invoice + tags: + - billing + /invoices/{invoiceId}/lines/{lineId}/reports: + get: + description: "The usage reports a USAGE or OVERAGE line was measured from: its pair's reports dated in its service period, in reportSeq order. Summed by reset window and floored at 0, they give the line's measured quantity. Addressed by invoice, it answers after the instance was deleted. Requires billing to be enabled for the organization." + operationId: listInvoiceLineReports + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + - description: Line identifier, as the invoice's lines carry it + in: path + name: lineId + required: true + schema: + description: Line identifier, as the invoice's lines carry it + type: string + - description: Reports after this reportSeq; 0 starts at the first + explode: false + in: query + name: afterSeq + schema: + description: Reports after this reportSeq; 0 starts at the first + format: int64 + minimum: 0 + type: integer + - description: Page size, 100 by default, 500 at most + explode: false + in: query + name: limit + schema: + description: Page size, 100 by default, 500 at most + format: int32 + maximum: 500 + minimum: 0 + type: integer + - description: "json (the default): one page. csv: every report, streamed as RFC 4180 with a header row. Anything else answers 422 ListInvoiceLineReports.InvalidFormat." + explode: false + in: query + name: format + schema: + description: "json (the default): one page. csv: every report, streamed as RFC 4180 with a header row. Anything else answers 422 ListInvoiceLineReports.InvalidFormat." + examples: + - json + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/LineReportPage" + text/csv: + schema: + type: string + description: One page of reports (json), or every report as a CSV attachment (csv) + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:billing + summary: List the usage reports behind an invoice line + tags: + - billing + /invoices/{invoiceId}/mark-paid: + post: + description: Records that a MANUAL invoice was paid. An invoice still waiting in the handoff queue is acknowledged by the same write. Marking it paid again with the same external reference answers it unchanged. An invoice a payment provider collects is settled there. Requires billing to be enabled for the organization. + operationId: markInvoicePaid + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InvoicePayment" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Mark an invoice paid + tags: + - billing + /invoices/{invoiceId}/recompose: + post: + description: "Composes an invoice again from the usage journal as it is now, keeping its BASE lines as they were sold. A held DRAFT is rewritten in place (200), under the subscription's current provider and terms: issued when its journal is now sound and its provider takes as many items, held again otherwise. A VOID invoice gets a replacement for the same boundary (201), under the subscription's current terms and billing e-mail. Any other invoice must be voided first. Requires billing to be enabled for the organization." + operationId: recomposeInvoice + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: The replacement of a VOID invoice, for the same boundary + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Recompose an invoice + tags: + - billing + /invoices/{invoiceId}/release-hold: + post: + description: "Accepts a held invoice as composed and issues it: MANUAL and pending in the handoff queue, or PAID when nothing is owed. The release keeps who released it and why. 409 .ProviderItemLimit for a PROVIDER_ITEM_LIMIT hold, which its provider would refuse: switch the subscription to NoOp and recompose in place, or void and recompose after that switch. Requires billing to be enabled for the organization." + operationId: releaseInvoiceHold + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/HoldRelease" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Release a held invoice + tags: + - billing + /invoices/{invoiceId}/retry-push: + post: + description: Puts a DRAFT or PUSH_FAILED invoice of a payment provider back in the push queue now, which resumes the push where its last attempt stopped. A draft waiting for finalization in its provider is finalized at once. Refused for a held invoice (409 RetryInvoicePush.Held) and for any other (409 RetryInvoicePush.InvalidStatus). Requires billing to be enabled for the organization. + operationId: retryInvoicePush + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + responses: + "202": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: Accepted + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Retry pushing an invoice to its provider + tags: + - billing + /invoices/{invoiceId}/sync: + post: + description: "Reads an invoice from its payment provider now and applies what the provider says: its finalization, payment, write-off or void. Refused for an invoice not in a provider (409 SyncInvoice.NotPushed). Requires billing to be enabled for the organization." + operationId: syncInvoice + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Sync an invoice with its provider + tags: + - billing + /invoices/{invoiceId}/void: + post: + description: Voids an invoice not yet settled -- a DRAFT, held or not, a MANUAL one, or one a payment provider issued -- which frees its boundary for a recompose. A provider's invoice is voided there first, and here only once the provider confirms (503 VoidInvoice.ProviderUnavailable leaves it unchanged); one the provider reports paid is refused. A paid or written-off invoice cannot be voided. A handoff still pending stays so, and its consumer sees the VOID. Voiding a VOID invoice again answers it unchanged. Requires billing to be enabled for the organization. + operationId: voidInvoice + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InvoiceVoid" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Void an invoice + tags: + - billing + /invoices/{invoiceId}/write-off: + post: + description: "Records that the organization gave up collecting a MANUAL invoice: it becomes UNCOLLECTIBLE. A handoff still pending stays so, and its consumer sees the new status. Writing off an UNCOLLECTIBLE invoice again answers it unchanged. Requires billing to be enabled for the organization." + operationId: writeOffInvoice + parameters: + - description: Invoice identifier + in: path + name: invoiceId + required: true + schema: + description: Invoice identifier + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InvoiceWriteOff" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Invoice" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:billing + summary: Write off an invoice + tags: + - billing + /license-families: + get: + description: "Returns a cursor-paginated page of license families for the current organization, each with the version it currently resolves to: the family's default version, or its highest-numbered published version. A family whose versions are all drafts or all archived is still listed, with no current version." + operationId: list-license-families + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageLicenseFamilyView" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:licenses + summary: List license families + tags: + - license-families + /license-families/{familySlug}: + get: + description: |- + Resolves a license family to the version it currently serves, returned as currentVersion: the family's default version if it has one, otherwise its highest-numbered PUBLISHED version. This is the stable address for "the current version of this product", so a caller never has to name a version slug. + + A family with a default keeps serving it when a newer version is published -- the default is what the vendor puts forward, and superseding it is an explicit act (make another version the default, or unset it). A family without a default follows publication: the newest PUBLISHED version wins. + + Returns 404 GetLicenseFamily.NoPublishedVersion when the family exists but has no PUBLISHED version, since there is nothing to resolve to. Use ?include=versions to read such a family anyway: with the history requested, the family is returned with its versions and no currentVersion. Use ?version=N for explicit access to one version, including draft and archived ones. + operationId: get-license-family + parameters: + - description: License family slug + in: path + name: familySlug + required: true + schema: + description: License family slug + examples: + - enterprise-license + type: string + - description: "Address this version number instead of resolving the current one. Explicit historical access: the version is returned in currentVersion whatever its lifecycle state, draft and archived included, and the resolution rule does not apply." + explode: false + in: query + name: version + schema: + description: "Address this version number instead of resolving the current one. Explicit historical access: the version is returned in currentVersion whatever its lifecycle state, draft and archived included, and the resolution rule does not apply." + format: int32 + minimum: 1 + type: integer + - description: "Extra representations to add, comma-separated. Only 'versions' exists today: it adds every version of the family, oldest first, and makes a family with no PUBLISHED version readable instead of a 404. A list, so that another representation can be added later without changing the parameter's type." + explode: false + in: query + name: include + schema: + description: "Extra representations to add, comma-separated. Only 'versions' exists today: it adds every version of the family, oldest first, and makes a family with no PUBLISHED version readable instead of a 404. A list, so that another representation can be added later without changing the parameter's type." + items: + enum: + - versions + type: string + type: array + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/LicenseFamilyView" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:licenses + summary: Get a license family by slug + tags: + - license-families + patch: + description: "Changes a family's own settings: whether the public catalogue lists it (families are private until made public), its fallback family, what a trial without a payment method becomes at its end, and the trial reminder's lead. An absent member keeps the stored value. 404 .NotFound; 422 .InvalidFallback, .InvalidTrialEndBehavior, .InvalidTrialReminderDays." + operationId: updateLicenseFamily + parameters: + - description: License family slug + in: path + name: familySlug + required: true + schema: + description: License family slug + examples: + - pro + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LicenseFamilyUpdate" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/LicenseFamilyView" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:licenses + summary: Update a license family + tags: + - licenses + /licenses: + get: + description: Returns a cursor-paginated page of licenses for the current organization. + operationId: get-licenses + parameters: + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageLicense" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:licenses + summary: GetUser all licenses + tags: + - licenses + post: + description: |- + Create a new license with the provided details. Without familySlug or familyId this creates a new license family and the license becomes its version 1. With one of them it adds the next version to that family (familySlug names it by slug, familyId by identifier; sent together they must agree), and the version number is assigned by the server as the next one in the family's sequence. The version starts PUBLISHED, or DRAFT when lifecycleState says so; ARCHIVED is refused (422 CreateLicense.LifecycleStateNotSettable), because a version is archived with archive-license once it has been on sale. + + Where the installation limits how many licenses an organization may have, only a new family counts against that limit (409 CreateLicense.EntitlementLimitReached once it is reached): a new version of an existing family never does. + operationId: create-license + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/License" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/License" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:licenses + summary: Create a new license + tags: + - licenses + /licenses/{licenseSlug}: + delete: + description: Delete a license with the provided details. Deleting the last version of a license family also deletes the family, and frees its slug for a new product to take. A deleted version's number is not reused by the next version of its family. + operationId: delete-license + parameters: + - description: License slug + in: path + name: licenseSlug + required: true + schema: + description: License slug + examples: + - license-slug + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Delete a license + tags: + - licenses + get: + description: Retrieve a license by their unique slug + operationId: get-license + parameters: + - description: License slug + in: path + name: licenseSlug + required: true + schema: + description: License slug + examples: + - license-slug + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/License" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:licenses + summary: Get a license by slug + tags: + - licenses + put: + description: Update a license with the provided details + operationId: update-license + parameters: + - description: License slug + in: path + name: licenseSlug + required: true + schema: + description: License slug + examples: + - license-slug + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/License" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Update a license + tags: + - licenses + /licenses/{licenseSlug}/archive: + post: + description: "Withdraws a PUBLISHED license version from sale: its family stops resolving to it and no instance can be assigned to it any more, while instances already on it keep it. Only a published version can be archived (409 ArchiveLicense.NotPublished) -- a draft that was never on sale is deleted instead -- and not while it is its family's default (409 ArchiveLicense.DefaultMustBePublished): make another version the default, or unset it, first. Emits LICENSE_ARCHIVED." + operationId: archive-license + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - enterprise-license-v2 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/License" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Archive a license version + tags: + - licenses + /licenses/{licenseSlug}/entitlements: + get: + description: Returns a cursor-paginated page of entitlements granted by a license, identified by its unique slug. + operationId: get-license-entitlements + parameters: + - description: License slug + in: path + name: licenseSlug + required: true + schema: + description: License slug + examples: + - license-slug + type: string + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageLicenseEntitlement" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:licenses + summary: Get all entitlements for a license + tags: + - licenses + post: + description: Associate an entitlement with a license by providing the license slug and entitlement details + operationId: associate-entitlement-with-license + parameters: + - description: License slug + in: path + name: licenseSlug + required: true + schema: + description: License slug + examples: + - license-slug + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LicenseEntitlement" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:licenses + summary: Associate an entitlement with a license + tags: + - licenses + /licenses/{licenseSlug}/entitlements/{entitlementSlug}: + delete: + description: Delete a license entitlement with the provided details + operationId: delete-license-entitlement + parameters: + - in: path + name: licenseSlug + required: true + schema: + type: string + - in: path + name: entitlementSlug + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Delete a license entitlement + tags: + - licenses + get: + description: Retrieve a entitlement for a license by their unique slugs + operationId: get-license-entitlement + parameters: + - in: path + name: licenseSlug + required: true + schema: + type: string + - in: path + name: entitlementSlug + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/LicenseEntitlement" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:licenses + summary: Get a license entitlement + tags: + - licenses + put: + description: Update an license entitlement with the provided details + operationId: update-license-entitlement + parameters: + - in: path + name: licenseSlug + required: true + schema: + type: string + - in: path + name: entitlementSlug + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LicenseEntitlement" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Update an license entitlement + tags: + - licenses + /licenses/{licenseSlug}/invoice-preview: + post: + description: "Composes the RENEWAL invoice a subscription to this licence version would be billed at a boundary now, without writing anything: an instance's usage or a sample, rated in arrears over the billing period that ends; the base price for the period that starts (or the one that ends, for an ARREARS base); the listed add-ons; a voucher's discount. Works on DRAFT versions, to check prices before publishing. Requires billing to be enabled for the organization." + operationId: previewLicenseInvoice + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - pro-v2 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InvoicePreviewScenario" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/InvoicePreview" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:licenses + summary: Preview a license invoice + tags: + - licenses + /licenses/{licenseSlug}/prices: + get: + description: The prices of one licence version, in display order. Requires billing to be enabled for the organization. + operationId: listLicensePrices + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - pro-v2 + type: string + - description: Only prices in this status; both when omitted + explode: false + in: query + name: status + schema: + description: Only prices in this status; both when omitted + enum: + - ACTIVE + - DEPRECATED + type: string + - description: Only prices of this billing model + explode: false + in: query + name: billingModel + schema: + description: Only prices of this billing model + enum: + - FLAT_FEE + - USAGE_BASED + - OVERAGE + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Price" + type: array + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:licenses + summary: List a license version's prices + tags: + - licenses + post: + description: Adds a price to a licence version. A metered price's sale-unit factor is captured from its entitlement now and never changes. Requires billing to be enabled for the organization. + operationId: createLicensePrice + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - pro-v2 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewLicensePrice" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Price" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:licenses + summary: Create a license price + tags: + - licenses + /licenses/{licenseSlug}/prices/{priceId}: + get: + description: One price of a licence version. Requires billing to be enabled for the organization. + operationId: getLicensePrice + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - pro-v2 + type: string + - description: Price identifier + in: path + name: priceId + required: true + schema: + description: Price identifier + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Price" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - read:licenses + summary: Get a license price + tags: + - licenses + patch: + description: Edits a price of a DRAFT licence version; the prices of a published version are immutable (UpdateLicensePrice.VersionNotDraft). Currency and billing model cannot change. Requires billing to be enabled for the organization. + operationId: updateLicensePrice + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - pro-v2 + type: string + - description: Price identifier + in: path + name: priceId + required: true + schema: + description: Price identifier + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LicensePriceChanges" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Price" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:licenses + summary: Update a license price + tags: + - licenses + /licenses/{licenseSlug}/prices/{priceId}/deprecate: + post: + description: "Retires a price from the catalogue: it keeps billing what is already pinned to it, is no longer offered, and stops being a default. Deprecation cannot be undone. Requires billing to be enabled for the organization." + operationId: deprecateLicensePrice + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - pro-v2 + type: string + - description: Price identifier + in: path + name: priceId + required: true + schema: + description: Price identifier + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Price" + description: OK + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:licenses + summary: Deprecate a license price + tags: + - licenses + /licenses/{licenseSlug}/publish: + post: + description: "Puts a DRAFT license version on sale: it becomes PUBLISHED, so it can be made its family's default, and its family resolves to it when the family has no default and this is its highest published version. Only a draft can be published (409 PublishLicense.NotADraft); an archived version goes back on sale with unarchive-license. Emits LICENSE_PUBLISHED." + operationId: publish-license + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - enterprise-license-v2 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/License" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Publish a license version + tags: + - licenses + /licenses/{licenseSlug}/unarchive: + post: + description: "Puts an ARCHIVED license version back on sale: it is PUBLISHED again, can be assigned to instances, and can be what its family resolves to. Only an archived version can be unarchived (409 UnarchiveLicense.NotArchived). Emits LICENSE_UNARCHIVED." + operationId: unarchive-license + parameters: + - description: License version slug + in: path + name: licenseSlug + required: true + schema: + description: License version slug + examples: + - enterprise-license-v2 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/License" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:licenses + summary: Unarchive a license version + tags: + - licenses + /me: + get: + description: Available to every active human member, including members with no assigned roles or permissions. + operationId: GetMe + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Me" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:profile + summary: Read current user and effective authorization + tags: + - users + /metadata-fields: + get: + description: "Returns a cursor-paginated page of the metadata fields declared for the org and the given resource type, ordered by displayOrder then createdAt. Only active fields are listed, unless includeArchived is set: an archived field's id is what unarchive-metadata-field takes." + operationId: list-metadata-fields + parameters: + - explode: false + in: query + name: resourceType + required: true + schema: + enum: + - DEPLOYMENT_ZONE + - INSTANCE + type: string + - description: Include archived fields too, each with its archivedAt + explode: false + in: query + name: includeArchived + schema: + description: Include archived fields too, each with its archivedAt + type: boolean + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageMetadataField" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:metadata_fields + summary: List metadata fields for a resource type + tags: + - metadataFields + post: + description: Declares a typed metadata field for a given resource type (DEPLOYMENT_ZONE, INSTANCE). The json_schema is validated against JSON Schema 2020-12 before insertion. Restricted to write:metadata_fields scope (admin-only policy). + operationId: create-metadata-field + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataField" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataField" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - bearerAuth: + - write:metadata_fields + summary: Create a metadata field + tags: + - metadataFields + /metadata-fields/reorder: + post: + description: Sets the displayOrder of the listed fields to their position in the array (0-indexed). All ids must belong to the same organization, resource type, and be non-archived; the entire reorder is applied in a single transaction. + operationId: reorder-metadata-fields + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ReorderMetadataFieldsInput" + required: true + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:metadata_fields + summary: Reorder metadata fields + tags: + - metadataFields + /metadata-fields/{id}: + patch: + description: "Updates label and json_schema. `key` and `resourceType` are immutable, and `displayOrder` belongs to POST /metadata-fields/reorder. The new json_schema is validated against JSON Schema 2020-12 and the transition rules: type changes, cardinality changes, and free-string ↔ enum toggles are rejected with 422." + operationId: update-metadata-field + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataField" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataField" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:metadata_fields + summary: Update a metadata field + tags: + - metadataFields + /metadata-fields/{id}/archive: + post: + description: Soft-deletes a metadata field. The row stays in the DB (its archived_at is set) so historic metadata is still interpretable; the field stops appearing in the active schema composition. + operationId: archive-metadata-field + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataField" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:metadata_fields + summary: Archive a metadata field + tags: + - metadataFields + /metadata-fields/{id}/dry-run: + post: + description: Computes, server-side, how many existing resources of this field's resource type carry a value under this field's key that would NOT satisfy the supplied candidate JSON Schema, plus a small sample. Read-only — does not mutate the field. Replaces the former client-side dry-run that pulled every resource's metadata. + operationId: dry-run-metadata-field + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataFieldSchemaProposal" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Impact" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:metadata_fields + summary: Preview the impact of a metadata field schema change + tags: + - metadataFields + /metadata-fields/{id}/unarchive: + post: + description: Restores a previously archived metadata field (clears its archived_at). The field re-enters the active schema composition. Returns 409 if an active field already uses the same key for this resource type, or 404 if the field does not exist or is not archived. + operationId: unarchive-metadata-field + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataField" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:metadata_fields + summary: Unarchive a metadata field + tags: + - metadataFields + /ofrep/v1/evaluate/flags: + post: + description: |- + OFREP bulk evaluation request. + The endpoint is called by the client providers to evaluate all flags at once. + operationId: evaluateFlagsBulk + parameters: + - description: The request will be processed only if ETag doesn't match any of the values listed. + in: header + name: If-None-Match + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/BulkEvaluationRequest" + description: EvaluateVariant multiple flags in one request + responses: + "200": + content: + application/json: + schema: + oneOf: + - properties: + flags: + description: Array of evaluation results + items: + oneOf: + - $ref: "#/components/schemas/EvaluationSuccess" + - $ref: "#/components/schemas/EvaluationFailure" + type: array + - $ref: "#/components/schemas/BulkEvaluationFailure" + description: OFREP successful evaluation response + headers: + ETag: + description: Entity tag used for cache validation + schema: + type: string + "304": + description: Bulk evaluation is not modified + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/BulkEvaluationFailure" + description: Bad evaluation request + "401": + description: Unauthorized - You need credentials to access the API + "403": + description: Forbidden - You are not authorized to access the API + "429": + description: Rate limit reached on the Flag Management System + headers: + Retry-After: + description: Indicates when to retry the request again + schema: + type: string + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/BulkEvaluationFailure" + description: Internal server error + security: + - bearerAuth: + - read:feature_flags + summary: OFREP bulk flag evaluation contract + tags: + - OFREP Core + /ofrep/v1/evaluate/flags/{key}: + post: + description: EvaluateVariant a single feature flag for a given key + operationId: evaluateFlag + parameters: + - in: path + name: key + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EvaluationRequest" + description: Evaluation context + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EvaluationSuccess" + description: Flag evaluated successfully + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/EvaluationFailure" + description: Evaluation failed + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/EvaluationFailure" + description: Internal error + security: + - bearerAuth: + - read:feature_flags + summary: OFREP single flag evaluation contract + tags: + - OFREP Core + /openfeature/v0/manifest: + get: + description: Returns a manifest of all feature flags that have a fallback_value set in their metadata + operationId: get-openfeature-manifest + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ManifestEnvelope" + description: OK + headers: + X-Manifest-Capabilities: + schema: + type: string + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - read:feature_flags + summary: Get OpenFeature manifest + tags: + - OpenFeature + /public/catalog: + get: + description: "What a pricing page shows: for each licence family the organization made public, its default version with its ACTIVE prices and user-facing entitlements, and the same for public add-on families. Read with a publishable key (X-Kaiten-Publishable-Key); the organization is the key's. Amounts are in minor units; never sum metered prices into a headline. Cacheable for 60 seconds." + operationId: getPublicCatalog + parameters: + - description: Only this licence family + explode: false + in: query + name: familySlug + schema: + description: Only this licence family + examples: + - pro + type: string + - description: addons (the default) lists the public add-ons too; none leaves them out + explode: false + in: query + name: include + schema: + description: addons (the default) lists the public add-ons too; none leaves them out + items: + enum: + - addons + - none + type: string + type: + - array + - "null" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicCatalog" + description: OK + headers: + Cache-Control: + schema: + type: string + Vary: + schema: + type: string + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - publishableKey: [] + summary: Get the public catalogue + tags: + - public + /public/checkout-sessions: + post: + description: "Starts a checkout for a visitor the vendor's backend does not know yet, with the publishable key and a Turnstile token. Writes one checkout session and nothing else: the customer, the instance and the subscription are created by the completion. Answers 201 status=open with the session's ksc_ token, returned once; with dryRun, 200 status=preview, writing and emitting nothing. dueToday is computed by the server. 403 .AnonymousCheckoutDisabled; 422 .TurnstileNotConfigured, .TurnstileFailed, .InvalidPriceSelection, .PriceNotPublic, .AddonNotPublic, .PriceNotFlatFee, .NotSelfServe, .ProviderCannotCapturePayment, .QuantityExceedsMax, .CurrencyMismatch, .InvalidEmail, .InvalidReturnUrl, .VoucherInvalid, .FallbackRequired; 429 .RateLimited (Retry-After): 10 starts per 10 minutes and 30 an hour per visitor address, dryRun included, 1,000 an hour per key; 503 .TurnstileUnavailable (Retry-After)." + operationId: createCheckoutSession + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutSessionDraft" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutSessionStart" + description: "A preview (dryRun): nothing was written" + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutSessionStart" + description: Created + headers: + Cache-Control: + schema: + type: string + Vary: + schema: + type: string + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - publishableKey: [] + summary: Start an anonymous checkout + tags: + - public + /public/checkout-sessions/{sessionId}: + get: + description: "The session its ksc_ was minted for: its status, what it bought and what is due, and, once completed, the customer and instance created. A session still OPEN after expiresAt reads ABANDONED. The token keeps reading it for 24 h after it closed, then answers 401 PublicAuth.InvalidCredential." + operationId: getCheckoutSession parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor + - description: The session the ksc_ was minted for; another one answers 401 + in: path + name: sessionId + required: true schema: - description: Opaque pagination cursor from a previous response's nextCursor + description: The session the ksc_ was minted for; another one answers 401 type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutSession" + description: OK + headers: + Cache-Control: + schema: + type: string + Vary: + schema: + type: string + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - checkoutSession: [] + summary: Read an anonymous checkout session + tags: + - public + /public/checkout-sessions/{sessionId}/confirm: + post: + description: "Completes the session when nothing has to be captured -- a FREE plan, a trial on a plan that needs no payment method, nothing due -- creating the customer, the instance and the subscription in one transaction: status=completed. Otherwise answers status=requires_payment_method with a hosted setup page, keeps the ksc_ across its redirect, and completes on the next confirm: awaiting_payment while the first invoice is charged, completed once paid. No body. 403 .AnonymousCheckoutDisabled; 409 .SessionClosed (errors[0].value.status), .DueTodayChanged, .EntitlementLimitReached; 422 .PriceNotPublic, .AddonNotPublic, .FallbackRequired, .ProviderCannotCapturePayment, .VoucherInvalid; 429 .RateLimited (Retry-After): 30 confirms per 10 minutes per visitor address, 1,000 an hour per key; 503 .ProviderUnavailable (Retry-After), Entitlement.VerificationUnavailable." + operationId: confirmCheckoutSession + parameters: + - description: The session the ksc_ was minted for; another one answers 401 + in: path + name: sessionId + required: true schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + description: The session the ksc_ was minted for; another one answers 401 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutConfirmation" + description: OK + headers: + Cache-Control: + schema: + type: string + Vary: + schema: + type: string + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - checkoutSession: [] + summary: Confirm an anonymous checkout + tags: + - public + /public/session/addons/{addonSlug}: + put: + description: "Attaches the add-on to the session's instance, changes its quantity, or removes it with 0. An attach or an increase changes the entitlements at once. With a live subscription a lower quantity, or 0, is scheduled for the next renewal: pendingQuantity shows it, and the quantity held, its entitlements and its billing stay until then; the quantity held cancels it, and so does an increase. Without a live subscription every change applies at once. The subscription bills the quantity held at each renewal, with no proration. Setting the quantity held, with nothing scheduled, changes nothing. 409 .OtherVersionAttached when another version of the same add-on is held, .BoundaryPending while the period that ended is being closed (Retry-After); 422 .AddonNotPublic, .Incompatible, .QuantityExceedsMax, .CurrencyMismatch, .InstanceRequired." + operationId: setSessionAddonQuantity + parameters: + - description: "An add-on version: the public one, or the one the instance holds" + in: path + name: addonSlug + required: true + schema: + description: "An add-on version: the public one, or the one the instance holds" + examples: + - extra-seats-v1 + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionAddonQuantity" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionAddon" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - customerSession: [] + summary: Set how many of an add-on the instance holds + tags: + - public + /public/session/billing/cancel: + post: + description: "Cancels the subscription of the session's instance at the end of the period paid for: it keeps billing and granting until then, and reactivating before then keeps it. A trial ends at once, with nothing to pay. Repeating it changes nothing. 409 .NotActive when there is no live subscription, .BoundaryPending while the period that ended is being closed (Retry-After); 422 .InstanceRequired for a session bound to no instance." + operationId: cancelSessionSubscription + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionCancellation" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/PageComponent" + $ref: "#/components/schemas/SessionSubscription" description: OK "400": content: @@ -3268,40 +30167,76 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - read:components - summary: List components + - customerSession: [] + summary: Cancel the subscription at the period's end tags: - - components + - public + /public/session/billing/payment-method-session: post: - description: Create a new standalone component. Optionally derive it from a previous component version. - operationId: create-component + description: Opens the payment provider's hosted page where the session's customer saves a payment method, for its invoices to be charged automatically. Send the customer to url; when it comes back, with kaiten_setup_session in the query, complete the session. Kaiten never sees card data. + operationId: createSessionPaymentMethodSession requestBody: content: application/json: schema: - $ref: "#/components/schemas/Component" + $ref: "#/components/schemas/NewSessionPaymentMethodSession" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/Component" - description: Created + $ref: "#/components/schemas/PaymentMethodSession" + description: OK "400": content: application/problem+json: @@ -3338,6 +30273,18 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: @@ -3350,29 +30297,38 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - write:components - summary: Create a new component + - customerSession: [] + summary: Open a page to save a payment method tags: - - components - /components/{componentSlug}: - delete: - description: Delete a component by slug if it is not linked to any release. - operationId: delete-component + - public + /public/session/billing/payment-method-session/{sessionId}/complete: + post: + description: Checks the setup session with the payment provider and makes its payment method the one the customer's invoices are charged to. Safe to repeat. A session of another customer answers not found; one the customer has not completed, a conflict. When the browser never comes back, the provider sync adopts the payment method within one interval. + operationId: completeSessionPaymentMethodSession parameters: - - description: Component slug + - description: "The setup session's id: the kaiten_setup_session the customer came back with" in: path - name: componentSlug + name: sessionId required: true schema: - description: Component slug - examples: - - api-gateway-v1-2-3 + description: "The setup session's id: the kaiten_setup_session the customer came back with" + maxLength: 255 + minLength: 1 type: string responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CompletedPaymentMethodSession" + description: OK "400": content: application/problem+json: @@ -3409,38 +30365,64 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - write:components - summary: Delete a component + - customerSession: [] + summary: Complete a payment method page tags: - - components - get: - description: Returns a single component by its slug. - operationId: get-component - parameters: - - description: Component slug - in: path - name: componentSlug - required: true - schema: - description: Component slug - examples: - - api-gateway-v1-2-3 - type: string + - public + /public/session/billing/portal-session: + post: + description: Opens the payment provider's hosted portal, where the session's customer manages its payment methods and billing details. What it changes there reaches Kaiten through the provider sync. + operationId: createSessionPortalSession + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewSessionPortalSession" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/Component" + $ref: "#/components/schemas/PortalSession" description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request "401": content: application/problem+json: @@ -3459,49 +30441,63 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - read:components - summary: Get a component by slug + - customerSession: [] + summary: Open the payment provider's customer portal tags: - - components - put: - description: Update a component in place when unlinked, or create a new version when it is already linked to releases. - operationId: update-component - parameters: - - description: Component slug - in: path - name: componentSlug - required: true - schema: - description: Component slug - examples: - - api-gateway-v1-2-3 - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Component" - required: true + - public + /public/session/billing/reactivate: + post: + description: "Takes back a cancellation scheduled for the period's end, before it: the subscription renews as before. 409 .NotScheduledForCancellation when none is scheduled (or the subscription has already ended), .BoundaryPending while the period that ended is being closed (Retry-After); 422 .InstanceRequired for a session bound to no instance." + operationId: reactivateSessionSubscription responses: "200": content: application/json: schema: - $ref: "#/components/schemas/Component" + $ref: "#/components/schemas/SessionSubscription" description: OK "400": content: @@ -3521,51 +30517,88 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found "409": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Conflict + headers: + Retry-After: + description: "On .BoundaryPending: seconds until the period's close is expected to have run" + schema: + minimum: 0 + type: integer "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - write:components - summary: Update a component + - customerSession: [] + summary: Keep a subscription scheduled for cancellation tags: - - components - /connectors: - get: - description: Returns every registered connector with its version and settings schema, ordered by name. The registry is platform-wide rather than per-organization and holds one row per connector, so the response is not paginated. - operationId: list-connectors + - public + /public/session/checkout: + post: + description: "Subscribes the session's instance to a price of the public catalogue, with add-ons and a voucher, in one call. With dryRun, answers 200 status=preview and what would be billed today, changing nothing. When a payment method has to be saved first, answers 200 status=requires_payment_method with a setup page: send the customer there, then call again with its setupSessionId. Otherwise answers 201 status=subscribed: the invoice of the first period has been pushed and charged, and payment.status says how that went -- poll GET /public/session/invoices on processing or requires_action. Needs a session bound to an instance. An instance whose live subscription is free (a FREE licence family, no add-on price above 0) is checked out too: that subscription is canceled at once, with no invoice, in the same transaction. 409 .AlreadySubscribed (any other live subscription), .SetupSessionIncomplete (no payment method was saved on the setup page); 422 .InstanceRequired, .NotSelfServe, .PriceNotPublic, .PriceNotFlatFee, .BillingEmailRequired, .InvalidReturnUrl, .ProviderCannotCapturePayment, .VoucherInvalid, .AddonInvalid, .AddonNotPublic, .QuantityExceedsMax, .CurrencyMismatch, .QuotaExceeded (the instance uses more than the plan allows: errors[0].value.entitlementSlugs, location body.licensePriceId); 503 .ProviderUnavailable (Retry-After)." + operationId: createSessionCheckout + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionCheckoutOrder" + required: true responses: "200": content: application/json: schema: - items: - $ref: "#/components/schemas/Connector" - type: - - array - - "null" - description: OK + $ref: "#/components/schemas/SessionCheckout" + description: "A preview (dryRun), or requires_payment_method with the hosted setup page: nothing was subscribed" + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionCheckout" + description: Created + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request "401": content: application/problem+json: @@ -3578,38 +30611,81 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Internal Server Error + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - read:organizations - summary: List registered connectors + - customerSession: [] + summary: Check out tags: - - connectors - /connectors/{connectorName}: + - public + /public/session/invoices: get: - description: Retrieves a registered connector resource including version and settings schema - operationId: get-connector + description: "The session's customer's issued invoices -- its instance's, for a session bound to one -- newest first, with how to pay each. An invoice still awaiting the outcome of its payment is read from the payment provider first, at most every 30 seconds: poll this after a checkout answered processing or requires_action." + operationId: listSessionInvoices parameters: - - description: Stable connector name - in: path - name: connectorName - required: true + - description: Opaque cursor from the previous page's nextCursor + explode: false + in: query + name: cursor schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio + description: Opaque cursor from the previous page's nextCursor type: string + - description: Page size, 50 by default, 200 at most + explode: false + in: query + name: limit + schema: + description: Page size, 50 by default, 200 at most + format: int32 + maximum: 200 + minimum: 0 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/Connector" + $ref: "#/components/schemas/PageSessionInvoice" description: OK "400": content: @@ -3629,18 +30705,24 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": + "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Not Found - "422": + description: Unprocessable Entity + "429": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unprocessable Entity + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: @@ -3648,34 +30730,21 @@ paths: $ref: "#/components/schemas/Problem" description: Internal Server Error security: - - bearerAuth: - - read:organizations - summary: Get connector registration + - customerSession: [] + summary: List the customer's invoices tags: - - connectors - /connectors/{connectorName}/activation: - delete: - description: "Deactivates a connector for the authenticated organization. Idempotent, and never refused for an unregistered or unlicensed connector: an organization must always be able to stop using something." - operationId: deactivate-connector - parameters: - - description: Stable connector name - in: path - name: connectorName - required: true - schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio - type: string + - public + /public/session/portal: + get: + description: "Everything the customer's portal shows, in one read: the customer, and for a session bound to an instance its subscription, effective quotas with where they come from, add-ons, vouchers and next invoice; the payment method; and what the portal may offer. A session bound to the customer only gets the customer, its instances and the payment method." + operationId: getSessionPortal responses: - "204": - description: No Content - "400": + "200": content: - application/problem+json: + application/json: schema: - $ref: "#/components/schemas/Problem" - description: Bad Request + $ref: "#/components/schemas/SessionPortal" + description: OK "401": content: application/problem+json: @@ -3688,44 +30757,58 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "422": + "429": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unprocessable Entity + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - write:organizations - summary: Deactivate connector + - customerSession: [] + summary: Read the customer portal tags: - - connectors - put: - description: "Activates a registered connector for the authenticated organization. Idempotent: activating an already-active connector succeeds without changing when it was first activated. Refused when the organization's licence does not include the connector." - operationId: activate-connector - parameters: - - description: Stable connector name - in: path - name: connectorName - required: true - schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio - type: string + - public + /public/session/vouchers/redeem: + post: + description: "Redeems a code on the session's instance, whose subscription is TRIAL, ACTIVE or PAST_DUE: 201 with the redemption, never the code; dryRun answers 200 with the redemption it would make and writes nothing. A PRICE voucher discounts from the next invoice composed (appliesFromInvoice: next), never one already issued; a boost applies at once. Every refusal of the code is the same 422 .VoucherInvalid. 409 .SubscriptionIncomplete (a checkout waits for its payment), .NoLiveSubscription; 422 .InstanceRequired (a session bound to the customer only); 429 .RateLimited, with Retry-After, past 10 attempts per 10 minutes per session or 30 an hour per customer, validate's included." + operationId: redeemSessionVoucher + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionRedeem" + required: true responses: - "200": + "201": content: application/json: schema: - $ref: "#/components/schemas/ConnectorActivation" - description: OK + $ref: "#/components/schemas/SessionRedemption" + description: Created "400": content: application/problem+json: @@ -3744,18 +30827,30 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": + "409": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Not Found + description: Conflict "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: @@ -3768,29 +30863,34 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - write:organizations - summary: Activate connector + - customerSession: [] + summary: Redeem a voucher code tags: - - connectors - /connectors/{connectorName}/settings: - delete: - description: Delete connector settings for the authenticated organization - operationId: delete-connector-settings - parameters: - - description: Stable connector name - in: path - name: connectorName - required: true - schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio - type: string + - public + /public/session/vouchers/validate: + post: + description: "Says whether a code would apply to the session's customer -- and its instance, and the price about to be checked out when given -- with what it gives. Every reason it would not gets the same {valid: false}. Nothing is redeemed: checkout redeems. 429 .RateLimited, with Retry-After, past 10 checks per 10 minutes per session or 30 an hour per customer." + operationId: validateSessionVoucher + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionVoucherCheck" + required: true responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionVoucherValidity" + description: OK "400": content: application/problem+json: @@ -3815,37 +30915,76 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity + "429": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt + schema: + minimum: 0 + type: integer "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - - bearerAuth: - - write:organizations - summary: Delete connector settings + - customerSession: [] + summary: Check a voucher code tags: - - connectors + - public + /publishable-keys: get: - description: Get settings for a connector scoped to the authenticated organization. Write-only fields (flagged `writeOnly` in the connector settings schema) are returned redacted as `***`. - operationId: get-connector-settings + description: The organization's publishable keys, newest first. The keys themselves are never returned, only their last four characters. Requires billing to be enabled for the organization. + operationId: listPublishableKeys parameters: - - description: Stable connector name - in: path - name: connectorName - required: true + - description: Also list revoked keys + explode: false + in: query + name: includeRevoked schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio + description: Also list revoked keys + type: boolean + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ConnectorSettings" + $ref: "#/components/schemas/PagePublishableKey" description: OK "400": content: @@ -3865,12 +31004,6 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found "422": content: application/problem+json: @@ -3883,39 +31016,40 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - read:organizations - summary: Get connector settings + - read:publishable_keys + summary: List publishable keys tags: - - connectors - put: - description: Create or update connector settings for the authenticated organization. Write-only fields (flagged `writeOnly` in the connector settings schema) may be omitted or sent as `***` to keep the stored value; they are returned redacted. - operationId: update-connector-settings - parameters: - - description: Stable connector name - in: path - name: connectorName - required: true - schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio - type: string + - publishable-keys + post: + description: Issues a pk_ key for a web page to read the organization's public catalogue (GET /api/public/catalog) and, with anonymousCheckout, to start anonymous checkouts (POST /api/public/checkout-sessions). The key is returned once; only its digest is stored. anonymousCheckout needs a Turnstile site key and secret (422 .TurnstileNotConfigured); the secret is stored in Vault (422 .VaultNotConfigured without one) and never returned. Requires billing to be enabled for the organization. + operationId: createPublishableKey requestBody: content: application/json: schema: - $ref: "#/components/schemas/ConnectorSettings" - description: Connector settings payload to upsert + $ref: "#/components/schemas/PublishableKeyDraft" required: true responses: - "200": + "201": content: application/json: schema: - $ref: "#/components/schemas/ConnectorSettings" - description: OK + $ref: "#/components/schemas/PublishableKeyCreated" + description: Created "400": content: application/problem+json: @@ -3934,50 +31068,60 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": + "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Not Found - "422": + description: Unprocessable Entity + "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": + description: Internal Server Error + "503": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Internal Server Error + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:organizations - summary: Upsert connector settings + - write:publishable_keys + summary: Create a publishable key tags: - - connectors - /connectors/{connectorName}/settings/schema: - get: - description: Get the JSON schema used to validate connector settings payloads - operationId: get-connector-settings-schema + - publishable-keys + /publishable-keys/{keyId}: + patch: + description: Changes a live key's label, allowed origins or anonymous checkout settings. An absent member keeps the stored value; an empty Turnstile value clears it. After the write, a key with anonymousCheckout must hold a Turnstile site key and secret, else 422 .TurnstileNotConfigured; a secret on a deployment without Vault answers 422 .VaultNotConfigured. A revoked key answers 409 UpdatePublishableKey.Revoked. Requires billing to be enabled for the organization. + operationId: updatePublishableKey parameters: - - description: Stable connector name + - description: Publishable key id in: path - name: connectorName + name: keyId required: true schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio + description: Publishable key id type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PublishableKeyPatch" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ConnectorSettingsSchema" + $ref: "#/components/schemas/PublishableKey" description: OK "400": content: @@ -4003,6 +31147,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -4015,32 +31165,42 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - read:organizations - summary: Get connector settings schema + - write:publishable_keys + summary: Update a publishable key tags: - - connectors - /connectors/{connectorName}/state: - get: - description: "Returns whether the connector is registered in this deployment, whether the authenticated organization's licence includes it, and whether the organization has activated it. Answers 200 with `available: false` for a connector this deployment does not have." - operationId: get-connector-state + - publishable-keys + /publishable-keys/{keyId}/revoke: + post: + description: The key stops authenticating on the next request, its Turnstile secret is deleted, and its anonymous checkout sessions stop authenticating. Idempotent; revocation is final. Requires billing to be enabled for the organization. + operationId: revokePublishableKey parameters: - - description: Stable connector name + - description: Publishable key id in: path - name: connectorName + name: keyId required: true schema: - description: Stable connector name - examples: - - kaiten.integration.crm.attio + description: Publishable key id type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ConnectorState" + $ref: "#/components/schemas/PublishableKey" description: OK "400": content: @@ -4060,6 +31220,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "422": content: application/problem+json: @@ -4078,16 +31244,22 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - read:organizations - summary: Get connector state for this organization + - write:publishable_keys + summary: Revoke a publishable key tags: - - connectors - /customers: + - publishable-keys + /releases: get: - description: Returns a cursor-paginated page of customers for the current organization. - operationId: list-customers + description: Returns a cursor-paginated page of releases for the current organization. + operationId: list-releases parameters: - description: Opaque pagination cursor from a previous response's nextCursor explode: false @@ -4111,7 +31283,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PageCustomer" + $ref: "#/components/schemas/PageRelease" description: OK "400": content: @@ -4145,25 +31317,25 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:customers - summary: List customers + - read:releases + summary: List releases tags: - - customers + - releases post: - description: Create a new customer with the provided details. - operationId: create-customer + description: "Create a new release with the provided details and link the requested components. A release is immutable: there is no update endpoint, and the component list cannot change afterwards. Correcting one means deleting it and creating a new one, which starts a new deployment history." + operationId: create-release requestBody: content: application/json: schema: - $ref: "#/components/schemas/Customer" + $ref: "#/components/schemas/Release" required: true responses: "201": content: application/json: schema: - $ref: "#/components/schemas/Customer" + $ref: "#/components/schemas/Release" description: Created "400": content: @@ -4213,21 +31385,31 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:customers - summary: Create a new customer + - write:releases + summary: Create a new release tags: - - customers - /customers/{customerSlug}: + - releases + /releases/{releaseSlug}: delete: - description: Delete a customer by their slug. - operationId: delete-customer + description: Delete a release by their slug. Since a release is immutable, this plus a fresh create is the only way to change one -- and it discards the deployment history recorded against the old release. A release still deployed to a zone cannot be deleted (409). + operationId: delete-release parameters: - - in: path - name: customerSlug + - description: Release slug + in: path + name: releaseSlug required: true schema: + description: Release slug + examples: + - release-slug type: string responses: "204": @@ -4256,118 +31438,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:customers - summary: Delete a customer - tags: - - customers - get: - description: Returns a single customer by their slug. - operationId: get-customer - parameters: - - in: path - name: customerSlug - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/Customer" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:customers - summary: Get a customer by slug - tags: - - customers - put: - description: Update a customer's details. Slug and integrations are not renameable/settable through this endpoint -- omit them, or send the values already on record. - operationId: update-customer - parameters: - - in: path - name: customerSlug - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Customer" - required: true - responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": + "409": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Not Found + description: Conflict "422": content: application/problem+json: @@ -4382,42 +31458,30 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:customers - summary: Update a customer + - write:releases + summary: Delete a release tags: - - customers - /customers/{customerSlug}/integrations/{integrationName}: - delete: - description: Delete a single integration for a customer - operationId: delete-customer-integration + - releases + get: + description: Returns a single release by their slug. + operationId: get-release-by-slug parameters: - - description: Customer slug - in: path - name: customerSlug - required: true - schema: - description: Customer slug - examples: - - acme-corp - type: string - - description: Integration adapter name + - description: Release slug in: path - name: integrationName + name: releaseSlug required: true schema: - description: Integration adapter name + description: Release slug examples: - - kaiten.integration.crm.attio - type: string - responses: - "204": - description: No Content - "400": + - release-slug + type: string + responses: + "200": content: - application/problem+json: + application/json: schema: - $ref: "#/components/schemas/Problem" - description: Bad Request + $ref: "#/components/schemas/Release" + description: OK "401": content: application/problem+json: @@ -4450,39 +31514,31 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:customers - summary: Delete a customer integration + - read:releases + summary: Get a release by slug tags: - - customers + - deploymentZones + /roles: get: - description: Retrieve a single integration for a customer - operationId: get-customer-integration - parameters: - - description: Customer slug - in: path - name: customerSlug - required: true - schema: - description: Customer slug - examples: - - acme-corp - type: string - - description: Integration adapter name - in: path - name: integrationName - required: true - schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio - type: string + description: Requires a live human member with stored read:users (write:users implies read). Lists immutable built-in templates and current organization roles. + operationId: ListRoles responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CustomerIntegration" + items: + $ref: "#/components/schemas/Role" + type: + - array + - "null" description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request "401": content: application/problem+json: @@ -4501,6 +31557,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -4515,45 +31577,25 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:customers - summary: Get a customer integration + - read:users + summary: List organization roles tags: - - customers + - users post: - description: Create a single integration for a customer - operationId: create-customer-integration - parameters: - - description: Customer slug - in: path - name: customerSlug - required: true - schema: - description: Customer slug - examples: - - acme-corp - type: string - - description: Integration adapter name - in: path - name: integrationName - required: true - schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio - type: string + description: Requires a live human member with stored write:users who covers current and proposed permissions. Built-in roles are immutable but assignable within caller authority; the last enabled administrator is protected. + operationId: CreateRole requestBody: content: application/json: schema: - $ref: "#/components/schemas/CustomerIntegration" - description: Integration payload + $ref: "#/components/schemas/CreateRole" required: true responses: "201": content: application/json: schema: - $ref: "#/components/schemas/CustomerIntegration" + $ref: "#/components/schemas/Role" description: Created "400": content: @@ -4599,45 +31641,92 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:customers - summary: Create a customer integration + - write:users + summary: Create a custom organization role tags: - - customers - put: - description: Update a single integration for a customer - operationId: update-customer-integration + - users + /roles/{slug}: + delete: + description: Requires a live human member with stored write:users who covers current and proposed permissions. Built-in roles are immutable but assignable within caller authority; the last enabled administrator is protected. + operationId: DeleteRole parameters: - - description: Customer slug - in: path - name: customerSlug + - in: path + name: slug required: true schema: - description: Customer slug - examples: - - acme-corp type: string - - description: Integration adapter name - in: path - name: integrationName + responses: + "204": + description: No Content + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + security: + - bearerAuth: + - write:users + summary: Delete an unassigned custom organization role + tags: + - users + put: + description: Requires a live human member with stored write:users who covers current and proposed permissions. Built-in roles are immutable but assignable within caller authority; the last enabled administrator is protected. + operationId: UpdateRole + parameters: + - in: path + name: slug required: true schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/CustomerIntegration" - description: Integration payload + $ref: "#/components/schemas/RoleDefinition" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CustomerIntegration" + $ref: "#/components/schemas/Role" description: OK "400": content: @@ -4683,14 +31772,14 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:customers - summary: Update a customer integration + - write:users + summary: Replace a custom organization role tags: - - customers - /deployment-zones: + - users + /service-accounts: get: - description: Returns a cursor-paginated page of deployment zones for the current organization. A deployment zone is one place a customer runs a release -- a target they name and classify themselves, holding the release currently on it. It is not an installation of Kaiten; that sense of the term belongs to operations and is not reachable through this API. - operationId: list-deployment-zones + description: Returns a cursor-paginated page of service accounts (machine users) for the current organization. + operationId: get-service-accounts parameters: - description: Opaque pagination cursor from a previous response's nextCursor explode: false @@ -4714,7 +31803,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PageDeploymentZone" + $ref: "#/components/schemas/PageServiceAccount" description: OK "400": content: @@ -4734,6 +31823,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "422": content: application/problem+json: @@ -4748,25 +31843,27 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:deployment_zones - summary: List deployment zones + - read:tokens + summary: List all service accounts tags: - - deploymentZones + - service-accounts post: - description: Create a new deployment zone with the provided details. Supplying releaseId records the zone's first deployment. A deployment zone is one place a customer runs a release -- a target they name and classify themselves, holding the release currently on it. It is not an installation of Kaiten; that sense of the term belongs to operations and is not reachable through this API. - operationId: create-deployment-zone + description: Create a new service account (machine user) with the provided name. Service accounts can be used to generate API tokens for programmatic access. + operationId: create-service-account requestBody: content: application/json: schema: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/ServiceAccount" + description: Service account details required: true responses: "201": content: application/json: schema: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/ServiceAccount" + description: Created service account description: Created "400": content: @@ -4786,6 +31883,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "409": content: application/problem+json: @@ -4810,83 +31913,46 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable - security: - - bearerAuth: - - write:deployment_zones - summary: Create a new deployment zone - tags: - - deploymentZones - /deployment-zones/{deploymentZoneSlug}: - delete: - description: Delete a deployment zone by their slug. - operationId: delete-deployment-zone - parameters: - - in: path - name: deploymentZoneSlug - required: true - schema: - type: string - responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error + minimum: 0 + type: integer security: - bearerAuth: - - write:deployment_zones - summary: Delete a deployment zone + - write:tokens + summary: Create a new service account tags: - - deploymentZones + - service-accounts + /service-accounts/{serviceAccountSlug}: get: - description: Returns a single deployment zone by their slug. - operationId: get-deployment-zone-by-slug + description: Retrieve details of a specific service account (machine user) by its slug. + operationId: get-service-account parameters: - - in: path - name: deploymentZoneSlug + - description: Service account slug + in: path + name: serviceAccountSlug required: true schema: + description: Service account slug + examples: + - service-account-slug type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/ServiceAccount" + description: Service account details description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request "401": content: application/problem+json: @@ -4919,24 +31985,29 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:deployment_zones - summary: Get a deployment zone by slug + - read:tokens + summary: Get a service account by slug tags: - - deploymentZones + - service-accounts put: - description: "Update a deployment zone's details. Changing releaseId records a deployment: the zone's current release becomes the new one and the change is appended to its deployment history, including when the new release is one the zone already ran (a rollback). Omitting releaseId, or echoing back the current one, leaves the history untouched." - operationId: update-deploymentZone + description: Update the name of a service account (machine user) by its slug. + operationId: update-service-account parameters: - - in: path - name: deploymentZoneSlug + - description: Service account slug + in: path + name: serviceAccountSlug required: true schema: + description: Service account slug + examples: + - service-account-slug type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/ServiceAccount" + description: Service account update details required: true responses: "204": @@ -4979,15 +32050,24 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:deployment_zones - summary: Update a deployment zone + - write:tokens + summary: Update a service account tags: - - deploymentZones - /entitlement-groups: + - service-accounts + /service-accounts/{serviceAccountSlug}/tokens: get: - description: Returns a cursor-paginated page of entitlement groups for the current organization. - operationId: list-entitlement-groups + description: Returns a cursor-paginated page of API tokens associated with a service account. The organization context is derived from the authenticated user's JWT token. Token secret values are never returned, only metadata. + operationId: get-service-account-tokens parameters: + - description: Service account slug + in: path + name: serviceAccountSlug + required: true + schema: + description: Service account slug + examples: + - service-account-slug + type: string - description: Opaque pagination cursor from a previous response's nextCursor explode: false in: query @@ -5010,7 +32090,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PageEntitlementGroup" + $ref: "#/components/schemas/PageToken" description: OK "400": content: @@ -5030,6 +32110,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "422": content: application/problem+json: @@ -5044,25 +32130,38 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:entitlements - summary: List all entitlement groups + - read:tokens + summary: List all tokens for a service account tags: - - entitlement-groups + - service-accounts post: - description: Create a new entitlement group to aggregate entitlements - operationId: create-entitlement-group + description: Create a new API token for a service account. The token value is only returned once upon creation and cannot be retrieved again. The organization context is derived from the authenticated user's JWT token. + operationId: create-service-account-token + parameters: + - description: Service account slug + in: path + name: serviceAccountSlug + required: true + schema: + description: Service account slug + examples: + - service-account-slug + format: text + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/EntitlementGroup" + $ref: "#/components/schemas/PlainToken" + description: Token creation details required: true responses: "201": content: application/json: schema: - $ref: "#/components/schemas/EntitlementGroup" + $ref: "#/components/schemas/PlainToken" + description: Created token with secret value (only shown once) description: Created "400": content: @@ -5082,6 +32181,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "409": content: application/problem+json: @@ -5106,21 +32211,42 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:entitlements - summary: Create a new entitlement group + - write:tokens + summary: Create a new token for a service account tags: - - entitlement-groups - /entitlement-groups/{entitlementGroupSlug}: + - service-accounts + /service-accounts/{serviceAccountSlug}/tokens/{tokenSlug}: delete: - description: Delete an entitlement group by its slug. This does not delete the member entitlements. - operationId: delete-entitlement-group + description: Revoke an API token for a service account. Once revoked, the token can no longer be used for authentication. The organization context is derived from the authenticated user's JWT token. + operationId: delete-service-account-token parameters: - - in: path - name: entitlementGroupSlug + - description: Service account slug + in: path + name: serviceAccountSlug + required: true + schema: + description: Service account slug + examples: + - service-account-slug + format: text + type: string + - description: Token slug to revoke + in: path + name: tokenSlug required: true schema: + description: Token slug to revoke + examples: + - my-token-slug + format: text type: string responses: "204": @@ -5163,26 +32289,91 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Delete an entitlement group + - write:tokens + summary: Revoke a token for a service account tags: - - entitlement-groups + - service-accounts + /usage/reports/export: get: - description: Retrieve details of a specific entitlement group by its slug - operationId: get-entitlement-group + description: Streams every usage report of the organization in the range, across instances and entitlements, as CSV or NDJSON, ordered by reportedAt. Filters narrow it to one instance or one entitlement, including deleted ones by ID. Use it to keep the usage history before deleting the organization. + operationId: exportOrganizationUsageReports parameters: - - in: path - name: entitlementGroupSlug - required: true + - description: Start of the range, inclusive (RFC 3339). Defaults to 30 days before to, moved up to the start of the organization's usage history when that is later. An explicit from before it answers 422 ExportUsageReports.OutsideRetention. + explode: false + in: query + name: from + schema: + description: Start of the range, inclusive (RFC 3339). Defaults to 30 days before to, moved up to the start of the organization's usage history when that is later. An explicit from before it answers 422 ExportUsageReports.OutsideRetention. + format: date-time + type: string + - description: End of the range, exclusive (RFC 3339). Defaults to now. At most 31 days after from. + explode: false + in: query + name: to + schema: + description: End of the range, exclusive (RFC 3339). Defaults to now. At most 31 days after from. + format: date-time + type: string + - description: "csv (the default): RFC 4180 with a header row. json: NDJSON, one report per line. Anything else answers 422 ExportUsageReports.InvalidFormat." + explode: false + in: query + name: format + schema: + description: "csv (the default): RFC 4180 with a header row. json: NDJSON, one report per line. Anything else answers 422 ExportUsageReports.InvalidFormat." + examples: + - csv + type: string + - description: Only this instance's reports + explode: false + in: query + name: instanceSlug + schema: + description: Only this instance's reports + type: string + - description: Only this instance's reports. Reaches a deleted instance, whose reports are kept. + explode: false + in: query + name: instanceId + schema: + description: Only this instance's reports. Reaches a deleted instance, whose reports are kept. + format: uuid + type: string + - description: Only this entitlement's reports + explode: false + in: query + name: entitlementSlug + schema: + description: Only this entitlement's reports + type: string + - description: Only this entitlement's reports. Reaches a deleted entitlement, whose reports are kept. + explode: false + in: query + name: entitlementId schema: + description: Only this entitlement's reports. Reaches a deleted entitlement, whose reports are kept. + format: uuid type: string responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/EntitlementGroup" - description: OK + type: string + text/csv: + schema: + type: string + description: "The reports, as an attachment: CSV with a header row for format=csv, one JSON object per line for format=json" + headers: + Content-Disposition: + description: attachment, with a file name naming the range + schema: + type: string + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request "401": content: application/problem+json: @@ -5215,28 +32406,36 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:entitlements - summary: Get entitlement group by slug + - read:instances + summary: Export the organization's usage reports tags: - - entitlement-groups - put: - description: Update an existing entitlement group with the provided details - operationId: update-entitlement-group + - instances + /users: + get: + description: Requires a live human member with stored read:users (write:users implies read). Provider authorization claims are ignored. + operationId: ListUsers parameters: - - in: path - name: entitlementGroupSlug - required: true + - explode: false + in: query + name: limit + schema: + default: 50 + format: int32 + maximum: 200 + minimum: 1 + type: integer + - explode: false + in: query + name: cursor schema: type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/EntitlementGroup" - required: true responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageManagedUser" + description: OK "400": content: application/problem+json: @@ -5261,6 +32460,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -5275,29 +32480,28 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Update an entitlement group + - read:users + summary: List human users tags: - - entitlement-groups - /entitlement-groups/{entitlementGroupSlug}/entitlements: - post: - description: Add an existing entitlement as a member of an entitlement group - operationId: add-entitlement-to-group + - users + /users/{id}: + get: + description: Requires a live human member with stored read:users (write:users implies read). Provider authorization claims are ignored. + operationId: GetUser parameters: - in: path - name: entitlementGroupSlug + name: id required: true schema: + format: uuid type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Command" - required: true responses: - "201": - description: Created + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ManagedUser" + description: OK "400": content: application/problem+json: @@ -5342,25 +32546,26 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Add an entitlement to a group + - read:users + summary: Get a human user tags: - - entitlement-groups - /entitlement-groups/{entitlementGroupSlug}/entitlements/{entitlementSlug}: - delete: - description: Remove an entitlement from an entitlement group. The entitlement itself is not deleted. - operationId: remove-entitlement-from-group + - users + put: + description: Requires a live human member with stored write:users who covers the target's current authority. Profile name and email are global across memberships and become locally managed. + operationId: UpdateUser parameters: - in: path - name: entitlementGroupSlug - required: true - schema: - type: string - - in: path - name: entitlementSlug + name: id required: true schema: + format: uuid type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Profile" + required: true responses: "204": description: No Content @@ -5388,6 +32593,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -5402,38 +32613,33 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Remove an entitlement from a group + - write:users + summary: Update a human profile tags: - - entitlement-groups - /entitlement-groups/{entitlementGroupSlug}/usage: - get: - description: Retrieve the aggregated usage metrics of all member entitlements for a given instance - operationId: get-entitlement-group-usage + - users + /users/{id}/assignments: + patch: + description: Requires a live human member with stored write:users who covers current and proposed permissions. Built-in roles are immutable but assignable within caller authority; the last enabled administrator is protected. + operationId: SetUserAssignments parameters: - in: path - name: entitlementGroupSlug - required: true - schema: - type: string - - description: Instance slug to get usage for - explode: false - in: query - name: instance + name: id required: true schema: - description: Instance slug to get usage for + format: uuid type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UserRoleAssignments" + required: true responses: "200": content: application/json: schema: - items: - $ref: "#/components/schemas/EntitlementGroupUsage" - type: - - array - - "null" + $ref: "#/components/schemas/UserAuthorization" description: OK "400": content: @@ -5459,6 +32665,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -5473,38 +32685,33 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:entitlements - summary: Get aggregated usage for an entitlement group + - write:users + summary: Set user roles and direct scopes atomically tags: - - entitlement-groups - /entitlements: - get: - description: Returns a cursor-paginated page of entitlements for the current organization. - operationId: list-entitlements + - users + /users/{id}/authorization: + put: + description: Requires a live human member with stored write:users who covers current and proposed target authority. The last enabled administrator is protected. Provider authorization claims are ignored. + operationId: PutUserAuthorization parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor + - in: path + name: id + required: true schema: - description: Opaque pagination cursor from a previous response's nextCursor + format: uuid type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UserAuthorization" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/PageEntitlement" + $ref: "#/components/schemas/UserAuthorization" description: OK "400": content: @@ -5524,6 +32731,18 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -5538,26 +32757,34 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:entitlements - summary: List all entitlements + - write:users + summary: Replace user roles, permissions and enabled state tags: - - entitlements - post: - description: Create a new entitlement with the provided details - operationId: create-entitlement + - users + /users/{id}/enabled: + patch: + description: Requires a live human member with stored write:users who covers current and proposed permissions. Built-in roles are immutable but assignable within caller authority; the last enabled administrator is protected. + operationId: SetUserEnabled + parameters: + - in: path + name: id + required: true + schema: + format: uuid + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/Entitlement" + $ref: "#/components/schemas/UserEnablement" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/Entitlement" - description: Created + $ref: "#/components/schemas/UserAuthorization" + description: OK "400": content: application/problem+json: @@ -5600,31 +32827,63 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error - "503": + security: + - bearerAuth: + - write:users + summary: Set user enabled state atomically + tags: + - users + /v1/notification-preferences: + get: + description: "Every notifiable event, with this user's effective value per channel: their own choice where they made one, the catalog default otherwise. Events absent from the catalog of an installation never appear, which is what keeps high-volume system events out of the feed." + operationId: getNotificationPreferences + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PreferenceMatrix" + description: OK + "401": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Service Unavailable + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Create a new entitlement + - read:notifications + summary: Read the caller's notification preferences tags: - - entitlements - /entitlements/{entitlementSlug}: - delete: - description: Delete an entitlement by its slug - operationId: delete-entitlement - parameters: - - in: path - name: entitlementSlug - required: true - schema: - type: string + - notifications + put: + description: Stores the signed-in user's choices and answers with the whole matrix. A value that equals the catalog default is stored as no row at all, so a later change to that default still reaches this user. + operationId: putNotificationPreferences + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PreferenceChoices" + required: true responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PreferenceMatrix" + description: OK "400": content: application/problem+json: @@ -5643,12 +32902,6 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found "422": content: application/problem+json: @@ -5663,26 +32916,74 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Delete an entitlement + - write:notifications + summary: Set the caller's notification preferences tags: - - entitlements + - notifications + /v1/notifications: get: - description: Retrieve details of a specific entitlement by its slug - operationId: get-entitlement + description: Returns the signed-in user's notification feed, newest first, with the unread count. Notifications are a view over this organization's audit trail, narrowed to the events the user subscribes to; the id of a notification is the id of the audit trail entry it reports. + operationId: listNotifications parameters: - - in: path - name: entitlementSlug - required: true + - description: Which notifications to return (default all) + explode: false + in: query + name: status + schema: + description: Which notifications to return (default all) + enum: + - all + - unread + type: string + - description: "Only notifications about these kinds of object; repeat the parameter for several (default: all)" + explode: true + in: query + name: objectType + schema: + description: "Only notifications about these kinds of object; repeat the parameter for several (default: all)" + items: + enum: + - instance + - customer + - release + - deployment_zone + - component + - license + - token + type: string + type: + - array + - "null" + - description: Opaque cursor from a previous response's next_cursor + explode: false + in: query + name: cursor schema: + description: Opaque cursor from a previous response's next_cursor type: string + - description: Page size (default 20, max 50) + explode: false + in: query + name: limit + schema: + description: Page size (default 20, max 50) + format: int32 + maximum: 50 + minimum: 1 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/Entitlement" + $ref: "#/components/schemas/List" description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request "401": content: application/problem+json: @@ -5695,12 +32996,6 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found "422": content: application/problem+json: @@ -5715,28 +33010,27 @@ paths: description: Internal Server Error security: - bearerAuth: - - read:entitlements - summary: Get entitlement by slug + - read:notifications + summary: List the caller's notifications tags: - - entitlements - put: - description: Update an existing entitlement with the provided details - operationId: update-entitlement - parameters: - - in: path - name: entitlementSlug - required: true - schema: - type: string + - notifications + /v1/notifications/mark-read: + post: + description: "Marks the given notifications read for the signed-in user, or all of them with all: true. Ids belonging to another organization are ignored rather than rejected, so a retry of a partially applied call is safe." + operationId: markNotificationsRead requestBody: content: application/json: schema: - $ref: "#/components/schemas/Entitlement" + $ref: "#/components/schemas/ReadSelection" required: true responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/MarkReadResult" + description: OK "400": content: application/problem+json: @@ -5755,18 +33049,6 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict "422": content: application/problem+json: @@ -5781,15 +33063,39 @@ paths: description: Internal Server Error security: - bearerAuth: - - write:entitlements - summary: Update an entitlement + - write:notifications + summary: Mark notifications read tags: - - entitlements - /feature-flags: + - notifications + /vouchers: get: - description: Returns a cursor-paginated page of feature flags for the given organization. - operationId: get-feature-flags + description: The organization's vouchers, newest first, with their codes. Requires billing to be enabled for the organization. + operationId: listVouchers parameters: + - explode: false + in: query + name: status + schema: + enum: + - DRAFT + - ACTIVE + - EXPIRED + - EXHAUSTED + - ARCHIVED + type: string + - explode: false + in: query + name: voucherType + schema: + enum: + - PRICE + - ENTITLEMENT_BOOST + type: string + - explode: false + in: query + name: restrictedCustomerSlug + schema: + type: string - description: Opaque pagination cursor from a previous response's nextCursor explode: false in: query @@ -5812,7 +33118,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PageFeatureFlag" + $ref: "#/components/schemas/PageVoucher" description: OK "400": content: @@ -5850,29 +33156,39 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - read:feature_flags - summary: Get a list of feature flags + - read:vouchers + summary: List vouchers tags: - - featureflags + - vouchers post: - description: Create a new featureflag with the provided details - operationId: create-feature-flag + description: "Creates a DRAFT voucher: a PRICE discount or an ENTITLEMENT_BOOST. The code is generated when none is given, and returned. Emits VOUCHER_CREATED, without the code. Requires billing to be enabled for the organization." + operationId: createVoucher requestBody: content: application/json: schema: - $ref: "#/components/schemas/FeatureFlag" - description: Feature flag details + $ref: "#/components/schemas/VoucherDraft" required: true responses: "201": content: application/json: schema: - $ref: "#/components/schemas/FeatureFlag" - description: Created feature flag + $ref: "#/components/schemas/Voucher" description: Created "400": content: @@ -5892,6 +33208,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "409": content: application/problem+json: @@ -5916,63 +33238,34 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable - security: - - bearerAuth: - - write:feature_flags - summary: Create a new featureflag - tags: - - featureflags - /feature-flags/targeting/context: - get: - description: "Returns the identifiers a targeting rule may read, with their types and descriptions, including the organization's own entitlement slugs. Intended for an editor: it is what autocomplete and hover documentation are built from. The set is not closed — a rule may also target on attributes the caller supplies in its own evaluation context." - operationId: get-targeting-context - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/TargetingContext" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "500": - content: - application/problem+json: + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error + minimum: 0 + type: integer security: - bearerAuth: - - read:feature_flags - summary: Get the targeting context schema + - write:vouchers + summary: Create a voucher tags: - - featureflags - /feature-flags/targeting/lint: + - vouchers + /vouchers/lookup: post: - description: "Runs the same check create and update run before accepting a targeting rule, and reports what it found without writing anything. A rule that references a fact or an entitlement that does not exist does not fail loudly at evaluation — it simply never matches — so this is what lets an editor say so while the rule is being written. A rejected rule is a 200 carrying `valid: false`, not a client error." - operationId: lint-targeting-rule + description: Finds the voucher a code names, matched without case or separators. The code travels in the body, never in a URL. Requires billing to be enabled for the organization. + operationId: lookupVoucher requestBody: content: application/json: schema: - $ref: "#/components/schemas/TargetingRuleDraft" + $ref: "#/components/schemas/VoucherCode" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/TargetingRuleVerdict" + $ref: "#/components/schemas/Voucher" description: OK "400": content: @@ -5992,6 +33285,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "422": content: application/problem+json: @@ -6004,28 +33303,40 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:feature_flags - summary: Check a targeting rule + - read:vouchers + summary: Find a voucher by code tags: - - featureflags - /feature-flags/targeting/test: + - vouchers + /vouchers/validate: post: - description: "Runs a rule the way an evaluation would, without a flag and without writing anything: the context is enriched from the same sources (license, entitlements, instance, customer, deployment zone), anything the caller forged under __kaiten is discarded the same way, and the same engine answers. The response carries the enriched facts so an author can see exactly what the rule read — which is usually the answer to why it did or did not match. Nothing is metered or audited: this is authoring, not serving." - operationId: test-targeting-rule + description: "Runs the redemption checks without redeeming: the voucher's own; with instanceSlug, the instance's; with licensePriceId, those that read the subscription, on the subscription that price would start. Answers 200 with valid, and the first failing reason (and eligibility rule) otherwise. 404 .InstanceNotFound, .PriceNotFound; 429 .RateLimited past 60 checks a minute per principal, with Retry-After. Requires billing to be enabled for the organization." + operationId: validateVoucher requestBody: content: application/json: schema: - $ref: "#/components/schemas/TargetingRuleTrial" + $ref: "#/components/schemas/VoucherCheck" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/TargetingRuleRehearsal" + $ref: "#/components/schemas/Validity" description: OK "400": content: @@ -6045,49 +33356,6 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:feature_flags - summary: Rehearse a targeting rule against a context - tags: - - featureflags - /feature-flags/{featureFlagSlug}: - delete: - description: Delete a featureflag with the provided details - operationId: delete-feature-flag - parameters: - - in: path - name: featureFlagSlug - required: true - schema: - type: string - responses: - "204": - description: No Content - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden "404": content: application/problem+json: @@ -6100,86 +33368,54 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:feature_flags - summary: Delete a featureflag - tags: - - featureflags - get: - description: Get a feature flag with the provided slug - operationId: get-feature-flag - parameters: - - in: path - name: featureFlagSlug - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/FeatureFlag" - description: OK - "401": + "429": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: + description: Too Many Requests + headers: + Retry-After: + description: Seconds to wait before the next attempt schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": + minimum: 0 + type: integer + "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Not Found - "422": + description: Internal Server Error + "503": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error + minimum: 0 + type: integer security: - bearerAuth: - - read:feature_flags - summary: Get a feature flag by slug + - read:voucher_redemptions + summary: Validate a voucher code tags: - - featureflags - put: - description: Update a featureflag with the provided details - operationId: update-feature-flag + - vouchers + /vouchers/{voucherId}: + delete: + description: "Deletes a voucher no instance ever redeemed, whatever its status, with its grants and flag grants: the entitlements and flags they held can be deleted again, and its code can be given to another voucher. A voucher redeemed at least once, or one a checkout waiting for its payment applies, is history (409 DeleteVoucher.Redeemed): archive it instead. Emits VOUCHER_DELETED. Requires billing to be enabled for the organization." + operationId: deleteVoucher parameters: - - in: path - name: featureFlagSlug + - description: Voucher id + in: path + name: voucherId required: true schema: + description: Voucher id type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/FeatureFlag" - description: Feature flag details - required: true responses: "204": description: No Content @@ -6207,6 +33443,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -6219,40 +33461,41 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:feature_flags - summary: Update a featureflag + - write:vouchers + summary: Delete a voucher tags: - - featureflags - /instances: + - vouchers get: - description: Returns a cursor-paginated page of instances for the current organization. - operationId: getInstances + description: One voucher, with its code. Requires billing to be enabled for the organization. + operationId: getVoucher parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor + - description: Voucher id + in: path + name: voucherId + required: true schema: - description: Opaque pagination cursor from a previous response's nextCursor + description: Voucher id type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/PageInstance" + $ref: "#/components/schemas/Voucher" description: OK "400": content: @@ -6272,6 +33515,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found "422": content: application/problem+json: @@ -6284,28 +33533,48 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - read:instances - summary: List all instances + - read:vouchers + summary: Get a voucher tags: - - instances - post: - description: Create a new instance with the provided details - operationId: createInstance + - vouchers + put: + description: Replaces a DRAFT voucher, but its type. An ACTIVE voucher takes a new name, description, expiresAt and maxRedemptions (at least redemptionsCount); its other members must stay as they are (409 UpdateVoucher.NotEditable). Emits VOUCHER_UPDATED. Requires billing to be enabled for the organization. + operationId: updateVoucher + parameters: + - description: Voucher id + in: path + name: voucherId + required: true + schema: + description: Voucher id + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/Instance" + $ref: "#/components/schemas/VoucherDraft" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/Instance" - description: Created + $ref: "#/components/schemas/Voucher" + description: OK "400": content: application/problem+json: @@ -6354,29 +33623,37 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:instances - summary: Create a new instance + - write:vouchers + summary: Update a voucher tags: - - instances - /instances/{instanceSlug}: - delete: - description: Delete an instance with the provided slug - operationId: deleteInstance + - vouchers + /vouchers/{voucherId}/archive: + post: + description: Stops a voucher from being redeemed. Redemptions already made keep applying until revoked. Emits VOUCHER_ARCHIVED. Requires billing to be enabled for the organization. + operationId: archiveVoucher parameters: - - description: Instance slug + - description: Voucher id in: path - name: instanceSlug + name: voucherId required: true schema: - description: Instance slug - examples: - - instance-slug + description: Voucher id type: string responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Voucher" + description: OK "400": content: application/problem+json: @@ -6401,102 +33678,61 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:instances - summary: Delete an instance - tags: - - instances - get: - description: Retrieve an instance by their unique slug - operationId: getInstance - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/Instance" - description: OK - "401": + "409": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": + description: Conflict + "422": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Forbidden - "404": + description: Unprocessable Entity + "500": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Not Found - "422": + description: Internal Server Error + "503": content: application/problem+json: schema: $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error + minimum: 0 + type: integer security: - bearerAuth: - - read:instances - summary: Get an instance by slug + - write:vouchers + summary: Archive a voucher tags: - - instances - patch: - description: Partially update an instance. Currently supports the operational status and the commercial lifecycle stage. - operationId: patchInstance + - vouchers + /vouchers/{voucherId}/publish: + post: + description: Makes a DRAFT voucher redeemable (409 PublishVoucher.NotADraft otherwise). Emits VOUCHER_PUBLISHED. Requires billing to be enabled for the organization. + operationId: publishVoucher parameters: - - description: Instance slug + - description: Voucher id in: path - name: instanceSlug + name: voucherId required: true schema: - description: Instance slug - examples: - - instance-slug + description: Voucher id type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/PatchInstanceBody" - required: true responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Voucher" + description: OK "400": content: application/problem+json: @@ -6521,6 +33757,12 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found + "409": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Conflict "422": content: application/problem+json: @@ -6533,34 +33775,60 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:instances - summary: Patch an instance + - write:vouchers + summary: Publish a voucher tags: - - instances - put: - description: Update an instance with the provided details - operationId: updateInstance + - vouchers + /vouchers/{voucherId}/redemptions: + get: + description: Every redemption of a voucher, newest first. Requires billing to be enabled for the organization. + operationId: listVoucherRedemptions parameters: - - description: Instance slug + - description: Voucher id in: path - name: instanceSlug + name: voucherId required: true schema: - description: Instance slug - examples: - - instance-slug + description: Voucher id type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Instance" - required: true + - description: Opaque pagination cursor from a previous response's nextCursor + explode: false + in: query + name: cursor + schema: + description: Opaque pagination cursor from a previous response's nextCursor + type: string + - description: Maximum number of entries to return (default 50, max 200) + explode: false + in: query + name: limit + schema: + description: Maximum number of entries to return (default 50, max 200) + format: int32 + maximum: 200 + minimum: 1 + type: integer responses: - "204": - description: No Content + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PageRedemption" + description: OK "400": content: application/problem+json: @@ -6585,12 +33853,6 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict "422": content: application/problem+json: @@ -6603,3585 +33865,2575 @@ paths: schema: $ref: "#/components/schemas/Problem" description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer security: - bearerAuth: - - write:instances - summary: Update an instance + - read:vouchers + summary: List a voucher's redemptions tags: - - instances - /instances/{instanceSlug}/audit-trails: - get: - description: "Returns audit trail events for a given instance, optionally filtered by event name and time range. Results are cursor-paginated (default limit: 50, max: 200) — pass the previous response's nextCursor as the cursor parameter to fetch the next page." - operationId: getAuditTrails - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Filter by exact event name (e.g. ENTITLEMENT_VALUE_GET) - explode: false - in: query - name: event_name - schema: - description: Filter by exact event name (e.g. ENTITLEMENT_VALUE_GET) - type: string - - description: RFC3339 timestamp — include entries with timestamp ≥ this value - explode: false - in: query - name: after - schema: - description: RFC3339 timestamp — include entries with timestamp ≥ this value - type: string - - description: RFC3339 timestamp — include entries with timestamp ≤ this value - explode: false - in: query - name: before - schema: - description: RFC3339 timestamp — include entries with timestamp ≤ this value - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string + - vouchers +security: + - bearerAuth: [] +servers: + - url: /api +webhooks: + com.kaiten.addon.entitlement.v1.assigned: + post: + description: Triggered when an add-on version grants an entitlement. + operationId: onAddonEntitlementAssigned + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonGrantUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_ENTITLEMENT_ASSIGNED + examples: + - ADDON_ENTITLEMENT_ASSIGNED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.entitlement.v1.assigned + examples: + - com.kaiten.addon.entitlement.v1.assigned + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Entitlement Assigned Webhook + tags: + - webhooks + - addons + com.kaiten.addon.entitlement.v1.unassigned: + post: + description: Triggered when an add-on grant is removed. + operationId: onAddonEntitlementUnassigned + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonGrantUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_ENTITLEMENT_UNASSIGNED + examples: + - ADDON_ENTITLEMENT_UNASSIGNED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.entitlement.v1.unassigned + examples: + - com.kaiten.addon.entitlement.v1.unassigned + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Entitlement Unassigned Webhook + tags: + - webhooks + - addons + com.kaiten.addon.entitlement.v1.updated: + post: + description: Triggered when an add-on grant changes. + operationId: onAddonEntitlementUpdated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonGrantUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_ENTITLEMENT_UPDATED + examples: + - ADDON_ENTITLEMENT_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.entitlement.v1.updated + examples: + - com.kaiten.addon.entitlement.v1.updated + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Entitlement Updated Webhook + tags: + - webhooks + - addons + com.kaiten.addon.price.v1.created: + post: + description: Triggered when a price is added to an add-on version. + operationId: onAddonPriceCreated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonPriceUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_PRICE_CREATED + examples: + - ADDON_PRICE_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.price.v1.created + examples: + - com.kaiten.addon.price.v1.created + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Price Created Webhook + tags: + - webhooks + - addons + com.kaiten.addon.price.v1.deprecated: + post: + description: Triggered when an add-on price stops being offered. + operationId: onAddonPriceDeprecated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonPriceUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_PRICE_DEPRECATED + examples: + - ADDON_PRICE_DEPRECATED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.price.v1.deprecated + examples: + - com.kaiten.addon.price.v1.deprecated + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Price Deprecated Webhook + tags: + - webhooks + - addons + com.kaiten.addon.v1.archived: + post: + description: Triggered when an add-on version is withdrawn from sale. + operationId: onAddonArchived + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_ARCHIVED + examples: + - ADDON_ARCHIVED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.v1.archived + examples: + - com.kaiten.addon.v1.archived + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Archived Webhook + tags: + - webhooks + - addons + com.kaiten.addon.v1.created: + post: + description: Triggered when an add-on version is created. + operationId: onAddonCreated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_CREATED + examples: + - ADDON_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.v1.created + examples: + - com.kaiten.addon.v1.created + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageAuditTrail" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:instances - summary: List audit trail entries for an instance + description: Webhook processed successfully + summary: Add-on Created Webhook tags: - - instances - /instances/{instanceSlug}/entitlements/usage: - get: - description: Retrieve a list of all entitlements usage metrics for a specific instance - operationId: getEntitlementsUsageMetrics - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string + - webhooks + - addons + com.kaiten.addon.v1.deleted: + post: + description: Triggered when an add-on version is deleted. + operationId: onAddonDeleted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/DeletedAddonDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_DELETED + examples: + - ADDON_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.v1.deleted + examples: + - com.kaiten.addon.v1.deleted + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - items: - $ref: "#/components/schemas/EntitlementUsage" - type: - - array - - "null" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:instances - summary: List all entitlements usage metrics + description: Webhook processed successfully + summary: Add-on Deleted Webhook tags: - - instances - /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage: - get: - description: Retrieve the usage metrics for a specific entitlement in a given instance - operationId: getEntitlementUsageMetrics - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Entitlement slug - in: path - name: entitlementSlug - required: true - schema: - description: Entitlement slug - examples: - - entitlement-slug - type: string + - webhooks + - addons + com.kaiten.addon.v1.published: + post: + description: Triggered when a draft add-on version goes on sale. + operationId: onAddonPublished + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_PUBLISHED + examples: + - ADDON_PUBLISHED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.v1.published + examples: + - com.kaiten.addon.v1.published + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/EntitlementUsage" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:instances - summary: Get entitlement usage metrics for an instance + description: Webhook processed successfully + summary: Add-on Published Webhook + tags: + - webhooks + - addons + com.kaiten.addon.v1.unarchived: + post: + description: Triggered when an archived add-on version goes back on sale. + operationId: onAddonUnarchived + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_UNARCHIVED + examples: + - ADDON_UNARCHIVED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.v1.unarchived + examples: + - com.kaiten.addon.v1.unarchived + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Add-on Unarchived Webhook tags: - - instances + - webhooks + - addons + com.kaiten.addon.v1.updated: post: - description: Report a usage metric for a specific entitlement in a given instance. This endpoint allows you to report the usage of an entitlement, including optional metadata and a timestamp. - operationId: reportEntitlementUsageMetric - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Entitlement slug - in: path - name: entitlementSlug - required: true - schema: - description: Entitlement slug - examples: - - entitlement-slug - type: string + description: Triggered when an add-on version, its compatibility or its family's visibility changes. + operationId: onAddonUpdated requestBody: content: application/json: schema: - $ref: "#/components/schemas/ReportEntitlementUsageBody" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/AddonUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ADDON_UPDATED + examples: + - ADDON_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.addon.v1.updated + examples: + - com.kaiten.addon.v1.updated + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/EntitlementUsage" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:instances - summary: Report entitlement usage metric for an instance + description: Webhook processed successfully + summary: Add-on Updated Webhook tags: - - instances - /instances/{instanceSlug}/integrations/{integrationName}: - delete: - description: Delete a single integration for an instance - operationId: delete-instance-integration - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Integration adapter name - in: path - name: integrationName - required: true - schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio - type: string + - webhooks + - addons + com.kaiten.billing_provider.v1.connected: + post: + description: Triggered when an organization connects a payment provider (activates its connector). + operationId: onBillingProviderConnected + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/ConnectionDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - BILLING_PROVIDER_CONNECTED + examples: + - BILLING_PROVIDER_CONNECTED + type: string + type: + description: Event type + enum: + - com.kaiten.billing_provider.v1.connected + examples: + - com.kaiten.billing_provider.v1.connected + type: string + required: + - name + - type + - data + type: object + required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:instances - summary: Delete an instance integration + "200": + description: Webhook processed successfully + summary: Billing Provider Connected Webhook tags: - - instances - get: - description: Retrieve a single integration for an instance - operationId: get-instance-integration - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Integration adapter name - in: path - name: integrationName - required: true - schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio - type: string + - webhooks + - billing + com.kaiten.billing_provider.v1.disconnected: + post: + description: Triggered when an organization disconnects a payment provider (deactivates its connector). + operationId: onBillingProviderDisconnected + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/ConnectionDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - BILLING_PROVIDER_DISCONNECTED + examples: + - BILLING_PROVIDER_DISCONNECTED + type: string + type: + description: Event type + enum: + - com.kaiten.billing_provider.v1.disconnected + examples: + - com.kaiten.billing_provider.v1.disconnected + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/InstanceIntegration" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:instances - summary: Get an instance integration + description: Webhook processed successfully + summary: Billing Provider Disconnected Webhook tags: - - instances + - webhooks + - billing + com.kaiten.billing_provider.v1.sync_failed: post: - description: Create a single integration for an instance - operationId: create-instance-integration - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Integration adapter name - in: path - name: integrationName - required: true - schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio - type: string + description: Triggered when syncing with a payment provider has failed three passes in a row, once per streak. + operationId: onBillingProviderSyncFailed requestBody: content: application/json: schema: - $ref: "#/components/schemas/InstanceIntegration" - description: Integration payload + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/SyncFailureDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - BILLING_PROVIDER_SYNC_FAILED + examples: + - BILLING_PROVIDER_SYNC_FAILED + type: string + type: + description: Event type + enum: + - com.kaiten.billing_provider.v1.sync_failed + examples: + - com.kaiten.billing_provider.v1.sync_failed + type: string + required: + - name + - type + - data + type: object required: true responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/InstanceIntegration" - description: Created - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:instances - summary: Create an instance integration + "200": + description: Webhook processed successfully + summary: Billing Provider Sync Failed Webhook tags: - - instances - put: - description: Update a single integration for an instance - operationId: update-instance-integration - parameters: - - description: Instance slug - in: path - name: instanceSlug - required: true - schema: - description: Instance slug - examples: - - instance-slug - type: string - - description: Integration adapter name - in: path - name: integrationName - required: true - schema: - description: Integration adapter name - examples: - - kaiten.integration.crm.attio - type: string + - webhooks + - billing + com.kaiten.checkout_session.v1.abandoned: + post: + description: "Triggered when an anonymous checkout ends unpaid: still OPEN 24 h after its start (status ABANDONED), or its INCOMPLETE subscription expired or was canceled (status EXPIRED, with the customer and instance to clean up)." + operationId: onCheckoutSessionAbandoned requestBody: content: application/json: schema: - $ref: "#/components/schemas/InstanceIntegration" - description: Integration payload + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/SessionEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CHECKOUT_SESSION_ABANDONED + examples: + - CHECKOUT_SESSION_ABANDONED + type: string + type: + description: Event type + enum: + - com.kaiten.checkout_session.v1.abandoned + examples: + - com.kaiten.checkout_session.v1.abandoned + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/InstanceIntegration" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:instances - summary: Update an instance integration + description: Webhook processed successfully + summary: Checkout Session Abandoned Webhook + tags: + - webhooks + - checkout-sessions + com.kaiten.checkout_session.v1.completed: + post: + description: "Triggered when an anonymous checkout completes: its ACTIVATION invoice is paid, or nothing was due. Carries the customer and instance it created; provision on this event, never on instance.v1.created." + operationId: onCheckoutSessionCompleted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/SessionEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CHECKOUT_SESSION_COMPLETED + examples: + - CHECKOUT_SESSION_COMPLETED + type: string + type: + description: Event type + enum: + - com.kaiten.checkout_session.v1.completed + examples: + - com.kaiten.checkout_session.v1.completed + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Checkout Session Completed Webhook tags: - - instances - /integration/{adapter}/customer/{externalId}: - get: - description: Resolve a customer through one of its integrations using adapter + external ID. - operationId: get-customer-integration-by-external-id - parameters: - - description: Integration adapter without the kaiten.integration. prefix - in: path - name: adapter - required: true - schema: - description: Integration adapter without the kaiten.integration. prefix - examples: - - crm.attio - type: string - - description: External identifier in the third-party system - in: path - name: externalId - required: true - schema: - description: External identifier in the third-party system - examples: - - rec_12345 - type: string + - webhooks + - checkout-sessions + com.kaiten.checkout_session.v1.started: + post: + description: Triggered when an anonymous checkout starts (POST /public/checkout-sessions). A dryRun emits nothing. + operationId: onCheckoutSessionStarted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/SessionEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CHECKOUT_SESSION_STARTED + examples: + - CHECKOUT_SESSION_STARTED + type: string + type: + description: Event type + enum: + - com.kaiten.checkout_session.v1.started + examples: + - com.kaiten.checkout_session.v1.started + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/CustomerIntegrationResource" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:customers - summary: Get a customer by integration external ID + description: Webhook processed successfully + summary: Checkout Session Started Webhook tags: - - integrations - - customers - patch: - description: Create or partially update a customer resolved through adapter + external ID. Missing fields are left unchanged. - operationId: upsert-customer-integration-by-external-id - parameters: - - description: Integration adapter without the kaiten.integration. prefix - in: path - name: adapter - required: true - schema: - description: Integration adapter without the kaiten.integration. prefix - examples: - - crm.attio - type: string - - description: External identifier in the third-party system - in: path - name: externalId - required: true - schema: - description: External identifier in the third-party system - examples: - - rec_12345 - type: string + - webhooks + - checkout-sessions + com.kaiten.component.v1.created: + post: + description: Triggered when a new component is created. + operationId: onComponentCreated requestBody: content: application/json: schema: - $ref: "#/components/schemas/CustomerBody" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/ComponentDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - COMPONENT_CREATED + examples: + - COMPONENT_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.component.v1.created + examples: + - com.kaiten.component.v1.created + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/CustomerIntegrationResource" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:customers - summary: Upsert a customer by integration external ID + description: Webhook processed successfully + summary: Component Created Webhook tags: - - integrations - - customers - /integration/{adapter}/instance/{externalId}: - get: - description: Resolve an instance through one of its integrations using adapter + external ID. - operationId: get-instance-integration-by-external-id - parameters: - - description: Integration adapter without the kaiten.integration. prefix - in: path - name: adapter - required: true - schema: - description: Integration adapter without the kaiten.integration. prefix - examples: - - crm.attio - type: string - - description: External identifier in the third-party system - in: path - name: externalId - required: true - schema: - description: External identifier in the third-party system - examples: - - rec_12345 - type: string + - webhooks + - components + com.kaiten.component.v1.deleted: + post: + description: Triggered when a component is deleted. + operationId: onComponentDeleted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/ComponentDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - COMPONENT_DELETED + examples: + - COMPONENT_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.component.v1.deleted + examples: + - com.kaiten.component.v1.deleted + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/InstanceIntegrationResource" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:instances - summary: Get an instance by integration external ID + description: Webhook processed successfully + summary: Component Deleted Webhook tags: - - integrations - - instances - patch: - description: Create or partially update an instance resolved through adapter + external ID. Missing fields are left unchanged. - operationId: upsert-instance-integration-by-external-id - parameters: - - description: Integration adapter without the kaiten.integration. prefix - in: path - name: adapter - required: true - schema: - description: Integration adapter without the kaiten.integration. prefix - examples: - - crm.attio - type: string - - description: External identifier in the third-party system - in: path - name: externalId - required: true - schema: - description: External identifier in the third-party system - examples: - - rec_12345 - type: string + - webhooks + - components + com.kaiten.component.v1.updated: + post: + description: Triggered when a component is updated or auto-versioned. + operationId: onComponentUpdated requestBody: content: application/json: schema: - $ref: "#/components/schemas/InstanceBody" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/ComponentDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - COMPONENT_UPDATED + examples: + - COMPONENT_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.component.v1.updated + examples: + - com.kaiten.component.v1.updated + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/InstanceIntegrationResource" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:instances - summary: Upsert an instance by integration external ID + description: Webhook processed successfully + summary: Component Updated Webhook tags: - - integrations - - instances - /license-families: - get: - description: "Returns a cursor-paginated page of license families for the current organization, each with the version it currently resolves to: the family's default version, or its highest-numbered published version. A family whose versions are all drafts or all archived is still listed, with no current version." - operationId: list-license-families - parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + - webhooks + - components + com.kaiten.customer.payment_method.v1.attached: + post: + description: Triggered when a customer's default payment method in a provider is set or replaced (a completed setup session, or a change in the provider's portal). Carries no card data. + operationId: onCustomerPaymentMethodAttached + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/PaymentMethodAttachedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_PAYMENT_METHOD_ATTACHED + examples: + - CUSTOMER_PAYMENT_METHOD_ATTACHED + type: string + type: + description: Event type + enum: + - com.kaiten.customer.payment_method.v1.attached + examples: + - com.kaiten.customer.payment_method.v1.attached + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageLicenseFamilyView" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:licenses - summary: List license families + description: Webhook processed successfully + summary: Customer Payment Method Attached Webhook + tags: + - webhooks + - billing + com.kaiten.customer.payment_method.v1.detached: + post: + description: Triggered when a customer no longer has a payment method in a provider. + operationId: onCustomerPaymentMethodDetached + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/PaymentMethodDetachedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_PAYMENT_METHOD_DETACHED + examples: + - CUSTOMER_PAYMENT_METHOD_DETACHED + type: string + type: + description: Event type + enum: + - com.kaiten.customer.payment_method.v1.detached + examples: + - com.kaiten.customer.payment_method.v1.detached + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Customer Payment Method Detached Webhook tags: - - license-families - /license-families/{familySlug}: - get: - description: |- - Resolves a license family to the version it currently serves, returned as currentVersion: the family's default version if it has one, otherwise its highest-numbered PUBLISHED version. This is the stable address for "the current version of this product", so a caller never has to name a version slug. - - A family with a default keeps serving it when a newer version is published -- the default is what the vendor puts forward, and superseding it is an explicit act (make another version the default, or unset it). A family without a default follows publication: the newest PUBLISHED version wins. - - Returns 404 GetLicenseFamily.NoPublishedVersion when the family exists but has no PUBLISHED version, since there is nothing to resolve to. Use ?include=versions to read such a family anyway: with the history requested, the family is returned with its versions and no currentVersion. Use ?version=N for explicit access to one version, including draft and archived ones. - operationId: get-license-family - parameters: - - description: License family slug - in: path - name: familySlug - required: true - schema: - description: License family slug - examples: - - enterprise-license - type: string - - description: "Address this version number instead of resolving the current one. Explicit historical access: the version is returned in currentVersion whatever its lifecycle state, draft and archived included, and the resolution rule does not apply." - explode: false - in: query - name: version - schema: - description: "Address this version number instead of resolving the current one. Explicit historical access: the version is returned in currentVersion whatever its lifecycle state, draft and archived included, and the resolution rule does not apply." - format: int32 - minimum: 1 - type: integer - - description: "Extra representations to add, comma-separated. Only 'versions' exists today: it adds every version of the family, oldest first, and makes a family with no PUBLISHED version readable instead of a 404. A list, so that another representation can be added later without changing the parameter's type." - explode: false - in: query - name: include - schema: - description: "Extra representations to add, comma-separated. Only 'versions' exists today: it adds every version of the family, oldest first, and makes a family with no PUBLISHED version readable instead of a 404. A list, so that another representation can be added later without changing the parameter's type." - items: - enum: - - versions - type: string - type: array + - webhooks + - billing + com.kaiten.customer.payment_method.v1.expiring: + post: + description: Triggered once, 30 days before the end of the expiry month of a customer's payment method. + operationId: onCustomerPaymentMethodExpiring + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/PaymentMethodExpiringDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_PAYMENT_METHOD_EXPIRING + examples: + - CUSTOMER_PAYMENT_METHOD_EXPIRING + type: string + type: + description: Event type + enum: + - com.kaiten.customer.payment_method.v1.expiring + examples: + - com.kaiten.customer.payment_method.v1.expiring + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/LicenseFamilyView" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:licenses - summary: Get a license family by slug + description: Webhook processed successfully + summary: Customer Payment Method Expiring Webhook tags: - - license-families - /licenses: - get: - description: Returns a cursor-paginated page of licenses for the current organization. - operationId: get-licenses - parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + - webhooks + - billing + com.kaiten.customer.v1.created: + post: + description: Triggered when a new customer is created. + operationId: onCustomerCreated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CustomerEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_CREATED + examples: + - CUSTOMER_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.customer.v1.created + examples: + - com.kaiten.customer.v1.created + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageLicense" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:licenses - summary: GetUser all licenses + description: Webhook processed successfully + summary: Customer Created Webhook + tags: + - webhooks + - customers + com.kaiten.customer.v1.creation_rejected: + post: + description: Triggered when a customer creation is refused because the organization has reached its customer entitlement cap. No customer is created. + operationId: onCustomerCreationRejected + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CustomerCreationRejectedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_CREATION_REJECTED + examples: + - CUSTOMER_CREATION_REJECTED + type: string + type: + description: Event type + enum: + - com.kaiten.customer.v1.creation_rejected + examples: + - com.kaiten.customer.v1.creation_rejected + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Customer Creation Rejected Webhook tags: - - licenses + - webhooks + - customers + com.kaiten.customer.v1.deleted: post: - description: |- - Create a new license with the provided details. Without familySlug or familyId this creates a new license family and the license becomes its version 1. With one of them it adds the next version to that family (familySlug names it by slug, familyId by identifier; sent together they must agree), and the version number is assigned by the server as the next one in the family's sequence. The version starts PUBLISHED, or DRAFT when lifecycleState says so; ARCHIVED is refused (422 CreateLicense.LifecycleStateNotSettable), because a version is archived with archive-license once it has been on sale. - - Where the deployment limits how many licenses an organization may have, only a new family counts against that limit (409 CreateLicense.EntitlementLimitReached once it is reached): a new version of an existing family never does. - operationId: create-license + description: Triggered when a customer is deleted. + operationId: onCustomerDeleted requestBody: content: application/json: schema: - $ref: "#/components/schemas/License" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CustomerEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_DELETED + examples: + - CUSTOMER_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.customer.v1.deleted + examples: + - com.kaiten.customer.v1.deleted + type: string + required: + - name + - type + - data + type: object required: true - responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/License" - description: Created - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:licenses - summary: Create a new license - tags: - - licenses - /licenses/{licenseSlug}: - delete: - description: Delete a license with the provided details. Deleting the last version of a license family also deletes the family, and frees its slug for a new product to take. A deleted version's number is not reused by the next version of its family. - operationId: delete-license - parameters: - - description: License slug - in: path - name: licenseSlug - required: true - schema: - description: License slug - examples: - - license-slug - type: string - responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Delete a license - tags: - - licenses - get: - description: Retrieve a license by their unique slug - operationId: get-license - parameters: - - description: License slug - in: path - name: licenseSlug - required: true - schema: - description: License slug - examples: - - license-slug - type: string responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/License" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:licenses - summary: Get a license by slug + description: Webhook processed successfully + summary: Customer Deleted Webhook + tags: + - webhooks + - customers + com.kaiten.customer.v1.updated: + post: + description: Triggered when a customer is updated. + operationId: onCustomerUpdated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CustomerEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - CUSTOMER_UPDATED + examples: + - CUSTOMER_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.customer.v1.updated + examples: + - com.kaiten.customer.v1.updated + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Customer Updated Webhook tags: - - licenses - put: - description: Update a license with the provided details - operationId: update-license - parameters: - - description: License slug - in: path - name: licenseSlug - required: true - schema: - description: License slug - examples: - - license-slug - type: string + - webhooks + - customers + com.kaiten.deployment_zone.v1.created: + post: + description: Triggered when a new deployment zone is created. + operationId: onDeploymentZoneCreated requestBody: content: application/json: schema: - $ref: "#/components/schemas/License" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/DeploymentZoneDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - DEPLOYMENT_ZONE_CREATED + examples: + - DEPLOYMENT_ZONE_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.deployment_zone.v1.created + examples: + - com.kaiten.deployment_zone.v1.created + type: string + required: + - name + - type + - data + type: object required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Update a license + "200": + description: Webhook processed successfully + summary: Deployment Zone Created Webhook tags: - - licenses - /licenses/{licenseSlug}/archive: + - webhooks + - deploymentZones + com.kaiten.deployment_zone.v1.deleted: post: - description: "Withdraws a PUBLISHED license version from sale: its family stops resolving to it and no instance can be assigned to it any more, while instances already on it keep it. Only a published version can be archived (409 ArchiveLicense.NotPublished) -- a draft that was never on sale is deleted instead -- and not while it is its family's default (409 ArchiveLicense.DefaultMustBePublished): make another version the default, or unset it, first. Emits LICENSE_ARCHIVED." - operationId: archive-license - parameters: - - description: License version slug - in: path - name: licenseSlug - required: true - schema: - description: License version slug - examples: - - enterprise-license-v2 - type: string + description: Triggered when a deployment zone is deleted. + operationId: onDeploymentZoneDeleted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/DeploymentZoneDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - DEPLOYMENT_ZONE_DELETED + examples: + - DEPLOYMENT_ZONE_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.deployment_zone.v1.deleted + examples: + - com.kaiten.deployment_zone.v1.deleted + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/License" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Archive a license version + description: Webhook processed successfully + summary: Deployment Zone Deleted Webhook tags: - - licenses - /licenses/{licenseSlug}/entitlements: - get: - description: Returns a cursor-paginated page of entitlements granted by a license, identified by its unique slug. - operationId: get-license-entitlements - parameters: - - description: License slug - in: path - name: licenseSlug - required: true - schema: - description: License slug - examples: - - license-slug - type: string - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + - webhooks + - deploymentZones + com.kaiten.deployment_zone.v1.release_deployed: + post: + description: Triggered when a release is deployed to a deployment zone, either at zone creation or when an existing zone's release changes. + operationId: onReleaseDeployed + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/DeploymentDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - RELEASE_DEPLOYED + examples: + - RELEASE_DEPLOYED + type: string + type: + description: Event type + enum: + - com.kaiten.deployment_zone.v1.release_deployed + examples: + - com.kaiten.deployment_zone.v1.release_deployed + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageLicenseEntitlement" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:licenses - summary: Get all entitlements for a license + description: Webhook processed successfully + summary: Release Deployed Webhook tags: - - licenses + - webhooks + - deploymentZones + com.kaiten.deployment_zone.v1.updated: post: - description: Associate an entitlement with a license by providing the license slug and entitlement details - operationId: associate-entitlement-with-license - parameters: - - description: License slug - in: path - name: licenseSlug - required: true - schema: - description: License slug - examples: - - license-slug - type: string + description: Triggered when a deployment zone is updated. + operationId: onDeploymentZoneUpdated requestBody: content: application/json: schema: - $ref: "#/components/schemas/LicenseEntitlement" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/DeploymentZoneDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - DEPLOYMENT_ZONE_UPDATED + examples: + - DEPLOYMENT_ZONE_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.deployment_zone.v1.updated + examples: + - com.kaiten.deployment_zone.v1.updated + type: string + required: + - name + - type + - data + type: object required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:licenses - summary: Associate an entitlement with a license + "200": + description: Webhook processed successfully + summary: Deployment Zone Updated Webhook tags: - - licenses - /licenses/{licenseSlug}/entitlements/{entitlementSlug}: - delete: - description: Delete a license entitlement with the provided details - operationId: delete-license-entitlement - parameters: - - in: path - name: licenseSlug - required: true - schema: - type: string - - in: path - name: entitlementSlug - required: true - schema: - type: string + - webhooks + - deploymentZones + com.kaiten.entitlement.v1.created: + post: + description: Triggered when a new entitlement is created. + operationId: onEntitlementCreated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_CREATED + examples: + - ENTITLEMENT_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.entitlement.v1.created + examples: + - com.kaiten.entitlement.v1.created + type: string + required: + - name + - type + - data + type: object + required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Delete a license entitlement + "200": + description: Webhook processed successfully + summary: Entitlement Created Webhook + tags: + - webhooks + - entitlements + com.kaiten.entitlement.v1.deleted: + post: + description: Triggered when an entitlement is deleted. + operationId: onEntitlementDeleted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_DELETED + examples: + - ENTITLEMENT_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.entitlement.v1.deleted + examples: + - com.kaiten.entitlement.v1.deleted + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Entitlement Deleted Webhook tags: - - licenses - get: - description: Retrieve a entitlement for a license by their unique slugs - operationId: get-license-entitlement - parameters: - - in: path - name: licenseSlug - required: true - schema: - type: string - - in: path - name: entitlementSlug - required: true - schema: - type: string + - webhooks + - entitlements + com.kaiten.entitlement.v1.updated: + post: + description: Triggered when an entitlement is updated. + operationId: onEntitlementUpdated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_UPDATED + examples: + - ENTITLEMENT_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.entitlement.v1.updated + examples: + - com.kaiten.entitlement.v1.updated + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/LicenseEntitlement" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:licenses - summary: Get a license entitlement + description: Webhook processed successfully + summary: Entitlement Updated Webhook tags: - - licenses - put: - description: Update an license entitlement with the provided details - operationId: update-license-entitlement - parameters: - - in: path - name: licenseSlug - required: true - schema: - type: string - - in: path - name: entitlementSlug - required: true - schema: - type: string + - webhooks + - entitlements + com.kaiten.entitlement_group.v1.created: + post: + description: Triggered when a new entitlement group is created. + operationId: onEntitlementGroupCreated requestBody: content: application/json: schema: - $ref: "#/components/schemas/LicenseEntitlement" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementGroupDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_GROUP_CREATED + examples: + - ENTITLEMENT_GROUP_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.entitlement_group.v1.created + examples: + - com.kaiten.entitlement_group.v1.created + type: string + required: + - name + - type + - data + type: object required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Update an license entitlement + "200": + description: Webhook processed successfully + summary: Entitlement Group Created Webhook tags: - - licenses - /licenses/{licenseSlug}/publish: + - webhooks + - entitlement-groups + com.kaiten.entitlement_group.v1.deleted: post: - description: "Puts a DRAFT license version on sale: it becomes PUBLISHED, so it can be made its family's default, and its family resolves to it when the family has no default and this is its highest published version. Only a draft can be published (409 PublishLicense.NotADraft); an archived version goes back on sale with unarchive-license. Emits LICENSE_PUBLISHED." - operationId: publish-license - parameters: - - description: License version slug - in: path - name: licenseSlug - required: true - schema: - description: License version slug - examples: - - enterprise-license-v2 - type: string + description: Triggered when an entitlement group is deleted. + operationId: onEntitlementGroupDeleted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementGroupDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_GROUP_DELETED + examples: + - ENTITLEMENT_GROUP_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.entitlement_group.v1.deleted + examples: + - com.kaiten.entitlement_group.v1.deleted + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/License" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Publish a license version + description: Webhook processed successfully + summary: Entitlement Group Deleted Webhook tags: - - licenses - /licenses/{licenseSlug}/unarchive: + - webhooks + - entitlement-groups + com.kaiten.entitlement_group.v1.updated: post: - description: "Puts an ARCHIVED license version back on sale: it is PUBLISHED again, can be assigned to instances, and can be what its family resolves to. Only an archived version can be unarchived (409 UnarchiveLicense.NotArchived). Emits LICENSE_UNARCHIVED." - operationId: unarchive-license - parameters: - - description: License version slug - in: path - name: licenseSlug - required: true - schema: - description: License version slug - examples: - - enterprise-license-v2 - type: string + description: Triggered when an entitlement group is updated. + operationId: onEntitlementGroupUpdated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementGroupDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_GROUP_UPDATED + examples: + - ENTITLEMENT_GROUP_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.entitlement_group.v1.updated + examples: + - com.kaiten.entitlement_group.v1.updated + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/License" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:licenses - summary: Unarchive a license version + description: Webhook processed successfully + summary: Entitlement Group Updated Webhook tags: - - licenses - /metadata-fields: - get: - description: Returns a cursor-paginated page of the active (non-archived) metadata fields declared for the org and the given resource type, ordered by displayOrder then createdAt. - operationId: list-metadata-fields - parameters: - - explode: false - in: query - name: resourceType - required: true - schema: - enum: - - DEPLOYMENT_ZONE - - INSTANCE - type: string - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + - webhooks + - entitlement-groups + com.kaiten.feature_flag.v1.created: + post: + description: Triggered when a new feature flag is created. + operationId: onFeatureFlagCreated + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/FeatureFlagDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - FEATURE_FLAG_CREATED + examples: + - FEATURE_FLAG_CREATED + type: string + type: + description: Event type + enum: + - com.kaiten.feature_flag.v1.created + examples: + - com.kaiten.feature_flag.v1.created + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageMetadataField" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:metadata_fields - summary: List active metadata fields for a resource type + description: Webhook processed successfully + summary: Feature Flag Created Webhook tags: - - metadataFields + - webhooks + - featureflags + com.kaiten.feature_flag.v1.deleted: post: - description: Declares a typed metadata field for a given resource type (DEPLOYMENT_ZONE, INSTANCE). The json_schema is validated against JSON Schema 2020-12 before insertion. Restricted to write:metadata_fields scope (admin-only policy). - operationId: create-metadata-field + description: Triggered when a feature flag is deleted. + operationId: onFeatureFlagDeleted requestBody: content: application/json: schema: - $ref: "#/components/schemas/MetadataField" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/FeatureFlagDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - FEATURE_FLAG_DELETED + examples: + - FEATURE_FLAG_DELETED + type: string + type: + description: Event type + enum: + - com.kaiten.feature_flag.v1.deleted + examples: + - com.kaiten.feature_flag.v1.deleted + type: string + required: + - name + - type + - data + type: object required: true responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/MetadataField" - description: Created - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:metadata_fields - summary: Create a metadata field + "200": + description: Webhook processed successfully + summary: Feature Flag Deleted Webhook tags: - - metadataFields - /metadata-fields/reorder: + - webhooks + - featureflags + com.kaiten.feature_flag.v1.evaluated: post: - description: Sets the displayOrder of the listed fields to their position in the array (0-indexed). All ids must belong to the same organization, resource type, and be non-archived; the entire reorder is applied in a single transaction. - operationId: reorder-metadata-fields + description: Triggered on every flag evaluation. The payload carries no evaluation context, so no targeting key or user attribute is exposed. + operationId: onFeatureFlagEvaluated requestBody: content: application/json: schema: - $ref: "#/components/schemas/ReorderMetadataFieldsInput" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/FeatureFlagEvaluatedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - FEATURE_FLAG_EVALUATED + examples: + - FEATURE_FLAG_EVALUATED + type: string + type: + description: Event type + enum: + - com.kaiten.feature_flag.v1.evaluated + examples: + - com.kaiten.feature_flag.v1.evaluated + type: string + required: + - name + - type + - data + type: object required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:metadata_fields - summary: Reorder metadata fields + "200": + description: Webhook processed successfully + summary: Feature Flag Evaluated Webhook tags: - - metadataFields - /metadata-fields/{id}: - patch: - description: "Updates label and json_schema. `key` and `resourceType` are immutable, and `displayOrder` belongs to POST /metadata-fields/reorder. The new json_schema is validated against JSON Schema 2020-12 and the transition rules: type changes, cardinality changes, and free-string ↔ enum toggles are rejected with 422." - operationId: update-metadata-field - parameters: - - in: path - name: id - required: true - schema: - type: string + - webhooks + - featureflags + com.kaiten.feature_flag.v1.updated: + post: + description: Triggered when a feature flag is updated. + operationId: onFeatureFlagUpdated requestBody: content: application/json: schema: - $ref: "#/components/schemas/MetadataField" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/FeatureFlagDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - FEATURE_FLAG_UPDATED + examples: + - FEATURE_FLAG_UPDATED + type: string + type: + description: Event type + enum: + - com.kaiten.feature_flag.v1.updated + examples: + - com.kaiten.feature_flag.v1.updated + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/MetadataField" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:metadata_fields - summary: Update a metadata field + description: Webhook processed successfully + summary: Feature Flag Updated Webhook tags: - - metadataFields - /metadata-fields/{id}/archive: + - webhooks + - featureflags + com.kaiten.identity.v1.system_token_issued: post: - description: Soft-deletes a metadata field. The row stays in the DB (its archived_at is set) so historic metadata is still interpretable; the field stops appearing in the active schema composition. - operationId: archive-metadata-field - parameters: - - in: path - name: id - required: true - schema: - type: string + description: "Triggered when a platform credential mints an organization-scoped credential for the `system:kaiten` platform identity inside this organization. Carries the platform credential that performed the mint and the scopes granted, never the credential value. Expect this at the rate an integration refreshes: 96 a day per process at a 15-minute lifetime, more if it refreshes before expiry." + operationId: onSystemOrganizationTokenIssued + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/SystemTokenIssuanceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - SYSTEM_ORGANIZATION_TOKEN_ISSUED + examples: + - SYSTEM_ORGANIZATION_TOKEN_ISSUED + type: string + type: + description: Event type + enum: + - com.kaiten.identity.v1.system_token_issued + examples: + - com.kaiten.identity.v1.system_token_issued + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/MetadataField" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:metadata_fields - summary: Archive a metadata field + description: Webhook processed successfully + summary: System Organization Token Issued Webhook tags: - - metadataFields - /metadata-fields/{id}/dry-run: + - webhooks + - identity + com.kaiten.instance.addon.v1.added: post: - description: Computes, server-side, how many existing resources of this field's resource type carry a value under this field's key that would NOT satisfy the supplied candidate JSON Schema, plus a small sample. Read-only — does not mutate the field. Replaces the former client-side dry-run that pulled every resource's metadata. - operationId: dry-run-metadata-field - parameters: - - in: path - name: id - required: true - schema: - type: string + description: Triggered when an add-on is attached to an instance. + operationId: onInstanceAddonAdded requestBody: content: application/json: schema: - $ref: "#/components/schemas/MetadataFieldSchemaProposal" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceAddonUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ADDON_ADDED + examples: + - INSTANCE_ADDON_ADDED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.addon.v1.added + examples: + - com.kaiten.instance.addon.v1.added + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/Impact" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:metadata_fields - summary: Preview the impact of a metadata field schema change + description: Webhook processed successfully + summary: Instance Add-on Added Webhook tags: - - metadataFields - /metadata-fields/{id}/unarchive: + - webhooks + - instances + com.kaiten.instance.addon.v1.quantity_changed: post: - description: Restores a previously archived metadata field (clears its archived_at). The field re-enters the active schema composition. Returns 409 if an active field already uses the same key for this resource type, or 404 if the field does not exist or is not archived. - operationId: unarchive-metadata-field - parameters: - - in: path - name: id - required: true - schema: - type: string + description: Triggered when the quantity of an instance's add-on changes. + operationId: onInstanceAddonQuantityChanged + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceAddonUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ADDON_QUANTITY_CHANGED + examples: + - INSTANCE_ADDON_QUANTITY_CHANGED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.addon.v1.quantity_changed + examples: + - com.kaiten.instance.addon.v1.quantity_changed + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/MetadataField" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:metadata_fields - summary: Unarchive a metadata field + description: Webhook processed successfully + summary: Instance Add-on Quantity Changed Webhook tags: - - metadataFields - /ofrep/v1/evaluate/flags: + - webhooks + - instances + com.kaiten.instance.addon.v1.removed: post: - description: |- - OFREP bulk evaluation request. - The endpoint is called by the client providers to evaluate all flags at once. - operationId: evaluateFlagsBulk - parameters: - - description: The request will be processed only if ETag doesn't match any of the values listed. - in: header - name: If-None-Match - schema: - type: string + description: Triggered when an add-on is removed from an instance. + operationId: onInstanceAddonRemoved requestBody: content: application/json: schema: - $ref: "#/components/schemas/BulkEvaluationRequest" - description: EvaluateVariant multiple flags in one request + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceAddonUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ADDON_REMOVED + examples: + - INSTANCE_ADDON_REMOVED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.addon.v1.removed + examples: + - com.kaiten.instance.addon.v1.removed + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - oneOf: - - properties: - flags: - description: Array of evaluation results - items: - oneOf: - - $ref: "#/components/schemas/EvaluationSuccess" - - $ref: "#/components/schemas/EvaluationFailure" - type: array - - $ref: "#/components/schemas/BulkEvaluationFailure" - description: OFREP successful evaluation response - headers: - ETag: - description: Entity tag used for cache validation - schema: - type: string - "304": - description: Bulk evaluation is not modified - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/BulkEvaluationFailure" - description: Bad evaluation request - "401": - description: Unauthorized - You need credentials to access the API - "403": - description: Forbidden - You are not authorized to access the API - "429": - description: Rate limit reached on the Flag Management System - headers: - Retry-After: - description: Indicates when to retry the request again - schema: - type: string - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/BulkEvaluationFailure" - description: Internal server error - security: - - bearerAuth: - - read:feature_flags - summary: OFREP bulk flag evaluation contract + description: Webhook processed successfully + summary: Instance Add-on Removed Webhook tags: - - OFREP Core - /ofrep/v1/evaluate/flags/{key}: + - webhooks + - instances + com.kaiten.instance.billing.v1.canceled: post: - description: EvaluateVariant a single feature flag for a given key - operationId: evaluateFlag - parameters: - - in: path - name: key - required: true - schema: - type: string + description: "Triggered when a subscription is canceled: at its period's end, immediately, or during its trial." + operationId: onInstanceBillingCanceled requestBody: content: application/json: schema: - $ref: "#/components/schemas/EvaluationRequest" - description: Evaluation context + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CancellationDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_CANCELED + examples: + - INSTANCE_BILLING_CANCELED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.canceled + examples: + - com.kaiten.instance.billing.v1.canceled + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/EvaluationSuccess" - description: Flag evaluated successfully - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/EvaluationFailure" - description: Evaluation failed - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/EvaluationFailure" - description: Internal error - security: - - bearerAuth: - - read:feature_flags - summary: OFREP single flag evaluation contract - tags: - - OFREP Core - /openfeature/v0/manifest: - get: - description: Returns a manifest of all feature flags that have a fallback_value set in their metadata - operationId: get-openfeature-manifest - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/ManifestEnvelope" - description: OK - headers: - X-Manifest-Capabilities: - schema: - type: string - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:feature_flags - summary: Get OpenFeature manifest + description: Webhook processed successfully + summary: Instance Billing Canceled Webhook tags: - - OpenFeature - /releases: - get: - description: Returns a cursor-paginated page of releases for the current organization. - operationId: list-releases - parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageRelease" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:releases - summary: List releases + - webhooks + - billing + com.kaiten.instance.billing.v1.cancellation_reverted: + post: + description: Triggered when a scheduled cancellation is reverted before the boundary. + operationId: onInstanceBillingCancellationReverted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CancellationChangeDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_CANCELLATION_REVERTED + examples: + - INSTANCE_BILLING_CANCELLATION_REVERTED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.cancellation_reverted + examples: + - com.kaiten.instance.billing.v1.cancellation_reverted + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Instance Billing Cancellation Reverted Webhook tags: - - releases + - webhooks + - billing + com.kaiten.instance.billing.v1.cancellation_scheduled: post: - description: "Create a new release with the provided details and link the requested components. A release is immutable: there is no update endpoint, and the component list cannot change afterwards. Correcting one means deleting it and creating a new one, which starts a new deployment history." - operationId: create-release + description: Triggered when a subscription is set to cancel at the end of its period. + operationId: onInstanceBillingCancellationScheduled requestBody: content: application/json: schema: - $ref: "#/components/schemas/Release" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/CancellationChangeDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_CANCELLATION_SCHEDULED + examples: + - INSTANCE_BILLING_CANCELLATION_SCHEDULED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.cancellation_scheduled + examples: + - com.kaiten.instance.billing.v1.cancellation_scheduled + type: string + required: + - name + - type + - data + type: object required: true responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/Release" - description: Created - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:releases - summary: Create a new release - tags: - - releases - /releases/{releaseSlug}: - delete: - description: Delete a release by their slug. Since a release is immutable, this plus a fresh create is the only way to change one -- and it discards the deployment history recorded against the old release. A release still deployed to a zone cannot be deleted (409). - operationId: delete-release - parameters: - - description: Release slug - in: path - name: releaseSlug - required: true - schema: - description: Release slug - examples: - - release-slug - type: string - responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:releases - summary: Delete a release + "200": + description: Webhook processed successfully + summary: Instance Billing Cancellation Scheduled Webhook tags: - - releases - get: - description: Returns a single release by their slug. - operationId: get-release-by-slug - parameters: - - description: Release slug - in: path - name: releaseSlug - required: true - schema: - description: Release slug - examples: - - release-slug - type: string + - webhooks + - billing + com.kaiten.instance.billing.v1.plan_change_canceled: + post: + description: "Triggered when a scheduled plan change is dropped: by request, or by a cancellation." + operationId: onInstanceBillingPlanChangeCanceled + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/PlanChangeScheduleDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_PLAN_CHANGE_CANCELED + examples: + - INSTANCE_BILLING_PLAN_CHANGE_CANCELED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.plan_change_canceled + examples: + - com.kaiten.instance.billing.v1.plan_change_canceled + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/Release" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:releases - summary: Get a release by slug + description: Webhook processed successfully + summary: Instance Billing Plan Change Canceled Webhook + tags: + - webhooks + - billing + com.kaiten.instance.billing.v1.plan_change_scheduled: + post: + description: Triggered when a plan change is scheduled for the next boundary, or replaced. + operationId: onInstanceBillingPlanChangeScheduled + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/PlanChangeScheduleDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_PLAN_CHANGE_SCHEDULED + examples: + - INSTANCE_BILLING_PLAN_CHANGE_SCHEDULED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.plan_change_scheduled + examples: + - com.kaiten.instance.billing.v1.plan_change_scheduled + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Instance Billing Plan Change Scheduled Webhook tags: - - deploymentZones - /service-accounts: - get: - description: Returns a cursor-paginated page of service accounts (machine users) for the current organization. - operationId: get-service-accounts - parameters: - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + - webhooks + - billing + com.kaiten.instance.billing.v1.plan_changed: + post: + description: Triggered when a scheduled plan change is applied at a boundary. + operationId: onInstanceBillingPlanChanged + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/PlanChangeDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_PLAN_CHANGED + examples: + - INSTANCE_BILLING_PLAN_CHANGED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.plan_changed + examples: + - com.kaiten.instance.billing.v1.plan_changed + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageServiceAccount" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:tokens - summary: List all service accounts + description: Webhook processed successfully + summary: Instance Billing Plan Changed Webhook tags: - - service-accounts + - webhooks + - billing + com.kaiten.instance.billing.v1.provider_changed: post: - description: Create a new service account (machine user) with the provided name. Service accounts can be used to generate API tokens for programmatic access. - operationId: create-service-account + description: Triggered when a subscription moves to another payment provider, from its next invoice on. + operationId: onInstanceBillingProviderChanged requestBody: content: application/json: schema: - $ref: "#/components/schemas/ServiceAccount" - description: Service account details + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/ProviderChangeDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_PROVIDER_CHANGED + examples: + - INSTANCE_BILLING_PROVIDER_CHANGED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.provider_changed + examples: + - com.kaiten.instance.billing.v1.provider_changed + type: string + required: + - name + - type + - data + type: object required: true responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/ServiceAccount" - description: Created service account - description: Created - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:tokens - summary: Create a new service account + "200": + description: Webhook processed successfully + summary: Instance Billing Provider Changed Webhook tags: - - service-accounts - /service-accounts/{serviceAccountSlug}: - get: - description: Retrieve details of a specific service account (machine user) by its slug. - operationId: get-service-account - parameters: - - description: Service account slug - in: path - name: serviceAccountSlug - required: true - schema: - description: Service account slug - examples: - - service-account-slug - type: string + - webhooks + - billing + com.kaiten.instance.billing.v1.started: + post: + description: Triggered when an instance is subscribed, or a CANCELED subscription subscribed again. + operationId: onInstanceBillingStarted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/BillingStartedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_STARTED + examples: + - INSTANCE_BILLING_STARTED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.started + examples: + - com.kaiten.instance.billing.v1.started + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Instance Billing Started Webhook + tags: + - webhooks + - billing + com.kaiten.instance.billing.v1.status_changed: + post: + description: Triggered when a trial converts, or a subscription becomes PAST_DUE or leaves it. + operationId: onInstanceBillingStatusChanged + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/StatusChangeDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_STATUS_CHANGED + examples: + - INSTANCE_BILLING_STATUS_CHANGED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.status_changed + examples: + - com.kaiten.instance.billing.v1.status_changed + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/ServiceAccount" - description: Service account details - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:tokens - summary: Get a service account by slug + description: Webhook processed successfully + summary: Instance Billing Status Changed Webhook tags: - - service-accounts - put: - description: Update the name of a service account (machine user) by its slug. - operationId: update-service-account - parameters: - - description: Service account slug - in: path - name: serviceAccountSlug - required: true - schema: - description: Service account slug - examples: - - service-account-slug - type: string + - webhooks + - billing + com.kaiten.instance.billing.v1.trial_will_end: + post: + description: "Triggered once per trial, at reminderAt: its family's trialReminderDays before trialEndsAt (3 by default), or at once when the trial is shorter. Carries what a reminder e-mail needs, read when it is emitted: the customer and its billing e-mail, the instance, the plan, daysLeft, the charge the conversion would make and the card it would be made to." + operationId: onInstanceBillingTrialWillEnd requestBody: content: application/json: schema: - $ref: "#/components/schemas/ServiceAccount" - description: Service account update details + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/TrialWillEndDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_BILLING_TRIAL_WILL_END + examples: + - INSTANCE_BILLING_TRIAL_WILL_END + type: string + type: + description: Event type + enum: + - com.kaiten.instance.billing.v1.trial_will_end + examples: + - com.kaiten.instance.billing.v1.trial_will_end + type: string + required: + - name + - type + - data + type: object required: true responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:tokens - summary: Update a service account + "200": + description: Webhook processed successfully + summary: Instance Billing Trial Will End Webhook tags: - - service-accounts - /service-accounts/{serviceAccountSlug}/tokens: - get: - description: Returns a cursor-paginated page of API tokens associated with a service account. The organization context is derived from the authenticated user's JWT token. Token secret values are never returned, only metadata. - operationId: get-service-account-tokens - parameters: - - description: Service account slug - in: path - name: serviceAccountSlug - required: true - schema: - description: Service account slug - examples: - - service-account-slug - type: string - - description: Opaque pagination cursor from a previous response's nextCursor - explode: false - in: query - name: cursor - schema: - description: Opaque pagination cursor from a previous response's nextCursor - type: string - - description: Maximum number of entries to return (default 50, max 200) - explode: false - in: query - name: limit - schema: - description: Maximum number of entries to return (default 50, max 200) - format: int32 - maximum: 200 - minimum: 1 - type: integer + - webhooks + - billing + com.kaiten.instance.entitlement.v1.cap_exceeded: + post: + description: Triggered when a SOFT-enforcement entitlement's usage crosses from below the cap to above it. Fires once per crossing. + operationId: onInstanceEntitlementCapExceeded + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceEntitlementCapExceededDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ENTITLEMENT_CAP_EXCEEDED + examples: + - INSTANCE_ENTITLEMENT_CAP_EXCEEDED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.cap_exceeded + examples: + - com.kaiten.instance.entitlement.v1.cap_exceeded + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PageToken" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:tokens - summary: List all tokens for a service account + description: Webhook processed successfully + summary: Instance Entitlement Cap Exceeded Webhook tags: - - service-accounts - post: - description: Create a new API token for a service account. The token value is only returned once upon creation and cannot be retrieved again. The organization context is derived from the authenticated user's JWT token. - operationId: create-service-account-token - parameters: - - description: Service account slug - in: path - name: serviceAccountSlug - required: true - schema: - description: Service account slug - examples: - - service-account-slug - format: text - type: string + - webhooks + - instances + com.kaiten.instance.entitlement.v1.usage_period_rolled_over: + post: + description: "Triggered once per usage window closed by a report: the entitlement's own stored bucket, and one synthetic event per window skipped entirely since the last report." + operationId: onInstanceEntitlementUsagePeriodRolledOver requestBody: content: application/json: schema: - $ref: "#/components/schemas/PlainToken" - description: Token creation details + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceEntitlementUsagePeriodRolledOverDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER + examples: + - INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.usage_period_rolled_over + examples: + - com.kaiten.instance.entitlement.v1.usage_period_rolled_over + type: string + required: + - name + - type + - data + type: object required: true responses: - "201": - content: - application/json: - schema: - $ref: "#/components/schemas/PlainToken" - description: Created token with secret value (only shown once) - description: Created - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "409": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Conflict - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - "503": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Service Unavailable - security: - - bearerAuth: - - write:tokens - summary: Create a new token for a service account - tags: - - service-accounts - /service-accounts/{serviceAccountSlug}/tokens/{tokenSlug}: - delete: - description: Revoke an API token for a service account. Once revoked, the token can no longer be used for authentication. The organization context is derived from the authenticated user's JWT token. - operationId: delete-service-account-token - parameters: - - description: Service account slug - in: path - name: serviceAccountSlug - required: true - schema: - description: Service account slug - examples: - - service-account-slug - format: text - type: string - - description: Token slug to revoke - in: path - name: tokenSlug - required: true - schema: - description: Token slug to revoke - examples: - - my-token-slug - format: text - type: string - responses: - "204": - description: No Content - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "404": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Not Found - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:tokens - summary: Revoke a token for a service account + "200": + description: Webhook processed successfully + summary: Entitlement Usage Period Rolled Over Webhook tags: - - service-accounts - /v1/notification-preferences: - get: - description: "Every notifiable event, with this user's effective value per channel: their own choice where they made one, the catalogue default otherwise. Events absent from the catalogue of a deployment never appear, which is what keeps high-volume system events out of the feed." - operationId: getNotificationPreferences + - webhooks + - instances + com.kaiten.instance.entitlement.v1.usage_reached: + post: + description: Triggered when an instance's entitlement usage reaches its threshold. + operationId: onInstanceEntitlementUsageReached + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/EntitlementUsageDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ENTITLEMENT_USAGE_REACHED + examples: + - INSTANCE_ENTITLEMENT_USAGE_REACHED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.usage_reached + examples: + - com.kaiten.instance.entitlement.v1.usage_reached + type: string + required: + - name + - type + - data + type: object + required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PreferenceMatrix" - description: OK - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:notifications - summary: Read the caller's notification preferences + description: Webhook processed successfully + summary: Instance Entitlement Usage Reached Webhook + tags: + - webhooks + - instances + com.kaiten.instance.entitlement.v1.usage_report_accepted: + post: + description: Triggered when a usage report is persisted, including a SOFT-enforcement overage. + operationId: onInstanceEntitlementUsageReportAccepted + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceEntitlementUsageReportAcceptedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_USAGE_REPORT_ACCEPTED + examples: + - ENTITLEMENT_USAGE_REPORT_ACCEPTED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.usage_report_accepted + examples: + - com.kaiten.instance.entitlement.v1.usage_report_accepted + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Entitlement Usage Report Accepted Webhook tags: - - notifications - put: - description: Stores the signed-in user's choices and answers with the whole matrix. A value that equals the catalogue default is stored as no row at all, so a later change to that default still reaches this user. - operationId: putNotificationPreferences + - webhooks + - instances + com.kaiten.instance.entitlement.v1.usage_report_rejected: + post: + description: Triggered when a usage report is refused because it would take a HARD-enforcement entitlement past its cap. The usage is not persisted. + operationId: onInstanceEntitlementUsageReportRejected requestBody: content: application/json: schema: - $ref: "#/components/schemas/PreferenceChoices" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceEntitlementUsageReportAcceptedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_USAGE_REPORT_REJECTED + examples: + - ENTITLEMENT_USAGE_REPORT_REJECTED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.usage_report_rejected + examples: + - com.kaiten.instance.entitlement.v1.usage_report_rejected + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/PreferenceMatrix" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:notifications - summary: Set the caller's notification preferences - tags: - - notifications - /v1/notifications: - get: - description: Returns the signed-in user's notification feed, newest first, with the unread count. Notifications are a view over this organization's audit trail, narrowed to the events the user subscribes to; the id of a notification is the id of the audit trail entry it reports. - operationId: listNotifications - parameters: - - description: Which notifications to return (default all) - explode: false - in: query - name: status - schema: - description: Which notifications to return (default all) - enum: - - all - - unread - type: string - - description: "Only notifications about these kinds of object; repeat the parameter for several (default: all)" - explode: true - in: query - name: objectType - schema: - description: "Only notifications about these kinds of object; repeat the parameter for several (default: all)" - items: - enum: - - instance - - customer - - release - - deployment_zone - - component - - license - - token - type: string - type: - - array - - "null" - - description: Opaque cursor from a previous response's next_cursor - explode: false - in: query - name: cursor - schema: - description: Opaque cursor from a previous response's next_cursor - type: string - - description: Page size (default 20, max 50) - explode: false - in: query - name: limit - schema: - description: Page size (default 20, max 50) - format: int32 - maximum: 50 - minimum: 1 - type: integer - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/List" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - read:notifications - summary: List the caller's notifications + description: Webhook processed successfully + summary: Entitlement Usage Report Rejected Webhook tags: - - notifications - /v1/notifications/mark-read: + - webhooks + - instances + com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached: post: - description: "Marks the given notifications read for the signed-in user, or all of them with all: true. Ids belonging to another organization are ignored rather than rejected, so a retry of a partially applied call is safe." - operationId: markNotificationsRead + description: Triggered when an instance's entitlement usage crosses its configured early-warning percentage boundary, before the cap is reached. Fires once per crossing. + operationId: onInstanceEntitlementUsageWarningThresholdReached requestBody: content: application/json: schema: - $ref: "#/components/schemas/ReadSelection" + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceEntitlementUsageWarningThresholdReachedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED + examples: + - INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached + examples: + - com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached + type: string + required: + - name + - type + - data + type: object required: true responses: "200": - content: - application/json: - schema: - $ref: "#/components/schemas/MarkReadResult" - description: OK - "400": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Bad Request - "401": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unauthorized - "403": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Forbidden - "422": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Unprocessable Entity - "500": - content: - application/problem+json: - schema: - $ref: "#/components/schemas/Problem" - description: Internal Server Error - security: - - bearerAuth: - - write:notifications - summary: Mark notifications read + description: Webhook processed successfully + summary: Instance Entitlement Usage Warning Threshold Reached Webhook + tags: + - webhooks + - instances + com.kaiten.instance.entitlement.v1.value_get: + post: + description: Triggered on every read of an instance entitlement's usage value. This is a read audit signal, not a state change. + operationId: onInstanceEntitlementValueGet + requestBody: + content: + application/json: + schema: + additionalProperties: false + properties: + data: + $ref: "#/components/schemas/InstanceEntitlementValueGetDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" + name: + description: Event name + enum: + - ENTITLEMENT_VALUE_GET + examples: + - ENTITLEMENT_VALUE_GET + type: string + type: + description: Event type + enum: + - com.kaiten.instance.entitlement.v1.value_get + examples: + - com.kaiten.instance.entitlement.v1.value_get + type: string + required: + - name + - type + - data + type: object + required: true + responses: + "200": + description: Webhook processed successfully + summary: Instance Entitlement Value Read Webhook tags: - - notifications -security: - - bearerAuth: [] -servers: - - url: /api -webhooks: - com.kaiten.component.v1.created: + - webhooks + - instances + com.kaiten.instance.entitlement_exception.v1.created: post: - description: Triggered when a new component is created. - operationId: onComponentCreated + description: Triggered when an exception is created on one entitlement of an instance. Emitted whether or not billing is enabled. + operationId: onInstanceEntitlementExceptionCreated requestBody: content: application/json: @@ -10189,20 +36441,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Component" + $ref: "#/components/schemas/ExceptionEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - COMPONENT_CREATED + - INSTANCE_ENTITLEMENT_EXCEPTION_CREATED examples: - - COMPONENT_CREATED + - INSTANCE_ENTITLEMENT_EXCEPTION_CREATED type: string type: description: Event type enum: - - com.kaiten.component.v1.created + - com.kaiten.instance.entitlement_exception.v1.created examples: - - com.kaiten.component.v1.created + - com.kaiten.instance.entitlement_exception.v1.created type: string required: - name @@ -10213,14 +36467,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Component Created Webhook + summary: Instance Entitlement Exception Created Webhook tags: - webhooks - - components - com.kaiten.component.v1.deleted: + - entitlement-exceptions + com.kaiten.instance.entitlement_exception.v1.expired: post: - description: Triggered when a component is deleted. - operationId: onComponentDeleted + description: Triggered once per exception, when its endsAt passes; never for a revoked one. + operationId: onInstanceEntitlementExceptionExpired requestBody: content: application/json: @@ -10228,20 +36482,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Component" + $ref: "#/components/schemas/ExceptionEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - COMPONENT_DELETED + - INSTANCE_ENTITLEMENT_EXCEPTION_EXPIRED examples: - - COMPONENT_DELETED + - INSTANCE_ENTITLEMENT_EXCEPTION_EXPIRED type: string type: description: Event type enum: - - com.kaiten.component.v1.deleted + - com.kaiten.instance.entitlement_exception.v1.expired examples: - - com.kaiten.component.v1.deleted + - com.kaiten.instance.entitlement_exception.v1.expired type: string required: - name @@ -10252,14 +36508,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Component Deleted Webhook + summary: Instance Entitlement Exception Expired Webhook tags: - webhooks - - components - com.kaiten.component.v1.updated: + - entitlement-exceptions + com.kaiten.instance.entitlement_exception.v1.revoked: post: - description: Triggered when a component is updated or auto-versioned. - operationId: onComponentUpdated + description: Triggered when an exception is revoked. Revoking one already revoked or ended emits nothing. + operationId: onInstanceEntitlementExceptionRevoked requestBody: content: application/json: @@ -10267,20 +36523,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Component" + $ref: "#/components/schemas/ExceptionEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - COMPONENT_UPDATED + - INSTANCE_ENTITLEMENT_EXCEPTION_REVOKED examples: - - COMPONENT_UPDATED + - INSTANCE_ENTITLEMENT_EXCEPTION_REVOKED type: string type: description: Event type enum: - - com.kaiten.component.v1.updated + - com.kaiten.instance.entitlement_exception.v1.revoked examples: - - com.kaiten.component.v1.updated + - com.kaiten.instance.entitlement_exception.v1.revoked type: string required: - name @@ -10291,14 +36549,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Component Updated Webhook + summary: Instance Entitlement Exception Revoked Webhook tags: - webhooks - - components - com.kaiten.customer.v1.created: + - entitlement-exceptions + com.kaiten.instance.invoice.v1.handoff_acknowledged: post: - description: Triggered when a new customer is created. - operationId: onCustomerCreated + description: Triggered when the organization's accounting system acknowledges an invoice from the handoff queue, or the invoice is marked paid while still waiting there. + operationId: onInstanceInvoiceHandoffAcknowledged requestBody: content: application/json: @@ -10306,20 +36564,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Customer" + $ref: "#/components/schemas/HandoffAcknowledgementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - CUSTOMER_CREATED + - INSTANCE_INVOICE_HANDOFF_ACKNOWLEDGED examples: - - CUSTOMER_CREATED + - INSTANCE_INVOICE_HANDOFF_ACKNOWLEDGED type: string type: description: Event type enum: - - com.kaiten.customer.v1.created + - com.kaiten.instance.invoice.v1.handoff_acknowledged examples: - - com.kaiten.customer.v1.created + - com.kaiten.instance.invoice.v1.handoff_acknowledged type: string required: - name @@ -10330,14 +36590,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Customer Created Webhook + summary: Instance Invoice Handoff Acknowledged Webhook tags: - webhooks - - customers - com.kaiten.customer.v1.creation_rejected: + - billing + com.kaiten.instance.invoice.v1.held: post: - description: Triggered when a customer creation is refused because the organization has reached its customer entitlement cap. No customer is created. - operationId: onCustomerCreationRejected + description: Triggered when a period closes into an invoice held as a DRAFT, because the usage journal it was measured from failed a consistency check. It is neither issued nor handed off until released or recomposed. + operationId: onInstanceInvoiceHeld requestBody: content: application/json: @@ -10345,20 +36605,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/CustomerCreationRejected" + $ref: "#/components/schemas/HeldInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - CUSTOMER_CREATION_REJECTED + - INSTANCE_INVOICE_HELD examples: - - CUSTOMER_CREATION_REJECTED + - INSTANCE_INVOICE_HELD type: string type: description: Event type enum: - - com.kaiten.customer.v1.creation_rejected + - com.kaiten.instance.invoice.v1.held examples: - - com.kaiten.customer.v1.creation_rejected + - com.kaiten.instance.invoice.v1.held type: string required: - name @@ -10369,14 +36631,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Customer Creation Rejected Webhook + summary: Instance Invoice Held Webhook tags: - webhooks - - customers - com.kaiten.customer.v1.deleted: + - billing + com.kaiten.instance.invoice.v1.issued: post: - description: Triggered when a customer is deleted. - operationId: onCustomerDeleted + description: Triggered once per invoice with something owed, when it is issued. For an invoice the organization collects itself, it is the signal to claim it from the handoff queue, which stays the source of truth. + operationId: onInstanceInvoiceIssued requestBody: content: application/json: @@ -10384,20 +36646,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Customer" + $ref: "#/components/schemas/IssuedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - CUSTOMER_DELETED + - INSTANCE_INVOICE_ISSUED examples: - - CUSTOMER_DELETED + - INSTANCE_INVOICE_ISSUED type: string type: description: Event type enum: - - com.kaiten.customer.v1.deleted + - com.kaiten.instance.invoice.v1.issued examples: - - com.kaiten.customer.v1.deleted + - com.kaiten.instance.invoice.v1.issued type: string required: - name @@ -10408,14 +36672,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Customer Deleted Webhook + summary: Instance Invoice Issued Webhook tags: - webhooks - - customers - com.kaiten.customer.v1.updated: + - billing + com.kaiten.instance.invoice.v1.marked_uncollectible: post: - description: Triggered when a customer is updated. - operationId: onCustomerUpdated + description: Triggered when an invoice is written off. + operationId: onInstanceInvoiceMarkedUncollectible requestBody: content: application/json: @@ -10423,20 +36687,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Customer" + $ref: "#/components/schemas/UncollectibleInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - CUSTOMER_UPDATED + - INSTANCE_INVOICE_MARKED_UNCOLLECTIBLE examples: - - CUSTOMER_UPDATED + - INSTANCE_INVOICE_MARKED_UNCOLLECTIBLE type: string type: description: Event type enum: - - com.kaiten.customer.v1.updated + - com.kaiten.instance.invoice.v1.marked_uncollectible examples: - - com.kaiten.customer.v1.updated + - com.kaiten.instance.invoice.v1.marked_uncollectible type: string required: - name @@ -10447,14 +36713,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Customer Updated Webhook + summary: Instance Invoice Marked Uncollectible Webhook tags: - webhooks - - customers - com.kaiten.deployment_zone.v1.created: + - billing + com.kaiten.instance.invoice.v1.paid: post: - description: Triggered when a new deployment zone is created. - operationId: onDeploymentZoneCreated + description: "Triggered when an invoice is paid: marked paid by the organization, or issued paid because nothing was owed." + operationId: onInstanceInvoicePaid requestBody: content: application/json: @@ -10462,20 +36728,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/PaidInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - DEPLOYMENT_ZONE_CREATED + - INSTANCE_INVOICE_PAID examples: - - DEPLOYMENT_ZONE_CREATED + - INSTANCE_INVOICE_PAID type: string type: description: Event type enum: - - com.kaiten.deployment_zone.v1.created + - com.kaiten.instance.invoice.v1.paid examples: - - com.kaiten.deployment_zone.v1.created + - com.kaiten.instance.invoice.v1.paid type: string required: - name @@ -10486,14 +36754,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Deployment Zone Created Webhook + summary: Instance Invoice Paid Webhook tags: - webhooks - - deploymentZones - com.kaiten.deployment_zone.v1.deleted: + - billing + com.kaiten.instance.invoice.v1.payment_failed: post: - description: Triggered when a deployment zone is deleted. - operationId: onDeploymentZoneDeleted + description: Triggered when the automatic charge of an invoice is refused, or needs the customer to authenticate it on the invoice's hosted page. + operationId: onInstanceInvoicePaymentFailed requestBody: content: application/json: @@ -10501,20 +36769,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/PaymentFailedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - DEPLOYMENT_ZONE_DELETED + - INSTANCE_INVOICE_PAYMENT_FAILED examples: - - DEPLOYMENT_ZONE_DELETED + - INSTANCE_INVOICE_PAYMENT_FAILED type: string type: description: Event type enum: - - com.kaiten.deployment_zone.v1.deleted + - com.kaiten.instance.invoice.v1.payment_failed examples: - - com.kaiten.deployment_zone.v1.deleted + - com.kaiten.instance.invoice.v1.payment_failed type: string required: - name @@ -10525,14 +36795,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Deployment Zone Deleted Webhook + summary: Instance Invoice Payment Failed Webhook tags: - webhooks - - deploymentZones - com.kaiten.deployment_zone.v1.release_deployed: + - billing + com.kaiten.instance.invoice.v1.push_failed: post: - description: Triggered when a release is deployed to a deployment zone, either at zone creation or when an existing zone's release changes. - operationId: onReleaseDeployed + description: Triggered when pushing an invoice to its payment provider has failed as many times as the alert threshold, then once a day while it keeps failing. + operationId: onInstanceInvoicePushFailed requestBody: content: application/json: @@ -10540,20 +36810,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Deployment" + $ref: "#/components/schemas/PushFailedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - RELEASE_DEPLOYED + - INSTANCE_INVOICE_PUSH_FAILED examples: - - RELEASE_DEPLOYED + - INSTANCE_INVOICE_PUSH_FAILED type: string type: description: Event type enum: - - com.kaiten.deployment_zone.v1.release_deployed + - com.kaiten.instance.invoice.v1.push_failed examples: - - com.kaiten.deployment_zone.v1.release_deployed + - com.kaiten.instance.invoice.v1.push_failed type: string required: - name @@ -10564,14 +36836,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Release Deployed Webhook + summary: Instance Invoice Push Failed Webhook tags: - webhooks - - deploymentZones - com.kaiten.deployment_zone.v1.updated: + - billing + com.kaiten.instance.invoice.v1.pushed: post: - description: Triggered when a deployment zone is updated. - operationId: onDeploymentZoneUpdated + description: Triggered when an invoice's payment provider has issued it, right after INSTANCE_INVOICE_ISSUED. + operationId: onInstanceInvoicePushed requestBody: content: application/json: @@ -10579,20 +36851,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/DeploymentZone" + $ref: "#/components/schemas/PushedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - DEPLOYMENT_ZONE_UPDATED + - INSTANCE_INVOICE_PUSHED examples: - - DEPLOYMENT_ZONE_UPDATED + - INSTANCE_INVOICE_PUSHED type: string type: description: Event type enum: - - com.kaiten.deployment_zone.v1.updated + - com.kaiten.instance.invoice.v1.pushed examples: - - com.kaiten.deployment_zone.v1.updated + - com.kaiten.instance.invoice.v1.pushed type: string required: - name @@ -10603,14 +36877,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Deployment Zone Updated Webhook + summary: Instance Invoice Pushed Webhook tags: - webhooks - - deploymentZones - com.kaiten.entitlement.v1.created: + - billing + com.kaiten.instance.invoice.v1.reconciliation_mismatch: post: - description: Triggered when a new entitlement is created. - operationId: onEntitlementCreated + description: "Triggered when an invoice's payment provider holds amounts that differ from Kaiten's. Kaiten never corrects itself: void and recompose the invoice." + operationId: onInstanceInvoiceReconciliationMismatch requestBody: content: application/json: @@ -10618,20 +36892,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Entitlement" + $ref: "#/components/schemas/MismatchedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_CREATED + - INSTANCE_INVOICE_RECONCILIATION_MISMATCH examples: - - ENTITLEMENT_CREATED + - INSTANCE_INVOICE_RECONCILIATION_MISMATCH type: string type: description: Event type enum: - - com.kaiten.entitlement.v1.created + - com.kaiten.instance.invoice.v1.reconciliation_mismatch examples: - - com.kaiten.entitlement.v1.created + - com.kaiten.instance.invoice.v1.reconciliation_mismatch type: string required: - name @@ -10642,14 +36918,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Created Webhook + summary: Instance Invoice Reconciliation Mismatch Webhook tags: - webhooks - - entitlements - com.kaiten.entitlement.v1.deleted: + - billing + com.kaiten.instance.invoice.v1.released: post: - description: Triggered when an entitlement is deleted. - operationId: onEntitlementDeleted + description: "Triggered when a held invoice leaves its hold: released by the organization, recomposed from a sound journal, or found sound by a later check. It is issued in the same transaction." + operationId: onInstanceInvoiceReleased requestBody: content: application/json: @@ -10657,20 +36933,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Entitlement" + $ref: "#/components/schemas/ReleasedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_DELETED + - INSTANCE_INVOICE_RELEASED examples: - - ENTITLEMENT_DELETED + - INSTANCE_INVOICE_RELEASED type: string type: description: Event type enum: - - com.kaiten.entitlement.v1.deleted + - com.kaiten.instance.invoice.v1.released examples: - - com.kaiten.entitlement.v1.deleted + - com.kaiten.instance.invoice.v1.released type: string required: - name @@ -10681,14 +36959,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Deleted Webhook + summary: Instance Invoice Released Webhook tags: - webhooks - - entitlements - com.kaiten.entitlement.v1.updated: + - billing + com.kaiten.instance.invoice.v1.voided: post: - description: Triggered when an entitlement is updated. - operationId: onEntitlementUpdated + description: Triggered when an invoice is voided. Its boundary can then be recomposed into a replacement. + operationId: onInstanceInvoiceVoided requestBody: content: application/json: @@ -10696,20 +36974,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Entitlement" + $ref: "#/components/schemas/VoidedInvoiceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_UPDATED + - INSTANCE_INVOICE_VOIDED examples: - - ENTITLEMENT_UPDATED + - INSTANCE_INVOICE_VOIDED type: string type: description: Event type enum: - - com.kaiten.entitlement.v1.updated + - com.kaiten.instance.invoice.v1.voided examples: - - com.kaiten.entitlement.v1.updated + - com.kaiten.instance.invoice.v1.voided type: string required: - name @@ -10720,14 +37000,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Updated Webhook + summary: Instance Invoice Voided Webhook tags: - webhooks - - entitlements - com.kaiten.entitlement_group.v1.created: + - billing + com.kaiten.instance.v1.created: post: - description: Triggered when a new entitlement group is created. - operationId: onEntitlementGroupCreated + description: Triggered when a new instance is created. + operationId: onInstanceCreated requestBody: content: application/json: @@ -10735,20 +37015,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/EntitlementGroup" + $ref: "#/components/schemas/InstanceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_GROUP_CREATED + - INSTANCE_CREATED examples: - - ENTITLEMENT_GROUP_CREATED + - INSTANCE_CREATED type: string type: description: Event type enum: - - com.kaiten.entitlement_group.v1.created + - com.kaiten.instance.v1.created examples: - - com.kaiten.entitlement_group.v1.created + - com.kaiten.instance.v1.created type: string required: - name @@ -10759,14 +37041,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Group Created Webhook + summary: Instance Created Webhook tags: - webhooks - - entitlement-groups - com.kaiten.entitlement_group.v1.deleted: + - instances + com.kaiten.instance.v1.deleted: post: - description: Triggered when an entitlement group is deleted. - operationId: onEntitlementGroupDeleted + description: Triggered when an instance is deleted. + operationId: onInstanceDeleted requestBody: content: application/json: @@ -10774,20 +37056,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/EntitlementGroup" + $ref: "#/components/schemas/InstanceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_GROUP_DELETED + - INSTANCE_DELETED examples: - - ENTITLEMENT_GROUP_DELETED + - INSTANCE_DELETED type: string type: description: Event type enum: - - com.kaiten.entitlement_group.v1.deleted + - com.kaiten.instance.v1.deleted examples: - - com.kaiten.entitlement_group.v1.deleted + - com.kaiten.instance.v1.deleted type: string required: - name @@ -10798,14 +37082,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Group Deleted Webhook + summary: Instance Deleted Webhook tags: - webhooks - - entitlement-groups - com.kaiten.entitlement_group.v1.updated: + - instances + com.kaiten.instance.v1.deployed: post: - description: Triggered when an entitlement group is updated. - operationId: onEntitlementGroupUpdated + description: Triggered when an instance is deployed to a deployment zone. + operationId: onInstanceDeployed requestBody: content: application/json: @@ -10813,20 +37097,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/EntitlementGroup" + $ref: "#/components/schemas/InstanceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_GROUP_UPDATED + - INSTANCE_DEPLOYED examples: - - ENTITLEMENT_GROUP_UPDATED + - INSTANCE_DEPLOYED type: string type: description: Event type enum: - - com.kaiten.entitlement_group.v1.updated + - com.kaiten.instance.v1.deployed examples: - - com.kaiten.entitlement_group.v1.updated + - com.kaiten.instance.v1.deployed type: string required: - name @@ -10837,14 +37123,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Group Updated Webhook + summary: Instance Deployed Webhook tags: - webhooks - - entitlement-groups - com.kaiten.feature_flag.v1.created: + - instances + com.kaiten.instance.v1.lifecycle_stage_changed: post: - description: Triggered when a new feature flag is created. - operationId: onFeatureFlagCreated + description: Triggered when an instance commercial lifecycle stage changes. + operationId: onInstanceLifecycleStageChanged requestBody: content: application/json: @@ -10852,20 +37138,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/FeatureFlag" + $ref: "#/components/schemas/InstanceLifecycleStageChangedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - FEATURE_FLAG_CREATED + - INSTANCE_LIFECYCLE_STAGE_CHANGED examples: - - FEATURE_FLAG_CREATED + - INSTANCE_LIFECYCLE_STAGE_CHANGED type: string type: description: Event type enum: - - com.kaiten.feature_flag.v1.created + - com.kaiten.instance.v1.lifecycle_stage_changed examples: - - com.kaiten.feature_flag.v1.created + - com.kaiten.instance.v1.lifecycle_stage_changed type: string required: - name @@ -10876,14 +37164,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Feature Flag Created Webhook + summary: Instance Lifecycle Stage Changed Webhook tags: - webhooks - - featureflags - com.kaiten.feature_flag.v1.deleted: + - instances + com.kaiten.instance.v1.migrated: post: - description: Triggered when a feature flag is deleted. - operationId: onFeatureFlagDeleted + description: Triggered when an instance is migrated from one deployment zone to another. + operationId: onInstanceMigrated requestBody: content: application/json: @@ -10891,20 +37179,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/FeatureFlag" + $ref: "#/components/schemas/InstanceDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - FEATURE_FLAG_DELETED + - INSTANCE_MIGRATED examples: - - FEATURE_FLAG_DELETED + - INSTANCE_MIGRATED type: string type: description: Event type enum: - - com.kaiten.feature_flag.v1.deleted + - com.kaiten.instance.v1.migrated examples: - - com.kaiten.feature_flag.v1.deleted + - com.kaiten.instance.v1.migrated type: string required: - name @@ -10915,14 +37205,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Feature Flag Deleted Webhook + summary: Instance Migrated Webhook tags: - webhooks - - featureflags - com.kaiten.feature_flag.v1.evaluated: + - instances + com.kaiten.instance.v1.status_changed: post: - description: Triggered on every flag evaluation. The payload carries no evaluation context, so no targeting key or user attribute is exposed. - operationId: onFeatureFlagEvaluated + description: Triggered when an instance operational status changes. + operationId: onInstanceStatusChanged requestBody: content: application/json: @@ -10930,20 +37220,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/FeatureFlagEvaluated" + $ref: "#/components/schemas/InstanceStatusChangedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - FEATURE_FLAG_EVALUATED + - INSTANCE_STATUS_CHANGED examples: - - FEATURE_FLAG_EVALUATED + - INSTANCE_STATUS_CHANGED type: string type: description: Event type enum: - - com.kaiten.feature_flag.v1.evaluated + - com.kaiten.instance.v1.status_changed examples: - - com.kaiten.feature_flag.v1.evaluated + - com.kaiten.instance.v1.status_changed type: string required: - name @@ -10954,14 +37246,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Feature Flag Evaluated Webhook + summary: Instance Status Changed Webhook tags: - webhooks - - featureflags - com.kaiten.feature_flag.v1.updated: + - instances + com.kaiten.instance.v1.updated: post: - description: Triggered when a feature flag is updated. - operationId: onFeatureFlagUpdated + description: "Triggered when an instance is updated, by a write or by billing: the licence version a paid checkout bought, or the fallback a subscription that ended moves it to (licenseChangeReason BILLING_FALLBACK)." + operationId: onInstanceUpdated requestBody: content: application/json: @@ -10969,20 +37261,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/FeatureFlag" + $ref: "#/components/schemas/InstanceUpdateDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - FEATURE_FLAG_UPDATED + - INSTANCE_UPDATED examples: - - FEATURE_FLAG_UPDATED + - INSTANCE_UPDATED type: string type: description: Event type enum: - - com.kaiten.feature_flag.v1.updated + - com.kaiten.instance.v1.updated examples: - - com.kaiten.feature_flag.v1.updated + - com.kaiten.instance.v1.updated type: string required: - name @@ -10993,14 +37287,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Feature Flag Updated Webhook + summary: Instance Updated Webhook tags: - webhooks - - featureflags - com.kaiten.identity.v1.system_token_issued: + - instances + com.kaiten.instance.voucher.v1.expired: post: - description: "Triggered when a platform credential mints an organization-scoped credential for the `system:kaiten` platform identity inside this organization. Carries the platform credential that performed the mint and the scopes granted, never the credential value. Expect this at the rate an integration refreshes: roughly 120 per day per process at a 15-minute lifetime." - operationId: onSystemOrganizationTokenIssued + description: Triggered when a PRICE redemption has discounted its last invoice, or a boost's window ends. + operationId: onInstanceVoucherExpired requestBody: content: application/json: @@ -11008,20 +37302,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/SystemTokenIssuance" + $ref: "#/components/schemas/RedemptionDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - SYSTEM_ORGANIZATION_TOKEN_ISSUED + - INSTANCE_VOUCHER_EXPIRED examples: - - SYSTEM_ORGANIZATION_TOKEN_ISSUED + - INSTANCE_VOUCHER_EXPIRED type: string type: description: Event type enum: - - com.kaiten.identity.v1.system_token_issued + - com.kaiten.instance.voucher.v1.expired examples: - - com.kaiten.identity.v1.system_token_issued + - com.kaiten.instance.voucher.v1.expired type: string required: - name @@ -11032,14 +37328,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: System Organization Token Issued Webhook + summary: Instance Voucher Expired Webhook tags: - webhooks - - identity - com.kaiten.instance.entitlement.v1.cap_exceeded: + - vouchers + com.kaiten.instance.voucher.v1.redeemed: post: - description: Triggered when a SOFT-enforcement entitlement's usage crosses from below the cap to above it. Fires once per crossing. - operationId: onInstanceEntitlementCapExceeded + description: Triggered when an instance redeems a voucher. + operationId: onInstanceVoucherRedeemed requestBody: content: application/json: @@ -11047,20 +37343,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceEntitlementCapExceeded" + $ref: "#/components/schemas/RedemptionDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_ENTITLEMENT_CAP_EXCEEDED + - INSTANCE_VOUCHER_REDEEMED examples: - - INSTANCE_ENTITLEMENT_CAP_EXCEEDED + - INSTANCE_VOUCHER_REDEEMED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.cap_exceeded + - com.kaiten.instance.voucher.v1.redeemed examples: - - com.kaiten.instance.entitlement.v1.cap_exceeded + - com.kaiten.instance.voucher.v1.redeemed type: string required: - name @@ -11071,14 +37369,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Entitlement Cap Exceeded Webhook + summary: Instance Voucher Redeemed Webhook tags: - webhooks - - instances - com.kaiten.instance.entitlement.v1.usage_period_rolled_over: + - vouchers + com.kaiten.instance.voucher.v1.revoked: post: - description: "Triggered once per usage window closed by a report: the entitlement's own stored bucket, and one synthetic event per window skipped entirely since the last report." - operationId: onInstanceEntitlementUsagePeriodRolledOver + description: Triggered when a redemption is revoked. + operationId: onInstanceVoucherRevoked requestBody: content: application/json: @@ -11086,20 +37384,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceEntitlementUsagePeriodRolledOver" + $ref: "#/components/schemas/RedemptionDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER + - INSTANCE_VOUCHER_REVOKED examples: - - INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER + - INSTANCE_VOUCHER_REVOKED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.usage_period_rolled_over + - com.kaiten.instance.voucher.v1.revoked examples: - - com.kaiten.instance.entitlement.v1.usage_period_rolled_over + - com.kaiten.instance.voucher.v1.revoked type: string required: - name @@ -11110,14 +37410,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Usage Period Rolled Over Webhook + summary: Instance Voucher Revoked Webhook tags: - webhooks - - instances - com.kaiten.instance.entitlement.v1.usage_reached: + - vouchers + com.kaiten.license.entitlement.v1.assigned: post: - description: Triggered when an instance's entitlement usage reaches its threshold. - operationId: onInstanceEntitlementUsageReached + description: Triggered when an entitlement is assigned to a license. + operationId: onLicenseEntitlementAssigned requestBody: content: application/json: @@ -11125,20 +37425,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/EntitlementUsage" + $ref: "#/components/schemas/LicenseEntitlementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_ENTITLEMENT_USAGE_REACHED + - LICENSE_ENTITLEMENT_ASSIGNED examples: - - INSTANCE_ENTITLEMENT_USAGE_REACHED + - LICENSE_ENTITLEMENT_ASSIGNED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.usage_reached + - com.kaiten.license.entitlement.v1.assigned examples: - - com.kaiten.instance.entitlement.v1.usage_reached + - com.kaiten.license.entitlement.v1.assigned type: string required: - name @@ -11149,14 +37451,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Entitlement Usage Reached Webhook + summary: License Entitlement Assigned Webhook tags: - webhooks - - instances - com.kaiten.instance.entitlement.v1.usage_report_accepted: + - licenses + com.kaiten.license.entitlement.v1.unassigned: post: - description: Triggered when a usage report is persisted, including a SOFT-enforcement overage. - operationId: onInstanceEntitlementUsageReportAccepted + description: Triggered when an entitlement is unassigned from a license. + operationId: onLicenseEntitlementUnassigned requestBody: content: application/json: @@ -11164,20 +37466,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceEntitlementUsageReportAccepted" + $ref: "#/components/schemas/LicenseEntitlementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_USAGE_REPORT_ACCEPTED + - LICENSE_ENTITLEMENT_UNASSIGNED examples: - - ENTITLEMENT_USAGE_REPORT_ACCEPTED + - LICENSE_ENTITLEMENT_UNASSIGNED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.usage_report_accepted + - com.kaiten.license.entitlement.v1.unassigned examples: - - com.kaiten.instance.entitlement.v1.usage_report_accepted + - com.kaiten.license.entitlement.v1.unassigned type: string required: - name @@ -11188,14 +37492,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Usage Report Accepted Webhook + summary: License Entitlement Unassigned Webhook tags: - webhooks - - instances - com.kaiten.instance.entitlement.v1.usage_report_rejected: + - licenses + com.kaiten.license.entitlement.v1.updated: post: - description: Triggered when a usage report is refused because it would take a HARD-enforcement entitlement past its cap. The usage is not persisted. - operationId: onInstanceEntitlementUsageReportRejected + description: Triggered when a license entitlement is updated. + operationId: onLicenseEntitlementUpdated requestBody: content: application/json: @@ -11203,20 +37507,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceEntitlementUsageReportAccepted" + $ref: "#/components/schemas/LicenseEntitlementDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_USAGE_REPORT_REJECTED + - LICENSE_ENTITLEMENT_UPDATED examples: - - ENTITLEMENT_USAGE_REPORT_REJECTED + - LICENSE_ENTITLEMENT_UPDATED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.usage_report_rejected + - com.kaiten.license.entitlement.v1.updated examples: - - com.kaiten.instance.entitlement.v1.usage_report_rejected + - com.kaiten.license.entitlement.v1.updated type: string required: - name @@ -11227,14 +37533,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Entitlement Usage Report Rejected Webhook + summary: License Entitlement Updated Webhook tags: - webhooks - - instances - com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached: + - licenses + com.kaiten.license.price.v1.created: post: - description: Triggered when an instance's entitlement usage crosses its configured early-warning percentage boundary, before the cap is reached. Fires once per crossing. - operationId: onInstanceEntitlementUsageWarningThresholdReached + description: Triggered when a price is added to a licence version. + operationId: onLicensePriceCreated requestBody: content: application/json: @@ -11242,20 +37548,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceEntitlementUsageWarningThresholdReached" + $ref: "#/components/schemas/LicensePriceEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED + - LICENSE_PRICE_CREATED examples: - - INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED + - LICENSE_PRICE_CREATED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached + - com.kaiten.license.price.v1.created examples: - - com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached + - com.kaiten.license.price.v1.created type: string required: - name @@ -11266,14 +37574,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Entitlement Usage Warning Threshold Reached Webhook + summary: License Price Created Webhook tags: - webhooks - - instances - com.kaiten.instance.entitlement.v1.value_get: + - licenses + com.kaiten.license.price.v1.deprecated: post: - description: Triggered on every read of an instance entitlement's usage value. This is a read audit signal, not a state change. - operationId: onInstanceEntitlementValueGet + description: Triggered when a licence price is deprecated. + operationId: onLicensePriceDeprecated requestBody: content: application/json: @@ -11281,20 +37589,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceEntitlementValueGet" + $ref: "#/components/schemas/LicensePriceEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - ENTITLEMENT_VALUE_GET + - LICENSE_PRICE_DEPRECATED examples: - - ENTITLEMENT_VALUE_GET + - LICENSE_PRICE_DEPRECATED type: string type: description: Event type enum: - - com.kaiten.instance.entitlement.v1.value_get + - com.kaiten.license.price.v1.deprecated examples: - - com.kaiten.instance.entitlement.v1.value_get + - com.kaiten.license.price.v1.deprecated type: string required: - name @@ -11305,14 +37615,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Entitlement Value Read Webhook + summary: License Price Deprecated Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.created: + - licenses + com.kaiten.license.price.v1.updated: post: - description: Triggered when a new instance is created. - operationId: onInstanceCreated + description: Triggered when a price of a DRAFT licence version is edited. + operationId: onLicensePriceUpdated requestBody: content: application/json: @@ -11320,20 +37630,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Instance" + $ref: "#/components/schemas/LicensePriceUpdatedEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_CREATED + - LICENSE_PRICE_UPDATED examples: - - INSTANCE_CREATED + - LICENSE_PRICE_UPDATED type: string type: description: Event type enum: - - com.kaiten.instance.v1.created + - com.kaiten.license.price.v1.updated examples: - - com.kaiten.instance.v1.created + - com.kaiten.license.price.v1.updated type: string required: - name @@ -11344,14 +37656,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Created Webhook + summary: License Price Updated Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.deleted: + - licenses + com.kaiten.license.v1.archived: post: - description: Triggered when an instance is deleted. - operationId: onInstanceDeleted + description: Triggered when a published license version is archived and withdrawn from sale. + operationId: onLicenseArchived requestBody: content: application/json: @@ -11359,20 +37671,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Instance" + $ref: "#/components/schemas/LicenseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_DELETED + - LICENSE_ARCHIVED examples: - - INSTANCE_DELETED + - LICENSE_ARCHIVED type: string type: description: Event type enum: - - com.kaiten.instance.v1.deleted + - com.kaiten.license.v1.archived examples: - - com.kaiten.instance.v1.deleted + - com.kaiten.license.v1.archived type: string required: - name @@ -11383,14 +37697,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Deleted Webhook + summary: License Archived Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.deployed: + - licenses + com.kaiten.license.v1.created: post: - description: Triggered when an instance is deployed to a deployment zone. - operationId: onInstanceDeployed + description: Triggered when a new license is created. + operationId: onLicenseCreated requestBody: content: application/json: @@ -11398,20 +37712,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Instance" + $ref: "#/components/schemas/LicenseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_DEPLOYED + - LICENSE_CREATED examples: - - INSTANCE_DEPLOYED + - LICENSE_CREATED type: string type: description: Event type enum: - - com.kaiten.instance.v1.deployed + - com.kaiten.license.v1.created examples: - - com.kaiten.instance.v1.deployed + - com.kaiten.license.v1.created type: string required: - name @@ -11422,14 +37738,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Deployed Webhook + summary: License Created Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.lifecycle_stage_changed: + - licenses + com.kaiten.license.v1.deleted: post: - description: Triggered when an instance commercial lifecycle stage changes. - operationId: onInstanceLifecycleStageChanged + description: Triggered when a license is deleted. + operationId: onLicenseDeleted requestBody: content: application/json: @@ -11437,20 +37753,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceLifecycleStageChanged" + $ref: "#/components/schemas/LicenseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_LIFECYCLE_STAGE_CHANGED + - LICENSE_DELETED examples: - - INSTANCE_LIFECYCLE_STAGE_CHANGED + - LICENSE_DELETED type: string type: description: Event type enum: - - com.kaiten.instance.v1.lifecycle_stage_changed + - com.kaiten.license.v1.deleted examples: - - com.kaiten.instance.v1.lifecycle_stage_changed + - com.kaiten.license.v1.deleted type: string required: - name @@ -11461,14 +37779,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Lifecycle Stage Changed Webhook + summary: License Deleted Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.migrated: + - licenses + com.kaiten.license.v1.published: post: - description: Triggered when an instance is migrated from one deployment zone to another. - operationId: onInstanceMigrated + description: Triggered when a draft license version is published and goes on sale. + operationId: onLicensePublished requestBody: content: application/json: @@ -11476,20 +37794,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Instance" + $ref: "#/components/schemas/LicenseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_MIGRATED + - LICENSE_PUBLISHED examples: - - INSTANCE_MIGRATED + - LICENSE_PUBLISHED type: string type: description: Event type enum: - - com.kaiten.instance.v1.migrated + - com.kaiten.license.v1.published examples: - - com.kaiten.instance.v1.migrated + - com.kaiten.license.v1.published type: string required: - name @@ -11500,14 +37820,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Migrated Webhook + summary: License Published Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.status_changed: + - licenses + com.kaiten.license.v1.unarchived: post: - description: Triggered when an instance operational status changes. - operationId: onInstanceStatusChanged + description: Triggered when an archived license version is put back on sale. + operationId: onLicenseUnarchived requestBody: content: application/json: @@ -11515,20 +37835,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/InstanceStatusChanged" + $ref: "#/components/schemas/LicenseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_STATUS_CHANGED + - LICENSE_UNARCHIVED examples: - - INSTANCE_STATUS_CHANGED + - LICENSE_UNARCHIVED type: string type: description: Event type enum: - - com.kaiten.instance.v1.status_changed + - com.kaiten.license.v1.unarchived examples: - - com.kaiten.instance.v1.status_changed + - com.kaiten.license.v1.unarchived type: string required: - name @@ -11539,14 +37861,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Status Changed Webhook + summary: License Unarchived Webhook tags: - webhooks - - instances - com.kaiten.instance.v1.updated: + - licenses + com.kaiten.license.v1.updated: post: - description: Triggered when an instance is updated. - operationId: onInstanceUpdated + description: Triggered when a license is updated. + operationId: onLicenseUpdated requestBody: content: application/json: @@ -11554,20 +37876,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Instance" + $ref: "#/components/schemas/LicenseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - INSTANCE_UPDATED + - LICENSE_UPDATED examples: - - INSTANCE_UPDATED + - LICENSE_UPDATED type: string type: description: Event type enum: - - com.kaiten.instance.v1.updated + - com.kaiten.license.v1.updated examples: - - com.kaiten.instance.v1.updated + - com.kaiten.license.v1.updated type: string required: - name @@ -11578,14 +37902,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Instance Updated Webhook + summary: License Updated Webhook tags: - webhooks - - instances - com.kaiten.license.entitlement.v1.assigned: + - licenses + com.kaiten.license_family.v1.created: post: - description: Triggered when an entitlement is assigned to a license. - operationId: onLicenseEntitlementAssigned + description: "Triggered when a license is created without naming a family: its version 1 opens a new family, which this event carries, with the version it serves -- none if that version is a draft. It is recorded in the same transaction as that version's LICENSE_CREATED." + operationId: onLicenseFamilyCreated requestBody: content: application/json: @@ -11593,20 +37917,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/LicenseEntitlement" + $ref: "#/components/schemas/LicenseFamilyViewDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_ENTITLEMENT_ASSIGNED + - LICENSE_FAMILY_CREATED examples: - - LICENSE_ENTITLEMENT_ASSIGNED + - LICENSE_FAMILY_CREATED type: string type: description: Event type enum: - - com.kaiten.license.entitlement.v1.assigned + - com.kaiten.license_family.v1.created examples: - - com.kaiten.license.entitlement.v1.assigned + - com.kaiten.license_family.v1.created type: string required: - name @@ -11617,14 +37943,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Entitlement Assigned Webhook + summary: License Family Created Webhook tags: - webhooks - - licenses - com.kaiten.license.entitlement.v1.unassigned: + - license-families + com.kaiten.license_family.v1.deleted: post: - description: Triggered when an entitlement is unassigned from a license. - operationId: onLicenseEntitlementUnassigned + description: Triggered when a license family's last version is deleted, which deletes the family and frees its slug. It carries the family as it stood before that version went, and is recorded in the same transaction as that version's LICENSE_DELETED. + operationId: onLicenseFamilyDeleted requestBody: content: application/json: @@ -11632,20 +37958,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/LicenseEntitlement" + $ref: "#/components/schemas/LicenseFamilyViewDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_ENTITLEMENT_UNASSIGNED + - LICENSE_FAMILY_DELETED examples: - - LICENSE_ENTITLEMENT_UNASSIGNED + - LICENSE_FAMILY_DELETED type: string type: description: Event type enum: - - com.kaiten.license.entitlement.v1.unassigned + - com.kaiten.license_family.v1.deleted examples: - - com.kaiten.license.entitlement.v1.unassigned + - com.kaiten.license_family.v1.deleted type: string required: - name @@ -11656,14 +37984,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Entitlement Unassigned Webhook + summary: License Family Deleted Webhook tags: - webhooks - - licenses - com.kaiten.license.entitlement.v1.updated: + - license-families + com.kaiten.license_family.v1.updated: post: - description: Triggered when a license entitlement is updated. - operationId: onLicenseEntitlementUpdated + description: "Triggered when a license family starts serving another version, or none: a newer version published, a version published, archived or unarchived, the family's default taken or given up, or the served version deleted. It carries the family with the version it serves now, and is recorded in the same transaction as the version event that caused the change. A change that leaves the served version where it was -- a draft added, a version renamed, a default restated -- triggers none." + operationId: onLicenseFamilyUpdated requestBody: content: application/json: @@ -11671,20 +37999,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/LicenseEntitlement" + $ref: "#/components/schemas/LicenseFamilyViewDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_ENTITLEMENT_UPDATED + - LICENSE_FAMILY_UPDATED examples: - - LICENSE_ENTITLEMENT_UPDATED + - LICENSE_FAMILY_UPDATED type: string type: description: Event type enum: - - com.kaiten.license.entitlement.v1.updated + - com.kaiten.license_family.v1.updated examples: - - com.kaiten.license.entitlement.v1.updated + - com.kaiten.license_family.v1.updated type: string required: - name @@ -11695,14 +38025,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Entitlement Updated Webhook + summary: License Family Updated Webhook tags: - webhooks - - licenses - com.kaiten.license.v1.archived: + - license-families + com.kaiten.metadata_field.v1.archived: post: - description: Triggered when a published license version is archived and withdrawn from sale. - operationId: onLicenseArchived + description: Triggered when a metadata field is archived and leaves the active schema composition. + operationId: onMetadataFieldArchived requestBody: content: application/json: @@ -11710,20 +38040,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/License" + $ref: "#/components/schemas/MetadataFieldDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_ARCHIVED + - METADATA_FIELD_ARCHIVED examples: - - LICENSE_ARCHIVED + - METADATA_FIELD_ARCHIVED type: string type: description: Event type enum: - - com.kaiten.license.v1.archived + - com.kaiten.metadata_field.v1.archived examples: - - com.kaiten.license.v1.archived + - com.kaiten.metadata_field.v1.archived type: string required: - name @@ -11734,14 +38066,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Archived Webhook + summary: Metadata Field Archived Webhook tags: - webhooks - - licenses - com.kaiten.license.v1.created: + - metadataFields + com.kaiten.metadata_field.v1.created: post: - description: Triggered when a new license is created. - operationId: onLicenseCreated + description: Triggered when a metadata field is declared for a resource type. + operationId: onMetadataFieldCreated requestBody: content: application/json: @@ -11749,20 +38081,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/License" + $ref: "#/components/schemas/MetadataFieldDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_CREATED + - METADATA_FIELD_CREATED examples: - - LICENSE_CREATED + - METADATA_FIELD_CREATED type: string type: description: Event type enum: - - com.kaiten.license.v1.created + - com.kaiten.metadata_field.v1.created examples: - - com.kaiten.license.v1.created + - com.kaiten.metadata_field.v1.created type: string required: - name @@ -11773,14 +38107,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Created Webhook + summary: Metadata Field Created Webhook tags: - webhooks - - licenses - com.kaiten.license.v1.deleted: + - metadataFields + com.kaiten.metadata_field.v1.reordered: post: - description: Triggered when a license is deleted. - operationId: onLicenseDeleted + description: Triggered when the display order of a resource type's metadata fields is rewritten. + operationId: onMetadataFieldReordered requestBody: content: application/json: @@ -11788,20 +38122,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/License" + $ref: "#/components/schemas/MetadataFieldReorderedDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_DELETED + - METADATA_FIELD_REORDERED examples: - - LICENSE_DELETED + - METADATA_FIELD_REORDERED type: string type: description: Event type enum: - - com.kaiten.license.v1.deleted + - com.kaiten.metadata_field.v1.reordered examples: - - com.kaiten.license.v1.deleted + - com.kaiten.metadata_field.v1.reordered type: string required: - name @@ -11812,14 +38148,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Deleted Webhook + summary: Metadata Fields Reordered Webhook tags: - webhooks - - licenses - com.kaiten.license.v1.published: + - metadataFields + com.kaiten.metadata_field.v1.unarchived: post: - description: Triggered when a draft license version is published and goes on sale. - operationId: onLicensePublished + description: Triggered when an archived metadata field is restored to the active schema composition. + operationId: onMetadataFieldUnarchived requestBody: content: application/json: @@ -11827,20 +38163,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/License" + $ref: "#/components/schemas/MetadataFieldDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_PUBLISHED + - METADATA_FIELD_UNARCHIVED examples: - - LICENSE_PUBLISHED + - METADATA_FIELD_UNARCHIVED type: string type: description: Event type enum: - - com.kaiten.license.v1.published + - com.kaiten.metadata_field.v1.unarchived examples: - - com.kaiten.license.v1.published + - com.kaiten.metadata_field.v1.unarchived type: string required: - name @@ -11851,14 +38189,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Published Webhook + summary: Metadata Field Unarchived Webhook tags: - webhooks - - licenses - com.kaiten.license.v1.unarchived: + - metadataFields + com.kaiten.metadata_field.v1.updated: post: - description: Triggered when an archived license version is put back on sale. - operationId: onLicenseUnarchived + description: Triggered when a metadata field's label or JSON schema changes. A reordering emits METADATA_FIELD_REORDERED instead. + operationId: onMetadataFieldUpdated requestBody: content: application/json: @@ -11866,20 +38204,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/License" + $ref: "#/components/schemas/MetadataFieldDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_UNARCHIVED + - METADATA_FIELD_UPDATED examples: - - LICENSE_UNARCHIVED + - METADATA_FIELD_UPDATED type: string type: description: Event type enum: - - com.kaiten.license.v1.unarchived + - com.kaiten.metadata_field.v1.updated examples: - - com.kaiten.license.v1.unarchived + - com.kaiten.metadata_field.v1.updated type: string required: - name @@ -11890,14 +38230,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Unarchived Webhook + summary: Metadata Field Updated Webhook tags: - webhooks - - licenses - com.kaiten.license.v1.updated: + - metadataFields + com.kaiten.publishable_key.v1.created: post: - description: Triggered when a license is updated. - operationId: onLicenseUpdated + description: Triggered when a publishable key is issued. The key is never in the payload, only its hint. + operationId: onPublishableKeyCreated requestBody: content: application/json: @@ -11905,20 +38245,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/License" + $ref: "#/components/schemas/PublishableKeyEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_UPDATED + - PUBLISHABLE_KEY_CREATED examples: - - LICENSE_UPDATED + - PUBLISHABLE_KEY_CREATED type: string type: description: Event type enum: - - com.kaiten.license.v1.updated + - com.kaiten.publishable_key.v1.created examples: - - com.kaiten.license.v1.updated + - com.kaiten.publishable_key.v1.created type: string required: - name @@ -11929,14 +38271,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Updated Webhook + summary: Publishable Key Created Webhook tags: - webhooks - - licenses - com.kaiten.license_family.v1.created: + - publishable-keys + com.kaiten.publishable_key.v1.revoked: post: - description: "Triggered when a license is created without naming a family: its version 1 opens a new family, which this event carries, with the version it serves -- none if that version is a draft. It is recorded in the same transaction as that version's LICENSE_CREATED." - operationId: onLicenseFamilyCreated + description: Triggered once, when a publishable key is revoked. + operationId: onPublishableKeyRevoked requestBody: content: application/json: @@ -11944,20 +38286,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/LicenseFamilyView" + $ref: "#/components/schemas/PublishableKeyEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_FAMILY_CREATED + - PUBLISHABLE_KEY_REVOKED examples: - - LICENSE_FAMILY_CREATED + - PUBLISHABLE_KEY_REVOKED type: string type: description: Event type enum: - - com.kaiten.license_family.v1.created + - com.kaiten.publishable_key.v1.revoked examples: - - com.kaiten.license_family.v1.created + - com.kaiten.publishable_key.v1.revoked type: string required: - name @@ -11968,14 +38312,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Family Created Webhook + summary: Publishable Key Revoked Webhook tags: - webhooks - - license-families - com.kaiten.license_family.v1.deleted: + - publishable-keys + com.kaiten.release.v1.created: post: - description: Triggered when a license family's last version is deleted, which deletes the family and frees its slug. It carries the family as it stood before that version went, and is recorded in the same transaction as that version's LICENSE_DELETED. - operationId: onLicenseFamilyDeleted + description: Triggered when a new release is created. + operationId: onReleaseCreated requestBody: content: application/json: @@ -11983,20 +38327,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/LicenseFamilyView" + $ref: "#/components/schemas/ReleaseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_FAMILY_DELETED + - RELEASE_CREATED examples: - - LICENSE_FAMILY_DELETED + - RELEASE_CREATED type: string type: description: Event type enum: - - com.kaiten.license_family.v1.deleted + - com.kaiten.release.v1.created examples: - - com.kaiten.license_family.v1.deleted + - com.kaiten.release.v1.created type: string required: - name @@ -12007,14 +38353,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Family Deleted Webhook + summary: Release Created Webhook tags: - webhooks - - license-families - com.kaiten.license_family.v1.updated: + - releases + com.kaiten.release.v1.deleted: post: - description: "Triggered when a license family starts serving another version, or none: a newer version published, a version published, archived or unarchived, the family's default taken or given up, or the served version deleted. It carries the family with the version it serves now, and is recorded in the same transaction as the version event that caused the change. A change that leaves the served version where it was -- a draft added, a version renamed, a default restated -- triggers none." - operationId: onLicenseFamilyUpdated + description: Triggered when a release is deleted. + operationId: onReleaseDeleted requestBody: content: application/json: @@ -12022,20 +38368,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/LicenseFamilyView" + $ref: "#/components/schemas/ReleaseDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - LICENSE_FAMILY_UPDATED + - RELEASE_DELETED examples: - - LICENSE_FAMILY_UPDATED + - RELEASE_DELETED type: string type: description: Event type enum: - - com.kaiten.license_family.v1.updated + - com.kaiten.release.v1.deleted examples: - - com.kaiten.license_family.v1.updated + - com.kaiten.release.v1.deleted type: string required: - name @@ -12046,14 +38394,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: License Family Updated Webhook + summary: Release Deleted Webhook tags: - webhooks - - license-families - com.kaiten.metadata_field.v1.archived: + - releases + com.kaiten.voucher.v1.archived: post: - description: Triggered when a metadata field is archived and leaves the active schema composition. - operationId: onMetadataFieldArchived + description: Triggered when a voucher is archived. + operationId: onVoucherArchived requestBody: content: application/json: @@ -12061,20 +38409,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/MetadataField" + $ref: "#/components/schemas/VoucherDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - METADATA_FIELD_ARCHIVED + - VOUCHER_ARCHIVED examples: - - METADATA_FIELD_ARCHIVED + - VOUCHER_ARCHIVED type: string type: description: Event type enum: - - com.kaiten.metadata_field.v1.archived + - com.kaiten.voucher.v1.archived examples: - - com.kaiten.metadata_field.v1.archived + - com.kaiten.voucher.v1.archived type: string required: - name @@ -12085,14 +38435,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Metadata Field Archived Webhook + summary: Voucher Archived Webhook tags: - webhooks - - metadataFields - com.kaiten.metadata_field.v1.created: + - vouchers + com.kaiten.voucher.v1.created: post: - description: Triggered when a metadata field is declared for a resource type. - operationId: onMetadataFieldCreated + description: Triggered when a voucher is created. The code is never in the payload. + operationId: onVoucherCreated requestBody: content: application/json: @@ -12100,20 +38450,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/MetadataField" + $ref: "#/components/schemas/VoucherDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - METADATA_FIELD_CREATED + - VOUCHER_CREATED examples: - - METADATA_FIELD_CREATED + - VOUCHER_CREATED type: string type: description: Event type enum: - - com.kaiten.metadata_field.v1.created + - com.kaiten.voucher.v1.created examples: - - com.kaiten.metadata_field.v1.created + - com.kaiten.voucher.v1.created type: string required: - name @@ -12124,14 +38476,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Metadata Field Created Webhook + summary: Voucher Created Webhook tags: - webhooks - - metadataFields - com.kaiten.metadata_field.v1.reordered: + - vouchers + com.kaiten.voucher.v1.deleted: post: - description: Triggered when the display order of a resource type's metadata fields is rewritten. - operationId: onMetadataFieldReordered + description: Triggered when a voucher no instance ever redeemed is deleted, with its last state. + operationId: onVoucherDeleted requestBody: content: application/json: @@ -12139,20 +38491,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/MetadataFieldReordered" + $ref: "#/components/schemas/VoucherDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - METADATA_FIELD_REORDERED + - VOUCHER_DELETED examples: - - METADATA_FIELD_REORDERED + - VOUCHER_DELETED type: string type: description: Event type enum: - - com.kaiten.metadata_field.v1.reordered + - com.kaiten.voucher.v1.deleted examples: - - com.kaiten.metadata_field.v1.reordered + - com.kaiten.voucher.v1.deleted type: string required: - name @@ -12163,14 +38517,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Metadata Fields Reordered Webhook + summary: Voucher Deleted Webhook tags: - webhooks - - metadataFields - com.kaiten.metadata_field.v1.unarchived: + - vouchers + com.kaiten.voucher.v1.exhausted: post: - description: Triggered when an archived metadata field is restored to the active schema composition. - operationId: onMetadataFieldUnarchived + description: Triggered by the redemption that reaches maxRedemptions. + operationId: onVoucherExhausted requestBody: content: application/json: @@ -12178,20 +38532,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/MetadataField" + $ref: "#/components/schemas/ExhaustedEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - METADATA_FIELD_UNARCHIVED + - VOUCHER_EXHAUSTED examples: - - METADATA_FIELD_UNARCHIVED + - VOUCHER_EXHAUSTED type: string type: description: Event type enum: - - com.kaiten.metadata_field.v1.unarchived + - com.kaiten.voucher.v1.exhausted examples: - - com.kaiten.metadata_field.v1.unarchived + - com.kaiten.voucher.v1.exhausted type: string required: - name @@ -12202,14 +38558,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Metadata Field Unarchived Webhook + summary: Voucher Exhausted Webhook tags: - webhooks - - metadataFields - com.kaiten.metadata_field.v1.updated: + - vouchers + com.kaiten.voucher.v1.expired: post: - description: Triggered when a metadata field's label or JSON schema changes. A reordering emits METADATA_FIELD_REORDERED instead. - operationId: onMetadataFieldUpdated + description: Triggered when an active voucher reaches its expiresAt and can no longer be redeemed. + operationId: onVoucherExpired requestBody: content: application/json: @@ -12217,20 +38573,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/MetadataField" + $ref: "#/components/schemas/ExpiredEventDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - METADATA_FIELD_UPDATED + - VOUCHER_EXPIRED examples: - - METADATA_FIELD_UPDATED + - VOUCHER_EXPIRED type: string type: description: Event type enum: - - com.kaiten.metadata_field.v1.updated + - com.kaiten.voucher.v1.expired examples: - - com.kaiten.metadata_field.v1.updated + - com.kaiten.voucher.v1.expired type: string required: - name @@ -12241,14 +38599,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Metadata Field Updated Webhook + summary: Voucher Expired Webhook tags: - webhooks - - metadataFields - com.kaiten.release.v1.created: + - vouchers + com.kaiten.voucher.v1.published: post: - description: Triggered when a new release is created. - operationId: onReleaseCreated + description: Triggered when a draft voucher becomes redeemable. + operationId: onVoucherPublished requestBody: content: application/json: @@ -12256,20 +38614,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Release" + $ref: "#/components/schemas/VoucherDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - RELEASE_CREATED + - VOUCHER_PUBLISHED examples: - - RELEASE_CREATED + - VOUCHER_PUBLISHED type: string type: description: Event type enum: - - com.kaiten.release.v1.created + - com.kaiten.voucher.v1.published examples: - - com.kaiten.release.v1.created + - com.kaiten.voucher.v1.published type: string required: - name @@ -12280,14 +38640,14 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Release Created Webhook + summary: Voucher Published Webhook tags: - webhooks - - releases - com.kaiten.release.v1.deleted: + - vouchers + com.kaiten.voucher.v1.updated: post: - description: Triggered when a release is deleted. - operationId: onReleaseDeleted + description: Triggered when a voucher changes. + operationId: onVoucherUpdated requestBody: content: application/json: @@ -12295,20 +38655,22 @@ webhooks: additionalProperties: false properties: data: - $ref: "#/components/schemas/Release" + $ref: "#/components/schemas/VoucherDelivered" + headers: + $ref: "#/components/schemas/EventHeaders" name: description: Event name enum: - - RELEASE_DELETED + - VOUCHER_UPDATED examples: - - RELEASE_DELETED + - VOUCHER_UPDATED type: string type: description: Event type enum: - - com.kaiten.release.v1.deleted + - com.kaiten.voucher.v1.updated examples: - - com.kaiten.release.v1.deleted + - com.kaiten.voucher.v1.updated type: string required: - name @@ -12319,7 +38681,7 @@ webhooks: responses: "200": description: Webhook processed successfully - summary: Release Deleted Webhook + summary: Voucher Updated Webhook tags: - webhooks - - releases + - vouchers diff --git a/openapi/platform-openapi.yaml b/openapi/platform-openapi.yaml index 38b05bc..71fc09d 100644 --- a/openapi/platform-openapi.yaml +++ b/openapi/platform-openapi.yaml @@ -1,5 +1,84 @@ components: schemas: + ClosePeriodsReport: + additionalProperties: false + properties: + closed: + description: Periods closed into an invoice + format: int64 + type: integer + examined: + description: Subscriptions the batch looked at + format: int64 + type: integer + hasMore: + description: Set when due subscriptions are left beyond the batch + type: boolean + held: + description: Of those, invoices held as DRAFTs for their usage journal + format: int64 + type: integer + invoices: + items: + $ref: "#/components/schemas/ClosedInvoice" + type: array + skipped: + description: "Subscriptions left for later: held by another transaction, their meters' clock not at the boundary, or failed (logged)" + format: int64 + type: integer + required: + - examined + - closed + - held + - skipped + - hasMore + - invoices + type: object + ClosePeriodsScope: + additionalProperties: false + properties: + instanceSlug: + description: Close only this instance's subscription + type: string + type: object + ClosedInvoice: + additionalProperties: false + properties: + boundaryAt: + format: date-time + type: string + held: + description: Set when the invoice is a held DRAFT + type: boolean + id: + type: string + instanceSlug: + type: string + kind: + enum: + - ACTIVATION + - RENEWAL + - FINAL + type: string + status: + enum: + - DRAFT + - PUSHED + - PUSH_FAILED + - MANUAL + - PAID + - PAYMENT_FAILED + - UNCOLLECTIBLE + - VOID + type: string + required: + - id + - instanceSlug + - kind + - boundaryAt + - status + - held + type: object Connector: additionalProperties: false properties: @@ -365,7 +444,7 @@ openapi: 3.1.0 paths: /platform/connectors: post: - description: Registers or updates connector metadata and settings schema for this Kaiten deployment + description: Registers or updates connector metadata and settings schema for this Kaiten installation operationId: register-connector requestBody: content: @@ -454,7 +533,7 @@ paths: - platform /platform/organizations: post: - description: "Converges on the organization an external identity provider describes, creating it if it is not already there and returning it either way. The internal id is derived from the external id, so the operation is idempotent: a re-run returns the same organization instead of creating a second tenant, and an organization that already exists -- including one provisioned on a first login -- is returned unchanged apart from acquiring a name if it had none. Platform API: requires a Kaiten platform token (`ksm_...`) with write:organizations." + description: "Converges on the organization an external identity provider describes, creating it if it is not already there and returning it either way. The internal id is derived from the external id, so the operation is idempotent: a re-run returns the same organization instead of creating a second one, and an organization that already exists -- including one provisioned on a first login -- is returned unchanged apart from acquiring a name if it had none. Platform API: requires a Kaiten platform token (`ksm_...`) with write:organizations." operationId: ensure-organization requestBody: content: @@ -612,6 +691,86 @@ paths: summary: Get an organization by ID tags: - organizations + /platform/organizations/{orgId}/billing/close-periods: + post: + description: "Closes the subscriptions whose period has ended now, rather than at the period-close job's next pass: each one's RENEWAL invoice is composed and issued (or held, when its usage journal fails a check) and its period advances. A subscription several periods behind closes one period at a time until it is current. Only what is due closes, at most the configured batch size per call. Platform API: requires a Kaiten platform token (`ksm_...`) with write:billing; the closes are recorded under system:kaiten." + operationId: platform-close-billing-periods + parameters: + - description: Organization ID + in: path + name: orgId + required: true + schema: + description: Organization ID + format: uuid + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ClosePeriodsScope" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ClosePeriodsReport" + description: OK + "400": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Bad Request + "401": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unauthorized + "403": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Forbidden + "404": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Not Found + "422": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Unprocessable Entity + "500": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Internal Server Error + "503": + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: Service Unavailable + headers: + Retry-After: + description: Seconds to wait before retrying, when a dependency (a payment provider) could not be reached + schema: + minimum: 0 + type: integer + security: + - platformAuth: + - write:billing + summary: Close an organization's due billing periods + tags: + - billing /platform/organizations/{orgId}/memberships/{userId}: delete: description: "Soft-deletes a single membership. Platform API: requires a Kaiten platform token (`ksm_...`) with delete:memberships. The system:kaiten membership cannot be removed — the database refuses it, and this answers 403." diff --git a/openapi/source.yaml b/openapi/source.yaml index 3f7c144..19d4f94 100644 --- a/openapi/source.yaml +++ b/openapi/source.yaml @@ -1,9 +1,9 @@ # Where the two snapshots in this directory were copied from. Rewritten by # `task sync:openapi`; never edit the snapshots themselves by hand. repository: kaitencloud/kaiten -ref: origin/main -commit: fa7f554fb6464f52c2a3f1ed487f4c7383c8125d -synced_at: "2026-09-30" +ref: 26.10.8 +commit: f685ceb86bfeb5f6110663662fb955336ce1a1a4 +synced_at: "2026-11-01" files: openapi.yaml: app/openapi.yaml platform-openapi.yaml: app/platform-openapi.yaml diff --git a/pyproject.toml b/pyproject.toml index e7a85b4..c54007e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,8 @@ ignore = [ # Docstrings and contract descriptions run long; the formatter owns code width. "E501", ] +# The contract's descriptions write their arithmetic with these, and the models keep them. +allowed-confusables = ["×", "−"] [tool.ruff.lint.isort] known-first-party = ["kaitencloud"] diff --git a/scripts/generate_models.py b/scripts/generate_models.py index 441da06..240588d 100644 --- a/scripts/generate_models.py +++ b/scripts/generate_models.py @@ -13,7 +13,7 @@ uv run python scripts/generate_models.py # rewrite src/kaitencloud/types/ uv run python scripts/generate_models.py --check # exit 1 when the committed files are stale -Three rules shape the output, each because the alternative breaks a caller: +Four rules shape the output, each because the alternative breaks a caller: - Only what the SDK can return is generated: the response schemas of the operations ``openapi/coverage.yaml`` maps, and the payloads of the published webhooks. Request bodies @@ -24,6 +24,9 @@ - Nothing is guessed. A new multi-value enum or inline union must be named below, and an attribute that would shadow a pydantic member must be renamed below; the generator stops until it is. +- A model keeps its class. A webhook payload the contract renamed keeps the name it was + published under, and one that is a resource plus who wrote the change subclasses that + resource's model, so an event's ``data`` stays an instance of the class it had. """ from __future__ import annotations @@ -68,6 +71,66 @@ "ImpactSample": "MetadataFieldImpactSample", "RolloutDate": "RolloutDateVariant", "RolloutPercentage": "RolloutPercentageVariant", + # Webhook payloads the contract renamed when it named every delivered shape ...Delivered. + # They keep the name the SDK published them under. + "CustomerCreationRejectedDelivered": "CustomerCreationRejected", + "DeploymentDelivered": "Deployment", + "FeatureFlagEvaluatedDelivered": "FeatureFlagEvaluated", + "InstanceEntitlementCapExceededDelivered": "InstanceEntitlementCapExceeded", + "InstanceEntitlementUsagePeriodRolledOverDelivered": "InstanceEntitlementUsagePeriodRolledOver", + "InstanceEntitlementUsageReportAcceptedDelivered": "InstanceEntitlementUsageReportAccepted", + "InstanceEntitlementUsageWarningThresholdReachedDelivered": ( + "InstanceEntitlementUsageWarningThresholdReached" + ), + "InstanceEntitlementValueGetDelivered": "InstanceEntitlementValueGet", + "InstanceLifecycleStageChangedDelivered": "InstanceLifecycleStageChanged", + "InstanceStatusChangedDelivered": "InstanceStatusChanged", + "MetadataFieldReorderedDelivered": "MetadataFieldReordered", + "SystemTokenIssuanceDelivered": "SystemTokenIssuance", +} + +# Webhook payloads that are a resource plus who wrote the change, keyed to that resource's +# schema. Each is generated as a subclass of the resource's model, so an event's ``data`` stays +# an instance of the class it was before the contract gave it a delivered shape. The generator +# checks the claim: every property of the resource must keep its type in the payload. +EXTENDS: dict[str, str] = { + "AddonDelivered": "Addon", + "AddonGrantUpdateDelivered": "AddonEntitlement", + "AddonPriceUpdateDelivered": "Price", + "AddonUpdateDelivered": "Addon", + "BillingStartedDelivered": "InstanceBilling", + "CancellationChangeDelivered": "InstanceBilling", + "CancellationDelivered": "InstanceBilling", + "ComponentDelivered": "Component", + "CustomerEventDelivered": "Customer", + "DeploymentZoneDelivered": "DeploymentZone", + "EntitlementDelivered": "Entitlement", + "EntitlementGroupDelivered": "EntitlementGroup", + "EntitlementUsageDelivered": "EntitlementUsage", + "FeatureFlagDelivered": "FeatureFlag", + "HeldInvoiceDelivered": "InvoiceSummary", + "InstanceAddonUpdateDelivered": "InstanceAddon", + "InstanceDelivered": "Instance", + "InstanceUpdateDelivered": "Instance", + "IssuedInvoiceDelivered": "InvoiceSummary", + "LicenseDelivered": "License", + "LicenseEntitlementDelivered": "LicenseEntitlement", + "LicenseFamilyViewDelivered": "LicenseFamilyView", + "LicensePriceEventDelivered": "Price", + "LicensePriceUpdatedEventDelivered": "Price", + "MetadataFieldDelivered": "MetadataField", + "MismatchedInvoiceDelivered": "InvoiceSummary", + "PaidInvoiceDelivered": "InvoiceSummary", + "PaymentFailedInvoiceDelivered": "InvoiceSummary", + "PushFailedInvoiceDelivered": "InvoiceSummary", + "PushedInvoiceDelivered": "InvoiceSummary", + "RedemptionDelivered": "Redemption", + "ReleaseDelivered": "Release", + "ReleasedInvoiceDelivered": "InvoiceSummary", + "TrialWillEndDelivered": "InstanceBilling", + "UncollectibleInvoiceDelivered": "InvoiceSummary", + "VoidedInvoiceDelivered": "InvoiceSummary", + "VoucherDelivered": "Voucher", } # Names for inline unions, keyed by their members. @@ -82,16 +145,13 @@ # Names for enums with more than one value, keyed by (schema, property). ENUM_NAMES: dict[tuple[str, str], str] = { + ("Addon", "lifecycleState"): "AddonLifecycleState", ("Entitlement", "aggregationMethod"): "AggregationMethod", ("Entitlement", "resetAnchor"): "ResetAnchor", ("Entitlement", "resetPeriod"): "ResetPeriod", ("Entitlement", "type"): "EntitlementType", ("FeatureFlag", "type"): "FeatureFlagType", ("Instance", "status"): "InstanceStatus", - ("InstanceEntitlementUsageReportAccepted", "behavior"): "UsageBehavior", - ("InstanceEntitlementUsageReportAccepted", "status"): "UsageReportStatus", - ("InstanceStatusChanged", "new_status"): "InstanceStatus", - ("InstanceStatusChanged", "previous_status"): "InstanceStatus", ("License", "lifecycleState"): "LicenseLifecycleState", ("License", "type"): "LicenseType", ("LicenseEntitlement", "entitlementType"): "LicenseEntitlementType", @@ -99,18 +159,246 @@ ("TargetingContextNode", "type"): "TargetingFactType", } -# Attribute names that would shadow a pydantic.BaseModel member, keyed by (schema, property). +# Names for the enums several schemas share -- an invoice's status, a billing period -- keyed by +# (property, values). A property must match as well as the values for a name to apply. +ENUM_VOCABULARY: dict[tuple[str, frozenset[str]], str] = { + ("aggregationMethod", frozenset({"COUNT", "SUM", "AVERAGE", "MIN", "MAX", "LATEST"})): ( + "AggregationMethod" + ), + ("appliesTo", frozenset({"LICENSE_BASE", "ADDONS", "BOTH", "SELECTED_PRICES"})): ( + "DiscountAppliesTo" + ), + ("behavior", frozenset({"append", "set"})): "UsageBehavior", + ("billingModel", frozenset({"FLAT_FEE", "USAGE_BASED", "OVERAGE"})): "BillingModel", + ("billingPeriod", frozenset({"MONTHLY", "QUARTERLY", "SEMI_ANNUAL", "ANNUAL"})): ( + "BillingPeriod" + ), + ("billingStatus", frozenset({"INCOMPLETE", "TRIAL", "ACTIVE", "PAST_DUE", "CANCELED"})): ( + "SubscriptionStatus" + ), + ("billingTiming", frozenset({"ADVANCE", "ARREARS"})): "BillingTiming", + ( + "cancellationReason", + frozenset({"TRIAL_ENDED_WITHOUT_PAYMENT_METHOD", "INCOMPLETE_EXPIRED"}), + ): ("CancellationReason"), + ("collectionMethod", frozenset({"SEND_INVOICE", "CHARGE_AUTOMATICALLY"})): "CollectionMethod", + ("defaultCollectionMethod", frozenset({"SEND_INVOICE", "CHARGE_AUTOMATICALLY"})): ( + "CollectionMethod" + ), + ("disabledReason", frozenset({"DEPLOYMENT_DISABLED", "NOT_ENTITLED"})): ( + "BillingDisabledReason" + ), + ("discountType", frozenset({"PERCENTAGE", "FIXED_AMOUNT"})): "DiscountType", + ("duration", frozenset({"ONE_TIME", "REPEATING", "FOREVER"})): "VoucherDuration", + ("from", frozenset({"NOOP", "STRIPE"})): "BillingProviderKind", + ("from", frozenset({"INCOMPLETE", "TRIAL", "ACTIVE", "PAST_DUE", "CANCELED"})): ( + "SubscriptionStatus" + ), + ("entitlementType", frozenset({"BOOLEAN", "NUMBER", "CONFIG", "NUMBER_AI_CREDIT"})): ( + "EntitlementType" + ), + ("handoffStatus", frozenset({"NOT_REQUIRED", "PENDING", "ACKNOWLEDGED"})): "HandoffStatus", + ( + "holdReason", + frozenset( + { + "LEDGER_SEQUENCE_GAP", + "LEDGER_CHAIN_BREAK", + "LEDGER_COUNTER_MISMATCH", + "PROVIDER_ITEM_LIMIT", + } + ), + ): "InvoiceHoldReason", + ("increaseBilling", frozenset({"TRUE_UP", "NEXT_BOUNDARY"})): "IncreaseBilling", + ("kind", frozenset({"ACTIVATION", "RENEWAL", "FINAL"})): "InvoiceKind", + ("kind", frozenset({"NOOP", "STRIPE"})): "BillingProviderKind", + ("kind", frozenset({"SET", "ADD", "UNLIMITED"})): "ExceptionKind", + ("kind", frozenset({"USER", "SERVICE", "SYSTEM"})): "ActorKind", + ("lastSyncStatus", frozenset({"SUCCESS", "PARTIAL", "FAILED"})): "SyncStatus", + ("mode", frozenset({"AT_PERIOD_END", "IMMEDIATE"})): "CancellationMode", + ("modifierType", frozenset({"SET", "ADD", "MULTIPLY", "UNLIMITED"})): "GrantModifier", + ("new_status", frozenset({"HEALTHY", "DEGRADED", "INCIDENT", "MAINTENANCE"})): "InstanceStatus", + ("overrideBehavior", frozenset({"ADD", "OVERRIDE", "MAX"})): "OverrideBehavior", + ("previous_status", frozenset({"HEALTHY", "DEGRADED", "INCIDENT", "MAINTENANCE"})): ( + "InstanceStatus" + ), + ("priceAppliesTo", frozenset({"LICENSE_BASE", "ADDONS", "BOTH", "SELECTED_PRICES"})): ( + "DiscountAppliesTo" + ), + ("priceDiscountType", frozenset({"PERCENTAGE", "FIXED_AMOUNT"})): "DiscountType", + ("pricingType", frozenset({"FREE", "PAID", "CUSTOM"})): "PricingType", + ("providerKind", frozenset({"NOOP", "STRIPE"})): "BillingProviderKind", + ("reason", frozenset({"PERIOD_ENDED", "ANCHOR_CHANGED"})): "PeriodRolloverReason", + ("reason", frozenset({"TRIAL_ENDED", "INVOICE_OVERDUE", "PAYMENT_FAILED", "SETTLED"})): ( + "SubscriptionStatusChangeReason" + ), + ( + "reason", + frozenset( + { + "NOT_FOUND", + "NOT_ACTIVE", + "NOT_YET_VALID", + "EXPIRED", + "EXHAUSTED", + "ALREADY_REDEEMED", + "NOT_ELIGIBLE", + "CURRENCY_MISMATCH", + "DISCOUNT_LIMIT_EXCEEDED", + } + ), + ): "VoucherInvalidReason", + ("reconciliationStatus", frozenset({"MATCHED", "MISMATCH"})): "ReconciliationStatus", + ("retentionSource", frozenset({"KBK", "ENV", "UNKNOWN"})): "RetentionSource", + ( + "rule", + frozenset( + { + "RESTRICTED_CUSTOMER", + "LICENSE_NOT_APPLICABLE", + "ADDON_NOT_APPLICABLE", + "FIRST_TIME_ONLY", + "ANNUAL_ONLY", + "MINIMUM_SUBSCRIPTION_AMOUNT", + "NOTHING_TO_BOOST", + } + ), + ): "VoucherRule", + ("source", frozenset({"license", "addon", "exception"})): "EntitlementSource", + ("source", frozenset({"MARK_PAID", "PROVIDER", "ZERO_TOTAL"})): "PaymentSource", + ("status", frozenset({"ACCEPTED", "REJECTED"})): "UsageReportStatus", + ("status", frozenset({"ACTIVE", "DEPRECATED"})): "PriceStatus", + ("status", frozenset({"ACTIVE", "EXPIRED", "FAILED"})): "PaymentMethodStatus", + ("status", frozenset({"ACTIVE", "EXPIRED", "REVOKED"})): "RedemptionStatus", + ("status", frozenset({"DRAFT", "ACTIVE", "EXPIRED", "EXHAUSTED", "ARCHIVED"})): "VoucherStatus", + ( + "status", + frozenset( + { + "DRAFT", + "PUSHED", + "PUSH_FAILED", + "MANUAL", + "PAID", + "PAYMENT_FAILED", + "UNCOLLECTIBLE", + "VOID", + } + ), + ): "InvoiceStatus", + ("status", frozenset({"HEALTHY", "DEGRADED", "INCIDENT", "MAINTENANCE"})): "InstanceStatus", + ("status", frozenset({"INCOMPLETE", "TRIAL", "ACTIVE", "PAST_DUE", "CANCELED"})): ( + "SubscriptionStatus" + ), + ( + "status", + frozenset({"MANUAL", "PUSHED", "PAID", "PAYMENT_FAILED", "UNCOLLECTIBLE", "VOID"}), + ): ("SessionInvoiceStatus"), + ("status", frozenset({"NOT_REQUIRED", "PENDING", "ACKNOWLEDGED"})): "HandoffStatus", + ("status", frozenset({"OPEN", "AWAITING_PAYMENT", "COMPLETED", "ABANDONED", "EXPIRED"})): ( + "CheckoutSessionStatus" + ), + ("status", frozenset({"OPEN", "COMPLETED", "ABANDONED", "EXPIRED"})): ( + "CheckoutSessionEventStatus" + ), + ("status", frozenset({"SCHEDULED", "ACTIVE", "EXPIRED", "REVOKED"})): "ExceptionStatus", + ("status", frozenset({"SUCCESS", "PARTIAL", "FAILED"})): "SyncStatus", + ("status", frozenset({"draft", "open", "paid", "uncollectible", "void"})): ( + "ProviderInvoiceStatus" + ), + ("status", frozenset({"paid", "requires_action", "failed", "processing", "not_required"})): ( + "PaymentStatus" + ), + ("status", frozenset({"preview", "open"})): "CheckoutStartStatus", + ("status", frozenset({"preview", "requires_payment_method", "subscribed"})): ( + "SessionCheckoutStatus" + ), + ("status", frozenset({"requires_payment_method", "awaiting_payment", "completed"})): ( + "CheckoutConfirmationStatus" + ), + ("to", frozenset({"NOOP", "STRIPE"})): "BillingProviderKind", + ("to", frozenset({"INCOMPLETE", "TRIAL", "ACTIVE", "PAST_DUE", "CANCELED"})): ( + "SubscriptionStatus" + ), + ("trialEndWithoutPaymentMethod", frozenset({"CANCEL", "CONVERT"})): ( + "TrialEndWithoutPaymentMethod" + ), + ("type", frozenset({"BASE", "ADDON", "USAGE", "OVERAGE", "DISCOUNT"})): "InvoiceLineType", + ("type", frozenset({"BOOLEAN", "NUMBER", "CONFIG", "NUMBER_AI_CREDIT"})): "EntitlementType", + ("type", frozenset({"PERCENTAGE", "FIXED_AMOUNT"})): "DiscountType", + ("unavailableReason", frozenset({"NOT_ENTITLED", "VAULT_NOT_CONFIGURED"})): ( + "ProviderUnavailableReason" + ), + ("usageHistoryRetentionSource", frozenset({"KBK", "ENV", "UNKNOWN"})): "RetentionSource", + ("via", frozenset({"customer_session", "checkout_session"})): "EventVia", + ("voucherType", frozenset({"PRICE", "ENTITLEMENT_BOOST", "FLAG_GRANT", "COMPOSITE"})): ( + "VoucherType" + ), +} + +# Attribute names that would shadow a pydantic.BaseModel member or a Python keyword, keyed by +# (schema, property). FIELD_NAMES: dict[tuple[str, str], str] = { ("ConnectorSettingsSchema", "schema"): "json_schema", + ("ProviderChangeDelivered", "from"): "from_", + ("StatusChangeDelivered", "from"): "from_", } ALIAS_DOCS: dict[str, str] = { + "ActorKind": "Who acted: a person, a service account, or Kaiten itself.", + "AddonLifecycleState": ( + "Whether an add-on version may be sold: ``DRAFT`` before it goes on sale, ``PUBLISHED`` " + "while it is on sale, ``ARCHIVED`` once it has been withdrawn." + ), "AggregationMethod": "How each reported usage value folds into an entitlement's stored total.", + "BillingDisabledReason": "Why billing is off: the deployment disabled it, or the plan does not include it.", + "BillingModel": ( + "How a price bills: ``FLAT_FEE`` per period, ``USAGE_BASED`` per sale unit of usage, " + "``OVERAGE`` per sale unit above the license's limit." + ), + "BillingPeriod": "How often a subscription bills its flat fees.", + "BillingProviderKind": ( + "Who issues and collects an invoice: ``NOOP``, the organization itself through the " + "handoff queue, or ``STRIPE``." + ), + "BillingTiming": "When a price bills: in ``ADVANCE`` of its period, or in ``ARREARS`` after it.", + "CancellationMode": "When a cancellation takes effect: at the end of the period paid for, or now.", + "CancellationReason": "Why Kaiten canceled a subscription on its own.", + "CheckoutConfirmationStatus": "Where an anonymous checkout stands after a confirmation.", + "CheckoutSessionEventStatus": "The status of an anonymous checkout session, as its events report it.", + "CheckoutSessionStatus": "The status of an anonymous checkout session.", + "CheckoutStartStatus": "What starting an anonymous checkout did: ``preview`` for a dry run, ``open`` otherwise.", + "CollectionMethod": ( + "How an invoice is collected: ``SEND_INVOICE`` sends it for payment, " + "``CHARGE_AUTOMATICALLY`` charges the customer's payment method." + ), "DefaultVariant": "What a feature flag serves when no targeting rule matches, discriminated by ``type``.", + "DiscountAppliesTo": "What a price voucher discounts.", + "DiscountType": "How a discount is expressed: a percentage, or a fixed amount in minor units.", + "EntitlementSource": ( + "Where an instance's entitlement comes from: its ``license``, an ``addon``, or an " + "``exception`` alone." + ), "EntitlementType": "The value type of an entitlement in the catalog.", "EntitlementValue": "A typed entitlement value, discriminated by ``type``: a number, a boolean or a config object.", + "EventVia": ( + "What wrote the change an event reports, when it was not an organization credential: " + "a customer session, or an anonymous checkout." + ), + "ExceptionKind": "How an entitlement exception overrides a value: ``SET`` it, ``ADD`` to it, or lift it (``UNLIMITED``).", + "ExceptionStatus": "Where an entitlement exception stands.", "FeatureFlagType": "The value type of a feature flag's variants.", + "GrantModifier": "How a voucher grant changes an entitlement: ``SET``, ``ADD``, ``MULTIPLY`` or ``UNLIMITED``.", + "HandoffStatus": "Where an invoice stands in the accounting handoff.", + "IncreaseBilling": ( + "How a mid-period increase of an add-on billed in advance is billed: a ``TRUE_UP`` line, " + "or from the ``NEXT_BOUNDARY``." + ), "InstanceStatus": "The operational status of an instance.", + "InvoiceHoldReason": "Why an invoice is held as a draft.", + "InvoiceKind": "What an invoice bills: a subscription's ``ACTIVATION``, a ``RENEWAL``, or its ``FINAL`` period.", + "InvoiceLineType": "What an invoice line bills.", + "InvoiceStatus": "Where an invoice stands, from ``DRAFT`` to ``PAID``, ``UNCOLLECTIBLE`` or ``VOID``.", "LicenseEntitlementType": "The value type of an entitlement granted by a license.", "LicenseLifecycleState": ( "Whether a license version may be served: ``DRAFT`` before it goes on sale, " @@ -118,8 +406,25 @@ ), "LicenseType": "The kind of a license.", "MetadataResourceType": "The resource a metadata field applies to.", + "OverrideBehavior": "How an add-on's number grant combines with the license's: ``ADD``, ``OVERRIDE`` or ``MAX``.", + "PaymentMethodStatus": "Whether a saved payment method can be charged.", + "PaymentSource": "What recorded an invoice as paid.", + "PaymentStatus": "How a checkout's first payment went.", + "PeriodRolloverReason": "Why a usage window closed: its period ended, or its anchor changed.", + "PriceStatus": "Whether a price is ``ACTIVE``, or ``DEPRECATED``: still billing the subscriptions on it, and no new one.", + "PricingType": "How a version is sold: ``FREE`` or ``PAID`` self-serve, or ``CUSTOM``, through a conversation.", + "ProviderInvoiceStatus": "An invoice's status at its payment provider.", + "ProviderUnavailableReason": "Why a payment provider cannot be connected.", + "ReconciliationStatus": "Whether an invoice matches its copy at the payment provider.", + "RedemptionStatus": "Where a voucher redemption stands.", "ResetAnchor": "What a periodic usage window is aligned on.", "ResetPeriod": "How often a periodic entitlement's usage resets.", + "RetentionSource": "Where the usage history's retention period comes from.", + "SessionCheckoutStatus": "What a customer session's checkout did.", + "SessionInvoiceStatus": "An issued invoice's status, as the customer's portal shows it.", + "SubscriptionStatus": "Where a subscription stands.", + "SubscriptionStatusChangeReason": "Why a subscription's status changed.", + "SyncStatus": "How a sync with a payment provider went.", "TargetingFactType": "The shape of a fact a targeting rule can read.", "Scope": ( "A permission an organization credential can carry, ``read:`` or " @@ -127,8 +432,17 @@ ), "Targeting": "One targeting rule of a feature flag, discriminated by ``type``.", "Targetings": "A feature flag's targeting rules, evaluated in order; the first match wins.", + "TrialEndWithoutPaymentMethod": ( + "What a trial charged automatically becomes at its end without a payment method: " + "``CANCEL`` or ``CONVERT``." + ), "UsageBehavior": "How a usage report applies: ``append`` folds the value in, ``set`` overwrites it.", "UsageReportStatus": "Whether a usage report was persisted.", + "VoucherDuration": "How long a price voucher's discount lasts.", + "VoucherInvalidReason": "Why a voucher code would not apply.", + "VoucherRule": "The redemption rule a voucher code failed.", + "VoucherStatus": "Where a voucher stands.", + "VoucherType": "What a voucher gives: a price discount, an entitlement boost, flag grants, or several.", } # Docstrings for the schemas the contract leaves undescribed. @@ -236,7 +550,8 @@ def core_module( names = {name: CLASS_NAMES.get(name, name) for name in emitted} module = ModelModule(source="openapi/openapi.yaml", schemas=schemas, names=names) - for name in sorted(emitted, key=names.__getitem__): + # A subclass is defined after the class it extends. + for name in sorted(emitted, key=lambda name: (name in EXTENDS, names[name])): module.add_schema(name) module.enums["Scope"] = scopes(spec) return module.render(), names, module.exports @@ -333,10 +648,18 @@ def webhooks_module(spec: dict[str, Any], core_names: dict[str, str]) -> str: ] events: list[tuple[str, str, str, str, str]] = [] + headers: set[str] = set() for key, item in sorted((spec.get("webhooks") or {}).items()): operation = item["post"] schema = operation["requestBody"]["content"]["application/json"]["schema"] properties = schema["properties"] + unknown = set(properties) - {"name", "type", "data", "headers"} + if unknown: + raise SystemExit(f"webhook {key}: the envelope has unsupported properties {unknown}") + # Every envelope carries the same headers, or none does: one rule renders them all. + headers.add( + core_names[ref_name(properties["headers"]["$ref"])] if "headers" in properties else "" + ) event_type = only_enum_value(properties["type"], f"webhook {key}.type") if event_type != key: raise SystemExit(f"webhook {key}: its type enum says {event_type!r}") @@ -346,7 +669,10 @@ def webhooks_module(spec: dict[str, Any], core_names: dict[str, str]) -> str: summary = operation.get("description") or operation.get("summary") or "" events.append((python, event_name, event_type, data, summary)) - data_types = sorted({data for _, _, _, data, _ in events}) + if len(headers) > 1: + raise SystemExit(f"webhooks: the envelopes disagree on their headers: {sorted(headers)}") + header = next(iter(headers), "") + data_types = sorted({data for _, _, _, data, _ in events} | ({header} - {""})) lines.append(f"from ._core import {', '.join(data_types)}") lines += ["", "__all__ = [", ' "WEBHOOK_EVENTS",', ' "WebhookEvent",'] lines += [f' "{python}",' for python, *_ in sorted(events)] @@ -367,6 +693,12 @@ def webhooks_module(spec: dict[str, Any], core_names: dict[str, str]) -> str: ' """What the event is about."""', "", ] + if header: + lines += [ + f" headers: {header} | None = None", + ' """The trace context of the request that produced the event."""', + "", + ] members = "\n".join(f" | {python}" for python, *_ in events).lstrip(" |") lines += [ @@ -418,15 +750,19 @@ def add_schema(self, name: str) -> None: def render_class(self, name: str, schema: dict[str, Any]) -> list[str]: python = self.names[name] - lines = [f"class {python}(KaitenModel):"] + base = EXTENDS.get(name) + lines = [f"class {python}({self.names[base] if base else 'KaitenModel'}):"] lines += docstring(schema.get("description") or CLASS_DOCS.get(python), " ") required = set(schema.get("required") or []) properties: dict[str, Any] = schema.get("properties") or {} - if not properties and len(lines) == 1: + inherited = self.inherited(name, base) if base else set() + if not set(properties) - inherited and len(lines) == 1: lines.append(" pass") for wire, node in properties.items(): + if wire in inherited: + continue attribute = FIELD_NAMES.get((name, wire), snake_case(wire)) if keyword.iskeyword(attribute) or hasattr(BaseModel, attribute): raise SystemExit( @@ -450,6 +786,30 @@ def render_class(self, name: str, schema: dict[str, Any]) -> list[str]: lines += docstring(node.get("description"), " ") return lines + def inherited(self, name: str, base: str) -> set[str]: + """The properties ``name`` takes from ``base`` as they are, checking it really extends it. + + Each property of the base must keep its type in the derived schema -- rendered as the + base's, so their enums get the base's names -- and stay required where the base requires + it. A property the base has and the derived schema lacks must be optional in the base. + """ + own = self.schemas[name].get("properties") or {} + parent = self.schemas[base] + required = set(self.schemas[name].get("required") or []) + for wire, node in (parent.get("properties") or {}).items(): + where = f"{name}.{wire}" + if wire not in own: + if wire in (parent.get("required") or []): + raise SystemExit(f"{where}: missing, but required by {base}, which it extends") + continue + if wire in (parent.get("required") or []) and wire not in required: + raise SystemExit(f"{where}: optional, but required by {base}, which it extends") + ours = self.annotation(own[wire], owner=base, prop=wire) + theirs = self.annotation(node, owner=base, prop=wire) + if ours != theirs: + raise SystemExit(f"{where}: {ours}, but {theirs} in {base}, which it extends") + return set(own) & set(parent.get("properties") or {}) + def annotation(self, node: dict[str, Any], *, owner: str, prop: str | None) -> str: where = f"{owner}.{prop}" if prop else owner if "$ref" in node: @@ -494,15 +854,20 @@ def annotation(self, node: dict[str, Any], *, owner: str, prop: str | None) -> s return f"{text} | None" if nullable else text def string(self, node: dict[str, Any], *, owner: str, prop: str | None) -> str: - values = node.get("enum") + # A nullable enum lists null among its values; its type already says it is nullable. + values = [value for value in node.get("enum") or [] if value is not None] if values: if len(values) == 1: return f"Literal[{json.dumps(values[0])}]" - name = ENUM_NAMES.get((owner, prop or "")) + name = ENUM_NAMES.get((owner, prop or "")) or ENUM_VOCABULARY.get( + (prop or "", frozenset(values)) + ) if name is None: - raise SystemExit(f"{owner}.{prop}: name this enum in ENUM_NAMES ({values})") + raise SystemExit( + f"{owner}.{prop}: name this enum in ENUM_NAMES or ENUM_VOCABULARY ({values})" + ) known = self.enums.setdefault(name, tuple(values)) - if known != tuple(values): + if set(known) != set(values): raise SystemExit(f"{name}: {owner}.{prop} has {values}, elsewhere {list(known)}") return f"{name} | str" if node.get("format") == "date-time": @@ -512,17 +877,35 @@ def string(self, node: dict[str, Any], *, owner: str, prop: str | None) -> str: def union(self, node: dict[str, Any], *, owner: str, name: str | None = None) -> str: key = "oneOf" if "oneOf" in node else "anyOf" members: list[str] = [] + scalars: list[dict[str, Any]] = [] + nullable = False for member in node[key]: - if "$ref" not in member: + if member.get("type") == "null": + # A null member makes the union nullable: `X | None`, not a model of its own. + nullable = True + elif "$ref" in member: + members.append(ref_name(member["$ref"])) + elif member.get("type") in ("boolean", "number", "integer", "string", "object") and ( + not member.get("properties") and not member.get("enum") + ): + scalars.append(member) + else: raise SystemExit(f"{owner}: inline union members are not supported") - members.append(ref_name(member["$ref"])) - - name = name or UNION_NAMES.get(frozenset(members)) - if name is None: - raise SystemExit(f"{owner}: name the union of {members} in UNION_NAMES") - tag = (node.get("discriminator") or {}).get("propertyName") or self.shared_tag(members) - self.unions[name] = (tag, [self.names[member] for member in members]) - return name + + if scalars: + if members or name: + raise SystemExit(f"{owner}: a union mixes inline values and schemas") + text = " | ".join(self.annotation(member, owner=owner, prop=None) for member in scalars) + elif len(members) == 1 and name is None: + text = self.names[members[0]] + else: + name = name or UNION_NAMES.get(frozenset(members)) + if name is None: + raise SystemExit(f"{owner}: name the union of {members} in UNION_NAMES") + tag = (node.get("discriminator") or {}).get("propertyName") or self.shared_tag(members) + self.unions[name] = (tag, [self.names[member] for member in members]) + text = name + return f"{text} | None" if nullable else text def shared_tag(self, members: Iterable[str]) -> str | None: """``type``, when every member pins it to its own single value.""" diff --git a/src/kaitencloud/__init__.py b/src/kaitencloud/__init__.py index dd539e5..0699059 100644 --- a/src/kaitencloud/__init__.py +++ b/src/kaitencloud/__init__.py @@ -37,11 +37,12 @@ RateLimitError, ServiceUnavailableError, ThresholdExceededError, + TransactionIdReusedError, UnprocessableEntityError, WebhookPayloadError, WebhookVerificationError, ) -from ._models import KaitenModel +from ._models import KaitenModel, UsageReportResult from ._sync.client import KaitenClient, KaitenPlatformClient from ._types import NOT_GIVEN, NotGiven from ._version import __version__ @@ -77,7 +78,9 @@ "RateLimitError", "ServiceUnavailableError", "ThresholdExceededError", + "TransactionIdReusedError", "UnprocessableEntityError", + "UsageReportResult", "WebhookPayloadError", "WebhookVerificationError", "__version__", diff --git a/src/kaitencloud/_async/_http.py b/src/kaitencloud/_async/_http.py index b96196c..8faf1cc 100644 --- a/src/kaitencloud/_async/_http.py +++ b/src/kaitencloud/_async/_http.py @@ -12,7 +12,7 @@ import httpx from pydantic import BaseModel, ValidationError -from .._constants import DEFAULT_TIMEOUT, MAX_PAGE_SIZE +from .._constants import DEFAULT_TIMEOUT, MAX_PAGE_SIZE, MAX_REPORT_PAGE_SIZE from .._exceptions import ( APIConnectionError, APIResponseValidationError, @@ -22,7 +22,15 @@ make_status_error, ) from .._types import NOT_GIVEN, AsyncCredential, NotGiven, TimeoutTypes -from .._utils import USER_AGENT, Surface, async_resolve_token, async_sleep, check_token, retry_delay +from .._utils import ( + PUBLISHABLE_KEY_PREFIX, + USER_AGENT, + Surface, + async_resolve_token, + async_sleep, + check_token, + retry_delay, +) __all__ = ["AsyncAPIClient"] @@ -96,6 +104,7 @@ async def request( *, params: Mapping[str, Any] | None = None, body: Any = None, + headers: Mapping[str, str] | None = None, idempotent: bool | None = None, errors: Mapping[int, type[APIStatusError]] | None = None, ) -> httpx.Response: @@ -113,7 +122,11 @@ async def request( while True: request = self._build( - method, url, params=_query(params), body=body, headers=await self._headers() + method, + url, + params=_query(params), + body=body, + headers={**await self._headers(path), **(headers or {})}, ) try: response = await self.http_client.send(request) @@ -250,6 +263,43 @@ async def list_all( rows.append(row) return rows + async def list_reports( + self, + path: str, + model: type[ModelT], + *, + params: Mapping[str, Any] | None = None, + limit: int | None = None, + ) -> list[ModelT]: + """Every row of a usage report history, which pages by ``afterSeq`` rather than a cursor. + + ``limit`` is a ceiling, as for :meth:`paginate`. A ``nextAfterSeq`` that does not move + past the page it follows raises :class:`PaginationError`. + """ + rows: list[ModelT] = [] + after: int | None = None + while True: + size = MAX_REPORT_PAGE_SIZE + if limit is not None: + size = min(size, limit - len(rows)) + response = await self.request( + "GET", path, params={**(params or {}), "afterSeq": after, "limit": size} + ) + page = self.decode(response) + if not isinstance(page, dict) or not isinstance(page.get("items"), list): + raise APIResponseValidationError( + f"GET {path}: expected a page of items", response=response + ) + rows += [self.validate(model, item, response) for item in page["items"]] + following = page.get("nextAfterSeq") + if following is None or (limit is not None and len(rows) >= limit): + return rows[:limit] + if not isinstance(following, int) or (after is not None and following <= after): + raise PaginationError( + f"GET {path}: the API reported more rows but no afterSeq past {after}" + ) + after = following + @staticmethod def decode(response: httpx.Response) -> Any: if not response.content: @@ -291,14 +341,15 @@ def _build( method, url, params=params, json=body, headers=headers, timeout=self.timeout ) - async def _headers(self) -> dict[str, str]: - token = check_token(await async_resolve_token(self.credential), self.surface) - return { - "Accept": "application/json", - "User-Agent": USER_AGENT, - **self.default_headers, - "Authorization": f"Bearer {token}", - } + async def _headers(self, path: str) -> dict[str, str]: + token = check_token(await async_resolve_token(self.credential), self.surface, path) + headers = {"Accept": "application/json", "User-Agent": USER_AGENT, **self.default_headers} + if token.startswith(PUBLISHABLE_KEY_PREFIX): + # Not a bearer credential: the API refuses a publishable key sent as Authorization. + headers["X-Kaiten-Publishable-Key"] = token + else: + headers["Authorization"] = f"Bearer {token}" + return headers async def _back_off( self, request: httpx.Request, attempt: int, reason: str, retry_after: str | None = None diff --git a/src/kaitencloud/_async/client.py b/src/kaitencloud/_async/client.py index 5b36901..0e5c6b2 100644 --- a/src/kaitencloud/_async/client.py +++ b/src/kaitencloud/_async/client.py @@ -26,6 +26,7 @@ from ..types import PlatformCredential from ._http import AsyncAPIClient from .resources import ( + AsyncBilling, AsyncComponents, AsyncConnectors, AsyncCustomers, @@ -42,6 +43,7 @@ AsyncServiceAccounts, ) from .resources.platform import ( + AsyncPlatformBilling, AsyncPlatformConnectors, AsyncPlatformOrganizations, AsyncPlatformTokens, @@ -113,7 +115,7 @@ class AsyncKaitenClient(_AsyncBaseClient): The Core API is what an organization's own credential talks to: an organization token (``ksh_...``) or a JWT from your identity provider. It covers customers, instances, - licenses, entitlements and usage, feature flags, releases and connectors:: + licenses, entitlements and usage, billing, feature flags, releases and connectors:: async with AsyncKaitenClient( base_url="https://kaiten.example.com", token="ksh_..." @@ -136,6 +138,9 @@ class AsyncKaitenClient(_AsyncBaseClient): TLS or transports. The Kaiten client never closes a client it did not create. """ + billing: AsyncBilling + """Billing: prices, subscriptions, invoices, add-ons, vouchers and the public catalog.""" + components: AsyncComponents """Components: the versioned building blocks releases are made of.""" @@ -210,6 +215,7 @@ def __init__( def _bind(self) -> None: api = self._api + self.billing = AsyncBilling(api) self.components = AsyncComponents(api) self.connectors = AsyncConnectors(api) self.customers = AsyncCustomers(api) @@ -251,6 +257,9 @@ class AsyncKaitenPlatformClient(_AsyncBaseClient): http_client: An :class:`httpx.AsyncClient` to send requests with. """ + billing: AsyncPlatformBilling + """Billing across organizations: closing their due periods.""" + connectors: AsyncPlatformConnectors """The deployment-wide connector registry.""" @@ -306,6 +315,7 @@ async def me(self) -> PlatformCredential: def _bind(self) -> None: api = self._api + self.billing = AsyncPlatformBilling(api) self.connectors = AsyncPlatformConnectors(api) self.organizations = AsyncPlatformOrganizations(api) self.tokens = AsyncPlatformTokens(api) diff --git a/src/kaitencloud/_async/resources/__init__.py b/src/kaitencloud/_async/resources/__init__.py index 8a40fa4..452395c 100644 --- a/src/kaitencloud/_async/resources/__init__.py +++ b/src/kaitencloud/_async/resources/__init__.py @@ -1,6 +1,7 @@ # Copyright 2026 KAITEN INC # SPDX-License-Identifier: Apache-2.0 +from .billing import AsyncBilling from .components import AsyncComponents from .connectors import AsyncConnectors from .customers import AsyncCustomers @@ -17,6 +18,7 @@ from .service_accounts import AsyncServiceAccounts __all__ = [ + "AsyncBilling", "AsyncComponents", "AsyncConnectors", "AsyncCustomers", diff --git a/src/kaitencloud/_async/resources/billing/__init__.py b/src/kaitencloud/_async/resources/billing/__init__.py new file mode 100644 index 0000000..76a462a --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/__init__.py @@ -0,0 +1,216 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from ...._utils import compact +from ....types import ( + BillingCapabilities, + BillingHealth, + BillingSettings, + ClosePeriodsReport, + CollectionMethod, + SyncReport, +) +from ..._http import AsyncAPIClient +from .._base import AsyncResource +from .addons import AsyncBillingAddons +from .customers import AsyncBillingCustomers +from .invoices import AsyncBillingHandoff, AsyncBillingInvoices +from .license_prices import AsyncBillingLicensePrices +from .public import AsyncBillingPublic, AsyncBillingSession +from .sessions import ( + AsyncBillingCheckoutSessions, + AsyncBillingCustomerSessions, + AsyncBillingPublishableKeys, +) +from .subscriptions import ( + AsyncBillingInstanceAddons, + AsyncBillingInstanceVouchers, + AsyncBillingSubscriptions, +) +from .vouchers import AsyncBillingVouchers + +__all__ = [ + "AsyncBilling", + "AsyncBillingAddons", + "AsyncBillingCheckoutSessions", + "AsyncBillingCustomerSessions", + "AsyncBillingCustomers", + "AsyncBillingHandoff", + "AsyncBillingInstanceAddons", + "AsyncBillingInstanceVouchers", + "AsyncBillingInvoices", + "AsyncBillingLicensePrices", + "AsyncBillingPublic", + "AsyncBillingPublishableKeys", + "AsyncBillingSession", + "AsyncBillingSubscriptions", + "AsyncBillingVouchers", +] + + +class AsyncBilling(AsyncResource): + """Billing: the organization's settings and payment providers, and a namespace per resource. + + Kaiten composes every invoice from the prices of a license version, its add-ons and the + usage it metered. Each subscription names who issues and collects them: ``NOOP``, the + organization itself through :attr:`handoff`, or a payment provider. Most operations here + require billing to be enabled for the organization; :meth:`get_capabilities` says whether + it is. + """ + + __slots__ = ( + "addons", + "checkout_sessions", + "customer_sessions", + "customers", + "handoff", + "instance_addons", + "instance_vouchers", + "invoices", + "license_prices", + "public", + "publishable_keys", + "session", + "subscriptions", + "vouchers", + ) + + addons: AsyncBillingAddons + """The add-on catalog: families, versions, grants, prices and the license families they fit.""" + + checkout_sessions: AsyncBillingCheckoutSessions + """Anonymous checkouts, as the organization reads them.""" + + customer_sessions: AsyncBillingCustomerSessions + """Customer sessions (``kst_...``), for a customer's browser to act on its own billing.""" + + customers: AsyncBillingCustomers + """A customer's billing: its payment method, and the payment provider's portal.""" + + handoff: AsyncBillingHandoff + """The accounting handoff: invoices claimed in leased batches and acknowledged one by one.""" + + instance_addons: AsyncBillingInstanceAddons + """The add-ons an instance holds.""" + + instance_vouchers: AsyncBillingInstanceVouchers + """The vouchers an instance redeemed.""" + + invoices: AsyncBillingInvoices + """The organization's invoices.""" + + license_prices: AsyncBillingLicensePrices + """The prices of license versions, and the invoice a subscription to one would get.""" + + public: AsyncBillingPublic + """What a publishable key (``pk_...``) reaches. Build the client with that key.""" + + publishable_keys: AsyncBillingPublishableKeys + """Publishable keys (``pk_...``), which a web page reads the public catalog with.""" + + session: AsyncBillingSession + """What a customer session (``kst_...``) reaches. Build the client with that session.""" + + subscriptions: AsyncBillingSubscriptions + """Instances' subscriptions.""" + + vouchers: AsyncBillingVouchers + """The organization's vouchers.""" + + def __init__(self, api: AsyncAPIClient) -> None: + super().__init__(api) + self.addons = AsyncBillingAddons(api) + self.checkout_sessions = AsyncBillingCheckoutSessions(api) + self.customer_sessions = AsyncBillingCustomerSessions(api) + self.customers = AsyncBillingCustomers(api) + self.handoff = AsyncBillingHandoff(api) + self.instance_addons = AsyncBillingInstanceAddons(api) + self.instance_vouchers = AsyncBillingInstanceVouchers(api) + self.invoices = AsyncBillingInvoices(api) + self.license_prices = AsyncBillingLicensePrices(api) + self.public = AsyncBillingPublic(api) + self.publishable_keys = AsyncBillingPublishableKeys(api) + self.session = AsyncBillingSession(api) + self.subscriptions = AsyncBillingSubscriptions(api) + self.vouchers = AsyncBillingVouchers(api) + + async def get_settings(self) -> BillingSettings: + """Return the organization's billing defaults. + + They are the collection method and payment terms a subscription gets when it names none + of its own. An organization that never wrote them has ``SEND_INVOICE`` and 30 days. + """ + return await self._api.get("/billing/settings", BillingSettings) + + async def update_settings( + self, + *, + default_collection_method: CollectionMethod, + default_days_until_due: int, + handoff_stripe_invoices: bool, + ) -> BillingSettings: + """Replace the organization's billing defaults, and return them. + + A subscription that names its own terms keeps them; the others take these at their + next invoice. + + Args: + default_collection_method: ``SEND_INVOICE`` sends an invoice for payment; + ``CHARGE_AUTOMATICALLY`` charges the customer's payment method, on a provider + that can. + default_days_until_due: Days an invoice is due after it is issued, 0 to 365. + handoff_stripe_invoices: Whether invoices a payment provider issues also enter the + handoff queue, for an accounting system that wants every invoice. + """ + body = { + "defaultCollectionMethod": default_collection_method, + "defaultDaysUntilDue": default_days_until_due, + "handoffStripeInvoices": handoff_stripe_invoices, + } + return await self._api.send("PUT", "/billing/settings", BillingSettings, body=body) + + async def get_capabilities(self) -> BillingCapabilities: + """Return whether billing is enabled -- and if not, why -- and what it can do here. + + Who can collect the organization's invoices, and which parts of billing this release + ships, so a client shows only what works. It answers even when billing is disabled. + """ + return await self._api.get("/billing/capabilities", BillingCapabilities) + + async def get_health(self) -> BillingHealth: + """Return what the organization's billing needs attention for, computed when read. + + Periods waiting for their close, held invoices, invoices a payment provider keeps + refusing, reconciliation mismatches, each provider's sync, overdue invoices and the + handoff backlog. + """ + return await self._api.get("/billing/health", BillingHealth) + + async def sync(self) -> SyncReport: + """Run one sync pass now with each payment provider the organization connected. + + What was paid, voided or finalized in a provider is mirrored, as the periodic pass does. + + Raises: + ConflictError: No provider is connected (``SyncProvider.NotConnected``). + """ + return await self._api.send("POST", "/billing/sync", SyncReport) + + async def close_periods(self, *, instance_slug: str | None = None) -> ClosePeriodsReport: + """Close the subscriptions whose period has ended, now rather than at the next pass. + + Each one's ``RENEWAL`` invoice is composed and issued -- or held, when its usage journal + fails a check -- and its period advances. Only what is due closes, at most a batch per + call: ``has_more`` says when more is left. + + Args: + instance_slug: Close only this instance's subscription. + """ + return await self._api.send( + "POST", + "/billing/close-periods", + ClosePeriodsReport, + body=compact({"instanceSlug": instance_slug}), + ) diff --git a/src/kaitencloud/_async/resources/billing/addons.py b/src/kaitencloud/_async/resources/billing/addons.py new file mode 100644 index 0000000..89c4bd9 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/addons.py @@ -0,0 +1,349 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from typing import Any, Literal + +from ...._types import EntitlementValueInput +from ...._utils import api_path, compact, entitlement_value, price_body +from ....types import ( + Addon, + AddonEntitlement, + AddonFamily, + AddonLifecycleState, + BillingModel, + BillingPeriod, + BillingTiming, + CompatibleLicenseFamilies, + IncreaseBilling, + OverrideBehavior, + Price, + PriceStatus, + PricingType, +) +from .._base import AsyncResource + +__all__ = ["AsyncBillingAddons"] + + +class AsyncBillingAddons(AsyncResource): + """The add-on catalog: what an instance can hold on top of its license, by the unit. + + Add-ons are versioned like licenses: an add-on *family* is the product, each version has + its grants and its prices, and moves through ``DRAFT``, ``PUBLISHED`` and ``ARCHIVED``. A + version fits the license families :meth:`set_compatibility` names, and none until then. + """ + + async def list( + self, + *, + lifecycle_state: AddonLifecycleState | None = None, + family_slug: str | None = None, + ) -> builtins.list[Addon]: + """Return the organization's add-on versions, newest first, walking all pages. + + Args: + lifecycle_state: Only versions in this state. + family_slug: Only versions of this family. + """ + return await self._api.list_all( + "/addons", + Addon, + params={"lifecycleState": lifecycle_state, "familySlug": family_slug}, + ) + + async def get(self, addon_slug: str) -> Addon: + """Return the add-on version identified by ``addon_slug``.""" + return await self._api.get(api_path("addons", addon_slug), Addon) + + async def create( + self, + *, + name: str, + pricing_type: PricingType, + description: str = "", + slug: str | None = None, + family_slug: str | None = None, + family_id: str | None = None, + version_name: str | None = None, + lifecycle_state: Literal["DRAFT", "PUBLISHED"] | None = None, + is_default: bool | None = None, + max_quantity: int | None = None, + increase_billing: IncreaseBilling | None = None, + ) -> Addon: + """Create an add-on version: the first of a new family, or the next of an existing one. + + Args: + name: The add-on's name, e.g. ``"Extra seats"``. + pricing_type: ``FREE``, ``PAID`` or ``CUSTOM``. + description: A description. + slug: The version's slug. Defaults to ``-v`` for a new version of a + family, and to one generated from the name for a new family. + family_slug: Make this a new version of this family. + family_id: The same family, by identifier. + version_name: A label for the version. Defaults to ``"Version - "``. + lifecycle_state: ``DRAFT``, or ``PUBLISHED`` when omitted. + is_default: Make this version its family's default. + max_quantity: The largest quantity an instance can hold. + increase_billing: Read only when this opens a new family: how a mid-period increase + of its add-ons billed in advance is billed, ``TRUE_UP`` (the default) or + ``NEXT_BOUNDARY``. + """ + body = { + "name": name, + "description": description, + "pricingType": pricing_type, + **compact( + { + "slug": slug, + "familySlug": family_slug, + "familyId": family_id, + "versionName": version_name, + "lifecycleState": lifecycle_state, + "isDefault": is_default, + "maxQuantity": max_quantity, + "increaseBilling": increase_billing, + } + ), + } + return await self._api.send("POST", "/addons", Addon, body=body) + + async def update( + self, + addon_slug: str, + *, + name: str, + description: str = "", + is_default: bool = False, + version_name: str | None = None, + max_quantity: int | None = None, + ) -> Addon: + """Replace an add-on version's name, description, version name, default flag and maximum. + + A full replacement, as for a license version: its family, pricing type and state are + not part of it. + """ + body = { + "name": name, + "description": description, + "isDefault": is_default, + **compact({"versionName": version_name, "maxQuantity": max_quantity}), + } + return await self._api.send("PUT", api_path("addons", addon_slug), Addon, body=body) + + async def delete(self, addon_slug: str) -> None: + """Delete an add-on version that was never attached and grants nothing. + + Its family goes with its last version. + + Raises: + ConflictError: The version is in use (``DeleteAddon.InUseConflict``). + """ + await self._api.send_empty("DELETE", api_path("addons", addon_slug)) + + async def publish(self, addon_slug: str) -> Addon: + """Put a ``DRAFT`` version on sale, and return it ``PUBLISHED``.""" + return await self._api.send("POST", api_path("addons", addon_slug, "publish"), Addon) + + async def archive(self, addon_slug: str) -> Addon: + """Withdraw a ``PUBLISHED`` version from sale: instances holding it keep it.""" + return await self._api.send("POST", api_path("addons", addon_slug, "archive"), Addon) + + async def unarchive(self, addon_slug: str) -> Addon: + """Put an ``ARCHIVED`` version back on sale, and return it ``PUBLISHED``.""" + return await self._api.send("POST", api_path("addons", addon_slug, "unarchive"), Addon) + + async def list_compatibility(self, addon_slug: str) -> CompatibleLicenseFamilies: + """Return the license families an instance must be on to attach this version. + + None means it cannot be attached at all. + """ + return await self._api.get( + api_path("addons", addon_slug, "compatible-license-families"), + CompatibleLicenseFamilies, + ) + + async def set_compatibility(self, addon_slug: str, family_slug: str) -> None: + """Let instances on any version of the license family attach this add-on version.""" + await self._api.send_empty( + "PUT", api_path("addons", addon_slug, "compatible-license-families", family_slug) + ) + + async def remove_compatibility(self, addon_slug: str, family_slug: str) -> None: + """Stop the license family attaching this version; instances holding it keep it.""" + await self._api.send_empty( + "DELETE", api_path("addons", addon_slug, "compatible-license-families", family_slug) + ) + + async def list_entitlements(self, addon_slug: str) -> builtins.list[AddonEntitlement]: + """Return what the add-on version grants per unit of quantity.""" + return await self._api.get_list( + api_path("addons", addon_slug, "entitlements"), AddonEntitlement + ) + + async def get_entitlement(self, addon_slug: str, entitlement_slug: str) -> AddonEntitlement: + """Return one grant of the add-on version.""" + return await self._api.get( + api_path("addons", addon_slug, "entitlements", entitlement_slug), AddonEntitlement + ) + + async def assign_entitlement( + self, + addon_slug: str, + entitlement_slug: str, + value: EntitlementValueInput, + *, + override_behavior: OverrideBehavior | None = None, + overage_percent: int | None = None, + ) -> AddonEntitlement: + """Grant an entitlement per unit of the add-on version, and return the grant. + + Args: + addon_slug: The add-on version that grants. + entitlement_slug: The entitlement granted. + value: What one unit grants, as for a license grant: a ``bool``, a number, or a + mapping for a CONFIG entitlement. A number counts once per unit held. + override_behavior: How a number grant combines with the license's: ``ADD``, + ``OVERRIDE`` or ``MAX``. + overage_percent: The overage the add-on allows: ``-1`` with an unlimited value, + ``0`` or more otherwise. Omitted, it inherits the license grant's. + + Raises: + ConflictError: An instance with a live subscription holds the version + (``AssignAddonEntitlement.BillingActive``). + """ + body = { + "entitlementSlug": entitlement_slug, + **_grant(value, override_behavior, overage_percent), + } + return await self._api.send( + "POST", api_path("addons", addon_slug, "entitlements"), AddonEntitlement, body=body + ) + + async def update_entitlement( + self, + addon_slug: str, + entitlement_slug: str, + value: EntitlementValueInput, + *, + override_behavior: OverrideBehavior | None = None, + overage_percent: int | None = None, + ) -> AddonEntitlement: + """Replace a grant's value, override behavior and overage: see :meth:`assign_entitlement`.""" + return await self._api.send( + "PUT", + api_path("addons", addon_slug, "entitlements", entitlement_slug), + AddonEntitlement, + body=_grant(value, override_behavior, overage_percent), + ) + + async def unassign_entitlement(self, addon_slug: str, entitlement_slug: str) -> None: + """Remove a grant from the add-on version. + + Raises: + ConflictError: An instance with a live subscription holds the version, or an + ``ACTIVE`` price of the version meters the entitlement. + """ + await self._api.send_empty( + "DELETE", api_path("addons", addon_slug, "entitlements", entitlement_slug) + ) + + async def list_prices( + self, addon_slug: str, *, status: PriceStatus | None = None + ) -> builtins.list[Price]: + """Return the prices of the add-on version, in display order; both statuses when omitted.""" + return await self._api.get_list( + api_path("addons", addon_slug, "prices"), Price, params={"status": status} + ) + + async def create_price( + self, + addon_slug: str, + *, + billing_model: BillingModel, + currency: str, + unit_amount_decimal: str, + billing_period: BillingPeriod | None = None, + billing_timing: BillingTiming | None = None, + metered_entitlement_slug: str | None = None, + is_default: bool | None = None, + display_label: str | None = None, + display_order: int | None = None, + ) -> Price: + """Add a price to the add-on version, under the rules of a license price. + + A price never changes once created: create another and deprecate this one. See + :meth:`AsyncBillingLicensePrices.create` for the arguments. + """ + body = price_body( + billing_model=billing_model, + currency=currency, + unit_amount_decimal=unit_amount_decimal, + billing_period=billing_period, + billing_timing=billing_timing, + metered_entitlement_slug=metered_entitlement_slug, + is_default=is_default, + display_label=display_label, + display_order=display_order, + ) + return await self._api.send( + "POST", api_path("addons", addon_slug, "prices"), Price, body=body + ) + + async def deprecate_price(self, addon_slug: str, price_id: str) -> Price: + """Stop offering a price of the add-on version. + + Raises: + ConflictError: It is a default price (``DeprecateAddonPrice.IsDefault``), which bills + every instance holding the version: retire it through a new version. + """ + return await self._api.send( + "POST", api_path("addons", addon_slug, "prices", price_id, "deprecate"), Price + ) + + async def list_families(self) -> builtins.list[AddonFamily]: + """Return every add-on family with its versions, newest family first.""" + return await self._api.get_list("/addon-families", AddonFamily) + + async def get_family(self, family_slug: str) -> AddonFamily: + """Return an add-on family with its versions and its current version.""" + return await self._api.get(api_path("addon-families", family_slug), AddonFamily) + + async def update_family( + self, + family_slug: str, + *, + is_public: bool | None = None, + increase_billing: IncreaseBilling | None = None, + ) -> AddonFamily: + """Change an add-on family's own settings; what is omitted keeps its value. + + Args: + family_slug: The family. + is_public: List the family's default ``PUBLISHED`` version, with its ``ACTIVE`` + prices, in the public catalog. Families are private until made public. + increase_billing: How a mid-period increase of its add-ons billed in advance is + billed, from the next composition: ``TRUE_UP`` or ``NEXT_BOUNDARY``. + """ + body = compact({"isPublic": is_public, "increaseBilling": increase_billing}) + return await self._api.send( + "PATCH", api_path("addon-families", family_slug), AddonFamily, body=body + ) + + +def _grant( + value: EntitlementValueInput, + override_behavior: OverrideBehavior | None, + overage_percent: int | None, +) -> dict[str, Any]: + return { + "value": entitlement_value(value), + **compact( + { + "overrideBehavior": override_behavior, + "limitCapExceededOveragePercent": overage_percent, + } + ), + } diff --git a/src/kaitencloud/_async/resources/billing/customers.py b/src/kaitencloud/_async/resources/billing/customers.py new file mode 100644 index 0000000..8cc833a --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/customers.py @@ -0,0 +1,102 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from ...._utils import api_path, compact +from ....types import ( + CompletedPaymentMethodSession, + CustomerBilling, + PaymentMethodSession, + PortalSession, +) +from .._base import AsyncResource + +__all__ = ["AsyncBillingCustomers"] + + +class AsyncBillingCustomers(AsyncResource): + """A customer's billing: its payment method, and its payment provider's portal. + + The pages it opens are the provider's own: Kaiten never sees card data. + """ + + async def get(self, customer_slug: str) -> CustomerBilling: + """Return the customer's billing e-mail, and its side in each payment provider. + + Its id there and the labels of its default payment method -- never card data. + """ + return await self._api.get(api_path("customers", customer_slug, "billing"), CustomerBilling) + + async def detach_payment_method(self, customer_slug: str) -> None: + """Remove the customer's payment method from its payment provider. + + Raises: + ConflictError: A live subscription of the customer is charged automatically + (``.InUseByAutomaticCollection``), or there is no payment method. + """ + await self._api.send_empty( + "DELETE", api_path("customers", customer_slug, "billing", "payment-method") + ) + + async def create_payment_method_session( + self, customer_slug: str, *, return_url: str, currency: str | None = None + ) -> PaymentMethodSession: + """Open the payment provider's page where the customer saves a payment method. + + Send the customer to its ``url``. When it comes back to ``return_url``, with + ``kaiten_setup_session`` in the query, call :meth:`complete_payment_method_session`. + + Args: + customer_slug: The customer. + return_url: Where the customer comes back to: https, or http on localhost. + currency: The currency the payment method is set up in; required when the customer + has no live subscription, whose currency is used otherwise. + """ + return await self._api.send( + "POST", + api_path("customers", customer_slug, "billing", "payment-method-session"), + PaymentMethodSession, + body={"returnUrl": return_url, **compact({"currency": currency})}, + ) + + async def complete_payment_method_session( + self, customer_slug: str, session_id: str + ) -> CompletedPaymentMethodSession: + """Check a setup session with the provider, and charge the customer's invoices to it. + + The redirect is never trusted: the provider is asked. Idempotent. + + Raises: + NotFoundError: The session is unknown, or another customer's. + ConflictError: The customer did not complete it (``.SessionNotComplete``). + """ + return await self._api.send( + "POST", + api_path( + "customers", + customer_slug, + "billing", + "payment-method-session", + session_id, + "complete", + ), + CompletedPaymentMethodSession, + idempotent=True, + ) + + async def create_portal_session(self, customer_slug: str, *, return_url: str) -> PortalSession: + """Open the payment provider's portal, where the customer manages its payment methods. + + What it changes there reaches Kaiten through the provider sync. + + Args: + customer_slug: The customer. + return_url: Where the portal sends the customer back: https, or http on localhost. + """ + return await self._api.send( + "POST", + api_path("customers", customer_slug, "billing", "portal-session"), + PortalSession, + body={"returnUrl": return_url}, + ) diff --git a/src/kaitencloud/_async/resources/billing/invoices.py b/src/kaitencloud/_async/resources/billing/invoices.py new file mode 100644 index 0000000..dc08466 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/invoices.py @@ -0,0 +1,379 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from collections.abc import Sequence +from datetime import datetime +from typing import Any, Literal + +from ...._constants import EXPORT_MEDIA_TYPES +from ...._utils import api_path, check_limit, compact, optional_datetime +from ....types import ( + BillingProviderKind, + HandoffClaim, + HandoffStatus, + Invoice, + InvoiceKind, + InvoiceStatus, + InvoiceSummary, + QueuedInvoice, + UsageReport, +) +from .._base import AsyncResource + +__all__ = ["AsyncBillingHandoff", "AsyncBillingInvoices"] + + +class AsyncBillingInvoices(AsyncResource): + """The organization's invoices, addressed by id: an invoice outlives its instance and customer. + + Amounts are integer minor units, as strings. An invoice is a ``DRAFT`` until it is issued: + ``MANUAL`` when the organization collects it, ``PUSHED`` to a payment provider otherwise. + """ + + async def list( + self, + *, + instance_slug: str | None = None, + customer_slug: str | None = None, + status: Sequence[InvoiceStatus] | None = None, + kind: InvoiceKind | None = None, + provider_kind: BillingProviderKind | None = None, + overdue: bool | None = None, + held: bool | None = None, + handoff_status: HandoffStatus | None = None, + issued_from: datetime | str | None = None, + issued_to: datetime | str | None = None, + boundary_from: datetime | str | None = None, + boundary_to: datetime | str | None = None, + updated_since: datetime | str | None = None, + limit: int | None = None, + ) -> builtins.list[InvoiceSummary]: + """Return the organization's invoices, newest first. + + With ``updated_since``, the invoices changed since then, oldest change first: store the + last ``updated_at`` you read and pass it back for an incremental sync. + + Args: + instance_slug: Only this instance's invoices. + customer_slug: Only this customer's invoices. + status: Only invoices in these statuses. + kind: ``ACTIVATION``, ``RENEWAL`` or ``FINAL``. + provider_kind: Only invoices issued by ``NOOP`` or by a payment provider. + overdue: Only issued invoices still unpaid past their due date. + held: Only drafts held for their usage journal. + handoff_status: ``NOT_REQUIRED``, ``PENDING`` or ``ACKNOWLEDGED``. + issued_from: Issued at or after this moment. + issued_to: Issued before this moment. + boundary_from: Billing a boundary at or after this moment. + boundary_to: Billing a boundary before this moment. + updated_since: Only invoices changed at or after this moment. + limit: The most invoices to return. Every page is walked when omitted. + """ + return await self._api.list_all( + "/invoices", + InvoiceSummary, + params={ + "instanceSlug": instance_slug, + **invoice_query( + customer_slug=customer_slug, + status=status, + kind=kind, + provider_kind=provider_kind, + overdue=overdue, + held=held, + handoff_status=handoff_status, + issued_from=issued_from, + issued_to=issued_to, + boundary_from=boundary_from, + boundary_to=boundary_to, + updated_since=updated_since, + ), + }, + limit=check_limit(limit), + ) + + async def get(self, invoice_id: str) -> Invoice: + """Return an invoice with its lines. + + It carries its hold and its handoff, and its customer and instance as they were when it + was composed. + """ + return await self._api.get(api_path("invoices", invoice_id), Invoice) + + async def export( + self, + *, + format: Literal["csv", "json"] = "csv", + granularity: Literal["line", "invoice"] | None = None, + instance_slug: str | None = None, + customer_slug: str | None = None, + status: Sequence[InvoiceStatus] | None = None, + kind: InvoiceKind | None = None, + provider_kind: BillingProviderKind | None = None, + overdue: bool | None = None, + held: bool | None = None, + handoff_status: HandoffStatus | None = None, + issued_from: datetime | str | None = None, + issued_to: datetime | str | None = None, + boundary_from: datetime | str | None = None, + boundary_to: datetime | str | None = None, + updated_since: datetime | str | None = None, + ) -> bytes: + """Return the invoices the filters of :meth:`list` select, exported. + + Amounts are integer minor units with the currency's exponent beside them. The export + carries billing e-mails: treat it as the accounting read it is. + + Args: + format: ``"csv"`` (the default), RFC 4180 with a header row, or ``"json"`` for + NDJSON, one invoice with its lines per line. + granularity: For CSV, ``"line"`` (the default) -- one row per invoice line -- or + ``"invoice"``, one row per invoice. + instance_slug: Only this instance's invoices. The other filters are + :meth:`list`'s. + """ + response = await self._api.request( + "GET", + "/invoices/export", + params={ + "format": format, + "granularity": granularity, + "instanceSlug": instance_slug, + **invoice_query( + customer_slug=customer_slug, + status=status, + kind=kind, + provider_kind=provider_kind, + overdue=overdue, + held=held, + handoff_status=handoff_status, + issued_from=issued_from, + issued_to=issued_to, + boundary_from=boundary_from, + boundary_to=boundary_to, + updated_since=updated_since, + ), + }, + headers={"Accept": EXPORT_MEDIA_TYPES[format]}, + ) + return response.content + + async def void(self, invoice_id: str, *, reason: str) -> Invoice: + """Void an invoice not yet settled, which frees its boundary for a recompose. + + A payment provider's invoice is voided there first, and here only once it confirms. + Voiding a ``VOID`` invoice again answers it unchanged. + + Args: + invoice_id: The invoice. + reason: Why, 1 to 500 characters. + """ + return await self._api.send( + "POST", + api_path("invoices", invoice_id, "void"), + Invoice, + body={"reason": reason}, + idempotent=True, + ) + + async def recompose(self, invoice_id: str) -> Invoice: + """Compose an invoice again from the usage journal as it is now, keeping its base lines. + + A held draft is rewritten in place; a ``VOID`` invoice gets a replacement for the same + boundary, which is returned. Any other invoice must be voided first. + """ + return await self._api.send("POST", api_path("invoices", invoice_id, "recompose"), Invoice) + + async def mark_paid( + self, + invoice_id: str, + *, + paid_at: datetime | str | None = None, + external_reference: str | None = None, + note: str | None = None, + ) -> Invoice: + """Record that a ``MANUAL`` invoice was paid. + + An invoice still waiting in the handoff queue is acknowledged by the same write. An + invoice a payment provider collects is settled there instead. + + Args: + invoice_id: The invoice. + paid_at: When it was paid, now or earlier. Now when omitted. + external_reference: Its number in the accounting system, 1 to 255 characters. + note: A note, such as the transfer's reference, kept only in the paid event. + """ + body = compact( + { + "paidAt": optional_datetime(paid_at), + "externalReference": external_reference, + "note": note, + } + ) + return await self._api.send( + "POST", api_path("invoices", invoice_id, "mark-paid"), Invoice, body=body + ) + + async def write_off(self, invoice_id: str, *, reason: str) -> Invoice: + """Give up collecting a ``MANUAL`` invoice: it becomes ``UNCOLLECTIBLE``. + + Args: + invoice_id: The invoice. + reason: Why, 1 to 500 characters. + """ + return await self._api.send( + "POST", + api_path("invoices", invoice_id, "write-off"), + Invoice, + body={"reason": reason}, + idempotent=True, + ) + + async def release_hold(self, invoice_id: str, *, reason: str) -> Invoice: + """Accept a held invoice as composed, and issue it. + + Args: + invoice_id: The invoice. + reason: Why, 1 to 500 characters. The release keeps it, with who released it. + + Raises: + ConflictError: The hold is ``PROVIDER_ITEM_LIMIT``, which the provider would refuse + (``ReleaseInvoiceHold.ProviderItemLimit``). + """ + return await self._api.send( + "POST", + api_path("invoices", invoice_id, "release-hold"), + Invoice, + body={"reason": reason}, + ) + + async def retry_push(self, invoice_id: str) -> Invoice: + """Put a ``DRAFT`` or ``PUSH_FAILED`` invoice of a payment provider back in the push queue.""" + return await self._api.send("POST", api_path("invoices", invoice_id, "retry-push"), Invoice) + + async def sync(self, invoice_id: str) -> Invoice: + """Read an invoice from its payment provider now, and apply what the provider says.""" + return await self._api.send("POST", api_path("invoices", invoice_id, "sync"), Invoice) + + async def list_line_reports( + self, invoice_id: str, line_id: str, *, limit: int | None = None + ) -> builtins.list[UsageReport]: + """Return the usage reports a ``USAGE`` or ``OVERAGE`` line was measured from. + + Summed by reset window and floored at 0, they give the line's measured quantity. It + answers after the instance was deleted. + + Args: + invoice_id: The invoice. + line_id: The line, as the invoice's lines carry it. + limit: The most reports to return. Every page is walked when omitted. + """ + return await self._api.list_reports( + api_path("invoices", invoice_id, "lines", line_id, "reports"), + UsageReport, + limit=check_limit(limit), + ) + + +class AsyncBillingHandoff(AsyncResource): + """The accounting handoff: the invoices waiting for the organization's accounting system. + + Claim a batch under a lease, book each invoice, then acknowledge it. An invoice not + acknowledged before its lease expires is claimed again: the queue delivers at least once, + so deduplicate on the invoice id. + """ + + async def list( + self, + *, + status: Literal["PENDING", "ACKNOWLEDGED"] | None = None, + limit: int | None = None, + ) -> builtins.list[QueuedInvoice]: + """Return the queue, oldest issue first, without leasing anything. + + Args: + status: ``PENDING`` (the default) or ``ACKNOWLEDGED``. + limit: The most invoices to return. Every page is walked when omitted. + """ + return await self._api.list_all( + "/billing/handoff", + QueuedInvoice, + params={"status": status}, + limit=check_limit(limit), + ) + + async def claim( + self, *, limit: int | None = None, lease_seconds: int | None = None + ) -> HandoffClaim: + """Lease the oldest invoices waiting, under one lease, and return them. + + Two consumers claiming at once get different invoices. A claim is not idempotent: a lost + response leaves its invoices leased until ``leased_until``, so it is never retried. + + Args: + limit: How many invoices to lease, 1 to 100. 25 when omitted. + lease_seconds: How long the lease lasts, 60 to 3600 seconds. 900 when omitted. + """ + body = compact({"limit": limit, "leaseSeconds": lease_seconds}) + return await self._api.send("POST", "/billing/handoff/claim", HandoffClaim, body=body) + + async def ack( + self, + invoice_id: str, + *, + lease_id: str | None = None, + external_reference: str | None = None, + ) -> Invoice: + """Record that the accounting system booked an invoice of the queue. Safe to repeat. + + Args: + invoice_id: The invoice. + lease_id: The claim's lease. When given, an invoice another claim took since is + refused (``AckHandoff.LeaseMismatch``). + external_reference: The invoice's number in the accounting system. Another + reference than the one it was acknowledged under is refused + (``AckHandoff.ReferenceMismatch``): take it as already handed off. + """ + body = compact({"leaseId": lease_id, "externalReference": external_reference}) + return await self._api.send( + "POST", + api_path("billing", "handoff", invoice_id, "ack"), + Invoice, + body=body, + idempotent=True, + ) + + +def invoice_query( + *, + customer_slug: str | None, + status: Sequence[InvoiceStatus] | None, + kind: InvoiceKind | None, + provider_kind: BillingProviderKind | None, + overdue: bool | None, + held: bool | None, + handoff_status: HandoffStatus | None, + issued_from: datetime | str | None, + issued_to: datetime | str | None, + boundary_from: datetime | str | None, + boundary_to: datetime | str | None, + updated_since: datetime | str | None, +) -> dict[str, Any]: + """The filters every invoice listing takes, as query parameters.""" + return { + "customerSlug": customer_slug, + "status": list(status) if status is not None else None, + "kind": kind, + "providerKind": provider_kind, + "overdue": overdue, + "held": held, + "handoffStatus": handoff_status, + "issuedFrom": optional_datetime(issued_from), + "issuedTo": optional_datetime(issued_to), + "boundaryFrom": optional_datetime(boundary_from), + "boundaryTo": optional_datetime(boundary_to), + "updatedSince": optional_datetime(updated_since), + } diff --git a/src/kaitencloud/_async/resources/billing/license_prices.py b/src/kaitencloud/_async/resources/billing/license_prices.py new file mode 100644 index 0000000..c522c70 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/license_prices.py @@ -0,0 +1,192 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from collections.abc import Mapping + +from ...._utils import addon_quantities, api_path, compact, price_body +from ....types import ( + BillingModel, + BillingPeriod, + BillingTiming, + InvoicePreview, + Price, + PriceStatus, +) +from .._base import AsyncResource + +__all__ = ["AsyncBillingLicensePrices"] + + +class AsyncBillingLicensePrices(AsyncResource): + """The prices of license versions. + + A ``FLAT_FEE`` price bills a subscription each period; a ``USAGE_BASED`` price bills the + usage of an entitlement, and an ``OVERAGE`` price the usage above the license's limit, per + sale unit. Amounts are decimal strings in minor units: ``"4900"`` is 49.00 in a currency + with two decimals. The prices of a published version never change. + """ + + async def list( + self, + license_slug: str, + *, + status: PriceStatus | None = None, + billing_model: BillingModel | None = None, + ) -> builtins.list[Price]: + """Return the prices of the license version, in display order; both statuses when omitted.""" + return await self._api.get_list( + api_path("licenses", license_slug, "prices"), + Price, + params={"status": status, "billingModel": billing_model}, + ) + + async def get(self, license_slug: str, price_id: str) -> Price: + """Return one price of the license version.""" + return await self._api.get(api_path("licenses", license_slug, "prices", price_id), Price) + + async def create( + self, + license_slug: str, + *, + billing_model: BillingModel, + currency: str, + unit_amount_decimal: str, + billing_period: BillingPeriod | None = None, + billing_timing: BillingTiming | None = None, + metered_entitlement_slug: str | None = None, + is_default: bool | None = None, + display_label: str | None = None, + display_order: int | None = None, + ) -> Price: + """Add a price to the license version, and return it. + + Args: + license_slug: The license version. + billing_model: ``FLAT_FEE``, ``USAGE_BASED`` or ``OVERAGE``. + currency: An ISO 4217 code, upper case. One currency per version. + unit_amount_decimal: The amount in minor units, as a decimal string: per period for + ``FLAT_FEE``, per sale unit for a metered price. + billing_period: ``MONTHLY``, ``QUARTERLY``, ``SEMI_ANNUAL`` or ``ANNUAL``. Required + on a ``FLAT_FEE`` price, refused on a metered one. + billing_timing: ``ADVANCE`` (the default for ``FLAT_FEE``) or ``ARREARS``, which a + metered price must be. + metered_entitlement_slug: The entitlement a metered price measures: granted by the + version, with a reset period, aggregated by ``SUM`` or ``COUNT``. + is_default: Make this ``FLAT_FEE`` price the default of its billing period. + display_label: The invoice line's label. + display_order: Its position among the version's prices. + + Raises: + ConflictError: Another price is already the default of the period + (``CreateLicensePrice.DefaultConflict``). + """ + body = price_body( + billing_model=billing_model, + currency=currency, + unit_amount_decimal=unit_amount_decimal, + billing_period=billing_period, + billing_timing=billing_timing, + metered_entitlement_slug=metered_entitlement_slug, + is_default=is_default, + display_label=display_label, + display_order=display_order, + ) + return await self._api.send( + "POST", api_path("licenses", license_slug, "prices"), Price, body=body + ) + + async def update( + self, + license_slug: str, + price_id: str, + *, + unit_amount_decimal: str | None = None, + billing_period: BillingPeriod | None = None, + billing_timing: BillingTiming | None = None, + metered_entitlement_slug: str | None = None, + is_default: bool | None = None, + display_label: str | None = None, + display_order: int | None = None, + ) -> Price: + """Edit a price of a ``DRAFT`` version; what is omitted keeps its value. + + Its currency and billing model never change, and the prices of a published version are + immutable (``UpdateLicensePrice.VersionNotDraft``). + """ + body = compact( + { + "unitAmountDecimal": unit_amount_decimal, + "billingPeriod": billing_period, + "billingTiming": billing_timing, + "meteredEntitlementSlug": metered_entitlement_slug, + "isDefault": is_default, + "displayLabel": display_label, + "displayOrder": display_order, + } + ) + return await self._api.send( + "PATCH", api_path("licenses", license_slug, "prices", price_id), Price, body=body + ) + + async def deprecate(self, license_slug: str, price_id: str) -> Price: + """Retire a price from the catalog, for good. + + It keeps billing the subscriptions pinned to it, is no longer offered, and stops being a + default. + """ + return await self._api.send( + "POST", api_path("licenses", license_slug, "prices", price_id, "deprecate"), Price + ) + + async def preview_invoice( + self, + license_slug: str, + *, + base_price_id: str | None = None, + add_ons: Mapping[str, int] | None = None, + sample_usage: Mapping[str, str] | None = None, + instance_slug: str | None = None, + voucher_code: str | None = None, + ) -> InvoicePreview: + """Compose the ``RENEWAL`` invoice a subscription to the version would get now. + + Nothing is written, and it works on a ``DRAFT`` version, to check its prices before + publishing it. + + Args: + license_slug: The license version. + base_price_id: The ``FLAT_FEE`` price billed as the base. Defaults to the version's + default one. + add_ons: Add-on versions priced in, as ``{addon_slug: quantity}``. + sample_usage: Usage of the metered entitlements over the period that ends, as + ``{entitlement_slug: quantity}`` in decimal strings. An entitlement left out + used nothing. + instance_slug: Rate this instance's own usage instead of a sample. + voucher_code: Apply a ``PRICE`` voucher's discount, consuming nothing. + """ + body = compact( + { + "basePriceId": base_price_id, + "addOns": addon_quantities(add_ons), + "sampleUsage": ( + [ + {"entitlementSlug": slug, "quantity": quantity} + for slug, quantity in sample_usage.items() + ] + if sample_usage is not None + else None + ), + "instanceSlug": instance_slug, + "voucherCode": voucher_code, + } + ) + return await self._api.send( + "POST", + api_path("licenses", license_slug, "invoice-preview"), + InvoicePreview, + body=body, + idempotent=True, + ) diff --git a/src/kaitencloud/_async/resources/billing/public.py b/src/kaitencloud/_async/resources/billing/public.py new file mode 100644 index 0000000..a9ddf13 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/public.py @@ -0,0 +1,330 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from collections.abc import Mapping + +from ...._utils import addon_quantities, api_path, check_limit, compact +from ....types import ( + BillingPeriod, + CheckoutConfirmation, + CheckoutSession, + CheckoutSessionStart, + CompletedPaymentMethodSession, + PaymentMethodSession, + PortalSession, + PublicCatalog, + SessionAddon, + SessionCheckout, + SessionInvoice, + SessionPortal, + SessionRedemption, + SessionSubscription, + SessionVoucherValidity, +) +from .._base import AsyncResource + +__all__ = ["AsyncBillingPublic", "AsyncBillingSession"] + + +class AsyncBillingPublic(AsyncResource): + """What a publishable key (``pk_...``) reaches: the public catalog and anonymous checkouts. + + Build the client with the key as its token -- the SDK sends it as the API requires, in + ``X-Kaiten-Publishable-Key`` and never as a bearer token:: + + public = AsyncKaitenClient(base_url="https://kaiten.example.com", token="pk_...") + catalog = await public.billing.public.get_catalog() + + An anonymous checkout's own routes take the token it was started with (``ksc_...``) + instead: build a client with that token to read or confirm it. + """ + + async def get_catalog( + self, *, family_slug: str | None = None, include_addons: bool = True + ) -> PublicCatalog: + """Return what a pricing page shows: the public plans, their prices and entitlements. + + For each license family the organization made public, its default version with its + ``ACTIVE`` prices and user-facing entitlements, and the same for public add-on + families. Amounts are in minor units: never sum metered prices into a headline. + + Args: + family_slug: Only this license family. + include_addons: Also list the public add-ons. True by default. + """ + return await self._api.get( + "/public/catalog", + PublicCatalog, + params={"familySlug": family_slug, "include": None if include_addons else ["none"]}, + ) + + async def create_checkout_session( + self, + *, + turnstile_token: str, + price_id: str | None = None, + license_slug: str | None = None, + billing_period: BillingPeriod | None = None, + add_ons: Mapping[str, int] | None = None, + email: str | None = None, + return_url: str | None = None, + voucher_code: str | None = None, + trial: bool | None = None, + client_reference_id: str | None = None, + dry_run: bool | None = None, + ) -> CheckoutSessionStart: + """Start a checkout for a visitor your backend does not know yet. + + It writes one checkout session and nothing else: the customer, the instance and the + subscription are created when it completes. The session's token (``ksc_...``) is in + this response only. The key must allow anonymous checkouts. + + Args: + turnstile_token: The token the Turnstile widget gave the page. Single-use. + price_id: A ``FLAT_FEE`` price of a public plan. Not with ``license_slug``. + license_slug: A public plan's version, with ``billing_period``: its default + ``ACTIVE`` ``FLAT_FEE`` price of that period. + billing_period: ``MONTHLY``, ``QUARTERLY``, ``SEMI_ANNUAL`` or ``ANNUAL``. + add_ons: Public add-ons, as ``{addon_slug: quantity}``. + email: The buyer's e-mail, which becomes the new customer's billing e-mail. Required + unless ``dry_run``. + return_url: Where the payment-method page sends the visitor back, on an origin the + key allows. Required unless ``dry_run``. + voucher_code: A code to apply to the selection. + trial: ``False`` declines the license's trial. + client_reference_id: Your own reference, echoed on the ``checkout_session`` events. + dry_run: Compute what would be due today, and write nothing. + """ + body = { + "turnstileToken": turnstile_token, + **compact( + { + "priceId": price_id, + "licenseSlug": license_slug, + "billingPeriod": billing_period, + "addOns": addon_quantities(add_ons), + "email": email, + "returnUrl": return_url, + "voucherCode": voucher_code, + "trial": trial, + "clientReferenceId": client_reference_id, + "dryRun": dry_run, + } + ), + } + return await self._api.send( + "POST", "/public/checkout-sessions", CheckoutSessionStart, body=body + ) + + async def get_checkout_session(self, session_id: str) -> CheckoutSession: + """Return the checkout session the client's ``ksc_`` token was minted for. + + Its status, what it bought and what is due, and once completed, the customer and + instance it created. The token reads it for 24 hours after it closed. + """ + return await self._api.get( + api_path("public", "checkout-sessions", session_id), CheckoutSession + ) + + async def confirm_checkout_session(self, session_id: str) -> CheckoutConfirmation: + """Complete the checkout session when nothing has to be captured, or say what is missing. + + A free plan, a trial that needs no payment method, or nothing due completes it at once: + ``completed``. Otherwise ``requires_payment_method`` comes with the page where the + visitor saves one, and the next confirmation completes it. + """ + return await self._api.send( + "POST", + api_path("public", "checkout-sessions", session_id, "confirm"), + CheckoutConfirmation, + ) + + +class AsyncBillingSession(AsyncResource): + """What a customer session (``kst_...``) reaches: one customer's own billing. + + These are the routes a customer's page calls with the session your backend minted with + :meth:`AsyncBillingCustomerSessions.create`. Build the client with the session's token to + call them from Python:: + + customer = AsyncKaitenClient(base_url="https://kaiten.example.com", token=session.token) + portal = await customer.billing.session.get_portal() + + The subscription routes need a session bound to an instance. + """ + + async def get_portal(self) -> SessionPortal: + """Return everything the customer's portal shows, in one read. + + The customer, and for a session bound to an instance its subscription, effective quotas + with where they come from, add-ons, vouchers and next invoice; the payment method; and + what the portal may offer. + """ + return await self._api.get("/public/session/portal", SessionPortal) + + async def list_invoices(self, *, limit: int | None = None) -> builtins.list[SessionInvoice]: + """Return the customer's issued invoices, newest first, with how to pay each. + + Args: + limit: The most invoices to return. Every page is walked when omitted. + """ + return await self._api.list_all( + "/public/session/invoices", SessionInvoice, limit=check_limit(limit) + ) + + async def checkout( + self, + *, + license_price_id: str, + add_ons: Mapping[str, int] | None = None, + voucher_code: str | None = None, + billing_email: str | None = None, + return_url: str | None = None, + setup_session_id: str | None = None, + trial: bool | None = None, + dry_run: bool | None = None, + ) -> SessionCheckout: + """Subscribe the session's instance to a price of the public catalog, in one call. + + ``status`` says what happened: ``preview`` for a dry run, ``requires_payment_method`` + with the page where the customer saves one -- call again with its ``setup_session_id`` + afterwards -- or ``subscribed``. + + Args: + license_price_id: An ``ACTIVE`` flat-fee price of a public plan. + add_ons: Public add-ons that fit the plan, as ``{addon_slug: quantity}``. + voucher_code: A code to redeem with the checkout. + billing_email: Where invoices go; required when the customer has none. + return_url: Where the payment-method page sends the customer back. + setup_session_id: The setup session a ``requires_payment_method`` answer gave. + trial: ``False`` skips the plan's trial. + dry_run: Answer what the checkout would bill today, and change nothing. + """ + body = { + "licensePriceId": license_price_id, + **compact( + { + "addOns": addon_quantities(add_ons), + "voucherCode": voucher_code, + "billingEmail": billing_email, + "returnUrl": return_url, + "setupSessionId": setup_session_id, + "trial": trial, + "dryRun": dry_run, + } + ), + } + return await self._api.send("POST", "/public/session/checkout", SessionCheckout, body=body) + + async def cancel(self, *, reason: str | None = None) -> SessionSubscription: + """Cancel the instance's subscription at the end of the period paid for. + + It keeps billing and granting until then. A trial ends at once. Repeating it changes + nothing. + + Args: + reason: Why the customer cancels, up to 500 characters; you see it on the + subscription. + """ + return await self._api.send( + "POST", + "/public/session/billing/cancel", + SessionSubscription, + body=compact({"reason": reason}), + idempotent=True, + ) + + async def reactivate(self) -> SessionSubscription: + """Take back a cancellation scheduled for the period's end, before it.""" + return await self._api.send( + "POST", "/public/session/billing/reactivate", SessionSubscription + ) + + async def set_addon_quantity(self, addon_slug: str, quantity: int) -> SessionAddon: + """Attach a public add-on to the instance, change its quantity, or remove it with ``0``. + + With a live subscription, a lower quantity is scheduled for the next renewal: + ``pending_quantity`` shows it. + """ + return await self._api.send( + "PUT", + api_path("public", "session", "addons", addon_slug), + SessionAddon, + body={"quantity": quantity}, + ) + + async def create_payment_method_session( + self, *, return_url: str, currency: str | None = None + ) -> PaymentMethodSession: + """Open the payment provider's page where the customer saves a payment method. + + Args: + return_url: Where the page sends the customer back, on an origin one of the + organization's publishable keys allows. + currency: Required when the customer has no live subscription. + """ + return await self._api.send( + "POST", + "/public/session/billing/payment-method-session", + PaymentMethodSession, + body={"returnUrl": return_url, **compact({"currency": currency})}, + ) + + async def complete_payment_method_session( + self, session_id: str + ) -> CompletedPaymentMethodSession: + """Check a setup session with the provider, and charge the customer's invoices to it. + + Safe to repeat. + """ + return await self._api.send( + "POST", + api_path( + "public", "session", "billing", "payment-method-session", session_id, "complete" + ), + CompletedPaymentMethodSession, + idempotent=True, + ) + + async def create_portal_session(self, *, return_url: str) -> PortalSession: + """Open the payment provider's portal for the customer. + + Args: + return_url: Where the portal sends the customer back, on an origin one of the + organization's publishable keys allows. + """ + return await self._api.send( + "POST", + "/public/session/billing/portal-session", + PortalSession, + body={"returnUrl": return_url}, + ) + + async def validate_voucher( + self, code: str, *, license_price_id: str | None = None + ) -> SessionVoucherValidity: + """Say whether a code would apply to the customer, with what it gives. Nothing is redeemed. + + Args: + code: The code as the customer typed it. + license_price_id: The price about to be checked out. + """ + body = {"code": code, **compact({"licensePriceId": license_price_id})} + return await self._api.send( + "POST", "/public/session/vouchers/validate", SessionVoucherValidity, body=body + ) + + async def redeem_voucher(self, code: str, *, dry_run: bool | None = None) -> SessionRedemption: + """Redeem a code on the instance, and return the redemption -- never the code. + + Args: + code: The code as the customer typed it. + dry_run: Answer the redemption it would make, and write nothing. + """ + body = {"code": code, **compact({"dryRun": dry_run})} + return await self._api.send( + "POST", "/public/session/vouchers/redeem", SessionRedemption, body=body + ) diff --git a/src/kaitencloud/_async/resources/billing/sessions.py b/src/kaitencloud/_async/resources/billing/sessions.py new file mode 100644 index 0000000..c437834 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/sessions.py @@ -0,0 +1,184 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from collections.abc import Sequence + +from ...._utils import api_path, check_limit, compact +from ....types import ( + CheckoutSessionAdmin, + CheckoutSessionStatus, + CreatedCustomerSession, + PublishableKey, + PublishableKeyCreated, +) +from .._base import AsyncResource + +__all__ = [ + "AsyncBillingCheckoutSessions", + "AsyncBillingCustomerSessions", + "AsyncBillingPublishableKeys", +] + + +class AsyncBillingPublishableKeys(AsyncResource): + """Publishable keys (``pk_...``): what a web page reads the public catalog with. + + A publishable key is not a secret: it ships in pages, and a browser may send it only from + one of its allowed origins. With ``anonymous_checkout``, it also starts checkouts for + visitors your backend does not know yet. + """ + + async def list(self, *, include_revoked: bool = False) -> builtins.list[PublishableKey]: + """Return the organization's keys, newest first: their last characters, never the keys.""" + return await self._api.list_all( + "/publishable-keys", PublishableKey, params={"includeRevoked": include_revoked or None} + ) + + async def create( + self, + *, + label: str, + allowed_origins: Sequence[str], + anonymous_checkout: bool | None = None, + turnstile_site_key: str | None = None, + turnstile_secret: str | None = None, + ) -> PublishableKeyCreated: + """Issue a publishable key, and return it with the key, which exists in this response only. + + Args: + label: What the key is for. + allowed_origins: The browser origins allowed to send it, as + ``scheme://host[:port]``: https, or http on localhost. Empty, no browser may; + a request without an ``Origin`` header -- server-side rendering -- still may. + anonymous_checkout: Let the key start anonymous checkouts. It needs a Turnstile site + key and secret. + turnstile_site_key: The Turnstile site key the page renders its widget with. + turnstile_secret: The Turnstile secret the server verifies tokens with, kept in + Vault and never returned. + """ + body = { + "label": label, + "allowedOrigins": list(allowed_origins), + **compact( + { + "anonymousCheckout": anonymous_checkout, + "turnstileSiteKey": turnstile_site_key, + "turnstileSecret": turnstile_secret, + } + ), + } + return await self._api.send("POST", "/publishable-keys", PublishableKeyCreated, body=body) + + async def update( + self, + key_id: str, + *, + label: str | None = None, + allowed_origins: Sequence[str] | None = None, + anonymous_checkout: bool | None = None, + turnstile_site_key: str | None = None, + turnstile_secret: str | None = None, + ) -> PublishableKey: + """Change a live key; what is omitted keeps its value, and ``""`` clears a Turnstile value. + + Raises: + ConflictError: The key is revoked (``UpdatePublishableKey.Revoked``). + """ + body = compact( + { + "label": label, + "allowedOrigins": list(allowed_origins) if allowed_origins is not None else None, + "anonymousCheckout": anonymous_checkout, + "turnstileSiteKey": turnstile_site_key, + "turnstileSecret": turnstile_secret, + } + ) + return await self._api.send( + "PATCH", api_path("publishable-keys", key_id), PublishableKey, body=body + ) + + async def revoke(self, key_id: str) -> PublishableKey: + """Revoke a key, for good, with its anonymous checkout sessions. Idempotent.""" + return await self._api.send( + "POST", api_path("publishable-keys", key_id, "revoke"), PublishableKey, idempotent=True + ) + + +class AsyncBillingCustomerSessions(AsyncResource): + """Customer sessions (``kst_...``): what lets a customer's browser act on its own billing. + + Mint one from your backend, where the customer is signed in, and hand its token to the + page, which calls the routes of :attr:`AsyncBilling.session` with it. + """ + + async def create( + self, + *, + customer_slug: str, + instance_slug: str | None = None, + ttl_seconds: int | None = None, + ) -> CreatedCustomerSession: + """Mint a session for a customer, and return it with its token, shown this once. + + What the session writes is attributed to the credential that minted it. + + Args: + customer_slug: The customer the session acts for. + instance_slug: Bind it to one of the customer's instances, which checkout, cancel + and the other subscription routes need. + ttl_seconds: How long it lives, 300 to 3600 seconds. 30 minutes when omitted. + """ + body = { + "customerSlug": customer_slug, + **compact({"instanceSlug": instance_slug, "ttlSeconds": ttl_seconds}), + } + return await self._api.send("POST", "/customer-sessions", CreatedCustomerSession, body=body) + + async def revoke(self, session_id: str) -> None: + """Stop a session authenticating, from its next request: on sign-out, say. Idempotent.""" + await self._api.send_empty( + "POST", api_path("customer-sessions", session_id, "revoke"), idempotent=True + ) + + +class AsyncBillingCheckoutSessions(AsyncResource): + """Anonymous checkouts, as the organization reads them: never their token or voucher code. + + It is how a buyer is linked to your own account when the ``checkout_session`` events were + missed. + """ + + async def list( + self, + *, + status: Sequence[CheckoutSessionStatus] | None = None, + email: str | None = None, + client_reference_id: str | None = None, + limit: int | None = None, + ) -> builtins.list[CheckoutSessionAdmin]: + """Return the organization's anonymous checkouts, newest first. + + Args: + status: Only sessions in these statuses. A session ``OPEN`` past its expiry is + ``ABANDONED``. + email: Only the visitor with this e-mail, in any letter case. + client_reference_id: Only the sessions started with this reference of yours. + limit: The most sessions to return. Every page is walked when omitted. + """ + return await self._api.list_all( + "/checkout-sessions", + CheckoutSessionAdmin, + params={ + "status": list(status) if status is not None else None, + "email": email, + "clientReferenceId": client_reference_id, + }, + limit=check_limit(limit), + ) + + async def get(self, session_id: str) -> CheckoutSessionAdmin: + """Return an anonymous checkout. One closed 90 days ago is gone.""" + return await self._api.get(api_path("checkout-sessions", session_id), CheckoutSessionAdmin) diff --git a/src/kaitencloud/_async/resources/billing/subscriptions.py b/src/kaitencloud/_async/resources/billing/subscriptions.py new file mode 100644 index 0000000..01f58d0 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/subscriptions.py @@ -0,0 +1,346 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any + +from ...._types import NOT_GIVEN, NotGiven +from ...._utils import addon_quantities, api_path, check_limit, compact, optional_datetime +from ....types import ( + BillingProviderKind, + CanceledSubscription, + CancellationMode, + CollectionMethod, + HandoffStatus, + InstanceAddon, + InstanceBilling, + InvoiceKind, + InvoicePreview, + InvoiceStatus, + InvoiceSummary, + Redemption, + RedemptionStatus, + StartedSubscription, +) +from .._base import AsyncResource +from .invoices import invoice_query + +__all__ = [ + "AsyncBillingInstanceAddons", + "AsyncBillingInstanceVouchers", + "AsyncBillingSubscriptions", +] + + +class AsyncBillingSubscriptions(AsyncResource): + """Instances' subscriptions: what bills an instance, period after period. + + A subscription is pinned to a ``FLAT_FEE`` price of the instance's license version. Each + period closes into a ``RENEWAL`` invoice; a base price billed in advance issues the first + period's ``ACTIVATION`` invoice at once. While it is live, the instance's customer and + license cannot change. + """ + + async def get(self, instance_slug: str) -> InstanceBilling: + """Return the instance's subscription, live or ``CANCELED``. + + Raises: + NotFoundError: The instance was never subscribed. + """ + return await self._api.get(api_path("instances", instance_slug, "billing"), InstanceBilling) + + async def subscribe( + self, + instance_slug: str, + *, + base_price_id: str, + add_ons: Mapping[str, int] | None = None, + voucher_code: str | None = None, + trial_days: int | None = None, + start_at: datetime | str | None = None, + provider_kind: BillingProviderKind | None = None, + collection_method: CollectionMethod | None = None, + days_until_due: int | None = None, + ) -> StartedSubscription: + """Start billing an instance, and return its subscription with its first invoice. + + A ``CANCELED`` subscription is subscribed again. + + Args: + instance_slug: The instance. + base_price_id: An ``ACTIVE`` ``FLAT_FEE`` price of the instance's license version, + which must be ``PUBLISHED``. The subscription stays pinned to it. + add_ons: Add-ons to attach with it, as ``{addon_slug: quantity}``; the + ``ACTIVATION`` invoice bills them. + voucher_code: A voucher to redeem with the subscription. + trial_days: Days of trial, 1 to 365: nothing is billed, and no usage counts, until + it ends. + start_at: What periods are counted from: now when omitted, at most one billing + period ago otherwise. + provider_kind: Who issues and collects the invoices: ``NOOP`` (the default), the + organization itself through the handoff queue, or a connected provider. + collection_method: ``SEND_INVOICE`` or ``CHARGE_AUTOMATICALLY``. The + organization's default, read at each invoice, when omitted. + days_until_due: Payment terms in days, 0 to 365. The organization's default when + omitted. + """ + body = { + "basePriceId": base_price_id, + **compact( + { + "addOns": addon_quantities(add_ons), + "voucherCode": voucher_code, + "trialDays": trial_days, + "startAt": optional_datetime(start_at), + "providerKind": provider_kind, + "collectionMethod": collection_method, + "daysUntilDue": days_until_due, + } + ), + } + return await self._api.send( + "POST", api_path("instances", instance_slug, "billing"), StartedSubscription, body=body + ) + + async def update( + self, + instance_slug: str, + *, + provider_kind: BillingProviderKind | None = None, + collection_method: CollectionMethod | NotGiven | None = NOT_GIVEN, + days_until_due: int | NotGiven | None = NOT_GIVEN, + ) -> InstanceBilling: + """Change a subscription's provider and terms from its next invoice on. + + What is not passed keeps its value; ``None`` restores the organization's default. + Invoices already composed keep their own terms and provider. + + Args: + instance_slug: The instance. + provider_kind: Who issues and collects the invoices from the next one on. + collection_method: ``SEND_INVOICE`` or ``CHARGE_AUTOMATICALLY``. + days_until_due: Payment terms in days, 0 to 365. + """ + body: dict[str, Any] = compact({"providerKind": provider_kind}) + if not isinstance(collection_method, NotGiven): + body["collectionMethod"] = collection_method + if not isinstance(days_until_due, NotGiven): + body["daysUntilDue"] = days_until_due + return await self._api.send( + "PATCH", api_path("instances", instance_slug, "billing"), InstanceBilling, body=body + ) + + async def cancel( + self, + instance_slug: str, + *, + mode: CancellationMode | None = None, + reason: str | None = None, + ) -> CanceledSubscription: + """Cancel a subscription. + + A trial ends at once, with no invoice. Otherwise ``AT_PERIOD_END`` (the default) lets the + period paid for run out, then issues the ``FINAL`` invoice; ``IMMEDIATE`` issues it now. + Cancellation changes billing only: entitlements and license dates stay yours to change. + + Args: + instance_slug: The instance. + mode: ``AT_PERIOD_END`` or ``IMMEDIATE``. + reason: Why, up to 500 characters. + """ + return await self._api.send( + "POST", + api_path("instances", instance_slug, "billing", "cancel"), + CanceledSubscription, + body=compact({"mode": mode, "reason": reason}), + ) + + async def reactivate(self, instance_slug: str) -> InstanceBilling: + """Take back a cancellation scheduled for the period's end, before it.""" + return await self._api.send( + "POST", api_path("instances", instance_slug, "billing", "reactivate"), InstanceBilling + ) + + async def schedule_plan_change( + self, instance_slug: str, license_price_id: str + ) -> InstanceBilling: + """Schedule a move to another ``FLAT_FEE`` price for the next boundary. + + That boundary's ``RENEWAL`` bills the old plan's arrears and the new plan's advance, and + moves the instance to the new version, without proration. Scheduling another target + replaces the change. + + Args: + instance_slug: The instance. + license_price_id: An ``ACTIVE`` ``FLAT_FEE`` price of a ``PUBLISHED`` license + version, in the subscription's currency. + """ + return await self._api.send( + "PUT", + api_path("instances", instance_slug, "billing", "scheduled-change"), + InstanceBilling, + body={"licensePriceId": license_price_id}, + ) + + async def cancel_plan_change(self, instance_slug: str) -> InstanceBilling: + """Drop the plan change scheduled for the next boundary.""" + return await self._api.send( + "DELETE", + api_path("instances", instance_slug, "billing", "scheduled-change"), + InstanceBilling, + ) + + async def get_upcoming_invoice(self, instance_slug: str) -> InvoicePreview: + """Return the invoice the next boundary will issue, from the usage so far, writing nothing. + + ``would_hold`` lists the meters whose usage journal fails a check and would hold it. + """ + return await self._api.get( + api_path("instances", instance_slug, "billing", "upcoming-invoice"), InvoicePreview + ) + + async def list_invoices( + self, + instance_slug: str, + *, + customer_slug: str | None = None, + status: Sequence[InvoiceStatus] | None = None, + kind: InvoiceKind | None = None, + provider_kind: BillingProviderKind | None = None, + overdue: bool | None = None, + held: bool | None = None, + handoff_status: HandoffStatus | None = None, + issued_from: datetime | str | None = None, + issued_to: datetime | str | None = None, + boundary_from: datetime | str | None = None, + boundary_to: datetime | str | None = None, + updated_since: datetime | str | None = None, + limit: int | None = None, + ) -> builtins.list[InvoiceSummary]: + """Return the invoices of an instance's subscription, across every time it was subscribed. + + The filters are those of :meth:`AsyncBillingInvoices.list`. + """ + return await self._api.list_all( + api_path("instances", instance_slug, "invoices"), + InvoiceSummary, + params=invoice_query( + customer_slug=customer_slug, + status=status, + kind=kind, + provider_kind=provider_kind, + overdue=overdue, + held=held, + handoff_status=handoff_status, + issued_from=issued_from, + issued_to=issued_to, + boundary_from=boundary_from, + boundary_to=boundary_to, + updated_since=updated_since, + ), + limit=check_limit(limit), + ) + + +class AsyncBillingInstanceAddons(AsyncResource): + """The add-ons an instance holds, by the unit. + + Their entitlements apply at once; a subscription bills the quantity held at each boundary, + without proration. An instance holds one version per add-on family. + """ + + async def list( + self, instance_slug: str, *, include_removed: bool = False + ) -> builtins.list[InstanceAddon]: + """Return the add-ons the instance holds, in attachment order, with the prices billed. + + Args: + instance_slug: The instance. + include_removed: Also return the add-ons removed from it. + """ + return await self._api.get_list( + api_path("instances", instance_slug, "addons"), + InstanceAddon, + params={"includeRemoved": include_removed or None}, + ) + + async def attach( + self, instance_slug: str, addon_slug: str, *, quantity: int | None = None + ) -> InstanceAddon: + """Give the instance an add-on version, and return the attachment. + + Moving to another version of the same add-on is a :meth:`detach`, then an attach. + """ + body = {"addonSlug": addon_slug, **compact({"quantity": quantity})} + return await self._api.send( + "POST", api_path("instances", instance_slug, "addons"), InstanceAddon, body=body + ) + + async def set_quantity( + self, instance_slug: str, addon_slug: str, quantity: int + ) -> InstanceAddon: + """Change how many units of an add-on the instance holds, at once, a decrease included. + + It cancels a decrease the customer scheduled, even to the quantity held. + """ + return await self._api.send( + "PATCH", + api_path("instances", instance_slug, "addons", addon_slug), + InstanceAddon, + body={"quantity": quantity}, + idempotent=True, + ) + + async def detach(self, instance_slug: str, addon_slug: str) -> None: + """Remove an add-on from the instance, at once. Its entitlements drop; nothing is refunded.""" + await self._api.send_empty( + "DELETE", api_path("instances", instance_slug, "addons", addon_slug) + ) + + +class AsyncBillingInstanceVouchers(AsyncResource): + """The vouchers an instance redeemed.""" + + async def list( + self, instance_slug: str, *, status: RedemptionStatus | None = None + ) -> builtins.list[Redemption]: + """Return the instance's redemptions, newest first.""" + return await self._api.get_list( + api_path("instances", instance_slug, "vouchers"), Redemption, params={"status": status} + ) + + async def redeem(self, instance_slug: str, code: str) -> Redemption: + """Redeem a voucher code for the instance, and return the redemption. + + A boost applies from the next read or report, for its duration; a ``PRICE`` voucher + discounts the subscription's next invoices. + """ + return await self._api.send( + "POST", + api_path("instances", instance_slug, "vouchers", "redeem"), + Redemption, + body={"code": code}, + ) + + async def revoke( + self, instance_slug: str, instance_voucher_id: str, *, reason: str + ) -> Redemption: + """Stop a redemption applying: a boost ends at once, a discount applies to no further invoice. + + Invoices already issued are untouched. + + Args: + instance_slug: The instance. + instance_voucher_id: The redemption's id. + reason: Why, 1 to 500 characters. + """ + return await self._api.send( + "POST", + api_path("instances", instance_slug, "vouchers", instance_voucher_id, "revoke"), + Redemption, + body={"reason": reason}, + ) diff --git a/src/kaitencloud/_async/resources/billing/vouchers.py b/src/kaitencloud/_async/resources/billing/vouchers.py new file mode 100644 index 0000000..b5171b3 --- /dev/null +++ b/src/kaitencloud/_async/resources/billing/vouchers.py @@ -0,0 +1,321 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import builtins +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any, Literal + +from ...._utils import api_path, check_limit, compact, optional_datetime +from ....types import ( + DiscountAppliesTo, + DiscountType, + Redemption, + RedemptionRulesParam, + Validity, + Voucher, + VoucherDuration, + VoucherGrantParam, + VoucherStatus, + VoucherType, +) +from .._base import AsyncResource + +__all__ = ["AsyncBillingVouchers"] + + +class AsyncBillingVouchers(AsyncResource): + """The organization's vouchers: codes that discount a price or boost entitlements. + + A ``PRICE`` voucher discounts a subscription's invoices; an ``ENTITLEMENT_BOOST`` changes + entitlements while it applies; a ``FLAG_GRANT`` serves flag variants; a ``COMPOSITE`` does + several. A voucher is created ``DRAFT`` and becomes redeemable once published. + """ + + async def list( + self, + *, + status: VoucherStatus | None = None, + voucher_type: Literal["PRICE", "ENTITLEMENT_BOOST"] | None = None, + restricted_customer_slug: str | None = None, + ) -> builtins.list[Voucher]: + """Return the organization's vouchers, newest first, with their codes, walking all pages.""" + return await self._api.list_all( + "/vouchers", + Voucher, + params={ + "status": status, + "voucherType": voucher_type, + "restrictedCustomerSlug": restricted_customer_slug, + }, + ) + + async def get(self, voucher_id: str) -> Voucher: + """Return a voucher, with its code.""" + return await self._api.get(api_path("vouchers", voucher_id), Voucher) + + async def create( + self, + *, + name: str, + voucher_type: VoucherType, + duration: VoucherDuration, + code: str | None = None, + description: str | None = None, + duration_in_periods: int | None = None, + starts_at: datetime | str | None = None, + expires_at: datetime | str | None = None, + max_redemptions: int | None = None, + restricted_customer_slug: str | None = None, + price_discount_type: DiscountType | None = None, + price_discount_value: str | None = None, + currency: str | None = None, + price_applies_to: DiscountAppliesTo | None = None, + applicable_license_ids: Sequence[str] | None = None, + applicable_license_price_ids: Sequence[str] | None = None, + applicable_addon_ids: Sequence[str] | None = None, + applicable_addon_price_ids: Sequence[str] | None = None, + grants: Sequence[VoucherGrantParam] | None = None, + flag_grants: Mapping[str, str] | None = None, + redemption_rules: RedemptionRulesParam | None = None, + ) -> Voucher: + """Create a ``DRAFT`` voucher, and return it with its code. + + Args: + name: The voucher's name. + voucher_type: ``PRICE``, ``ENTITLEMENT_BOOST``, ``FLAG_GRANT`` or ``COMPOSITE``. + duration: How long a discount lasts: ``ONE_TIME``, ``REPEATING`` or ``FOREVER``. + code: 8 to 64 of ``[A-Za-z0-9_-]``, matched without case or separators. Generated + when omitted. + description: A description. + duration_in_periods: How many periods a ``REPEATING`` discount lasts. + starts_at: When it can be redeemed from. + expires_at: When it can no longer be redeemed. + max_redemptions: How many times it can be redeemed. + restricted_customer_slug: The only customer that may redeem it. + price_discount_type: ``PERCENTAGE`` or ``FIXED_AMOUNT``. + price_discount_value: A percentage in (0, 100], or an amount in minor units, as a + string. + currency: Required with ``FIXED_AMOUNT``, refused otherwise. + price_applies_to: What the discount applies to: ``LICENSE_BASE``, ``ADDONS``, + ``BOTH``, or ``SELECTED_PRICES``. + applicable_license_ids: The license versions it applies to. + applicable_license_price_ids: With ``SELECTED_PRICES``, the license prices. + applicable_addon_ids: The add-on versions it applies to. + applicable_addon_price_ids: With ``SELECTED_PRICES``, the add-on prices. + grants: What an ``ENTITLEMENT_BOOST`` grants, one entitlement each. + flag_grants: What a ``FLAG_GRANT`` serves, as ``{flag_slug: variant}``. + redemption_rules: Who may redeem it, beyond its dates and count. + """ + body = _voucher( + name=name, + voucher_type=voucher_type, + duration=duration, + code=code, + description=description, + duration_in_periods=duration_in_periods, + starts_at=starts_at, + expires_at=expires_at, + max_redemptions=max_redemptions, + restricted_customer_slug=restricted_customer_slug, + price_discount_type=price_discount_type, + price_discount_value=price_discount_value, + currency=currency, + price_applies_to=price_applies_to, + applicable_license_ids=applicable_license_ids, + applicable_license_price_ids=applicable_license_price_ids, + applicable_addon_ids=applicable_addon_ids, + applicable_addon_price_ids=applicable_addon_price_ids, + grants=grants, + flag_grants=flag_grants, + redemption_rules=redemption_rules, + ) + return await self._api.send("POST", "/vouchers", Voucher, body=body) + + async def update( + self, + voucher_id: str, + *, + name: str, + voucher_type: VoucherType, + duration: VoucherDuration, + code: str | None = None, + description: str | None = None, + duration_in_periods: int | None = None, + starts_at: datetime | str | None = None, + expires_at: datetime | str | None = None, + max_redemptions: int | None = None, + restricted_customer_slug: str | None = None, + price_discount_type: DiscountType | None = None, + price_discount_value: str | None = None, + currency: str | None = None, + price_applies_to: DiscountAppliesTo | None = None, + applicable_license_ids: Sequence[str] | None = None, + applicable_license_price_ids: Sequence[str] | None = None, + applicable_addon_ids: Sequence[str] | None = None, + applicable_addon_price_ids: Sequence[str] | None = None, + grants: Sequence[VoucherGrantParam] | None = None, + flag_grants: Mapping[str, str] | None = None, + redemption_rules: RedemptionRulesParam | None = None, + ) -> Voucher: + """Replace a voucher, and return it. The arguments are :meth:`create`'s. + + A ``DRAFT`` is replaced whole, but its type. An ``ACTIVE`` voucher takes a new name, + description, expiry and redemption count only; its other members must stay as they are + (``UpdateVoucher.NotEditable``). + """ + body = _voucher( + name=name, + voucher_type=voucher_type, + duration=duration, + code=code, + description=description, + duration_in_periods=duration_in_periods, + starts_at=starts_at, + expires_at=expires_at, + max_redemptions=max_redemptions, + restricted_customer_slug=restricted_customer_slug, + price_discount_type=price_discount_type, + price_discount_value=price_discount_value, + currency=currency, + price_applies_to=price_applies_to, + applicable_license_ids=applicable_license_ids, + applicable_license_price_ids=applicable_license_price_ids, + applicable_addon_ids=applicable_addon_ids, + applicable_addon_price_ids=applicable_addon_price_ids, + grants=grants, + flag_grants=flag_grants, + redemption_rules=redemption_rules, + ) + return await self._api.send("PUT", api_path("vouchers", voucher_id), Voucher, body=body) + + async def delete(self, voucher_id: str) -> None: + """Delete a voucher no instance ever redeemed, whatever its status, freeing its code. + + Raises: + ConflictError: It was redeemed at least once, or a checkout waiting for its payment + applies it (``DeleteVoucher.Redeemed``): :meth:`archive` it instead. + """ + await self._api.send_empty("DELETE", api_path("vouchers", voucher_id)) + + async def publish(self, voucher_id: str) -> Voucher: + """Make a ``DRAFT`` voucher redeemable.""" + return await self._api.send("POST", api_path("vouchers", voucher_id, "publish"), Voucher) + + async def archive(self, voucher_id: str) -> Voucher: + """Stop a voucher being redeemed. Redemptions already made keep applying until revoked.""" + return await self._api.send("POST", api_path("vouchers", voucher_id, "archive"), Voucher) + + async def list_redemptions( + self, voucher_id: str, *, limit: int | None = None + ) -> builtins.list[Redemption]: + """Return every redemption of a voucher, newest first. + + Args: + voucher_id: The voucher. + limit: The most redemptions to return. Every page is walked when omitted. + """ + return await self._api.list_all( + api_path("vouchers", voucher_id, "redemptions"), Redemption, limit=check_limit(limit) + ) + + async def lookup(self, code: str) -> Voucher: + """Return the voucher a code names, matched without case or separators. + + The code travels in the request body, never in a URL. + """ + return await self._api.send( + "POST", "/vouchers/lookup", Voucher, body={"code": code}, idempotent=True + ) + + async def validate( + self, + code: str, + *, + instance_slug: str | None = None, + license_price_id: str | None = None, + ) -> Validity: + """Run the redemption checks of a code without redeeming it. + + ``valid`` says whether it would apply; otherwise ``reason`` is the first check that + failed, and ``rule`` the eligibility rule. + + Args: + code: The code. + instance_slug: Also run the checks that need the redeeming instance. + license_price_id: The ``FLAT_FEE`` price a subscription would start on, read instead + of the instance's live subscription. + """ + body = { + "code": code, + **compact({"instanceSlug": instance_slug, "licensePriceId": license_price_id}), + } + return await self._api.send( + "POST", "/vouchers/validate", Validity, body=body, idempotent=True + ) + + +def _voucher( + *, + name: str, + voucher_type: VoucherType, + duration: VoucherDuration, + code: str | None, + description: str | None, + duration_in_periods: int | None, + starts_at: datetime | str | None, + expires_at: datetime | str | None, + max_redemptions: int | None, + restricted_customer_slug: str | None, + price_discount_type: DiscountType | None, + price_discount_value: str | None, + currency: str | None, + price_applies_to: DiscountAppliesTo | None, + applicable_license_ids: Sequence[str] | None, + applicable_license_price_ids: Sequence[str] | None, + applicable_addon_ids: Sequence[str] | None, + applicable_addon_price_ids: Sequence[str] | None, + grants: Sequence[VoucherGrantParam] | None, + flag_grants: Mapping[str, str] | None, + redemption_rules: RedemptionRulesParam | None, +) -> dict[str, Any]: + return { + "name": name, + "voucherType": voucher_type, + "duration": duration, + **compact( + { + "code": code, + "description": description, + "durationInPeriods": duration_in_periods, + "startsAt": optional_datetime(starts_at), + "expiresAt": optional_datetime(expires_at), + "maxRedemptions": max_redemptions, + "restrictedCustomerSlug": restricted_customer_slug, + "priceDiscountType": price_discount_type, + "priceDiscountValue": price_discount_value, + "currency": currency, + "priceAppliesTo": price_applies_to, + "applicableLicenseIds": _listed(applicable_license_ids), + "applicableLicensePriceIds": _listed(applicable_license_price_ids), + "applicableAddonIds": _listed(applicable_addon_ids), + "applicableAddonPriceIds": _listed(applicable_addon_price_ids), + "grants": _listed(grants), + "flagGrants": ( + [ + {"flagSlug": flag, "variant": variant} + for flag, variant in flag_grants.items() + ] + if flag_grants is not None + else None + ), + "redemptionRules": dict(redemption_rules) if redemption_rules is not None else None, + } + ), + } + + +def _listed(values: Sequence[Any] | None) -> list[Any] | None: + return list(values) if values is not None else None diff --git a/src/kaitencloud/_async/resources/customers.py b/src/kaitencloud/_async/resources/customers.py index cacdfd0..3439a22 100644 --- a/src/kaitencloud/_async/resources/customers.py +++ b/src/kaitencloud/_async/resources/customers.py @@ -33,6 +33,7 @@ async def create( external_customer_id: str | None = None, domain: str | None = None, integrations: Mapping[str, IntegrationParam] | None = None, + billing_email: str | None = None, ) -> Customer: """Create a customer. @@ -44,6 +45,8 @@ async def create( domain: Its domain name, e.g. ``"acme.com"``. integrations: Links to records in third-party systems, keyed by adapter name. Only settable here: afterwards, use :meth:`update_integration`. + billing_email: Where invoices sent to the customer are delivered, at most 254 + characters. Personal data: never included in event payloads. Raises: ConflictError: The slug is taken, or the organization reached its customer limit. @@ -55,6 +58,7 @@ async def create( "externalCustomerId": external_customer_id, "domain": domain, "integrations": integrations_body(integrations), + "billingEmail": billing_email, } ) return await self._api.send("POST", "/customers", Customer, body=body) @@ -66,13 +70,22 @@ async def update( name: str, external_customer_id: str | None = None, domain: str | None = None, + billing_email: str | None = None, ) -> None: """Replace a customer's details. A full replacement: the external customer id and the domain are cleared when left out. - The slug cannot be changed, and integrations have their own methods. + The billing e-mail is the exception: left out, it keeps its value, and ``""`` removes + it. The slug cannot be changed, and integrations have their own methods. """ - body = compact({"name": name, "externalCustomerId": external_customer_id, "domain": domain}) + body = compact( + { + "name": name, + "externalCustomerId": external_customer_id, + "domain": domain, + "billingEmail": billing_email, + } + ) await self._api.send_empty("PUT", api_path("customers", customer_slug), body=body) async def delete(self, customer_slug: str) -> None: diff --git a/src/kaitencloud/_async/resources/instances.py b/src/kaitencloud/_async/resources/instances.py index d7b6906..c0f6a61 100644 --- a/src/kaitencloud/_async/resources/instances.py +++ b/src/kaitencloud/_async/resources/instances.py @@ -6,11 +6,15 @@ import builtins from collections.abc import Mapping from datetime import datetime -from typing import Any +from typing import Any, Literal -from ..._exceptions import ThresholdExceededError +from ..._constants import EXPORT_MEDIA_TYPES +from ..._exceptions import ThresholdExceededError, TransactionIdReusedError +from ..._models import UsageReportResult +from ..._types import EntitlementValueInput from ..._utils import ( api_path, + check_limit, compact, format_datetime, integration_body, @@ -19,12 +23,16 @@ ) from ...types import ( AuditTrail, + EntitlementException, EntitlementUsage, + ExceptionKind, Instance, InstanceIntegration, InstanceStatus, IntegrationParam, + ReportedEntitlementUsage, UsageBehavior, + UsageReport, ) from ._base import AsyncResource @@ -237,26 +245,272 @@ async def report_usage( ThresholdExceededError: The report would take usage past the maximum the license allows -- the granted value plus its overage allowance. Nothing was recorded. - Never retried automatically: without an idempotency key on the wire, a report retried - after a lost response would be counted twice. + Never retried automatically: a report retried after a lost response would be counted + twice. :meth:`report_usage_detailed` sends one under an idempotency key, which makes + sending it again safe. """ - if isinstance(value, bool) or not isinstance(value, (int, float)): - raise TypeError(f"usage must be a number, got {type(value).__name__}") - body = compact( - { - "value": {"type": "number", "value": value}, - "behavior": behavior, - "metadata": dict(metadata) if metadata is not None else None, - } - ) return await self._api.send( "POST", - api_path("instances", instance_slug, "entitlements", entitlement_slug, "usage"), + _usage_path(instance_slug, entitlement_slug), EntitlementUsage, - body=body, + body=_usage_report(value, behavior, metadata), errors={409: ThresholdExceededError}, ) + async def report_usage_detailed( + self, + instance_slug: str, + entitlement_slug: str, + value: float, + *, + behavior: UsageBehavior = "append", + metadata: Mapping[str, Any] | None = None, + transaction_id: str | None = None, + ) -> UsageReportResult: + """Report usage, under an idempotency key, and return what the API said about it. + + Kaiten applies a report at most once per key, instance and entitlement within its + idempotency window -- 35 days by default. A report sent again under the same key, with + the same behavior and value, answers the original report and counts nothing: it comes + back with ``replayed`` set. + + Args: + instance_slug: The instance that consumed. + entitlement_slug: The entitlement consumed. + value: The amount, as :meth:`report_usage` takes it. + behavior: ``"append"`` (the default) or ``"set"``. + metadata: Free-form metadata, stored with the report in the usage history when its + compact JSON is at most 4 KiB; above, it is dropped and ``metadata_dropped`` set. + It must hold no personal data. + transaction_id: The id of the business event the report counts -- 1 to 128 of + letters, digits, ``.``, ``_``, ``:`` and ``-``, e.g. ``"llm-call-9f2c:tokens"``. + Sent as the ``Idempotency-Key`` header. + + Raises: + ThresholdExceededError: The report would take usage past the maximum the license + allows. Nothing was recorded. + TransactionIdReusedError: The key was spent on a report with another behavior or + value. Nothing was recorded. + + Never retried automatically, key or not: a Kaiten older than keys ignores the header and + would count a resent report twice. Resend it yourself, under the same key. + """ + try: + response = await self._api.request( + "POST", + _usage_path(instance_slug, entitlement_slug), + body=_usage_report(value, behavior, metadata), + headers=compact({"Idempotency-Key": transaction_id}), + errors={409: ThresholdExceededError}, + ) + except ThresholdExceededError as error: + # The usage endpoint answers 409 for two reasons; only the code tells them apart. + if error.code == "ReportEntitlementUsageMetric.TransactionIdReused": + raise TransactionIdReusedError( + str(error), response=error.response, problem=error.problem + ) from None + raise + return UsageReportResult( + usage=self._api.validate( + ReportedEntitlementUsage, self._api.decode(response), response + ), + replayed=response.headers.get("Idempotent-Replayed") == "true", + metadata_dropped=bool(response.headers.get("Kaiten-Metadata-Dropped")), + ) + + async def list_usage_reports( + self, + instance_slug: str, + entitlement_slug: str, + *, + from_: datetime | str | None = None, + to: datetime | str | None = None, + transaction_id: str | None = None, + limit: int | None = None, + ) -> builtins.list[UsageReport]: + """Return the usage history of one entitlement for an instance, oldest report first. + + Every report the API accepted in the range, with the counter before and after it and + the limit in force. Decimals are strings, exact to the digit. Reports are kept for the + organization's usage history retention. + + Args: + instance_slug: The instance. + entitlement_slug: The entitlement. + from_: Start of the range, inclusive. Defaults to 30 days before ``to``; a moment + before the usage history starts is refused (``ListUsageReports.OutsideRetention``). + to: End of the range, exclusive. Defaults to now. + transaction_id: Only the report sent with this key. + limit: The most reports to return. Every page is walked when omitted. + """ + return await self._api.list_reports( + api_path( + "instances", instance_slug, "entitlements", entitlement_slug, "usage", "reports" + ), + UsageReport, + params={ + "from": optional_datetime(from_), + "to": optional_datetime(to), + "transactionId": transaction_id, + }, + limit=check_limit(limit), + ) + + async def export_usage_reports( + self, + instance_slug: str, + entitlement_slug: str, + *, + from_: datetime | str | None = None, + to: datetime | str | None = None, + format: Literal["csv", "json"] = "csv", + ) -> bytes: + """Return the usage history of one entitlement for an instance, exported. + + At most 366 days per call, decimals written exactly as the journal stores them. + + Args: + instance_slug: The instance. + entitlement_slug: The entitlement. + from_: Start of the range, inclusive (see :meth:`list_usage_reports`). + to: End of the range, exclusive. Defaults to now. + format: ``"csv"`` (the default), RFC 4180 with a header row, or ``"json"`` for + NDJSON, one report per line in the shape :meth:`list_usage_reports` returns. + """ + response = await self._api.request( + "GET", + api_path( + "instances", + instance_slug, + "entitlements", + entitlement_slug, + "usage", + "reports", + "export", + ), + params={ + "from": optional_datetime(from_), + "to": optional_datetime(to), + "format": format, + }, + headers={"Accept": EXPORT_MEDIA_TYPES[format]}, + ) + return response.content + + async def export_organization_usage_reports( + self, + *, + from_: datetime | str | None = None, + to: datetime | str | None = None, + format: Literal["csv", "json"] = "csv", + instance_slug: str | None = None, + instance_id: str | None = None, + entitlement_slug: str | None = None, + entitlement_id: str | None = None, + ) -> bytes: + """Return the whole organization's usage history, exported, ordered by acceptance. + + Every instance and entitlement, at most 31 days per call: keep it before deleting the + organization. ``instance_id`` and ``entitlement_id`` reach deleted ones, whose reports + are kept. + + Args: + from_: Start of the range, inclusive (see :meth:`list_usage_reports`). + to: End of the range, exclusive. Defaults to now. + format: ``"csv"`` (the default) or ``"json"`` for NDJSON. + instance_slug: Only this instance. + instance_id: Only this instance, by id. + entitlement_slug: Only this entitlement. + entitlement_id: Only this entitlement, by id. + """ + response = await self._api.request( + "GET", + "/usage/reports/export", + params={ + "from": optional_datetime(from_), + "to": optional_datetime(to), + "format": format, + "instanceSlug": instance_slug, + "instanceId": instance_id, + "entitlementSlug": entitlement_slug, + "entitlementId": entitlement_id, + }, + headers={"Accept": EXPORT_MEDIA_TYPES[format]}, + ) + return response.content + + async def list_entitlement_exceptions( + self, instance_slug: str, *, include_inactive: bool = False + ) -> builtins.list[EntitlementException]: + """Return the instance's entitlement exceptions, latest start first, with their status. + + Args: + instance_slug: The instance. + include_inactive: Also return the revoked and ended ones. Only the active and the + scheduled ones otherwise. + """ + return await self._api.get_list( + api_path("instances", instance_slug, "entitlement-exceptions"), + EntitlementException, + params={"filter": "all" if include_inactive else None}, + ) + + async def create_entitlement_exception( + self, + instance_slug: str, + entitlement_slug: str, + *, + kind: ExceptionKind, + reason: str, + value: EntitlementValueInput | None = None, + starts_at: datetime | str | None = None, + ends_at: datetime | str | None = None, + ) -> EntitlementException: + """Override one entitlement of an instance, for a while, and return the exception. + + An exception is your explicit, dated decision on one instance: the last layer of its + effective entitlements, after the license, the add-ons and the boosts. It wins over + them, and may lower the value. An instance has one unrevoked exception per entitlement + at any moment. It works whether or not billing is enabled. + + Args: + instance_slug: The instance. + entitlement_slug: The entitlement it overrides, granted or not. + kind: ``SET`` works on every type; ``ADD`` and ``UNLIMITED`` on numbers only. + reason: Why, 1 to 500 characters, shown to you only. + value: For ``SET``, a number of 0 or more, a ``bool`` or a mapping, by the + entitlement's type; for ``ADD``, a number above 0; nothing for ``UNLIMITED``. + starts_at: When it starts: now when omitted; a future moment schedules it. + ends_at: When it ends. It never does when omitted. + """ + body = { + "entitlementSlug": entitlement_slug, + "kind": kind, + "reason": reason, + **compact( + { + "value": dict(value) if isinstance(value, Mapping) else value, + "startsAt": optional_datetime(starts_at), + "endsAt": optional_datetime(ends_at), + } + ), + } + return await self._api.send( + "POST", + api_path("instances", instance_slug, "entitlement-exceptions"), + EntitlementException, + body=body, + ) + + async def revoke_entitlement_exception(self, instance_slug: str, exception_id: str) -> None: + """Revoke an exception, which is kept: a scheduled one never starts, an active one ends. + + Revoking one already revoked or ended changes nothing. + """ + await self._api.send_empty( + "DELETE", api_path("instances", instance_slug, "entitlement-exceptions", exception_id) + ) + async def get_integration( self, instance_slug: str, integration_name: str ) -> InstanceIntegration: @@ -347,3 +601,21 @@ def _instance( } ), } + + +def _usage_path(instance_slug: str, entitlement_slug: str) -> str: + return api_path("instances", instance_slug, "entitlements", entitlement_slug, "usage") + + +def _usage_report( + value: float, behavior: UsageBehavior, metadata: Mapping[str, Any] | None +) -> dict[str, Any]: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError(f"usage must be a number, got {type(value).__name__}") + return compact( + { + "value": {"type": "number", "value": value}, + "behavior": behavior, + "metadata": dict(metadata) if metadata is not None else None, + } + ) diff --git a/src/kaitencloud/_async/resources/license_families.py b/src/kaitencloud/_async/resources/license_families.py index c71485f..35ad794 100644 --- a/src/kaitencloud/_async/resources/license_families.py +++ b/src/kaitencloud/_async/resources/license_families.py @@ -6,8 +6,8 @@ import builtins from typing import Any -from ..._utils import api_path -from ...types import LicenseFamilyView +from ..._utils import api_path, compact +from ...types import LicenseFamilyView, TrialEndWithoutPaymentMethod from ._base import AsyncResource __all__ = ["AsyncLicenseFamilies"] @@ -59,3 +59,38 @@ async def get( return await self._api.get( api_path("license-families", family_slug), LicenseFamilyView, params=params or None ) + + async def update( + self, + family_slug: str, + *, + is_public: bool | None = None, + fallback_license_family_slug: str | None = None, + trial_end_without_payment_method: TrialEndWithoutPaymentMethod | None = None, + trial_reminder_days: int | None = None, + ) -> LicenseFamilyView: + """Change a family's own settings, and return the family; what is omitted keeps its value. + + Args: + family_slug: The family's slug. + is_public: List the family's default ``PUBLISHED`` version, with its ``ACTIVE`` + prices, in the public catalog. Families are private until made public. + fallback_license_family_slug: Another family of the organization, every version of + it priced ``FREE``, whose default version an instance moves to when its + subscription ends. ``""`` removes the fallback. + trial_end_without_payment_method: What a trial charged automatically becomes at its + end when the customer has no active payment method: ``CANCEL`` or ``CONVERT``. + trial_reminder_days: How many days before a trial's end ``trial_will_end`` fires, + 1 to 30, for the trials started after the change. + """ + body = compact( + { + "isPublic": is_public, + "fallbackLicenseFamilySlug": fallback_license_family_slug, + "trialEndWithoutPaymentMethod": trial_end_without_payment_method, + "trialReminderDays": trial_reminder_days, + } + ) + return await self._api.send( + "PATCH", api_path("license-families", family_slug), LicenseFamilyView, body=body + ) diff --git a/src/kaitencloud/_async/resources/licenses.py b/src/kaitencloud/_async/resources/licenses.py index 2e49181..4ec6fa5 100644 --- a/src/kaitencloud/_async/resources/licenses.py +++ b/src/kaitencloud/_async/resources/licenses.py @@ -8,7 +8,7 @@ from ..._types import EntitlementValueInput from ..._utils import api_path, compact, entitlement_value -from ...types import License, LicenseEntitlement, LicenseType +from ...types import License, LicenseEntitlement, LicenseType, PricingType from ._base import AsyncResource __all__ = ["AsyncLicenses"] @@ -45,6 +45,10 @@ async def create( family_slug: str | None = None, family_id: str | None = None, lifecycle_state: Literal["DRAFT", "PUBLISHED"] | None = None, + pricing_type: PricingType | None = None, + trial_period_days: int | None = None, + requires_payment_method: bool | None = None, + self_serve_cta_url: str | None = None, ) -> License: """Create a license: a new license family, or the next version of an existing one. @@ -71,6 +75,15 @@ async def create( lifecycle_state: ``DRAFT`` to create the version off sale, to be put on sale with :meth:`publish`. ``PUBLISHED`` when omitted. A version is never created ``ARCHIVED``: it is archived with :meth:`archive` once it has been on sale. + pricing_type: How the version is sold: ``FREE`` or ``PAID`` can be bought self-serve, + ``CUSTOM`` sends a buyer to ``self_serve_cta_url`` or a conversation. ``CUSTOM`` + when omitted. + trial_period_days: The trial a subscription to the version starts with by default, + 1 to 365 days. + requires_payment_method: Whether self-serve signup captures a payment method before + activation. + self_serve_cta_url: Where a buyer is sent when the version cannot be bought + self-serve: an http(s) URL. Raises: NotFoundError: ``family_slug`` or ``family_id`` names no family @@ -93,6 +106,10 @@ async def create( "familySlug": family_slug, "familyId": family_id, "lifecycleState": lifecycle_state, + "pricingType": pricing_type, + "trialPeriodDays": trial_period_days, + "requiresPaymentMethod": requires_payment_method, + "selfServeCtaUrl": self_serve_cta_url, } ), } @@ -107,6 +124,10 @@ async def update( description: str = "", is_default: bool = False, version_name: str | None = None, + pricing_type: PricingType | None = None, + trial_period_days: int | None = None, + requires_payment_method: bool | None = None, + self_serve_cta_url: str | None = None, ) -> None: """Replace a license version's details. @@ -115,6 +136,11 @@ async def update( :meth:`publish`, :meth:`archive` and :meth:`unarchive`. ``is_default=True`` takes the flag from the family's previous default, and ``False`` unsets it on this version. + The commercial terms -- ``pricing_type``, ``trial_period_days``, + ``requires_payment_method`` and ``self_serve_cta_url`` -- keep their value when left + out, as :meth:`create` describes them. ``trial_period_days=0`` removes the trial, and + ``self_serve_cta_url=""`` the URL. + Raises: ConflictError: ``is_default=True`` on a version that is not ``PUBLISHED`` (``UpdateLicense.DefaultMustBePublished``): publish or unarchive it first. @@ -124,7 +150,15 @@ async def update( "description": description, "type": type, "isDefault": is_default, - **compact({"versionName": version_name}), + **compact( + { + "versionName": version_name, + "pricingType": pricing_type, + "trialPeriodDays": trial_period_days, + "requiresPaymentMethod": requires_payment_method, + "selfServeCtaUrl": self_serve_cta_url, + } + ), } await self._api.send_empty("PUT", api_path("licenses", license_slug), body=body) diff --git a/src/kaitencloud/_async/resources/metadata_fields.py b/src/kaitencloud/_async/resources/metadata_fields.py index bcf2a2b..1fb8f07 100644 --- a/src/kaitencloud/_async/resources/metadata_fields.py +++ b/src/kaitencloud/_async/resources/metadata_fields.py @@ -21,10 +21,17 @@ class AsyncMetadataFields(AsyncResource): against the active fields declared for its resource type. """ - async def list(self, resource_type: MetadataResourceType) -> builtins.list[MetadataField]: - """Return the metadata fields declared for ``resource_type``, walking all pages.""" + async def list( + self, resource_type: MetadataResourceType, *, include_archived: bool = False + ) -> builtins.list[MetadataField]: + """Return the metadata fields declared for ``resource_type``, walking all pages. + + Archived fields are left out unless ``include_archived`` asks for them. + """ return await self._api.list_all( - "/metadata-fields", MetadataField, params={"resourceType": resource_type} + "/metadata-fields", + MetadataField, + params={"resourceType": resource_type, "includeArchived": include_archived or None}, ) async def create( diff --git a/src/kaitencloud/_async/resources/platform/__init__.py b/src/kaitencloud/_async/resources/platform/__init__.py index 5b0ec25..a145a9e 100644 --- a/src/kaitencloud/_async/resources/platform/__init__.py +++ b/src/kaitencloud/_async/resources/platform/__init__.py @@ -1,12 +1,14 @@ # Copyright 2026 KAITEN INC # SPDX-License-Identifier: Apache-2.0 +from .billing import AsyncPlatformBilling from .connectors import AsyncPlatformConnectors from .organizations import AsyncPlatformOrganizations from .tokens import AsyncPlatformTokens from .users import AsyncPlatformUsers __all__ = [ + "AsyncPlatformBilling", "AsyncPlatformConnectors", "AsyncPlatformOrganizations", "AsyncPlatformTokens", diff --git a/src/kaitencloud/_async/resources/platform/billing.py b/src/kaitencloud/_async/resources/platform/billing.py new file mode 100644 index 0000000..8647f12 --- /dev/null +++ b/src/kaitencloud/_async/resources/platform/billing.py @@ -0,0 +1,39 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from ...._utils import api_path, compact, validate_uuid +from ....types import ClosePeriodsReport +from .._base import AsyncResource + +__all__ = ["AsyncPlatformBilling"] + + +class AsyncPlatformBilling(AsyncResource): + """Billing across organizations.""" + + async def close_periods( + self, organization_id: str, *, instance_slug: str | None = None + ) -> ClosePeriodsReport: + """Close an organization's subscriptions whose period has ended, now. + + The same as :meth:`AsyncBilling.close_periods` on the Core API, for any organization, + and recorded under ``system:kaiten``. It needs ``write:billing``. + + Args: + organization_id: The organization. + instance_slug: Close only this instance's subscription. + """ + return await self._api.send( + "POST", + api_path( + "platform", + "organizations", + validate_uuid(organization_id, "organization_id"), + "billing", + "close-periods", + ), + ClosePeriodsReport, + body=compact({"instanceSlug": instance_slug}), + ) diff --git a/src/kaitencloud/_constants.py b/src/kaitencloud/_constants.py index 7acd6a3..d3cd1ee 100644 --- a/src/kaitencloud/_constants.py +++ b/src/kaitencloud/_constants.py @@ -22,6 +22,12 @@ Walking at this size is the fewest round trips a complete list can take. """ +MAX_REPORT_PAGE_SIZE: Final = 500 +"""The largest page the usage report histories accept, which page by ``afterSeq``.""" + +EXPORT_MEDIA_TYPES: Final = {"csv": "text/csv", "json": "application/x-ndjson"} +"""What an export answers in each format it takes: CSV, or NDJSON for ``json``.""" + INITIAL_RETRY_DELAY: Final = 0.25 MAX_RETRY_DELAY: Final = 8.0 MAX_RETRY_AFTER: Final = 60.0 diff --git a/src/kaitencloud/_exceptions.py b/src/kaitencloud/_exceptions.py index 23c680e..bcbce46 100644 --- a/src/kaitencloud/_exceptions.py +++ b/src/kaitencloud/_exceptions.py @@ -30,6 +30,7 @@ "RateLimitError", "ServiceUnavailableError", "ThresholdExceededError", + "TransactionIdReusedError", "UnprocessableEntityError", "WebhookPayloadError", "WebhookVerificationError", @@ -44,8 +45,10 @@ class CredentialError(KaitenError, ValueError): """A credential is missing, or belongs to a class the target API refuses. Raised before anything is sent: a platform credential (``ksm_...``) handed to the Core - client, an organization token handed to the platform client, or a webhook signing secret - (``whsec_...``) handed to either. + client, an organization token handed to the platform client, a webhook signing secret + (``whsec_...``) handed to either, or a route under ``/public`` called without the + credential it takes -- a publishable key (``pk_...``), a customer session (``kst_...``) or a + checkout session's token (``ksc_...``). """ @@ -162,6 +165,15 @@ class ThresholdExceededError(ConflictError): """ +class TransactionIdReusedError(ConflictError): + """A usage report reused a ``transaction_id`` already spent on a different report. + + The same key with the same behavior and value is a replay, applied once; with another + behavior or value, it is this error. Nothing was recorded, and sending it again will not + change the answer: give the report a key of its own. + """ + + class UnprocessableEntityError(APIStatusError): """The API answered 422: the request is well-formed but its content is refused.""" diff --git a/src/kaitencloud/_models.py b/src/kaitencloud/_models.py index 1f1a869..e5654a0 100644 --- a/src/kaitencloud/_models.py +++ b/src/kaitencloud/_models.py @@ -1,15 +1,19 @@ # Copyright 2026 KAITEN INC # SPDX-License-Identifier: Apache-2.0 -"""The base class of every model the SDK returns.""" +"""The base class of every model the SDK returns, and the results it composes from them.""" from __future__ import annotations -from typing import Any +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any from pydantic import BaseModel, ConfigDict -__all__ = ["KaitenModel"] +if TYPE_CHECKING: + from .types import ReportedEntitlementUsage + +__all__ = ["KaitenModel", "UsageReportResult"] class KaitenModel(BaseModel): @@ -37,3 +41,20 @@ def to_dict(self) -> dict[str, Any]: def to_json(self, *, indent: int | None = None) -> str: """Return the model as a JSON document, with wire key names.""" return self.model_dump_json(by_alias=True, exclude_unset=True, indent=indent) + + +@dataclass(frozen=True) +class UsageReportResult: + """What a usage report answered: the usage it results in, and what the API said about it.""" + + usage: ReportedEntitlementUsage + """The entitlement's usage after the report. On a replay, the earlier report's answer: it + may describe a usage window that has closed since, so do not read it as the current gauge. + """ + + replayed: bool + """The report repeated one already accepted under its ``transaction_id``, and nothing was + counted again.""" + + metadata_dropped: bool + """The report's metadata was above 4 KiB and was not stored. The report itself counted.""" diff --git a/src/kaitencloud/_sync/_http.py b/src/kaitencloud/_sync/_http.py index 7c44ffc..77b77d2 100644 --- a/src/kaitencloud/_sync/_http.py +++ b/src/kaitencloud/_sync/_http.py @@ -14,7 +14,7 @@ import httpx from pydantic import BaseModel, ValidationError -from .._constants import DEFAULT_TIMEOUT, MAX_PAGE_SIZE +from .._constants import DEFAULT_TIMEOUT, MAX_PAGE_SIZE, MAX_REPORT_PAGE_SIZE from .._exceptions import ( APIConnectionError, APIResponseValidationError, @@ -24,7 +24,15 @@ make_status_error, ) from .._types import NOT_GIVEN, Credential, NotGiven, TimeoutTypes -from .._utils import USER_AGENT, Surface, check_token, resolve_token, retry_delay, sleep +from .._utils import ( + PUBLISHABLE_KEY_PREFIX, + USER_AGENT, + Surface, + check_token, + resolve_token, + retry_delay, + sleep, +) __all__ = ["APIClient"] @@ -98,6 +106,7 @@ def request( *, params: Mapping[str, Any] | None = None, body: Any = None, + headers: Mapping[str, str] | None = None, idempotent: bool | None = None, errors: Mapping[int, type[APIStatusError]] | None = None, ) -> httpx.Response: @@ -115,7 +124,11 @@ def request( while True: request = self._build( - method, url, params=_query(params), body=body, headers=self._headers() + method, + url, + params=_query(params), + body=body, + headers={**self._headers(path), **(headers or {})}, ) try: response = self.http_client.send(request) @@ -252,6 +265,43 @@ def list_all( rows.append(row) return rows + def list_reports( + self, + path: str, + model: type[ModelT], + *, + params: Mapping[str, Any] | None = None, + limit: int | None = None, + ) -> list[ModelT]: + """Every row of a usage report history, which pages by ``afterSeq`` rather than a cursor. + + ``limit`` is a ceiling, as for :meth:`paginate`. A ``nextAfterSeq`` that does not move + past the page it follows raises :class:`PaginationError`. + """ + rows: list[ModelT] = [] + after: int | None = None + while True: + size = MAX_REPORT_PAGE_SIZE + if limit is not None: + size = min(size, limit - len(rows)) + response = self.request( + "GET", path, params={**(params or {}), "afterSeq": after, "limit": size} + ) + page = self.decode(response) + if not isinstance(page, dict) or not isinstance(page.get("items"), list): + raise APIResponseValidationError( + f"GET {path}: expected a page of items", response=response + ) + rows += [self.validate(model, item, response) for item in page["items"]] + following = page.get("nextAfterSeq") + if following is None or (limit is not None and len(rows) >= limit): + return rows[:limit] + if not isinstance(following, int) or (after is not None and following <= after): + raise PaginationError( + f"GET {path}: the API reported more rows but no afterSeq past {after}" + ) + after = following + @staticmethod def decode(response: httpx.Response) -> Any: if not response.content: @@ -293,14 +343,15 @@ def _build( method, url, params=params, json=body, headers=headers, timeout=self.timeout ) - def _headers(self) -> dict[str, str]: - token = check_token(resolve_token(self.credential), self.surface) - return { - "Accept": "application/json", - "User-Agent": USER_AGENT, - **self.default_headers, - "Authorization": f"Bearer {token}", - } + def _headers(self, path: str) -> dict[str, str]: + token = check_token(resolve_token(self.credential), self.surface, path) + headers = {"Accept": "application/json", "User-Agent": USER_AGENT, **self.default_headers} + if token.startswith(PUBLISHABLE_KEY_PREFIX): + # Not a bearer credential: the API refuses a publishable key sent as Authorization. + headers["X-Kaiten-Publishable-Key"] = token + else: + headers["Authorization"] = f"Bearer {token}" + return headers def _back_off( self, request: httpx.Request, attempt: int, reason: str, retry_after: str | None = None diff --git a/src/kaitencloud/_sync/client.py b/src/kaitencloud/_sync/client.py index 7699942..5a5c9ab 100644 --- a/src/kaitencloud/_sync/client.py +++ b/src/kaitencloud/_sync/client.py @@ -28,6 +28,7 @@ from ..types import PlatformCredential from ._http import APIClient from .resources import ( + Billing, Components, Connectors, Customers, @@ -44,6 +45,7 @@ ServiceAccounts, ) from .resources.platform import ( + PlatformBilling, PlatformConnectors, PlatformOrganizations, PlatformTokens, @@ -115,7 +117,7 @@ class KaitenClient(_BaseClient): The Core API is what an organization's own credential talks to: an organization token (``ksh_...``) or a JWT from your identity provider. It covers customers, instances, - licenses, entitlements and usage, feature flags, releases and connectors:: + licenses, entitlements and usage, billing, feature flags, releases and connectors:: with KaitenClient( base_url="https://kaiten.example.com", token="ksh_..." @@ -138,6 +140,9 @@ class KaitenClient(_BaseClient): TLS or transports. The Kaiten client never closes a client it did not create. """ + billing: Billing + """Billing: prices, subscriptions, invoices, add-ons, vouchers and the public catalog.""" + components: Components """Components: the versioned building blocks releases are made of.""" @@ -212,6 +217,7 @@ def __init__( def _bind(self) -> None: api = self._api + self.billing = Billing(api) self.components = Components(api) self.connectors = Connectors(api) self.customers = Customers(api) @@ -253,6 +259,9 @@ class KaitenPlatformClient(_BaseClient): http_client: An :class:`httpx.Client` to send requests with. """ + billing: PlatformBilling + """Billing across organizations: closing their due periods.""" + connectors: PlatformConnectors """The deployment-wide connector registry.""" @@ -308,6 +317,7 @@ def me(self) -> PlatformCredential: def _bind(self) -> None: api = self._api + self.billing = PlatformBilling(api) self.connectors = PlatformConnectors(api) self.organizations = PlatformOrganizations(api) self.tokens = PlatformTokens(api) diff --git a/src/kaitencloud/_sync/resources/__init__.py b/src/kaitencloud/_sync/resources/__init__.py index ddad12b..38f1f7a 100644 --- a/src/kaitencloud/_sync/resources/__init__.py +++ b/src/kaitencloud/_sync/resources/__init__.py @@ -3,6 +3,7 @@ # Generated by scripts/unasync.py from src/kaitencloud/_async/resources/__init__.py. Do not edit by hand: # change the async source, then run `task generate`. +from .billing import Billing from .components import Components from .connectors import Connectors from .customers import Customers @@ -19,6 +20,7 @@ from .service_accounts import ServiceAccounts __all__ = [ + "Billing", "Components", "Connectors", "Customers", diff --git a/src/kaitencloud/_sync/resources/billing/__init__.py b/src/kaitencloud/_sync/resources/billing/__init__.py new file mode 100644 index 0000000..02ce218 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/__init__.py @@ -0,0 +1,218 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/__init__.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +from ...._utils import compact +from ....types import ( + BillingCapabilities, + BillingHealth, + BillingSettings, + ClosePeriodsReport, + CollectionMethod, + SyncReport, +) +from ..._http import APIClient +from .._base import Resource +from .addons import BillingAddons +from .customers import BillingCustomers +from .invoices import BillingHandoff, BillingInvoices +from .license_prices import BillingLicensePrices +from .public import BillingPublic, BillingSession +from .sessions import ( + BillingCheckoutSessions, + BillingCustomerSessions, + BillingPublishableKeys, +) +from .subscriptions import ( + BillingInstanceAddons, + BillingInstanceVouchers, + BillingSubscriptions, +) +from .vouchers import BillingVouchers + +__all__ = [ + "Billing", + "BillingAddons", + "BillingCheckoutSessions", + "BillingCustomerSessions", + "BillingCustomers", + "BillingHandoff", + "BillingInstanceAddons", + "BillingInstanceVouchers", + "BillingInvoices", + "BillingLicensePrices", + "BillingPublic", + "BillingPublishableKeys", + "BillingSession", + "BillingSubscriptions", + "BillingVouchers", +] + + +class Billing(Resource): + """Billing: the organization's settings and payment providers, and a namespace per resource. + + Kaiten composes every invoice from the prices of a license version, its add-ons and the + usage it metered. Each subscription names who issues and collects them: ``NOOP``, the + organization itself through :attr:`handoff`, or a payment provider. Most operations here + require billing to be enabled for the organization; :meth:`get_capabilities` says whether + it is. + """ + + __slots__ = ( + "addons", + "checkout_sessions", + "customer_sessions", + "customers", + "handoff", + "instance_addons", + "instance_vouchers", + "invoices", + "license_prices", + "public", + "publishable_keys", + "session", + "subscriptions", + "vouchers", + ) + + addons: BillingAddons + """The add-on catalog: families, versions, grants, prices and the license families they fit.""" + + checkout_sessions: BillingCheckoutSessions + """Anonymous checkouts, as the organization reads them.""" + + customer_sessions: BillingCustomerSessions + """Customer sessions (``kst_...``), for a customer's browser to act on its own billing.""" + + customers: BillingCustomers + """A customer's billing: its payment method, and the payment provider's portal.""" + + handoff: BillingHandoff + """The accounting handoff: invoices claimed in leased batches and acknowledged one by one.""" + + instance_addons: BillingInstanceAddons + """The add-ons an instance holds.""" + + instance_vouchers: BillingInstanceVouchers + """The vouchers an instance redeemed.""" + + invoices: BillingInvoices + """The organization's invoices.""" + + license_prices: BillingLicensePrices + """The prices of license versions, and the invoice a subscription to one would get.""" + + public: BillingPublic + """What a publishable key (``pk_...``) reaches. Build the client with that key.""" + + publishable_keys: BillingPublishableKeys + """Publishable keys (``pk_...``), which a web page reads the public catalog with.""" + + session: BillingSession + """What a customer session (``kst_...``) reaches. Build the client with that session.""" + + subscriptions: BillingSubscriptions + """Instances' subscriptions.""" + + vouchers: BillingVouchers + """The organization's vouchers.""" + + def __init__(self, api: APIClient) -> None: + super().__init__(api) + self.addons = BillingAddons(api) + self.checkout_sessions = BillingCheckoutSessions(api) + self.customer_sessions = BillingCustomerSessions(api) + self.customers = BillingCustomers(api) + self.handoff = BillingHandoff(api) + self.instance_addons = BillingInstanceAddons(api) + self.instance_vouchers = BillingInstanceVouchers(api) + self.invoices = BillingInvoices(api) + self.license_prices = BillingLicensePrices(api) + self.public = BillingPublic(api) + self.publishable_keys = BillingPublishableKeys(api) + self.session = BillingSession(api) + self.subscriptions = BillingSubscriptions(api) + self.vouchers = BillingVouchers(api) + + def get_settings(self) -> BillingSettings: + """Return the organization's billing defaults. + + They are the collection method and payment terms a subscription gets when it names none + of its own. An organization that never wrote them has ``SEND_INVOICE`` and 30 days. + """ + return self._api.get("/billing/settings", BillingSettings) + + def update_settings( + self, + *, + default_collection_method: CollectionMethod, + default_days_until_due: int, + handoff_stripe_invoices: bool, + ) -> BillingSettings: + """Replace the organization's billing defaults, and return them. + + A subscription that names its own terms keeps them; the others take these at their + next invoice. + + Args: + default_collection_method: ``SEND_INVOICE`` sends an invoice for payment; + ``CHARGE_AUTOMATICALLY`` charges the customer's payment method, on a provider + that can. + default_days_until_due: Days an invoice is due after it is issued, 0 to 365. + handoff_stripe_invoices: Whether invoices a payment provider issues also enter the + handoff queue, for an accounting system that wants every invoice. + """ + body = { + "defaultCollectionMethod": default_collection_method, + "defaultDaysUntilDue": default_days_until_due, + "handoffStripeInvoices": handoff_stripe_invoices, + } + return self._api.send("PUT", "/billing/settings", BillingSettings, body=body) + + def get_capabilities(self) -> BillingCapabilities: + """Return whether billing is enabled -- and if not, why -- and what it can do here. + + Who can collect the organization's invoices, and which parts of billing this release + ships, so a client shows only what works. It answers even when billing is disabled. + """ + return self._api.get("/billing/capabilities", BillingCapabilities) + + def get_health(self) -> BillingHealth: + """Return what the organization's billing needs attention for, computed when read. + + Periods waiting for their close, held invoices, invoices a payment provider keeps + refusing, reconciliation mismatches, each provider's sync, overdue invoices and the + handoff backlog. + """ + return self._api.get("/billing/health", BillingHealth) + + def sync(self) -> SyncReport: + """Run one sync pass now with each payment provider the organization connected. + + What was paid, voided or finalized in a provider is mirrored, as the periodic pass does. + + Raises: + ConflictError: No provider is connected (``SyncProvider.NotConnected``). + """ + return self._api.send("POST", "/billing/sync", SyncReport) + + def close_periods(self, *, instance_slug: str | None = None) -> ClosePeriodsReport: + """Close the subscriptions whose period has ended, now rather than at the next pass. + + Each one's ``RENEWAL`` invoice is composed and issued -- or held, when its usage journal + fails a check -- and its period advances. Only what is due closes, at most a batch per + call: ``has_more`` says when more is left. + + Args: + instance_slug: Close only this instance's subscription. + """ + return self._api.send( + "POST", + "/billing/close-periods", + ClosePeriodsReport, + body=compact({"instanceSlug": instance_slug}), + ) diff --git a/src/kaitencloud/_sync/resources/billing/addons.py b/src/kaitencloud/_sync/resources/billing/addons.py new file mode 100644 index 0000000..37b5958 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/addons.py @@ -0,0 +1,347 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/addons.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from typing import Any, Literal + +from ...._types import EntitlementValueInput +from ...._utils import api_path, compact, entitlement_value, price_body +from ....types import ( + Addon, + AddonEntitlement, + AddonFamily, + AddonLifecycleState, + BillingModel, + BillingPeriod, + BillingTiming, + CompatibleLicenseFamilies, + IncreaseBilling, + OverrideBehavior, + Price, + PriceStatus, + PricingType, +) +from .._base import Resource + +__all__ = ["BillingAddons"] + + +class BillingAddons(Resource): + """The add-on catalog: what an instance can hold on top of its license, by the unit. + + Add-ons are versioned like licenses: an add-on *family* is the product, each version has + its grants and its prices, and moves through ``DRAFT``, ``PUBLISHED`` and ``ARCHIVED``. A + version fits the license families :meth:`set_compatibility` names, and none until then. + """ + + def list( + self, + *, + lifecycle_state: AddonLifecycleState | None = None, + family_slug: str | None = None, + ) -> builtins.list[Addon]: + """Return the organization's add-on versions, newest first, walking all pages. + + Args: + lifecycle_state: Only versions in this state. + family_slug: Only versions of this family. + """ + return self._api.list_all( + "/addons", + Addon, + params={"lifecycleState": lifecycle_state, "familySlug": family_slug}, + ) + + def get(self, addon_slug: str) -> Addon: + """Return the add-on version identified by ``addon_slug``.""" + return self._api.get(api_path("addons", addon_slug), Addon) + + def create( + self, + *, + name: str, + pricing_type: PricingType, + description: str = "", + slug: str | None = None, + family_slug: str | None = None, + family_id: str | None = None, + version_name: str | None = None, + lifecycle_state: Literal["DRAFT", "PUBLISHED"] | None = None, + is_default: bool | None = None, + max_quantity: int | None = None, + increase_billing: IncreaseBilling | None = None, + ) -> Addon: + """Create an add-on version: the first of a new family, or the next of an existing one. + + Args: + name: The add-on's name, e.g. ``"Extra seats"``. + pricing_type: ``FREE``, ``PAID`` or ``CUSTOM``. + description: A description. + slug: The version's slug. Defaults to ``-v`` for a new version of a + family, and to one generated from the name for a new family. + family_slug: Make this a new version of this family. + family_id: The same family, by identifier. + version_name: A label for the version. Defaults to ``"Version - "``. + lifecycle_state: ``DRAFT``, or ``PUBLISHED`` when omitted. + is_default: Make this version its family's default. + max_quantity: The largest quantity an instance can hold. + increase_billing: Read only when this opens a new family: how a mid-period increase + of its add-ons billed in advance is billed, ``TRUE_UP`` (the default) or + ``NEXT_BOUNDARY``. + """ + body = { + "name": name, + "description": description, + "pricingType": pricing_type, + **compact( + { + "slug": slug, + "familySlug": family_slug, + "familyId": family_id, + "versionName": version_name, + "lifecycleState": lifecycle_state, + "isDefault": is_default, + "maxQuantity": max_quantity, + "increaseBilling": increase_billing, + } + ), + } + return self._api.send("POST", "/addons", Addon, body=body) + + def update( + self, + addon_slug: str, + *, + name: str, + description: str = "", + is_default: bool = False, + version_name: str | None = None, + max_quantity: int | None = None, + ) -> Addon: + """Replace an add-on version's name, description, version name, default flag and maximum. + + A full replacement, as for a license version: its family, pricing type and state are + not part of it. + """ + body = { + "name": name, + "description": description, + "isDefault": is_default, + **compact({"versionName": version_name, "maxQuantity": max_quantity}), + } + return self._api.send("PUT", api_path("addons", addon_slug), Addon, body=body) + + def delete(self, addon_slug: str) -> None: + """Delete an add-on version that was never attached and grants nothing. + + Its family goes with its last version. + + Raises: + ConflictError: The version is in use (``DeleteAddon.InUseConflict``). + """ + self._api.send_empty("DELETE", api_path("addons", addon_slug)) + + def publish(self, addon_slug: str) -> Addon: + """Put a ``DRAFT`` version on sale, and return it ``PUBLISHED``.""" + return self._api.send("POST", api_path("addons", addon_slug, "publish"), Addon) + + def archive(self, addon_slug: str) -> Addon: + """Withdraw a ``PUBLISHED`` version from sale: instances holding it keep it.""" + return self._api.send("POST", api_path("addons", addon_slug, "archive"), Addon) + + def unarchive(self, addon_slug: str) -> Addon: + """Put an ``ARCHIVED`` version back on sale, and return it ``PUBLISHED``.""" + return self._api.send("POST", api_path("addons", addon_slug, "unarchive"), Addon) + + def list_compatibility(self, addon_slug: str) -> CompatibleLicenseFamilies: + """Return the license families an instance must be on to attach this version. + + None means it cannot be attached at all. + """ + return self._api.get( + api_path("addons", addon_slug, "compatible-license-families"), + CompatibleLicenseFamilies, + ) + + def set_compatibility(self, addon_slug: str, family_slug: str) -> None: + """Let instances on any version of the license family attach this add-on version.""" + self._api.send_empty( + "PUT", api_path("addons", addon_slug, "compatible-license-families", family_slug) + ) + + def remove_compatibility(self, addon_slug: str, family_slug: str) -> None: + """Stop the license family attaching this version; instances holding it keep it.""" + self._api.send_empty( + "DELETE", api_path("addons", addon_slug, "compatible-license-families", family_slug) + ) + + def list_entitlements(self, addon_slug: str) -> builtins.list[AddonEntitlement]: + """Return what the add-on version grants per unit of quantity.""" + return self._api.get_list(api_path("addons", addon_slug, "entitlements"), AddonEntitlement) + + def get_entitlement(self, addon_slug: str, entitlement_slug: str) -> AddonEntitlement: + """Return one grant of the add-on version.""" + return self._api.get( + api_path("addons", addon_slug, "entitlements", entitlement_slug), AddonEntitlement + ) + + def assign_entitlement( + self, + addon_slug: str, + entitlement_slug: str, + value: EntitlementValueInput, + *, + override_behavior: OverrideBehavior | None = None, + overage_percent: int | None = None, + ) -> AddonEntitlement: + """Grant an entitlement per unit of the add-on version, and return the grant. + + Args: + addon_slug: The add-on version that grants. + entitlement_slug: The entitlement granted. + value: What one unit grants, as for a license grant: a ``bool``, a number, or a + mapping for a CONFIG entitlement. A number counts once per unit held. + override_behavior: How a number grant combines with the license's: ``ADD``, + ``OVERRIDE`` or ``MAX``. + overage_percent: The overage the add-on allows: ``-1`` with an unlimited value, + ``0`` or more otherwise. Omitted, it inherits the license grant's. + + Raises: + ConflictError: An instance with a live subscription holds the version + (``AssignAddonEntitlement.BillingActive``). + """ + body = { + "entitlementSlug": entitlement_slug, + **_grant(value, override_behavior, overage_percent), + } + return self._api.send( + "POST", api_path("addons", addon_slug, "entitlements"), AddonEntitlement, body=body + ) + + def update_entitlement( + self, + addon_slug: str, + entitlement_slug: str, + value: EntitlementValueInput, + *, + override_behavior: OverrideBehavior | None = None, + overage_percent: int | None = None, + ) -> AddonEntitlement: + """Replace a grant's value, override behavior and overage: see :meth:`assign_entitlement`.""" + return self._api.send( + "PUT", + api_path("addons", addon_slug, "entitlements", entitlement_slug), + AddonEntitlement, + body=_grant(value, override_behavior, overage_percent), + ) + + def unassign_entitlement(self, addon_slug: str, entitlement_slug: str) -> None: + """Remove a grant from the add-on version. + + Raises: + ConflictError: An instance with a live subscription holds the version, or an + ``ACTIVE`` price of the version meters the entitlement. + """ + self._api.send_empty( + "DELETE", api_path("addons", addon_slug, "entitlements", entitlement_slug) + ) + + def list_prices( + self, addon_slug: str, *, status: PriceStatus | None = None + ) -> builtins.list[Price]: + """Return the prices of the add-on version, in display order; both statuses when omitted.""" + return self._api.get_list( + api_path("addons", addon_slug, "prices"), Price, params={"status": status} + ) + + def create_price( + self, + addon_slug: str, + *, + billing_model: BillingModel, + currency: str, + unit_amount_decimal: str, + billing_period: BillingPeriod | None = None, + billing_timing: BillingTiming | None = None, + metered_entitlement_slug: str | None = None, + is_default: bool | None = None, + display_label: str | None = None, + display_order: int | None = None, + ) -> Price: + """Add a price to the add-on version, under the rules of a license price. + + A price never changes once created: create another and deprecate this one. See + :meth:`BillingLicensePrices.create` for the arguments. + """ + body = price_body( + billing_model=billing_model, + currency=currency, + unit_amount_decimal=unit_amount_decimal, + billing_period=billing_period, + billing_timing=billing_timing, + metered_entitlement_slug=metered_entitlement_slug, + is_default=is_default, + display_label=display_label, + display_order=display_order, + ) + return self._api.send("POST", api_path("addons", addon_slug, "prices"), Price, body=body) + + def deprecate_price(self, addon_slug: str, price_id: str) -> Price: + """Stop offering a price of the add-on version. + + Raises: + ConflictError: It is a default price (``DeprecateAddonPrice.IsDefault``), which bills + every instance holding the version: retire it through a new version. + """ + return self._api.send( + "POST", api_path("addons", addon_slug, "prices", price_id, "deprecate"), Price + ) + + def list_families(self) -> builtins.list[AddonFamily]: + """Return every add-on family with its versions, newest family first.""" + return self._api.get_list("/addon-families", AddonFamily) + + def get_family(self, family_slug: str) -> AddonFamily: + """Return an add-on family with its versions and its current version.""" + return self._api.get(api_path("addon-families", family_slug), AddonFamily) + + def update_family( + self, + family_slug: str, + *, + is_public: bool | None = None, + increase_billing: IncreaseBilling | None = None, + ) -> AddonFamily: + """Change an add-on family's own settings; what is omitted keeps its value. + + Args: + family_slug: The family. + is_public: List the family's default ``PUBLISHED`` version, with its ``ACTIVE`` + prices, in the public catalog. Families are private until made public. + increase_billing: How a mid-period increase of its add-ons billed in advance is + billed, from the next composition: ``TRUE_UP`` or ``NEXT_BOUNDARY``. + """ + body = compact({"isPublic": is_public, "increaseBilling": increase_billing}) + return self._api.send( + "PATCH", api_path("addon-families", family_slug), AddonFamily, body=body + ) + + +def _grant( + value: EntitlementValueInput, + override_behavior: OverrideBehavior | None, + overage_percent: int | None, +) -> dict[str, Any]: + return { + "value": entitlement_value(value), + **compact( + { + "overrideBehavior": override_behavior, + "limitCapExceededOveragePercent": overage_percent, + } + ), + } diff --git a/src/kaitencloud/_sync/resources/billing/customers.py b/src/kaitencloud/_sync/resources/billing/customers.py new file mode 100644 index 0000000..958e4d9 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/customers.py @@ -0,0 +1,104 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/customers.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +from ...._utils import api_path, compact +from ....types import ( + CompletedPaymentMethodSession, + CustomerBilling, + PaymentMethodSession, + PortalSession, +) +from .._base import Resource + +__all__ = ["BillingCustomers"] + + +class BillingCustomers(Resource): + """A customer's billing: its payment method, and its payment provider's portal. + + The pages it opens are the provider's own: Kaiten never sees card data. + """ + + def get(self, customer_slug: str) -> CustomerBilling: + """Return the customer's billing e-mail, and its side in each payment provider. + + Its id there and the labels of its default payment method -- never card data. + """ + return self._api.get(api_path("customers", customer_slug, "billing"), CustomerBilling) + + def detach_payment_method(self, customer_slug: str) -> None: + """Remove the customer's payment method from its payment provider. + + Raises: + ConflictError: A live subscription of the customer is charged automatically + (``.InUseByAutomaticCollection``), or there is no payment method. + """ + self._api.send_empty( + "DELETE", api_path("customers", customer_slug, "billing", "payment-method") + ) + + def create_payment_method_session( + self, customer_slug: str, *, return_url: str, currency: str | None = None + ) -> PaymentMethodSession: + """Open the payment provider's page where the customer saves a payment method. + + Send the customer to its ``url``. When it comes back to ``return_url``, with + ``kaiten_setup_session`` in the query, call :meth:`complete_payment_method_session`. + + Args: + customer_slug: The customer. + return_url: Where the customer comes back to: https, or http on localhost. + currency: The currency the payment method is set up in; required when the customer + has no live subscription, whose currency is used otherwise. + """ + return self._api.send( + "POST", + api_path("customers", customer_slug, "billing", "payment-method-session"), + PaymentMethodSession, + body={"returnUrl": return_url, **compact({"currency": currency})}, + ) + + def complete_payment_method_session( + self, customer_slug: str, session_id: str + ) -> CompletedPaymentMethodSession: + """Check a setup session with the provider, and charge the customer's invoices to it. + + The redirect is never trusted: the provider is asked. Idempotent. + + Raises: + NotFoundError: The session is unknown, or another customer's. + ConflictError: The customer did not complete it (``.SessionNotComplete``). + """ + return self._api.send( + "POST", + api_path( + "customers", + customer_slug, + "billing", + "payment-method-session", + session_id, + "complete", + ), + CompletedPaymentMethodSession, + idempotent=True, + ) + + def create_portal_session(self, customer_slug: str, *, return_url: str) -> PortalSession: + """Open the payment provider's portal, where the customer manages its payment methods. + + What it changes there reaches Kaiten through the provider sync. + + Args: + customer_slug: The customer. + return_url: Where the portal sends the customer back: https, or http on localhost. + """ + return self._api.send( + "POST", + api_path("customers", customer_slug, "billing", "portal-session"), + PortalSession, + body={"returnUrl": return_url}, + ) diff --git a/src/kaitencloud/_sync/resources/billing/invoices.py b/src/kaitencloud/_sync/resources/billing/invoices.py new file mode 100644 index 0000000..dea6a88 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/invoices.py @@ -0,0 +1,379 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/invoices.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from collections.abc import Sequence +from datetime import datetime +from typing import Any, Literal + +from ...._constants import EXPORT_MEDIA_TYPES +from ...._utils import api_path, check_limit, compact, optional_datetime +from ....types import ( + BillingProviderKind, + HandoffClaim, + HandoffStatus, + Invoice, + InvoiceKind, + InvoiceStatus, + InvoiceSummary, + QueuedInvoice, + UsageReport, +) +from .._base import Resource + +__all__ = ["BillingHandoff", "BillingInvoices"] + + +class BillingInvoices(Resource): + """The organization's invoices, addressed by id: an invoice outlives its instance and customer. + + Amounts are integer minor units, as strings. An invoice is a ``DRAFT`` until it is issued: + ``MANUAL`` when the organization collects it, ``PUSHED`` to a payment provider otherwise. + """ + + def list( + self, + *, + instance_slug: str | None = None, + customer_slug: str | None = None, + status: Sequence[InvoiceStatus] | None = None, + kind: InvoiceKind | None = None, + provider_kind: BillingProviderKind | None = None, + overdue: bool | None = None, + held: bool | None = None, + handoff_status: HandoffStatus | None = None, + issued_from: datetime | str | None = None, + issued_to: datetime | str | None = None, + boundary_from: datetime | str | None = None, + boundary_to: datetime | str | None = None, + updated_since: datetime | str | None = None, + limit: int | None = None, + ) -> builtins.list[InvoiceSummary]: + """Return the organization's invoices, newest first. + + With ``updated_since``, the invoices changed since then, oldest change first: store the + last ``updated_at`` you read and pass it back for an incremental sync. + + Args: + instance_slug: Only this instance's invoices. + customer_slug: Only this customer's invoices. + status: Only invoices in these statuses. + kind: ``ACTIVATION``, ``RENEWAL`` or ``FINAL``. + provider_kind: Only invoices issued by ``NOOP`` or by a payment provider. + overdue: Only issued invoices still unpaid past their due date. + held: Only drafts held for their usage journal. + handoff_status: ``NOT_REQUIRED``, ``PENDING`` or ``ACKNOWLEDGED``. + issued_from: Issued at or after this moment. + issued_to: Issued before this moment. + boundary_from: Billing a boundary at or after this moment. + boundary_to: Billing a boundary before this moment. + updated_since: Only invoices changed at or after this moment. + limit: The most invoices to return. Every page is walked when omitted. + """ + return self._api.list_all( + "/invoices", + InvoiceSummary, + params={ + "instanceSlug": instance_slug, + **invoice_query( + customer_slug=customer_slug, + status=status, + kind=kind, + provider_kind=provider_kind, + overdue=overdue, + held=held, + handoff_status=handoff_status, + issued_from=issued_from, + issued_to=issued_to, + boundary_from=boundary_from, + boundary_to=boundary_to, + updated_since=updated_since, + ), + }, + limit=check_limit(limit), + ) + + def get(self, invoice_id: str) -> Invoice: + """Return an invoice with its lines. + + It carries its hold and its handoff, and its customer and instance as they were when it + was composed. + """ + return self._api.get(api_path("invoices", invoice_id), Invoice) + + def export( + self, + *, + format: Literal["csv", "json"] = "csv", + granularity: Literal["line", "invoice"] | None = None, + instance_slug: str | None = None, + customer_slug: str | None = None, + status: Sequence[InvoiceStatus] | None = None, + kind: InvoiceKind | None = None, + provider_kind: BillingProviderKind | None = None, + overdue: bool | None = None, + held: bool | None = None, + handoff_status: HandoffStatus | None = None, + issued_from: datetime | str | None = None, + issued_to: datetime | str | None = None, + boundary_from: datetime | str | None = None, + boundary_to: datetime | str | None = None, + updated_since: datetime | str | None = None, + ) -> bytes: + """Return the invoices the filters of :meth:`list` select, exported. + + Amounts are integer minor units with the currency's exponent beside them. The export + carries billing e-mails: treat it as the accounting read it is. + + Args: + format: ``"csv"`` (the default), RFC 4180 with a header row, or ``"json"`` for + NDJSON, one invoice with its lines per line. + granularity: For CSV, ``"line"`` (the default) -- one row per invoice line -- or + ``"invoice"``, one row per invoice. + instance_slug: Only this instance's invoices. The other filters are + :meth:`list`'s. + """ + response = self._api.request( + "GET", + "/invoices/export", + params={ + "format": format, + "granularity": granularity, + "instanceSlug": instance_slug, + **invoice_query( + customer_slug=customer_slug, + status=status, + kind=kind, + provider_kind=provider_kind, + overdue=overdue, + held=held, + handoff_status=handoff_status, + issued_from=issued_from, + issued_to=issued_to, + boundary_from=boundary_from, + boundary_to=boundary_to, + updated_since=updated_since, + ), + }, + headers={"Accept": EXPORT_MEDIA_TYPES[format]}, + ) + return response.content + + def void(self, invoice_id: str, *, reason: str) -> Invoice: + """Void an invoice not yet settled, which frees its boundary for a recompose. + + A payment provider's invoice is voided there first, and here only once it confirms. + Voiding a ``VOID`` invoice again answers it unchanged. + + Args: + invoice_id: The invoice. + reason: Why, 1 to 500 characters. + """ + return self._api.send( + "POST", + api_path("invoices", invoice_id, "void"), + Invoice, + body={"reason": reason}, + idempotent=True, + ) + + def recompose(self, invoice_id: str) -> Invoice: + """Compose an invoice again from the usage journal as it is now, keeping its base lines. + + A held draft is rewritten in place; a ``VOID`` invoice gets a replacement for the same + boundary, which is returned. Any other invoice must be voided first. + """ + return self._api.send("POST", api_path("invoices", invoice_id, "recompose"), Invoice) + + def mark_paid( + self, + invoice_id: str, + *, + paid_at: datetime | str | None = None, + external_reference: str | None = None, + note: str | None = None, + ) -> Invoice: + """Record that a ``MANUAL`` invoice was paid. + + An invoice still waiting in the handoff queue is acknowledged by the same write. An + invoice a payment provider collects is settled there instead. + + Args: + invoice_id: The invoice. + paid_at: When it was paid, now or earlier. Now when omitted. + external_reference: Its number in the accounting system, 1 to 255 characters. + note: A note, such as the transfer's reference, kept only in the paid event. + """ + body = compact( + { + "paidAt": optional_datetime(paid_at), + "externalReference": external_reference, + "note": note, + } + ) + return self._api.send( + "POST", api_path("invoices", invoice_id, "mark-paid"), Invoice, body=body + ) + + def write_off(self, invoice_id: str, *, reason: str) -> Invoice: + """Give up collecting a ``MANUAL`` invoice: it becomes ``UNCOLLECTIBLE``. + + Args: + invoice_id: The invoice. + reason: Why, 1 to 500 characters. + """ + return self._api.send( + "POST", + api_path("invoices", invoice_id, "write-off"), + Invoice, + body={"reason": reason}, + idempotent=True, + ) + + def release_hold(self, invoice_id: str, *, reason: str) -> Invoice: + """Accept a held invoice as composed, and issue it. + + Args: + invoice_id: The invoice. + reason: Why, 1 to 500 characters. The release keeps it, with who released it. + + Raises: + ConflictError: The hold is ``PROVIDER_ITEM_LIMIT``, which the provider would refuse + (``ReleaseInvoiceHold.ProviderItemLimit``). + """ + return self._api.send( + "POST", + api_path("invoices", invoice_id, "release-hold"), + Invoice, + body={"reason": reason}, + ) + + def retry_push(self, invoice_id: str) -> Invoice: + """Put a ``DRAFT`` or ``PUSH_FAILED`` invoice of a payment provider back in the push queue.""" + return self._api.send("POST", api_path("invoices", invoice_id, "retry-push"), Invoice) + + def sync(self, invoice_id: str) -> Invoice: + """Read an invoice from its payment provider now, and apply what the provider says.""" + return self._api.send("POST", api_path("invoices", invoice_id, "sync"), Invoice) + + def list_line_reports( + self, invoice_id: str, line_id: str, *, limit: int | None = None + ) -> builtins.list[UsageReport]: + """Return the usage reports a ``USAGE`` or ``OVERAGE`` line was measured from. + + Summed by reset window and floored at 0, they give the line's measured quantity. It + answers after the instance was deleted. + + Args: + invoice_id: The invoice. + line_id: The line, as the invoice's lines carry it. + limit: The most reports to return. Every page is walked when omitted. + """ + return self._api.list_reports( + api_path("invoices", invoice_id, "lines", line_id, "reports"), + UsageReport, + limit=check_limit(limit), + ) + + +class BillingHandoff(Resource): + """The accounting handoff: the invoices waiting for the organization's accounting system. + + Claim a batch under a lease, book each invoice, then acknowledge it. An invoice not + acknowledged before its lease expires is claimed again: the queue delivers at least once, + so deduplicate on the invoice id. + """ + + def list( + self, + *, + status: Literal["PENDING", "ACKNOWLEDGED"] | None = None, + limit: int | None = None, + ) -> builtins.list[QueuedInvoice]: + """Return the queue, oldest issue first, without leasing anything. + + Args: + status: ``PENDING`` (the default) or ``ACKNOWLEDGED``. + limit: The most invoices to return. Every page is walked when omitted. + """ + return self._api.list_all( + "/billing/handoff", + QueuedInvoice, + params={"status": status}, + limit=check_limit(limit), + ) + + def claim(self, *, limit: int | None = None, lease_seconds: int | None = None) -> HandoffClaim: + """Lease the oldest invoices waiting, under one lease, and return them. + + Two consumers claiming at once get different invoices. A claim is not idempotent: a lost + response leaves its invoices leased until ``leased_until``, so it is never retried. + + Args: + limit: How many invoices to lease, 1 to 100. 25 when omitted. + lease_seconds: How long the lease lasts, 60 to 3600 seconds. 900 when omitted. + """ + body = compact({"limit": limit, "leaseSeconds": lease_seconds}) + return self._api.send("POST", "/billing/handoff/claim", HandoffClaim, body=body) + + def ack( + self, + invoice_id: str, + *, + lease_id: str | None = None, + external_reference: str | None = None, + ) -> Invoice: + """Record that the accounting system booked an invoice of the queue. Safe to repeat. + + Args: + invoice_id: The invoice. + lease_id: The claim's lease. When given, an invoice another claim took since is + refused (``AckHandoff.LeaseMismatch``). + external_reference: The invoice's number in the accounting system. Another + reference than the one it was acknowledged under is refused + (``AckHandoff.ReferenceMismatch``): take it as already handed off. + """ + body = compact({"leaseId": lease_id, "externalReference": external_reference}) + return self._api.send( + "POST", + api_path("billing", "handoff", invoice_id, "ack"), + Invoice, + body=body, + idempotent=True, + ) + + +def invoice_query( + *, + customer_slug: str | None, + status: Sequence[InvoiceStatus] | None, + kind: InvoiceKind | None, + provider_kind: BillingProviderKind | None, + overdue: bool | None, + held: bool | None, + handoff_status: HandoffStatus | None, + issued_from: datetime | str | None, + issued_to: datetime | str | None, + boundary_from: datetime | str | None, + boundary_to: datetime | str | None, + updated_since: datetime | str | None, +) -> dict[str, Any]: + """The filters every invoice listing takes, as query parameters.""" + return { + "customerSlug": customer_slug, + "status": list(status) if status is not None else None, + "kind": kind, + "providerKind": provider_kind, + "overdue": overdue, + "held": held, + "handoffStatus": handoff_status, + "issuedFrom": optional_datetime(issued_from), + "issuedTo": optional_datetime(issued_to), + "boundaryFrom": optional_datetime(boundary_from), + "boundaryTo": optional_datetime(boundary_to), + "updatedSince": optional_datetime(updated_since), + } diff --git a/src/kaitencloud/_sync/resources/billing/license_prices.py b/src/kaitencloud/_sync/resources/billing/license_prices.py new file mode 100644 index 0000000..4780630 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/license_prices.py @@ -0,0 +1,194 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/license_prices.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from collections.abc import Mapping + +from ...._utils import addon_quantities, api_path, compact, price_body +from ....types import ( + BillingModel, + BillingPeriod, + BillingTiming, + InvoicePreview, + Price, + PriceStatus, +) +from .._base import Resource + +__all__ = ["BillingLicensePrices"] + + +class BillingLicensePrices(Resource): + """The prices of license versions. + + A ``FLAT_FEE`` price bills a subscription each period; a ``USAGE_BASED`` price bills the + usage of an entitlement, and an ``OVERAGE`` price the usage above the license's limit, per + sale unit. Amounts are decimal strings in minor units: ``"4900"`` is 49.00 in a currency + with two decimals. The prices of a published version never change. + """ + + def list( + self, + license_slug: str, + *, + status: PriceStatus | None = None, + billing_model: BillingModel | None = None, + ) -> builtins.list[Price]: + """Return the prices of the license version, in display order; both statuses when omitted.""" + return self._api.get_list( + api_path("licenses", license_slug, "prices"), + Price, + params={"status": status, "billingModel": billing_model}, + ) + + def get(self, license_slug: str, price_id: str) -> Price: + """Return one price of the license version.""" + return self._api.get(api_path("licenses", license_slug, "prices", price_id), Price) + + def create( + self, + license_slug: str, + *, + billing_model: BillingModel, + currency: str, + unit_amount_decimal: str, + billing_period: BillingPeriod | None = None, + billing_timing: BillingTiming | None = None, + metered_entitlement_slug: str | None = None, + is_default: bool | None = None, + display_label: str | None = None, + display_order: int | None = None, + ) -> Price: + """Add a price to the license version, and return it. + + Args: + license_slug: The license version. + billing_model: ``FLAT_FEE``, ``USAGE_BASED`` or ``OVERAGE``. + currency: An ISO 4217 code, upper case. One currency per version. + unit_amount_decimal: The amount in minor units, as a decimal string: per period for + ``FLAT_FEE``, per sale unit for a metered price. + billing_period: ``MONTHLY``, ``QUARTERLY``, ``SEMI_ANNUAL`` or ``ANNUAL``. Required + on a ``FLAT_FEE`` price, refused on a metered one. + billing_timing: ``ADVANCE`` (the default for ``FLAT_FEE``) or ``ARREARS``, which a + metered price must be. + metered_entitlement_slug: The entitlement a metered price measures: granted by the + version, with a reset period, aggregated by ``SUM`` or ``COUNT``. + is_default: Make this ``FLAT_FEE`` price the default of its billing period. + display_label: The invoice line's label. + display_order: Its position among the version's prices. + + Raises: + ConflictError: Another price is already the default of the period + (``CreateLicensePrice.DefaultConflict``). + """ + body = price_body( + billing_model=billing_model, + currency=currency, + unit_amount_decimal=unit_amount_decimal, + billing_period=billing_period, + billing_timing=billing_timing, + metered_entitlement_slug=metered_entitlement_slug, + is_default=is_default, + display_label=display_label, + display_order=display_order, + ) + return self._api.send( + "POST", api_path("licenses", license_slug, "prices"), Price, body=body + ) + + def update( + self, + license_slug: str, + price_id: str, + *, + unit_amount_decimal: str | None = None, + billing_period: BillingPeriod | None = None, + billing_timing: BillingTiming | None = None, + metered_entitlement_slug: str | None = None, + is_default: bool | None = None, + display_label: str | None = None, + display_order: int | None = None, + ) -> Price: + """Edit a price of a ``DRAFT`` version; what is omitted keeps its value. + + Its currency and billing model never change, and the prices of a published version are + immutable (``UpdateLicensePrice.VersionNotDraft``). + """ + body = compact( + { + "unitAmountDecimal": unit_amount_decimal, + "billingPeriod": billing_period, + "billingTiming": billing_timing, + "meteredEntitlementSlug": metered_entitlement_slug, + "isDefault": is_default, + "displayLabel": display_label, + "displayOrder": display_order, + } + ) + return self._api.send( + "PATCH", api_path("licenses", license_slug, "prices", price_id), Price, body=body + ) + + def deprecate(self, license_slug: str, price_id: str) -> Price: + """Retire a price from the catalog, for good. + + It keeps billing the subscriptions pinned to it, is no longer offered, and stops being a + default. + """ + return self._api.send( + "POST", api_path("licenses", license_slug, "prices", price_id, "deprecate"), Price + ) + + def preview_invoice( + self, + license_slug: str, + *, + base_price_id: str | None = None, + add_ons: Mapping[str, int] | None = None, + sample_usage: Mapping[str, str] | None = None, + instance_slug: str | None = None, + voucher_code: str | None = None, + ) -> InvoicePreview: + """Compose the ``RENEWAL`` invoice a subscription to the version would get now. + + Nothing is written, and it works on a ``DRAFT`` version, to check its prices before + publishing it. + + Args: + license_slug: The license version. + base_price_id: The ``FLAT_FEE`` price billed as the base. Defaults to the version's + default one. + add_ons: Add-on versions priced in, as ``{addon_slug: quantity}``. + sample_usage: Usage of the metered entitlements over the period that ends, as + ``{entitlement_slug: quantity}`` in decimal strings. An entitlement left out + used nothing. + instance_slug: Rate this instance's own usage instead of a sample. + voucher_code: Apply a ``PRICE`` voucher's discount, consuming nothing. + """ + body = compact( + { + "basePriceId": base_price_id, + "addOns": addon_quantities(add_ons), + "sampleUsage": ( + [ + {"entitlementSlug": slug, "quantity": quantity} + for slug, quantity in sample_usage.items() + ] + if sample_usage is not None + else None + ), + "instanceSlug": instance_slug, + "voucherCode": voucher_code, + } + ) + return self._api.send( + "POST", + api_path("licenses", license_slug, "invoice-preview"), + InvoicePreview, + body=body, + idempotent=True, + ) diff --git a/src/kaitencloud/_sync/resources/billing/public.py b/src/kaitencloud/_sync/resources/billing/public.py new file mode 100644 index 0000000..ee1fdb0 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/public.py @@ -0,0 +1,324 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/public.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from collections.abc import Mapping + +from ...._utils import addon_quantities, api_path, check_limit, compact +from ....types import ( + BillingPeriod, + CheckoutConfirmation, + CheckoutSession, + CheckoutSessionStart, + CompletedPaymentMethodSession, + PaymentMethodSession, + PortalSession, + PublicCatalog, + SessionAddon, + SessionCheckout, + SessionInvoice, + SessionPortal, + SessionRedemption, + SessionSubscription, + SessionVoucherValidity, +) +from .._base import Resource + +__all__ = ["BillingPublic", "BillingSession"] + + +class BillingPublic(Resource): + """What a publishable key (``pk_...``) reaches: the public catalog and anonymous checkouts. + + Build the client with the key as its token -- the SDK sends it as the API requires, in + ``X-Kaiten-Publishable-Key`` and never as a bearer token:: + + public = KaitenClient(base_url="https://kaiten.example.com", token="pk_...") + catalog = public.billing.public.get_catalog() + + An anonymous checkout's own routes take the token it was started with (``ksc_...``) + instead: build a client with that token to read or confirm it. + """ + + def get_catalog( + self, *, family_slug: str | None = None, include_addons: bool = True + ) -> PublicCatalog: + """Return what a pricing page shows: the public plans, their prices and entitlements. + + For each license family the organization made public, its default version with its + ``ACTIVE`` prices and user-facing entitlements, and the same for public add-on + families. Amounts are in minor units: never sum metered prices into a headline. + + Args: + family_slug: Only this license family. + include_addons: Also list the public add-ons. True by default. + """ + return self._api.get( + "/public/catalog", + PublicCatalog, + params={"familySlug": family_slug, "include": None if include_addons else ["none"]}, + ) + + def create_checkout_session( + self, + *, + turnstile_token: str, + price_id: str | None = None, + license_slug: str | None = None, + billing_period: BillingPeriod | None = None, + add_ons: Mapping[str, int] | None = None, + email: str | None = None, + return_url: str | None = None, + voucher_code: str | None = None, + trial: bool | None = None, + client_reference_id: str | None = None, + dry_run: bool | None = None, + ) -> CheckoutSessionStart: + """Start a checkout for a visitor your backend does not know yet. + + It writes one checkout session and nothing else: the customer, the instance and the + subscription are created when it completes. The session's token (``ksc_...``) is in + this response only. The key must allow anonymous checkouts. + + Args: + turnstile_token: The token the Turnstile widget gave the page. Single-use. + price_id: A ``FLAT_FEE`` price of a public plan. Not with ``license_slug``. + license_slug: A public plan's version, with ``billing_period``: its default + ``ACTIVE`` ``FLAT_FEE`` price of that period. + billing_period: ``MONTHLY``, ``QUARTERLY``, ``SEMI_ANNUAL`` or ``ANNUAL``. + add_ons: Public add-ons, as ``{addon_slug: quantity}``. + email: The buyer's e-mail, which becomes the new customer's billing e-mail. Required + unless ``dry_run``. + return_url: Where the payment-method page sends the visitor back, on an origin the + key allows. Required unless ``dry_run``. + voucher_code: A code to apply to the selection. + trial: ``False`` declines the license's trial. + client_reference_id: Your own reference, echoed on the ``checkout_session`` events. + dry_run: Compute what would be due today, and write nothing. + """ + body = { + "turnstileToken": turnstile_token, + **compact( + { + "priceId": price_id, + "licenseSlug": license_slug, + "billingPeriod": billing_period, + "addOns": addon_quantities(add_ons), + "email": email, + "returnUrl": return_url, + "voucherCode": voucher_code, + "trial": trial, + "clientReferenceId": client_reference_id, + "dryRun": dry_run, + } + ), + } + return self._api.send("POST", "/public/checkout-sessions", CheckoutSessionStart, body=body) + + def get_checkout_session(self, session_id: str) -> CheckoutSession: + """Return the checkout session the client's ``ksc_`` token was minted for. + + Its status, what it bought and what is due, and once completed, the customer and + instance it created. The token reads it for 24 hours after it closed. + """ + return self._api.get(api_path("public", "checkout-sessions", session_id), CheckoutSession) + + def confirm_checkout_session(self, session_id: str) -> CheckoutConfirmation: + """Complete the checkout session when nothing has to be captured, or say what is missing. + + A free plan, a trial that needs no payment method, or nothing due completes it at once: + ``completed``. Otherwise ``requires_payment_method`` comes with the page where the + visitor saves one, and the next confirmation completes it. + """ + return self._api.send( + "POST", + api_path("public", "checkout-sessions", session_id, "confirm"), + CheckoutConfirmation, + ) + + +class BillingSession(Resource): + """What a customer session (``kst_...``) reaches: one customer's own billing. + + These are the routes a customer's page calls with the session your backend minted with + :meth:`BillingCustomerSessions.create`. Build the client with the session's token to + call them from Python:: + + customer = KaitenClient(base_url="https://kaiten.example.com", token=session.token) + portal = customer.billing.session.get_portal() + + The subscription routes need a session bound to an instance. + """ + + def get_portal(self) -> SessionPortal: + """Return everything the customer's portal shows, in one read. + + The customer, and for a session bound to an instance its subscription, effective quotas + with where they come from, add-ons, vouchers and next invoice; the payment method; and + what the portal may offer. + """ + return self._api.get("/public/session/portal", SessionPortal) + + def list_invoices(self, *, limit: int | None = None) -> builtins.list[SessionInvoice]: + """Return the customer's issued invoices, newest first, with how to pay each. + + Args: + limit: The most invoices to return. Every page is walked when omitted. + """ + return self._api.list_all( + "/public/session/invoices", SessionInvoice, limit=check_limit(limit) + ) + + def checkout( + self, + *, + license_price_id: str, + add_ons: Mapping[str, int] | None = None, + voucher_code: str | None = None, + billing_email: str | None = None, + return_url: str | None = None, + setup_session_id: str | None = None, + trial: bool | None = None, + dry_run: bool | None = None, + ) -> SessionCheckout: + """Subscribe the session's instance to a price of the public catalog, in one call. + + ``status`` says what happened: ``preview`` for a dry run, ``requires_payment_method`` + with the page where the customer saves one -- call again with its ``setup_session_id`` + afterwards -- or ``subscribed``. + + Args: + license_price_id: An ``ACTIVE`` flat-fee price of a public plan. + add_ons: Public add-ons that fit the plan, as ``{addon_slug: quantity}``. + voucher_code: A code to redeem with the checkout. + billing_email: Where invoices go; required when the customer has none. + return_url: Where the payment-method page sends the customer back. + setup_session_id: The setup session a ``requires_payment_method`` answer gave. + trial: ``False`` skips the plan's trial. + dry_run: Answer what the checkout would bill today, and change nothing. + """ + body = { + "licensePriceId": license_price_id, + **compact( + { + "addOns": addon_quantities(add_ons), + "voucherCode": voucher_code, + "billingEmail": billing_email, + "returnUrl": return_url, + "setupSessionId": setup_session_id, + "trial": trial, + "dryRun": dry_run, + } + ), + } + return self._api.send("POST", "/public/session/checkout", SessionCheckout, body=body) + + def cancel(self, *, reason: str | None = None) -> SessionSubscription: + """Cancel the instance's subscription at the end of the period paid for. + + It keeps billing and granting until then. A trial ends at once. Repeating it changes + nothing. + + Args: + reason: Why the customer cancels, up to 500 characters; you see it on the + subscription. + """ + return self._api.send( + "POST", + "/public/session/billing/cancel", + SessionSubscription, + body=compact({"reason": reason}), + idempotent=True, + ) + + def reactivate(self) -> SessionSubscription: + """Take back a cancellation scheduled for the period's end, before it.""" + return self._api.send("POST", "/public/session/billing/reactivate", SessionSubscription) + + def set_addon_quantity(self, addon_slug: str, quantity: int) -> SessionAddon: + """Attach a public add-on to the instance, change its quantity, or remove it with ``0``. + + With a live subscription, a lower quantity is scheduled for the next renewal: + ``pending_quantity`` shows it. + """ + return self._api.send( + "PUT", + api_path("public", "session", "addons", addon_slug), + SessionAddon, + body={"quantity": quantity}, + ) + + def create_payment_method_session( + self, *, return_url: str, currency: str | None = None + ) -> PaymentMethodSession: + """Open the payment provider's page where the customer saves a payment method. + + Args: + return_url: Where the page sends the customer back, on an origin one of the + organization's publishable keys allows. + currency: Required when the customer has no live subscription. + """ + return self._api.send( + "POST", + "/public/session/billing/payment-method-session", + PaymentMethodSession, + body={"returnUrl": return_url, **compact({"currency": currency})}, + ) + + def complete_payment_method_session(self, session_id: str) -> CompletedPaymentMethodSession: + """Check a setup session with the provider, and charge the customer's invoices to it. + + Safe to repeat. + """ + return self._api.send( + "POST", + api_path( + "public", "session", "billing", "payment-method-session", session_id, "complete" + ), + CompletedPaymentMethodSession, + idempotent=True, + ) + + def create_portal_session(self, *, return_url: str) -> PortalSession: + """Open the payment provider's portal for the customer. + + Args: + return_url: Where the portal sends the customer back, on an origin one of the + organization's publishable keys allows. + """ + return self._api.send( + "POST", + "/public/session/billing/portal-session", + PortalSession, + body={"returnUrl": return_url}, + ) + + def validate_voucher( + self, code: str, *, license_price_id: str | None = None + ) -> SessionVoucherValidity: + """Say whether a code would apply to the customer, with what it gives. Nothing is redeemed. + + Args: + code: The code as the customer typed it. + license_price_id: The price about to be checked out. + """ + body = {"code": code, **compact({"licensePriceId": license_price_id})} + return self._api.send( + "POST", "/public/session/vouchers/validate", SessionVoucherValidity, body=body + ) + + def redeem_voucher(self, code: str, *, dry_run: bool | None = None) -> SessionRedemption: + """Redeem a code on the instance, and return the redemption -- never the code. + + Args: + code: The code as the customer typed it. + dry_run: Answer the redemption it would make, and write nothing. + """ + body = {"code": code, **compact({"dryRun": dry_run})} + return self._api.send( + "POST", "/public/session/vouchers/redeem", SessionRedemption, body=body + ) diff --git a/src/kaitencloud/_sync/resources/billing/sessions.py b/src/kaitencloud/_sync/resources/billing/sessions.py new file mode 100644 index 0000000..e085136 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/sessions.py @@ -0,0 +1,186 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/sessions.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from collections.abc import Sequence + +from ...._utils import api_path, check_limit, compact +from ....types import ( + CheckoutSessionAdmin, + CheckoutSessionStatus, + CreatedCustomerSession, + PublishableKey, + PublishableKeyCreated, +) +from .._base import Resource + +__all__ = [ + "BillingCheckoutSessions", + "BillingCustomerSessions", + "BillingPublishableKeys", +] + + +class BillingPublishableKeys(Resource): + """Publishable keys (``pk_...``): what a web page reads the public catalog with. + + A publishable key is not a secret: it ships in pages, and a browser may send it only from + one of its allowed origins. With ``anonymous_checkout``, it also starts checkouts for + visitors your backend does not know yet. + """ + + def list(self, *, include_revoked: bool = False) -> builtins.list[PublishableKey]: + """Return the organization's keys, newest first: their last characters, never the keys.""" + return self._api.list_all( + "/publishable-keys", PublishableKey, params={"includeRevoked": include_revoked or None} + ) + + def create( + self, + *, + label: str, + allowed_origins: Sequence[str], + anonymous_checkout: bool | None = None, + turnstile_site_key: str | None = None, + turnstile_secret: str | None = None, + ) -> PublishableKeyCreated: + """Issue a publishable key, and return it with the key, which exists in this response only. + + Args: + label: What the key is for. + allowed_origins: The browser origins allowed to send it, as + ``scheme://host[:port]``: https, or http on localhost. Empty, no browser may; + a request without an ``Origin`` header -- server-side rendering -- still may. + anonymous_checkout: Let the key start anonymous checkouts. It needs a Turnstile site + key and secret. + turnstile_site_key: The Turnstile site key the page renders its widget with. + turnstile_secret: The Turnstile secret the server verifies tokens with, kept in + Vault and never returned. + """ + body = { + "label": label, + "allowedOrigins": list(allowed_origins), + **compact( + { + "anonymousCheckout": anonymous_checkout, + "turnstileSiteKey": turnstile_site_key, + "turnstileSecret": turnstile_secret, + } + ), + } + return self._api.send("POST", "/publishable-keys", PublishableKeyCreated, body=body) + + def update( + self, + key_id: str, + *, + label: str | None = None, + allowed_origins: Sequence[str] | None = None, + anonymous_checkout: bool | None = None, + turnstile_site_key: str | None = None, + turnstile_secret: str | None = None, + ) -> PublishableKey: + """Change a live key; what is omitted keeps its value, and ``""`` clears a Turnstile value. + + Raises: + ConflictError: The key is revoked (``UpdatePublishableKey.Revoked``). + """ + body = compact( + { + "label": label, + "allowedOrigins": list(allowed_origins) if allowed_origins is not None else None, + "anonymousCheckout": anonymous_checkout, + "turnstileSiteKey": turnstile_site_key, + "turnstileSecret": turnstile_secret, + } + ) + return self._api.send( + "PATCH", api_path("publishable-keys", key_id), PublishableKey, body=body + ) + + def revoke(self, key_id: str) -> PublishableKey: + """Revoke a key, for good, with its anonymous checkout sessions. Idempotent.""" + return self._api.send( + "POST", api_path("publishable-keys", key_id, "revoke"), PublishableKey, idempotent=True + ) + + +class BillingCustomerSessions(Resource): + """Customer sessions (``kst_...``): what lets a customer's browser act on its own billing. + + Mint one from your backend, where the customer is signed in, and hand its token to the + page, which calls the routes of :attr:`Billing.session` with it. + """ + + def create( + self, + *, + customer_slug: str, + instance_slug: str | None = None, + ttl_seconds: int | None = None, + ) -> CreatedCustomerSession: + """Mint a session for a customer, and return it with its token, shown this once. + + What the session writes is attributed to the credential that minted it. + + Args: + customer_slug: The customer the session acts for. + instance_slug: Bind it to one of the customer's instances, which checkout, cancel + and the other subscription routes need. + ttl_seconds: How long it lives, 300 to 3600 seconds. 30 minutes when omitted. + """ + body = { + "customerSlug": customer_slug, + **compact({"instanceSlug": instance_slug, "ttlSeconds": ttl_seconds}), + } + return self._api.send("POST", "/customer-sessions", CreatedCustomerSession, body=body) + + def revoke(self, session_id: str) -> None: + """Stop a session authenticating, from its next request: on sign-out, say. Idempotent.""" + self._api.send_empty( + "POST", api_path("customer-sessions", session_id, "revoke"), idempotent=True + ) + + +class BillingCheckoutSessions(Resource): + """Anonymous checkouts, as the organization reads them: never their token or voucher code. + + It is how a buyer is linked to your own account when the ``checkout_session`` events were + missed. + """ + + def list( + self, + *, + status: Sequence[CheckoutSessionStatus] | None = None, + email: str | None = None, + client_reference_id: str | None = None, + limit: int | None = None, + ) -> builtins.list[CheckoutSessionAdmin]: + """Return the organization's anonymous checkouts, newest first. + + Args: + status: Only sessions in these statuses. A session ``OPEN`` past its expiry is + ``ABANDONED``. + email: Only the visitor with this e-mail, in any letter case. + client_reference_id: Only the sessions started with this reference of yours. + limit: The most sessions to return. Every page is walked when omitted. + """ + return self._api.list_all( + "/checkout-sessions", + CheckoutSessionAdmin, + params={ + "status": list(status) if status is not None else None, + "email": email, + "clientReferenceId": client_reference_id, + }, + limit=check_limit(limit), + ) + + def get(self, session_id: str) -> CheckoutSessionAdmin: + """Return an anonymous checkout. One closed 90 days ago is gone.""" + return self._api.get(api_path("checkout-sessions", session_id), CheckoutSessionAdmin) diff --git a/src/kaitencloud/_sync/resources/billing/subscriptions.py b/src/kaitencloud/_sync/resources/billing/subscriptions.py new file mode 100644 index 0000000..dcb29f7 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/subscriptions.py @@ -0,0 +1,340 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/subscriptions.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any + +from ...._types import NOT_GIVEN, NotGiven +from ...._utils import addon_quantities, api_path, check_limit, compact, optional_datetime +from ....types import ( + BillingProviderKind, + CanceledSubscription, + CancellationMode, + CollectionMethod, + HandoffStatus, + InstanceAddon, + InstanceBilling, + InvoiceKind, + InvoicePreview, + InvoiceStatus, + InvoiceSummary, + Redemption, + RedemptionStatus, + StartedSubscription, +) +from .._base import Resource +from .invoices import invoice_query + +__all__ = [ + "BillingInstanceAddons", + "BillingInstanceVouchers", + "BillingSubscriptions", +] + + +class BillingSubscriptions(Resource): + """Instances' subscriptions: what bills an instance, period after period. + + A subscription is pinned to a ``FLAT_FEE`` price of the instance's license version. Each + period closes into a ``RENEWAL`` invoice; a base price billed in advance issues the first + period's ``ACTIVATION`` invoice at once. While it is live, the instance's customer and + license cannot change. + """ + + def get(self, instance_slug: str) -> InstanceBilling: + """Return the instance's subscription, live or ``CANCELED``. + + Raises: + NotFoundError: The instance was never subscribed. + """ + return self._api.get(api_path("instances", instance_slug, "billing"), InstanceBilling) + + def subscribe( + self, + instance_slug: str, + *, + base_price_id: str, + add_ons: Mapping[str, int] | None = None, + voucher_code: str | None = None, + trial_days: int | None = None, + start_at: datetime | str | None = None, + provider_kind: BillingProviderKind | None = None, + collection_method: CollectionMethod | None = None, + days_until_due: int | None = None, + ) -> StartedSubscription: + """Start billing an instance, and return its subscription with its first invoice. + + A ``CANCELED`` subscription is subscribed again. + + Args: + instance_slug: The instance. + base_price_id: An ``ACTIVE`` ``FLAT_FEE`` price of the instance's license version, + which must be ``PUBLISHED``. The subscription stays pinned to it. + add_ons: Add-ons to attach with it, as ``{addon_slug: quantity}``; the + ``ACTIVATION`` invoice bills them. + voucher_code: A voucher to redeem with the subscription. + trial_days: Days of trial, 1 to 365: nothing is billed, and no usage counts, until + it ends. + start_at: What periods are counted from: now when omitted, at most one billing + period ago otherwise. + provider_kind: Who issues and collects the invoices: ``NOOP`` (the default), the + organization itself through the handoff queue, or a connected provider. + collection_method: ``SEND_INVOICE`` or ``CHARGE_AUTOMATICALLY``. The + organization's default, read at each invoice, when omitted. + days_until_due: Payment terms in days, 0 to 365. The organization's default when + omitted. + """ + body = { + "basePriceId": base_price_id, + **compact( + { + "addOns": addon_quantities(add_ons), + "voucherCode": voucher_code, + "trialDays": trial_days, + "startAt": optional_datetime(start_at), + "providerKind": provider_kind, + "collectionMethod": collection_method, + "daysUntilDue": days_until_due, + } + ), + } + return self._api.send( + "POST", api_path("instances", instance_slug, "billing"), StartedSubscription, body=body + ) + + def update( + self, + instance_slug: str, + *, + provider_kind: BillingProviderKind | None = None, + collection_method: CollectionMethod | NotGiven | None = NOT_GIVEN, + days_until_due: int | NotGiven | None = NOT_GIVEN, + ) -> InstanceBilling: + """Change a subscription's provider and terms from its next invoice on. + + What is not passed keeps its value; ``None`` restores the organization's default. + Invoices already composed keep their own terms and provider. + + Args: + instance_slug: The instance. + provider_kind: Who issues and collects the invoices from the next one on. + collection_method: ``SEND_INVOICE`` or ``CHARGE_AUTOMATICALLY``. + days_until_due: Payment terms in days, 0 to 365. + """ + body: dict[str, Any] = compact({"providerKind": provider_kind}) + if not isinstance(collection_method, NotGiven): + body["collectionMethod"] = collection_method + if not isinstance(days_until_due, NotGiven): + body["daysUntilDue"] = days_until_due + return self._api.send( + "PATCH", api_path("instances", instance_slug, "billing"), InstanceBilling, body=body + ) + + def cancel( + self, + instance_slug: str, + *, + mode: CancellationMode | None = None, + reason: str | None = None, + ) -> CanceledSubscription: + """Cancel a subscription. + + A trial ends at once, with no invoice. Otherwise ``AT_PERIOD_END`` (the default) lets the + period paid for run out, then issues the ``FINAL`` invoice; ``IMMEDIATE`` issues it now. + Cancellation changes billing only: entitlements and license dates stay yours to change. + + Args: + instance_slug: The instance. + mode: ``AT_PERIOD_END`` or ``IMMEDIATE``. + reason: Why, up to 500 characters. + """ + return self._api.send( + "POST", + api_path("instances", instance_slug, "billing", "cancel"), + CanceledSubscription, + body=compact({"mode": mode, "reason": reason}), + ) + + def reactivate(self, instance_slug: str) -> InstanceBilling: + """Take back a cancellation scheduled for the period's end, before it.""" + return self._api.send( + "POST", api_path("instances", instance_slug, "billing", "reactivate"), InstanceBilling + ) + + def schedule_plan_change(self, instance_slug: str, license_price_id: str) -> InstanceBilling: + """Schedule a move to another ``FLAT_FEE`` price for the next boundary. + + That boundary's ``RENEWAL`` bills the old plan's arrears and the new plan's advance, and + moves the instance to the new version, without proration. Scheduling another target + replaces the change. + + Args: + instance_slug: The instance. + license_price_id: An ``ACTIVE`` ``FLAT_FEE`` price of a ``PUBLISHED`` license + version, in the subscription's currency. + """ + return self._api.send( + "PUT", + api_path("instances", instance_slug, "billing", "scheduled-change"), + InstanceBilling, + body={"licensePriceId": license_price_id}, + ) + + def cancel_plan_change(self, instance_slug: str) -> InstanceBilling: + """Drop the plan change scheduled for the next boundary.""" + return self._api.send( + "DELETE", + api_path("instances", instance_slug, "billing", "scheduled-change"), + InstanceBilling, + ) + + def get_upcoming_invoice(self, instance_slug: str) -> InvoicePreview: + """Return the invoice the next boundary will issue, from the usage so far, writing nothing. + + ``would_hold`` lists the meters whose usage journal fails a check and would hold it. + """ + return self._api.get( + api_path("instances", instance_slug, "billing", "upcoming-invoice"), InvoicePreview + ) + + def list_invoices( + self, + instance_slug: str, + *, + customer_slug: str | None = None, + status: Sequence[InvoiceStatus] | None = None, + kind: InvoiceKind | None = None, + provider_kind: BillingProviderKind | None = None, + overdue: bool | None = None, + held: bool | None = None, + handoff_status: HandoffStatus | None = None, + issued_from: datetime | str | None = None, + issued_to: datetime | str | None = None, + boundary_from: datetime | str | None = None, + boundary_to: datetime | str | None = None, + updated_since: datetime | str | None = None, + limit: int | None = None, + ) -> builtins.list[InvoiceSummary]: + """Return the invoices of an instance's subscription, across every time it was subscribed. + + The filters are those of :meth:`BillingInvoices.list`. + """ + return self._api.list_all( + api_path("instances", instance_slug, "invoices"), + InvoiceSummary, + params=invoice_query( + customer_slug=customer_slug, + status=status, + kind=kind, + provider_kind=provider_kind, + overdue=overdue, + held=held, + handoff_status=handoff_status, + issued_from=issued_from, + issued_to=issued_to, + boundary_from=boundary_from, + boundary_to=boundary_to, + updated_since=updated_since, + ), + limit=check_limit(limit), + ) + + +class BillingInstanceAddons(Resource): + """The add-ons an instance holds, by the unit. + + Their entitlements apply at once; a subscription bills the quantity held at each boundary, + without proration. An instance holds one version per add-on family. + """ + + def list( + self, instance_slug: str, *, include_removed: bool = False + ) -> builtins.list[InstanceAddon]: + """Return the add-ons the instance holds, in attachment order, with the prices billed. + + Args: + instance_slug: The instance. + include_removed: Also return the add-ons removed from it. + """ + return self._api.get_list( + api_path("instances", instance_slug, "addons"), + InstanceAddon, + params={"includeRemoved": include_removed or None}, + ) + + def attach( + self, instance_slug: str, addon_slug: str, *, quantity: int | None = None + ) -> InstanceAddon: + """Give the instance an add-on version, and return the attachment. + + Moving to another version of the same add-on is a :meth:`detach`, then an attach. + """ + body = {"addonSlug": addon_slug, **compact({"quantity": quantity})} + return self._api.send( + "POST", api_path("instances", instance_slug, "addons"), InstanceAddon, body=body + ) + + def set_quantity(self, instance_slug: str, addon_slug: str, quantity: int) -> InstanceAddon: + """Change how many units of an add-on the instance holds, at once, a decrease included. + + It cancels a decrease the customer scheduled, even to the quantity held. + """ + return self._api.send( + "PATCH", + api_path("instances", instance_slug, "addons", addon_slug), + InstanceAddon, + body={"quantity": quantity}, + idempotent=True, + ) + + def detach(self, instance_slug: str, addon_slug: str) -> None: + """Remove an add-on from the instance, at once. Its entitlements drop; nothing is refunded.""" + self._api.send_empty("DELETE", api_path("instances", instance_slug, "addons", addon_slug)) + + +class BillingInstanceVouchers(Resource): + """The vouchers an instance redeemed.""" + + def list( + self, instance_slug: str, *, status: RedemptionStatus | None = None + ) -> builtins.list[Redemption]: + """Return the instance's redemptions, newest first.""" + return self._api.get_list( + api_path("instances", instance_slug, "vouchers"), Redemption, params={"status": status} + ) + + def redeem(self, instance_slug: str, code: str) -> Redemption: + """Redeem a voucher code for the instance, and return the redemption. + + A boost applies from the next read or report, for its duration; a ``PRICE`` voucher + discounts the subscription's next invoices. + """ + return self._api.send( + "POST", + api_path("instances", instance_slug, "vouchers", "redeem"), + Redemption, + body={"code": code}, + ) + + def revoke(self, instance_slug: str, instance_voucher_id: str, *, reason: str) -> Redemption: + """Stop a redemption applying: a boost ends at once, a discount applies to no further invoice. + + Invoices already issued are untouched. + + Args: + instance_slug: The instance. + instance_voucher_id: The redemption's id. + reason: Why, 1 to 500 characters. + """ + return self._api.send( + "POST", + api_path("instances", instance_slug, "vouchers", instance_voucher_id, "revoke"), + Redemption, + body={"reason": reason}, + ) diff --git a/src/kaitencloud/_sync/resources/billing/vouchers.py b/src/kaitencloud/_sync/resources/billing/vouchers.py new file mode 100644 index 0000000..b41e6e4 --- /dev/null +++ b/src/kaitencloud/_sync/resources/billing/vouchers.py @@ -0,0 +1,321 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/billing/vouchers.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +import builtins +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any, Literal + +from ...._utils import api_path, check_limit, compact, optional_datetime +from ....types import ( + DiscountAppliesTo, + DiscountType, + Redemption, + RedemptionRulesParam, + Validity, + Voucher, + VoucherDuration, + VoucherGrantParam, + VoucherStatus, + VoucherType, +) +from .._base import Resource + +__all__ = ["BillingVouchers"] + + +class BillingVouchers(Resource): + """The organization's vouchers: codes that discount a price or boost entitlements. + + A ``PRICE`` voucher discounts a subscription's invoices; an ``ENTITLEMENT_BOOST`` changes + entitlements while it applies; a ``FLAG_GRANT`` serves flag variants; a ``COMPOSITE`` does + several. A voucher is created ``DRAFT`` and becomes redeemable once published. + """ + + def list( + self, + *, + status: VoucherStatus | None = None, + voucher_type: Literal["PRICE", "ENTITLEMENT_BOOST"] | None = None, + restricted_customer_slug: str | None = None, + ) -> builtins.list[Voucher]: + """Return the organization's vouchers, newest first, with their codes, walking all pages.""" + return self._api.list_all( + "/vouchers", + Voucher, + params={ + "status": status, + "voucherType": voucher_type, + "restrictedCustomerSlug": restricted_customer_slug, + }, + ) + + def get(self, voucher_id: str) -> Voucher: + """Return a voucher, with its code.""" + return self._api.get(api_path("vouchers", voucher_id), Voucher) + + def create( + self, + *, + name: str, + voucher_type: VoucherType, + duration: VoucherDuration, + code: str | None = None, + description: str | None = None, + duration_in_periods: int | None = None, + starts_at: datetime | str | None = None, + expires_at: datetime | str | None = None, + max_redemptions: int | None = None, + restricted_customer_slug: str | None = None, + price_discount_type: DiscountType | None = None, + price_discount_value: str | None = None, + currency: str | None = None, + price_applies_to: DiscountAppliesTo | None = None, + applicable_license_ids: Sequence[str] | None = None, + applicable_license_price_ids: Sequence[str] | None = None, + applicable_addon_ids: Sequence[str] | None = None, + applicable_addon_price_ids: Sequence[str] | None = None, + grants: Sequence[VoucherGrantParam] | None = None, + flag_grants: Mapping[str, str] | None = None, + redemption_rules: RedemptionRulesParam | None = None, + ) -> Voucher: + """Create a ``DRAFT`` voucher, and return it with its code. + + Args: + name: The voucher's name. + voucher_type: ``PRICE``, ``ENTITLEMENT_BOOST``, ``FLAG_GRANT`` or ``COMPOSITE``. + duration: How long a discount lasts: ``ONE_TIME``, ``REPEATING`` or ``FOREVER``. + code: 8 to 64 of ``[A-Za-z0-9_-]``, matched without case or separators. Generated + when omitted. + description: A description. + duration_in_periods: How many periods a ``REPEATING`` discount lasts. + starts_at: When it can be redeemed from. + expires_at: When it can no longer be redeemed. + max_redemptions: How many times it can be redeemed. + restricted_customer_slug: The only customer that may redeem it. + price_discount_type: ``PERCENTAGE`` or ``FIXED_AMOUNT``. + price_discount_value: A percentage in (0, 100], or an amount in minor units, as a + string. + currency: Required with ``FIXED_AMOUNT``, refused otherwise. + price_applies_to: What the discount applies to: ``LICENSE_BASE``, ``ADDONS``, + ``BOTH``, or ``SELECTED_PRICES``. + applicable_license_ids: The license versions it applies to. + applicable_license_price_ids: With ``SELECTED_PRICES``, the license prices. + applicable_addon_ids: The add-on versions it applies to. + applicable_addon_price_ids: With ``SELECTED_PRICES``, the add-on prices. + grants: What an ``ENTITLEMENT_BOOST`` grants, one entitlement each. + flag_grants: What a ``FLAG_GRANT`` serves, as ``{flag_slug: variant}``. + redemption_rules: Who may redeem it, beyond its dates and count. + """ + body = _voucher( + name=name, + voucher_type=voucher_type, + duration=duration, + code=code, + description=description, + duration_in_periods=duration_in_periods, + starts_at=starts_at, + expires_at=expires_at, + max_redemptions=max_redemptions, + restricted_customer_slug=restricted_customer_slug, + price_discount_type=price_discount_type, + price_discount_value=price_discount_value, + currency=currency, + price_applies_to=price_applies_to, + applicable_license_ids=applicable_license_ids, + applicable_license_price_ids=applicable_license_price_ids, + applicable_addon_ids=applicable_addon_ids, + applicable_addon_price_ids=applicable_addon_price_ids, + grants=grants, + flag_grants=flag_grants, + redemption_rules=redemption_rules, + ) + return self._api.send("POST", "/vouchers", Voucher, body=body) + + def update( + self, + voucher_id: str, + *, + name: str, + voucher_type: VoucherType, + duration: VoucherDuration, + code: str | None = None, + description: str | None = None, + duration_in_periods: int | None = None, + starts_at: datetime | str | None = None, + expires_at: datetime | str | None = None, + max_redemptions: int | None = None, + restricted_customer_slug: str | None = None, + price_discount_type: DiscountType | None = None, + price_discount_value: str | None = None, + currency: str | None = None, + price_applies_to: DiscountAppliesTo | None = None, + applicable_license_ids: Sequence[str] | None = None, + applicable_license_price_ids: Sequence[str] | None = None, + applicable_addon_ids: Sequence[str] | None = None, + applicable_addon_price_ids: Sequence[str] | None = None, + grants: Sequence[VoucherGrantParam] | None = None, + flag_grants: Mapping[str, str] | None = None, + redemption_rules: RedemptionRulesParam | None = None, + ) -> Voucher: + """Replace a voucher, and return it. The arguments are :meth:`create`'s. + + A ``DRAFT`` is replaced whole, but its type. An ``ACTIVE`` voucher takes a new name, + description, expiry and redemption count only; its other members must stay as they are + (``UpdateVoucher.NotEditable``). + """ + body = _voucher( + name=name, + voucher_type=voucher_type, + duration=duration, + code=code, + description=description, + duration_in_periods=duration_in_periods, + starts_at=starts_at, + expires_at=expires_at, + max_redemptions=max_redemptions, + restricted_customer_slug=restricted_customer_slug, + price_discount_type=price_discount_type, + price_discount_value=price_discount_value, + currency=currency, + price_applies_to=price_applies_to, + applicable_license_ids=applicable_license_ids, + applicable_license_price_ids=applicable_license_price_ids, + applicable_addon_ids=applicable_addon_ids, + applicable_addon_price_ids=applicable_addon_price_ids, + grants=grants, + flag_grants=flag_grants, + redemption_rules=redemption_rules, + ) + return self._api.send("PUT", api_path("vouchers", voucher_id), Voucher, body=body) + + def delete(self, voucher_id: str) -> None: + """Delete a voucher no instance ever redeemed, whatever its status, freeing its code. + + Raises: + ConflictError: It was redeemed at least once, or a checkout waiting for its payment + applies it (``DeleteVoucher.Redeemed``): :meth:`archive` it instead. + """ + self._api.send_empty("DELETE", api_path("vouchers", voucher_id)) + + def publish(self, voucher_id: str) -> Voucher: + """Make a ``DRAFT`` voucher redeemable.""" + return self._api.send("POST", api_path("vouchers", voucher_id, "publish"), Voucher) + + def archive(self, voucher_id: str) -> Voucher: + """Stop a voucher being redeemed. Redemptions already made keep applying until revoked.""" + return self._api.send("POST", api_path("vouchers", voucher_id, "archive"), Voucher) + + def list_redemptions( + self, voucher_id: str, *, limit: int | None = None + ) -> builtins.list[Redemption]: + """Return every redemption of a voucher, newest first. + + Args: + voucher_id: The voucher. + limit: The most redemptions to return. Every page is walked when omitted. + """ + return self._api.list_all( + api_path("vouchers", voucher_id, "redemptions"), Redemption, limit=check_limit(limit) + ) + + def lookup(self, code: str) -> Voucher: + """Return the voucher a code names, matched without case or separators. + + The code travels in the request body, never in a URL. + """ + return self._api.send( + "POST", "/vouchers/lookup", Voucher, body={"code": code}, idempotent=True + ) + + def validate( + self, + code: str, + *, + instance_slug: str | None = None, + license_price_id: str | None = None, + ) -> Validity: + """Run the redemption checks of a code without redeeming it. + + ``valid`` says whether it would apply; otherwise ``reason`` is the first check that + failed, and ``rule`` the eligibility rule. + + Args: + code: The code. + instance_slug: Also run the checks that need the redeeming instance. + license_price_id: The ``FLAT_FEE`` price a subscription would start on, read instead + of the instance's live subscription. + """ + body = { + "code": code, + **compact({"instanceSlug": instance_slug, "licensePriceId": license_price_id}), + } + return self._api.send("POST", "/vouchers/validate", Validity, body=body, idempotent=True) + + +def _voucher( + *, + name: str, + voucher_type: VoucherType, + duration: VoucherDuration, + code: str | None, + description: str | None, + duration_in_periods: int | None, + starts_at: datetime | str | None, + expires_at: datetime | str | None, + max_redemptions: int | None, + restricted_customer_slug: str | None, + price_discount_type: DiscountType | None, + price_discount_value: str | None, + currency: str | None, + price_applies_to: DiscountAppliesTo | None, + applicable_license_ids: Sequence[str] | None, + applicable_license_price_ids: Sequence[str] | None, + applicable_addon_ids: Sequence[str] | None, + applicable_addon_price_ids: Sequence[str] | None, + grants: Sequence[VoucherGrantParam] | None, + flag_grants: Mapping[str, str] | None, + redemption_rules: RedemptionRulesParam | None, +) -> dict[str, Any]: + return { + "name": name, + "voucherType": voucher_type, + "duration": duration, + **compact( + { + "code": code, + "description": description, + "durationInPeriods": duration_in_periods, + "startsAt": optional_datetime(starts_at), + "expiresAt": optional_datetime(expires_at), + "maxRedemptions": max_redemptions, + "restrictedCustomerSlug": restricted_customer_slug, + "priceDiscountType": price_discount_type, + "priceDiscountValue": price_discount_value, + "currency": currency, + "priceAppliesTo": price_applies_to, + "applicableLicenseIds": _listed(applicable_license_ids), + "applicableLicensePriceIds": _listed(applicable_license_price_ids), + "applicableAddonIds": _listed(applicable_addon_ids), + "applicableAddonPriceIds": _listed(applicable_addon_price_ids), + "grants": _listed(grants), + "flagGrants": ( + [ + {"flagSlug": flag, "variant": variant} + for flag, variant in flag_grants.items() + ] + if flag_grants is not None + else None + ), + "redemptionRules": dict(redemption_rules) if redemption_rules is not None else None, + } + ), + } + + +def _listed(values: Sequence[Any] | None) -> list[Any] | None: + return list(values) if values is not None else None diff --git a/src/kaitencloud/_sync/resources/customers.py b/src/kaitencloud/_sync/resources/customers.py index b17eac3..aa39eea 100644 --- a/src/kaitencloud/_sync/resources/customers.py +++ b/src/kaitencloud/_sync/resources/customers.py @@ -35,6 +35,7 @@ def create( external_customer_id: str | None = None, domain: str | None = None, integrations: Mapping[str, IntegrationParam] | None = None, + billing_email: str | None = None, ) -> Customer: """Create a customer. @@ -46,6 +47,8 @@ def create( domain: Its domain name, e.g. ``"acme.com"``. integrations: Links to records in third-party systems, keyed by adapter name. Only settable here: afterwards, use :meth:`update_integration`. + billing_email: Where invoices sent to the customer are delivered, at most 254 + characters. Personal data: never included in event payloads. Raises: ConflictError: The slug is taken, or the organization reached its customer limit. @@ -57,6 +60,7 @@ def create( "externalCustomerId": external_customer_id, "domain": domain, "integrations": integrations_body(integrations), + "billingEmail": billing_email, } ) return self._api.send("POST", "/customers", Customer, body=body) @@ -68,13 +72,22 @@ def update( name: str, external_customer_id: str | None = None, domain: str | None = None, + billing_email: str | None = None, ) -> None: """Replace a customer's details. A full replacement: the external customer id and the domain are cleared when left out. - The slug cannot be changed, and integrations have their own methods. + The billing e-mail is the exception: left out, it keeps its value, and ``""`` removes + it. The slug cannot be changed, and integrations have their own methods. """ - body = compact({"name": name, "externalCustomerId": external_customer_id, "domain": domain}) + body = compact( + { + "name": name, + "externalCustomerId": external_customer_id, + "domain": domain, + "billingEmail": billing_email, + } + ) self._api.send_empty("PUT", api_path("customers", customer_slug), body=body) def delete(self, customer_slug: str) -> None: diff --git a/src/kaitencloud/_sync/resources/instances.py b/src/kaitencloud/_sync/resources/instances.py index ecae47d..d7a838f 100644 --- a/src/kaitencloud/_sync/resources/instances.py +++ b/src/kaitencloud/_sync/resources/instances.py @@ -8,11 +8,15 @@ import builtins from collections.abc import Mapping from datetime import datetime -from typing import Any +from typing import Any, Literal -from ..._exceptions import ThresholdExceededError +from ..._constants import EXPORT_MEDIA_TYPES +from ..._exceptions import ThresholdExceededError, TransactionIdReusedError +from ..._models import UsageReportResult +from ..._types import EntitlementValueInput from ..._utils import ( api_path, + check_limit, compact, format_datetime, integration_body, @@ -21,12 +25,16 @@ ) from ...types import ( AuditTrail, + EntitlementException, EntitlementUsage, + ExceptionKind, Instance, InstanceIntegration, InstanceStatus, IntegrationParam, + ReportedEntitlementUsage, UsageBehavior, + UsageReport, ) from ._base import Resource @@ -239,26 +247,272 @@ def report_usage( ThresholdExceededError: The report would take usage past the maximum the license allows -- the granted value plus its overage allowance. Nothing was recorded. - Never retried automatically: without an idempotency key on the wire, a report retried - after a lost response would be counted twice. + Never retried automatically: a report retried after a lost response would be counted + twice. :meth:`report_usage_detailed` sends one under an idempotency key, which makes + sending it again safe. """ - if isinstance(value, bool) or not isinstance(value, (int, float)): - raise TypeError(f"usage must be a number, got {type(value).__name__}") - body = compact( - { - "value": {"type": "number", "value": value}, - "behavior": behavior, - "metadata": dict(metadata) if metadata is not None else None, - } - ) return self._api.send( "POST", - api_path("instances", instance_slug, "entitlements", entitlement_slug, "usage"), + _usage_path(instance_slug, entitlement_slug), EntitlementUsage, - body=body, + body=_usage_report(value, behavior, metadata), errors={409: ThresholdExceededError}, ) + def report_usage_detailed( + self, + instance_slug: str, + entitlement_slug: str, + value: float, + *, + behavior: UsageBehavior = "append", + metadata: Mapping[str, Any] | None = None, + transaction_id: str | None = None, + ) -> UsageReportResult: + """Report usage, under an idempotency key, and return what the API said about it. + + Kaiten applies a report at most once per key, instance and entitlement within its + idempotency window -- 35 days by default. A report sent again under the same key, with + the same behavior and value, answers the original report and counts nothing: it comes + back with ``replayed`` set. + + Args: + instance_slug: The instance that consumed. + entitlement_slug: The entitlement consumed. + value: The amount, as :meth:`report_usage` takes it. + behavior: ``"append"`` (the default) or ``"set"``. + metadata: Free-form metadata, stored with the report in the usage history when its + compact JSON is at most 4 KiB; above, it is dropped and ``metadata_dropped`` set. + It must hold no personal data. + transaction_id: The id of the business event the report counts -- 1 to 128 of + letters, digits, ``.``, ``_``, ``:`` and ``-``, e.g. ``"llm-call-9f2c:tokens"``. + Sent as the ``Idempotency-Key`` header. + + Raises: + ThresholdExceededError: The report would take usage past the maximum the license + allows. Nothing was recorded. + TransactionIdReusedError: The key was spent on a report with another behavior or + value. Nothing was recorded. + + Never retried automatically, key or not: a Kaiten older than keys ignores the header and + would count a resent report twice. Resend it yourself, under the same key. + """ + try: + response = self._api.request( + "POST", + _usage_path(instance_slug, entitlement_slug), + body=_usage_report(value, behavior, metadata), + headers=compact({"Idempotency-Key": transaction_id}), + errors={409: ThresholdExceededError}, + ) + except ThresholdExceededError as error: + # The usage endpoint answers 409 for two reasons; only the code tells them apart. + if error.code == "ReportEntitlementUsageMetric.TransactionIdReused": + raise TransactionIdReusedError( + str(error), response=error.response, problem=error.problem + ) from None + raise + return UsageReportResult( + usage=self._api.validate( + ReportedEntitlementUsage, self._api.decode(response), response + ), + replayed=response.headers.get("Idempotent-Replayed") == "true", + metadata_dropped=bool(response.headers.get("Kaiten-Metadata-Dropped")), + ) + + def list_usage_reports( + self, + instance_slug: str, + entitlement_slug: str, + *, + from_: datetime | str | None = None, + to: datetime | str | None = None, + transaction_id: str | None = None, + limit: int | None = None, + ) -> builtins.list[UsageReport]: + """Return the usage history of one entitlement for an instance, oldest report first. + + Every report the API accepted in the range, with the counter before and after it and + the limit in force. Decimals are strings, exact to the digit. Reports are kept for the + organization's usage history retention. + + Args: + instance_slug: The instance. + entitlement_slug: The entitlement. + from_: Start of the range, inclusive. Defaults to 30 days before ``to``; a moment + before the usage history starts is refused (``ListUsageReports.OutsideRetention``). + to: End of the range, exclusive. Defaults to now. + transaction_id: Only the report sent with this key. + limit: The most reports to return. Every page is walked when omitted. + """ + return self._api.list_reports( + api_path( + "instances", instance_slug, "entitlements", entitlement_slug, "usage", "reports" + ), + UsageReport, + params={ + "from": optional_datetime(from_), + "to": optional_datetime(to), + "transactionId": transaction_id, + }, + limit=check_limit(limit), + ) + + def export_usage_reports( + self, + instance_slug: str, + entitlement_slug: str, + *, + from_: datetime | str | None = None, + to: datetime | str | None = None, + format: Literal["csv", "json"] = "csv", + ) -> bytes: + """Return the usage history of one entitlement for an instance, exported. + + At most 366 days per call, decimals written exactly as the journal stores them. + + Args: + instance_slug: The instance. + entitlement_slug: The entitlement. + from_: Start of the range, inclusive (see :meth:`list_usage_reports`). + to: End of the range, exclusive. Defaults to now. + format: ``"csv"`` (the default), RFC 4180 with a header row, or ``"json"`` for + NDJSON, one report per line in the shape :meth:`list_usage_reports` returns. + """ + response = self._api.request( + "GET", + api_path( + "instances", + instance_slug, + "entitlements", + entitlement_slug, + "usage", + "reports", + "export", + ), + params={ + "from": optional_datetime(from_), + "to": optional_datetime(to), + "format": format, + }, + headers={"Accept": EXPORT_MEDIA_TYPES[format]}, + ) + return response.content + + def export_organization_usage_reports( + self, + *, + from_: datetime | str | None = None, + to: datetime | str | None = None, + format: Literal["csv", "json"] = "csv", + instance_slug: str | None = None, + instance_id: str | None = None, + entitlement_slug: str | None = None, + entitlement_id: str | None = None, + ) -> bytes: + """Return the whole organization's usage history, exported, ordered by acceptance. + + Every instance and entitlement, at most 31 days per call: keep it before deleting the + organization. ``instance_id`` and ``entitlement_id`` reach deleted ones, whose reports + are kept. + + Args: + from_: Start of the range, inclusive (see :meth:`list_usage_reports`). + to: End of the range, exclusive. Defaults to now. + format: ``"csv"`` (the default) or ``"json"`` for NDJSON. + instance_slug: Only this instance. + instance_id: Only this instance, by id. + entitlement_slug: Only this entitlement. + entitlement_id: Only this entitlement, by id. + """ + response = self._api.request( + "GET", + "/usage/reports/export", + params={ + "from": optional_datetime(from_), + "to": optional_datetime(to), + "format": format, + "instanceSlug": instance_slug, + "instanceId": instance_id, + "entitlementSlug": entitlement_slug, + "entitlementId": entitlement_id, + }, + headers={"Accept": EXPORT_MEDIA_TYPES[format]}, + ) + return response.content + + def list_entitlement_exceptions( + self, instance_slug: str, *, include_inactive: bool = False + ) -> builtins.list[EntitlementException]: + """Return the instance's entitlement exceptions, latest start first, with their status. + + Args: + instance_slug: The instance. + include_inactive: Also return the revoked and ended ones. Only the active and the + scheduled ones otherwise. + """ + return self._api.get_list( + api_path("instances", instance_slug, "entitlement-exceptions"), + EntitlementException, + params={"filter": "all" if include_inactive else None}, + ) + + def create_entitlement_exception( + self, + instance_slug: str, + entitlement_slug: str, + *, + kind: ExceptionKind, + reason: str, + value: EntitlementValueInput | None = None, + starts_at: datetime | str | None = None, + ends_at: datetime | str | None = None, + ) -> EntitlementException: + """Override one entitlement of an instance, for a while, and return the exception. + + An exception is your explicit, dated decision on one instance: the last layer of its + effective entitlements, after the license, the add-ons and the boosts. It wins over + them, and may lower the value. An instance has one unrevoked exception per entitlement + at any moment. It works whether or not billing is enabled. + + Args: + instance_slug: The instance. + entitlement_slug: The entitlement it overrides, granted or not. + kind: ``SET`` works on every type; ``ADD`` and ``UNLIMITED`` on numbers only. + reason: Why, 1 to 500 characters, shown to you only. + value: For ``SET``, a number of 0 or more, a ``bool`` or a mapping, by the + entitlement's type; for ``ADD``, a number above 0; nothing for ``UNLIMITED``. + starts_at: When it starts: now when omitted; a future moment schedules it. + ends_at: When it ends. It never does when omitted. + """ + body = { + "entitlementSlug": entitlement_slug, + "kind": kind, + "reason": reason, + **compact( + { + "value": dict(value) if isinstance(value, Mapping) else value, + "startsAt": optional_datetime(starts_at), + "endsAt": optional_datetime(ends_at), + } + ), + } + return self._api.send( + "POST", + api_path("instances", instance_slug, "entitlement-exceptions"), + EntitlementException, + body=body, + ) + + def revoke_entitlement_exception(self, instance_slug: str, exception_id: str) -> None: + """Revoke an exception, which is kept: a scheduled one never starts, an active one ends. + + Revoking one already revoked or ended changes nothing. + """ + self._api.send_empty( + "DELETE", api_path("instances", instance_slug, "entitlement-exceptions", exception_id) + ) + def get_integration(self, instance_slug: str, integration_name: str) -> InstanceIntegration: """Return the instance's link to the ``integration_name`` adapter.""" return self._api.get( @@ -347,3 +601,21 @@ def _instance( } ), } + + +def _usage_path(instance_slug: str, entitlement_slug: str) -> str: + return api_path("instances", instance_slug, "entitlements", entitlement_slug, "usage") + + +def _usage_report( + value: float, behavior: UsageBehavior, metadata: Mapping[str, Any] | None +) -> dict[str, Any]: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError(f"usage must be a number, got {type(value).__name__}") + return compact( + { + "value": {"type": "number", "value": value}, + "behavior": behavior, + "metadata": dict(metadata) if metadata is not None else None, + } + ) diff --git a/src/kaitencloud/_sync/resources/license_families.py b/src/kaitencloud/_sync/resources/license_families.py index cc2525e..16d6636 100644 --- a/src/kaitencloud/_sync/resources/license_families.py +++ b/src/kaitencloud/_sync/resources/license_families.py @@ -8,8 +8,8 @@ import builtins from typing import Any -from ..._utils import api_path -from ...types import LicenseFamilyView +from ..._utils import api_path, compact +from ...types import LicenseFamilyView, TrialEndWithoutPaymentMethod from ._base import Resource __all__ = ["LicenseFamilies"] @@ -61,3 +61,38 @@ def get( return self._api.get( api_path("license-families", family_slug), LicenseFamilyView, params=params or None ) + + def update( + self, + family_slug: str, + *, + is_public: bool | None = None, + fallback_license_family_slug: str | None = None, + trial_end_without_payment_method: TrialEndWithoutPaymentMethod | None = None, + trial_reminder_days: int | None = None, + ) -> LicenseFamilyView: + """Change a family's own settings, and return the family; what is omitted keeps its value. + + Args: + family_slug: The family's slug. + is_public: List the family's default ``PUBLISHED`` version, with its ``ACTIVE`` + prices, in the public catalog. Families are private until made public. + fallback_license_family_slug: Another family of the organization, every version of + it priced ``FREE``, whose default version an instance moves to when its + subscription ends. ``""`` removes the fallback. + trial_end_without_payment_method: What a trial charged automatically becomes at its + end when the customer has no active payment method: ``CANCEL`` or ``CONVERT``. + trial_reminder_days: How many days before a trial's end ``trial_will_end`` fires, + 1 to 30, for the trials started after the change. + """ + body = compact( + { + "isPublic": is_public, + "fallbackLicenseFamilySlug": fallback_license_family_slug, + "trialEndWithoutPaymentMethod": trial_end_without_payment_method, + "trialReminderDays": trial_reminder_days, + } + ) + return self._api.send( + "PATCH", api_path("license-families", family_slug), LicenseFamilyView, body=body + ) diff --git a/src/kaitencloud/_sync/resources/licenses.py b/src/kaitencloud/_sync/resources/licenses.py index ecbc355..84e1fb8 100644 --- a/src/kaitencloud/_sync/resources/licenses.py +++ b/src/kaitencloud/_sync/resources/licenses.py @@ -10,7 +10,7 @@ from ..._types import EntitlementValueInput from ..._utils import api_path, compact, entitlement_value -from ...types import License, LicenseEntitlement, LicenseType +from ...types import License, LicenseEntitlement, LicenseType, PricingType from ._base import Resource __all__ = ["Licenses"] @@ -47,6 +47,10 @@ def create( family_slug: str | None = None, family_id: str | None = None, lifecycle_state: Literal["DRAFT", "PUBLISHED"] | None = None, + pricing_type: PricingType | None = None, + trial_period_days: int | None = None, + requires_payment_method: bool | None = None, + self_serve_cta_url: str | None = None, ) -> License: """Create a license: a new license family, or the next version of an existing one. @@ -73,6 +77,15 @@ def create( lifecycle_state: ``DRAFT`` to create the version off sale, to be put on sale with :meth:`publish`. ``PUBLISHED`` when omitted. A version is never created ``ARCHIVED``: it is archived with :meth:`archive` once it has been on sale. + pricing_type: How the version is sold: ``FREE`` or ``PAID`` can be bought self-serve, + ``CUSTOM`` sends a buyer to ``self_serve_cta_url`` or a conversation. ``CUSTOM`` + when omitted. + trial_period_days: The trial a subscription to the version starts with by default, + 1 to 365 days. + requires_payment_method: Whether self-serve signup captures a payment method before + activation. + self_serve_cta_url: Where a buyer is sent when the version cannot be bought + self-serve: an http(s) URL. Raises: NotFoundError: ``family_slug`` or ``family_id`` names no family @@ -95,6 +108,10 @@ def create( "familySlug": family_slug, "familyId": family_id, "lifecycleState": lifecycle_state, + "pricingType": pricing_type, + "trialPeriodDays": trial_period_days, + "requiresPaymentMethod": requires_payment_method, + "selfServeCtaUrl": self_serve_cta_url, } ), } @@ -109,6 +126,10 @@ def update( description: str = "", is_default: bool = False, version_name: str | None = None, + pricing_type: PricingType | None = None, + trial_period_days: int | None = None, + requires_payment_method: bool | None = None, + self_serve_cta_url: str | None = None, ) -> None: """Replace a license version's details. @@ -117,6 +138,11 @@ def update( :meth:`publish`, :meth:`archive` and :meth:`unarchive`. ``is_default=True`` takes the flag from the family's previous default, and ``False`` unsets it on this version. + The commercial terms -- ``pricing_type``, ``trial_period_days``, + ``requires_payment_method`` and ``self_serve_cta_url`` -- keep their value when left + out, as :meth:`create` describes them. ``trial_period_days=0`` removes the trial, and + ``self_serve_cta_url=""`` the URL. + Raises: ConflictError: ``is_default=True`` on a version that is not ``PUBLISHED`` (``UpdateLicense.DefaultMustBePublished``): publish or unarchive it first. @@ -126,7 +152,15 @@ def update( "description": description, "type": type, "isDefault": is_default, - **compact({"versionName": version_name}), + **compact( + { + "versionName": version_name, + "pricingType": pricing_type, + "trialPeriodDays": trial_period_days, + "requiresPaymentMethod": requires_payment_method, + "selfServeCtaUrl": self_serve_cta_url, + } + ), } self._api.send_empty("PUT", api_path("licenses", license_slug), body=body) diff --git a/src/kaitencloud/_sync/resources/metadata_fields.py b/src/kaitencloud/_sync/resources/metadata_fields.py index 7cdc36b..6ba6cb9 100644 --- a/src/kaitencloud/_sync/resources/metadata_fields.py +++ b/src/kaitencloud/_sync/resources/metadata_fields.py @@ -23,10 +23,17 @@ class MetadataFields(Resource): against the active fields declared for its resource type. """ - def list(self, resource_type: MetadataResourceType) -> builtins.list[MetadataField]: - """Return the metadata fields declared for ``resource_type``, walking all pages.""" + def list( + self, resource_type: MetadataResourceType, *, include_archived: bool = False + ) -> builtins.list[MetadataField]: + """Return the metadata fields declared for ``resource_type``, walking all pages. + + Archived fields are left out unless ``include_archived`` asks for them. + """ return self._api.list_all( - "/metadata-fields", MetadataField, params={"resourceType": resource_type} + "/metadata-fields", + MetadataField, + params={"resourceType": resource_type, "includeArchived": include_archived or None}, ) def create( diff --git a/src/kaitencloud/_sync/resources/platform/__init__.py b/src/kaitencloud/_sync/resources/platform/__init__.py index 2d43ae9..a225aea 100644 --- a/src/kaitencloud/_sync/resources/platform/__init__.py +++ b/src/kaitencloud/_sync/resources/platform/__init__.py @@ -3,12 +3,14 @@ # Generated by scripts/unasync.py from src/kaitencloud/_async/resources/platform/__init__.py. Do not edit by hand: # change the async source, then run `task generate`. +from .billing import PlatformBilling from .connectors import PlatformConnectors from .organizations import PlatformOrganizations from .tokens import PlatformTokens from .users import PlatformUsers __all__ = [ + "PlatformBilling", "PlatformConnectors", "PlatformOrganizations", "PlatformTokens", diff --git a/src/kaitencloud/_sync/resources/platform/billing.py b/src/kaitencloud/_sync/resources/platform/billing.py new file mode 100644 index 0000000..9436890 --- /dev/null +++ b/src/kaitencloud/_sync/resources/platform/billing.py @@ -0,0 +1,41 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +# Generated by scripts/unasync.py from src/kaitencloud/_async/resources/platform/billing.py. Do not edit by hand: +# change the async source, then run `task generate`. +from __future__ import annotations + +from ...._utils import api_path, compact, validate_uuid +from ....types import ClosePeriodsReport +from .._base import Resource + +__all__ = ["PlatformBilling"] + + +class PlatformBilling(Resource): + """Billing across organizations.""" + + def close_periods( + self, organization_id: str, *, instance_slug: str | None = None + ) -> ClosePeriodsReport: + """Close an organization's subscriptions whose period has ended, now. + + The same as :meth:`Billing.close_periods` on the Core API, for any organization, + and recorded under ``system:kaiten``. It needs ``write:billing``. + + Args: + organization_id: The organization. + instance_slug: Close only this instance's subscription. + """ + return self._api.send( + "POST", + api_path( + "platform", + "organizations", + validate_uuid(organization_id, "organization_id"), + "billing", + "close-periods", + ), + ClosePeriodsReport, + body=compact({"instanceSlug": instance_slug}), + ) diff --git a/src/kaitencloud/_utils.py b/src/kaitencloud/_utils.py index f568008..34e87b2 100644 --- a/src/kaitencloud/_utils.py +++ b/src/kaitencloud/_utils.py @@ -146,6 +146,50 @@ def integrations_body( return {name: dict(integration) for name, integration in integrations.items()} +def price_body( + *, + billing_model: str, + currency: str, + unit_amount_decimal: str, + billing_period: str | None, + billing_timing: str | None, + metered_entitlement_slug: str | None, + is_default: bool | None, + display_label: str | None, + display_order: int | None, +) -> dict[str, Any]: + """The body of a new price: a license version's or an add-on version's.""" + return { + "billingModel": billing_model, + "currency": currency, + "unitAmountDecimal": unit_amount_decimal, + **compact( + { + "billingPeriod": billing_period, + "billingTiming": billing_timing, + "meteredEntitlementSlug": metered_entitlement_slug, + "isDefault": is_default, + "displayLabel": display_label, + "displayOrder": display_order, + } + ), + } + + +def addon_quantities(add_ons: Mapping[str, int] | None) -> list[dict[str, Any]] | None: + """``{addon_slug: quantity}``, as the list of add-ons a billing request carries.""" + if add_ons is None: + return None + return [{"addonSlug": slug, "quantity": quantity} for slug, quantity in add_ons.items()] + + +def check_limit(limit: int | None) -> int | None: + """``limit``, refused below 1: a ceiling of zero rows is a mistake, not a request.""" + if limit is not None and limit < 1: + raise ValueError(f"limit must be at least 1, got {limit}") + return limit + + def validate_uuid(value: str, name: str) -> str: try: return str(uuid.UUID(str(value))) @@ -153,12 +197,29 @@ def validate_uuid(value: str, name: str) -> str: raise ValueError(f"{name} must be a UUID, got {value!r}") from None -def check_token(token: object, surface: Surface) -> str: +PUBLISHABLE_KEY_PREFIX = "pk_" + +# The routes under /public take a credential of their own, chosen by path as the API chooses +# it: a customer session on /public/session, an anonymous checkout's own session on one +# checkout session, and a publishable key on the rest. +_PUBLIC_CREDENTIALS = ( + ( + "/public/session/", + "kst_", + "a customer session (kst_...), from billing.customer_sessions.create()", + ), + ("/public/checkout-sessions/", "ksc_", "the checkout session's own token (ksc_...)"), + ("/public/", PUBLISHABLE_KEY_PREFIX, "a publishable key (pk_...)"), +) + + +def check_token(token: object, surface: Surface, path: str = "") -> str: """Return ``token`` when the API ``surface`` accepts its credential class; raise otherwise. Each Kaiten listener refuses the other's credential with a deliberately uninformative error. Catching the mix-up here, before a request leaves, is the only place a useful - message can come from. + message can come from. A route under ``/public`` takes the one credential its path calls + for, and nothing else. """ if not isinstance(token, str) or not token.strip(): raise CredentialError("The token is empty.") @@ -178,6 +239,14 @@ def check_token(token: object, surface: Surface) -> str: "The Platform API only accepts platform credentials (ksm_...). Organization " "tokens (ksh_...) and identity-provider JWTs belong to KaitenClient." ) + for prefix, required, credential in _PUBLIC_CREDENTIALS: + if path.startswith(prefix): + if not token.startswith(required): + raise CredentialError( + f"{path} takes {credential}, and refuses any other credential. Build a " + "KaitenClient with that token to call it." + ) + break return token diff --git a/src/kaitencloud/_version.py b/src/kaitencloud/_version.py index bfc8daa..d37edb2 100644 --- a/src/kaitencloud/_version.py +++ b/src/kaitencloud/_version.py @@ -1,4 +1,4 @@ # Copyright 2026 KAITEN INC # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/src/kaitencloud/types/__init__.py b/src/kaitencloud/types/__init__.py index 86d1c02..589163c 100644 --- a/src/kaitencloud/types/__init__.py +++ b/src/kaitencloud/types/__init__.py @@ -11,37 +11,123 @@ """ from ._core import ( + ActorKind, + Addon, + AddonDelivered, + AddonEntitlement, + AddonFamily, + AddonGrantUpdateDelivered, + AddonLifecycleState, + AddonPriceUpdateDelivered, + AddonUpdateDelivered, AggregationMethod, + AuditActor, AuditTrail, BasicTargeting, BasicVariant, + BillingCapabilities, + BillingDisabledReason, + BillingFeatures, + BillingHealth, + BillingModel, + BillingPeriod, + BillingProvider, + BillingProviderKind, + BillingSettings, + BillingStartedDelivered, + BillingTiming, BooleanEntitlementValue, + CanceledSubscription, + CancellationChangeDelivered, + CancellationDelivered, + CancellationMode, + CancellationReason, + CheckoutConfirmation, + CheckoutConfirmationStatus, + CheckoutPayment, + CheckoutSession, + CheckoutSessionAdmin, + CheckoutSessionEventStatus, + CheckoutSessionStart, + CheckoutSessionStatus, + CheckoutStartStatus, + CloseBacklog, + ClosedInvoice, + ClosePeriodsReport, + CollectionMethod, + CompatibleLicenseFamilies, + CompletedPaymentMethodSession, Component, + ComponentDelivered, ConfigEntitlementValue, + ConnectionDelivered, Connector, ConnectorActivation, ConnectorSettings, ConnectorSettingsSchema, ConnectorState, + CreatedCustomerSession, Customer, + CustomerBilling, CustomerCreationRejected, + CustomerEventDelivered, + CustomerInProvider, CustomerIntegration, CustomerIntegrationResource, DefaultVariant, + DeletedAddonDelivered, Deployment, DeploymentZone, + DeploymentZoneDelivered, + DiscountAllocation, + DiscountAppliesTo, + DiscountDifference, + DiscountType, + DueDateDifference, + DueToday, Entitlement, + EntitlementDelivered, + EntitlementException, EntitlementGroup, + EntitlementGroupDelivered, EntitlementGroupSummary, EntitlementGroupUsage, + EntitlementSource, EntitlementType, EntitlementUsage, + EntitlementUsageDelivered, EntitlementValue, ErrorDetail, + EventHeaders, + EventVia, + ExceptionEventDelivered, + ExceptionKind, + ExceptionStatus, + ExhaustedEventDelivered, + ExpiredEventDelivered, + ExtraDiscount, FeatureFlag, + FeatureFlagDelivered, FeatureFlagEvaluated, FeatureFlagType, + FlagGrant, + Grant, + GrantModifier, + HandoffAcknowledgementDelivered, + HandoffBacklog, + HandoffClaim, + HandoffStatus, + HeldInvoiceDelivered, + HeldInvoices, + HeldPair, + HoldDetail, + HoldRecord, + IncreaseBilling, Instance, + InstanceAddon, + InstanceAddonUpdateDelivered, + InstanceBilling, + InstanceDelivered, InstanceEntitlementCapExceeded, InstanceEntitlementUsagePeriodRolledOver, InstanceEntitlementUsageReportAccepted, @@ -52,32 +138,156 @@ InstanceLifecycleStageChanged, InstanceStatus, InstanceStatusChanged, + InstanceUpdateDelivered, + Invoice, + InvoiceHandoff, + InvoiceHold, + InvoiceHoldReason, + InvoiceKind, + InvoiceLine, + InvoiceLineDiscount, + InvoiceLineLedger, + InvoiceLineMetering, + InvoiceLineOverage, + InvoiceLineProvider, + InvoiceLineTrueUp, + InvoiceLineType, + InvoicePreview, + InvoiceStatus, + InvoiceSummary, + IssuedInvoiceDelivered, License, + LicenseDelivered, LicenseEntitlement, + LicenseEntitlementDelivered, LicenseEntitlementType, LicenseFamilyView, + LicenseFamilyViewDelivered, LicenseLifecycleState, + LicensePriceEventDelivered, + LicensePriceUpdatedEventDelivered, LicenseType, + LineDifference, + LineReportPage, ManifestEnvelope, ManifestFlag, MetadataField, + MetadataFieldDelivered, MetadataFieldImpact, MetadataFieldImpactSample, MetadataFieldReordered, MetadataResourceType, + MinimumAmount, + MismatchedInvoiceDelivered, NumberEntitlementValue, + Outcome, + OverageLimit, + OverrideBehavior, + PaidInvoiceDelivered, + Payment, + PaymentFailedInvoiceDelivered, + PaymentMethodAttachedDelivered, + PaymentMethodDetachedDelivered, + PaymentMethodExpiringDelivered, + PaymentMethodLabels, + PaymentMethodSession, + PaymentMethodStatus, + PaymentSource, + PaymentStatus, + PeriodRolloverReason, PlainToken, + PlanChangeDelivered, + PlanChangeScheduleDelivered, + PortalCapabilities, + PortalCustomer, + PortalEntitlement, + PortalInstance, + PortalInstanceSummary, + PortalPaymentMethod, + PortalSession, + PortalTrial, + PortalTrialCharge, + PortalTrialPayment, + PortalUpcomingInvoice, + Price, + PriceMeter, + PriceStatus, + PricingType, Problem, + Provenance, + ProvenanceAddon, + ProvenanceBoost, + ProvenanceException, + ProvenanceLicense, + ProvenanceNumber, + ProviderCapabilities, + ProviderChangeDelivered, + ProviderInvoiceStatus, + ProviderLineLimit, + ProviderRecord, + ProviderSync, + ProviderUnavailableReason, + PublicAddOn, + PublicAddOnEntitlement, + PublicBranding, + PublicCatalog, + PublicCatalogCapabilities, + PublicMeter, + PublicPlan, + PublicPlanEntitlement, + PublicPrice, + PublicSurface, + PublishableKey, + PublishableKeyCreated, + PublishableKeyEventDelivered, + PushedInvoiceDelivered, + PushFailedInvoiceDelivered, + PushFailures, + QueuedInvoice, + Reconciliation, + ReconciliationStatus, + Redemption, + RedemptionDelivered, + RedemptionRules, + RedemptionStatus, Release, + ReleaseDelivered, + ReleasedInvoiceDelivered, + ReportedEntitlementUsage, ResetAnchor, ResetPeriod, + RetentionSource, RolloutDateTargeting, RolloutDateVariant, RolloutPercentageTargeting, RolloutPercentageVariant, RolloutStep, + ScheduledChange, Scope, ServiceAccount, + SessionAddon, + SessionAddonSelection, + SessionBoost, + SessionCheckout, + SessionCheckoutStatus, + SessionDiscount, + SessionEventDelivered, + SessionInvoice, + SessionInvoiceStatus, + SessionLine, + SessionPortal, + SessionRedemption, + SessionScheduledChange, + SessionSubscription, + SessionVoucher, + SessionVoucherValidity, + StartedSubscription, + StatusChangeDelivered, + SubscriptionStatus, + SubscriptionStatusChangeReason, + SyncFailureDelivered, + SyncReport, + SyncStatus, SystemTokenIssuance, Targeting, TargetingContext, @@ -88,16 +298,38 @@ TargetingRuleVerdict, Targetings, Token, + TotalsDifference, + TrialCharge, + TrialCustomer, + TrialEndWithoutPaymentMethod, + TrialInstance, + TrialPaymentMethod, + TrialWillEndDelivered, + UncollectibleInvoiceDelivered, UsageBehavior, + UsageLedger, + UsageReport, + UsageReportPage, UsageReportStatus, User, + Validity, Variant, + VoidedInvoiceDelivered, + Voucher, + VoucherDelivered, + VoucherDuration, + VoucherInvalidReason, + VoucherRule, + VoucherStatus, + VoucherType, ) from ._params import ( BasicTargetingParam, BasicVariantParam, DefaultVariantParam, IntegrationParam, + MinimumAmountParam, + RedemptionRulesParam, RolloutDateTargetingParam, RolloutDateVariantParam, RolloutPercentageTargetingParam, @@ -105,6 +337,7 @@ RolloutStepParam, TargetingParam, VariantParam, + VoucherGrantParam, ) from ._platform import ( Organization, @@ -112,40 +345,126 @@ ) __all__ = [ + "ActorKind", + "Addon", + "AddonDelivered", + "AddonEntitlement", + "AddonFamily", + "AddonGrantUpdateDelivered", + "AddonLifecycleState", + "AddonPriceUpdateDelivered", + "AddonUpdateDelivered", "AggregationMethod", + "AuditActor", "AuditTrail", "BasicTargeting", "BasicTargetingParam", "BasicVariant", "BasicVariantParam", + "BillingCapabilities", + "BillingDisabledReason", + "BillingFeatures", + "BillingHealth", + "BillingModel", + "BillingPeriod", + "BillingProvider", + "BillingProviderKind", + "BillingSettings", + "BillingStartedDelivered", + "BillingTiming", "BooleanEntitlementValue", + "CanceledSubscription", + "CancellationChangeDelivered", + "CancellationDelivered", + "CancellationMode", + "CancellationReason", + "CheckoutConfirmation", + "CheckoutConfirmationStatus", + "CheckoutPayment", + "CheckoutSession", + "CheckoutSessionAdmin", + "CheckoutSessionEventStatus", + "CheckoutSessionStart", + "CheckoutSessionStatus", + "CheckoutStartStatus", + "CloseBacklog", + "ClosePeriodsReport", + "ClosedInvoice", + "CollectionMethod", + "CompatibleLicenseFamilies", + "CompletedPaymentMethodSession", "Component", + "ComponentDelivered", "ConfigEntitlementValue", + "ConnectionDelivered", "Connector", "ConnectorActivation", "ConnectorSettings", "ConnectorSettingsSchema", "ConnectorState", + "CreatedCustomerSession", "Customer", + "CustomerBilling", "CustomerCreationRejected", + "CustomerEventDelivered", + "CustomerInProvider", "CustomerIntegration", "CustomerIntegrationResource", "DefaultVariant", "DefaultVariantParam", + "DeletedAddonDelivered", "Deployment", "DeploymentZone", + "DeploymentZoneDelivered", + "DiscountAllocation", + "DiscountAppliesTo", + "DiscountDifference", + "DiscountType", + "DueDateDifference", + "DueToday", "Entitlement", + "EntitlementDelivered", + "EntitlementException", "EntitlementGroup", + "EntitlementGroupDelivered", "EntitlementGroupSummary", "EntitlementGroupUsage", + "EntitlementSource", "EntitlementType", "EntitlementUsage", + "EntitlementUsageDelivered", "EntitlementValue", "ErrorDetail", + "EventHeaders", + "EventVia", + "ExceptionEventDelivered", + "ExceptionKind", + "ExceptionStatus", + "ExhaustedEventDelivered", + "ExpiredEventDelivered", + "ExtraDiscount", "FeatureFlag", + "FeatureFlagDelivered", "FeatureFlagEvaluated", "FeatureFlagType", + "FlagGrant", + "Grant", + "GrantModifier", + "HandoffAcknowledgementDelivered", + "HandoffBacklog", + "HandoffClaim", + "HandoffStatus", + "HeldInvoiceDelivered", + "HeldInvoices", + "HeldPair", + "HoldDetail", + "HoldRecord", + "IncreaseBilling", "Instance", + "InstanceAddon", + "InstanceAddonUpdateDelivered", + "InstanceBilling", + "InstanceDelivered", "InstanceEntitlementCapExceeded", "InstanceEntitlementUsagePeriodRolledOver", "InstanceEntitlementUsageReportAccepted", @@ -156,28 +475,130 @@ "InstanceLifecycleStageChanged", "InstanceStatus", "InstanceStatusChanged", + "InstanceUpdateDelivered", "IntegrationParam", + "Invoice", + "InvoiceHandoff", + "InvoiceHold", + "InvoiceHoldReason", + "InvoiceKind", + "InvoiceLine", + "InvoiceLineDiscount", + "InvoiceLineLedger", + "InvoiceLineMetering", + "InvoiceLineOverage", + "InvoiceLineProvider", + "InvoiceLineTrueUp", + "InvoiceLineType", + "InvoicePreview", + "InvoiceStatus", + "InvoiceSummary", + "IssuedInvoiceDelivered", "License", + "LicenseDelivered", "LicenseEntitlement", + "LicenseEntitlementDelivered", "LicenseEntitlementType", "LicenseFamilyView", + "LicenseFamilyViewDelivered", "LicenseLifecycleState", + "LicensePriceEventDelivered", + "LicensePriceUpdatedEventDelivered", "LicenseType", + "LineDifference", + "LineReportPage", "ManifestEnvelope", "ManifestFlag", "MetadataField", + "MetadataFieldDelivered", "MetadataFieldImpact", "MetadataFieldImpactSample", "MetadataFieldReordered", "MetadataResourceType", + "MinimumAmount", + "MinimumAmountParam", + "MismatchedInvoiceDelivered", "NumberEntitlementValue", "Organization", + "Outcome", + "OverageLimit", + "OverrideBehavior", + "PaidInvoiceDelivered", + "Payment", + "PaymentFailedInvoiceDelivered", + "PaymentMethodAttachedDelivered", + "PaymentMethodDetachedDelivered", + "PaymentMethodExpiringDelivered", + "PaymentMethodLabels", + "PaymentMethodSession", + "PaymentMethodStatus", + "PaymentSource", + "PaymentStatus", + "PeriodRolloverReason", "PlainToken", + "PlanChangeDelivered", + "PlanChangeScheduleDelivered", "PlatformCredential", + "PortalCapabilities", + "PortalCustomer", + "PortalEntitlement", + "PortalInstance", + "PortalInstanceSummary", + "PortalPaymentMethod", + "PortalSession", + "PortalTrial", + "PortalTrialCharge", + "PortalTrialPayment", + "PortalUpcomingInvoice", + "Price", + "PriceMeter", + "PriceStatus", + "PricingType", "Problem", + "Provenance", + "ProvenanceAddon", + "ProvenanceBoost", + "ProvenanceException", + "ProvenanceLicense", + "ProvenanceNumber", + "ProviderCapabilities", + "ProviderChangeDelivered", + "ProviderInvoiceStatus", + "ProviderLineLimit", + "ProviderRecord", + "ProviderSync", + "ProviderUnavailableReason", + "PublicAddOn", + "PublicAddOnEntitlement", + "PublicBranding", + "PublicCatalog", + "PublicCatalogCapabilities", + "PublicMeter", + "PublicPlan", + "PublicPlanEntitlement", + "PublicPrice", + "PublicSurface", + "PublishableKey", + "PublishableKeyCreated", + "PublishableKeyEventDelivered", + "PushFailedInvoiceDelivered", + "PushFailures", + "PushedInvoiceDelivered", + "QueuedInvoice", + "Reconciliation", + "ReconciliationStatus", + "Redemption", + "RedemptionDelivered", + "RedemptionRules", + "RedemptionRulesParam", + "RedemptionStatus", "Release", + "ReleaseDelivered", + "ReleasedInvoiceDelivered", + "ReportedEntitlementUsage", "ResetAnchor", "ResetPeriod", + "RetentionSource", "RolloutDateTargeting", "RolloutDateTargetingParam", "RolloutDateVariant", @@ -188,8 +609,32 @@ "RolloutPercentageVariantParam", "RolloutStep", "RolloutStepParam", + "ScheduledChange", "Scope", "ServiceAccount", + "SessionAddon", + "SessionAddonSelection", + "SessionBoost", + "SessionCheckout", + "SessionCheckoutStatus", + "SessionDiscount", + "SessionEventDelivered", + "SessionInvoice", + "SessionInvoiceStatus", + "SessionLine", + "SessionPortal", + "SessionRedemption", + "SessionScheduledChange", + "SessionSubscription", + "SessionVoucher", + "SessionVoucherValidity", + "StartedSubscription", + "StatusChangeDelivered", + "SubscriptionStatus", + "SubscriptionStatusChangeReason", + "SyncFailureDelivered", + "SyncReport", + "SyncStatus", "SystemTokenIssuance", "Targeting", "TargetingContext", @@ -201,9 +646,30 @@ "TargetingRuleVerdict", "Targetings", "Token", + "TotalsDifference", + "TrialCharge", + "TrialCustomer", + "TrialEndWithoutPaymentMethod", + "TrialInstance", + "TrialPaymentMethod", + "TrialWillEndDelivered", + "UncollectibleInvoiceDelivered", "UsageBehavior", + "UsageLedger", + "UsageReport", + "UsageReportPage", "UsageReportStatus", "User", + "Validity", "Variant", "VariantParam", + "VoidedInvoiceDelivered", + "Voucher", + "VoucherDelivered", + "VoucherDuration", + "VoucherGrantParam", + "VoucherInvalidReason", + "VoucherRule", + "VoucherStatus", + "VoucherType", ] diff --git a/src/kaitencloud/types/_core.py b/src/kaitencloud/types/_core.py index 833b070..ac66228 100644 --- a/src/kaitencloud/types/_core.py +++ b/src/kaitencloud/types/_core.py @@ -14,37 +14,123 @@ from .._models import KaitenModel __all__ = [ + "ActorKind", + "Addon", + "AddonDelivered", + "AddonEntitlement", + "AddonFamily", + "AddonGrantUpdateDelivered", + "AddonLifecycleState", + "AddonPriceUpdateDelivered", + "AddonUpdateDelivered", "AggregationMethod", + "AuditActor", "AuditTrail", "BasicTargeting", "BasicVariant", + "BillingCapabilities", + "BillingDisabledReason", + "BillingFeatures", + "BillingHealth", + "BillingModel", + "BillingPeriod", + "BillingProvider", + "BillingProviderKind", + "BillingSettings", + "BillingStartedDelivered", + "BillingTiming", "BooleanEntitlementValue", + "CanceledSubscription", + "CancellationChangeDelivered", + "CancellationDelivered", + "CancellationMode", + "CancellationReason", + "CheckoutConfirmation", + "CheckoutConfirmationStatus", + "CheckoutPayment", + "CheckoutSession", + "CheckoutSessionAdmin", + "CheckoutSessionEventStatus", + "CheckoutSessionStart", + "CheckoutSessionStatus", + "CheckoutStartStatus", + "CloseBacklog", + "ClosePeriodsReport", + "ClosedInvoice", + "CollectionMethod", + "CompatibleLicenseFamilies", + "CompletedPaymentMethodSession", "Component", + "ComponentDelivered", "ConfigEntitlementValue", + "ConnectionDelivered", "Connector", "ConnectorActivation", "ConnectorSettings", "ConnectorSettingsSchema", "ConnectorState", + "CreatedCustomerSession", "Customer", + "CustomerBilling", "CustomerCreationRejected", + "CustomerEventDelivered", + "CustomerInProvider", "CustomerIntegration", "CustomerIntegrationResource", "DefaultVariant", + "DeletedAddonDelivered", "Deployment", "DeploymentZone", + "DeploymentZoneDelivered", + "DiscountAllocation", + "DiscountAppliesTo", + "DiscountDifference", + "DiscountType", + "DueDateDifference", + "DueToday", "Entitlement", + "EntitlementDelivered", + "EntitlementException", "EntitlementGroup", + "EntitlementGroupDelivered", "EntitlementGroupSummary", "EntitlementGroupUsage", + "EntitlementSource", "EntitlementType", "EntitlementUsage", + "EntitlementUsageDelivered", "EntitlementValue", "ErrorDetail", + "EventHeaders", + "EventVia", + "ExceptionEventDelivered", + "ExceptionKind", + "ExceptionStatus", + "ExhaustedEventDelivered", + "ExpiredEventDelivered", + "ExtraDiscount", "FeatureFlag", + "FeatureFlagDelivered", "FeatureFlagEvaluated", "FeatureFlagType", + "FlagGrant", + "Grant", + "GrantModifier", + "HandoffAcknowledgementDelivered", + "HandoffBacklog", + "HandoffClaim", + "HandoffStatus", + "HeldInvoiceDelivered", + "HeldInvoices", + "HeldPair", + "HoldDetail", + "HoldRecord", + "IncreaseBilling", "Instance", + "InstanceAddon", + "InstanceAddonUpdateDelivered", + "InstanceBilling", + "InstanceDelivered", "InstanceEntitlementCapExceeded", "InstanceEntitlementUsagePeriodRolledOver", "InstanceEntitlementUsageReportAccepted", @@ -55,32 +141,156 @@ "InstanceLifecycleStageChanged", "InstanceStatus", "InstanceStatusChanged", + "InstanceUpdateDelivered", + "Invoice", + "InvoiceHandoff", + "InvoiceHold", + "InvoiceHoldReason", + "InvoiceKind", + "InvoiceLine", + "InvoiceLineDiscount", + "InvoiceLineLedger", + "InvoiceLineMetering", + "InvoiceLineOverage", + "InvoiceLineProvider", + "InvoiceLineTrueUp", + "InvoiceLineType", + "InvoicePreview", + "InvoiceStatus", + "InvoiceSummary", + "IssuedInvoiceDelivered", "License", + "LicenseDelivered", "LicenseEntitlement", + "LicenseEntitlementDelivered", "LicenseEntitlementType", "LicenseFamilyView", + "LicenseFamilyViewDelivered", "LicenseLifecycleState", + "LicensePriceEventDelivered", + "LicensePriceUpdatedEventDelivered", "LicenseType", + "LineDifference", + "LineReportPage", "ManifestEnvelope", "ManifestFlag", "MetadataField", + "MetadataFieldDelivered", "MetadataFieldImpact", "MetadataFieldImpactSample", "MetadataFieldReordered", "MetadataResourceType", + "MinimumAmount", + "MismatchedInvoiceDelivered", "NumberEntitlementValue", + "Outcome", + "OverageLimit", + "OverrideBehavior", + "PaidInvoiceDelivered", + "Payment", + "PaymentFailedInvoiceDelivered", + "PaymentMethodAttachedDelivered", + "PaymentMethodDetachedDelivered", + "PaymentMethodExpiringDelivered", + "PaymentMethodLabels", + "PaymentMethodSession", + "PaymentMethodStatus", + "PaymentSource", + "PaymentStatus", + "PeriodRolloverReason", "PlainToken", + "PlanChangeDelivered", + "PlanChangeScheduleDelivered", + "PortalCapabilities", + "PortalCustomer", + "PortalEntitlement", + "PortalInstance", + "PortalInstanceSummary", + "PortalPaymentMethod", + "PortalSession", + "PortalTrial", + "PortalTrialCharge", + "PortalTrialPayment", + "PortalUpcomingInvoice", + "Price", + "PriceMeter", + "PriceStatus", + "PricingType", "Problem", + "Provenance", + "ProvenanceAddon", + "ProvenanceBoost", + "ProvenanceException", + "ProvenanceLicense", + "ProvenanceNumber", + "ProviderCapabilities", + "ProviderChangeDelivered", + "ProviderInvoiceStatus", + "ProviderLineLimit", + "ProviderRecord", + "ProviderSync", + "ProviderUnavailableReason", + "PublicAddOn", + "PublicAddOnEntitlement", + "PublicBranding", + "PublicCatalog", + "PublicCatalogCapabilities", + "PublicMeter", + "PublicPlan", + "PublicPlanEntitlement", + "PublicPrice", + "PublicSurface", + "PublishableKey", + "PublishableKeyCreated", + "PublishableKeyEventDelivered", + "PushFailedInvoiceDelivered", + "PushFailures", + "PushedInvoiceDelivered", + "QueuedInvoice", + "Reconciliation", + "ReconciliationStatus", + "Redemption", + "RedemptionDelivered", + "RedemptionRules", + "RedemptionStatus", "Release", + "ReleaseDelivered", + "ReleasedInvoiceDelivered", + "ReportedEntitlementUsage", "ResetAnchor", "ResetPeriod", + "RetentionSource", "RolloutDateTargeting", "RolloutDateVariant", "RolloutPercentageTargeting", "RolloutPercentageVariant", "RolloutStep", + "ScheduledChange", "Scope", "ServiceAccount", + "SessionAddon", + "SessionAddonSelection", + "SessionBoost", + "SessionCheckout", + "SessionCheckoutStatus", + "SessionDiscount", + "SessionEventDelivered", + "SessionInvoice", + "SessionInvoiceStatus", + "SessionLine", + "SessionPortal", + "SessionRedemption", + "SessionScheduledChange", + "SessionSubscription", + "SessionVoucher", + "SessionVoucherValidity", + "StartedSubscription", + "StatusChangeDelivered", + "SubscriptionStatus", + "SubscriptionStatusChangeReason", + "SyncFailureDelivered", + "SyncReport", + "SyncStatus", "SystemTokenIssuance", "Targeting", "TargetingContext", @@ -91,25 +301,148 @@ "TargetingRuleVerdict", "Targetings", "Token", + "TotalsDifference", + "TrialCharge", + "TrialCustomer", + "TrialEndWithoutPaymentMethod", + "TrialInstance", + "TrialPaymentMethod", + "TrialWillEndDelivered", + "UncollectibleInvoiceDelivered", "UsageBehavior", + "UsageLedger", + "UsageReport", + "UsageReportPage", "UsageReportStatus", "User", + "Validity", "Variant", + "VoidedInvoiceDelivered", + "Voucher", + "VoucherDelivered", + "VoucherDuration", + "VoucherInvalidReason", + "VoucherRule", + "VoucherStatus", + "VoucherType", ] +ActorKind = Literal["USER", "SERVICE", "SYSTEM"] +"""Who acted: a person, a service account, or Kaiten itself.""" + +AddonLifecycleState = Literal["DRAFT", "PUBLISHED", "ARCHIVED"] +"""Whether an add-on version may be sold: ``DRAFT`` before it goes on sale, ``PUBLISHED`` while +it is on sale, ``ARCHIVED`` once it has been withdrawn. +""" + AggregationMethod = Literal["COUNT", "SUM", "AVERAGE", "MIN", "MAX", "LATEST"] """How each reported usage value folds into an entitlement's stored total.""" +BillingDisabledReason = Literal["DEPLOYMENT_DISABLED", "NOT_ENTITLED"] +"""Why billing is off: the deployment disabled it, or the plan does not include it.""" + +BillingModel = Literal["FLAT_FEE", "USAGE_BASED", "OVERAGE"] +"""How a price bills: ``FLAT_FEE`` per period, ``USAGE_BASED`` per sale unit of usage, +``OVERAGE`` per sale unit above the license's limit. +""" + +BillingPeriod = Literal["MONTHLY", "QUARTERLY", "SEMI_ANNUAL", "ANNUAL"] +"""How often a subscription bills its flat fees.""" + +BillingProviderKind = Literal["NOOP", "STRIPE"] +"""Who issues and collects an invoice: ``NOOP``, the organization itself through the handoff +queue, or ``STRIPE``. +""" + +BillingTiming = Literal["ADVANCE", "ARREARS"] +"""When a price bills: in ``ADVANCE`` of its period, or in ``ARREARS`` after it.""" + +CancellationMode = Literal["AT_PERIOD_END", "IMMEDIATE"] +"""When a cancellation takes effect: at the end of the period paid for, or now.""" + +CancellationReason = Literal["TRIAL_ENDED_WITHOUT_PAYMENT_METHOD", "INCOMPLETE_EXPIRED"] +"""Why Kaiten canceled a subscription on its own.""" + +CheckoutConfirmationStatus = Literal["requires_payment_method", "awaiting_payment", "completed"] +"""Where an anonymous checkout stands after a confirmation.""" + +CheckoutSessionEventStatus = Literal["OPEN", "COMPLETED", "ABANDONED", "EXPIRED"] +"""The status of an anonymous checkout session, as its events report it.""" + +CheckoutSessionStatus = Literal["OPEN", "AWAITING_PAYMENT", "COMPLETED", "ABANDONED", "EXPIRED"] +"""The status of an anonymous checkout session.""" + +CheckoutStartStatus = Literal["preview", "open"] +"""What starting an anonymous checkout did: ``preview`` for a dry run, ``open`` otherwise.""" + +CollectionMethod = Literal["SEND_INVOICE", "CHARGE_AUTOMATICALLY"] +"""How an invoice is collected: ``SEND_INVOICE`` sends it for payment, ``CHARGE_AUTOMATICALLY`` +charges the customer's payment method. +""" + +DiscountAppliesTo = Literal["LICENSE_BASE", "ADDONS", "BOTH", "SELECTED_PRICES"] +"""What a price voucher discounts.""" + +DiscountType = Literal["PERCENTAGE", "FIXED_AMOUNT"] +"""How a discount is expressed: a percentage, or a fixed amount in minor units.""" + +EntitlementSource = Literal["license", "addon", "exception"] +"""Where an instance's entitlement comes from: its ``license``, an ``addon``, or an +``exception`` alone. +""" + EntitlementType = Literal["BOOLEAN", "NUMBER", "CONFIG", "NUMBER_AI_CREDIT"] """The value type of an entitlement in the catalog.""" +EventVia = Literal["customer_session", "checkout_session"] +"""What wrote the change an event reports, when it was not an organization credential: a +customer session, or an anonymous checkout. +""" + +ExceptionKind = Literal["SET", "ADD", "UNLIMITED"] +"""How an entitlement exception overrides a value: ``SET`` it, ``ADD`` to it, or lift it +(``UNLIMITED``). +""" + +ExceptionStatus = Literal["SCHEDULED", "ACTIVE", "EXPIRED", "REVOKED"] +"""Where an entitlement exception stands.""" + FeatureFlagType = Literal["boolean", "string", "number", "object"] """The value type of a feature flag's variants.""" +GrantModifier = Literal["SET", "ADD", "MULTIPLY", "UNLIMITED"] +"""How a voucher grant changes an entitlement: ``SET``, ``ADD``, ``MULTIPLY`` or ``UNLIMITED``.""" + +HandoffStatus = Literal["NOT_REQUIRED", "PENDING", "ACKNOWLEDGED"] +"""Where an invoice stands in the accounting handoff.""" + +IncreaseBilling = Literal["TRUE_UP", "NEXT_BOUNDARY"] +"""How a mid-period increase of an add-on billed in advance is billed: a ``TRUE_UP`` line, or +from the ``NEXT_BOUNDARY``. +""" + InstanceStatus = Literal["HEALTHY", "DEGRADED", "INCIDENT", "MAINTENANCE"] """The operational status of an instance.""" +InvoiceHoldReason = Literal[ + "LEDGER_SEQUENCE_GAP", "LEDGER_CHAIN_BREAK", "LEDGER_COUNTER_MISMATCH", "PROVIDER_ITEM_LIMIT" +] +"""Why an invoice is held as a draft.""" + +InvoiceKind = Literal["ACTIVATION", "RENEWAL", "FINAL"] +"""What an invoice bills: a subscription's ``ACTIVATION``, a ``RENEWAL``, or its ``FINAL`` +period. +""" + +InvoiceLineType = Literal["BASE", "ADDON", "USAGE", "OVERAGE", "DISCOUNT"] +"""What an invoice line bills.""" + +InvoiceStatus = Literal[ + "DRAFT", "PUSHED", "PUSH_FAILED", "MANUAL", "PAID", "PAYMENT_FAILED", "UNCOLLECTIBLE", "VOID" +] +"""Where an invoice stands, from ``DRAFT`` to ``PAID``, ``UNCOLLECTIBLE`` or ``VOID``.""" + LicenseEntitlementType = Literal["BOOLEAN", "NUMBER", "CONFIG"] """The value type of an entitlement granted by a license.""" @@ -124,14 +457,55 @@ MetadataResourceType = Literal["DEPLOYMENT_ZONE", "INSTANCE"] """The resource a metadata field applies to.""" -ResetAnchor = Literal["CALENDAR", "LICENSE_START"] +OverrideBehavior = Literal["ADD", "OVERRIDE", "MAX"] +"""How an add-on's number grant combines with the license's: ``ADD``, ``OVERRIDE`` or ``MAX``.""" + +PaymentMethodStatus = Literal["ACTIVE", "EXPIRED", "FAILED"] +"""Whether a saved payment method can be charged.""" + +PaymentSource = Literal["MARK_PAID", "PROVIDER", "ZERO_TOTAL"] +"""What recorded an invoice as paid.""" + +PaymentStatus = Literal["paid", "requires_action", "failed", "processing", "not_required"] +"""How a checkout's first payment went.""" + +PeriodRolloverReason = Literal["PERIOD_ENDED", "ANCHOR_CHANGED"] +"""Why a usage window closed: its period ended, or its anchor changed.""" + +PriceStatus = Literal["ACTIVE", "DEPRECATED"] +"""Whether a price is ``ACTIVE``, or ``DEPRECATED``: still billing the subscriptions on it, and +no new one. +""" + +PricingType = Literal["FREE", "PAID", "CUSTOM"] +"""How a version is sold: ``FREE`` or ``PAID`` self-serve, or ``CUSTOM``, through a conversation.""" + +ProviderInvoiceStatus = Literal["draft", "open", "paid", "uncollectible", "void"] +"""An invoice's status at its payment provider.""" + +ProviderUnavailableReason = Literal["NOT_ENTITLED", "VAULT_NOT_CONFIGURED"] +"""Why a payment provider cannot be connected.""" + +ReconciliationStatus = Literal["MATCHED", "MISMATCH"] +"""Whether an invoice matches its copy at the payment provider.""" + +RedemptionStatus = Literal["ACTIVE", "EXPIRED", "REVOKED"] +"""Where a voucher redemption stands.""" + +ResetAnchor = Literal["CALENDAR", "LICENSE_START", "BILLING_PERIOD"] """What a periodic usage window is aligned on.""" ResetPeriod = Literal["HOUR", "DAY", "WEEK", "MONTH", "YEAR"] """How often a periodic entitlement's usage resets.""" +RetentionSource = Literal["KBK", "ENV", "UNKNOWN"] +"""Where the usage history's retention period comes from.""" + Scope = Literal[ + "read:addons", + "read:billing", "read:components", + "read:customer_sessions", "read:customers", "read:deployment_zones", "read:entitlements", @@ -141,10 +515,18 @@ "read:metadata_fields", "read:notifications", "read:organizations", + "read:profile", + "read:publishable_keys", "read:releases", "read:tokens", + "read:users", + "read:voucher_redemptions", + "read:vouchers", "read:webhooks", + "write:addons", + "write:billing", "write:components", + "write:customer_sessions", "write:customers", "write:deployment_zones", "write:entitlements", @@ -154,27 +536,189 @@ "write:metadata_fields", "write:notifications", "write:organizations", + "write:profile", + "write:publishable_keys", "write:releases", "write:tokens", + "write:users", + "write:voucher_redemptions", + "write:vouchers", "write:webhooks", ] """A permission an organization credential can carry, ``read:`` or ``write:``; ``write`` implies ``read`` on the same resource. """ +SessionCheckoutStatus = Literal["preview", "requires_payment_method", "subscribed"] +"""What a customer session's checkout did.""" + +SessionInvoiceStatus = Literal[ + "MANUAL", "PUSHED", "PAID", "PAYMENT_FAILED", "UNCOLLECTIBLE", "VOID" +] +"""An issued invoice's status, as the customer's portal shows it.""" + +SubscriptionStatus = Literal["INCOMPLETE", "TRIAL", "ACTIVE", "PAST_DUE", "CANCELED"] +"""Where a subscription stands.""" + +SubscriptionStatusChangeReason = Literal[ + "TRIAL_ENDED", "INVOICE_OVERDUE", "PAYMENT_FAILED", "SETTLED" +] +"""Why a subscription's status changed.""" + +SyncStatus = Literal["SUCCESS", "PARTIAL", "FAILED"] +"""How a sync with a payment provider went.""" + TargetingFactType = Literal["string", "number", "boolean", "object", "map", "dyn"] """The shape of a fact a targeting rule can read.""" +TrialEndWithoutPaymentMethod = Literal["CANCEL", "CONVERT"] +"""What a trial charged automatically becomes at its end without a payment method: ``CANCEL`` +or ``CONVERT``. +""" + UsageBehavior = Literal["append", "set"] """How a usage report applies: ``append`` folds the value in, ``set`` overwrites it.""" UsageReportStatus = Literal["ACCEPTED", "REJECTED"] """Whether a usage report was persisted.""" +VoucherDuration = Literal["ONE_TIME", "REPEATING", "FOREVER"] +"""How long a price voucher's discount lasts.""" + +VoucherInvalidReason = Literal[ + "NOT_FOUND", + "NOT_ACTIVE", + "NOT_YET_VALID", + "EXPIRED", + "EXHAUSTED", + "ALREADY_REDEEMED", + "NOT_ELIGIBLE", + "CURRENCY_MISMATCH", + "DISCOUNT_LIMIT_EXCEEDED", +] +"""Why a voucher code would not apply.""" + +VoucherRule = Literal[ + "RESTRICTED_CUSTOMER", + "LICENSE_NOT_APPLICABLE", + "ADDON_NOT_APPLICABLE", + "FIRST_TIME_ONLY", + "ANNUAL_ONLY", + "MINIMUM_SUBSCRIPTION_AMOUNT", + "NOTHING_TO_BOOST", +] +"""The redemption rule a voucher code failed.""" + +VoucherStatus = Literal["DRAFT", "ACTIVE", "EXPIRED", "EXHAUSTED", "ARCHIVED"] +"""Where a voucher stands.""" + +VoucherType = Literal["PRICE", "ENTITLEMENT_BOOST", "FLAG_GRANT", "COMPOSITE"] +"""What a voucher gives: a price discount, an entitlement boost, flag grants, or several.""" + + +class Addon(KaitenModel): + created_at: datetime = Field(alias="createdAt") + + description: str + + family_slug: str = Field(alias="familySlug") + """The family the version belongs to""" + + id: str + + increase_billing: IncreaseBilling | str = Field(alias="increaseBilling") + """The family's: how a mid-period increase or attachment of an ADVANCE add-on is billed. + TRUE_UP adds a true-up line for the elapsed period at the next renewal; NEXT_BOUNDARY + bills from the next boundary only + """ + + is_default: bool = Field(alias="isDefault") + """The version the family resolves to. Only a PUBLISHED version.""" + + lifecycle_state: AddonLifecycleState | str = Field(alias="lifecycleState") + """A DRAFT can be attached to try it; an ARCHIVED version stays on the instances holding it + but cannot be attached again. + """ + + max_quantity: int | None = Field(alias="maxQuantity") + """The largest quantity an instance can hold; unbounded when null""" + + name: str + + pricing_type: PricingType | str = Field(alias="pricingType") + + slug: str + """The version's slug, unique in the organization""" + + updated_at: datetime = Field(alias="updatedAt") + + version: int + """The version number in its family, assigned on create""" + + version_name: str = Field(alias="versionName") + + +class AddonEntitlement(KaitenModel): + entitlement_slug: str = Field(alias="entitlementSlug") + + entitlement_type: EntitlementType | str = Field(alias="entitlementType") + + id: str + + limit_cap_exceeded_overage_percent: int | None = Field(alias="limitCapExceededOveragePercent") + """The overage the add-on allows; null inherits the licence grant's""" + + override_behavior: OverrideBehavior | str = Field(alias="overrideBehavior") + """How a number grant combines with the licence's: ADD sums value × quantity, OVERRIDE + replaces it with the latest attached add-on's, MAX keeps the larger. A boolean always + ORs, a config always overrides. + """ + + value: dict[str, Any] + """{type, value}, shaped like a licence grant. A number counts once per unit of quantity.""" + + +class AddonFamily(KaitenModel): + current_version: Addon | None = Field(alias="currentVersion") + """The default version, else the highest PUBLISHED one; null when none is published""" + + id: str + + increase_billing: IncreaseBilling | str = Field(alias="increaseBilling") + """How a mid-period increase or attachment of the family's ADVANCE add-ons is billed (V31-70)""" + + is_public: bool = Field(alias="isPublic") + """Whether the family is listed in the public catalogue""" + + last_version: int = Field(alias="lastVersion") + """The highest version number the family has assigned""" + + slug: str + + versions: list[Addon] | None + """Every version, newest first""" + + +class AuditActor(KaitenModel): + id: str + + kind: ActorKind | str + """USER: a person. SERVICE: a service account. SYSTEM: Kaiten itself -- jobs, and the + anonymous checkouts no one of the organization makes + """ + + name: str + """The user's name; "Kaiten (system)" for the work Kaiten does itself""" + class AuditTrail(KaitenModel): """One entry of an instance's audit trail.""" + actor: AuditActor | None + """Who the event is recorded under; null for an entry recorded before actors were (E-27), + or whose user is gone + """ + event_name: str = Field(alias="eventName") """Event name (e.g. ENTITLEMENT_VALUE_GET)""" @@ -218,703 +762,650 @@ class BasicVariant(KaitenModel): value: str -class BooleanEntitlementValue(KaitenModel): - """A boolean entitlement value: whether a feature is included.""" +class BillingCapabilities(KaitenModel): + disabled_reason: BillingDisabledReason | str | None = Field(alias="disabledReason") + """Why they do not: billing is off on this deployment, or the organization's plan does not + include it + """ - type: Literal["boolean"] - """Value type discriminator""" + enabled: bool + """Whether the billing routes answer for this organization""" - value: bool - """Boolean value""" + features: BillingFeatures + """Which parts of billing this release ships""" + providers: list[BillingProvider] + """Who can collect invoices""" -class Component(KaitenModel): - """A versioned building block of your product, that releases are made of.""" + public_surface: PublicSurface = Field(alias="publicSurface") - created_at: datetime = Field(alias="createdAt") - """Timestamp when the component was created""" + usage_history_retention_months: int | None = Field(alias="usageHistoryRetentionMonths") + """How long usage reports are kept, in months; null when they are kept forever or it cannot + be told + """ - created_by: User = Field(alias="createdBy") - """User who created this component""" + usage_history_retention_source: RetentionSource | str = Field( + alias="usageHistoryRetentionSource" + ) + """Where the retention comes from: KBK, the organization's licence or an add-on; ENV, the + deployment's KAITEN_USAGE_LEDGER_RETENTION_MONTHS; UNKNOWN, the licensing authority + could not tell. A null usageHistoryRetentionMonths reads as kept forever with KBK or + ENV, and as unknown with UNKNOWN (V31-11). + """ - description: str | None = None - """Description of the component""" + usage_idempotency_window_days: int = Field(alias="usageIdempotencyWindowDays") + """How long a usage report's transactionId is remembered, in days""" - id: str - """Unique identifier for the component""" - name: str - """Name of the component""" +class BillingFeatures(KaitenModel): + addons: bool - previous_component_id: str | None = Field(default=None, alias="previousComponentId") - """Reference to the previous version of this component""" + charge_automatically: bool = Field(alias="chargeAutomatically") - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + lifecycle: bool + """Cancellation, reactivation, plan changes and overdue tracking""" - version: str - """Version of the component""" + public_surface: bool = Field(alias="publicSurface") + stripe: bool -class ConfigEntitlementValue(KaitenModel): - """A configuration entitlement value: a free-form object.""" + trials: bool - type: Literal["object"] - """Value type discriminator""" + vouchers: bool - value: dict[str, Any] - """Object configuration value""" +class BillingHealth(KaitenModel): + close_backlog: CloseBacklog = Field(alias="closeBacklog") + """Subscriptions whose period ended more than KAITEN_BILLING_CLOSE_GRACE ago and has not + closed yet + """ -class Connector(KaitenModel): - """A connector registered with this Kaiten deployment.""" + handoff: HandoffBacklog - created_at: datetime - """Timestamp when connector was first registered""" + held_invoices: HeldInvoices = Field(alias="heldInvoices") - entitlement_slug: str | None = None - """Slug of the BOOLEAN entitlement a license must grant for an organization to activate - this connector. Absent means the connector is ungated. - """ + overdue_invoices: int = Field(alias="overdueInvoices") - name: str - """Stable connector identifier""" + past_due_subscriptions: int = Field(alias="pastDueSubscriptions") - settings_schema: dict[str, Any] - """JSON schema used to validate connector settings payloads""" + payment_methods_expiring30d: int = Field(alias="paymentMethodsExpiring30d") + """ACTIVE payment methods whose card expires in the next 30 days""" - updated_at: datetime - """Timestamp when connector registration was last updated""" + provider_sync: list[ProviderSync] = Field(alias="providerSync") - version: str - """Registered connector version""" + push_failures: PushFailures = Field(alias="pushFailures") + """Invoices whose push failed at least as many times as the alert threshold""" + reconciliation_mismatches30d: int = Field(alias="reconciliationMismatches30d") + """Invoices whose provider's amounts differed from Kaiten's, in the last 30 days""" -class ConnectorActivation(KaitenModel): - """An organization's activation of a connector.""" + usage_ledger: UsageLedger = Field(alias="usageLedger") - activated_at: datetime = Field(alias="activatedAt") - """When this organization first activated the connector""" - connector_name: str = Field(alias="connectorName") - """Stable connector identifier""" +class BillingProvider(KaitenModel): + available: bool + """Whether the organization may connect it on this deployment""" + capabilities: ProviderCapabilities -class ConnectorSettings(KaitenModel): - """An organization's settings for a connector.""" + connected: bool + """Whether it is set up for the organization; NOOP always is""" - connector_name: str - """Stable connector identifier""" + kind: BillingProviderKind | str - settings: dict[str, Any] - """Connector settings payload""" + livemode: bool | None + """Whether the connection reaches the provider's live account rather than a test one; null + when not connected, and for NOOP + """ + unavailable_reason: ProviderUnavailableReason | str | None = Field(alias="unavailableReason") + """Why it is not available; null when it is, and when the licensing authority cannot answer""" -class ConnectorSettingsSchema(KaitenModel): - """The JSON schema a connector's settings are validated against.""" - connector_name: str - """Stable connector identifier""" +class BillingSettings(KaitenModel): + default_collection_method: CollectionMethod | str = Field(alias="defaultCollectionMethod") + """How an invoice is collected when its subscription does not say: SEND_INVOICE sends it + for payment; CHARGE_AUTOMATICALLY charges the customer's payment method, on a provider + that can. A NOOP subscription sends its invoices whatever the default. + """ - json_schema: dict[str, Any] = Field(alias="schema") - """JSON schema describing supported connector settings payload""" + default_days_until_due: int = Field(alias="defaultDaysUntilDue") + """Days an invoice is due after it is issued, when its subscription does not say; 0 to 365.""" - version: str - """Registered connector version""" + handoff_stripe_invoices: bool = Field(alias="handoffStripeInvoices") + """Whether invoices a payment provider issues also enter the handoff queue, for an + accounting system that wants every invoice. Invoices without a provider always do. + """ -class ConnectorState(KaitenModel): - """Whether a connector is available, licensed and activated for this organization.""" +class BooleanEntitlementValue(KaitenModel): + """A boolean entitlement value: whether a feature is included.""" - activated: bool - """Whether this organization has turned the connector on""" + type: Literal["boolean"] + """Value type discriminator""" - activated_at: datetime | None = Field(default=None, alias="activatedAt") - """When this organization activated the connector. Absent when it has not.""" + value: bool + """Boolean value""" - available: bool - """Whether this deployment has the connector registered""" - connector_name: str = Field(alias="connectorName") - """Stable connector identifier""" +class CanceledSubscription(KaitenModel): + anchor_at: datetime = Field(alias="anchorAt") + """The instant its periods are counted from""" - entitled: bool - """Whether this organization's licence includes the connector. True for an ungated connector.""" + base_price: Price = Field(alias="basePrice") + """The FLAT_FEE price the subscription is pinned to, deprecated or not""" - version: str | None = None - """Registered connector version. Absent when the connector is not available.""" + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + cancel_at_period_end: bool = Field(alias="cancelAtPeriodEnd") -class Customer(KaitenModel): - """A customer of your organization: the end client instances are deployed for.""" + cancel_requested_at: datetime | None = Field(alias="cancelRequestedAt") - created_at: datetime = Field(alias="createdAt") - """Timestamp when the customer was created""" + canceled_at: datetime | None = Field(alias="canceledAt") - created_by: User = Field(alias="createdBy") - """User who created this customer""" + cancellation_reason: str | None = Field(alias="cancellationReason") - domain: str | None = None - """Optional customer domain name""" + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + """Effective: the subscription's own, else the organization's default""" - external_customer_id: str | None = Field(default=None, alias="externalCustomerId") - """Optional identifier for the customer in an external system""" + collection_method_override: str | None = Field(alias="collectionMethodOverride") + """The subscription's own collection method; null when it takes the organization's""" - id: str - """Unique identifier for the customer""" + created_at: datetime = Field(alias="createdAt") - integrations: dict[str, CustomerIntegration] | None = None - """Integrations grouped by adapter name""" + currency: str - name: str - """Name of the customer""" + current_period_end: datetime = Field(alias="currentPeriodEnd") + """The next boundary: the period closes, and its invoice is composed, after it""" - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + current_period_start: datetime = Field(alias="currentPeriodStart") - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when the customer was last updated""" + customer_name: str = Field(alias="customerName") - updated_by: User = Field(alias="updatedBy") - """User who last updated this customer""" + customer_slug: str = Field(alias="customerSlug") + days_until_due: int = Field(alias="daysUntilDue") + """Effective: the subscription's own, else the organization's default""" -class CustomerCreationRejected(KaitenModel): - """A customer creation refused because the organization reached its limit.""" + days_until_due_override: int | None = Field(alias="daysUntilDueOverride") + """The subscription's own payment terms; null when it takes the organization's""" - name: str + final_invoice: InvoiceSummary | None = Field(default=None, alias="finalInvoice") + """The FINAL invoice of an immediate cancellation""" - reason: str + id: str - slug: str + incomplete_expires_at: datetime | None = Field(alias="incompleteExpiresAt") + """When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription + that carries one + """ + instance_name: str = Field(alias="instanceName") -class CustomerIntegration(KaitenModel): - """A customer's link to a record in a third-party system.""" + instance_slug: str = Field(alias="instanceSlug") + """As it was at the subscription's last write""" - external_id: str - """External identifier in the third-party adapter""" + past_due_since: datetime | None = Field(alias="pastDueSince") - last_error: str | None = None - """Last synchronization error message, if any""" + pending_license_slug: str | None = Field(alias="pendingLicenseSlug") + """The licence version a checkout waiting for its payment moves the instance to once paid""" - metadata: dict[str, Any] | None = None - """Adapter-specific integration metadata""" + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + """Who collects its invoices. NOOP: the organization itself, through the handoff queue""" - synced_at: datetime - """Timestamp of the last synchronization with the adapter""" + reminder_at: datetime | None = Field(alias="reminderAt") + """When trial_will_end is (or was) emitted: trialEndsAt minus the family's + trialReminderDays as it was when the trial started; set with trialEndsAt + """ - web_url: str | None = None - """Absolute http(s) link to the record in the third-party system""" + scheduled_change: ScheduledChange | None = Field(alias="scheduledChange") + """A plan change waiting for the next boundary""" + started_at: datetime = Field(alias="startedAt") + """When billing started: usage before it is never billed""" -class CustomerIntegrationResource(KaitenModel): - """A customer as seen through one of its integrations.""" - - domain: str | None = None - """Optional customer domain name""" + status: SubscriptionStatus | str - error: str | None = None - """Last synchronization error for this integration, if any""" + trial_ends_at: datetime | None = Field(alias="trialEndsAt") + """When the trial ends, or ended: usage before it is never billed""" - integration_metadata: dict[str, Any] = Field(alias="integrationMetadata") - """Adapter-specific customer integration metadata""" + updated_at: datetime = Field(alias="updatedAt") - name: str - """Customer name""" - slug: str - """Customer slug""" +class CheckoutConfirmation(KaitenModel): + session: CheckoutSession - web_url: str | None = Field(default=None, alias="webUrl") - """Absolute http(s) link to the record in the third-party system""" + setup_expires_at: datetime | None = Field(default=None, alias="setupExpiresAt") + """requires_payment_method only""" + setup_url: str | None = Field(default=None, alias="setupUrl") + """requires_payment_method only: the hosted page that saves the payment method""" -class Deployment(KaitenModel): - """A release deployed to a deployment zone: an append-only history record.""" + status: CheckoutConfirmationStatus | str - created_at: datetime = Field(alias="createdAt") - """Timestamp when the deployment was recorded""" - created_by: User = Field(alias="createdBy") - """User who recorded this deployment""" +class CheckoutPayment(KaitenModel): + hosted_invoice_url: str | None = Field(alias="hostedInvoiceUrl") - deployment_zone_id: str = Field(alias="deploymentZoneId") - """Deployment zone the release was deployed to""" + status: PaymentStatus | str - id: str - """Unique identifier for this deployment event""" - release_id: str = Field(alias="releaseId") - """Release that was deployed""" +class CheckoutSession(KaitenModel): + activation_invoice: SessionInvoice | None = Field(alias="activationInvoice") + """Once an ACTIVATION was issued""" + add_ons: list[SessionAddonSelection] = Field(alias="addOns") -class DeploymentZone(KaitenModel): - """An environment instances run in, and the release currently deployed to it.""" + billing_period: str = Field(alias="billingPeriod") - created_at: datetime = Field(alias="createdAt") - """Timestamp when the deployment zone was created""" + client_reference_id: str | None = Field(alias="clientReferenceId") - created_by: User = Field(alias="createdBy") - """User who created this deployment zone""" + completed_at: datetime | None = Field(alias="completedAt") - description: str - """Description of the deployment zone""" + created_at: datetime = Field(alias="createdAt") - id: str - """Unique identifier for the deployment zone""" + customer_slug: str | None = Field(alias="customerSlug") + """Set by the completion""" - metadata: dict[str, Any] | None = None - """Typed metadata for the deployment zone, validated against the org-scoped MetadataField - schema - """ + due_today: DueToday = Field(alias="dueToday") + """The voucher's discount shows as a line; the code itself is never echoed""" - name: str - """Name of the deployment zone""" + email: str - release_id: str | None = Field(default=None, alias="releaseId") - """Currently deployed release ID""" + expires_at: datetime = Field(alias="expiresAt") - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + instance_slug: str | None = Field(alias="instanceSlug") + """Set by the completion""" - type: str - """Environment class of the deployment zone. Free-form: any value is stored as sent, but - production, staging and development are the ones the console labels, and feature-flag - targeting rules match on this field as __kaiten.deploymentZone.type. - """ + license_slug: str = Field(alias="licenseSlug") - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when the deployment zone was last updated""" + payment: CheckoutPayment | None - updated_by: User = Field(alias="updatedBy") - """User who last updated the deployment zone""" + price_id: str = Field(alias="priceId") + requires_payment_method: bool = Field(alias="requiresPaymentMethod") -class Entitlement(KaitenModel): - """A catalog entry: something a license can grant, and how its usage is measured.""" + session_id: str = Field(alias="sessionId") - aggregation_method: AggregationMethod | str | None = Field( - default=None, alias="aggregationMethod" - ) - """Method used to fold each reported usage value into the stored total (NUMBER only). - Defaults to SUM. - """ + status: CheckoutSessionStatus | str + """A session still OPEN after expiresAt reads ABANDONED""" - created_at: datetime = Field(alias="createdAt") - """Timestamp when this entitlement was created""" + trial_ends_at: datetime | None = Field(alias="trialEndsAt") - description: str | None - """Description of the entitlement""" + trial_refused_reason: str | None = Field(alias="trialRefusedReason") - display_order: int | None = Field(default=None, alias="displayOrder") - """Sort order of this entitlement in customer-facing components (ascending). Defaults to 0.""" - entitlement_groups: list[EntitlementGroupSummary] | None = Field( - default=None, alias="entitlementGroups" - ) - """Groups associated with this entitlement""" +class CheckoutSessionAdmin(KaitenModel): + add_ons: list[SessionAddonSelection] = Field(alias="addOns") - group_slugs: list[str] | None = Field(default=None, alias="groupSlugs") - """Groups associated with this entitlement, identified by slug""" + billing_period: str = Field(alias="billingPeriod") - icon: str | None = None - """Optional icon, stored as a provider-namespaced token (provider:name). Decoupled from any - specific icon library. - """ + client_reference_id: str | None = Field(alias="clientReferenceId") - id: str - """Unique identifier for the entitlement""" + closed_at: datetime | None = Field(alias="closedAt") + """When it became COMPLETED, ABANDONED or EXPIRED; null while OPEN or AWAITING_PAYMENT""" - name: str - """Name of the entitlement""" + completed_at: datetime | None = Field(alias="completedAt") - reset_anchor: ResetAnchor | str | None = Field(default=None, alias="resetAnchor") - """Phase of the periodic usage window: CALENDAR (UTC calendar-aligned) or LICENSE_START - (phased off the instance's license start date). Required exactly when resetPeriod is - set; defaults to CALENDAR when omitted. Immutable once set. - """ + created_at: datetime = Field(alias="createdAt") - reset_period: ResetPeriod | str | None = Field(default=None, alias="resetPeriod") - """Cadence at which usage automatically resets for this NUMBER entitlement. NULL means a - lifetime counter (default). Immutable once set -- a full-replace PUT must echo back the - stored value. - """ + customer_slug: str | None = Field(alias="customerSlug") + """Set by the completion""" - sale_unit_factor: float | None = Field(default=None, alias="saleUnitFactor") - """Number of base units one sale unit represents (1 sale unit = N base units).""" + due_today: DueToday = Field(alias="dueToday") - sale_unit_plural: str | None = Field(default=None, alias="saleUnitPlural") - """Plural label of the unit this entitlement is sold in, when it differs from the base unit.""" + email: str + """As the visitor gave it. Personal data""" - sale_unit_singular: str | None = Field(default=None, alias="saleUnitSingular") - """Singular label of the unit this entitlement is sold in, when it differs from the base - unit. Requires the base unit labels and the full sale unit trio (saleUnitSingular, - saleUnitPlural, saleUnitFactor). - """ + expires_at: datetime = Field(alias="expiresAt") - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + instance_slug: str | None = Field(alias="instanceSlug") + """Set by the completion""" - type: EntitlementType | str | None = None - """Type of the entitlement""" + license_slug: str = Field(alias="licenseSlug") - unit_plural: str | None = Field(default=None, alias="unitPlural") - """Plural label of the base unit this entitlement is measured in (NUMBER type only). Must - be provided together with unitSingular. - """ + price_id: str = Field(alias="priceId") - unit_singular: str | None = Field(default=None, alias="unitSingular") - """Singular label of the base unit this entitlement is measured in (NUMBER type only). Must - be provided together with unitPlural. - """ + publishable_key_id: str = Field(alias="publishableKeyId") + """The key whose page started it""" - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when this entitlement was last updated""" + session_id: str = Field(alias="sessionId") - user_facing: bool | None = Field(default=None, alias="userFacing") - """Whether this entitlement is displayed in customer-facing components (e.g. plan and - pricing pages). Defaults to false. - """ + status: CheckoutSessionStatus | str + """A session still OPEN after expiresAt reads ABANDONED""" - warning_threshold_percent: int | None = Field(default=None, alias="warningThresholdPercent") - """Percentage of the cap at which an early-warning event fires before the cap is reached. 0 - disables the warning (default). - """ + trial_ends_at: datetime | None = Field(alias="trialEndsAt") + trial_refused_reason: str | None = Field(alias="trialRefusedReason") -class EntitlementGroup(KaitenModel): - """A named group of entitlements.""" - description: str | None - """Description of the entitlement group""" +class CheckoutSessionStart(KaitenModel): + due_today: DueToday = Field(alias="dueToday") - id: str - """Unique identifier for the entitlement group""" + expires_at: datetime | None = Field(default=None, alias="expiresAt") + """open only: 24 h after the start""" - name: str - """Name of the entitlement group""" + requires_payment_method: bool = Field(alias="requiresPaymentMethod") + """Whether the confirm will send the visitor to a payment-method setup page""" - slug: str | None = None - """URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + session_id: str | None = Field(default=None, alias="sessionId") + """open only""" + status: CheckoutStartStatus | str -class EntitlementGroupSummary(KaitenModel): - """A reference to an entitlement group.""" + token: str | None = None + """open only: the session's ksc_, returned once. Keep it in memory and sessionStorage, + never in a URL. + """ - id: str - """Unique identifier for the entitlement group""" + trial_ends_at: datetime | None = Field(alias="trialEndsAt") + """The end of the trial the subscription would start with; null without one""" - name: str - """Name of the entitlement group""" + trial_refused_reason: str | None = Field(alias="trialRefusedReason") + """ALREADY_CONSUMED: a customer with this e-mail already had a trial of the family, so none + is given; null otherwise + """ - slug: str - """URL-friendly identifier for the entitlement group""" +class CloseBacklog(KaitenModel): + count: int -class EntitlementGroupUsage(KaitenModel): - """One entitlement's grant and usage within a group, for one instance.""" + oldest_due_at: datetime | None = Field(alias="oldestDueAt") - current_period_end: datetime | None = Field(default=None, alias="currentPeriodEnd") - """End of the current usage window (exclusive). Null for a lifetime entitlement.""" - current_period_start: datetime | None = Field(default=None, alias="currentPeriodStart") - """Start of the current usage window (inclusive). Null for a lifetime entitlement.""" +class ClosePeriodsReport(KaitenModel): + closed: int + """Periods closed into an invoice""" - entitlement_id: str = Field(alias="entitlementId") - """Entitlement ID""" + examined: int + """Subscriptions the batch looked at""" - entitlement_name: str = Field(alias="entitlementName") - """Entitlement name""" + has_more: bool = Field(alias="hasMore") + """Set when due subscriptions are left beyond the batch""" - entitlement_slug: str = Field(alias="entitlementSlug") - """Entitlement slug""" + held: int + """Of those, invoices held as DRAFTs for their usage journal""" - entitlement_type: str = Field(alias="entitlementType") - """Entitlement type""" + invoices: list[ClosedInvoice] - license_value: EntitlementValue | None = Field(default=None, alias="licenseValue") - """License limit value, discriminated by the 'type' field. Null when the license grants no - value. + skipped: int + """Subscriptions left for later: held by another transaction, their meters' clock not at + the boundary, or failed (logged) """ - usage_value: EntitlementValue | None = Field(default=None, alias="usageValue") - """Current usage value, discriminated by the 'type' field. Null when nothing has been - reported yet. - """ +class ClosedInvoice(KaitenModel): + boundary_at: datetime = Field(alias="boundaryAt") -class EntitlementUsage(KaitenModel): - """An instance's consumption of one entitlement, next to the grant it is measured against.""" + held: bool + """Set when the invoice is a held DRAFT""" - current_period_end: datetime | None = Field(default=None, alias="currentPeriodEnd") - """End of the current usage window (exclusive). Null for a lifetime entitlement (no - configured reset period). - """ + id: str - current_period_start: datetime | None = Field(default=None, alias="currentPeriodStart") - """Start of the current usage window (inclusive). Null for a lifetime entitlement (no - configured reset period). - """ + instance_slug: str = Field(alias="instanceSlug") - entitlement_id: str = Field(alias="entitlementId") - """Unique identifier for the entitlement""" + kind: InvoiceKind | str - entitlement_slug: str = Field(alias="entitlementSlug") - """URL-friendly identifier of the entitlement""" + status: InvoiceStatus | str - license_id: str = Field(alias="licenseId") - """Unique identifier for the license to which this entitlement usage belongs""" - license_slug: str = Field(alias="licenseSlug") - """URL-friendly identifier of the license""" +class CompatibleLicenseFamilies(KaitenModel): + family_slugs: list[str] = Field(alias="familySlugs") - limit: EntitlementValue | None = None - """License grant this usage is measured against, discriminated by the 'type' field. Null - when the license grants no value; -1 means unlimited for a NUMBER entitlement. - """ - value: EntitlementValue - """Resolved entitlement value, discriminated by the 'type' field""" +class CompletedPaymentMethodSession(KaitenModel): + payment_method: PaymentMethodLabels = Field(alias="paymentMethod") -class ErrorDetail(KaitenModel): - """One validation problem inside an API error.""" +class Component(KaitenModel): + """A versioned building block of your product, that releases are made of.""" - location: str | None = None - """Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'""" + created_at: datetime = Field(alias="createdAt") + """Timestamp when the component was created""" - message: str | None = None - """Error message text""" + created_by: User = Field(alias="createdBy") + """User who created this component""" - value: Any | None = None - """The value at the given location""" + description: str | None = None + """Description of the component""" + id: str + """Unique identifier for the component""" -class FeatureFlag(KaitenModel): - """A feature flag: its variants, targeting rules and default variant.""" + name: str + """Name of the component""" - default_variant: DefaultVariant + previous_component_id: str | None = Field(default=None, alias="previousComponentId") + """Reference to the previous version of this component""" - description: str | None + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" - enabled: bool + version: str + """Version of the component""" - event_name: str - id: str +class ConfigEntitlementValue(KaitenModel): + """A configuration entitlement value: a free-form object.""" - metadata: dict[str, Any] + type: Literal["object"] + """Value type discriminator""" - name: str + value: dict[str, Any] + """Object configuration value""" - slug: str | None = None - targetings: Targetings +class ConnectionDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - type: FeatureFlagType | str + connector_name: str = Field(alias="connectorName") + """The connector that configures it""" - variants: list[Variant] | None + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + livemode: bool + """Whether the connection reaches the provider's live account rather than a test one""" -class FeatureFlagEvaluated(KaitenModel): - """A feature flag evaluation, as reported to webhooks.""" + provider_kind: Literal["STRIPE"] = Field(alias="providerKind") + """The payment provider""" - error_code: str | None = None - """Set when the evaluation failed and the default was returned""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - error_message: str | None = None - """Human-readable detail accompanying error_code""" - flag_id: str - """Unique identifier of the evaluated flag""" +class Connector(KaitenModel): + """A connector registered with this Kaiten deployment.""" - flag_slug: str - """URL-friendly identifier of the evaluated flag""" - - matched_rule_name: str | None = None - """Name of the targeting rule that matched, when the reason is TARGETING_MATCH""" - - reason: str - """Why the value resolved as it did""" + created_at: datetime + """Timestamp when connector was first registered""" - variant: str | None = None - """Name of the resolved variant, when the flag has one""" + entitlement_slug: str | None = None + """Slug of the BOOLEAN entitlement a license must grant for an organization to activate + this connector. Absent means the connector is ungated. + """ + name: str + """Stable connector identifier""" -class Instance(KaitenModel): - """A deployment of your product for one customer, under one license.""" + settings_schema: dict[str, Any] + """JSON schema used to validate connector settings payloads""" - created_at: datetime = Field(alias="createdAt") - """Timestamp when the instance was created""" + updated_at: datetime + """Timestamp when connector registration was last updated""" - created_by: User = Field(alias="createdBy") - """User who created this instance""" + version: str + """Registered connector version""" - customer_id: str = Field(alias="customerId") - """ID of the customer associated with this instance""" - customer_slug: str = Field(alias="customerSlug") - """Slug of the customer associated with this instance""" +class ConnectorActivation(KaitenModel): + """An organization's activation of a connector.""" - deployment_zone_id: str | None = Field(default=None, alias="deploymentZoneId") - """ID of the deployment zone associated with this instance (optional)""" + activated_at: datetime = Field(alias="activatedAt") + """When this organization first activated the connector""" - deployment_zone_slug: str | None = Field(default=None, alias="deploymentZoneSlug") - """Slug of the deployment zone associated with this instance (optional)""" + connector_name: str = Field(alias="connectorName") + """Stable connector identifier""" - description: str - """Brief description of the instance""" - end_license_date: datetime = Field(alias="endLicenseDate") - """End date of the license for this instance""" +class ConnectorSettings(KaitenModel): + """An organization's settings for a connector.""" - id: str - """Unique identifier for the instance""" + configured: bool + """Whether settings were ever stored for this connector. A connector never configured reads + false, with empty settings, rather than 404. + """ - integrations: dict[str, InstanceIntegration] | None = None - """Integrations grouped by adapter name""" + connector_name: str + """Stable connector identifier""" - license_id: str = Field(alias="licenseId") - """ID of the license version this instance is pinned to. An ARCHIVED version cannot be - assigned, on create or by changing the license (CreateInstance.LicenseArchived, - UpdateInstance.LicenseArchived); an instance already on a version that is archived later - keeps it. A DRAFT version can be assigned, to try it before publishing. - """ + settings: dict[str, Any] + """Connector settings payload""" - license_slug: str = Field(alias="licenseSlug") - """Slug of the license associated with this instance""" - lifecycle_stage: str | None = Field(default=None, alias="lifecycleStage") - """Commercial lifecycle stage of the instance (free-form; suggested: TRIAL, ACTIVE, - AT_RISK, CHURNED) - """ +class ConnectorSettingsSchema(KaitenModel): + """The JSON schema a connector's settings are validated against.""" - metadata: dict[str, Any] | None = None - """Metadata for the instance""" + connector_name: str + """Stable connector identifier""" - name: str - """Name of the instance""" + json_schema: dict[str, Any] = Field(alias="schema") + """JSON schema describing supported connector settings payload""" - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + version: str + """Registered connector version""" - start_license_date: datetime = Field(alias="startLicenseDate") - """Start date of the license for this instance""" - status: InstanceStatus | str | None = None - """Operational status of the instance""" +class ConnectorState(KaitenModel): + """Whether a connector is available, licensed and activated for this organization.""" - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when the instance was last updated""" + activated: bool + """Whether this organization has turned the connector on""" - updated_by: User = Field(alias="updatedBy") - """User who last updated this instance""" + activated_at: datetime | None = Field(default=None, alias="activatedAt") + """When this organization activated the connector. Absent when it has not.""" + available: bool + """Whether this installation has the connector registered""" -class InstanceEntitlementCapExceeded(KaitenModel): - """Usage crossed an entitlement's included value, within its allowance.""" + connector_name: str = Field(alias="connectorName") + """Stable connector identifier""" - entitlement_slug: str - """URL-friendly identifier of the entitlement""" + entitled: bool + """Whether this organization's license includes the connector. True for an ungated connector.""" - overage: float - """Amount by which usage exceeds the cap""" + version: str | None = None + """Registered connector version. Absent when the connector is not available.""" - threshold: float - """The entitlement cap""" - value: float - """Current usage value, including overage""" +class CreatedCustomerSession(KaitenModel): + customer_slug: str = Field(alias="customerSlug") + expires_at: datetime = Field(alias="expiresAt") -class InstanceEntitlementUsagePeriodRolledOver(KaitenModel): - """A periodic usage window closed and a new one opened.""" + id: str + """Revoke it with POST /customer-sessions/{id}/revoke""" - closed_period_end: datetime | None + instance_slug: str | None = Field(default=None, alias="instanceSlug") - closed_period_start: datetime | None + token: str + """The kst_ token: hand it to the customer's browser, which sends it as Authorization: + Bearer on /api/public/session routes. Shown once; only its digest is stored. + """ - entitlement_id: str - entitlement_slug: str +class Customer(KaitenModel): + """A customer of your organization: the end client instances are deployed for.""" - event_count: int + billing_email: str | None = Field(default=None, alias="billingEmail") + """Where invoices sent to this customer are delivered, at most 254 characters + (CreateCustomer.InvalidBillingEmail). Absent when there is none. On update, omit it or + send null to keep the stored value, or send an empty string to remove it. Personal data: + never included in event payloads. + """ - instance_id: str + created_at: datetime = Field(alias="createdAt") + """Timestamp when the customer was created""" - is_synthetic: bool + created_by: User = Field(alias="createdBy") + """User who created this customer""" - new_period_start: datetime + domain: str | None = None + """Optional customer domain name""" - value: float + external_customer_id: str | None = Field(default=None, alias="externalCustomerId") + """Optional identifier for the customer in an external system""" + id: str + """Unique identifier for the customer""" -class InstanceEntitlementUsageReportAccepted(KaitenModel): - """The outcome of a usage report.""" + integrations: dict[str, CustomerIntegration] | None = None + """Integrations grouped by adapter name""" - behavior: UsageBehavior | str - """Report behavior: append folds the value into the stored total, set overwrites it""" + name: str + """Name of the customer""" - entitlement_slug: str - """URL-friendly identifier of the entitlement the usage was reported against""" + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" - event_count: int - """Number of reports folded into the current usage window, including this one""" + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when the customer was last updated""" - status: UsageReportStatus | str - """Whether the report was persisted""" + updated_by: User = Field(alias="updatedBy") + """User who last updated this customer""" - value: float - """Value carried by the report""" +class CustomerBilling(KaitenModel): + billing_email: str | None = Field(alias="billingEmail") + """Where the customer's invoices are sent. Personal data""" -class InstanceEntitlementUsageWarningThresholdReached(KaitenModel): - """Usage crossed an entitlement's warning threshold.""" + providers: list[CustomerInProvider] + """The customer in each payment provider; none for NOOP""" - boundary: float - """Usage value at which the warning fires (cap * warningThresholdPercent / 100)""" - entitlement_slug: str - """URL-friendly identifier of the entitlement""" +class CustomerCreationRejected(KaitenModel): + """A customer creation refused because the organization reached its limit.""" - threshold: float - """The entitlement cap""" + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - value: float - """Current usage value""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + name: str -class InstanceEntitlementValueGet(KaitenModel): - """An entitlement value read for an instance.""" + reason: str - entitlement_id: str - """Unique identifier for the entitlement""" + slug: str - entitlement_slug: str - """URL-friendly identifier of the entitlement""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - instance_id: str - """Unique identifier for the instance the value was read for""" - license_id: str - """Unique identifier for the license carrying the entitlement""" +class CustomerInProvider(KaitenModel): + external_customer_id: str = Field(alias="externalCustomerId") + """The customer's id in the provider""" - organization_id: str - """Unique identifier for the owning organization""" + payment_method: PaymentMethodLabels | None = Field(alias="paymentMethod") + """Its default payment method; null when there is none""" - timestamp: datetime - """When the value was read""" + provider_kind: Literal["STRIPE"] = Field(alias="providerKind") - type: str - """Discriminator of the resolved entitlement value""" + synced_at: datetime | None = Field(alias="syncedAt") - value: EntitlementValue - """Resolved entitlement value at read time""" + web_url: str | None = Field(alias="webUrl") + """The customer's page in the provider's dashboard""" -class InstanceIntegration(KaitenModel): - """An instance's link to a record in a third-party system.""" +class CustomerIntegration(KaitenModel): + """A customer's link to a record in a third-party system.""" external_id: str """External identifier in the third-party adapter""" @@ -932,658 +1423,4798 @@ class InstanceIntegration(KaitenModel): """Absolute http(s) link to the record in the third-party system""" -class InstanceIntegrationResource(KaitenModel): - """An instance as seen through one of its integrations.""" - - customer_external_id: str | None = Field(default=None, alias="customerExternalId") - """External identifier of the related customer in the same adapter""" - - deployment_zone_id: str | None = Field(default=None, alias="deploymentZoneId") - """Deployment zone ID""" - - description: str - """Instance description""" +class CustomerIntegrationResource(KaitenModel): + """A customer as seen through one of its integrations.""" - end_license_date: datetime = Field(alias="endLicenseDate") - """License end date""" + domain: str | None = None + """Optional customer domain name""" error: str | None = None """Last synchronization error for this integration, if any""" integration_metadata: dict[str, Any] = Field(alias="integrationMetadata") - """Adapter-specific instance integration metadata""" - - license_id: str = Field(alias="licenseId") - """License ID""" + """Adapter-specific customer integration metadata""" name: str - """Instance name""" + """Customer name""" slug: str - """Instance slug""" - - start_license_date: datetime = Field(alias="startLicenseDate") - """License start date""" + """Customer slug""" web_url: str | None = Field(default=None, alias="webUrl") """Absolute http(s) link to the record in the third-party system""" -class InstanceLifecycleStageChanged(KaitenModel): - """An instance's commercial lifecycle stage changed.""" +class DeletedAddonDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - changed_by: str + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - instance_id: str + family_slug: str = Field(alias="familySlug") - new_lifecycle_stage: str | None + id: str - previous_lifecycle_stage: str | None + slug: str + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ -class InstanceStatusChanged(KaitenModel): - """An instance's operational status changed.""" - changed_by: str +class Deployment(KaitenModel): + """A release deployed to a deployment zone: an append-only history record.""" - instance_id: str + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - new_status: InstanceStatus | str + created_at: datetime = Field(alias="createdAt") + """Timestamp when the deployment was recorded""" - previous_status: InstanceStatus | str + created_by: User = Field(alias="createdBy") + """User who recorded this deployment""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class License(KaitenModel): - """A version of a license family: one set of entitlement grants, with its lifecycle state.""" + deployment_zone_id: str = Field(alias="deploymentZoneId") + """Deployment zone the release was deployed to""" - created_at: datetime = Field(alias="createdAt") - """Timestamp when this license was created""" + id: str + """Unique identifier for this deployment event""" + + release_id: str = Field(alias="releaseId") + """Release that was deployed""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class DeploymentZone(KaitenModel): + """An environment instances run in, and the release currently deployed to it.""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when the deployment zone was created""" + + created_by: User = Field(alias="createdBy") + """User who created this deployment zone""" + + description: str + """Description of the deployment zone""" + + id: str + """Unique identifier for the deployment zone""" + + metadata: dict[str, Any] | None = None + """Typed metadata for the deployment zone, validated against the org-scoped MetadataField + schema + """ + + name: str + """Name of the deployment zone""" + + release_id: str | None = Field(default=None, alias="releaseId") + """Currently deployed release ID""" + + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + + type: str + """Environment class of the deployment zone. Free-form: any value is stored as sent, but + production, staging and development are the ones the console labels, and feature-flag + targeting rules match on this field as __kaiten.deploymentZone.type. + """ + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when the deployment zone was last updated""" + + updated_by: User = Field(alias="updatedBy") + """User who last updated the deployment zone""" + + +class DiscountAllocation(KaitenModel): + amount: int + """In minor units, positive""" + + target_seq: int = Field(alias="targetSeq") + """The target line""" + + +class DiscountDifference(KaitenModel): + coupon_id: str = Field(alias="couponId") + """The provider's discount, empty when none was recorded""" + + kaiten_amount: int = Field(alias="kaitenAmount") + + line_id: str = Field(alias="lineId") + """The DISCOUNT line""" + + provider_amount: int = Field(alias="providerAmount") + """0 when the provider did not apply it""" + + seq: int + + target_seq: int = Field(alias="targetSeq") + + +class DueDateDifference(KaitenModel): + kaiten_due_at: datetime = Field(alias="kaitenDueAt") + + provider_due_at: datetime = Field(alias="providerDueAt") + + +class DueToday(KaitenModel): + currency: str + + discount_total: int = Field(alias="discountTotal") + """Minor units""" + + lines: list[SessionLine] + + subtotal: int + """Minor units""" + + total: int + """Minor units""" + + +class Entitlement(KaitenModel): + """A catalog entry: something a license can grant, and how its usage is measured.""" + + aggregation_method: AggregationMethod | str | None = Field( + default=None, alias="aggregationMethod" + ) + """Method used to fold each reported usage value into the stored total (NUMBER only). + Defaults to SUM. + """ + + created_at: datetime = Field(alias="createdAt") + """Timestamp when this entitlement was created""" + + description: str | None + """Description of the entitlement""" + + display_order: int | None = Field(default=None, alias="displayOrder") + """Sort order of this entitlement in customer-facing components (ascending). Defaults to 0.""" + + entitlement_groups: list[EntitlementGroupSummary] | None = Field( + default=None, alias="entitlementGroups" + ) + """Groups associated with this entitlement""" + + group_slugs: list[str] | None = Field(default=None, alias="groupSlugs") + """Groups associated with this entitlement, identified by slug""" + + icon: str | None = None + """Optional icon, stored as a provider-namespaced token (provider:name). Decoupled from any + specific icon library. + """ + + id: str + """Unique identifier for the entitlement""" + + name: str + """Name of the entitlement""" + + reset_anchor: ResetAnchor | str | None = Field(default=None, alias="resetAnchor") + """Phase of the periodic usage window: CALENDAR (UTC calendar-aligned), LICENSE_START + (phased off the instance's license start date) or BILLING_PERIOD (phased off the + instance's live subscription's billing anchor, LICENSE_START without one; with + resetPeriod MONTH or YEAR only). Required exactly when resetPeriod is set; defaults to + CALENDAR when omitted. Immutable once set. + """ + + reset_period: ResetPeriod | str | None = Field(default=None, alias="resetPeriod") + """Cadence at which usage automatically resets for this NUMBER entitlement. NULL means a + lifetime counter (default). Immutable once set -- a full-replace PUT must echo back the + stored value. + """ + + sale_unit_factor: float | None = Field(default=None, alias="saleUnitFactor") + """Number of base units one sale unit represents (1 sale unit = N base units).""" + + sale_unit_plural: str | None = Field(default=None, alias="saleUnitPlural") + """Plural label of the unit this entitlement is sold in, when it differs from the base unit.""" + + sale_unit_singular: str | None = Field(default=None, alias="saleUnitSingular") + """Singular label of the unit this entitlement is sold in, when it differs from the base + unit. Requires the base unit labels and the full sale unit trio (saleUnitSingular, + saleUnitPlural, saleUnitFactor). + """ + + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + + type: EntitlementType | str | None = None + """Type of the entitlement""" + + unit_plural: str | None = Field(default=None, alias="unitPlural") + """Plural label of the base unit this entitlement is measured in (NUMBER type only). Must + be provided together with unitSingular. + """ + + unit_singular: str | None = Field(default=None, alias="unitSingular") + """Singular label of the base unit this entitlement is measured in (NUMBER type only). Must + be provided together with unitPlural. + """ + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when this entitlement was last updated""" + + user_facing: bool | None = Field(default=None, alias="userFacing") + """Whether this entitlement is displayed in customer-facing components (e.g. plan and + pricing pages). Defaults to false. + """ + + warning_threshold_percent: int | None = Field(default=None, alias="warningThresholdPercent") + """Percentage of the cap at which an early-warning event fires before the cap is reached. 0 + disables the warning (default). + """ + + +class EntitlementException(KaitenModel): + created_at: datetime = Field(alias="createdAt") + + ends_at: datetime | None = Field(alias="endsAt") + """Null: no end""" + + entitlement_slug: str = Field(alias="entitlementSlug") + + entitlement_type: EntitlementType | str = Field(alias="entitlementType") + + id: str + + instance_slug: str = Field(alias="instanceSlug") + + kind: ExceptionKind | str + + reason: str + + revoked_at: datetime | None = Field(alias="revokedAt") + + starts_at: datetime = Field(alias="startsAt") + + status: ExceptionStatus | str + """At the read: SCHEDULED before startsAt, ACTIVE in its window, EXPIRED after endsAt, + REVOKED once revoked + """ + + value: float | bool | dict[str, Any] | None + """A number for NUMBER and NUMBER_AI_CREDIT, a boolean for BOOLEAN, the CONFIG document as + an object; null for UNLIMITED + """ + + +class EntitlementGroup(KaitenModel): + """A named group of entitlements.""" + + description: str | None + """Description of the entitlement group""" + + id: str + """Unique identifier for the entitlement group""" + + name: str + """Name of the entitlement group""" + + slug: str | None = None + """URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + + +class EntitlementGroupSummary(KaitenModel): + """A reference to an entitlement group.""" + + id: str + """Unique identifier for the entitlement group""" + + name: str + """Name of the entitlement group""" + + slug: str + """URL-friendly identifier for the entitlement group""" + + +class EntitlementGroupUsage(KaitenModel): + """One entitlement's grant and usage within a group, for one instance.""" + + current_period_end: datetime | None = Field(default=None, alias="currentPeriodEnd") + """End of the current usage window (exclusive). Null for a lifetime entitlement.""" + + current_period_start: datetime | None = Field(default=None, alias="currentPeriodStart") + """Start of the current usage window (inclusive). Null for a lifetime entitlement.""" + + entitlement_id: str = Field(alias="entitlementId") + """Entitlement ID""" + + entitlement_name: str = Field(alias="entitlementName") + """Entitlement name""" + + entitlement_slug: str = Field(alias="entitlementSlug") + """Entitlement slug""" + + entitlement_type: str = Field(alias="entitlementType") + """Entitlement type""" + + license_value: EntitlementValue | None = Field(default=None, alias="licenseValue") + """License limit value, discriminated by the 'type' field. Null when the license grants no + value. + """ + + usage_value: EntitlementValue | None = Field(default=None, alias="usageValue") + """Current usage value, discriminated by the 'type' field. Null when nothing has been + reported yet. + """ + + +class EntitlementUsage(KaitenModel): + """An instance's consumption of one entitlement, next to the grant it is measured against.""" + + current_period_end: datetime | None = Field(default=None, alias="currentPeriodEnd") + """End of the current usage window (exclusive). Null for a lifetime entitlement (no + configured reset period). + """ + + current_period_start: datetime | None = Field(default=None, alias="currentPeriodStart") + """Start of the current usage window (inclusive). Null for a lifetime entitlement (no + configured reset period). + """ + + entitlement_id: str = Field(alias="entitlementId") + """Unique identifier for the entitlement""" + + entitlement_slug: str = Field(alias="entitlementSlug") + """URL-friendly identifier of the entitlement""" + + license_id: str = Field(alias="licenseId") + """Unique identifier for the license to which this entitlement usage belongs""" + + license_slug: str = Field(alias="licenseSlug") + """URL-friendly identifier of the license""" + + limit: EntitlementValue | None = None + """The effective value this usage is measured against (the licence's grant, with the + instance's add-ons and boosts applied; source and provenance say which), discriminated + by the 'type' field. Absent when nothing grants the entitlement a value; -1 means + unlimited for a NUMBER entitlement. + """ + + limit_cap_exceeded_overage_percent: int | None = Field( + default=None, alias="limitCapExceededOveragePercent" + ) + """How far above limit usage is still accepted, in percent: -1 unlimited, 0 a hard limit. + Absent for BOOLEAN and CONFIG. + """ + + provenance: Provenance | None = None + """What the licence, the add-ons and the boosts each contribute""" + + source: EntitlementSource | str + """license: the licence grants it (add-ons, boosts and an exception may change it); addon: + an add-on attached does; exception: only the instance's exception does + """ + + value: EntitlementValue + """Resolved entitlement value, discriminated by the 'type' field""" + + +class ErrorDetail(KaitenModel): + """One validation problem inside an API error.""" + + location: str | None = None + """Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'""" + + message: str | None = None + """Error message text""" + + value: Any | None = None + """The value at the given location""" + + +class EventHeaders(KaitenModel): + baggage: str | None = None + """W3C baggage of that request, when it carried any""" + + instance_id: str | None = None + """The instance the event concerns, set on the events of one instance. The usage events' + payloads do not name it. + """ + + traceparent: str | None = None + """W3C trace context of the request that produced the event""" + + tracestate: str | None = None + """W3C trace state of that request, when it carried one""" + + +class ExceptionEventDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + ends_at: datetime | None = Field(alias="endsAt") + """Null: no end""" + + entitlement_slug: str = Field(alias="entitlementSlug") + + id: str + + instance_slug: str = Field(alias="instanceSlug") + + kind: ExceptionKind | str + + reason: str + + revoked_at: datetime | None = Field(alias="revokedAt") + + starts_at: datetime = Field(alias="startsAt") + + value: float | bool | dict[str, Any] | None + """A number for NUMBER and NUMBER_AI_CREDIT, a boolean for BOOLEAN, the CONFIG document as + an object; null for UNLIMITED + """ + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class ExhaustedEventDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + id: str + + max_redemptions: int | None = Field(default=None, alias="maxRedemptions") + + name: str + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class ExpiredEventDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + expires_at: datetime = Field(alias="expiresAt") + + id: str + + name: str + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class ExtraDiscount(KaitenModel): + amount: int + + discount_id: str = Field(alias="discountId") + + external_line_id: str = Field(alias="externalLineId") + + +class FeatureFlag(KaitenModel): + """A feature flag: its variants, targeting rules and default variant.""" + + default_variant: DefaultVariant + + description: str | None + + enabled: bool + + event_name: str + + id: str + + metadata: dict[str, Any] + + name: str + + slug: str | None = None + + targetings: Targetings + + type: FeatureFlagType | str + + variants: list[Variant] | None + + +class FeatureFlagEvaluated(KaitenModel): + """A feature flag evaluation, as reported to webhooks.""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + error_code: str | None = None + """Set when the evaluation failed and the default was returned""" + + error_message: str | None = None + """Human-readable detail accompanying error_code""" + + flag_id: str + """Unique identifier of the evaluated flag""" + + flag_slug: str + """URL-friendly identifier of the evaluated flag""" + + matched_rule_name: str | None = None + """Name of the targeting rule that matched, when the reason is TARGETING_MATCH""" + + reason: str + """Why the value resolved as it did""" + + variant: str | None = None + """Name of the resolved variant, when the flag has one""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class FlagGrant(KaitenModel): + flag_slug: str = Field(alias="flagSlug") + + variant: str + """The name of one of the flag's variants""" + + +class Grant(KaitenModel): + entitlement_slug: str = Field(alias="entitlementSlug") + + modifier_type: GrantModifier | str = Field(alias="modifierType") + """SET replaces the value, ADD adds to it, MULTIPLY multiplies it, UNLIMITED lifts the + limit. A BOOLEAN or CONFIG entitlement takes SET only + """ + + modifier_value: str | None = Field(default=None, alias="modifierValue") + """A NUMBER grant's decimal: >= 0 for SET, > 0 for ADD and MULTIPLY, null for UNLIMITED; + null on a BOOLEAN or CONFIG grant. Optional on write. + """ + + value: bool | dict[str, Any] | None = None + """What a BOOLEAN or CONFIG SET grant sets: true or false, or the CONFIG document as an + object; null on a NUMBER grant. Optional on write. + """ + + +class HandoffAcknowledgementDelivered(KaitenModel): + acknowledged_by: str = Field(alias="acknowledgedBy") + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + external_reference: str | None = Field(default=None, alias="externalReference") + """The invoice's number in the organization's accounting system""" + + invoice_id: str = Field(alias="invoiceId") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class HandoffBacklog(KaitenModel): + oldest_pending_issued_at: datetime | None = Field(alias="oldestPendingIssuedAt") + + pending: int + + reclaimed_last24h: int = Field(alias="reclaimedLast24h") + """Invoices claimed again after a lease expired, in the last 24 hours: a consumer that + claims and does not acknowledge + """ + + +class HandoffClaim(KaitenModel): + invoices: list[Invoice] + """The leased invoices, oldest issue first, with their billing e-mail: what the accounting + system needs to book them + """ + + lease_id: str = Field(alias="leaseId") + """Pass it back when acknowledging an invoice of this claim""" + + leased_until: datetime = Field(alias="leasedUntil") + """When the lease expires and the invoices are claimable again""" + + +class HeldInvoices(KaitenModel): + by_reason: dict[str, int] = Field(alias="byReason") + + count: int + + +class HeldPair(KaitenModel): + counter_report_seq: int | None = Field(alias="counterReportSeq") + + detail: str + + entitlement_id: str = Field(alias="entitlementId") + + expected: str | None + + first_seq: int | None = Field(alias="firstSeq") + + found: str | None + + instance_id: str = Field(alias="instanceId") + + invariant: str + + last_seq: int | None = Field(alias="lastSeq") + + report_seq: int | None = Field(alias="reportSeq") + """The report the failure is at, when one is""" + + +class HoldDetail(KaitenModel): + item_limit: ProviderLineLimit | None = Field(default=None, alias="itemLimit") + """PROVIDER_ITEM_LIMIT only: the provider's line limit and how many items the invoice needs""" + + pairs: list[HeldPair] + + +class HoldRecord(KaitenModel): + held_at: datetime | None = Field(alias="heldAt") + + release_reason: str | None = Field(alias="releaseReason") + + released_at: datetime | None = Field(alias="releasedAt") + + released_by: str | None = Field(alias="releasedBy") + """The user who released it; null when it was released by a later check""" + + +class Instance(KaitenModel): + """A deployment of your product for one customer, under one license.""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when the instance was created""" + + created_by: User = Field(alias="createdBy") + """User who created this instance""" + + customer_id: str = Field(alias="customerId") + """ID of the customer associated with this instance""" + + customer_slug: str = Field(alias="customerSlug") + """Slug of the customer associated with this instance""" + + deployment_zone_id: str | None = Field(default=None, alias="deploymentZoneId") + """ID of the deployment zone associated with this instance (optional)""" + + deployment_zone_slug: str | None = Field(default=None, alias="deploymentZoneSlug") + """Slug of the deployment zone associated with this instance (optional)""" + + description: str + """Brief description of the instance""" + + end_license_date: datetime = Field(alias="endLicenseDate") + """End date of the license for this instance""" + + id: str + """Unique identifier for the instance""" + + integrations: dict[str, InstanceIntegration] | None = None + """Integrations grouped by adapter name""" + + license_id: str = Field(alias="licenseId") + """ID of the license version this instance is pinned to. An ARCHIVED version cannot be + assigned, on create or by changing the license (CreateInstance.LicenseArchived, + UpdateInstance.LicenseArchived); an instance already on a version that is archived later + keeps it. A DRAFT version can be assigned, to try it before publishing. + """ + + license_slug: str = Field(alias="licenseSlug") + """Slug of the license associated with this instance""" + + lifecycle_stage: str | None = Field(default=None, alias="lifecycleStage") + """Commercial lifecycle stage of the instance (free-form; suggested: TRIAL, ACTIVE, + AT_RISK, CHURNED) + """ + + metadata: dict[str, Any] | None = None + """Metadata for the instance""" + + name: str + """Name of the instance""" + + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + + start_license_date: datetime = Field(alias="startLicenseDate") + """Start date of the license for this instance""" + + status: InstanceStatus | str | None = None + """Operational status of the instance""" + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when the instance was last updated""" + + updated_by: User = Field(alias="updatedBy") + """User who last updated this instance""" + + +class InstanceAddon(KaitenModel): + addon_id: str = Field(alias="addonId") + + addon_slug: str = Field(alias="addonSlug") + + attached_at: datetime = Field(alias="attachedAt") + + family_slug: str = Field(alias="familySlug") + + id: str + + max_quantity: int | None = Field(alias="maxQuantity") + + name: str + """The add-on version's name""" + + pending_quantity: int | None = Field(alias="pendingQuantity") + """A decrease scheduled for the next boundary, 0 for a removal; the current quantity holds + until then. Null when nothing is scheduled (V31-67) + """ + + prices: list[Price] + """What the instance's subscription bills for it: the default FLAT_FEE price of the + subscription's period and the ACTIVE metered prices. Empty when the instance is not + billed. + """ + + quantity: int + + removed_at: datetime | None = Field(alias="removedAt") + + +class InstanceBilling(KaitenModel): + anchor_at: datetime = Field(alias="anchorAt") + """The instant its periods are counted from""" + + base_price: Price = Field(alias="basePrice") + """The FLAT_FEE price the subscription is pinned to, deprecated or not""" + + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + + cancel_at_period_end: bool = Field(alias="cancelAtPeriodEnd") + + cancel_requested_at: datetime | None = Field(alias="cancelRequestedAt") + + canceled_at: datetime | None = Field(alias="canceledAt") + + cancellation_reason: str | None = Field(alias="cancellationReason") + + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + """Effective: the subscription's own, else the organization's default""" + + collection_method_override: str | None = Field(alias="collectionMethodOverride") + """The subscription's own collection method; null when it takes the organization's""" + + created_at: datetime = Field(alias="createdAt") + + currency: str + + current_period_end: datetime = Field(alias="currentPeriodEnd") + """The next boundary: the period closes, and its invoice is composed, after it""" + + current_period_start: datetime = Field(alias="currentPeriodStart") + + customer_name: str = Field(alias="customerName") + + customer_slug: str = Field(alias="customerSlug") + + days_until_due: int = Field(alias="daysUntilDue") + """Effective: the subscription's own, else the organization's default""" + + days_until_due_override: int | None = Field(alias="daysUntilDueOverride") + """The subscription's own payment terms; null when it takes the organization's""" + + id: str + + incomplete_expires_at: datetime | None = Field(alias="incompleteExpiresAt") + """When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription + that carries one + """ + + instance_name: str = Field(alias="instanceName") + + instance_slug: str = Field(alias="instanceSlug") + """As it was at the subscription's last write""" + + past_due_since: datetime | None = Field(alias="pastDueSince") + + pending_license_slug: str | None = Field(alias="pendingLicenseSlug") + """The licence version a checkout waiting for its payment moves the instance to once paid""" + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + """Who collects its invoices. NOOP: the organization itself, through the handoff queue""" + + reminder_at: datetime | None = Field(alias="reminderAt") + """When trial_will_end is (or was) emitted: trialEndsAt minus the family's + trialReminderDays as it was when the trial started; set with trialEndsAt + """ + + scheduled_change: ScheduledChange | None = Field(alias="scheduledChange") + """A plan change waiting for the next boundary""" + + started_at: datetime = Field(alias="startedAt") + """When billing started: usage before it is never billed""" + + status: SubscriptionStatus | str + + trial_ends_at: datetime | None = Field(alias="trialEndsAt") + """When the trial ends, or ended: usage before it is never billed""" + + updated_at: datetime = Field(alias="updatedAt") + + +class InstanceEntitlementCapExceeded(KaitenModel): + """Usage crossed an entitlement's included value, within its allowance.""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + entitlement_slug: str + """URL-friendly identifier of the entitlement""" + + overage: float + """Amount by which usage exceeds the cap""" + + threshold: float + """The entitlement cap""" + + value: float + """Current usage value, including overage""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class InstanceEntitlementUsagePeriodRolledOver(KaitenModel): + """A periodic usage window closed and a new one opened.""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + closed_period_end: datetime | None + + closed_period_start: datetime | None + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + entitlement_id: str + + entitlement_slug: str + + event_count: int + + instance_id: str + + is_synthetic: bool + + new_period_start: datetime + + reason: PeriodRolloverReason | str + + value: float + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class InstanceEntitlementUsageReportAccepted(KaitenModel): + """The outcome of a usage report.""" + + behavior: UsageBehavior | str + """Report behavior: append folds the value into the stored total, set overwrites it""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + entitlement_slug: str + """URL-friendly identifier of the entitlement the usage was reported against""" + + event_count: int + """Number of reports folded into the current usage window, including this one""" + + status: UsageReportStatus | str + """Whether the report was persisted""" + + value: float + """Value carried by the report""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class InstanceEntitlementUsageWarningThresholdReached(KaitenModel): + """Usage crossed an entitlement's warning threshold.""" + + boundary: float + """Usage value at which the warning fires (cap * warningThresholdPercent / 100)""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + entitlement_slug: str + """URL-friendly identifier of the entitlement""" + + threshold: float + """The entitlement cap""" + + value: float + """Current usage value""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class InstanceEntitlementValueGet(KaitenModel): + """An entitlement value read for an instance.""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + entitlement_id: str + """Unique identifier for the entitlement""" + + entitlement_slug: str + """URL-friendly identifier of the entitlement""" + + instance_id: str + """Unique identifier for the instance the value was read for""" + + license_id: str + """Unique identifier for the license carrying the entitlement""" + + organization_id: str + """Unique identifier for the owning organization""" + + timestamp: datetime + """When the value was read""" + + type: str + """Discriminator of the resolved entitlement value""" + + value: EntitlementValue + """Resolved entitlement value at read time""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class InstanceIntegration(KaitenModel): + """An instance's link to a record in a third-party system.""" + + external_id: str + """External identifier in the third-party adapter""" + + last_error: str | None = None + """Last synchronization error message, if any""" + + metadata: dict[str, Any] | None = None + """Adapter-specific integration metadata""" + + synced_at: datetime + """Timestamp of the last synchronization with the adapter""" + + web_url: str | None = None + """Absolute http(s) link to the record in the third-party system""" + + +class InstanceIntegrationResource(KaitenModel): + """An instance as seen through one of its integrations.""" + + customer_external_id: str | None = Field(default=None, alias="customerExternalId") + """External identifier of the related customer in the same adapter""" + + deployment_zone_id: str | None = Field(default=None, alias="deploymentZoneId") + """Deployment zone ID""" + + description: str + """Instance description""" + + end_license_date: datetime = Field(alias="endLicenseDate") + """License end date""" + + error: str | None = None + """Last synchronization error for this integration, if any""" + + integration_metadata: dict[str, Any] = Field(alias="integrationMetadata") + """Adapter-specific instance integration metadata""" + + license_id: str = Field(alias="licenseId") + """License ID""" + + name: str + """Instance name""" + + slug: str + """Instance slug""" + + start_license_date: datetime = Field(alias="startLicenseDate") + """License start date""" + + web_url: str | None = Field(default=None, alias="webUrl") + """Absolute http(s) link to the record in the third-party system""" + + +class InstanceLifecycleStageChanged(KaitenModel): + """An instance's commercial lifecycle stage changed.""" + + changed_by: str + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + instance_id: str + + new_lifecycle_stage: str | None + + previous_lifecycle_stage: str | None + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class InstanceStatusChanged(KaitenModel): + """An instance's operational status changed.""" + + changed_by: str + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + instance_id: str + + new_status: InstanceStatus | str + + previous_status: InstanceStatus | str + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class Invoice(KaitenModel): + billing_email: str | None = Field(alias="billingEmail") + """The address the invoice is for, as it was when composed. Personal data: it appears in no + event + """ + + boundary_at: datetime = Field(alias="boundaryAt") + """The boundary the invoice bills. With the kind, it identifies the invoice within its + subscription + """ + + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + + created_at: datetime = Field(alias="createdAt") + + currency: str + + customer_name: str = Field(alias="customerName") + + customer_slug: str = Field(alias="customerSlug") + """As it was when the invoice was composed""" + + days_until_due: int | None = Field(alias="daysUntilDue") + + discount_total: int = Field(alias="discountTotal") + """Sum of the discounts, in minor units""" + + due_at: datetime | None = Field(alias="dueAt") + """issuedAt + daysUntilDue""" + + handoff: InvoiceHandoff + + handoff_status: HandoffStatus | str = Field(alias="handoffStatus") + """PENDING: waiting in the handoff queue for the organization's accounting system""" + + hold: HoldRecord | None + """When the invoice was held and released""" + + hold_detail: HoldDetail | None = Field(alias="holdDetail") + """Every meter whose journal failed a check""" + + hold_reason: InvoiceHoldReason | str | None = Field(alias="holdReason") + """Why a DRAFT is held: the usage journal it was measured from failed a consistency check + (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT) + """ + + id: str + """The invoice's identifier: the key an accounting system deduplicates on""" + + instance_name: str = Field(alias="instanceName") + + instance_slug: str = Field(alias="instanceSlug") + + issued_at: datetime | None = Field(alias="issuedAt") + + kind: InvoiceKind | str + """ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the + one that starts, FINAL what is left when a subscription ends + """ + + license_id: str = Field(alias="licenseId") + + license_slug: str = Field(alias="licenseSlug") + + lines: list[InvoiceLine] + + overdue: bool + """Whether the invoice is overdue when read, the grace period of automatic collection + included: the predicate of ?overdue=true. Computed by the invoice list and the invoice + read; false in the answers of writes and in event payloads. + """ + + paid_at: datetime | None = Field(alias="paidAt") + + provider: ProviderRecord | None + """The invoice in its payment provider; null for NOOP""" + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + """Who collects it. NOOP: the organization itself, through the handoff queue""" + + replaced_by_invoice_id: str | None = Field(alias="replacedByInvoiceId") + """The invoice recomposed from this VOID one""" + + replaces_invoice_id: str | None = Field(alias="replacesInvoiceId") + """The VOID invoice this one was recomposed from""" + + service_from: datetime = Field(alias="serviceFrom") + """Earliest start of its lines' service periods""" + + service_to: datetime = Field(alias="serviceTo") + """Latest end of its lines' service periods""" + + status: InvoiceStatus | str + """MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. + DRAFT: held, waiting for a release or a recompose + """ + + subtotal: int + """Sum of the lines, in minor units""" + + total: int + """subtotal − discountTotal, in minor units""" + + uncollectible_at: datetime | None = Field(alias="uncollectibleAt") + + updated_at: datetime = Field(alias="updatedAt") + + void_reason: str | None = Field(alias="voidReason") + + voided_at: datetime | None = Field(alias="voidedAt") + + +class InvoiceHandoff(KaitenModel): + acknowledged_at: datetime | None = Field(alias="acknowledgedAt") + + claim_count: int = Field(alias="claimCount") + + external_reference: str | None = Field(alias="externalReference") + """The invoice's number in the organization's accounting system""" + + lease_id: str | None = Field(alias="leaseId") + """The claim currently holding it""" + + leased_until: datetime | None = Field(alias="leasedUntil") + + status: HandoffStatus | str + + +class InvoiceHold(KaitenModel): + entitlement_id: str = Field(alias="entitlementId") + + invariant: str + """The check that failed""" + + +class InvoiceLine(KaitenModel): + addon_id: str | None = Field(alias="addonId") + """The add-on version an ADDON line bills""" + + addon_price_id: str | None = Field(alias="addonPriceId") + """The add-on price an ADDON line bills""" + + amount: int + """round_half_up(quantity × unitAmountDecimal), in minor units; negative on a DISCOUNT line""" + + billing_model: BillingModel | str | None = Field(alias="billingModel") + """The price's billing model; absent on a DISCOUNT line""" + + billing_timing: BillingTiming | str | None = Field(alias="billingTiming") + """ADVANCE lines bill the period that starts at the boundary, ARREARS lines the one that + ends there; absent on a DISCOUNT line + """ + + capped: bool | None = None + """Set on a preview line whose sample exceeded what the licence accepts: reports above that + are rejected, so the excess is not billed + """ + + description: str + """The arithmetic of the line""" + + discount: InvoiceLineDiscount | None + """How a DISCOUNT line was computed""" + + entitlement_id: str | None = Field(alias="entitlementId") + """The metered entitlement, on USAGE and OVERAGE lines""" + + entitlement_slug: str | None = Field(alias="entitlementSlug") + """Its slug, as it was when the line was composed""" + + id: str | None = None + """The line's identifier, stable for the invoice's life; absent on a preview""" + + instance_addon_id: str | None = Field(alias="instanceAddonId") + """The instance's attachment an ADDON line bills""" + + instance_voucher_id: str | None = Field(alias="instanceVoucherId") + """The redemption a DISCOUNT line applies""" + + label: str + """The price's display label, else a derived one""" + + license_price_id: str | None = Field(alias="licensePriceId") + """The licence price the line bills; null on an ADDON line""" + + metering: InvoiceLineMetering | None + """How a USAGE or OVERAGE line's quantity was measured""" + + overage: InvoiceLineOverage | None + """The arithmetic of an OVERAGE line""" + + provider: InvoiceLineProvider | None + """The line in the payment provider, once pushed""" + + quantity: str + """In sale units, a decimal string: 1 on a BASE line, the quantity held on an ADDON line""" + + seq: int + """Position on the invoice, from 1""" + + service_from: datetime = Field(alias="serviceFrom") + """Start of the period the line bills (inclusive)""" + + service_to: datetime = Field(alias="serviceTo") + """End of the period the line bills (exclusive)""" + + true_up: InvoiceLineTrueUp | None = Field(alias="trueUp") + """On the true-up line of an ADVANCE add-on of a TRUE_UP family: the highest quantity held + in the elapsed period and the quantity billed in advance for it (V31-70) + """ + + type: InvoiceLineType | str + """BASE: the subscription's FLAT_FEE price. ADDON: an add-on's FLAT_FEE price times the + quantity held at the boundary. USAGE: a USAGE_BASED price's metered usage. OVERAGE: an + OVERAGE price's usage above the licence's limit. DISCOUNT: a PRICE voucher's discount, + negative. + """ + + unit_amount_decimal: str | None = Field(alias="unitAmountDecimal") + """The price's unit amount in minor units; absent on a DISCOUNT line""" + + voucher_id: str | None = Field(alias="voucherId") + """The voucher a DISCOUNT line applies""" + + +class InvoiceLineDiscount(KaitenModel): + allocations: list[DiscountAllocation] + """The part of the line each target bears, by ascending targetSeq: they sum to the line's + magnitude, and no target is discounted below 0. A payment provider applies each one to + its target line + """ + + application: int + """Which invoice of the redemption this is, from 1""" + + applications_max: int | None = Field(alias="applicationsMax") + """How many invoices the redemption discounts; null for FOREVER""" + + applies_to: DiscountAppliesTo | str = Field(alias="appliesTo") + + base: str + """What its targets still amounted to before it, exact, in minor units""" + + currency: str | None + + discount_type: DiscountType | str = Field(alias="discountType") + + discount_value: str = Field(alias="discountValue") + """The percentage, or the amount in minor units""" + + target_seqs: list[int] | None = Field(alias="targetSeqs") + """The lines it discounts""" + + +class InvoiceLineLedger(KaitenModel): + first_seq: int | None = Field(alias="firstSeq") + """First report of the period, null when it has none""" + + instance_id: str | None = Field(alias="instanceId") + """The instance whose reports these are: with the line's entitlement, the pair the journal + is kept by. It outlives the instance + """ + + last_seq: int | None = Field(alias="lastSeq") + """Last report of the period, null when it has none""" + + rows: int + """Reports in the period""" + + sum_delta: str = Field(alias="sumDelta") + """Sum of the reports' movements, before any floor""" + + sum_overage: str | None = Field(alias="sumOverage") + """Sum of their movements above the limit, before any floor""" + + +class InvoiceLineMetering(KaitenModel): + ledger: InvoiceLineLedger | None + """The usage journal rows the line was measured from""" + + measured_quantity: str = Field(alias="measuredQuantity") + """In measured units: the usage for a USAGE line, the overage for an OVERAGE line""" + + negative_segments_floored: int = Field(alias="negativeSegmentsFloored") + """Windows whose net movement was negative and counted as 0""" + + sale_unit_factor: str = Field(alias="saleUnitFactor") + """Measured units in one sale unit""" + + windows: int + """Reset windows the period spans""" + + +class InvoiceLineOverage(KaitenModel): + limits: list[OverageLimit] + """The limits applied, in the order they first applied""" + + overage_measured: str = Field(alias="overageMeasured") + """Usage above the applied limit""" + + usage_measured: str = Field(alias="usageMeasured") + """Usage in measured units""" + + +class InvoiceLineProvider(KaitenModel): + amount: int | None = None + """The provider's amount, read back for reconciliation""" + + coupon_ids: list[str] | None = Field(default=None, alias="couponIds") + """On a DISCOUNT line: the provider's discount (a Stripe coupon) of each allocation, in the + allocations' order + """ + + external_line_id: str | None = Field(default=None, alias="externalLineId") + """The provider's id of the line; absent on a DISCOUNT line, which the provider holds as + discounts on its targets + """ + + +class InvoiceLineTrueUp(KaitenModel): + billed_quantity: int = Field(alias="billedQuantity") + """The quantity its ADVANCE line billed at the previous boundary; 0 when it started after it""" + + max_quantity: int = Field(alias="maxQuantity") + """The highest quantity the attachment held in the elapsed period""" + + +class InvoicePreview(KaitenModel): + as_of: datetime = Field(alias="asOf") + """When the preview was composed""" + + boundary_at: datetime = Field(alias="boundaryAt") + """The boundary the invoice bills""" + + currency: str + """ISO 4217 code""" + + discount_total: int = Field(alias="discountTotal") + """Sum of the discounts, in minor units""" + + kind: InvoiceKind | str + """The boundary previewed""" + + license_slug: str = Field(alias="licenseSlug") + """The licence version previewed""" + + lines: list[InvoiceLine] + + service_from: datetime | None = Field(default=None, alias="serviceFrom") + """Earliest start of the lines' service periods; absent without lines""" + + service_to: datetime | None = Field(default=None, alias="serviceTo") + """Latest end of the lines' service periods; absent without lines""" + + status: Literal["PREVIEW"] + """Always PREVIEW""" + + subtotal: int + """Sum of the lines, in minor units""" + + total: int + """subtotal − discountTotal, in minor units""" + + would_hold: list[InvoiceHold] = Field(alias="wouldHold") + """Meters whose usage journal fails a consistency check, which would hold the invoice. + Always empty for a sample. + """ + + +class InvoiceSummary(KaitenModel): + boundary_at: datetime = Field(alias="boundaryAt") + """The boundary the invoice bills. With the kind, it identifies the invoice within its + subscription + """ + + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + + created_at: datetime = Field(alias="createdAt") + + currency: str + + customer_name: str = Field(alias="customerName") + + customer_slug: str = Field(alias="customerSlug") + """As it was when the invoice was composed""" + + days_until_due: int | None = Field(alias="daysUntilDue") + + discount_total: int = Field(alias="discountTotal") + """Sum of the discounts, in minor units""" + + due_at: datetime | None = Field(alias="dueAt") + """issuedAt + daysUntilDue""" + + handoff_status: HandoffStatus | str = Field(alias="handoffStatus") + """PENDING: waiting in the handoff queue for the organization's accounting system""" + + hold_reason: InvoiceHoldReason | str | None = Field(alias="holdReason") + """Why a DRAFT is held: the usage journal it was measured from failed a consistency check + (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT) + """ + + id: str + """The invoice's identifier: the key an accounting system deduplicates on""" + + instance_name: str = Field(alias="instanceName") + + instance_slug: str = Field(alias="instanceSlug") + + issued_at: datetime | None = Field(alias="issuedAt") + + kind: InvoiceKind | str + """ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the + one that starts, FINAL what is left when a subscription ends + """ + + license_slug: str = Field(alias="licenseSlug") + + overdue: bool + """Whether the invoice is overdue when read, the grace period of automatic collection + included: the predicate of ?overdue=true. Computed by the invoice list and the invoice + read; false in the answers of writes and in event payloads. + """ + + paid_at: datetime | None = Field(alias="paidAt") + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + """Who collects it. NOOP: the organization itself, through the handoff queue""" + + service_from: datetime = Field(alias="serviceFrom") + """Earliest start of its lines' service periods""" + + service_to: datetime = Field(alias="serviceTo") + """Latest end of its lines' service periods""" + + status: InvoiceStatus | str + """MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. + DRAFT: held, waiting for a release or a recompose + """ + + subtotal: int + """Sum of the lines, in minor units""" + + total: int + """subtotal − discountTotal, in minor units""" + + updated_at: datetime = Field(alias="updatedAt") + + +class License(KaitenModel): + """A version of a license family: one set of entitlement grants, with its lifecycle state.""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when this license was created""" + + description: str + """Detailed description of the license capabilities and restrictions""" + + family_id: str | None = Field(default=None, alias="familyId") + """Identifier of the license family this license is a version of. Stable across renames and + new versions: two licenses sharing it are the same product. Always present in responses. + On create, optional: names the family this license is a new version of, as familySlug + does by slug; omit both to create a new family. On update, optional and must be the + license's own family: a version cannot move between families + (UpdateLicense.FamilyNotReassignable). + """ + + family_slug: str | None = Field(default=None, alias="familySlug") + """Write-only on create: slug of the family this license is a new version of. familyId + names the same family by identifier; sent together they must agree. Omit both to create + a new family, of which this license becomes version 1. Never returned -- see familyId. + Refused on update: a version cannot move between families. + """ + + id: str + """Unique identifier for the license""" + + is_default: bool = Field(alias="isDefault") + """Whether this version is the one its family puts forward: at most one version per family, + and only a PUBLISHED one (CreateLicense.DefaultMustBePublished, + UpdateLicense.DefaultMustBePublished). Resolution hands it out ahead of the highest- + numbered published version, so it keeps being served when a newer version is published. + Setting it takes the flag from the family's previous default, which gets a + LICENSE_UPDATED event of its own. On update, sending false unsets it; the default cannot + be archived until another version takes its place or the flag is unset + (ArchiveLicense.DefaultMustBePublished). + """ + + lifecycle_state: LicenseLifecycleState | str | None = Field( + default=None, alias="lifecycleState" + ) + """Whether this version may be served. Always present in responses. Optional on create, + where it is DRAFT or PUBLISHED and PUBLISHED when omitted; ARCHIVED is refused + (CreateLicense.LifecycleStateNotSettable), since a version is archived by withdrawing it + from sale. After that it changes only through publish-license (DRAFT to PUBLISHED), + archive-license (PUBLISHED to ARCHIVED) and unarchive-license (ARCHIVED to PUBLISHED); + update accepts the stored state and refuses any other + (UpdateLicense.LifecycleStateNotSettable). Only a PUBLISHED version can be the family's + default. An ARCHIVED version cannot be assigned to an instance + (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); instances already on + it keep it. + """ + + name: str + """Display name of the license""" + + pricing_type: PricingType | str | None = Field(default=None, alias="pricingType") + """How this version is sold. FREE and PAID can be bought self-serve; CUSTOM sends a buyer + to selfServeCtaUrl or a conversation. Always present in responses. On create it defaults + to CUSTOM; on update, omit it or send null to keep the stored value. + """ + + requires_payment_method: bool | None = Field(default=None, alias="requiresPaymentMethod") + """Whether self-serve signup captures a payment method before activation. Always present in + responses; false when omitted on create. On update, omit it or send null to keep the + stored value. + """ + + self_serve_cta_url: str | None = Field(default=None, alias="selfServeCtaUrl") + """Where a buyer is sent when this version cannot be bought self-serve: an http(s) URL of + at most 2048 characters (CreateLicense.InvalidSelfServeCtaUrl). Null when there is none. + On update, omit it or send null to keep the stored value, or send an empty string to + remove it. + """ + + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. When omitted, a new version + of an existing family takes {familySlug}-v{version}, and any other license gets one + generated from its name. A license that opens a new family gives the family its slug. + """ + + trial_period_days: int | None = Field(default=None, alias="trialPeriodDays") + """Trial length, in days, a subscription to this version starts with by default. Null when + there is none. From 1 to 365 (CreateLicense.InvalidTrialDays, + UpdateLicense.InvalidTrialDays). On update, omit it or send null to keep the stored + value, or send 0 to remove the trial. + """ + + type: LicenseType | str + """Type of the license determining its capabilities and restrictions""" + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when this license was last updated""" + + version: str + """Version of the license within its family, assigned by the server when the version is + created and never changed afterwards: the version's derived slug + ({familySlug}-v{version}, see slug) is built from it. A number is never reused within a + family, even after its version is deleted. Create refuses any value + (CreateLicense.VersionNotSettable); update accepts only the stored one, so a + representation can be written back as it was read (UpdateLicense.VersionNotSettable). + """ + + version_name: str | None = Field(default=None, alias="versionName") + """Optional human-readable name for the license version. Defaults to "Version - ".""" + + +class LicenseEntitlement(KaitenModel): + """A grant: the value a license gives one entitlement, and how it is enforced.""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when this entitlement was created""" + + created_by: User = Field(alias="createdBy") + """User who created this entitlement""" + + entitlement_groups: list[EntitlementGroupSummary] | None = Field( + default=None, alias="entitlementGroups" + ) + """Groups associated with this entitlement""" + + entitlement_name: str = Field(alias="entitlementName") + """Name of the entitlement feature or service""" + + entitlement_slug: str | None = Field(default=None, alias="entitlementSlug") + """URL-friendly identifier for the entitlement""" + + entitlement_type: LicenseEntitlementType | str | None = Field( + default=None, alias="entitlementType" + ) + """Type of entitlement (BOOLEAN, NUMBER, or CONFIG)""" + + license_id: str = Field(alias="licenseId") + """Unique identifier for the license this entitlement belongs to""" + + license_slug: str = Field(alias="licenseSlug") + """URL-friendly identifier for the license this entitlement belongs to""" + + limit_cap_exceeded_overage_percent: int | None = Field( + default=None, alias="limitCapExceededOveragePercent" + ) + """Derives this grant's enforcement from its own numeric value: -1 when the value is + unlimited (no cap, no overage); otherwise 0 rejects usage above the value (hard limit) + and a positive percentage allows usage to exceed it by that much before being rejected + (soft limit). Null for non-numeric (BOOLEAN/CONFIG) grants. + """ + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when this entitlement was last updated""" + + updated_by: User = Field(alias="updatedBy") + """User who last updated this entitlement""" + + value: EntitlementValue + """Typed entitlement value. Supported shapes: {type:number,value:number}, + {type:boolean,value:boolean}, {type:object,value:{...}} + """ + + +class LicenseFamilyView(KaitenModel): + """A license family -- the product -- resolved to one of its versions.""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when this family was created""" + + current_version: License | None = Field(default=None, alias="currentVersion") + """The version this read is about. By default the version the family resolves to: its + default version if it has one, otherwise its highest-numbered PUBLISHED version. Under + ?version=N it is the version asked for instead, whatever its lifecycle state. Absent + when the family has no PUBLISHED version, which only a list or an include=versions read + returns. + """ + + fallback_license_family_slug: str | None = Field(alias="fallbackLicenseFamilySlug") + """The FREE family whose default PUBLISHED version an instance of this family moves to when + its subscription ends; null without one + """ + + id: str + """Unique identifier for the license family""" + + is_public: bool = Field(alias="isPublic") + """Whether the family's default PUBLISHED version is listed in the public catalogue. + Private by default. + """ + + slug: str + """URL-friendly identifier of the family, unique per organization and stable across renames + and new versions. Deleting the family's last version deletes the family and frees the + slug. + """ + + trial_end_without_payment_method: TrialEndWithoutPaymentMethod | str = Field( + alias="trialEndWithoutPaymentMethod" + ) + """What a trial charged automatically with a total above 0 becomes at its end when the + customer has no ACTIVE payment method: CANCEL (the default) or CONVERT + """ + + trial_reminder_days: int = Field(alias="trialReminderDays") + """How many days before a trial's end trial_will_end fires, for the trials started after it + was set. 3 by default. + """ + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when a version was last added to this family, or when the family was created. + Renames, lifecycle moves and default changes update the versions' own updatedAt, not + this one. + """ + + version_count: int = Field(alias="versionCount") + """Number of versions in this family, whatever their lifecycle state""" + + versions: list[License] | None = None + """Every version of the family, oldest first. Only returned when explicitly requested with + include=versions, and then never null: a family always has at least one version. + """ + + +class LineDifference(KaitenModel): + external_line_id: str = Field(alias="externalLineId") + + kaiten_amount: int = Field(alias="kaitenAmount") + + line_id: str = Field(alias="lineId") + + provider_amount: int = Field(alias="providerAmount") + + seq: int + + +class LineReportPage(KaitenModel): + items: list[UsageReport] + """The reports, in reportSeq order""" + + next_after_seq: int | None = Field(default=None, alias="nextAfterSeq") + """Pass as afterSeq to read the next page. Absent on the last page.""" + + +class ManifestEnvelope(KaitenModel): + flags: list[ManifestFlag] | None + + +class ManifestFlag(KaitenModel): + """One flag of the OpenFeature manifest.""" + + default_value: Any = Field(alias="defaultValue") + + description: str | None = None + + key: str + + name: str | None = None + + type: str + + +class MetadataField(KaitenModel): + """A typed field an organization declares on its instances or deployment zones.""" + + archived_at: datetime | None = Field(default=None, alias="archivedAt") + """Timestamp when the field was archived (soft delete). Absent if active.""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when the field was created""" + + created_by: User = Field(alias="createdBy") + """User who created this field""" + + display_order: int | None = Field(default=None, alias="displayOrder") + """Sort order in the admin UI; ties are broken by createdAt ASC. Settable only through POST + /metadata-fields/reorder; never echoed back on update. + """ + + id: str + """Unique identifier""" + + json_schema: dict[str, Any] = Field(alias="jsonSchema") + """JSON Schema 2020-12 document describing the value space. Example: + {"type":"string","enum":["a","b"]} + """ + + key: str + """Stable identifier, unique per (organization, resource type) on non-archived rows. + Immutable after creation; update must echo back the stored value. + """ + + label: str + """Human-readable label shown in the admin UI""" + + resource_type: MetadataResourceType | str = Field(alias="resourceType") + """Resource type this field applies to. Immutable after creation; update must echo back the + stored value. + """ + + updated_at: datetime = Field(alias="updatedAt") + """Timestamp when the field was last updated""" + + updated_by: User = Field(alias="updatedBy") + """User who last updated this field""" + + +class MetadataFieldImpact(KaitenModel): + """What a candidate metadata field schema would invalidate.""" + + count: int + """Number of resources whose stored value for this field would no longer satisfy the + candidate schema + """ + + samples: list[MetadataFieldImpactSample] | None + """Up to 5 impacted resources, for display""" + + +class MetadataFieldImpactSample(KaitenModel): + """A resource a metadata field schema change would invalidate.""" + + name: str + """Resource display name""" + + slug: str + """Resource slug""" + + +class MetadataFieldReordered(KaitenModel): + """Metadata fields were given a new display order.""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + ids: list[str] | None + """Metadata field ids in their new display order""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class MinimumAmount(KaitenModel): + currency: str + + unit_amount_decimal: str = Field(alias="unitAmountDecimal") + """In minor units, above 0""" + + +class NumberEntitlementValue(KaitenModel): + """A numeric entitlement value: a quota, or an amount consumed.""" + + event_count: int | None = None + """Number of events accumulated (server-computed)""" + + type: Literal["number"] + """Value type discriminator""" + + value: float + """Numeric value""" + + +class Outcome(KaitenModel): + applied: int + """Invoices read and applied""" + + error: str | None = None + + failed: int + """Invoices that could not be read or applied""" + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + + status: SyncStatus | str + + synced_at: datetime = Field(alias="syncedAt") + + +class OverageLimit(KaitenModel): + limit_value: str | None = Field(alias="limitValue") + """The limit, null when unlimited""" + + overage_percent: int = Field(alias="overagePercent") + """Usage accepted above the limit, as a percent of it""" + + rows: int + """Usage reports measured against it; 0 for a sample""" + + +class Payment(KaitenModel): + hosted_invoice_url: str | None = Field(default=None, alias="hostedInvoiceUrl") + + status: PaymentStatus | str + """paid. requires_action: the customer must confirm the payment on hostedInvoiceUrl. + failed: the charge was refused; save another payment method, or pay on hostedInvoiceUrl. + processing: the outcome is not known yet; poll GET /public/session/invoices. + not_required: nothing is due today. + """ + + +class PaymentMethodAttachedDelivered(KaitenModel): + attached_at: datetime = Field(alias="attachedAt") + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + customer_slug: str = Field(alias="customerSlug") + + provider_kind: Literal["STRIPE"] = Field(alias="providerKind") + + status: Literal["ACTIVE"] + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class PaymentMethodDetachedDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + customer_slug: str = Field(alias="customerSlug") + + provider_kind: Literal["STRIPE"] = Field(alias="providerKind") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class PaymentMethodExpiringDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + customer_slug: str = Field(alias="customerSlug") + + expires_at: datetime = Field(alias="expiresAt") + """The last instant of the payment method's expiry month""" + + provider_kind: Literal["STRIPE"] = Field(alias="providerKind") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class PaymentMethodLabels(KaitenModel): + attached_at: datetime | None = Field(alias="attachedAt") + + brand: str | None + + exp_month: int | None = Field(alias="expMonth") + + exp_year: int | None = Field(alias="expYear") + + last4: str | None + + status: PaymentMethodStatus | str + """ACTIVE: charged automatically. EXPIRED, FAILED: a charge said it is no longer usable; + save another one + """ + + +class PaymentMethodSession(KaitenModel): + expires_at: datetime = Field(alias="expiresAt") + + session_id: str = Field(alias="sessionId") + """Complete it with POST …/payment-method-session/{sessionId}/complete once the customer is + back + """ + + url: str + """The provider's page; send the customer there""" + + +class PlainToken(KaitenModel): + """A token together with its plaintext secret, which the API returns exactly once.""" + + created_at: datetime = Field(alias="createdAt") + + created_by: User = Field(alias="createdBy") + + expires_at: datetime | None = Field(default=None, alias="expiresAt") + + id: str | None = None + + name: str + + revoked_at: datetime | None = Field(default=None, alias="revokedAt") + + revoked_by: User | None = Field(default=None, alias="revokedBy") + + scopes: list[str] | None + + service_account_id: str | None = Field(default=None, alias="serviceAccountId") + + slug: str | None = None + """Optional URL-friendly identifier, unique per service account. Auto-generated if not + provided. + """ + + token: str | None = None + + +class PlanChangeDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + from_license_slug: str = Field(alias="fromLicenseSlug") + + from_price_id: str = Field(alias="fromPriceId") + + instance_slug: str = Field(alias="instanceSlug") + + invoice_id: str = Field(alias="invoiceId") + """The RENEWAL that billed the old plan's arrears and the new plan's advance""" + + to_license_slug: str = Field(alias="toLicenseSlug") + + to_price_id: str = Field(alias="toPriceId") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class PlanChangeScheduleDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + effective_at: datetime = Field(alias="effectiveAt") + + from_price_id: str = Field(alias="fromPriceId") + + instance_slug: str = Field(alias="instanceSlug") + + to_price_id: str = Field(alias="toPriceId") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class PortalCapabilities(KaitenModel): + checkout: bool + """POST /public/session/checkout: the instance has no live subscription and its plan can be + bought alone, or its live subscription is free (a FREE licence family, no add-on price + above 0) and the catalogue has a self-serve plan, which the checkout replaces it with + """ + + invoices: bool + """Billing is on: GET /public/session/invoices lists the customer's""" + + payment_methods: bool = Field(alias="paymentMethods") + """The vendor's provider captures payment methods: the payment-method and portal sessions + work + """ + + unsubscribe: bool + """The instance's subscription is live and not already ending: POST + /public/session/billing/cancel + """ + + +class PortalCustomer(KaitenModel): + billing_email: str | None = Field(alias="billingEmail") + """Where its invoices are sent""" + + name: str + + slug: str + + +class PortalEntitlement(KaitenModel): + current_period_end: datetime | None = Field(alias="currentPeriodEnd") + """The usage window's end; null for a lifetime quota""" + + current_period_start: datetime | None = Field(alias="currentPeriodStart") + """The usage window's start; null for a lifetime quota""" + + current_value: float | None = Field(alias="currentValue") + """A number's usage in its current window; null for BOOLEAN and CONFIG""" + + limit_cap_exceeded_overage_percent: int | None = Field(alias="limitCapExceededOveragePercent") + """How far above the limit usage is still accepted, in percent: 0 a hard limit; null for + BOOLEAN and CONFIG + """ + + limit_value: EntitlementValue | None = Field(alias="limitValue") + """The effective value: a number's limit (-1 unlimited), a BOOLEAN's flag, a CONFIG's object""" + + maximum_allowed_usage: float | None = Field(alias="maximumAllowedUsage") + """The most usage accepted, limit × (1 + percent ÷ 100); null when unlimited, and for + BOOLEAN and CONFIG + """ + + name: str + + percentage_used: float | None = Field(alias="percentageUsed") + """currentValue ÷ limit × 100; null when unlimited or the limit is 0, and for BOOLEAN and + CONFIG + """ + + provenance: Provenance | None + """What the licence, the add-ons and the boosts each contribute""" + + slug: str + + source: EntitlementSource | str + """license: the licence grants it; addon: an add-on does; exception: only the vendor's + exception for this instance does + """ + + type: EntitlementType | str + + unlimited: bool + + +class PortalInstance(KaitenModel): + license_name: str = Field(alias="licenseName") + + license_slug: str = Field(alias="licenseSlug") + + name: str + + slug: str + + +class PortalInstanceSummary(KaitenModel): + billing_status: SubscriptionStatus | str | None = Field(alias="billingStatus") + """Its subscription's status; null when it was never subscribed""" + + license_slug: str = Field(alias="licenseSlug") + + name: str + + slug: str + + +class PortalPaymentMethod(KaitenModel): + brand: str | None + + exp_month: int | None = Field(alias="expMonth") + + exp_year: int | None = Field(alias="expYear") + + last4: str | None + + status: PaymentMethodStatus | str + """EXPIRED, FAILED: save another one""" + + +class PortalSession(KaitenModel): + url: str + + +class PortalTrial(KaitenModel): + charge: PortalTrialCharge | None + """What the conversion would charge, discounts included, before any tax the provider adds; + null when nothing would be + """ + + ends_at: datetime = Field(alias="endsAt") + + payment_method: PortalTrialPayment | None = Field(alias="paymentMethod") + """The ACTIVE card the conversion would be charged to; null without one: ask for a card""" + + reminder_at: datetime | None = Field(alias="reminderAt") + """When trial_will_end is, or was, due""" + + +class PortalTrialCharge(KaitenModel): + amount: int + + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + + currency: str + + +class PortalTrialPayment(KaitenModel): + brand: str | None + + last4: str | None + + +class PortalUpcomingInvoice(KaitenModel): + boundary_at: datetime = Field(alias="boundaryAt") + """When it is issued""" + + currency: str + + discount_total: int = Field(alias="discountTotal") + """Minor units""" + + subtotal: int + """Minor units""" + + total: int + """Minor units""" + + +class Price(KaitenModel): + billing_model: BillingModel | str = Field(alias="billingModel") + """FLAT_FEE: an amount per billing period. USAGE_BASED: an amount per sale unit of the + metered entitlement's usage. OVERAGE: an amount per sale unit of the usage above the + licence's limit. + """ + + billing_period: BillingPeriod | str | None = Field(alias="billingPeriod") + """The period a FLAT_FEE price is charged for. Null on metered prices, which bill on the + subscription's period. + """ + + billing_timing: BillingTiming | str = Field(alias="billingTiming") + """ADVANCE bills the period that starts; ARREARS the period that ended. Metered prices are + always ARREARS. + """ + + created_at: datetime = Field(alias="createdAt") + + currency: str + """ISO 4217 code, upper case. One currency per licence version.""" + + deprecated_at: datetime | None = Field(alias="deprecatedAt") + """When the price was deprecated""" + + display_label: str | None = Field(alias="displayLabel") + """The invoice line's label; a default is derived when it is null.""" + + display_order: int = Field(alias="displayOrder") + """Order among the version's prices""" + + id: str + """Unique identifier of the price""" + + is_default: bool = Field(alias="isDefault") + """The FLAT_FEE price picked for its billing period when none is named. At most one per + period; only an ACTIVE FLAT_FEE price. + """ + + metered: PriceMeter | None + """What a metered price measures. Null on FLAT_FEE prices.""" + + status: PriceStatus | str + """A DEPRECATED price keeps billing what is already pinned to it but is no longer offered.""" + + unit_amount: int | None = Field(alias="unitAmount") + """unitAmountDecimal as an integer number of minor units, when it is one; null otherwise.""" + + unit_amount_decimal: str = Field(alias="unitAmountDecimal") + """Amount in the currency's minor units (cents for EUR): per period for FLAT_FEE, per sale + unit for metered prices. Up to 12 decimal places. + """ + + updated_at: datetime = Field(alias="updatedAt") + + +class PriceMeter(KaitenModel): + entitlement_slug: str = Field(alias="entitlementSlug") + """The metered entitlement""" + + sale_unit_factor: str = Field(alias="saleUnitFactor") + """Measured units in one sale unit, captured when the price was created (1 when the + entitlement has no sale unit): 10000 bills per 10,000 tokens. + """ + + sale_unit_plural: str | None = Field(alias="saleUnitPlural") + """The sale unit's plural label; null when the entitlement has none""" + + sale_unit_singular: str | None = Field(alias="saleUnitSingular") + """The sale unit's singular label; null when the entitlement has none""" + + +class Problem(KaitenModel): + """An RFC 9457 problem document, which every API error carries.""" + + code: str | None = None + """Stable, machine-readable error code.""" + + detail: str | None = None + """A human-readable explanation specific to this occurrence of the problem.""" + + error_id: str | None = Field(default=None, alias="errorId") + """Correlation id for the server-side log entry describing the withheld cause.""" + + errors: list[ErrorDetail] | None = None + """Optional list of individual error details""" + + instance: str | None = None + """A URI reference that identifies the specific occurrence of the problem.""" + + status: int | None = None + """HTTP status code""" + + title: str | None = None + """A short, human-readable summary of the problem type. This value should not change + between occurrences of the error. + """ + + type: str | None = None + """A URI reference to human-readable documentation for the error.""" + + +class Provenance(KaitenModel): + addons: list[ProvenanceAddon] + """The active add-on grants, in attachment order""" + + boosts: list[ProvenanceBoost] + """The boosts in their window, in redemption order""" + + exception: ProvenanceException | None + """The instance's exception in force at the read, the last layer; null without one. Never + its reason or author + """ + + license: ProvenanceLicense | None + """The licence's grant; null when only add-ons grant the entitlement""" + + number: ProvenanceNumber | None + """How a number was composed; null for BOOLEAN and CONFIG, and when no add-on, boost or + exception changes it + """ + + +class ProvenanceAddon(KaitenModel): + addon_entitlement_id: str = Field(alias="addonEntitlementId") + + addon_id: str = Field(alias="addonId") + + attached_at: datetime = Field(alias="attachedAt") + + instance_addon_id: str = Field(alias="instanceAddonId") + + limit_cap_exceeded_overage_percent: int | None = Field(alias="limitCapExceededOveragePercent") + + override_behavior: OverrideBehavior | str = Field(alias="overrideBehavior") + + quantity: int + + value: EntitlementValue + """One unit's grant""" + + +class ProvenanceBoost(KaitenModel): + effective_expires_at: datetime | None = Field(alias="effectiveExpiresAt") + + effective_starts_at: datetime = Field(alias="effectiveStartsAt") + + instance_voucher_id: str = Field(alias="instanceVoucherId") + + modifier_type: GrantModifier | str = Field(alias="modifierType") + + modifier_value: float | None = Field(alias="modifierValue") + + redeemed_at: datetime = Field(alias="redeemedAt") + + value: EntitlementValue | None + """The value a BOOLEAN or CONFIG SET boost sets, in the {type, value} shape; null for the + NUMBER modifiers + """ + + voucher_entitlement_grant_id: str = Field(alias="voucherEntitlementGrantId") + + voucher_id: str = Field(alias="voucherId") + + +class ProvenanceException(KaitenModel): + ends_at: datetime | None = Field(alias="endsAt") + """Null: no end""" + + instance_entitlement_exception_id: str = Field(alias="instanceEntitlementExceptionId") + + kind: ExceptionKind | str + + starts_at: datetime = Field(alias="startsAt") + + value: EntitlementValue | None + """The exception's value, in the {type, value} shape; null for UNLIMITED""" + + +class ProvenanceLicense(KaitenModel): + license_entitlement_id: str = Field(alias="licenseEntitlementId") + + limit_cap_exceeded_overage_percent: int | None = Field(alias="limitCapExceededOveragePercent") + + value: EntitlementValue + + +class ProvenanceNumber(KaitenModel): + after_addons: float = Field(alias="afterAddons") + + after_boosts: float = Field(alias="afterBoosts") + """The value before the exception: -1 when unlimited""" + + boost_add: float | None = Field(alias="boostAdd") + + boost_multiply: float | None = Field(alias="boostMultiply") + + boost_set: float | None = Field(alias="boostSet") + + effective: float + """-1 when unlimited""" + + license: float | None + """The licence's value; null when only add-ons or an exception grant it""" + + unlimited: bool + """The licence, an add-on or a boost is unlimited; only an exception's SET makes it finite + again + """ + + +class ProviderCapabilities(KaitenModel): + automatic_collection: bool = Field(alias="automaticCollection") + + billing_portal: bool = Field(alias="billingPortal") + + payment_method_capture: bool = Field(alias="paymentMethodCapture") + + +class ProviderChangeDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + effective_from: Literal["next_composition"] = Field(alias="effectiveFrom") + """Invoices already composed keep their provider""" + + from_: BillingProviderKind | str = Field(alias="from") + + instance_slug: str = Field(alias="instanceSlug") + + to: BillingProviderKind | str + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class ProviderLineLimit(KaitenModel): + items: int + """The provider items the invoice needs""" + + limit: int + """The most the provider takes on one invoice""" + + provider_kind: Literal["STRIPE"] = Field(alias="providerKind") + + +class ProviderRecord(KaitenModel): + external_customer_id: str | None = Field(alias="externalCustomerId") + + external_invoice_id: str | None = Field(alias="externalInvoiceId") + + hosted_invoice_url: str | None = Field(alias="hostedInvoiceUrl") + + invoice_number: str | None = Field(alias="invoiceNumber") + """The provider's invoice number""" + + invoice_pdf_url: str | None = Field(alias="invoicePdfUrl") + + last_payment_error: str | None = Field(alias="lastPaymentError") + """Why the last automatic charge failed, as the provider coded it: authentication_required + (the customer must confirm the payment on the hosted invoice page), card_declined, + expired_card, no_payment_method... + """ + + last_push_error: str | None = Field(alias="lastPushError") + """The provider's code and message of the last failed push step""" + + next_push_at: datetime | None = Field(alias="nextPushAt") + """When the push queue tries it next; null when it waits for a human (finalization in the + provider) or is pushed + """ + + push_attempts: int = Field(alias="pushAttempts") + + pushed_at: datetime | None = Field(alias="pushedAt") + + reconciled_at: datetime | None = Field(alias="reconciledAt") + + reconciliation_detail: Reconciliation | None = Field(alias="reconciliationDetail") + """What differs, on a MISMATCH""" + + reconciliation_status: ReconciliationStatus | str | None = Field(alias="reconciliationStatus") + + status: ProviderInvoiceStatus | str | None + """The provider's status, mirrored verbatim""" + + synced_at: datetime | None = Field(alias="syncedAt") + + total_excluding_tax: int | None = Field(alias="totalExcludingTax") + """The provider's total excluding tax, read back""" + + +class ProviderSync(KaitenModel): + consecutive_failures: int = Field(alias="consecutiveFailures") + + lag_seconds: int | None = Field(alias="lagSeconds") + """Seconds since the last pass; null before the first""" + + last_full_sweep_at: datetime | None = Field(alias="lastFullSweepAt") + + last_successful_sync_at: datetime | None = Field(alias="lastSuccessfulSyncAt") + """The end of the last pass that ended SUCCESS; null before the first (V31-11)""" + + last_sync_error: str | None = Field(alias="lastSyncError") + + last_sync_status: SyncStatus | str | None = Field(alias="lastSyncStatus") + + last_synced_at: datetime | None = Field(alias="lastSyncedAt") + """The end of the last pass, whatever its status; null before the first""" + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + + +class PublicAddOn(KaitenModel): + addon_id: str = Field(alias="addonId") + + addon_slug: str = Field(alias="addonSlug") + + compatible_license_families: list[str] = Field(alias="compatibleLicenseFamilies") + """Public licence families this add-on fits""" + + description: str + + entitlements: list[PublicAddOnEntitlement] + """User-facing entitlements one unit grants""" + + family_slug: str = Field(alias="familySlug") + + max_quantity: int | None = Field(alias="maxQuantity") + """Most units one instance may attach; null when unbounded""" + + name: str + + prices: list[PublicPrice] + + pricing_type: PricingType | str = Field(alias="pricingType") + + +class PublicAddOnEntitlement(KaitenModel): + limit_cap_exceeded_overage_percent: int | None = Field(alias="limitCapExceededOveragePercent") + + name: str + + override_behavior: OverrideBehavior | str = Field(alias="overrideBehavior") + """How the grant combines with the plan's: added to it, replacing it, or the larger of the + two + """ + + slug: str + + type: EntitlementType | str + + value: Any + + +class PublicBranding(KaitenModel): + powered_by_removable: bool = Field(alias="poweredByRemovable") + """Whether the vendor's plan lets its storefront and portal hide "Powered by Kaiten" (the + remove-powered-by entitlement). False while Kaiten's licensing authority cannot answer; + always true self-hosted + """ + + +class PublicCatalog(KaitenModel): + add_ons: list[PublicAddOn] = Field(alias="addOns") + """The default version of each public add-on family; empty when include omits addons""" + + branding: PublicBranding + + capabilities: PublicCatalogCapabilities + + plans: list[PublicPlan] + """The default version of each public licence family, by family slug""" + + +class PublicCatalogCapabilities(KaitenModel): + anonymous_checkout: bool = Field(alias="anonymousCheckout") + """Whether this publishable key starts anonymous checkouts (POST /public/checkout- + sessions): on, with a Turnstile site key and secret + """ + + checkout: bool + """Whether a plan of the catalogue can be bought alone, through a customer session's + checkout (POST /public/session/checkout) + """ + + payment_methods: bool = Field(alias="paymentMethods") + """Whether the organization's billing provider captures payment methods""" + + turnstile_site_key: str | None = Field(alias="turnstileSiteKey") + """The Turnstile site key the page renders its widget with; null unless anonymousCheckout""" + + +class PublicMeter(KaitenModel): + entitlement_slug: str = Field(alias="entitlementSlug") + + sale_unit_factor: str = Field(alias="saleUnitFactor") + """Measured units in one sale unit""" + + sale_unit_plural: str | None = Field(alias="saleUnitPlural") + """The sale unit's plural label; null when the entitlement has none""" + + sale_unit_singular: str | None = Field(alias="saleUnitSingular") + """The sale unit's singular label; null when the entitlement has none""" + + +class PublicPlan(KaitenModel): + compatible_addon_families: list[str] = Field(alias="compatibleAddonFamilies") + """Public add-on families whose listed version fits this plan""" + + currency: str | None + """ISO 4217 code of the plan's prices; null when it has none""" + + description: str + + entitlements: list[PublicPlanEntitlement] + """User-facing entitlements the plan grants, in display order""" + + family_slug: str = Field(alias="familySlug") + + license_id: str = Field(alias="licenseId") + + license_slug: str = Field(alias="licenseSlug") + """The version's own slug""" + + lifecycle_state: Literal["PUBLISHED"] = Field(alias="lifecycleState") + """Always PUBLISHED: only the default version is listed, and a default version is published""" + + name: str + + prices: list[PublicPrice] + """ACTIVE prices, in display order""" + + pricing_type: PricingType | str = Field(alias="pricingType") + """CUSTOM: priced on request; render selfServeCtaUrl""" + + requires_payment_method: bool = Field(alias="requiresPaymentMethod") + """A card is captured before subscribing, even when nothing is due""" + + self_serve: bool = Field(alias="selfServe") + """Whether a customer can subscribe to this plan alone: not CUSTOM, with a default ACTIVE + flat-fee price, and either FREE or sold through a provider that captures payment + methods. Otherwise render selfServeCtaUrl. + """ + + self_serve_cta_url: str | None = Field(alias="selfServeCtaUrl") + """Where to send a customer who cannot subscribe alone ("Contact us")""" + + trial_period_days: int | None = Field(alias="trialPeriodDays") + """Length of the free trial; null when the plan has none""" + + +class PublicPlanEntitlement(KaitenModel): + description: str | None + + display_order: int = Field(alias="displayOrder") + + icon: str | None + + limit_cap_exceeded_overage_percent: int | None = Field(alias="limitCapExceededOveragePercent") + """How far usage may exceed a NUMBER grant before it is refused: 0 is a hard limit, -1 is + unlimited; null for other types + """ + + name: str + + sale_unit_factor: str | None = Field(alias="saleUnitFactor") + + sale_unit_plural: str | None = Field(alias="saleUnitPlural") + + sale_unit_singular: str | None = Field(alias="saleUnitSingular") + + slug: str + + type: EntitlementType | str + + unit_plural: str | None = Field(alias="unitPlural") + + unit_singular: str | None = Field(alias="unitSingular") + + unlimited: bool + """A NUMBER grant with no limit""" + + value: Any + """The granted value: a boolean, a number, or a configuration object""" + + +class PublicPrice(KaitenModel): + billing_model: BillingModel | str = Field(alias="billingModel") + """FLAT_FEE: per billing period. USAGE_BASED: per sale unit of usage. OVERAGE: per sale + unit of usage above the limit. Never sum metered prices into a headline. + """ + + billing_period: BillingPeriod | str | None = Field(alias="billingPeriod") + """The period a FLAT_FEE price is charged for; null on metered prices""" + + billing_timing: BillingTiming | str = Field(alias="billingTiming") + + currency: str + + display_label: str | None = Field(alias="displayLabel") + """The price's label; null when it has none""" + + display_order: int = Field(alias="displayOrder") + + id: str + + is_default: bool = Field(alias="isDefault") + """The FLAT_FEE price shown for its billing period""" + + metered: PublicMeter | None + """What a metered price measures; null on FLAT_FEE prices""" + + unit_amount: int | None = Field(alias="unitAmount") + """The amount in minor units, when it is a whole number of them; null otherwise (render + unitAmountDecimal) + """ + + unit_amount_decimal: str = Field(alias="unitAmountDecimal") + """The amount in minor units, up to 12 decimal places""" + + +class PublicSurface(KaitenModel): + enabled: bool + """Whether publishable keys and customer sessions work for the organization: billing is on + for it + """ + + +class PublishableKey(KaitenModel): + allowed_origins: list[str] = Field(alias="allowedOrigins") + """Browser origins allowed to send the key. Empty: no browser origin may; requests without + an Origin header (server-side rendering) still may. + """ + + anonymous_checkout: bool = Field(alias="anonymousCheckout") + """Whether the key may start an anonymous checkout (POST /public/checkout-sessions). Needs + a Turnstile site key and secret. + """ + + created_at: datetime = Field(alias="createdAt") + + id: str + """Unique identifier of the key""" + + key_hint: str = Field(alias="keyHint") + """The last four characters of the key""" + + label: str + """What the key is for, to tell keys apart""" + + last_used_at: datetime | None = Field(alias="lastUsedAt") + """When the key last authenticated a request, to the minute; null before its first""" + + revoked_at: datetime | None = Field(alias="revokedAt") + """When the key was revoked; null while it is live""" + + turnstile_secret_configured: bool = Field(alias="turnstileSecretConfigured") + """Whether a Turnstile secret is stored for the key. The secret itself is write-only.""" + + turnstile_site_key: str | None = Field(alias="turnstileSiteKey") + """The Turnstile site key the page renders its widget with; null without one. Public: the + catalogue hands it to the page. + """ + + updated_at: datetime = Field(alias="updatedAt") + + +class PublishableKeyCreated(KaitenModel): + allowed_origins: list[str] = Field(alias="allowedOrigins") + """Browser origins allowed to send the key. Empty: no browser origin may; requests without + an Origin header (server-side rendering) still may. + """ + + anonymous_checkout: bool = Field(alias="anonymousCheckout") + """Whether the key may start an anonymous checkout (POST /public/checkout-sessions). Needs + a Turnstile site key and secret. + """ + + created_at: datetime = Field(alias="createdAt") + + id: str + """Unique identifier of the key""" + + key: str + """The key itself. Shown once: only its digest is stored.""" + + key_hint: str = Field(alias="keyHint") + """The last four characters of the key""" + + label: str + """What the key is for, to tell keys apart""" + + last_used_at: datetime | None = Field(alias="lastUsedAt") + """When the key last authenticated a request, to the minute; null before its first""" + + revoked_at: datetime | None = Field(alias="revokedAt") + """When the key was revoked; null while it is live""" + + turnstile_secret_configured: bool = Field(alias="turnstileSecretConfigured") + """Whether a Turnstile secret is stored for the key. The secret itself is write-only.""" + + turnstile_site_key: str | None = Field(alias="turnstileSiteKey") + """The Turnstile site key the page renders its widget with; null without one. Public: the + catalogue hands it to the page. + """ + + updated_at: datetime = Field(alias="updatedAt") + + +class PublishableKeyEventDelivered(KaitenModel): + allowed_origins: list[str] = Field(alias="allowedOrigins") + + anonymous_checkout: bool = Field(alias="anonymousCheckout") + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + id: str + + key_hint: str = Field(alias="keyHint") + """The last four characters of the key""" + + label: str + + revoked_at: datetime | None = Field(alias="revokedAt") + + turnstile_site_key: str | None = Field(alias="turnstileSiteKey") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class PushFailures(KaitenModel): + count: int + + oldest_failed_at: datetime | None = Field(alias="oldestFailedAt") + + +class QueuedInvoice(KaitenModel): + boundary_at: datetime = Field(alias="boundaryAt") + """The boundary the invoice bills. With the kind, it identifies the invoice within its + subscription + """ + + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + + created_at: datetime = Field(alias="createdAt") + + currency: str + + customer_name: str = Field(alias="customerName") + + customer_slug: str = Field(alias="customerSlug") + """As it was when the invoice was composed""" + + days_until_due: int | None = Field(alias="daysUntilDue") + + discount_total: int = Field(alias="discountTotal") + """Sum of the discounts, in minor units""" + + due_at: datetime | None = Field(alias="dueAt") + """issuedAt + daysUntilDue""" + + handoff: InvoiceHandoff + + handoff_status: HandoffStatus | str = Field(alias="handoffStatus") + """PENDING: waiting in the handoff queue for the organization's accounting system""" + + hold_reason: InvoiceHoldReason | str | None = Field(alias="holdReason") + """Why a DRAFT is held: the usage journal it was measured from failed a consistency check + (LEDGER_*), or its provider cannot take that many lines (PROVIDER_ITEM_LIMIT) + """ + + id: str + """The invoice's identifier: the key an accounting system deduplicates on""" + + instance_name: str = Field(alias="instanceName") + + instance_slug: str = Field(alias="instanceSlug") + + issued_at: datetime | None = Field(alias="issuedAt") + + kind: InvoiceKind | str + """ACTIVATION bills a subscription's first period, RENEWAL the period that ended and the + one that starts, FINAL what is left when a subscription ends + """ + + license_slug: str = Field(alias="licenseSlug") + + overdue: bool + """Whether the invoice is overdue when read, the grace period of automatic collection + included: the predicate of ?overdue=true. Computed by the invoice list and the invoice + read; false in the answers of writes and in event payloads. + """ + + paid_at: datetime | None = Field(alias="paidAt") + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + """Who collects it. NOOP: the organization itself, through the handoff queue""" + + service_from: datetime = Field(alias="serviceFrom") + """Earliest start of its lines' service periods""" + + service_to: datetime = Field(alias="serviceTo") + """Latest end of its lines' service periods""" + + status: InvoiceStatus | str + """MANUAL: issued, for the organization to collect. PAID, UNCOLLECTIBLE and VOID are final. + DRAFT: held, waiting for a release or a recompose + """ + + subtotal: int + """Sum of the lines, in minor units""" + + total: int + """subtotal − discountTotal, in minor units""" + + updated_at: datetime = Field(alias="updatedAt") + + +class Reconciliation(KaitenModel): + discounts: list[DiscountDifference] + """Allocations of DISCOUNT lines the provider applied with another amount, or not at all""" + + due_date: DueDateDifference | None = Field(alias="dueDate") + """A SEND_INVOICE invoice the provider gives another due date than Kaiten's dueAt""" + + extra_discounts: list[ExtraDiscount] = Field(alias="extraDiscounts") + """Discounts the provider applied that Kaiten did not create, such as a coupon added in its + dashboard + """ + + extra_in_provider: list[str] = Field(alias="extraInProvider") + """Provider lines Kaiten does not have""" + + inclusive_tax: bool = Field(alias="inclusiveTax") + """Whether the provider's subtotal was compared, its tax being included in the amounts""" + + lines: list[LineDifference] + + missing_in_provider: list[str] = Field(alias="missingInProvider") + """Kaiten lines the provider does not have""" + + totals: TotalsDifference + + warnings: list[str] | None = None + """What the comparison noticed without calling it a mismatch (§12.7, V31-60)""" + + +class Redemption(KaitenModel): + applications_count: int = Field(alias="applicationsCount") + """Invoices a PRICE voucher discounted""" + + applications_max: int | None = Field(alias="applicationsMax") + """1 for ONE_TIME, durationInPeriods for REPEATING; null for FOREVER""" + + code_hint: str = Field(alias="codeHint") + + effective_expires_at: datetime | None = Field(alias="effectiveExpiresAt") + """When a boost stops applying; null for a PRICE voucher, counted in invoices, and for + FOREVER + """ + + effective_starts_at: datetime = Field(alias="effectiveStartsAt") + + expired_at: datetime | None = Field(alias="expiredAt") + + id: str + + instance_slug: str = Field(alias="instanceSlug") + + redeemed_at: datetime = Field(alias="redeemedAt") + + revoked_at: datetime | None = Field(alias="revokedAt") + + revoked_reason: str | None = Field(alias="revokedReason") + + status: RedemptionStatus | str + + voucher_id: str = Field(alias="voucherId") + + voucher_name: str = Field(alias="voucherName") + + voucher_type: VoucherType | str = Field(alias="voucherType") + + +class RedemptionRules(KaitenModel): + annual_only: bool | None = Field(default=None, alias="annualOnly") + """Only for an instance with a live ANNUAL subscription""" + + first_time_only: bool | None = Field(default=None, alias="firstTimeOnly") + """Only for a customer none of whose instances has paid an invoice""" + + minimum_subscription_amount: MinimumAmount | None = Field( + default=None, alias="minimumSubscriptionAmount" + ) + """Only for a live subscription whose base price is at least this. Strictly positive; omit + it for no minimum (0 is refused with .InvalidRedemptionRules). + """ + + +class Release(KaitenModel): + """An immutable, versioned set of components.""" + + component_ids: list[str] | None = Field(default=None, alias="componentIds") + """List of component IDs to link to this release""" + + components: list[Component] | None = None + """List of components included in this release, fixed at creation""" + + created_at: datetime = Field(alias="createdAt") + """Timestamp when the release was created""" + + created_by: User = Field(alias="createdBy") + """User who created this release""" + + description: str | None = None + """Description of the release""" + + id: str + """Unique identifier for the release""" + + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + + version: str + """Version of the release""" + + +class ReportedEntitlementUsage(KaitenModel): + current_period_end: datetime | None = Field(default=None, alias="currentPeriodEnd") + """End of the current usage window (exclusive). Null for a lifetime entitlement (no + configured reset period). + """ + + current_period_start: datetime | None = Field(default=None, alias="currentPeriodStart") + """Start of the current usage window (inclusive). Null for a lifetime entitlement (no + configured reset period). + """ + + entitlement_id: str = Field(alias="entitlementId") + """Unique identifier for the entitlement""" + + entitlement_slug: str = Field(alias="entitlementSlug") + """URL-friendly identifier of the entitlement""" + + license_id: str = Field(alias="licenseId") + """Unique identifier for the license to which this entitlement usage belongs""" + + license_slug: str = Field(alias="licenseSlug") + """URL-friendly identifier of the license""" + + limit: EntitlementValue | None = None + """The effective value this usage is measured against (the licence's grant, with the + instance's add-ons and boosts applied; source and provenance say which), discriminated + by the 'type' field. Absent when nothing grants the entitlement a value; -1 means + unlimited for a NUMBER entitlement. + """ + + limit_cap_exceeded_overage_percent: int | None = Field( + default=None, alias="limitCapExceededOveragePercent" + ) + """How far above limit usage is still accepted, in percent: -1 unlimited, 0 a hard limit. + Absent for BOOLEAN and CONFIG. + """ + + provenance: Provenance | None + """What the licence, the add-ons and the boosts each contribute. Null on a replay + (Idempotent-Replayed): the journal does not store it. + """ + + source: EntitlementSource | str | None + """license: the licence grants it (add-ons, boosts and an exception may change it); addon: + an add-on attached does; exception: only the instance's exception does. Null on a replay + (Idempotent-Replayed): the journal does not store it. + """ + + transaction_id: str | None = Field(alias="transactionId") + """The Idempotency-Key this report was accepted under, echoed on the first answer and on + every replay; null when the request carried none. A client can tell from it that the + server honours the key. + """ + + value: EntitlementValue + """Resolved entitlement value, discriminated by the 'type' field""" + + +class RolloutDateTargeting(KaitenModel): + """A targeting rule that shifts traffic between variants over a date range.""" + + end: RolloutStep + + name: str + + rule: str + """CEL expression for basicTargeting evaluation""" + + start: RolloutStep + + type: Literal["rollout_date"] + + +class RolloutDateVariant(KaitenModel): + """A default variant that shifts traffic between variants over a date range.""" + + end: RolloutStep + + start: RolloutStep + + type: Literal["rollout_date"] + + +class RolloutPercentageTargeting(KaitenModel): + """A targeting rule that splits the contexts it matches between variants.""" + + distribution: dict[str, int] + + name: str + + rule: str + """CEL expression for basicTargeting evaluation""" + + type: Literal["rollout_percentage"] + + +class RolloutPercentageVariant(KaitenModel): + """A default variant that splits traffic between variants by weight.""" + + distribution: dict[str, int] + + type: Literal["rollout_percentage"] + + +class RolloutStep(KaitenModel): + """One end of a date-based rollout.""" + + date: datetime + + percentage: int + + variant: str + + +class ScheduledChange(KaitenModel): + effective_at: datetime = Field(alias="effectiveAt") + """The boundary it applies at: the current period's end""" + + license_slug: str = Field(alias="licenseSlug") + """The licence version of that price""" + + price: Price + """The FLAT_FEE price the subscription moves to""" + + scheduled_at: datetime = Field(alias="scheduledAt") + + +class ServiceAccount(KaitenModel): + """A machine identity inside an organization, owning API tokens.""" + + created_at: datetime | None = Field(default=None, alias="createdAt") + + created_by: User | None = Field(default=None, alias="createdBy") + + external_id: str | None = Field(default=None, alias="externalId") + + id: str | None = None + + name: str + + slug: str | None = None + """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + + tokens: list[Token] | None + + +class SessionAddon(KaitenModel): + addon_slug: str = Field(alias="addonSlug") + + family_slug: str = Field(alias="familySlug") + + max_quantity: int | None = Field(alias="maxQuantity") + """The most one instance may hold; null when unbounded""" + + name: str + + pending_quantity: int | None = Field(alias="pendingQuantity") + """A decrease scheduled for the next renewal, 0 for a removal; null when nothing is scheduled""" + + prices: list[PublicPrice] + """What it is billed: the flat fee of the subscription's period and the metered prices. + Quantity changes are billed from the next renewal, with no proration. + """ + + quantity: int + """0 when the add-on is not held""" + + +class SessionAddonSelection(KaitenModel): + addon_slug: str = Field(alias="addonSlug") + + quantity: int + + +class SessionBoost(KaitenModel): + entitlement_slug: str = Field(alias="entitlementSlug") + + modifier_type: GrantModifier | str = Field(alias="modifierType") + + modifier_value: str | None = Field(default=None, alias="modifierValue") + """Absent for UNLIMITED, and on a BOOLEAN or CONFIG boost""" + + value: bool | dict[str, Any] | None = None + """A BOOLEAN or CONFIG boost's value: true or false, or the document""" + + +class SessionCheckout(KaitenModel): + activation_invoice: SessionInvoice | None = Field(default=None, alias="activationInvoice") + """subscribed: the invoice of the first period; absent during a trial""" + + due_today: DueToday | None = Field(default=None, alias="dueToday") + """preview: what the checkout bills today; zero, with no lines, during a trial""" + + expires_at: datetime | None = Field(default=None, alias="expiresAt") + """requires_payment_method: when the setup page expires""" + + payment: Payment | None = None + """subscribed: how paying the activation invoice went""" + + setup_session_id: str | None = Field(default=None, alias="setupSessionId") + """requires_payment_method: send it back as setupSessionId once the customer returns""" + + setup_url: str | None = Field(default=None, alias="setupUrl") + """requires_payment_method: the provider's page where the customer saves a payment method""" + + status: SessionCheckoutStatus | str + + subscription: SessionSubscription | None = None + """subscribed: the subscription started""" + + trial_ends_at: datetime | None = Field(default=None, alias="trialEndsAt") + """preview: when the trial would end, if there is one""" + + +class SessionDiscount(KaitenModel): + applies_to: DiscountAppliesTo | str = Field(alias="appliesTo") + + currency: str | None = None + """The currency of a FIXED_AMOUNT discount""" + + type: DiscountType | str + + value: str + """A percentage in (0, 100], or an amount in minor units of currency""" + + +class SessionEventDelivered(KaitenModel): + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + client_reference_id: str | None = Field(alias="clientReferenceId") + """The vendor's opaque reference, as given at the start""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + customer_slug: str | None = Field(alias="customerSlug") + """The customer the completion created; null before""" + + email: str + """The address the visitor gave at the start""" + + instance_slug: str | None = Field(alias="instanceSlug") + """The instance the completion created; null before""" + + license_slug: str = Field(alias="licenseSlug") + + session_id: str = Field(alias="sessionId") + + status: CheckoutSessionEventStatus | str + """OPEN on started, COMPLETED on completed; on abandoned, ABANDONED (never completed, slugs + null) or EXPIRED (its INCOMPLETE subscription expired or was canceled unpaid, slugs set) + """ + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class SessionInvoice(KaitenModel): + currency: str + + discount_total: int = Field(alias="discountTotal") + """Minor units""" + + due_at: datetime | None = Field(alias="dueAt") + + external_reference: str | None = Field(alias="externalReference") + """The vendor's own number for an invoice it collects itself""" + + hosted_invoice_url: str | None = Field(alias="hostedInvoiceUrl") + """Where the customer pays the invoice, or confirms a payment that needs authenticating""" + + id: str + + invoice_pdf_url: str | None = Field(alias="invoicePdfUrl") + + issued_at: datetime | None = Field(alias="issuedAt") + + kind: InvoiceKind | str + + lines: list[SessionLine] + + paid_at: datetime | None = Field(alias="paidAt") + + provider_invoice_number: str | None = Field(alias="providerInvoiceNumber") + + service_from: datetime = Field(alias="serviceFrom") + + service_to: datetime = Field(alias="serviceTo") + + status: SessionInvoiceStatus | str + """PUSHED: issued and awaiting payment. PAYMENT_FAILED: a charge failed; pay on + hostedInvoiceUrl. MANUAL: issued, the vendor collects it. + """ + + subtotal: int + """Minor units""" + + total: int + """Minor units""" + + +class SessionLine(KaitenModel): + amount: int + """Minor units; negative on a DISCOUNT line""" + + description: str + + label: str + + quantity: str + """In sale units, a decimal string""" + + seq: int + + service_from: datetime = Field(alias="serviceFrom") + + service_to: datetime = Field(alias="serviceTo") + + type: InvoiceLineType | str + + unit_amount_decimal: str | None = Field(alias="unitAmountDecimal") + """Minor units; null on a DISCOUNT line""" + + +class SessionPortal(KaitenModel): + add_ons: list[SessionAddon] = Field(alias="addOns") + """The add-ons the instance holds""" + + branding: PublicBranding + + capabilities: PortalCapabilities + + customer: PortalCustomer + + entitlements: list[PortalEntitlement] + """The instance's effective entitlements: its licence's, raised or added by add-ons and + boosts + """ + + instance: PortalInstance | None + """The session's instance; null for a session bound to the customer only""" + + instances: list[PortalInstanceSummary] + """The customer's instances, for a session bound to the customer only; empty otherwise""" + + payment_method: PortalPaymentMethod | None = Field(alias="paymentMethod") + """The payment method the customer's invoices are charged to; null when none is saved""" + + subscription: SessionSubscription | None + """The instance's subscription; null when it has none, or the session is bound to no instance""" + + trial: PortalTrial | None + """TRIAL only: when the trial ends, when its reminder is due, what the conversion would + charge and to which card (V32-09); null otherwise + """ + + upcoming_invoice: PortalUpcomingInvoice | None = Field(alias="upcomingInvoice") + """What the next boundary will bill, on usage so far; null without a live subscription""" + + updated_at: datetime = Field(alias="updatedAt") + """When this snapshot was read""" + + vouchers: list[SessionRedemption] + """The instance's ACTIVE redemptions. Never a code.""" + + +class SessionRedemption(KaitenModel): + applications_count: int = Field(alias="applicationsCount") + """Invoices its PRICE part discounted; 0 without one""" + + applications_max: int | None = Field(alias="applicationsMax") + """How many it discounts in all: 1 for ONE_TIME, its periods for REPEATING; null for + FOREVER and without a PRICE part + """ + + applies_from_invoice: Literal["next"] | None = Field(alias="appliesFromInvoice") + """next: a PRICE part that has not discounted an invoice yet -- it applies from the next + invoice composed, never to one already issued; null otherwise + """ + + effective_expires_at: datetime | None = Field(alias="effectiveExpiresAt") + """When a boost or a flag grant stops applying; null when it does not expire, and for a + PRICE voucher, counted in invoices + """ + + effective_starts_at: datetime = Field(alias="effectiveStartsAt") + + name: str + + status: RedemptionStatus | str + + voucher_type: VoucherType | str = Field(alias="voucherType") + + +class SessionScheduledChange(KaitenModel): + effective_at: datetime = Field(alias="effectiveAt") + """When it applies: the current period's end""" + + price: PublicPrice + """The price the subscription moves to""" + + +class SessionSubscription(KaitenModel): + base_price: PublicPrice = Field(alias="basePrice") + + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + + cancel_at_period_end: bool = Field(alias="cancelAtPeriodEnd") + """The subscription ends at currentPeriodEnd; reactivate it before then to keep it""" + + canceled_at: datetime | None = Field(alias="canceledAt") + """When the subscription was canceled""" + + cancellation_reason: CancellationReason | str | None = Field(alias="cancellationReason") + """Why the system ended it; null when it did not, a vendor's own reason is never shown""" + + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + + currency: str + + current_period_end: datetime = Field(alias="currentPeriodEnd") + """When the current period ends: the renewal date, or the trial's end""" + + current_period_start: datetime = Field(alias="currentPeriodStart") + + incomplete_expires_at: datetime | None = Field(alias="incompleteExpiresAt") + """INCOMPLETE only: when the checkout ends unpaid""" + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + + scheduled_change: SessionScheduledChange | None = Field(alias="scheduledChange") + """A plan change the vendor scheduled for the next boundary""" + + status: SubscriptionStatus | str + + trial_ends_at: datetime | None = Field(alias="trialEndsAt") + + +class SessionVoucher(KaitenModel): + boosts: list[SessionBoost] | None = None + """ENTITLEMENT_BOOST: what it raises""" + + discount: SessionDiscount | None = None + """PRICE: what it takes off""" + + duration: VoucherDuration | str + """PRICE: how many invoices it discounts (one, durationInPeriods, or every one). + ENTITLEMENT_BOOST and FLAG_GRANT: how long it lasts, in billing periods. COMPOSITE: + both, each for its part, which it carries each of. + """ + + duration_in_periods: int | None = Field(default=None, alias="durationInPeriods") + + flag_grants: list[FlagGrant] | None = Field(default=None, alias="flagGrants") + """FLAG_GRANT: the flag variants it grants""" + + name: str + + voucher_type: VoucherType | str = Field(alias="voucherType") + + +class SessionVoucherValidity(KaitenModel): + valid: bool + + voucher: SessionVoucher | None = None + """What the voucher gives, when valid. Never its code.""" + + +class StartedSubscription(KaitenModel): + activation_invoice: InvoiceSummary | None = Field(default=None, alias="activationInvoice") + """The invoice of the first period, when the base price bills in advance; absent otherwise. + Charged automatically, it is pushed and charged before this answer when the provider + answers within 20 seconds: its status says how it went. + """ + + anchor_at: datetime = Field(alias="anchorAt") + """The instant its periods are counted from""" + + base_price: Price = Field(alias="basePrice") + """The FLAT_FEE price the subscription is pinned to, deprecated or not""" + + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + + cancel_at_period_end: bool = Field(alias="cancelAtPeriodEnd") + + cancel_requested_at: datetime | None = Field(alias="cancelRequestedAt") + + canceled_at: datetime | None = Field(alias="canceledAt") + + cancellation_reason: str | None = Field(alias="cancellationReason") + + collection_method: CollectionMethod | str = Field(alias="collectionMethod") + """Effective: the subscription's own, else the organization's default""" + + collection_method_override: str | None = Field(alias="collectionMethodOverride") + """The subscription's own collection method; null when it takes the organization's""" + + created_at: datetime = Field(alias="createdAt") + + currency: str + + current_period_end: datetime = Field(alias="currentPeriodEnd") + """The next boundary: the period closes, and its invoice is composed, after it""" + + current_period_start: datetime = Field(alias="currentPeriodStart") + + customer_name: str = Field(alias="customerName") + + customer_slug: str = Field(alias="customerSlug") + + days_until_due: int = Field(alias="daysUntilDue") + """Effective: the subscription's own, else the organization's default""" + + days_until_due_override: int | None = Field(alias="daysUntilDueOverride") + """The subscription's own payment terms; null when it takes the organization's""" + + id: str + + incomplete_expires_at: datetime | None = Field(alias="incompleteExpiresAt") + """When a checkout waiting for its payment ends unpaid: INCOMPLETE, or a free subscription + that carries one + """ + + instance_name: str = Field(alias="instanceName") + + instance_slug: str = Field(alias="instanceSlug") + """As it was at the subscription's last write""" + + past_due_since: datetime | None = Field(alias="pastDueSince") + + pending_license_slug: str | None = Field(alias="pendingLicenseSlug") + """The licence version a checkout waiting for its payment moves the instance to once paid""" + + provider_kind: BillingProviderKind | str = Field(alias="providerKind") + """Who collects its invoices. NOOP: the organization itself, through the handoff queue""" + + reminder_at: datetime | None = Field(alias="reminderAt") + """When trial_will_end is (or was) emitted: trialEndsAt minus the family's + trialReminderDays as it was when the trial started; set with trialEndsAt + """ + + scheduled_change: ScheduledChange | None = Field(alias="scheduledChange") + """A plan change waiting for the next boundary""" + + started_at: datetime = Field(alias="startedAt") + """When billing started: usage before it is never billed""" + + status: SubscriptionStatus | str + + trial_ends_at: datetime | None = Field(alias="trialEndsAt") + """When the trial ends, or ended: usage before it is never billed""" + + updated_at: datetime = Field(alias="updatedAt") + + +class StatusChangeDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + from_: SubscriptionStatus | str = Field(alias="from") + + instance_billing_id: str = Field(alias="instanceBillingId") + + instance_slug: str = Field(alias="instanceSlug") + + reason: SubscriptionStatusChangeReason | str + + to: SubscriptionStatus | str + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class SyncFailureDelivered(KaitenModel): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + consecutive_failures: int = Field(alias="consecutiveFailures") + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + last_sync_error: str = Field(alias="lastSyncError") + + last_synced_at: datetime = Field(alias="lastSyncedAt") + + provider_kind: str = Field(alias="providerKind") + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class SyncReport(KaitenModel): + providers: list[Outcome] + + +class SystemTokenIssuance(KaitenModel): + """A platform credential minted a token inside this organization.""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + credential_kind: str = Field(alias="credentialKind") + """Credential class that performed the mint; "platform" is the system:kaiten identity""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + expires_at: datetime | None = Field(default=None, alias="expiresAt") + """Absent when the minted credential does not expire""" + + issued_at: datetime = Field(alias="issuedAt") + """When the credential was minted""" + + issued_token_id: str = Field(alias="issuedTokenId") + """The credential that was minted""" + + name: str + """Human-readable label the caller gave the minted credential""" + + platform_token_id: str = Field(alias="platformTokenId") + """Platform credential that performed the mint; revoking it revokes everything it issued""" + + scopes: list[str] | None + """Scopes granted to the minted credential""" + + slug: str + """Server-assigned identifier of the minted credential""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + + +class TargetingContext(KaitenModel): + roots: list[TargetingContextNode] + """Identifiers the server guarantees, and what is reachable below each. Not a closed set: a + host may target on its own attributes too + """ + + +class TargetingContextNode(KaitenModel): + description: str | None = None + """What this value means, shown by the editor""" + + entries: list[TargetingContextNode] | None = None + """Shape of each known key, for a map whose entries differ. Named after the key""" + + fields: list[TargetingContextNode] | None = None + """Named children, for an object""" + + known_keys: list[str] | None = Field(default=None, alias="knownKeys") + """Keys this map currently has, for completion. Not a closed set""" + + name: str + """Name as it is written in a rule""" + + optional: bool | None = None + """Whether this value may be absent, in which case has() is worth using""" + + type: TargetingFactType | str + """Shape of this node's value""" + + values: TargetingContextNode | None = None + """Shape shared by every entry, for a map""" + + +class TargetingRuleIssue(KaitenModel): + column: int + """1-based column the problem starts on, or 0 when it has no position""" + + end_column: int | None = Field(default=None, alias="endColumn") + """1-based column just past the problem, or 0 when only a point is known""" + + end_line: int | None = Field(default=None, alias="endLine") + """1-based line the problem ends on, or 0 when only a point is known""" + + line: int + """1-based line the problem starts on, or 0 when it has no position""" + + message: str + """What is wrong, in the same words the write path refuses it with""" + + +class TargetingRuleRehearsal(KaitenModel): + evaluation_error: str | None = Field(default=None, alias="evaluationError") + """Why the engine could not answer, when it could not — e.g. the rule reads a host + attribute this context does not carry. At a real evaluation the rule would simply not + match + """ + + facts: dict[str, Any] + """The __kaiten namespace as enriched for this context — what a rule actually reads. Empty + sub-roots mean the context resolved nothing for them + """ + + issues: list[TargetingRuleIssue] + """Problems the lint found. The rule is only run when there are none""" + + matched: bool + """Whether the rule matched this context. Meaningful only when valid and evaluationError is + empty + """ + + valid: bool + """Whether the rule lints clean""" + + +class TargetingRuleVerdict(KaitenModel): + issues: list[TargetingRuleIssue] + """Every problem found, positioned where it is written. Empty when the rule is accepted""" + + valid: bool + """Whether the write path would accept this rule""" + + +class Token(KaitenModel): + """A token's metadata. Never its secret.""" + + created_at: datetime = Field(alias="createdAt") + + created_by: User = Field(alias="createdBy") + + expires_at: datetime | None = Field(default=None, alias="expiresAt") + + id: str | None = None + + name: str + + revoked_at: datetime | None = Field(default=None, alias="revokedAt") + + revoked_by: User | None = Field(default=None, alias="revokedBy") - description: str - """Detailed description of the license capabilities and restrictions""" + scopes: list[str] | None - family_id: str | None = Field(default=None, alias="familyId") - """Identifier of the license family this license is a version of. Stable across renames and - new versions: two licenses sharing it are the same product. Always present in responses. - On create, optional: names the family this license is a new version of, as familySlug - does by slug; omit both to create a new family. On update, optional and must be the - license's own family: a version cannot move between families - (UpdateLicense.FamilyNotReassignable). - """ + service_account_id: str | None = Field(default=None, alias="serviceAccountId") - family_slug: str | None = Field(default=None, alias="familySlug") - """Write-only on create: slug of the family this license is a new version of. familyId - names the same family by identifier; sent together they must agree. Omit both to create - a new family, of which this license becomes version 1. Never returned -- see familyId. - Refused on update: a version cannot move between families. + slug: str | None = None + """Optional URL-friendly identifier, unique per service account. Auto-generated if not + provided. """ - id: str - """Unique identifier for the license""" - is_default: bool = Field(alias="isDefault") - """Whether this version is the one its family puts forward: at most one version per family, - and only a PUBLISHED one (CreateLicense.DefaultMustBePublished, - UpdateLicense.DefaultMustBePublished). Resolution hands it out ahead of the highest- - numbered published version, so it keeps being served when a newer version is published. - Setting it takes the flag from the family's previous default, which gets a - LICENSE_UPDATED event of its own. On update, sending false unsets it; the default cannot - be archived until another version takes its place or the flag is unset - (ArchiveLicense.DefaultMustBePublished). - """ +class TotalsDifference(KaitenModel): + kaiten_total: int = Field(alias="kaitenTotal") - lifecycle_state: LicenseLifecycleState | str | None = Field( - default=None, alias="lifecycleState" - ) - """Whether this version may be served. Always present in responses. Optional on create, - where it is DRAFT or PUBLISHED and PUBLISHED when omitted; ARCHIVED is refused - (CreateLicense.LifecycleStateNotSettable), since a version is archived by withdrawing it - from sale. After that it changes only through publish-license (DRAFT to PUBLISHED), - archive-license (PUBLISHED to ARCHIVED) and unarchive-license (ARCHIVED to PUBLISHED); - update accepts the stored state and refuses any other - (UpdateLicense.LifecycleStateNotSettable). Only a PUBLISHED version can be the family's - default. An ARCHIVED version cannot be assigned to an instance - (CreateInstance.LicenseArchived, UpdateInstance.LicenseArchived); instances already on - it keep it. - """ + provider_subtotal: int | None = Field(default=None, alias="providerSubtotal") + + provider_total_discount: int | None = Field(default=None, alias="providerTotalDiscount") + """With inclusive tax: the provider's discounts, already taken off its subtotal""" + + provider_total_excluding_tax: int = Field(alias="providerTotalExcludingTax") + + +class TrialCharge(KaitenModel): + amount: int + + billing_period: BillingPeriod | str = Field(alias="billingPeriod") + + currency: str + + +class TrialCustomer(KaitenModel): + billing_email: str | None = Field(alias="billingEmail") + """Personal data: the second payload that carries an e-mail (D-56)""" name: str - """Display name of the license""" - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. When omitted, a new version - of an existing family takes {familySlug}-v{version}, and any other license gets one - generated from its name. A license that opens a new family gives the family its slug. - """ + slug: str - type: LicenseType | str - """Type of the license determining its capabilities and restrictions""" - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when this license was last updated""" +class TrialInstance(KaitenModel): + name: str - version: str - """Version of the license within its family, assigned by the server when the version is - created and never changed afterwards: the version's derived slug - ({familySlug}-v{version}, see slug) is built from it. A number is never reused within a - family, even after its version is deleted. Create refuses any value - (CreateLicense.VersionNotSettable); update accepts only the stored one, so a - representation can be written back as it was read (UpdateLicense.VersionNotSettable). - """ + slug: str - version_name: str | None = Field(default=None, alias="versionName") - """Optional human-readable name for the license version. Defaults to "Version - ".""" +class TrialPaymentMethod(KaitenModel): + brand: str | None -class LicenseEntitlement(KaitenModel): - """A grant: the value a license gives one entitlement, and how it is enforced.""" + exp_month: int | None = Field(alias="expMonth") - created_at: datetime = Field(alias="createdAt") - """Timestamp when this entitlement was created""" + exp_year: int | None = Field(alias="expYear") - created_by: User = Field(alias="createdBy") - """User who created this entitlement""" + last4: str | None - entitlement_groups: list[EntitlementGroupSummary] | None = Field( - default=None, alias="entitlementGroups" - ) - """Groups associated with this entitlement""" - entitlement_name: str = Field(alias="entitlementName") - """Name of the entitlement feature or service""" +class UsageLedger(KaitenModel): + retention_months: int | None = Field(alias="retentionMonths") + """How long usage reports are kept, in months; null when they are kept forever (ENV) or the + retention is unknown (UNKNOWN) + """ - entitlement_slug: str | None = Field(default=None, alias="entitlementSlug") - """URL-friendly identifier for the entitlement""" + retention_source: RetentionSource | str = Field(alias="retentionSource") + """KBK: the organization's licence or an add-on; ENV: KAITEN_USAGE_LEDGER_RETENTION_MONTHS, + with no licensing authority; UNKNOWN: the licensing authority could not tell + """ - entitlement_type: LicenseEntitlementType | str | None = Field( - default=None, alias="entitlementType" - ) - """Type of entitlement (BOOLEAN, NUMBER, or CONFIG)""" + +class UsageReport(KaitenModel): + aggregation_method: str = Field(alias="aggregationMethod") + """The entitlement's aggregation method when the report was accepted""" + + behavior: UsageBehavior | str + """append adds the value through the aggregation method; set overwrites the counter""" + + delta: str + """valueAfter minus valueBefore; negative for a set that lowered the counter""" + + entitlement_id: str = Field(alias="entitlementId") + """The entitlement the report was made for. It may since have been deleted.""" + + event_count_after: int = Field(alias="eventCountAfter") + """Reports counted in the window after this one""" + + instance_id: str = Field(alias="instanceId") + """The instance the report was made for. It may since have been deleted.""" license_id: str = Field(alias="licenseId") - """Unique identifier for the license this entitlement belongs to""" + """The instance's licence when the report was accepted""" - license_slug: str = Field(alias="licenseSlug") - """URL-friendly identifier for the license this entitlement belongs to""" + limit_value: str | None = Field(alias="limitValue") + """The limit in force when the report was accepted. Null when unlimited.""" - limit_cap_exceeded_overage_percent: int | None = Field( - default=None, alias="limitCapExceededOveragePercent" - ) - """Derives this grant's enforcement from its own numeric value: -1 when the value is - unlimited (no cap, no overage); otherwise 0 rejects usage above the value (hard limit) - and a positive percentage allows usage to exceed it by that much before being rejected - (soft limit). Null for non-numeric (BOOLEAN/CONFIG) grants. + overage_delta: str = Field(alias="overageDelta") + """How much the usage above limitValue moved: max(0, valueAfter - limitValue) - max(0, + valueBefore - limitValue). 0 when unlimited. """ - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when this entitlement was last updated""" + overage_percent: int | None = Field(alias="overagePercent") + """The overage allowed above limitValue, in percent, when the report was accepted. Null + when unlimited. + """ - updated_by: User = Field(alias="updatedBy") - """User who last updated this entitlement""" + properties: dict[str, Any] | None + """The report's metadata; null when none was stored""" - value: EntitlementValue - """Typed entitlement value. Supported shapes: {type:number,value:number}, - {type:boolean,value:boolean}, {type:object,value:{...}} + report_seq: int = Field(alias="reportSeq") + """Position of the report among the pair's accepted reports, from 1, without gaps""" + + reported_at: datetime = Field(alias="reportedAt") + """When the server accepted the report (UTC, millisecond precision)""" + + reported_value: str = Field(alias="reportedValue") + """The value as sent: a delta for append, an absolute value for set""" + + transaction_id: str | None = Field(alias="transactionId") + """The report's idempotency key; null when it was sent without one""" + + value_after: str = Field(alias="valueAfter") + """The counter after the report""" + + value_before: str = Field(alias="valueBefore") + """The counter before the report, after any window reset""" + + window_end: datetime | None = Field(alias="windowEnd") + """End of the usage window the report counted in (exclusive). Null for a lifetime + entitlement. """ + window_start: datetime | None = Field(alias="windowStart") + """Start of the usage window the report counted in (inclusive). Null for a lifetime + entitlement. + """ -class LicenseFamilyView(KaitenModel): - """A license family -- the product -- resolved to one of its versions.""" + +class UsageReportPage(KaitenModel): + items: list[UsageReport] + """The reports, in reportSeq order""" + + next_after_seq: int | None = Field(default=None, alias="nextAfterSeq") + """Pass as afterSeq to read the next page. Absent on the last page.""" + + +class User(KaitenModel): + """A reference to the user behind an action.""" + + id: str + """ID of the user""" + + name: str | None = None + """Name of the user""" + + +class Validity(KaitenModel): + reason: VoucherInvalidReason | str | None = None + """The first failing check. DISCOUNT_LIMIT_EXCEEDED: a line of the instance's invoices + would bear more than 20 discounts + """ + + rule: VoucherRule | str | None = None + """With NOT_ELIGIBLE: the eligibility rule""" + + valid: bool + + voucher: Voucher | None = None + """The voucher, without its code, when the code names one""" + + +class Variant(KaitenModel): + """One possible value of a feature flag.""" + + description: str + + name: str + + value: Any + + +class Voucher(KaitenModel): + applicable_addon_ids: list[str] = Field(alias="applicableAddonIds") + """Only instances holding one of these add-on versions can redeem it; any when empty""" + + applicable_addon_price_ids: list[str] = Field(alias="applicableAddonPriceIds") + + applicable_license_ids: list[str] = Field(alias="applicableLicenseIds") + """Only instances on these licence versions can redeem it; any when empty""" + + applicable_license_price_ids: list[str] = Field(alias="applicableLicensePriceIds") + + code: str | None = None + """The code to redeem. Returned to read:vouchers callers only; never in an event.""" + + code_hint: str = Field(alias="codeHint") + """The last four characters of the normalized code""" created_at: datetime = Field(alias="createdAt") - """Timestamp when this family was created""" - current_version: License | None = Field(default=None, alias="currentVersion") - """The version this read is about. By default the version the family resolves to: its - default version if it has one, otherwise its highest-numbered PUBLISHED version. Under - ?version=N it is the version asked for instead, whatever its lifecycle state. Absent - when the family has no PUBLISHED version, which only a list or an include=versions read - returns. + currency: str | None + """The currency of a FIXED_AMOUNT discount""" + + description: str | None + + duration: VoucherDuration | str + """PRICE: how many invoices it discounts (one, durationInPeriods, or every one). BOOST and + FLAG_GRANT: how long a redemption lasts, in the subscription's billing periods. + COMPOSITE: both, each for its part. """ + duration_in_periods: int | None = Field(alias="durationInPeriods") + + expires_at: datetime | None = Field(alias="expiresAt") + + flag_grants: list[FlagGrant] = Field(alias="flagGrants") + """The flag variants a FLAG_GRANT grants""" + + grants: list[Grant] + """What an ENTITLEMENT_BOOST changes""" + id: str - """Unique identifier for the license family""" - slug: str - """URL-friendly identifier of the family, unique per organization and stable across renames - and new versions. Deleting the family's last version deletes the family and frees the - slug. - """ + max_redemptions: int | None = Field(alias="maxRedemptions") + + name: str + + price_applies_to: DiscountAppliesTo | str | None = Field(alias="priceAppliesTo") + + price_discount_type: DiscountType | str | None = Field(alias="priceDiscountType") + + price_discount_value: str | None = Field(alias="priceDiscountValue") + """A percentage in (0, 100], or an integer amount in minor units of currency""" + + redemption_rules: RedemptionRules = Field(alias="redemptionRules") + + redemptions_count: int = Field(alias="redemptionsCount") + + restricted_customer_slug: str | None = Field(alias="restrictedCustomerSlug") + """Only this customer's instances can redeem it""" + + starts_at: datetime | None = Field(alias="startsAt") + + status: VoucherStatus | str updated_at: datetime = Field(alias="updatedAt") - """Timestamp when a version was last added to this family, or when the family was created. - Renames, lifecycle moves and default changes update the versions' own updatedAt, not - this one. + + voucher_type: VoucherType | str = Field(alias="voucherType") + + +class AddonDelivered(Addon): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - version_count: int = Field(alias="versionCount") - """Number of versions in this family, whatever their lifecycle state""" - versions: list[License] | None = None - """Every version of the family, oldest first. Only returned when explicitly requested with - include=versions, and then never null: a family always has at least one version. +class AddonGrantUpdateDelivered(AddonEntitlement): + addon_slug: str = Field(alias="addonSlug") + """The add-on version the grant belongs to""" + + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ -class ManifestEnvelope(KaitenModel): - flags: list[ManifestFlag] | None +class AddonPriceUpdateDelivered(Price): + addon_slug: str = Field(alias="addonSlug") + """The add-on version the price belongs to""" + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class ManifestFlag(KaitenModel): - """One flag of the OpenFeature manifest.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - default_value: Any = Field(alias="defaultValue") + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - description: str | None = None - key: str +class AddonUpdateDelivered(Addon): + changed_fields: list[str] | None = Field(alias="changedFields") + """The members the change touched, as the API names them""" - name: str | None = None + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - type: str + compatible_license_families: list[str] | None = Field(alias="compatibleLicenseFamilies") + """The licence families the version fits after the change""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class MetadataField(KaitenModel): - """A typed field an organization declares on its instances or deployment zones.""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - archived_at: datetime | None = Field(default=None, alias="archivedAt") - """Timestamp when the field was archived (soft delete). Absent if active.""" - created_at: datetime = Field(alias="createdAt") - """Timestamp when the field was created""" +class BillingStartedDelivered(InstanceBilling): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - created_by: User = Field(alias="createdBy") - """User who created this field""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - display_order: int | None = Field(default=None, alias="displayOrder") - """Sort order in the admin UI; ties are broken by createdAt ASC. Settable only through POST - /metadata-fields/reorder; never echoed back on update. + resubscribed: bool + """Set when a CANCELED subscription was subscribed again""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - id: str - """Unique identifier""" - json_schema: dict[str, Any] = Field(alias="jsonSchema") - """JSON Schema 2020-12 document describing the value space. Example: - {"type":"string","enum":["a","b"]} +class CancellationChangeDelivered(InstanceBilling): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + effective_at: datetime = Field(alias="effectiveAt") + """The boundary the cancellation takes effect at""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - key: str - """Stable identifier, unique per (organization, resource type) on non-archived rows. - Immutable after creation; update must echo back the stored value. + +class CancellationDelivered(InstanceBilling): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + final_invoice_id: str | None = Field(alias="finalInvoiceId") + """The FINAL invoice; null when the cancellation issued none (a trial)""" + + mode: CancellationMode | str + """How it was canceled; a trial is canceled at once in either mode""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - label: str - """Human-readable label shown in the admin UI""" - resource_type: MetadataResourceType | str = Field(alias="resourceType") - """Resource type this field applies to. Immutable after creation; update must echo back the - stored value. +class ComponentDelivered(Component): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - updated_at: datetime = Field(alias="updatedAt") - """Timestamp when the field was last updated""" - - updated_by: User = Field(alias="updatedBy") - """User who last updated this field""" +class CustomerEventDelivered(Customer): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class MetadataFieldImpact(KaitenModel): - """What a candidate metadata field schema would invalidate.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - count: int - """Number of resources whose stored value for this field would no longer satisfy the - candidate schema + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - samples: list[MetadataFieldImpactSample] | None - """Up to 5 impacted resources, for display""" +class DeploymentZoneDelivered(DeploymentZone): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class MetadataFieldImpactSample(KaitenModel): - """A resource a metadata field schema change would invalidate.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - name: str - """Resource display name""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - slug: str - """Resource slug""" +class EntitlementDelivered(Entitlement): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class MetadataFieldReordered(KaitenModel): - """Metadata fields were given a new display order.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - ids: list[str] | None - """Metadata field ids in their new display order""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ -class NumberEntitlementValue(KaitenModel): - """A numeric entitlement value: a quota, or an amount consumed.""" +class EntitlementGroupDelivered(EntitlementGroup): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - event_count: int | None = None - """Number of events accumulated (server-computed)""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - type: Literal["number"] - """Value type discriminator""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - value: float - """Numeric value""" +class EntitlementUsageDelivered(EntitlementUsage): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class PlainToken(KaitenModel): - """A token together with its plaintext secret, which the API returns exactly once.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - created_at: datetime = Field(alias="createdAt") + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - created_by: User = Field(alias="createdBy") - expires_at: datetime | None = Field(default=None, alias="expiresAt") +class FeatureFlagDelivered(FeatureFlag): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - id: str | None = None + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - name: str + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - revoked_at: datetime | None = Field(default=None, alias="revokedAt") - revoked_by: User | None = Field(default=None, alias="revokedBy") +class HeldInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - scopes: list[str] | None + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - service_account_id: str | None = Field(default=None, alias="serviceAccountId") + hold_detail: HoldDetail = Field(alias="holdDetail") - slug: str | None = None - """Optional URL-friendly identifier, unique per service account. Auto-generated if not - provided. + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - token: str | None = None +class InstanceAddonUpdateDelivered(InstanceAddon): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class Problem(KaitenModel): - """An RFC 9457 problem document, which every API error carries.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - code: str | None = None - """Stable, machine-readable error code.""" + instance_slug: str = Field(alias="instanceSlug") - detail: str | None = None - """A human-readable explanation specific to this occurrence of the problem.""" + previous_quantity: int | None = Field(default=None, alias="previousQuantity") + """The quantity before a quantity change""" - error_id: str | None = Field(default=None, alias="errorId") - """Correlation id for the server-side log entry describing the withheld cause.""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - errors: list[ErrorDetail] | None = None - """Optional list of individual error details""" - instance: str | None = None - """A URI reference that identifies the specific occurrence of the problem.""" +class InstanceDelivered(Instance): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - status: int | None = None - """HTTP status code""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - title: str | None = None - """A short, human-readable summary of the problem type. This value should not change - between occurrences of the error. + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - type: str | None = None - """A URI reference to human-readable documentation for the error.""" +class InstanceUpdateDelivered(Instance): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class Release(KaitenModel): - """An immutable, versioned set of components.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - component_ids: list[str] | None = Field(default=None, alias="componentIds") - """List of component IDs to link to this release""" + license_change_reason: Literal["BILLING_FALLBACK"] | None = Field(alias="licenseChangeReason") + """BILLING_FALLBACK: the subscription ended and the instance moved to its family's fallback + (V31-66). Null for any other change, the move a paid checkout applies included + """ - components: list[Component] | None = None - """List of components included in this release, fixed at creation""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - created_at: datetime = Field(alias="createdAt") - """Timestamp when the release was created""" - created_by: User = Field(alias="createdBy") - """User who created this release""" +class IssuedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - description: str | None = None - """Description of the release""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - id: str - """Unique identifier for the release""" + license_id: str = Field(alias="licenseId") - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + lines: list[InvoiceLine] - version: str - """Version of the release""" + lines_truncated: bool | None = Field(default=None, alias="linesTruncated") + """Set when the lines did not fit the event: read the invoice for all of them""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ -class RolloutDateTargeting(KaitenModel): - """A targeting rule that shifts traffic between variants over a date range.""" - end: RolloutStep +class LicenseDelivered(License): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - name: str + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - rule: str - """CEL expression for basicTargeting evaluation""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - start: RolloutStep - type: Literal["rollout_date"] +class LicenseEntitlementDelivered(LicenseEntitlement): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class RolloutDateVariant(KaitenModel): - """A default variant that shifts traffic between variants over a date range.""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - end: RolloutStep - start: RolloutStep +class LicenseFamilyViewDelivered(LicenseFamilyView): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - type: Literal["rollout_date"] + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ -class RolloutPercentageTargeting(KaitenModel): - """A targeting rule that splits the contexts it matches between variants.""" - distribution: dict[str, int] +class LicensePriceEventDelivered(Price): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - name: str + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - rule: str - """CEL expression for basicTargeting evaluation""" + family_slug: str = Field(alias="familySlug") + """The family of that version""" - type: Literal["rollout_percentage"] + license_slug: str = Field(alias="licenseSlug") + """The licence version the price belongs to""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ -class RolloutPercentageVariant(KaitenModel): - """A default variant that splits traffic between variants by weight.""" - distribution: dict[str, int] +class LicensePriceUpdatedEventDelivered(Price): + changed_fields: list[str] | None = Field(alias="changedFields") + """The members the update changed, as the API names them""" - type: Literal["rollout_percentage"] + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class RolloutStep(KaitenModel): - """One end of a date-based rollout.""" + family_slug: str = Field(alias="familySlug") + """The family of that version""" - date: datetime + license_slug: str = Field(alias="licenseSlug") + """The licence version the price belongs to""" - percentage: int + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - variant: str +class MetadataFieldDelivered(MetadataField): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class ServiceAccount(KaitenModel): - """A machine identity inside an organization, owning API tokens.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - created_at: datetime | None = Field(default=None, alias="createdAt") + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - created_by: User | None = Field(default=None, alias="createdBy") - external_id: str | None = Field(default=None, alias="externalId") +class MismatchedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - id: str | None = None + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - name: str + reconciliation_detail: Reconciliation = Field(alias="reconciliationDetail") - slug: str | None = None - """Optional URL-friendly identifier, unique per organization. Auto-generated if not provided.""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - tokens: list[Token] | None +class PaidInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" -class SystemTokenIssuance(KaitenModel): - """A platform credential minted a token inside this organization.""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - credential_kind: str = Field(alias="credentialKind") - """Credential class that performed the mint; "platform" is the system:kaiten identity""" + external_reference: str | None = Field(default=None, alias="externalReference") - expires_at: datetime | None = Field(default=None, alias="expiresAt") - """Absent when the minted credential does not expire""" + note: str | None = None + """The note given when it was marked paid, kept only here""" - issued_at: datetime = Field(alias="issuedAt") - """When the credential was minted""" + source: PaymentSource | str + """MARK_PAID: recorded by the organization. ZERO_TOTAL: nothing was owed""" - issued_token_id: str = Field(alias="issuedTokenId") - """The credential that was minted""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - name: str - """Human-readable label the caller gave the minted credential""" - platform_token_id: str = Field(alias="platformTokenId") - """Platform credential that performed the mint; revoking it revokes everything it issued""" +class PaymentFailedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - scopes: list[str] | None - """Scopes granted to the minted credential""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - slug: str - """Server-assigned identifier of the minted credential""" + failure_code: str = Field(alias="failureCode") + """The provider's decline or failure code, such as card_declined, expired_card, + authentication_required or no_payment_method + """ + requires_action: bool = Field(alias="requiresAction") + """The customer must authenticate the payment on the invoice's hosted page""" -class TargetingContext(KaitenModel): - roots: list[TargetingContextNode] - """Identifiers the server guarantees, and what is reachable below each. Not a closed set: a - host may target on its own attributes too + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ -class TargetingContextNode(KaitenModel): - description: str | None = None - """What this value means, shown by the editor""" +class PushFailedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - fields: list[TargetingContextNode] | None = None - """Named children, for an object""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - known_keys: list[str] | None = Field(default=None, alias="knownKeys") - """Keys this map currently has, for completion. Not a closed set""" + last_push_error: str = Field(alias="lastPushError") + """The provider's code and message, never a request body""" - name: str - """Name as it is written in a rule""" + push_attempts: int = Field(alias="pushAttempts") - optional: bool | None = None - """Whether this value may be absent, in which case has() is worth using""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - type: TargetingFactType | str - """Shape of this node's value""" - values: TargetingContextNode | None = None - """Shape shared by every entry, for a map""" +class PushedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class TargetingRuleIssue(KaitenModel): - column: int - """1-based column the problem starts on, or 0 when it has no position""" + external_invoice_id: str = Field(alias="externalInvoiceId") - end_column: int | None = Field(default=None, alias="endColumn") - """1-based column just past the problem, or 0 when only a point is known""" + provider_invoice_number: str | None = Field(default=None, alias="providerInvoiceNumber") - end_line: int | None = Field(default=None, alias="endLine") - """1-based line the problem ends on, or 0 when only a point is known""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - line: int - """1-based line the problem starts on, or 0 when it has no position""" - message: str - """What is wrong, in the same words the write path refuses it with""" +class RedemptionDelivered(Redemption): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class TargetingRuleRehearsal(KaitenModel): - evaluation_error: str | None = Field(default=None, alias="evaluationError") - """Why the engine could not answer, when it could not — e.g. the rule reads a host - attribute this context does not carry. At a real evaluation the rule would simply not - match + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - facts: dict[str, Any] - """The __kaiten namespace as enriched for this context — what a rule actually reads. Empty - sub-roots mean the context resolved nothing for them + +class ReleaseDelivered(Release): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - issues: list[TargetingRuleIssue] - """Problems the lint found. The rule is only run when there are none""" - matched: bool - """Whether the rule matched this context. Meaningful only when valid and evaluationError is - empty +class ReleasedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" + + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" + + hold_detail: HoldDetail = Field(alias="holdDetail") + """The hold it left, which the invoice no longer shows""" + + release_reason: str = Field(alias="releaseReason") + + released_by: str = Field(alias="releasedBy") + """The user who released it, or system when a later check found the journal sound""" + + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ - valid: bool - """Whether the rule lints clean""" +class TrialWillEndDelivered(InstanceBilling): + charge: TrialCharge | None + """What the conversion would charge, as upcoming-invoice previews it, discounts included + and before any tax the provider adds; null when nothing would be + """ -class TargetingRuleVerdict(KaitenModel): - issues: list[TargetingRuleIssue] - """Every problem found, positioned where it is written. Empty when the rule is accepted""" + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - valid: bool - """Whether the write path would accept this rule""" + customer: TrialCustomer + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" -class Token(KaitenModel): - """A token's metadata. Never its secret.""" + days_left: int = Field(alias="daysLeft") + """Whole days from the event to trialEndsAt, rounded up; 0 once it passed""" - created_at: datetime = Field(alias="createdAt") + family_slug: str = Field(alias="familySlug") + """Its family""" - created_by: User = Field(alias="createdBy") + instance: TrialInstance - expires_at: datetime | None = Field(default=None, alias="expiresAt") + license_name: str = Field(alias="licenseName") + """The licence version of the base price""" - id: str | None = None + payment_method: TrialPaymentMethod | None = Field(alias="paymentMethod") + """The ACTIVE card the conversion would be charged to; null without one, or on NOOP""" - name: str + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ - revoked_at: datetime | None = Field(default=None, alias="revokedAt") - revoked_by: User | None = Field(default=None, alias="revokedBy") +class UncollectibleInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - scopes: list[str] | None + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - service_account_id: str | None = Field(default=None, alias="serviceAccountId") + reason: str - slug: str | None = None - """Optional URL-friendly identifier, unique per service account. Auto-generated if not - provided. + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise """ -class User(KaitenModel): - """A reference to the user behind an action.""" +class VoidedInvoiceDelivered(InvoiceSummary): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - id: str - """ID of the user""" + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - name: str | None = None - """Name of the user""" + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ + void_reason: str = Field(alias="voidReason") -class Variant(KaitenModel): - """One possible value of a feature flag.""" - description: str +class VoucherDelivered(Voucher): + checkout_session_id: str | None = Field(default=None, alias="checkoutSessionId") + """With via checkout_session: the session""" - name: str + customer_session_id: str | None = Field(default=None, alias="customerSessionId") + """With via customer_session: the session""" - value: Any + via: EventVia | str | None = None + """customer_session: written through a kst_ customer session, under the vendor principal + that minted it. checkout_session: written by an anonymous checkout, under system:kaiten. + Absent otherwise + """ DefaultVariant = Annotated[ @@ -1607,32 +6238,78 @@ class Variant(KaitenModel): for _model in ( + Addon, + AddonEntitlement, + AddonFamily, + AuditActor, AuditTrail, BasicTargeting, BasicVariant, + BillingCapabilities, + BillingFeatures, + BillingHealth, + BillingProvider, + BillingSettings, BooleanEntitlementValue, + CanceledSubscription, + CheckoutConfirmation, + CheckoutPayment, + CheckoutSession, + CheckoutSessionAdmin, + CheckoutSessionStart, + CloseBacklog, + ClosePeriodsReport, + ClosedInvoice, + CompatibleLicenseFamilies, + CompletedPaymentMethodSession, Component, ConfigEntitlementValue, + ConnectionDelivered, Connector, ConnectorActivation, ConnectorSettings, ConnectorSettingsSchema, ConnectorState, + CreatedCustomerSession, Customer, + CustomerBilling, CustomerCreationRejected, + CustomerInProvider, CustomerIntegration, CustomerIntegrationResource, + DeletedAddonDelivered, Deployment, DeploymentZone, + DiscountAllocation, + DiscountDifference, + DueDateDifference, + DueToday, Entitlement, + EntitlementException, EntitlementGroup, EntitlementGroupSummary, EntitlementGroupUsage, EntitlementUsage, ErrorDetail, + EventHeaders, + ExceptionEventDelivered, + ExhaustedEventDelivered, + ExpiredEventDelivered, + ExtraDiscount, FeatureFlag, FeatureFlagEvaluated, + FlagGrant, + Grant, + HandoffAcknowledgementDelivered, + HandoffBacklog, + HandoffClaim, + HeldInvoices, + HeldPair, + HoldDetail, + HoldRecord, Instance, + InstanceAddon, + InstanceBilling, InstanceEntitlementCapExceeded, InstanceEntitlementUsagePeriodRolledOver, InstanceEntitlementUsageReportAccepted, @@ -1642,25 +6319,112 @@ class Variant(KaitenModel): InstanceIntegrationResource, InstanceLifecycleStageChanged, InstanceStatusChanged, + Invoice, + InvoiceHandoff, + InvoiceHold, + InvoiceLine, + InvoiceLineDiscount, + InvoiceLineLedger, + InvoiceLineMetering, + InvoiceLineOverage, + InvoiceLineProvider, + InvoiceLineTrueUp, + InvoicePreview, + InvoiceSummary, License, LicenseEntitlement, LicenseFamilyView, + LineDifference, + LineReportPage, ManifestEnvelope, ManifestFlag, MetadataField, MetadataFieldImpact, MetadataFieldImpactSample, MetadataFieldReordered, + MinimumAmount, NumberEntitlementValue, + Outcome, + OverageLimit, + Payment, + PaymentMethodAttachedDelivered, + PaymentMethodDetachedDelivered, + PaymentMethodExpiringDelivered, + PaymentMethodLabels, + PaymentMethodSession, PlainToken, + PlanChangeDelivered, + PlanChangeScheduleDelivered, + PortalCapabilities, + PortalCustomer, + PortalEntitlement, + PortalInstance, + PortalInstanceSummary, + PortalPaymentMethod, + PortalSession, + PortalTrial, + PortalTrialCharge, + PortalTrialPayment, + PortalUpcomingInvoice, + Price, + PriceMeter, Problem, + Provenance, + ProvenanceAddon, + ProvenanceBoost, + ProvenanceException, + ProvenanceLicense, + ProvenanceNumber, + ProviderCapabilities, + ProviderChangeDelivered, + ProviderLineLimit, + ProviderRecord, + ProviderSync, + PublicAddOn, + PublicAddOnEntitlement, + PublicBranding, + PublicCatalog, + PublicCatalogCapabilities, + PublicMeter, + PublicPlan, + PublicPlanEntitlement, + PublicPrice, + PublicSurface, + PublishableKey, + PublishableKeyCreated, + PublishableKeyEventDelivered, + PushFailures, + QueuedInvoice, + Reconciliation, + Redemption, + RedemptionRules, Release, + ReportedEntitlementUsage, RolloutDateTargeting, RolloutDateVariant, RolloutPercentageTargeting, RolloutPercentageVariant, RolloutStep, + ScheduledChange, ServiceAccount, + SessionAddon, + SessionAddonSelection, + SessionBoost, + SessionCheckout, + SessionDiscount, + SessionEventDelivered, + SessionInvoice, + SessionLine, + SessionPortal, + SessionRedemption, + SessionScheduledChange, + SessionSubscription, + SessionVoucher, + SessionVoucherValidity, + StartedSubscription, + StatusChangeDelivered, + SyncFailureDelivered, + SyncReport, SystemTokenIssuance, TargetingContext, TargetingContextNode, @@ -1668,8 +6432,55 @@ class Variant(KaitenModel): TargetingRuleRehearsal, TargetingRuleVerdict, Token, + TotalsDifference, + TrialCharge, + TrialCustomer, + TrialInstance, + TrialPaymentMethod, + UsageLedger, + UsageReport, + UsageReportPage, User, + Validity, Variant, + Voucher, + AddonDelivered, + AddonGrantUpdateDelivered, + AddonPriceUpdateDelivered, + AddonUpdateDelivered, + BillingStartedDelivered, + CancellationChangeDelivered, + CancellationDelivered, + ComponentDelivered, + CustomerEventDelivered, + DeploymentZoneDelivered, + EntitlementDelivered, + EntitlementGroupDelivered, + EntitlementUsageDelivered, + FeatureFlagDelivered, + HeldInvoiceDelivered, + InstanceAddonUpdateDelivered, + InstanceDelivered, + InstanceUpdateDelivered, + IssuedInvoiceDelivered, + LicenseDelivered, + LicenseEntitlementDelivered, + LicenseFamilyViewDelivered, + LicensePriceEventDelivered, + LicensePriceUpdatedEventDelivered, + MetadataFieldDelivered, + MismatchedInvoiceDelivered, + PaidInvoiceDelivered, + PaymentFailedInvoiceDelivered, + PushFailedInvoiceDelivered, + PushedInvoiceDelivered, + RedemptionDelivered, + ReleaseDelivered, + ReleasedInvoiceDelivered, + TrialWillEndDelivered, + UncollectibleInvoiceDelivered, + VoidedInvoiceDelivered, + VoucherDelivered, ): _model.model_rebuild() del _model diff --git a/src/kaitencloud/types/_params.py b/src/kaitencloud/types/_params.py index e083959..9e08c89 100644 --- a/src/kaitencloud/types/_params.py +++ b/src/kaitencloud/types/_params.py @@ -24,6 +24,8 @@ "BasicVariantParam", "DefaultVariantParam", "IntegrationParam", + "MinimumAmountParam", + "RedemptionRulesParam", "RolloutDateTargetingParam", "RolloutDateVariantParam", "RolloutPercentageTargetingParam", @@ -31,6 +33,7 @@ "RolloutStepParam", "TargetingParam", "VariantParam", + "VoucherGrantParam", ] @@ -124,3 +127,38 @@ class RolloutDateTargetingParam(TypedDict): TargetingParam: TypeAlias = ( BasicTargetingParam | RolloutPercentageTargetingParam | RolloutDateTargetingParam ) + + +class VoucherGrantParam(TypedDict, total=False): + """What an entitlement boost voucher grants on one entitlement.""" + + entitlementSlug: Required[str] + modifierType: Required[Literal["SET", "ADD", "MULTIPLY", "UNLIMITED"]] + """``SET``, ``ADD`` and ``MULTIPLY`` take a ``modifierValue``; a BOOLEAN or CONFIG + entitlement takes ``SET`` only, with a ``value``.""" + + modifierValue: str + """A NUMBER grant's decimal, as a string: ``"10"``.""" + + value: bool | dict[str, Any] + """What a BOOLEAN or CONFIG ``SET`` grant sets.""" + + +class MinimumAmountParam(TypedDict): + """An amount in minor units, in a currency.""" + + currency: str + unitAmountDecimal: str + + +class RedemptionRulesParam(TypedDict, total=False): + """Who may redeem a voucher, beyond its dates and its redemption count.""" + + annualOnly: bool + """Only an instance with a live ``ANNUAL`` subscription.""" + + firstTimeOnly: bool + """Only a customer none of whose instances has paid an invoice.""" + + minimumSubscriptionAmount: MinimumAmountParam + """Only a live subscription whose base price is at least this.""" diff --git a/src/kaitencloud/types/_webhooks.py b/src/kaitencloud/types/_webhooks.py index 0cc73bc..a20934c 100644 --- a/src/kaitencloud/types/_webhooks.py +++ b/src/kaitencloud/types/_webhooks.py @@ -14,17 +14,33 @@ from .._models import KaitenModel from ._core import ( - Component, - Customer, + AddonDelivered, + AddonGrantUpdateDelivered, + AddonPriceUpdateDelivered, + AddonUpdateDelivered, + BillingStartedDelivered, + CancellationChangeDelivered, + CancellationDelivered, + ComponentDelivered, + ConnectionDelivered, CustomerCreationRejected, + CustomerEventDelivered, + DeletedAddonDelivered, Deployment, - DeploymentZone, - Entitlement, - EntitlementGroup, - EntitlementUsage, - FeatureFlag, + DeploymentZoneDelivered, + EntitlementDelivered, + EntitlementGroupDelivered, + EntitlementUsageDelivered, + EventHeaders, + ExceptionEventDelivered, + ExhaustedEventDelivered, + ExpiredEventDelivered, + FeatureFlagDelivered, FeatureFlagEvaluated, - Instance, + HandoffAcknowledgementDelivered, + HeldInvoiceDelivered, + InstanceAddonUpdateDelivered, + InstanceDelivered, InstanceEntitlementCapExceeded, InstanceEntitlementUsagePeriodRolledOver, InstanceEntitlementUsageReportAccepted, @@ -32,23 +48,68 @@ InstanceEntitlementValueGet, InstanceLifecycleStageChanged, InstanceStatusChanged, - License, - LicenseEntitlement, - LicenseFamilyView, - MetadataField, + InstanceUpdateDelivered, + IssuedInvoiceDelivered, + LicenseDelivered, + LicenseEntitlementDelivered, + LicenseFamilyViewDelivered, + LicensePriceEventDelivered, + LicensePriceUpdatedEventDelivered, + MetadataFieldDelivered, MetadataFieldReordered, - Release, + MismatchedInvoiceDelivered, + PaidInvoiceDelivered, + PaymentFailedInvoiceDelivered, + PaymentMethodAttachedDelivered, + PaymentMethodDetachedDelivered, + PaymentMethodExpiringDelivered, + PlanChangeDelivered, + PlanChangeScheduleDelivered, + ProviderChangeDelivered, + PublishableKeyEventDelivered, + PushedInvoiceDelivered, + PushFailedInvoiceDelivered, + RedemptionDelivered, + ReleaseDelivered, + ReleasedInvoiceDelivered, + SessionEventDelivered, + StatusChangeDelivered, + SyncFailureDelivered, SystemTokenIssuance, + TrialWillEndDelivered, + UncollectibleInvoiceDelivered, + VoidedInvoiceDelivered, + VoucherDelivered, ) __all__ = [ "WEBHOOK_EVENTS", + "AddonArchivedEvent", + "AddonCreatedEvent", + "AddonDeletedEvent", + "AddonEntitlementAssignedEvent", + "AddonEntitlementUnassignedEvent", + "AddonEntitlementUpdatedEvent", + "AddonPriceCreatedEvent", + "AddonPriceDeprecatedEvent", + "AddonPublishedEvent", + "AddonUnarchivedEvent", + "AddonUpdatedEvent", + "BillingProviderConnectedEvent", + "BillingProviderDisconnectedEvent", + "BillingProviderSyncFailedEvent", + "CheckoutSessionAbandonedEvent", + "CheckoutSessionCompletedEvent", + "CheckoutSessionStartedEvent", "ComponentCreatedEvent", "ComponentDeletedEvent", "ComponentUpdatedEvent", "CustomerCreatedEvent", "CustomerCreationRejectedEvent", "CustomerDeletedEvent", + "CustomerPaymentMethodAttachedEvent", + "CustomerPaymentMethodDetachedEvent", + "CustomerPaymentMethodExpiringEvent", "CustomerUpdatedEvent", "DeploymentZoneCreatedEvent", "DeploymentZoneDeletedEvent", @@ -63,20 +124,50 @@ "FeatureFlagDeletedEvent", "FeatureFlagEvaluatedEvent", "FeatureFlagUpdatedEvent", + "InstanceAddonAddedEvent", + "InstanceAddonQuantityChangedEvent", + "InstanceAddonRemovedEvent", + "InstanceBillingCanceledEvent", + "InstanceBillingCancellationRevertedEvent", + "InstanceBillingCancellationScheduledEvent", + "InstanceBillingPlanChangeCanceledEvent", + "InstanceBillingPlanChangeScheduledEvent", + "InstanceBillingPlanChangedEvent", + "InstanceBillingProviderChangedEvent", + "InstanceBillingStartedEvent", + "InstanceBillingStatusChangedEvent", + "InstanceBillingTrialWillEndEvent", "InstanceCreatedEvent", "InstanceDeletedEvent", "InstanceDeployedEvent", "InstanceEntitlementCapExceededEvent", + "InstanceEntitlementExceptionCreatedEvent", + "InstanceEntitlementExceptionExpiredEvent", + "InstanceEntitlementExceptionRevokedEvent", "InstanceEntitlementUsagePeriodRolledOverEvent", "InstanceEntitlementUsageReachedEvent", "InstanceEntitlementUsageReportAcceptedEvent", "InstanceEntitlementUsageReportRejectedEvent", "InstanceEntitlementUsageWarningThresholdReachedEvent", "InstanceEntitlementValueGetEvent", + "InstanceInvoiceHandoffAcknowledgedEvent", + "InstanceInvoiceHeldEvent", + "InstanceInvoiceIssuedEvent", + "InstanceInvoiceMarkedUncollectibleEvent", + "InstanceInvoicePaidEvent", + "InstanceInvoicePaymentFailedEvent", + "InstanceInvoicePushFailedEvent", + "InstanceInvoicePushedEvent", + "InstanceInvoiceReconciliationMismatchEvent", + "InstanceInvoiceReleasedEvent", + "InstanceInvoiceVoidedEvent", "InstanceLifecycleStageChangedEvent", "InstanceMigratedEvent", "InstanceStatusChangedEvent", "InstanceUpdatedEvent", + "InstanceVoucherExpiredEvent", + "InstanceVoucherRedeemedEvent", + "InstanceVoucherRevokedEvent", "LicenseArchivedEvent", "LicenseCreatedEvent", "LicenseDeletedEvent", @@ -86,6 +177,9 @@ "LicenseFamilyCreatedEvent", "LicenseFamilyDeletedEvent", "LicenseFamilyUpdatedEvent", + "LicensePriceCreatedEvent", + "LicensePriceDeprecatedEvent", + "LicensePriceUpdatedEvent", "LicensePublishedEvent", "LicenseUnarchivedEvent", "LicenseUpdatedEvent", @@ -94,14 +188,328 @@ "MetadataFieldReorderedEvent", "MetadataFieldUnarchivedEvent", "MetadataFieldUpdatedEvent", + "PublishableKeyCreatedEvent", + "PublishableKeyRevokedEvent", "ReleaseCreatedEvent", "ReleaseDeletedEvent", "ReleaseDeployedEvent", "SystemOrganizationTokenIssuedEvent", + "VoucherArchivedEvent", + "VoucherCreatedEvent", + "VoucherDeletedEvent", + "VoucherExhaustedEvent", + "VoucherExpiredEvent", + "VoucherPublishedEvent", + "VoucherUpdatedEvent", "WebhookEvent", ] +class AddonEntitlementAssignedEvent(KaitenModel): + """Triggered when an add-on version grants an entitlement. Delivered as + ``com.kaiten.addon.entitlement.v1.assigned``. + """ + + name: Literal["ADDON_ENTITLEMENT_ASSIGNED"] + """The event name.""" + + type: Literal["com.kaiten.addon.entitlement.v1.assigned"] + """The event type: what tells one event from another.""" + + data: AddonGrantUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonEntitlementUnassignedEvent(KaitenModel): + """Triggered when an add-on grant is removed. Delivered as + ``com.kaiten.addon.entitlement.v1.unassigned``. + """ + + name: Literal["ADDON_ENTITLEMENT_UNASSIGNED"] + """The event name.""" + + type: Literal["com.kaiten.addon.entitlement.v1.unassigned"] + """The event type: what tells one event from another.""" + + data: AddonGrantUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonEntitlementUpdatedEvent(KaitenModel): + """Triggered when an add-on grant changes. Delivered as + ``com.kaiten.addon.entitlement.v1.updated``. + """ + + name: Literal["ADDON_ENTITLEMENT_UPDATED"] + """The event name.""" + + type: Literal["com.kaiten.addon.entitlement.v1.updated"] + """The event type: what tells one event from another.""" + + data: AddonGrantUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonPriceCreatedEvent(KaitenModel): + """Triggered when a price is added to an add-on version. Delivered as + ``com.kaiten.addon.price.v1.created``. + """ + + name: Literal["ADDON_PRICE_CREATED"] + """The event name.""" + + type: Literal["com.kaiten.addon.price.v1.created"] + """The event type: what tells one event from another.""" + + data: AddonPriceUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonPriceDeprecatedEvent(KaitenModel): + """Triggered when an add-on price stops being offered. Delivered as + ``com.kaiten.addon.price.v1.deprecated``. + """ + + name: Literal["ADDON_PRICE_DEPRECATED"] + """The event name.""" + + type: Literal["com.kaiten.addon.price.v1.deprecated"] + """The event type: what tells one event from another.""" + + data: AddonPriceUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonArchivedEvent(KaitenModel): + """Triggered when an add-on version is withdrawn from sale. Delivered as + ``com.kaiten.addon.v1.archived``. + """ + + name: Literal["ADDON_ARCHIVED"] + """The event name.""" + + type: Literal["com.kaiten.addon.v1.archived"] + """The event type: what tells one event from another.""" + + data: AddonDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonCreatedEvent(KaitenModel): + """Triggered when an add-on version is created. Delivered as ``com.kaiten.addon.v1.created``.""" + + name: Literal["ADDON_CREATED"] + """The event name.""" + + type: Literal["com.kaiten.addon.v1.created"] + """The event type: what tells one event from another.""" + + data: AddonDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonDeletedEvent(KaitenModel): + """Triggered when an add-on version is deleted. Delivered as ``com.kaiten.addon.v1.deleted``.""" + + name: Literal["ADDON_DELETED"] + """The event name.""" + + type: Literal["com.kaiten.addon.v1.deleted"] + """The event type: what tells one event from another.""" + + data: DeletedAddonDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonPublishedEvent(KaitenModel): + """Triggered when a draft add-on version goes on sale. Delivered as + ``com.kaiten.addon.v1.published``. + """ + + name: Literal["ADDON_PUBLISHED"] + """The event name.""" + + type: Literal["com.kaiten.addon.v1.published"] + """The event type: what tells one event from another.""" + + data: AddonDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonUnarchivedEvent(KaitenModel): + """Triggered when an archived add-on version goes back on sale. Delivered as + ``com.kaiten.addon.v1.unarchived``. + """ + + name: Literal["ADDON_UNARCHIVED"] + """The event name.""" + + type: Literal["com.kaiten.addon.v1.unarchived"] + """The event type: what tells one event from another.""" + + data: AddonDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class AddonUpdatedEvent(KaitenModel): + """Triggered when an add-on version, its compatibility or its family's visibility changes. + Delivered as ``com.kaiten.addon.v1.updated``. + """ + + name: Literal["ADDON_UPDATED"] + """The event name.""" + + type: Literal["com.kaiten.addon.v1.updated"] + """The event type: what tells one event from another.""" + + data: AddonUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class BillingProviderConnectedEvent(KaitenModel): + """Triggered when an organization connects a payment provider (activates its connector). + Delivered as ``com.kaiten.billing_provider.v1.connected``. + """ + + name: Literal["BILLING_PROVIDER_CONNECTED"] + """The event name.""" + + type: Literal["com.kaiten.billing_provider.v1.connected"] + """The event type: what tells one event from another.""" + + data: ConnectionDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class BillingProviderDisconnectedEvent(KaitenModel): + """Triggered when an organization disconnects a payment provider (deactivates its + connector). Delivered as ``com.kaiten.billing_provider.v1.disconnected``. + """ + + name: Literal["BILLING_PROVIDER_DISCONNECTED"] + """The event name.""" + + type: Literal["com.kaiten.billing_provider.v1.disconnected"] + """The event type: what tells one event from another.""" + + data: ConnectionDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class BillingProviderSyncFailedEvent(KaitenModel): + """Triggered when syncing with a payment provider has failed three passes in a row, once + per streak. Delivered as ``com.kaiten.billing_provider.v1.sync_failed``. + """ + + name: Literal["BILLING_PROVIDER_SYNC_FAILED"] + """The event name.""" + + type: Literal["com.kaiten.billing_provider.v1.sync_failed"] + """The event type: what tells one event from another.""" + + data: SyncFailureDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class CheckoutSessionAbandonedEvent(KaitenModel): + """Triggered when an anonymous checkout ends unpaid: still OPEN 24 h after its start + (status ABANDONED), or its INCOMPLETE subscription expired or was canceled (status + EXPIRED, with the customer and instance to clean up). Delivered as + ``com.kaiten.checkout_session.v1.abandoned``. + """ + + name: Literal["CHECKOUT_SESSION_ABANDONED"] + """The event name.""" + + type: Literal["com.kaiten.checkout_session.v1.abandoned"] + """The event type: what tells one event from another.""" + + data: SessionEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class CheckoutSessionCompletedEvent(KaitenModel): + """Triggered when an anonymous checkout completes: its ACTIVATION invoice is paid, or + nothing was due. Carries the customer and instance it created; provision on this event, + never on instance.v1.created. Delivered as ``com.kaiten.checkout_session.v1.completed``. + """ + + name: Literal["CHECKOUT_SESSION_COMPLETED"] + """The event name.""" + + type: Literal["com.kaiten.checkout_session.v1.completed"] + """The event type: what tells one event from another.""" + + data: SessionEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class CheckoutSessionStartedEvent(KaitenModel): + """Triggered when an anonymous checkout starts (POST /public/checkout-sessions). A dryRun + emits nothing. Delivered as ``com.kaiten.checkout_session.v1.started``. + """ + + name: Literal["CHECKOUT_SESSION_STARTED"] + """The event name.""" + + type: Literal["com.kaiten.checkout_session.v1.started"] + """The event type: what tells one event from another.""" + + data: SessionEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + class ComponentCreatedEvent(KaitenModel): """Triggered when a new component is created. Delivered as ``com.kaiten.component.v1.created``. @@ -113,9 +521,12 @@ class ComponentCreatedEvent(KaitenModel): type: Literal["com.kaiten.component.v1.created"] """The event type: what tells one event from another.""" - data: Component + data: ComponentDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class ComponentDeletedEvent(KaitenModel): """Triggered when a component is deleted. Delivered as ``com.kaiten.component.v1.deleted``.""" @@ -126,9 +537,12 @@ class ComponentDeletedEvent(KaitenModel): type: Literal["com.kaiten.component.v1.deleted"] """The event type: what tells one event from another.""" - data: Component + data: ComponentDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class ComponentUpdatedEvent(KaitenModel): """Triggered when a component is updated or auto-versioned. Delivered as @@ -141,9 +555,67 @@ class ComponentUpdatedEvent(KaitenModel): type: Literal["com.kaiten.component.v1.updated"] """The event type: what tells one event from another.""" - data: Component + data: ComponentDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class CustomerPaymentMethodAttachedEvent(KaitenModel): + """Triggered when a customer's default payment method in a provider is set or replaced (a + completed setup session, or a change in the provider's portal). Carries no card data. + Delivered as ``com.kaiten.customer.payment_method.v1.attached``. + """ + + name: Literal["CUSTOMER_PAYMENT_METHOD_ATTACHED"] + """The event name.""" + + type: Literal["com.kaiten.customer.payment_method.v1.attached"] + """The event type: what tells one event from another.""" + + data: PaymentMethodAttachedDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class CustomerPaymentMethodDetachedEvent(KaitenModel): + """Triggered when a customer no longer has a payment method in a provider. Delivered as + ``com.kaiten.customer.payment_method.v1.detached``. + """ + + name: Literal["CUSTOMER_PAYMENT_METHOD_DETACHED"] + """The event name.""" + + type: Literal["com.kaiten.customer.payment_method.v1.detached"] + """The event type: what tells one event from another.""" + + data: PaymentMethodDetachedDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class CustomerPaymentMethodExpiringEvent(KaitenModel): + """Triggered once, 30 days before the end of the expiry month of a customer's payment + method. Delivered as ``com.kaiten.customer.payment_method.v1.expiring``. + """ + + name: Literal["CUSTOMER_PAYMENT_METHOD_EXPIRING"] + """The event name.""" + + type: Literal["com.kaiten.customer.payment_method.v1.expiring"] + """The event type: what tells one event from another.""" + + data: PaymentMethodExpiringDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class CustomerCreatedEvent(KaitenModel): """Triggered when a new customer is created. Delivered as ``com.kaiten.customer.v1.created``.""" @@ -154,9 +626,12 @@ class CustomerCreatedEvent(KaitenModel): type: Literal["com.kaiten.customer.v1.created"] """The event type: what tells one event from another.""" - data: Customer + data: CustomerEventDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class CustomerCreationRejectedEvent(KaitenModel): """Triggered when a customer creation is refused because the organization has reached its @@ -173,6 +648,9 @@ class CustomerCreationRejectedEvent(KaitenModel): data: CustomerCreationRejected """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class CustomerDeletedEvent(KaitenModel): """Triggered when a customer is deleted. Delivered as ``com.kaiten.customer.v1.deleted``.""" @@ -183,9 +661,12 @@ class CustomerDeletedEvent(KaitenModel): type: Literal["com.kaiten.customer.v1.deleted"] """The event type: what tells one event from another.""" - data: Customer + data: CustomerEventDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class CustomerUpdatedEvent(KaitenModel): """Triggered when a customer is updated. Delivered as ``com.kaiten.customer.v1.updated``.""" @@ -196,9 +677,12 @@ class CustomerUpdatedEvent(KaitenModel): type: Literal["com.kaiten.customer.v1.updated"] """The event type: what tells one event from another.""" - data: Customer + data: CustomerEventDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class DeploymentZoneCreatedEvent(KaitenModel): """Triggered when a new deployment zone is created. Delivered as @@ -211,9 +695,12 @@ class DeploymentZoneCreatedEvent(KaitenModel): type: Literal["com.kaiten.deployment_zone.v1.created"] """The event type: what tells one event from another.""" - data: DeploymentZone + data: DeploymentZoneDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class DeploymentZoneDeletedEvent(KaitenModel): """Triggered when a deployment zone is deleted. Delivered as @@ -226,9 +713,12 @@ class DeploymentZoneDeletedEvent(KaitenModel): type: Literal["com.kaiten.deployment_zone.v1.deleted"] """The event type: what tells one event from another.""" - data: DeploymentZone + data: DeploymentZoneDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class ReleaseDeployedEvent(KaitenModel): """Triggered when a release is deployed to a deployment zone, either at zone creation or @@ -245,6 +735,9 @@ class ReleaseDeployedEvent(KaitenModel): data: Deployment """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class DeploymentZoneUpdatedEvent(KaitenModel): """Triggered when a deployment zone is updated. Delivered as @@ -257,9 +750,12 @@ class DeploymentZoneUpdatedEvent(KaitenModel): type: Literal["com.kaiten.deployment_zone.v1.updated"] """The event type: what tells one event from another.""" - data: DeploymentZone + data: DeploymentZoneDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class EntitlementCreatedEvent(KaitenModel): """Triggered when a new entitlement is created. Delivered as @@ -272,9 +768,12 @@ class EntitlementCreatedEvent(KaitenModel): type: Literal["com.kaiten.entitlement.v1.created"] """The event type: what tells one event from another.""" - data: Entitlement + data: EntitlementDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class EntitlementDeletedEvent(KaitenModel): """Triggered when an entitlement is deleted. Delivered as @@ -287,9 +786,12 @@ class EntitlementDeletedEvent(KaitenModel): type: Literal["com.kaiten.entitlement.v1.deleted"] """The event type: what tells one event from another.""" - data: Entitlement + data: EntitlementDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class EntitlementUpdatedEvent(KaitenModel): """Triggered when an entitlement is updated. Delivered as @@ -302,9 +804,12 @@ class EntitlementUpdatedEvent(KaitenModel): type: Literal["com.kaiten.entitlement.v1.updated"] """The event type: what tells one event from another.""" - data: Entitlement + data: EntitlementDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class EntitlementGroupCreatedEvent(KaitenModel): """Triggered when a new entitlement group is created. Delivered as @@ -317,9 +822,12 @@ class EntitlementGroupCreatedEvent(KaitenModel): type: Literal["com.kaiten.entitlement_group.v1.created"] """The event type: what tells one event from another.""" - data: EntitlementGroup + data: EntitlementGroupDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class EntitlementGroupDeletedEvent(KaitenModel): """Triggered when an entitlement group is deleted. Delivered as @@ -332,9 +840,12 @@ class EntitlementGroupDeletedEvent(KaitenModel): type: Literal["com.kaiten.entitlement_group.v1.deleted"] """The event type: what tells one event from another.""" - data: EntitlementGroup + data: EntitlementGroupDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class EntitlementGroupUpdatedEvent(KaitenModel): """Triggered when an entitlement group is updated. Delivered as @@ -347,9 +858,12 @@ class EntitlementGroupUpdatedEvent(KaitenModel): type: Literal["com.kaiten.entitlement_group.v1.updated"] """The event type: what tells one event from another.""" - data: EntitlementGroup + data: EntitlementGroupDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class FeatureFlagCreatedEvent(KaitenModel): """Triggered when a new feature flag is created. Delivered as @@ -362,183 +876,717 @@ class FeatureFlagCreatedEvent(KaitenModel): type: Literal["com.kaiten.feature_flag.v1.created"] """The event type: what tells one event from another.""" - data: FeatureFlag + data: FeatureFlagDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class FeatureFlagDeletedEvent(KaitenModel): + """Triggered when a feature flag is deleted. Delivered as + ``com.kaiten.feature_flag.v1.deleted``. + """ + + name: Literal["FEATURE_FLAG_DELETED"] + """The event name.""" + + type: Literal["com.kaiten.feature_flag.v1.deleted"] + """The event type: what tells one event from another.""" + + data: FeatureFlagDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class FeatureFlagEvaluatedEvent(KaitenModel): + """Triggered on every flag evaluation. The payload carries no evaluation context, so no + targeting key or user attribute is exposed. Delivered as + ``com.kaiten.feature_flag.v1.evaluated``. + """ + + name: Literal["FEATURE_FLAG_EVALUATED"] + """The event name.""" + + type: Literal["com.kaiten.feature_flag.v1.evaluated"] + """The event type: what tells one event from another.""" + + data: FeatureFlagEvaluated """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class FeatureFlagUpdatedEvent(KaitenModel): + """Triggered when a feature flag is updated. Delivered as + ``com.kaiten.feature_flag.v1.updated``. + """ + + name: Literal["FEATURE_FLAG_UPDATED"] + """The event name.""" + + type: Literal["com.kaiten.feature_flag.v1.updated"] + """The event type: what tells one event from another.""" + + data: FeatureFlagDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class SystemOrganizationTokenIssuedEvent(KaitenModel): + """Triggered when a platform credential mints an organization-scoped credential for the + `system:kaiten` platform identity inside this organization. Carries the platform + credential that performed the mint and the scopes granted, never the credential value. + Expect this at the rate an integration refreshes: 96 a day per process at a 15-minute + lifetime, more if it refreshes before expiry. Delivered as + ``com.kaiten.identity.v1.system_token_issued``. + """ + + name: Literal["SYSTEM_ORGANIZATION_TOKEN_ISSUED"] + """The event name.""" + + type: Literal["com.kaiten.identity.v1.system_token_issued"] + """The event type: what tells one event from another.""" + + data: SystemTokenIssuance + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceAddonAddedEvent(KaitenModel): + """Triggered when an add-on is attached to an instance. Delivered as + ``com.kaiten.instance.addon.v1.added``. + """ + + name: Literal["INSTANCE_ADDON_ADDED"] + """The event name.""" + + type: Literal["com.kaiten.instance.addon.v1.added"] + """The event type: what tells one event from another.""" + + data: InstanceAddonUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceAddonQuantityChangedEvent(KaitenModel): + """Triggered when the quantity of an instance's add-on changes. Delivered as + ``com.kaiten.instance.addon.v1.quantity_changed``. + """ + + name: Literal["INSTANCE_ADDON_QUANTITY_CHANGED"] + """The event name.""" + + type: Literal["com.kaiten.instance.addon.v1.quantity_changed"] + """The event type: what tells one event from another.""" + + data: InstanceAddonUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceAddonRemovedEvent(KaitenModel): + """Triggered when an add-on is removed from an instance. Delivered as + ``com.kaiten.instance.addon.v1.removed``. + """ + + name: Literal["INSTANCE_ADDON_REMOVED"] + """The event name.""" + + type: Literal["com.kaiten.instance.addon.v1.removed"] + """The event type: what tells one event from another.""" + + data: InstanceAddonUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingCanceledEvent(KaitenModel): + """Triggered when a subscription is canceled: at its period's end, immediately, or during + its trial. Delivered as ``com.kaiten.instance.billing.v1.canceled``. + """ + + name: Literal["INSTANCE_BILLING_CANCELED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.canceled"] + """The event type: what tells one event from another.""" + + data: CancellationDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingCancellationRevertedEvent(KaitenModel): + """Triggered when a scheduled cancellation is reverted before the boundary. Delivered as + ``com.kaiten.instance.billing.v1.cancellation_reverted``. + """ + + name: Literal["INSTANCE_BILLING_CANCELLATION_REVERTED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.cancellation_reverted"] + """The event type: what tells one event from another.""" + + data: CancellationChangeDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingCancellationScheduledEvent(KaitenModel): + """Triggered when a subscription is set to cancel at the end of its period. Delivered as + ``com.kaiten.instance.billing.v1.cancellation_scheduled``. + """ + + name: Literal["INSTANCE_BILLING_CANCELLATION_SCHEDULED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.cancellation_scheduled"] + """The event type: what tells one event from another.""" + + data: CancellationChangeDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingPlanChangeCanceledEvent(KaitenModel): + """Triggered when a scheduled plan change is dropped: by request, or by a cancellation. + Delivered as ``com.kaiten.instance.billing.v1.plan_change_canceled``. + """ + + name: Literal["INSTANCE_BILLING_PLAN_CHANGE_CANCELED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.plan_change_canceled"] + """The event type: what tells one event from another.""" + + data: PlanChangeScheduleDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingPlanChangeScheduledEvent(KaitenModel): + """Triggered when a plan change is scheduled for the next boundary, or replaced. Delivered + as ``com.kaiten.instance.billing.v1.plan_change_scheduled``. + """ + + name: Literal["INSTANCE_BILLING_PLAN_CHANGE_SCHEDULED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.plan_change_scheduled"] + """The event type: what tells one event from another.""" + + data: PlanChangeScheduleDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingPlanChangedEvent(KaitenModel): + """Triggered when a scheduled plan change is applied at a boundary. Delivered as + ``com.kaiten.instance.billing.v1.plan_changed``. + """ + + name: Literal["INSTANCE_BILLING_PLAN_CHANGED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.plan_changed"] + """The event type: what tells one event from another.""" + + data: PlanChangeDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingProviderChangedEvent(KaitenModel): + """Triggered when a subscription moves to another payment provider, from its next invoice + on. Delivered as ``com.kaiten.instance.billing.v1.provider_changed``. + """ + + name: Literal["INSTANCE_BILLING_PROVIDER_CHANGED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.provider_changed"] + """The event type: what tells one event from another.""" + + data: ProviderChangeDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingStartedEvent(KaitenModel): + """Triggered when an instance is subscribed, or a CANCELED subscription subscribed again. + Delivered as ``com.kaiten.instance.billing.v1.started``. + """ + + name: Literal["INSTANCE_BILLING_STARTED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.started"] + """The event type: what tells one event from another.""" + + data: BillingStartedDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingStatusChangedEvent(KaitenModel): + """Triggered when a trial converts, or a subscription becomes PAST_DUE or leaves it. + Delivered as ``com.kaiten.instance.billing.v1.status_changed``. + """ + + name: Literal["INSTANCE_BILLING_STATUS_CHANGED"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.status_changed"] + """The event type: what tells one event from another.""" + + data: StatusChangeDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceBillingTrialWillEndEvent(KaitenModel): + """Triggered once per trial, at reminderAt: its family's trialReminderDays before + trialEndsAt (3 by default), or at once when the trial is shorter. Carries what a + reminder e-mail needs, read when it is emitted: the customer and its billing e-mail, the + instance, the plan, daysLeft, the charge the conversion would make and the card it would + be made to. Delivered as ``com.kaiten.instance.billing.v1.trial_will_end``. + """ + + name: Literal["INSTANCE_BILLING_TRIAL_WILL_END"] + """The event name.""" + + type: Literal["com.kaiten.instance.billing.v1.trial_will_end"] + """The event type: what tells one event from another.""" + + data: TrialWillEndDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementCapExceededEvent(KaitenModel): + """Triggered when a SOFT-enforcement entitlement's usage crosses from below the cap to + above it. Fires once per crossing. Delivered as + ``com.kaiten.instance.entitlement.v1.cap_exceeded``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_CAP_EXCEEDED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.cap_exceeded"] + """The event type: what tells one event from another.""" + + data: InstanceEntitlementCapExceeded + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementUsagePeriodRolledOverEvent(KaitenModel): + """Triggered once per usage window closed by a report: the entitlement's own stored bucket, + and one synthetic event per window skipped entirely since the last report. Delivered as + ``com.kaiten.instance.entitlement.v1.usage_period_rolled_over``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.usage_period_rolled_over"] + """The event type: what tells one event from another.""" + + data: InstanceEntitlementUsagePeriodRolledOver + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementUsageReachedEvent(KaitenModel): + """Triggered when an instance's entitlement usage reaches its threshold. Delivered as + ``com.kaiten.instance.entitlement.v1.usage_reached``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_USAGE_REACHED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.usage_reached"] + """The event type: what tells one event from another.""" + + data: EntitlementUsageDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementUsageReportAcceptedEvent(KaitenModel): + """Triggered when a usage report is persisted, including a SOFT-enforcement overage. + Delivered as ``com.kaiten.instance.entitlement.v1.usage_report_accepted``. + """ + + name: Literal["ENTITLEMENT_USAGE_REPORT_ACCEPTED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.usage_report_accepted"] + """The event type: what tells one event from another.""" + + data: InstanceEntitlementUsageReportAccepted + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementUsageReportRejectedEvent(KaitenModel): + """Triggered when a usage report is refused because it would take a HARD-enforcement + entitlement past its cap. The usage is not persisted. Delivered as + ``com.kaiten.instance.entitlement.v1.usage_report_rejected``. + """ + + name: Literal["ENTITLEMENT_USAGE_REPORT_REJECTED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.usage_report_rejected"] + """The event type: what tells one event from another.""" + + data: InstanceEntitlementUsageReportAccepted + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementUsageWarningThresholdReachedEvent(KaitenModel): + """Triggered when an instance's entitlement usage crosses its configured early-warning + percentage boundary, before the cap is reached. Fires once per crossing. Delivered as + ``com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached"] + """The event type: what tells one event from another.""" + + data: InstanceEntitlementUsageWarningThresholdReached + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementValueGetEvent(KaitenModel): + """Triggered on every read of an instance entitlement's usage value. This is a read audit + signal, not a state change. Delivered as + ``com.kaiten.instance.entitlement.v1.value_get``. + """ + + name: Literal["ENTITLEMENT_VALUE_GET"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement.v1.value_get"] + """The event type: what tells one event from another.""" + + data: InstanceEntitlementValueGet + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementExceptionCreatedEvent(KaitenModel): + """Triggered when an exception is created on one entitlement of an instance. Emitted + whether or not billing is enabled. Delivered as + ``com.kaiten.instance.entitlement_exception.v1.created``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_EXCEPTION_CREATED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement_exception.v1.created"] + """The event type: what tells one event from another.""" + + data: ExceptionEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementExceptionExpiredEvent(KaitenModel): + """Triggered once per exception, when its endsAt passes; never for a revoked one. Delivered + as ``com.kaiten.instance.entitlement_exception.v1.expired``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_EXCEPTION_EXPIRED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement_exception.v1.expired"] + """The event type: what tells one event from another.""" + + data: ExceptionEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceEntitlementExceptionRevokedEvent(KaitenModel): + """Triggered when an exception is revoked. Revoking one already revoked or ended emits + nothing. Delivered as ``com.kaiten.instance.entitlement_exception.v1.revoked``. + """ + + name: Literal["INSTANCE_ENTITLEMENT_EXCEPTION_REVOKED"] + """The event name.""" + + type: Literal["com.kaiten.instance.entitlement_exception.v1.revoked"] + """The event type: what tells one event from another.""" + + data: ExceptionEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + -class FeatureFlagDeletedEvent(KaitenModel): - """Triggered when a feature flag is deleted. Delivered as - ``com.kaiten.feature_flag.v1.deleted``. +class InstanceInvoiceHandoffAcknowledgedEvent(KaitenModel): + """Triggered when the organization's accounting system acknowledges an invoice from the + handoff queue, or the invoice is marked paid while still waiting there. Delivered as + ``com.kaiten.instance.invoice.v1.handoff_acknowledged``. """ - name: Literal["FEATURE_FLAG_DELETED"] + name: Literal["INSTANCE_INVOICE_HANDOFF_ACKNOWLEDGED"] """The event name.""" - type: Literal["com.kaiten.feature_flag.v1.deleted"] + type: Literal["com.kaiten.instance.invoice.v1.handoff_acknowledged"] """The event type: what tells one event from another.""" - data: FeatureFlag + data: HandoffAcknowledgementDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class FeatureFlagEvaluatedEvent(KaitenModel): - """Triggered on every flag evaluation. The payload carries no evaluation context, so no - targeting key or user attribute is exposed. Delivered as - ``com.kaiten.feature_flag.v1.evaluated``. + +class InstanceInvoiceHeldEvent(KaitenModel): + """Triggered when a period closes into an invoice held as a DRAFT, because the usage + journal it was measured from failed a consistency check. It is neither issued nor handed + off until released or recomposed. Delivered as ``com.kaiten.instance.invoice.v1.held``. """ - name: Literal["FEATURE_FLAG_EVALUATED"] + name: Literal["INSTANCE_INVOICE_HELD"] """The event name.""" - type: Literal["com.kaiten.feature_flag.v1.evaluated"] + type: Literal["com.kaiten.instance.invoice.v1.held"] """The event type: what tells one event from another.""" - data: FeatureFlagEvaluated + data: HeldInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class FeatureFlagUpdatedEvent(KaitenModel): - """Triggered when a feature flag is updated. Delivered as - ``com.kaiten.feature_flag.v1.updated``. + +class InstanceInvoiceIssuedEvent(KaitenModel): + """Triggered once per invoice with something owed, when it is issued. For an invoice the + organization collects itself, it is the signal to claim it from the handoff queue, which + stays the source of truth. Delivered as ``com.kaiten.instance.invoice.v1.issued``. """ - name: Literal["FEATURE_FLAG_UPDATED"] + name: Literal["INSTANCE_INVOICE_ISSUED"] """The event name.""" - type: Literal["com.kaiten.feature_flag.v1.updated"] + type: Literal["com.kaiten.instance.invoice.v1.issued"] """The event type: what tells one event from another.""" - data: FeatureFlag + data: IssuedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class SystemOrganizationTokenIssuedEvent(KaitenModel): - """Triggered when a platform credential mints an organization-scoped credential for the - `system:kaiten` platform identity inside this organization. Carries the platform - credential that performed the mint and the scopes granted, never the credential value. - Expect this at the rate an integration refreshes: roughly 120 per day per process at a - 15-minute lifetime. Delivered as ``com.kaiten.identity.v1.system_token_issued``. + +class InstanceInvoiceMarkedUncollectibleEvent(KaitenModel): + """Triggered when an invoice is written off. Delivered as + ``com.kaiten.instance.invoice.v1.marked_uncollectible``. """ - name: Literal["SYSTEM_ORGANIZATION_TOKEN_ISSUED"] + name: Literal["INSTANCE_INVOICE_MARKED_UNCOLLECTIBLE"] """The event name.""" - type: Literal["com.kaiten.identity.v1.system_token_issued"] + type: Literal["com.kaiten.instance.invoice.v1.marked_uncollectible"] """The event type: what tells one event from another.""" - data: SystemTokenIssuance + data: UncollectibleInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementCapExceededEvent(KaitenModel): - """Triggered when a SOFT-enforcement entitlement's usage crosses from below the cap to - above it. Fires once per crossing. Delivered as - ``com.kaiten.instance.entitlement.v1.cap_exceeded``. + +class InstanceInvoicePaidEvent(KaitenModel): + """Triggered when an invoice is paid: marked paid by the organization, or issued paid + because nothing was owed. Delivered as ``com.kaiten.instance.invoice.v1.paid``. """ - name: Literal["INSTANCE_ENTITLEMENT_CAP_EXCEEDED"] + name: Literal["INSTANCE_INVOICE_PAID"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.cap_exceeded"] + type: Literal["com.kaiten.instance.invoice.v1.paid"] """The event type: what tells one event from another.""" - data: InstanceEntitlementCapExceeded + data: PaidInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementUsagePeriodRolledOverEvent(KaitenModel): - """Triggered once per usage window closed by a report: the entitlement's own stored bucket, - and one synthetic event per window skipped entirely since the last report. Delivered as - ``com.kaiten.instance.entitlement.v1.usage_period_rolled_over``. + +class InstanceInvoicePaymentFailedEvent(KaitenModel): + """Triggered when the automatic charge of an invoice is refused, or needs the customer to + authenticate it on the invoice's hosted page. Delivered as + ``com.kaiten.instance.invoice.v1.payment_failed``. """ - name: Literal["INSTANCE_ENTITLEMENT_USAGE_PERIOD_ROLLED_OVER"] + name: Literal["INSTANCE_INVOICE_PAYMENT_FAILED"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.usage_period_rolled_over"] + type: Literal["com.kaiten.instance.invoice.v1.payment_failed"] """The event type: what tells one event from another.""" - data: InstanceEntitlementUsagePeriodRolledOver + data: PaymentFailedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementUsageReachedEvent(KaitenModel): - """Triggered when an instance's entitlement usage reaches its threshold. Delivered as - ``com.kaiten.instance.entitlement.v1.usage_reached``. + +class InstanceInvoicePushFailedEvent(KaitenModel): + """Triggered when pushing an invoice to its payment provider has failed as many times as + the alert threshold, then once a day while it keeps failing. Delivered as + ``com.kaiten.instance.invoice.v1.push_failed``. """ - name: Literal["INSTANCE_ENTITLEMENT_USAGE_REACHED"] + name: Literal["INSTANCE_INVOICE_PUSH_FAILED"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.usage_reached"] + type: Literal["com.kaiten.instance.invoice.v1.push_failed"] """The event type: what tells one event from another.""" - data: EntitlementUsage + data: PushFailedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementUsageReportAcceptedEvent(KaitenModel): - """Triggered when a usage report is persisted, including a SOFT-enforcement overage. - Delivered as ``com.kaiten.instance.entitlement.v1.usage_report_accepted``. + +class InstanceInvoicePushedEvent(KaitenModel): + """Triggered when an invoice's payment provider has issued it, right after + INSTANCE_INVOICE_ISSUED. Delivered as ``com.kaiten.instance.invoice.v1.pushed``. """ - name: Literal["ENTITLEMENT_USAGE_REPORT_ACCEPTED"] + name: Literal["INSTANCE_INVOICE_PUSHED"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.usage_report_accepted"] + type: Literal["com.kaiten.instance.invoice.v1.pushed"] """The event type: what tells one event from another.""" - data: InstanceEntitlementUsageReportAccepted + data: PushedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementUsageReportRejectedEvent(KaitenModel): - """Triggered when a usage report is refused because it would take a HARD-enforcement - entitlement past its cap. The usage is not persisted. Delivered as - ``com.kaiten.instance.entitlement.v1.usage_report_rejected``. + +class InstanceInvoiceReconciliationMismatchEvent(KaitenModel): + """Triggered when an invoice's payment provider holds amounts that differ from Kaiten's. + Kaiten never corrects itself: void and recompose the invoice. Delivered as + ``com.kaiten.instance.invoice.v1.reconciliation_mismatch``. """ - name: Literal["ENTITLEMENT_USAGE_REPORT_REJECTED"] + name: Literal["INSTANCE_INVOICE_RECONCILIATION_MISMATCH"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.usage_report_rejected"] + type: Literal["com.kaiten.instance.invoice.v1.reconciliation_mismatch"] """The event type: what tells one event from another.""" - data: InstanceEntitlementUsageReportAccepted + data: MismatchedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementUsageWarningThresholdReachedEvent(KaitenModel): - """Triggered when an instance's entitlement usage crosses its configured early-warning - percentage boundary, before the cap is reached. Fires once per crossing. Delivered as - ``com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached``. + +class InstanceInvoiceReleasedEvent(KaitenModel): + """Triggered when a held invoice leaves its hold: released by the organization, recomposed + from a sound journal, or found sound by a later check. It is issued in the same + transaction. Delivered as ``com.kaiten.instance.invoice.v1.released``. """ - name: Literal["INSTANCE_ENTITLEMENT_USAGE_WARNING_THRESHOLD_REACHED"] + name: Literal["INSTANCE_INVOICE_RELEASED"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached"] + type: Literal["com.kaiten.instance.invoice.v1.released"] """The event type: what tells one event from another.""" - data: InstanceEntitlementUsageWarningThresholdReached + data: ReleasedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" -class InstanceEntitlementValueGetEvent(KaitenModel): - """Triggered on every read of an instance entitlement's usage value. This is a read audit - signal, not a state change. Delivered as - ``com.kaiten.instance.entitlement.v1.value_get``. + +class InstanceInvoiceVoidedEvent(KaitenModel): + """Triggered when an invoice is voided. Its boundary can then be recomposed into a + replacement. Delivered as ``com.kaiten.instance.invoice.v1.voided``. """ - name: Literal["ENTITLEMENT_VALUE_GET"] + name: Literal["INSTANCE_INVOICE_VOIDED"] """The event name.""" - type: Literal["com.kaiten.instance.entitlement.v1.value_get"] + type: Literal["com.kaiten.instance.invoice.v1.voided"] """The event type: what tells one event from another.""" - data: InstanceEntitlementValueGet + data: VoidedInvoiceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceCreatedEvent(KaitenModel): """Triggered when a new instance is created. Delivered as ``com.kaiten.instance.v1.created``.""" @@ -549,9 +1597,12 @@ class InstanceCreatedEvent(KaitenModel): type: Literal["com.kaiten.instance.v1.created"] """The event type: what tells one event from another.""" - data: Instance + data: InstanceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceDeletedEvent(KaitenModel): """Triggered when an instance is deleted. Delivered as ``com.kaiten.instance.v1.deleted``.""" @@ -562,9 +1613,12 @@ class InstanceDeletedEvent(KaitenModel): type: Literal["com.kaiten.instance.v1.deleted"] """The event type: what tells one event from another.""" - data: Instance + data: InstanceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceDeployedEvent(KaitenModel): """Triggered when an instance is deployed to a deployment zone. Delivered as @@ -577,9 +1631,12 @@ class InstanceDeployedEvent(KaitenModel): type: Literal["com.kaiten.instance.v1.deployed"] """The event type: what tells one event from another.""" - data: Instance + data: InstanceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceLifecycleStageChangedEvent(KaitenModel): """Triggered when an instance commercial lifecycle stage changes. Delivered as @@ -595,6 +1652,9 @@ class InstanceLifecycleStageChangedEvent(KaitenModel): data: InstanceLifecycleStageChanged """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceMigratedEvent(KaitenModel): """Triggered when an instance is migrated from one deployment zone to another. Delivered as @@ -607,9 +1667,12 @@ class InstanceMigratedEvent(KaitenModel): type: Literal["com.kaiten.instance.v1.migrated"] """The event type: what tells one event from another.""" - data: Instance + data: InstanceDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceStatusChangedEvent(KaitenModel): """Triggered when an instance operational status changes. Delivered as @@ -625,9 +1688,15 @@ class InstanceStatusChangedEvent(KaitenModel): data: InstanceStatusChanged """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class InstanceUpdatedEvent(KaitenModel): - """Triggered when an instance is updated. Delivered as ``com.kaiten.instance.v1.updated``.""" + """Triggered when an instance is updated, by a write or by billing: the licence version a + paid checkout bought, or the fallback a subscription that ended moves it to + (licenseChangeReason BILLING_FALLBACK). Delivered as ``com.kaiten.instance.v1.updated``. + """ name: Literal["INSTANCE_UPDATED"] """The event name.""" @@ -635,9 +1704,66 @@ class InstanceUpdatedEvent(KaitenModel): type: Literal["com.kaiten.instance.v1.updated"] """The event type: what tells one event from another.""" - data: Instance + data: InstanceUpdateDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceVoucherExpiredEvent(KaitenModel): + """Triggered when a PRICE redemption has discounted its last invoice, or a boost's window + ends. Delivered as ``com.kaiten.instance.voucher.v1.expired``. + """ + + name: Literal["INSTANCE_VOUCHER_EXPIRED"] + """The event name.""" + + type: Literal["com.kaiten.instance.voucher.v1.expired"] + """The event type: what tells one event from another.""" + + data: RedemptionDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceVoucherRedeemedEvent(KaitenModel): + """Triggered when an instance redeems a voucher. Delivered as + ``com.kaiten.instance.voucher.v1.redeemed``. + """ + + name: Literal["INSTANCE_VOUCHER_REDEEMED"] + """The event name.""" + + type: Literal["com.kaiten.instance.voucher.v1.redeemed"] + """The event type: what tells one event from another.""" + + data: RedemptionDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class InstanceVoucherRevokedEvent(KaitenModel): + """Triggered when a redemption is revoked. Delivered as + ``com.kaiten.instance.voucher.v1.revoked``. + """ + + name: Literal["INSTANCE_VOUCHER_REVOKED"] + """The event name.""" + + type: Literal["com.kaiten.instance.voucher.v1.revoked"] + """The event type: what tells one event from another.""" + + data: RedemptionDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseEntitlementAssignedEvent(KaitenModel): """Triggered when an entitlement is assigned to a license. Delivered as @@ -650,9 +1776,12 @@ class LicenseEntitlementAssignedEvent(KaitenModel): type: Literal["com.kaiten.license.entitlement.v1.assigned"] """The event type: what tells one event from another.""" - data: LicenseEntitlement + data: LicenseEntitlementDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseEntitlementUnassignedEvent(KaitenModel): """Triggered when an entitlement is unassigned from a license. Delivered as @@ -665,9 +1794,12 @@ class LicenseEntitlementUnassignedEvent(KaitenModel): type: Literal["com.kaiten.license.entitlement.v1.unassigned"] """The event type: what tells one event from another.""" - data: LicenseEntitlement + data: LicenseEntitlementDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseEntitlementUpdatedEvent(KaitenModel): """Triggered when a license entitlement is updated. Delivered as @@ -680,9 +1812,66 @@ class LicenseEntitlementUpdatedEvent(KaitenModel): type: Literal["com.kaiten.license.entitlement.v1.updated"] """The event type: what tells one event from another.""" - data: LicenseEntitlement + data: LicenseEntitlementDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class LicensePriceCreatedEvent(KaitenModel): + """Triggered when a price is added to a licence version. Delivered as + ``com.kaiten.license.price.v1.created``. + """ + + name: Literal["LICENSE_PRICE_CREATED"] + """The event name.""" + + type: Literal["com.kaiten.license.price.v1.created"] + """The event type: what tells one event from another.""" + + data: LicensePriceEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class LicensePriceDeprecatedEvent(KaitenModel): + """Triggered when a licence price is deprecated. Delivered as + ``com.kaiten.license.price.v1.deprecated``. + """ + + name: Literal["LICENSE_PRICE_DEPRECATED"] + """The event name.""" + + type: Literal["com.kaiten.license.price.v1.deprecated"] + """The event type: what tells one event from another.""" + + data: LicensePriceEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class LicensePriceUpdatedEvent(KaitenModel): + """Triggered when a price of a DRAFT licence version is edited. Delivered as + ``com.kaiten.license.price.v1.updated``. + """ + + name: Literal["LICENSE_PRICE_UPDATED"] + """The event name.""" + + type: Literal["com.kaiten.license.price.v1.updated"] + """The event type: what tells one event from another.""" + + data: LicensePriceUpdatedEventDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseArchivedEvent(KaitenModel): """Triggered when a published license version is archived and withdrawn from sale. @@ -695,9 +1884,12 @@ class LicenseArchivedEvent(KaitenModel): type: Literal["com.kaiten.license.v1.archived"] """The event type: what tells one event from another.""" - data: License + data: LicenseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseCreatedEvent(KaitenModel): """Triggered when a new license is created. Delivered as ``com.kaiten.license.v1.created``.""" @@ -708,9 +1900,12 @@ class LicenseCreatedEvent(KaitenModel): type: Literal["com.kaiten.license.v1.created"] """The event type: what tells one event from another.""" - data: License + data: LicenseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseDeletedEvent(KaitenModel): """Triggered when a license is deleted. Delivered as ``com.kaiten.license.v1.deleted``.""" @@ -721,9 +1916,12 @@ class LicenseDeletedEvent(KaitenModel): type: Literal["com.kaiten.license.v1.deleted"] """The event type: what tells one event from another.""" - data: License + data: LicenseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicensePublishedEvent(KaitenModel): """Triggered when a draft license version is published and goes on sale. Delivered as @@ -736,9 +1934,12 @@ class LicensePublishedEvent(KaitenModel): type: Literal["com.kaiten.license.v1.published"] """The event type: what tells one event from another.""" - data: License + data: LicenseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseUnarchivedEvent(KaitenModel): """Triggered when an archived license version is put back on sale. Delivered as @@ -751,9 +1952,12 @@ class LicenseUnarchivedEvent(KaitenModel): type: Literal["com.kaiten.license.v1.unarchived"] """The event type: what tells one event from another.""" - data: License + data: LicenseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseUpdatedEvent(KaitenModel): """Triggered when a license is updated. Delivered as ``com.kaiten.license.v1.updated``.""" @@ -764,9 +1968,12 @@ class LicenseUpdatedEvent(KaitenModel): type: Literal["com.kaiten.license.v1.updated"] """The event type: what tells one event from another.""" - data: License + data: LicenseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseFamilyCreatedEvent(KaitenModel): """Triggered when a license is created without naming a family: its version 1 opens a new @@ -781,9 +1988,12 @@ class LicenseFamilyCreatedEvent(KaitenModel): type: Literal["com.kaiten.license_family.v1.created"] """The event type: what tells one event from another.""" - data: LicenseFamilyView + data: LicenseFamilyViewDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseFamilyDeletedEvent(KaitenModel): """Triggered when a license family's last version is deleted, which deletes the family and @@ -798,9 +2008,12 @@ class LicenseFamilyDeletedEvent(KaitenModel): type: Literal["com.kaiten.license_family.v1.deleted"] """The event type: what tells one event from another.""" - data: LicenseFamilyView + data: LicenseFamilyViewDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class LicenseFamilyUpdatedEvent(KaitenModel): """Triggered when a license family starts serving another version, or none: a newer version @@ -818,9 +2031,12 @@ class LicenseFamilyUpdatedEvent(KaitenModel): type: Literal["com.kaiten.license_family.v1.updated"] """The event type: what tells one event from another.""" - data: LicenseFamilyView + data: LicenseFamilyViewDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class MetadataFieldArchivedEvent(KaitenModel): """Triggered when a metadata field is archived and leaves the active schema composition. @@ -833,9 +2049,12 @@ class MetadataFieldArchivedEvent(KaitenModel): type: Literal["com.kaiten.metadata_field.v1.archived"] """The event type: what tells one event from another.""" - data: MetadataField + data: MetadataFieldDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class MetadataFieldCreatedEvent(KaitenModel): """Triggered when a metadata field is declared for a resource type. Delivered as @@ -848,9 +2067,12 @@ class MetadataFieldCreatedEvent(KaitenModel): type: Literal["com.kaiten.metadata_field.v1.created"] """The event type: what tells one event from another.""" - data: MetadataField + data: MetadataFieldDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class MetadataFieldReorderedEvent(KaitenModel): """Triggered when the display order of a resource type's metadata fields is rewritten. @@ -866,6 +2088,9 @@ class MetadataFieldReorderedEvent(KaitenModel): data: MetadataFieldReordered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class MetadataFieldUnarchivedEvent(KaitenModel): """Triggered when an archived metadata field is restored to the active schema composition. @@ -878,9 +2103,12 @@ class MetadataFieldUnarchivedEvent(KaitenModel): type: Literal["com.kaiten.metadata_field.v1.unarchived"] """The event type: what tells one event from another.""" - data: MetadataField + data: MetadataFieldDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class MetadataFieldUpdatedEvent(KaitenModel): """Triggered when a metadata field's label or JSON schema changes. A reordering emits @@ -893,9 +2121,48 @@ class MetadataFieldUpdatedEvent(KaitenModel): type: Literal["com.kaiten.metadata_field.v1.updated"] """The event type: what tells one event from another.""" - data: MetadataField + data: MetadataFieldDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class PublishableKeyCreatedEvent(KaitenModel): + """Triggered when a publishable key is issued. The key is never in the payload, only its + hint. Delivered as ``com.kaiten.publishable_key.v1.created``. + """ + + name: Literal["PUBLISHABLE_KEY_CREATED"] + """The event name.""" + + type: Literal["com.kaiten.publishable_key.v1.created"] + """The event type: what tells one event from another.""" + + data: PublishableKeyEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class PublishableKeyRevokedEvent(KaitenModel): + """Triggered once, when a publishable key is revoked. Delivered as + ``com.kaiten.publishable_key.v1.revoked``. + """ + + name: Literal["PUBLISHABLE_KEY_REVOKED"] + """The event name.""" + + type: Literal["com.kaiten.publishable_key.v1.revoked"] + """The event type: what tells one event from another.""" + + data: PublishableKeyEventDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class ReleaseCreatedEvent(KaitenModel): """Triggered when a new release is created. Delivered as ``com.kaiten.release.v1.created``.""" @@ -906,9 +2173,12 @@ class ReleaseCreatedEvent(KaitenModel): type: Literal["com.kaiten.release.v1.created"] """The event type: what tells one event from another.""" - data: Release + data: ReleaseDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + class ReleaseDeletedEvent(KaitenModel): """Triggered when a release is deleted. Delivered as ``com.kaiten.release.v1.deleted``.""" @@ -919,14 +2189,159 @@ class ReleaseDeletedEvent(KaitenModel): type: Literal["com.kaiten.release.v1.deleted"] """The event type: what tells one event from another.""" - data: Release + data: ReleaseDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherArchivedEvent(KaitenModel): + """Triggered when a voucher is archived. Delivered as ``com.kaiten.voucher.v1.archived``.""" + + name: Literal["VOUCHER_ARCHIVED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.archived"] + """The event type: what tells one event from another.""" + + data: VoucherDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherCreatedEvent(KaitenModel): + """Triggered when a voucher is created. The code is never in the payload. Delivered as + ``com.kaiten.voucher.v1.created``. + """ + + name: Literal["VOUCHER_CREATED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.created"] + """The event type: what tells one event from another.""" + + data: VoucherDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherDeletedEvent(KaitenModel): + """Triggered when a voucher no instance ever redeemed is deleted, with its last state. + Delivered as ``com.kaiten.voucher.v1.deleted``. + """ + + name: Literal["VOUCHER_DELETED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.deleted"] + """The event type: what tells one event from another.""" + + data: VoucherDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherExhaustedEvent(KaitenModel): + """Triggered by the redemption that reaches maxRedemptions. Delivered as + ``com.kaiten.voucher.v1.exhausted``. + """ + + name: Literal["VOUCHER_EXHAUSTED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.exhausted"] + """The event type: what tells one event from another.""" + + data: ExhaustedEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherExpiredEvent(KaitenModel): + """Triggered when an active voucher reaches its expiresAt and can no longer be redeemed. + Delivered as ``com.kaiten.voucher.v1.expired``. + """ + + name: Literal["VOUCHER_EXPIRED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.expired"] + """The event type: what tells one event from another.""" + + data: ExpiredEventDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherPublishedEvent(KaitenModel): + """Triggered when a draft voucher becomes redeemable. Delivered as + ``com.kaiten.voucher.v1.published``. + """ + + name: Literal["VOUCHER_PUBLISHED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.published"] + """The event type: what tells one event from another.""" + + data: VoucherDelivered + """What the event is about.""" + + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + + +class VoucherUpdatedEvent(KaitenModel): + """Triggered when a voucher changes. Delivered as ``com.kaiten.voucher.v1.updated``.""" + + name: Literal["VOUCHER_UPDATED"] + """The event name.""" + + type: Literal["com.kaiten.voucher.v1.updated"] + """The event type: what tells one event from another.""" + + data: VoucherDelivered """What the event is about.""" + headers: EventHeaders | None = None + """The trace context of the request that produced the event.""" + WebhookEvent = Annotated[ - ComponentCreatedEvent + AddonEntitlementAssignedEvent + | AddonEntitlementUnassignedEvent + | AddonEntitlementUpdatedEvent + | AddonPriceCreatedEvent + | AddonPriceDeprecatedEvent + | AddonArchivedEvent + | AddonCreatedEvent + | AddonDeletedEvent + | AddonPublishedEvent + | AddonUnarchivedEvent + | AddonUpdatedEvent + | BillingProviderConnectedEvent + | BillingProviderDisconnectedEvent + | BillingProviderSyncFailedEvent + | CheckoutSessionAbandonedEvent + | CheckoutSessionCompletedEvent + | CheckoutSessionStartedEvent + | ComponentCreatedEvent | ComponentDeletedEvent | ComponentUpdatedEvent + | CustomerPaymentMethodAttachedEvent + | CustomerPaymentMethodDetachedEvent + | CustomerPaymentMethodExpiringEvent | CustomerCreatedEvent | CustomerCreationRejectedEvent | CustomerDeletedEvent @@ -946,6 +2361,19 @@ class ReleaseDeletedEvent(KaitenModel): | FeatureFlagEvaluatedEvent | FeatureFlagUpdatedEvent | SystemOrganizationTokenIssuedEvent + | InstanceAddonAddedEvent + | InstanceAddonQuantityChangedEvent + | InstanceAddonRemovedEvent + | InstanceBillingCanceledEvent + | InstanceBillingCancellationRevertedEvent + | InstanceBillingCancellationScheduledEvent + | InstanceBillingPlanChangeCanceledEvent + | InstanceBillingPlanChangeScheduledEvent + | InstanceBillingPlanChangedEvent + | InstanceBillingProviderChangedEvent + | InstanceBillingStartedEvent + | InstanceBillingStatusChangedEvent + | InstanceBillingTrialWillEndEvent | InstanceEntitlementCapExceededEvent | InstanceEntitlementUsagePeriodRolledOverEvent | InstanceEntitlementUsageReachedEvent @@ -953,6 +2381,20 @@ class ReleaseDeletedEvent(KaitenModel): | InstanceEntitlementUsageReportRejectedEvent | InstanceEntitlementUsageWarningThresholdReachedEvent | InstanceEntitlementValueGetEvent + | InstanceEntitlementExceptionCreatedEvent + | InstanceEntitlementExceptionExpiredEvent + | InstanceEntitlementExceptionRevokedEvent + | InstanceInvoiceHandoffAcknowledgedEvent + | InstanceInvoiceHeldEvent + | InstanceInvoiceIssuedEvent + | InstanceInvoiceMarkedUncollectibleEvent + | InstanceInvoicePaidEvent + | InstanceInvoicePaymentFailedEvent + | InstanceInvoicePushFailedEvent + | InstanceInvoicePushedEvent + | InstanceInvoiceReconciliationMismatchEvent + | InstanceInvoiceReleasedEvent + | InstanceInvoiceVoidedEvent | InstanceCreatedEvent | InstanceDeletedEvent | InstanceDeployedEvent @@ -960,9 +2402,15 @@ class ReleaseDeletedEvent(KaitenModel): | InstanceMigratedEvent | InstanceStatusChangedEvent | InstanceUpdatedEvent + | InstanceVoucherExpiredEvent + | InstanceVoucherRedeemedEvent + | InstanceVoucherRevokedEvent | LicenseEntitlementAssignedEvent | LicenseEntitlementUnassignedEvent | LicenseEntitlementUpdatedEvent + | LicensePriceCreatedEvent + | LicensePriceDeprecatedEvent + | LicensePriceUpdatedEvent | LicenseArchivedEvent | LicenseCreatedEvent | LicenseDeletedEvent @@ -977,16 +2425,45 @@ class ReleaseDeletedEvent(KaitenModel): | MetadataFieldReorderedEvent | MetadataFieldUnarchivedEvent | MetadataFieldUpdatedEvent + | PublishableKeyCreatedEvent + | PublishableKeyRevokedEvent | ReleaseCreatedEvent - | ReleaseDeletedEvent, + | ReleaseDeletedEvent + | VoucherArchivedEvent + | VoucherCreatedEvent + | VoucherDeletedEvent + | VoucherExhaustedEvent + | VoucherExpiredEvent + | VoucherPublishedEvent + | VoucherUpdatedEvent, Field(discriminator="type"), ] """Any event Kaiten delivers to a webhook endpoint, discriminated by ``type``.""" WEBHOOK_EVENTS: dict[str, type[KaitenModel]] = { + "com.kaiten.addon.entitlement.v1.assigned": AddonEntitlementAssignedEvent, + "com.kaiten.addon.entitlement.v1.unassigned": AddonEntitlementUnassignedEvent, + "com.kaiten.addon.entitlement.v1.updated": AddonEntitlementUpdatedEvent, + "com.kaiten.addon.price.v1.created": AddonPriceCreatedEvent, + "com.kaiten.addon.price.v1.deprecated": AddonPriceDeprecatedEvent, + "com.kaiten.addon.v1.archived": AddonArchivedEvent, + "com.kaiten.addon.v1.created": AddonCreatedEvent, + "com.kaiten.addon.v1.deleted": AddonDeletedEvent, + "com.kaiten.addon.v1.published": AddonPublishedEvent, + "com.kaiten.addon.v1.unarchived": AddonUnarchivedEvent, + "com.kaiten.addon.v1.updated": AddonUpdatedEvent, + "com.kaiten.billing_provider.v1.connected": BillingProviderConnectedEvent, + "com.kaiten.billing_provider.v1.disconnected": BillingProviderDisconnectedEvent, + "com.kaiten.billing_provider.v1.sync_failed": BillingProviderSyncFailedEvent, + "com.kaiten.checkout_session.v1.abandoned": CheckoutSessionAbandonedEvent, + "com.kaiten.checkout_session.v1.completed": CheckoutSessionCompletedEvent, + "com.kaiten.checkout_session.v1.started": CheckoutSessionStartedEvent, "com.kaiten.component.v1.created": ComponentCreatedEvent, "com.kaiten.component.v1.deleted": ComponentDeletedEvent, "com.kaiten.component.v1.updated": ComponentUpdatedEvent, + "com.kaiten.customer.payment_method.v1.attached": CustomerPaymentMethodAttachedEvent, + "com.kaiten.customer.payment_method.v1.detached": CustomerPaymentMethodDetachedEvent, + "com.kaiten.customer.payment_method.v1.expiring": CustomerPaymentMethodExpiringEvent, "com.kaiten.customer.v1.created": CustomerCreatedEvent, "com.kaiten.customer.v1.creation_rejected": CustomerCreationRejectedEvent, "com.kaiten.customer.v1.deleted": CustomerDeletedEvent, @@ -1006,6 +2483,19 @@ class ReleaseDeletedEvent(KaitenModel): "com.kaiten.feature_flag.v1.evaluated": FeatureFlagEvaluatedEvent, "com.kaiten.feature_flag.v1.updated": FeatureFlagUpdatedEvent, "com.kaiten.identity.v1.system_token_issued": SystemOrganizationTokenIssuedEvent, + "com.kaiten.instance.addon.v1.added": InstanceAddonAddedEvent, + "com.kaiten.instance.addon.v1.quantity_changed": InstanceAddonQuantityChangedEvent, + "com.kaiten.instance.addon.v1.removed": InstanceAddonRemovedEvent, + "com.kaiten.instance.billing.v1.canceled": InstanceBillingCanceledEvent, + "com.kaiten.instance.billing.v1.cancellation_reverted": InstanceBillingCancellationRevertedEvent, + "com.kaiten.instance.billing.v1.cancellation_scheduled": InstanceBillingCancellationScheduledEvent, + "com.kaiten.instance.billing.v1.plan_change_canceled": InstanceBillingPlanChangeCanceledEvent, + "com.kaiten.instance.billing.v1.plan_change_scheduled": InstanceBillingPlanChangeScheduledEvent, + "com.kaiten.instance.billing.v1.plan_changed": InstanceBillingPlanChangedEvent, + "com.kaiten.instance.billing.v1.provider_changed": InstanceBillingProviderChangedEvent, + "com.kaiten.instance.billing.v1.started": InstanceBillingStartedEvent, + "com.kaiten.instance.billing.v1.status_changed": InstanceBillingStatusChangedEvent, + "com.kaiten.instance.billing.v1.trial_will_end": InstanceBillingTrialWillEndEvent, "com.kaiten.instance.entitlement.v1.cap_exceeded": InstanceEntitlementCapExceededEvent, "com.kaiten.instance.entitlement.v1.usage_period_rolled_over": InstanceEntitlementUsagePeriodRolledOverEvent, "com.kaiten.instance.entitlement.v1.usage_reached": InstanceEntitlementUsageReachedEvent, @@ -1013,6 +2503,20 @@ class ReleaseDeletedEvent(KaitenModel): "com.kaiten.instance.entitlement.v1.usage_report_rejected": InstanceEntitlementUsageReportRejectedEvent, "com.kaiten.instance.entitlement.v1.usage_warning_threshold_reached": InstanceEntitlementUsageWarningThresholdReachedEvent, "com.kaiten.instance.entitlement.v1.value_get": InstanceEntitlementValueGetEvent, + "com.kaiten.instance.entitlement_exception.v1.created": InstanceEntitlementExceptionCreatedEvent, + "com.kaiten.instance.entitlement_exception.v1.expired": InstanceEntitlementExceptionExpiredEvent, + "com.kaiten.instance.entitlement_exception.v1.revoked": InstanceEntitlementExceptionRevokedEvent, + "com.kaiten.instance.invoice.v1.handoff_acknowledged": InstanceInvoiceHandoffAcknowledgedEvent, + "com.kaiten.instance.invoice.v1.held": InstanceInvoiceHeldEvent, + "com.kaiten.instance.invoice.v1.issued": InstanceInvoiceIssuedEvent, + "com.kaiten.instance.invoice.v1.marked_uncollectible": InstanceInvoiceMarkedUncollectibleEvent, + "com.kaiten.instance.invoice.v1.paid": InstanceInvoicePaidEvent, + "com.kaiten.instance.invoice.v1.payment_failed": InstanceInvoicePaymentFailedEvent, + "com.kaiten.instance.invoice.v1.push_failed": InstanceInvoicePushFailedEvent, + "com.kaiten.instance.invoice.v1.pushed": InstanceInvoicePushedEvent, + "com.kaiten.instance.invoice.v1.reconciliation_mismatch": InstanceInvoiceReconciliationMismatchEvent, + "com.kaiten.instance.invoice.v1.released": InstanceInvoiceReleasedEvent, + "com.kaiten.instance.invoice.v1.voided": InstanceInvoiceVoidedEvent, "com.kaiten.instance.v1.created": InstanceCreatedEvent, "com.kaiten.instance.v1.deleted": InstanceDeletedEvent, "com.kaiten.instance.v1.deployed": InstanceDeployedEvent, @@ -1020,9 +2524,15 @@ class ReleaseDeletedEvent(KaitenModel): "com.kaiten.instance.v1.migrated": InstanceMigratedEvent, "com.kaiten.instance.v1.status_changed": InstanceStatusChangedEvent, "com.kaiten.instance.v1.updated": InstanceUpdatedEvent, + "com.kaiten.instance.voucher.v1.expired": InstanceVoucherExpiredEvent, + "com.kaiten.instance.voucher.v1.redeemed": InstanceVoucherRedeemedEvent, + "com.kaiten.instance.voucher.v1.revoked": InstanceVoucherRevokedEvent, "com.kaiten.license.entitlement.v1.assigned": LicenseEntitlementAssignedEvent, "com.kaiten.license.entitlement.v1.unassigned": LicenseEntitlementUnassignedEvent, "com.kaiten.license.entitlement.v1.updated": LicenseEntitlementUpdatedEvent, + "com.kaiten.license.price.v1.created": LicensePriceCreatedEvent, + "com.kaiten.license.price.v1.deprecated": LicensePriceDeprecatedEvent, + "com.kaiten.license.price.v1.updated": LicensePriceUpdatedEvent, "com.kaiten.license.v1.archived": LicenseArchivedEvent, "com.kaiten.license.v1.created": LicenseCreatedEvent, "com.kaiten.license.v1.deleted": LicenseDeletedEvent, @@ -1037,7 +2547,16 @@ class ReleaseDeletedEvent(KaitenModel): "com.kaiten.metadata_field.v1.reordered": MetadataFieldReorderedEvent, "com.kaiten.metadata_field.v1.unarchived": MetadataFieldUnarchivedEvent, "com.kaiten.metadata_field.v1.updated": MetadataFieldUpdatedEvent, + "com.kaiten.publishable_key.v1.created": PublishableKeyCreatedEvent, + "com.kaiten.publishable_key.v1.revoked": PublishableKeyRevokedEvent, "com.kaiten.release.v1.created": ReleaseCreatedEvent, "com.kaiten.release.v1.deleted": ReleaseDeletedEvent, + "com.kaiten.voucher.v1.archived": VoucherArchivedEvent, + "com.kaiten.voucher.v1.created": VoucherCreatedEvent, + "com.kaiten.voucher.v1.deleted": VoucherDeletedEvent, + "com.kaiten.voucher.v1.exhausted": VoucherExhaustedEvent, + "com.kaiten.voucher.v1.expired": VoucherExpiredEvent, + "com.kaiten.voucher.v1.published": VoucherPublishedEvent, + "com.kaiten.voucher.v1.updated": VoucherUpdatedEvent, } """Every published event type, mapped to the model that parses it.""" diff --git a/tests/contract.py b/tests/contract.py index ea8a8cb..0dd797a 100644 --- a/tests/contract.py +++ b/tests/contract.py @@ -121,7 +121,8 @@ def _write_registry(surface: str) -> Registry: The API validates a request against the resource schema with its readOnly properties no longer required -- and a client has no business sending one -- so readOnly properties are dropped outright. Every body is ``additionalProperties: false``, so a request carrying one - then fails exactly like a misspelled key. + then fails exactly like a misspelled key. And a nullable enum takes null, as the API's + validator does, though the document lists only its other values. """ writable = _without_read_only(copy.deepcopy(document(surface))) resource = Resource.from_contents(writable, default_specification=DRAFT202012) @@ -141,6 +142,14 @@ def _without_read_only(node: Any) -> Any: del properties[name] if isinstance(node.get("required"), list): node["required"] = [name for name in node["required"] if name not in read_only] + kind, enum = node.get("type"), node.get("enum") + if ( + isinstance(kind, list) + and "null" in kind + and isinstance(enum, list) + and None not in enum + ): + enum.append(None) for value in node.values(): _without_read_only(value) elif isinstance(node, list): diff --git a/tests/test_billing.py b/tests/test_billing.py new file mode 100644 index 0000000..3248b7e --- /dev/null +++ b/tests/test_billing.py @@ -0,0 +1,200 @@ +# Copyright 2026 KAITEN INC +# SPDX-License-Identifier: Apache-2.0 + +"""Billing and the usage history: what the contract table cannot see. + +The public routes' credentials, the exports' bytes, the usage report's idempotency key and +the history's ``afterSeq`` pages. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any + +import httpx +import pytest +from contract import sample +from helpers import body, respond + +from kaitencloud import ( + CredentialError, + PaginationError, + ServiceUnavailableError, + ThresholdExceededError, + TransactionIdReusedError, +) + +REPORTED = sample({"$ref": "#/components/schemas/ReportedEntitlementUsage"}, "core") +CATALOG = sample({"$ref": "#/components/schemas/PublicCatalog"}, "core") +REPORT = sample({"$ref": "#/components/schemas/UsageReport"}, "core") +HISTORY = "/api/instances/acme-production/entitlements/seats/usage/reports" + + +def test_a_publishable_key_travels_in_its_own_header_never_as_a_bearer_token( + make_client: Callable[..., Any], +) -> None: + client, recorder = make_client(respond(200, CATALOG), token="pk_live_3f6b") + client.billing.public.get_catalog(include_addons=False) + + request = recorder.only + assert request.headers["x-kaiten-publishable-key"] == "pk_live_3f6b" + assert "authorization" not in request.headers + assert request.url.params["include"] == "none" + + +@pytest.mark.parametrize( + ("token", "call"), + [ + ("ksh_org", lambda c: c.billing.public.get_catalog()), + ("kst_session", lambda c: c.billing.public.create_checkout_session(turnstile_token="t")), + ("pk_live", lambda c: c.billing.session.get_portal()), + ("ksh_org", lambda c: c.billing.session.cancel()), + ("pk_live", lambda c: c.billing.public.confirm_checkout_session("cs_1")), + ], + ids=["catalog", "checkout-start", "session", "session-write", "checkout-confirm"], +) +def test_a_public_route_refuses_any_other_credential_before_sending( + make_client: Callable[..., Any], token: str, call: Callable[[Any], Any] +) -> None: + client, recorder = make_client(respond(200, {}), token=token) + with pytest.raises(CredentialError, match="takes"): + call(client) + assert not recorder + + +def test_invoice_statuses_are_repeated_query_parameters(make_client: Callable[..., Any]) -> None: + client, recorder = make_client(respond(200, {"items": [], "hasMore": False})) + client.billing.invoices.list(status=["MANUAL", "PAID"], overdue=True) + + params = recorder.only.url.params + assert params.get_list("status") == ["MANUAL", "PAID"] + assert params["overdue"] == "true" + + +@pytest.mark.parametrize( + ("export_format", "media_type"), [("csv", "text/csv"), ("json", "application/x-ndjson")] +) +def test_an_export_asks_for_its_format_and_returns_the_bytes( + make_client: Callable[..., Any], export_format: Any, media_type: str +) -> None: + client, recorder = make_client(respond(200, "id,total\ninv_1,4900\n")) + + exported = client.billing.invoices.export(format=export_format) + + assert exported == b"id,total\ninv_1,4900\n" + assert recorder.only.headers["accept"] == media_type + assert recorder.only.url.params["format"] == export_format + + +def test_a_limit_below_one_is_refused_before_sending(make_client: Callable[..., Any]) -> None: + client, recorder = make_client(respond(200, {"items": [], "hasMore": False})) + with pytest.raises(ValueError, match="at least 1"): + client.billing.invoices.list(limit=0) + assert not recorder + + +def test_a_keyed_usage_report_sends_its_key_and_says_it_was_replayed( + make_client: Callable[..., Any], +) -> None: + headers = {"Idempotent-Replayed": "true", "Kaiten-Metadata-Dropped": "too_large"} + client, recorder = make_client(respond(200, REPORTED, headers=headers)) + + result = client.instances.report_usage_detailed( + "acme-production", "tokens", 1200, transaction_id="llm-call-9f2c:tokens" + ) + + assert recorder.only.headers["idempotency-key"] == "llm-call-9f2c:tokens" + assert body(recorder.only) == {"value": {"type": "number", "value": 1200}, "behavior": "append"} + assert (result.replayed, result.metadata_dropped) == (True, True) + assert result.usage.entitlement_slug == REPORTED["entitlementSlug"] + + +def test_a_usage_report_without_a_key_sends_none(make_client: Callable[..., Any]) -> None: + client, recorder = make_client(respond(200, REPORTED)) + + result = client.instances.report_usage_detailed("acme-production", "seats", 1) + + assert "idempotency-key" not in recorder.only.headers + assert (result.replayed, result.metadata_dropped) == (False, False) + + +@pytest.mark.parametrize( + ("code", "error"), + [ + ("ReportEntitlementUsageMetric.TransactionIdReused", TransactionIdReusedError), + ("ReportEntitlementUsageMetric.ThresholdExceeded", ThresholdExceededError), + ], + ids=["key-reused", "threshold"], +) +def test_a_refused_usage_report_is_named_by_its_code( + make_client: Callable[..., Any], code: str, error: type[Exception] +) -> None: + problem = {"title": "Conflict", "status": 409, "code": code} + client, _ = make_client(respond(409, problem)) + + with pytest.raises(error) as raised: + client.instances.report_usage_detailed("acme-production", "seats", 1, transaction_id="k1") + assert type(raised.value) is error + + +def test_a_keyed_usage_report_is_never_retried(make_client: Callable[..., Any]) -> None: + # A Kaiten older than keys ignores the header, and would count a resent report twice. + client, recorder = make_client(respond(503), max_retries=2) + with pytest.raises(ServiceUnavailableError): + client.instances.report_usage_detailed("acme-production", "seats", 1, transaction_id="k1") + assert len(recorder) == 1 + + +def history(count: int, *, stuck: bool = False) -> Callable[[httpx.Request], httpx.Response]: + """A usage history endpoint paging by afterSeq, as Kaiten does; ``stuck`` never advances.""" + + def handler(request: httpx.Request) -> httpx.Response: + after = int(request.url.params.get("afterSeq", "0")) + limit = int(request.url.params["limit"]) + rows = [ + {**REPORT, "reportSeq": seq} for seq in range(after + 1, min(after + limit, count) + 1) + ] + page: dict[str, Any] = {"items": rows} + if rows and rows[-1]["reportSeq"] < count: + page["nextAfterSeq"] = after if stuck else rows[-1]["reportSeq"] + return httpx.Response(200, json=page) + + return handler + + +def test_the_usage_history_walks_every_page(make_client: Callable[..., Any]) -> None: + client, recorder = make_client(history(1200)) + + reports = client.instances.list_usage_reports("acme-production", "seats") + + assert [report.report_seq for report in reports] == list(range(1, 1201)) + assert [request.url.params.get("afterSeq") for request in recorder] == [None, "500", "1000"] + assert {request.url.params["limit"] for request in recorder} == {"500"} + assert {request.url.path for request in recorder} == {HISTORY} + + +def test_the_usage_history_stops_at_its_ceiling(make_client: Callable[..., Any]) -> None: + client, recorder = make_client(history(1200)) + + reports = client.instances.list_usage_reports("acme-production", "seats", limit=3) + + assert [report.report_seq for report in reports] == [1, 2, 3] + assert recorder.only.url.params["limit"] == "3" + + +def test_a_history_page_that_does_not_advance_is_refused(make_client: Callable[..., Any]) -> None: + client, _ = make_client(history(1200, stuck=True)) + with pytest.raises(PaginationError, match="afterSeq"): + client.instances.list_usage_reports("acme-production", "seats") + + +def test_the_async_client_walks_the_history_too(make_async_client: Callable[..., Any]) -> None: + async def scenario() -> list[int]: + client, _ = make_async_client(history(700)) + async with client: + reports = await client.billing.invoices.list_line_reports("inv_1", "line_1") + return [report.report_seq for report in reports] + + assert asyncio.run(scenario()) == list(range(1, 701)) diff --git a/tests/test_contract.py b/tests/test_contract.py index 6331982..3bacc84 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -14,7 +14,9 @@ is ``additionalProperties: false``, so a key too many and a required key missing end the same way -- a 422 on every call, with no type error to catch it; - the rules the schema cannot express hold: a slug an update refuses, a license version a - create refuses. + create refuses; +- the credential travels where the operation's security scheme says: a bearer token, or a + publishable key in its own header. A ``-minimal`` variant calls the method with its required arguments only, which is where a required-but-nullable key left out would show. Both clients run the whole table: the async @@ -31,7 +33,7 @@ from typing import Any import pytest -from contract import contract_api, find_operation, write_errors +from contract import Operation, contract_api, document, find_operation, write_errors from helpers import ORG_ID, USER_ID, Recorder, body from kaitencloud import targeting @@ -48,6 +50,32 @@ "last_error": "timeout", } VARIANTS = [targeting.variant("on", True, description="Enabled"), targeting.variant("off", False)] +VOUCHER: dict[str, Any] = { + "name": "Welcome", + "voucher_type": "COMPOSITE", + "duration": "REPEATING", + "code": "WELCOME-2026", + "description": "First three months at 20% off, and ten more seats", + "duration_in_periods": 3, + "starts_at": NOW, + "expires_at": LATER, + "max_redemptions": 100, + "restricted_customer_slug": "acme", + "price_discount_type": "PERCENTAGE", + "price_discount_value": "20", + "price_applies_to": "SELECTED_PRICES", + "applicable_license_ids": [ID], + "applicable_license_price_ids": [ID], + "applicable_addon_ids": [SAMPLE_ID], + "applicable_addon_price_ids": [SAMPLE_ID], + "grants": [{"entitlementSlug": "seats", "modifierType": "ADD", "modifierValue": "10"}], + "flag_grants": {"new-checkout": "on"}, + "redemption_rules": { + "annualOnly": True, + "firstTimeOnly": True, + "minimumSubscriptionAmount": {"currency": "EUR", "unitAmountDecimal": "1000"}, + }, +} RULES = [ targeting.rule("Growth plan", "__kaiten.license.slug == 'growth'", "on"), targeting.split_rule("Progressive rollout", "true", {"on": 20, "off": 80}), @@ -66,6 +94,7 @@ class Case: required: frozenset[str] = frozenset() surface: str = "core" name: str = "" + token: str | None = None CASES = [ @@ -117,13 +146,18 @@ class Case: external_customer_id="crm-42", domain="acme.com", integrations={"attio": INTEGRATION}, + billing_email="billing@acme.com", ), minimal=lambda c: c.customers.create(name="Acme"), ), Case( "update-customer", lambda c: c.customers.update( - "acme", name="Acme Corp", external_customer_id="crm-42", domain="acme.com" + "acme", + name="Acme Corp", + external_customer_id="crm-42", + domain="acme.com", + billing_email="billing@acme.com", ), minimal=lambda c: c.customers.update("acme", name="Acme Corp"), forbidden=frozenset({"slug", "integrations"}), @@ -482,6 +516,10 @@ class Case: family_slug="growth", family_id=ID, lifecycle_state="PUBLISHED", + pricing_type="PAID", + trial_period_days=14, + requires_payment_method=True, + self_serve_cta_url="https://acme.com/contact", ), minimal=lambda c: c.licenses.create(name="Growth", type="PAID"), forbidden=frozenset({"version"}), @@ -504,6 +542,10 @@ class Case: description="For scaling teams", is_default=True, version_name="Winter 2026", + pricing_type="CUSTOM", + trial_period_days=0, + requires_payment_method=False, + self_serve_cta_url="", ), minimal=lambda c: c.licenses.update("growth-v2", name="Growth", type="PAID"), # a version never changes family or number, and moves state only through the @@ -539,7 +581,11 @@ class Case: ), Case("delete-license-entitlement", lambda c: c.licenses.delete_entitlement("growth", "seats")), # metadata fields - Case("list-metadata-fields", lambda c: c.metadata_fields.list("INSTANCE")), + Case( + "list-metadata-fields", + lambda c: c.metadata_fields.list("INSTANCE", include_archived=True), + minimal=lambda c: c.metadata_fields.list("INSTANCE"), + ), Case( "create-metadata-field", lambda c: c.metadata_fields.create( @@ -612,6 +658,688 @@ class Case: "delete-service-account-token", lambda c: c.service_accounts.delete_token("billing-sync", "ci"), ), + # instances: keyed usage reports, the usage history, entitlement exceptions + Case( + "reportEntitlementUsageMetric", + lambda c: c.instances.report_usage_detailed( + "acme-production", + "seats", + 3, + behavior="set", + metadata={"source": "billing"}, + transaction_id="llm-call-9f2c:tokens", + ), + minimal=lambda c: c.instances.report_usage_detailed("acme-production", "seats", 1), + name="reportEntitlementUsageMetric-detailed", + ), + Case( + "listUsageReports", + # A ceiling: the fake API answers every page with the same nextAfterSeq. + lambda c: c.instances.list_usage_reports( + "acme-production", "seats", from_=NOW, to=LATER, transaction_id="call-1", limit=1 + ), + ), + Case( + "exportUsageReports", + lambda c: c.instances.export_usage_reports( + "acme-production", "seats", from_=NOW, to=LATER, format="json" + ), + minimal=lambda c: c.instances.export_usage_reports("acme-production", "seats"), + ), + Case( + "exportOrganizationUsageReports", + lambda c: c.instances.export_organization_usage_reports( + from_=NOW, + to=LATER, + format="json", + instance_slug="acme-production", + instance_id=ID, + entitlement_slug="seats", + entitlement_id=SAMPLE_ID, + ), + minimal=lambda c: c.instances.export_organization_usage_reports(), + ), + Case( + "listEntitlementExceptions", + lambda c: c.instances.list_entitlement_exceptions("acme-production", include_inactive=True), + minimal=lambda c: c.instances.list_entitlement_exceptions("acme-production"), + ), + Case( + "createEntitlementException", + lambda c: c.instances.create_entitlement_exception( + "acme-production", + "seats", + kind="SET", + reason="Pilot extension", + value=200, + starts_at=NOW, + ends_at=LATER, + ), + minimal=lambda c: c.instances.create_entitlement_exception( + "acme-production", "seats", kind="UNLIMITED", reason="Pilot extension" + ), + required=frozenset({"entitlementSlug", "kind", "reason"}), + ), + Case( + "revokeEntitlementException", + lambda c: c.instances.revoke_entitlement_exception("acme-production", ID), + ), + # license families + Case( + "updateLicenseFamily", + lambda c: c.license_families.update( + "growth", + is_public=True, + fallback_license_family_slug="free", + trial_end_without_payment_method="CANCEL", + trial_reminder_days=3, + ), + minimal=lambda c: c.license_families.update("growth"), + ), + # billing + Case("getBillingSettings", lambda c: c.billing.get_settings()), + Case( + "updateBillingSettings", + lambda c: c.billing.update_settings( + default_collection_method="SEND_INVOICE", + default_days_until_due=30, + handoff_stripe_invoices=False, + ), + ), + Case("getBillingCapabilities", lambda c: c.billing.get_capabilities()), + Case("getBillingHealth", lambda c: c.billing.get_health()), + Case("syncBillingProvider", lambda c: c.billing.sync()), + Case( + "closeBillingPeriods", + lambda c: c.billing.close_periods(instance_slug="acme-production"), + minimal=lambda c: c.billing.close_periods(), + ), + # billing: add-ons + Case( + "listAddons", + lambda c: c.billing.addons.list(lifecycle_state="PUBLISHED", family_slug="extra-seats"), + minimal=lambda c: c.billing.addons.list(), + ), + Case("getAddon", lambda c: c.billing.addons.get("extra-seats-v1")), + Case( + "createAddon", + lambda c: c.billing.addons.create( + name="Extra seats", + pricing_type="PAID", + description="Ten more seats", + slug="extra-seats-v2", + family_slug="extra-seats", + family_id=ID, + version_name="Winter 2026", + lifecycle_state="DRAFT", + is_default=False, + max_quantity=10, + increase_billing="TRUE_UP", + ), + minimal=lambda c: c.billing.addons.create(name="Extra seats", pricing_type="PAID"), + required=frozenset({"description"}), + ), + Case( + "updateAddon", + lambda c: c.billing.addons.update( + "extra-seats-v1", + name="Extra seats", + description="Ten more seats", + is_default=True, + version_name="Autumn 2026", + max_quantity=20, + ), + minimal=lambda c: c.billing.addons.update("extra-seats-v1", name="Extra seats"), + required=frozenset({"description", "isDefault"}), + ), + Case("deleteAddon", lambda c: c.billing.addons.delete("extra-seats-v1")), + Case("publishAddon", lambda c: c.billing.addons.publish("extra-seats-v1")), + Case("archiveAddon", lambda c: c.billing.addons.archive("extra-seats-v1")), + Case("unarchiveAddon", lambda c: c.billing.addons.unarchive("extra-seats-v1")), + Case("listAddonCompatibility", lambda c: c.billing.addons.list_compatibility("extra-seats-v1")), + Case( + "setAddonCompatibility", + lambda c: c.billing.addons.set_compatibility("extra-seats-v1", "growth"), + ), + Case( + "removeAddonCompatibility", + lambda c: c.billing.addons.remove_compatibility("extra-seats-v1", "growth"), + ), + Case("listAddonEntitlements", lambda c: c.billing.addons.list_entitlements("extra-seats-v1")), + Case( + "getAddonEntitlement", + lambda c: c.billing.addons.get_entitlement("extra-seats-v1", "seats"), + ), + Case( + "assignAddonEntitlement", + lambda c: c.billing.addons.assign_entitlement( + "extra-seats-v1", "seats", 10, override_behavior="ADD", overage_percent=0 + ), + minimal=lambda c: c.billing.addons.assign_entitlement("extra-seats-v1", "sso", True), + required=frozenset({"entitlementSlug", "value"}), + ), + Case( + "updateAddonEntitlement", + lambda c: c.billing.addons.update_entitlement( + "extra-seats-v1", "seats", 20, override_behavior="MAX", overage_percent=10 + ), + minimal=lambda c: c.billing.addons.update_entitlement("extra-seats-v1", "seats", 20), + forbidden=frozenset({"entitlementSlug"}), + ), + Case( + "unassignAddonEntitlement", + lambda c: c.billing.addons.unassign_entitlement("extra-seats-v1", "seats"), + ), + Case( + "listAddonPrices", + lambda c: c.billing.addons.list_prices("extra-seats-v1", status="ACTIVE"), + minimal=lambda c: c.billing.addons.list_prices("extra-seats-v1"), + ), + Case( + "createAddonPrice", + lambda c: c.billing.addons.create_price( + "extra-seats-v1", + billing_model="FLAT_FEE", + currency="EUR", + unit_amount_decimal="1500", + billing_period="MONTHLY", + billing_timing="ADVANCE", + is_default=True, + display_label="Extra seats", + display_order=1, + ), + minimal=lambda c: c.billing.addons.create_price( + "extra-seats-v1", billing_model="USAGE_BASED", currency="EUR", unit_amount_decimal="2" + ), + ), + Case( + "deprecateAddonPrice", + lambda c: c.billing.addons.deprecate_price("extra-seats-v1", ID), + ), + Case("listAddonFamilies", lambda c: c.billing.addons.list_families()), + Case("getAddonFamily", lambda c: c.billing.addons.get_family("extra-seats")), + Case( + "updateAddonFamily", + lambda c: c.billing.addons.update_family( + "extra-seats", is_public=True, increase_billing="NEXT_BOUNDARY" + ), + minimal=lambda c: c.billing.addons.update_family("extra-seats"), + ), + # billing: anonymous checkouts, customer sessions, publishable keys + Case( + "listCheckoutSessions", + lambda c: c.billing.checkout_sessions.list( + status=["COMPLETED", "ABANDONED"], + email="ada@acme.com", + client_reference_id="ref-1", + limit=10, + ), + minimal=lambda c: c.billing.checkout_sessions.list(), + ), + Case("getCheckoutSessionAdmin", lambda c: c.billing.checkout_sessions.get(ID)), + Case( + "createCustomerSession", + lambda c: c.billing.customer_sessions.create( + customer_slug="acme", instance_slug="acme-production", ttl_seconds=900 + ), + minimal=lambda c: c.billing.customer_sessions.create(customer_slug="acme"), + ), + Case("revokeCustomerSession", lambda c: c.billing.customer_sessions.revoke(ID)), + Case( + "listPublishableKeys", + lambda c: c.billing.publishable_keys.list(include_revoked=True), + minimal=lambda c: c.billing.publishable_keys.list(), + ), + Case( + "createPublishableKey", + lambda c: c.billing.publishable_keys.create( + label="Pricing page", + allowed_origins=["https://acme.com"], + anonymous_checkout=True, + turnstile_site_key="0x4AAAAAAA", + turnstile_secret="0x4BBBBBBB", + ), + minimal=lambda c: c.billing.publishable_keys.create( + label="Pricing page", allowed_origins=[] + ), + required=frozenset({"allowedOrigins"}), + ), + Case( + "updatePublishableKey", + lambda c: c.billing.publishable_keys.update( + ID, + label="Pricing page", + allowed_origins=["https://acme.com"], + anonymous_checkout=False, + turnstile_site_key="", + turnstile_secret="", + ), + minimal=lambda c: c.billing.publishable_keys.update(ID), + ), + Case("revokePublishableKey", lambda c: c.billing.publishable_keys.revoke(ID)), + # billing: a customer's payment method and portal + Case("getCustomerBilling", lambda c: c.billing.customers.get("acme")), + Case("detachPaymentMethod", lambda c: c.billing.customers.detach_payment_method("acme")), + Case( + "createPaymentMethodSession", + lambda c: c.billing.customers.create_payment_method_session( + "acme", return_url="https://acme.com/billing", currency="EUR" + ), + minimal=lambda c: c.billing.customers.create_payment_method_session( + "acme", return_url="https://acme.com/billing" + ), + ), + Case( + "completePaymentMethodSession", + lambda c: c.billing.customers.complete_payment_method_session("acme", "seti_1"), + ), + Case( + "createPortalSession", + lambda c: c.billing.customers.create_portal_session( + "acme", return_url="https://acme.com/billing" + ), + ), + # billing: the accounting handoff + Case( + "listHandoff", + lambda c: c.billing.handoff.list(status="ACKNOWLEDGED", limit=10), + minimal=lambda c: c.billing.handoff.list(), + ), + Case( + "claimHandoff", + lambda c: c.billing.handoff.claim(limit=25, lease_seconds=900), + minimal=lambda c: c.billing.handoff.claim(), + ), + Case( + "ackHandoff", + lambda c: c.billing.handoff.ack(ID, lease_id=SAMPLE_ID, external_reference="INV-0042"), + minimal=lambda c: c.billing.handoff.ack(ID), + ), + # billing: an instance's add-ons and vouchers + Case( + "listInstanceAddons", + lambda c: c.billing.instance_addons.list("acme-production", include_removed=True), + minimal=lambda c: c.billing.instance_addons.list("acme-production"), + ), + Case( + "attachInstanceAddon", + lambda c: c.billing.instance_addons.attach("acme-production", "extra-seats-v1", quantity=2), + minimal=lambda c: c.billing.instance_addons.attach("acme-production", "extra-seats-v1"), + required=frozenset({"addonSlug"}), + ), + Case( + "setInstanceAddonQuantity", + lambda c: c.billing.instance_addons.set_quantity("acme-production", "extra-seats-v1", 3), + ), + Case( + "detachInstanceAddon", + lambda c: c.billing.instance_addons.detach("acme-production", "extra-seats-v1"), + ), + Case( + "listInstanceVouchers", + lambda c: c.billing.instance_vouchers.list("acme-production", status="ACTIVE"), + minimal=lambda c: c.billing.instance_vouchers.list("acme-production"), + ), + Case( + "redeemVoucher", + lambda c: c.billing.instance_vouchers.redeem("acme-production", "WELCOME-2026"), + ), + Case( + "revokeInstanceVoucher", + lambda c: c.billing.instance_vouchers.revoke( + "acme-production", ID, reason="Granted by mistake" + ), + ), + # billing: invoices + Case( + "listInvoices", + lambda c: c.billing.invoices.list( + instance_slug="acme-production", + customer_slug="acme", + status=["MANUAL", "PAID"], + kind="RENEWAL", + provider_kind="NOOP", + overdue=True, + held=False, + handoff_status="PENDING", + issued_from=NOW, + issued_to=LATER, + boundary_from=NOW, + boundary_to=LATER, + updated_since=NOW, + limit=10, + ), + minimal=lambda c: c.billing.invoices.list(), + ), + Case("getInvoice", lambda c: c.billing.invoices.get(ID)), + Case( + "exportInvoices", + lambda c: c.billing.invoices.export( + format="json", + granularity="invoice", + instance_slug="acme-production", + customer_slug="acme", + status=["PAID"], + kind="RENEWAL", + provider_kind="STRIPE", + overdue=False, + held=False, + handoff_status="ACKNOWLEDGED", + issued_from=NOW, + issued_to=LATER, + boundary_from=NOW, + boundary_to=LATER, + updated_since=NOW, + ), + minimal=lambda c: c.billing.invoices.export(), + ), + Case("voidInvoice", lambda c: c.billing.invoices.void(ID, reason="Duplicate")), + Case("recomposeInvoice", lambda c: c.billing.invoices.recompose(ID)), + Case( + "markInvoicePaid", + lambda c: c.billing.invoices.mark_paid( + ID, paid_at=NOW, external_reference="INV-0042", note="Wire 7731" + ), + minimal=lambda c: c.billing.invoices.mark_paid(ID), + ), + Case("writeOffInvoice", lambda c: c.billing.invoices.write_off(ID, reason="Bankrupt")), + Case( + "releaseInvoiceHold", + lambda c: c.billing.invoices.release_hold(ID, reason="Usage checked by hand"), + ), + Case("retryInvoicePush", lambda c: c.billing.invoices.retry_push(ID)), + Case("syncInvoice", lambda c: c.billing.invoices.sync(ID)), + Case( + "listInvoiceLineReports", + # A ceiling: the fake API answers every page with the same nextAfterSeq. + lambda c: c.billing.invoices.list_line_reports(ID, SAMPLE_ID, limit=1), + ), + # billing: license prices + Case( + "listLicensePrices", + lambda c: c.billing.license_prices.list( + "growth-v2", status="ACTIVE", billing_model="FLAT_FEE" + ), + minimal=lambda c: c.billing.license_prices.list("growth-v2"), + ), + Case("getLicensePrice", lambda c: c.billing.license_prices.get("growth-v2", ID)), + Case( + "createLicensePrice", + lambda c: c.billing.license_prices.create( + "growth-v2", + billing_model="FLAT_FEE", + currency="EUR", + unit_amount_decimal="4900", + billing_period="ANNUAL", + billing_timing="ADVANCE", + is_default=True, + display_label="Growth, yearly", + display_order=1, + ), + minimal=lambda c: c.billing.license_prices.create( + "growth-v2", + billing_model="OVERAGE", + currency="EUR", + unit_amount_decimal="2", + metered_entitlement_slug="api-calls", + ), + ), + Case( + "updateLicensePrice", + lambda c: c.billing.license_prices.update( + "growth-v2", + ID, + unit_amount_decimal="5900", + billing_period="MONTHLY", + billing_timing="ARREARS", + metered_entitlement_slug="api-calls", + is_default=False, + display_label="Growth", + display_order=2, + ), + minimal=lambda c: c.billing.license_prices.update("growth-v2", ID), + ), + Case("deprecateLicensePrice", lambda c: c.billing.license_prices.deprecate("growth-v2", ID)), + Case( + "previewLicenseInvoice", + lambda c: c.billing.license_prices.preview_invoice( + "growth-v2", + base_price_id=ID, + add_ons={"extra-seats-v1": 2}, + sample_usage={"api-calls": "120000"}, + instance_slug="acme-production", + voucher_code="WELCOME-2026", + ), + minimal=lambda c: c.billing.license_prices.preview_invoice("growth-v2"), + ), + # billing: what a publishable key and an anonymous checkout's token reach + Case( + "getPublicCatalog", + lambda c: c.billing.public.get_catalog(family_slug="growth", include_addons=False), + minimal=lambda c: c.billing.public.get_catalog(), + token="pk_test", + ), + Case( + "createCheckoutSession", + lambda c: c.billing.public.create_checkout_session( + turnstile_token="0.turnstile", + license_slug="growth-v2", + billing_period="MONTHLY", + add_ons={"extra-seats-v1": 1}, + email="ada@acme.com", + return_url="https://acme.com/welcome", + voucher_code="WELCOME-2026", + trial=False, + client_reference_id="ref-1", + dry_run=True, + ), + minimal=lambda c: c.billing.public.create_checkout_session( + turnstile_token="0.turnstile", price_id=ID + ), + required=frozenset({"turnstileToken"}), + token="pk_test", + ), + Case( + "getCheckoutSession", + lambda c: c.billing.public.get_checkout_session(ID), + token="ksc_test", + ), + Case( + "confirmCheckoutSession", + lambda c: c.billing.public.confirm_checkout_session(ID), + token="ksc_test", + ), + # billing: what a customer session reaches + Case("getSessionPortal", lambda c: c.billing.session.get_portal(), token="kst_test"), + Case( + "listSessionInvoices", + lambda c: c.billing.session.list_invoices(limit=10), + minimal=lambda c: c.billing.session.list_invoices(), + token="kst_test", + ), + Case( + "createSessionCheckout", + lambda c: c.billing.session.checkout( + license_price_id=ID, + add_ons={"extra-seats-v1": 2}, + voucher_code="WELCOME-2026", + billing_email="billing@acme.com", + return_url="https://acme.com/billing", + setup_session_id="seti_1", + trial=False, + dry_run=True, + ), + minimal=lambda c: c.billing.session.checkout(license_price_id=ID), + token="kst_test", + ), + Case( + "cancelSessionSubscription", + lambda c: c.billing.session.cancel(reason="Too expensive"), + minimal=lambda c: c.billing.session.cancel(), + token="kst_test", + ), + Case( + "reactivateSessionSubscription", + lambda c: c.billing.session.reactivate(), + token="kst_test", + ), + Case( + "setSessionAddonQuantity", + lambda c: c.billing.session.set_addon_quantity("extra-seats-v1", 0), + token="kst_test", + ), + Case( + "createSessionPaymentMethodSession", + lambda c: c.billing.session.create_payment_method_session( + return_url="https://acme.com/billing", currency="EUR" + ), + minimal=lambda c: c.billing.session.create_payment_method_session( + return_url="https://acme.com/billing" + ), + token="kst_test", + ), + Case( + "completeSessionPaymentMethodSession", + lambda c: c.billing.session.complete_payment_method_session("seti_1"), + token="kst_test", + ), + Case( + "createSessionPortalSession", + lambda c: c.billing.session.create_portal_session(return_url="https://acme.com/billing"), + token="kst_test", + ), + Case( + "validateSessionVoucher", + lambda c: c.billing.session.validate_voucher("WELCOME-2026", license_price_id=ID), + minimal=lambda c: c.billing.session.validate_voucher("WELCOME-2026"), + token="kst_test", + ), + Case( + "redeemSessionVoucher", + lambda c: c.billing.session.redeem_voucher("WELCOME-2026", dry_run=True), + minimal=lambda c: c.billing.session.redeem_voucher("WELCOME-2026"), + token="kst_test", + ), + # billing: subscriptions + Case("getInstanceBilling", lambda c: c.billing.subscriptions.get("acme-production")), + Case( + "subscribeInstance", + lambda c: c.billing.subscriptions.subscribe( + "acme-production", + base_price_id=ID, + add_ons={"extra-seats-v1": 2}, + voucher_code="WELCOME-2026", + trial_days=14, + start_at=NOW, + provider_kind="STRIPE", + collection_method="CHARGE_AUTOMATICALLY", + days_until_due=30, + ), + minimal=lambda c: c.billing.subscriptions.subscribe("acme-production", base_price_id=ID), + ), + Case( + "updateInstanceBilling", + lambda c: c.billing.subscriptions.update( + "acme-production", + provider_kind="STRIPE", + collection_method="CHARGE_AUTOMATICALLY", + days_until_due=14, + ), + minimal=lambda c: c.billing.subscriptions.update("acme-production"), + ), + Case( + "updateInstanceBilling", + # None is sent as null: back to the organization's default. + lambda c: c.billing.subscriptions.update( + "acme-production", collection_method=None, days_until_due=None + ), + required=frozenset({"collectionMethod", "daysUntilDue"}), + name="updateInstanceBilling-defaults", + ), + Case( + "cancelSubscription", + lambda c: c.billing.subscriptions.cancel( + "acme-production", mode="IMMEDIATE", reason="Churned" + ), + minimal=lambda c: c.billing.subscriptions.cancel("acme-production"), + ), + Case("reactivateSubscription", lambda c: c.billing.subscriptions.reactivate("acme-production")), + Case( + "schedulePlanChange", + lambda c: c.billing.subscriptions.schedule_plan_change("acme-production", ID), + required=frozenset({"licensePriceId"}), + ), + Case( + "cancelPlanChange", + lambda c: c.billing.subscriptions.cancel_plan_change("acme-production"), + ), + Case( + "getUpcomingInvoice", + lambda c: c.billing.subscriptions.get_upcoming_invoice("acme-production"), + ), + Case( + "listInstanceInvoices", + lambda c: c.billing.subscriptions.list_invoices( + "acme-production", + customer_slug="acme", + status=["PUSHED"], + kind="ACTIVATION", + provider_kind="STRIPE", + overdue=True, + held=False, + handoff_status="NOT_REQUIRED", + issued_from=NOW, + issued_to=LATER, + boundary_from=NOW, + boundary_to=LATER, + updated_since=NOW, + limit=5, + ), + minimal=lambda c: c.billing.subscriptions.list_invoices("acme-production"), + ), + # billing: vouchers + Case( + "listVouchers", + lambda c: c.billing.vouchers.list( + status="ACTIVE", voucher_type="PRICE", restricted_customer_slug="acme" + ), + minimal=lambda c: c.billing.vouchers.list(), + ), + Case("getVoucher", lambda c: c.billing.vouchers.get(ID)), + Case( + "createVoucher", + lambda c: c.billing.vouchers.create(**VOUCHER), + minimal=lambda c: c.billing.vouchers.create( + name="Welcome", voucher_type="PRICE", duration="ONE_TIME" + ), + ), + Case( + "updateVoucher", + lambda c: c.billing.vouchers.update(ID, **VOUCHER), + minimal=lambda c: c.billing.vouchers.update( + ID, name="Welcome", voucher_type="PRICE", duration="FOREVER" + ), + ), + Case("deleteVoucher", lambda c: c.billing.vouchers.delete(ID)), + Case("publishVoucher", lambda c: c.billing.vouchers.publish(ID)), + Case("archiveVoucher", lambda c: c.billing.vouchers.archive(ID)), + Case( + "listVoucherRedemptions", + lambda c: c.billing.vouchers.list_redemptions(ID, limit=10), + minimal=lambda c: c.billing.vouchers.list_redemptions(ID), + ), + Case( + "lookupVoucher", + lambda c: c.billing.vouchers.lookup("WELCOME-2026"), + required=frozenset({"code"}), + ), + Case( + "validateVoucher", + lambda c: c.billing.vouchers.validate( + "WELCOME-2026", instance_slug="acme-production", license_price_id=ID + ), + minimal=lambda c: c.billing.vouchers.validate("WELCOME-2026"), + ), # platform Case("get-platform-me", lambda p: p.me(), surface="platform"), Case( @@ -654,6 +1382,12 @@ class Case: surface="platform", ), Case("delete-user", lambda p: p.users.delete(USER_ID), surface="platform"), + Case( + "platform-close-billing-periods", + lambda p: p.billing.close_periods(ORG_ID, instance_slug="acme-production"), + minimal=lambda p: p.billing.close_periods(ORG_ID), + surface="platform", + ), ] @@ -673,7 +1407,12 @@ def check(case: Case, recorder: Recorder) -> None: operation = find_operation(case.surface, request.method, request.url.path) assert operation.operation_id == case.operation_id assert set(request.url.params.keys()) <= operation.query_parameters - assert request.headers["authorization"].startswith("Bearer ") + header = credential_header(case.surface, operation) + if header == "Authorization": + assert request.headers["authorization"].startswith("Bearer ") + else: + assert "authorization" not in request.headers, "the API refuses this credential there" + assert request.headers[header] schema = operation.request_schema if schema is None: @@ -685,6 +1424,19 @@ def check(case: Case, recorder: Recorder) -> None: assert case.required <= payload.keys(), f"missing {case.required - payload.keys()}" +def credential_header(surface: str, operation: Operation) -> str: + """The header the operation's security scheme carries the credential in.""" + contract = document(surface) + requirements = operation.definition.get("security", contract.get("security")) or [{}] + (scheme,) = {name for requirement in requirements for name in requirement} + definition = contract["components"]["securitySchemes"][scheme] + return str(definition["name"]) if definition["type"] == "apiKey" else "Authorization" + + +def options(case: Case) -> dict[str, Any]: + return {"token": case.token} if case.token else {} + + @pytest.mark.parametrize(("case", "call"), _invocations()) def test_sync_client_speaks_the_contract( case: Case, @@ -693,7 +1445,7 @@ def test_sync_client_speaks_the_contract( make_platform_client: Callable[..., Any], ) -> None: factory = make_platform_client if case.surface == "platform" else make_client - client, recorder = factory(contract_api(case.surface)) + client, recorder = factory(contract_api(case.surface), **options(case)) call(client) check(case, recorder) @@ -708,7 +1460,7 @@ def test_async_client_speaks_the_contract( factory = make_async_platform_client if case.surface == "platform" else make_async_client async def scenario() -> Recorder: - client, recorder = factory(contract_api(case.surface)) + client, recorder = factory(contract_api(case.surface), **options(case)) async with client: await call(client) return recorder diff --git a/tests/test_coverage.py b/tests/test_coverage.py index cbc29f7..394534f 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -25,6 +25,8 @@ KaitenClient, KaitenPlatformClient, ) +from kaitencloud._async.resources._base import AsyncResource +from kaitencloud._sync.resources._base import Resource SURFACES = ["core", "platform"] LIFECYCLE = {"close", "with_options"} @@ -66,14 +68,20 @@ def public_methods(client: Any) -> set[str]: for name, _ in inspect.getmembers(type(client), inspect.isfunction) if not name.startswith("_") and name not in LIFECYCLE } - for namespace, resource in vars(client).items(): - if namespace.startswith("_"): - continue + # Namespaces nest -- client.billing.invoices -- so walk them all the way down. + namespaces = [(name, value) for name, value in vars(client).items() if not name.startswith("_")] + while namespaces: + path, resource = namespaces.pop() exposed |= { - f"{namespace}.{name}" + f"{path}.{name}" for name, _ in inspect.getmembers(type(resource), inspect.isfunction) if not name.startswith("_") } + namespaces += [ + (f"{path}.{name}", value) + for name, value in inspect.getmembers(resource) + if not name.startswith("_") and isinstance(value, (AsyncResource, Resource)) + ] return exposed diff --git a/tests/test_models.py b/tests/test_models.py index 245e7f8..f76d151 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -102,6 +102,7 @@ def test_discriminated_values_parse_into_their_own_class() -> None: "entitlementSlug": "seats", "licenseId": "l1", "licenseSlug": "growth", + "source": "license", "value": {"type": "number", "value": 7, "event_count": 3}, "limit": {"type": "number", "value": 10}, } diff --git a/tests/test_readme.py b/tests/test_readme.py index a0ce926..155db3a 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -34,7 +34,7 @@ } LINK = re.compile(r"\[[^\]]+\]\((?!https?:)([^)#]*)(#[^)]+)?\)") EXAMPLES = re.findall(r"```python\n(.*?)```", README, flags=re.DOTALL) -CLIENT_CALL = re.compile(r"\b(client|platform)\.(\w+)(?:\.(\w+))?\(") +CLIENT_CALL = re.compile(r"\b(client|platform)((?:\.\w+)+)\(") HELPER_CALL = re.compile(r"\b(targeting|usage)\.(\w+)\(") ENVIRONMENT_ASSUMED = "With `KAITEN_BASE_URL` and `KAITEN_AUTH_TOKEN` set" @@ -68,14 +68,20 @@ def test_every_import_from_the_sdk_exists() -> None: assert hasattr(module, alias.name), f"from {node.module} import {alias.name}" +def resolve(clients: dict[str, object], owner: str, path: str) -> object: + """``client.billing.invoices.list`` and the like, attribute by attribute.""" + target = clients[owner] + for attribute in path.strip(".").split("."): + assert hasattr(target, attribute), f"{owner}{path}" + target = getattr(target, attribute) + return target + + def test_every_client_call_names_a_real_method(clients: dict[str, object]) -> None: calls = [call for example in EXAMPLES for call in CLIENT_CALL.findall(example)] assert calls - for owner, attribute, method in calls: - target = getattr(clients[owner], attribute) - if method: - target = getattr(target, method) - assert callable(target), f"{owner}.{attribute}.{method}" + for owner, path in calls: + assert callable(resolve(clients, owner, path)), f"{owner}{path}" def test_every_helper_call_names_a_real_function() -> None: @@ -117,17 +123,14 @@ def test_the_error_tree_names_real_exceptions() -> None: def test_the_api_reference_names_real_namespaces_and_methods(clients: dict[str, object]) -> None: """The reference is tables rather than ``python`` blocks, so the checks above skip it.""" reference = README[README.index("\n## API reference") : README.index("\n## How the SDK stays")] - namespaces = re.findall(r"(client|platform)\.(\w+)", reference) + namespaces = re.findall(r"(client|platform)((?:\.\w+)+)", reference) calls = CLIENT_CALL.findall(reference) assert namespaces assert calls - for owner, attribute in namespaces: - assert hasattr(clients[owner], attribute), f"{owner}.{attribute}" - for owner, attribute, method in calls: - target = getattr(clients[owner], attribute) - if method: - target = getattr(target, method) - assert callable(target), f"{owner}.{attribute}.{method}" + for owner, path in namespaces: + resolve(clients, owner, path) + for owner, path in calls: + assert callable(resolve(clients, owner, path)), f"{owner}{path}" def anchors(text: str) -> set[str]: