diff --git a/docs/articles/monetization/api-access.mdx b/docs/articles/monetization/api-access.mdx index 3e5fe50bf..9b6c10d66 100644 --- a/docs/articles/monetization/api-access.mdx +++ b/docs/articles/monetization/api-access.mdx @@ -62,112 +62,154 @@ curl \ ## Bucket monetization configuration -Each bucket has an optional `MonetizationConfiguration` record that holds -bucket-wide defaults. The configuration is read by the runtime and the Developer -Portal — it is not stored in OpenMeter. - -| Method | Path | -| -------- | ---------------------------------------------------- | -| `GET` | `/v3/metering/{bucketId}/monetization-configuration` | -| `PUT` | `/v3/metering/{bucketId}/monetization-configuration` | -| `DELETE` | `/v3/metering/{bucketId}/monetization-configuration` | - -The `PUT` endpoint upserts the record. At least one of the four fields below -must be present. Pass any combination — fields that are omitted retain their -previous value. - -| Field | Type | Description | -| ------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `multipleSubscriptionsEnabled` | `boolean` | Stored on the bucket. Reserved for future enforcement of multi-subscription rules; today the Developer Portal create-subscription path enforces a single active subscription per customer regardless of this flag. | -| `planOrder` | `string[]` | Plan keys in display order. Drives the pricing page sort and is used by [plan changes](./subscription-lifecycle.md#plan-changes-upgrades-and-downgrades) to decide upgrade vs downgrade — moving to a plan with a higher (or equal) index is treated as an upgrade with `"immediate"` timing; a lower index is a downgrade with `"next_billing_cycle"` timing. | -| `planSettings` | `object` | Per-plan overrides keyed by plan key. The supported sub-key today is `visiblePhases` — an array of phase keys that should appear on the pricing page. Omitted means no filtering; `[]` hides all phases for that plan. | -| `maxPaymentOverdueDays` | `integer` (`>= 0`) | Bucket-level grace period for overdue payments. Used as the lowest-priority value in the resolution chain: customer metadata → plan metadata → this bucket value → built-in default of `3` days. See [Subscription and payment validation](./monetization-policy.md#subscription-and-payment-validation). | +Each bucket has an optional `MonetizationConfiguration` that holds bucket-wide +behavior — multi-subscription support, plan display order, plan-level overrides, +and the default payment grace period. The configuration is read by the runtime +and the Developer Portal; it is not stored in OpenMeter. -```bash -curl -X PUT "https://dev.zuplo.com/v3/metering/$BUCKET_ID/monetization-configuration" \ +### Read + +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/monetization-configuration \ + --header "Authorization: Bearer $ZAPI_KEY" +``` + +When no configuration row exists for the bucket, the endpoint returns a default +body with `multipleSubscriptionsEnabled: false`, an empty `planOrder`, empty +`planSettings`, and `maxPaymentOverdueDays: 3`. + +### Upsert + +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/monetization-configuration \ + --request PUT \ --header "Authorization: Bearer $ZAPI_KEY" \ --header "Content-Type: application/json" \ - --data '{ - "planOrder": ["free", "developer", "pro"], - "planSettings": { - "pro": { "visiblePhases": ["default"] } - }, - "maxPaymentOverdueDays": 7 - }' + --data @- << EOF +{ + "multipleSubscriptionsEnabled": false, + "planOrder": ["free", "starter", "pro", "enterprise"], + "planSettings": { + "pro": { "visiblePhases": ["default"] } + }, + "maxPaymentOverdueDays": 7 +} +EOF ``` -`DELETE` removes the record entirely; `GET` on a bucket with no record returns -the schema defaults (`multipleSubscriptionsEnabled: false`, `planOrder: []`, -`planSettings: {}`, `maxPaymentOverdueDays: 3`). +| Field | Type | Description | +| ------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `multipleSubscriptionsEnabled` | `boolean` | Stored on the bucket. Reserved for future multi-subscription rules; today the Developer Portal create-subscription path enforces a single active subscription per customer regardless of this flag. | +| `planOrder` | `string[]` | Ordered list of plan keys; drives pricing-page sort and upgrade/downgrade direction during plan changes | +| `planSettings` | `object` | Per-plan overrides keyed by plan key. The supported sub-key today is `visiblePhases` — an array of phase keys that should appear on the pricing page | +| `maxPaymentOverdueDays` | `integer` | Bucket-default payment grace period. Must be ≥ 0. Defaults to `3` when not set | -## Stripe setup and billing readiness +The request body must include at least one of these fields. All four fields are +optional in the request — the upsert preserves any field you don't send. + +`planOrder` is consumed when a customer changes plans through the Developer +Portal: a target plan whose index is greater than or equal to the current plan's +index is treated as an upgrade (immediate timing); a lower index is treated as a +downgrade (next-billing-cycle timing). Plans not listed in `planOrder` default +to upgrade timing. -These endpoints script the Stripe integration that the -[Monetization Service UI](./stripe-integration.md#connecting-your-stripe-account) -runs interactively. They live on the Zuplo developer API (not OpenMeter), so the -request shape is documented here. +`maxPaymentOverdueDays` is the lowest-precedence default for the payment grace +period. See +[Subscription and payment validation](./monetization-policy.md#subscription-and-payment-validation) +for the full precedence chain (customer metadata → plan metadata → bucket +configuration → built-in default). -### Connect a Stripe app +### Delete -```http -POST /v3/metering/{bucketId}/setup/stripe +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/monetization-configuration \ + --request DELETE \ + --header "Authorization: Bearer $ZAPI_KEY" ``` -Installs a Stripe app on the bucket and creates the default billing profile -linked to that app. +After deletion, GET returns the default body again. + +## Stripe setup and billing readiness -| Field | Type | Description | -| ------------- | --------- | ------------------------------------------------------------------------------------------------ | -| `apiKey` | `string` | Stripe secret or restricted key. Required. | -| `name` | `string` | Display name for the app. Required. | -| `taxEnabled` | `boolean` | Initial value for `workflow.tax.enabled` on the billing profile. Optional; defaults to `false`. | -| `taxEnforced` | `boolean` | Initial value for `workflow.tax.enforced` on the billing profile. Optional; defaults to `false`. | -| `country` | `string` | ISO 3166-1 alpha-2 supplier country for the billing profile. Optional; defaults to `"US"`. | +Most users connect Stripe through the +[Zuplo Portal](./stripe-integration.md#connecting-your-stripe-account). For +automated provisioning — CI scripts, infrastructure-as-code, or self-hosted +control planes — the same flow is available via these API endpoints. -The request fails if the key prefix does not match the bucket environment: +### Install the Stripe app -- Working-copy or preview buckets accept `sk_test_*` or `rk_test_*`. -- Production buckets accept `sk_live_*` or `rk_live_*`. +Connect a Stripe account to a bucket and create the default billing profile in +one call: -```bash -curl -X POST "https://dev.zuplo.com/v3/metering/$BUCKET_ID/setup/stripe" \ +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/setup/stripe \ + --request POST \ --header "Authorization: Bearer $ZAPI_KEY" \ --header "Content-Type: application/json" \ - --data '{ - "apiKey": "rk_test_...", - "name": "Zuplo Monetization (test)", - "taxEnabled": false, - "country": "US" - }' + --data @- << EOF +{ + "apiKey": "rk_test_...", + "name": "Stripe Billing Profile", + "taxEnabled": false, + "taxEnforced": false, + "country": "US" +} +EOF ``` -### Read the connected Stripe app +The endpoint validates the Stripe key prefix against the bucket's environment: -```http -GET /v3/metering/{bucketId}/setup/stripe -``` +- Working-copy and preview buckets accept `sk_test_*` or `rk_test_*` +- Production buckets accept `sk_live_*` or `rk_live_*` -Returns a summary of the connected Stripe app, the matched billing profile, and -connection-test status. Use this to confirm the integration is wired up before -continuing. +The response returns the installed `appId`. The endpoint fails with a +`409 Conflict` if a Stripe app is already installed for the bucket. -### Add a billing profile to a Stripe app +### Read the current Stripe setup -```http -POST /v3/metering/{bucketId}/setup/stripe/{stripeAppId}/billing-profile +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/setup/stripe \ + --header "Authorization: Bearer $ZAPI_KEY" ``` -Creates an additional billing profile against an already-installed Stripe app. -This is rarely needed — the default profile is created during initial setup. Use -this endpoint to create per-supplier-country profiles. +Returns the connected Stripe app summary and the billing profiles linked to it. + +### Create an additional billing profile + +To attach more billing profiles to the same Stripe app: + +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/setup/stripe/$STRIPE_APP_ID/billing-profile \ + --request POST \ + --header "Authorization: Bearer $ZAPI_KEY" \ + --header "Content-Type: application/json" \ + --data @- << EOF +{ + "name": "EU Billing Profile", + "taxEnabled": true, + "taxEnforced": false, + "country": "DE" +} +EOF +``` ### Check billing readiness -```http -GET /v3/metering/{bucketId}/billing-readiness +A lightweight check for tooling that gates deploys on Stripe being connected: + +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/billing-readiness \ + --header "Authorization: Bearer $ZAPI_KEY" ``` -Returns: +Response: ```json { @@ -180,15 +222,27 @@ Returns: Use this in setup wizards to gate the UI on whether Stripe is connected. -### Update an app +### Update a connected app + +Rotate the Stripe key on an existing app, or update its name and metadata: -```http -PUT /v3/metering/{bucketId}/apps/{appId} +```shell +curl \ + https://dev.zuplo.com/v3/metering/$BUCKET_ID/apps/$APP_ID \ + --request PUT \ + --header "Authorization: Bearer $ZAPI_KEY" \ + --header "Content-Type: application/json" \ + --data @- << EOF +{ + "type": "stripe", + "name": "Stripe Billing Profile", + "secretAPIKey": "rk_test_..." +} +EOF ``` -Replaces an app's configuration (name, description, metadata, and — for Stripe -apps — `secretAPIKey`). The same key-prefix validation as `POST /setup/stripe` -applies: a Stripe key must match the bucket environment. +The same key-prefix validation applies — a live key is rejected on a +non-production bucket and vice versa. ## API Reference diff --git a/docs/articles/monetization/meters.mdx b/docs/articles/monetization/meters.mdx index e6f3d9def..3ef47740d 100644 --- a/docs/articles/monetization/meters.mdx +++ b/docs/articles/monetization/meters.mdx @@ -62,9 +62,10 @@ Each event contains the `subscription` ID linking it to a subscription and a :::note -Events emitted by the `MonetizationInboundPolicy` always set `subject` and -`subscription` to the same subscription ULID. See -[Monetization Policy](./monetization-policy.md) for how usage is recorded. +The `MonetizationInboundPolicy` sets both `subject` and `subscription` to the +same subscription ID. The CloudEvents spec uses `subject` as a generic event +producer field; Zuplo populates it with the subscription ID so usage routes to +the right entitlement. ::: diff --git a/docs/articles/monetization/monetization-policy.md b/docs/articles/monetization/monetization-policy.md index eb4ad15dc..f325cf904 100644 --- a/docs/articles/monetization/monetization-policy.md +++ b/docs/articles/monetization/monetization-policy.md @@ -154,9 +154,9 @@ below it: 1. **Customer metadata** — `zuplo_max_payment_overdue_days` on the customer 2. **Plan metadata** — `zuplo_max_payment_overdue_days` on the plan -3. **Bucket configuration** — `maxPaymentOverdueDays` on the bucket's - monetization configuration (PUT - `/v3/metering/{bucketId}/monetization-configuration`) +3. **Bucket configuration** — + [`maxPaymentOverdueDays`](./api-access.mdx#bucket-monetization-configuration) + on the bucket's monetization configuration 4. **Default** — `3` days Set the value to `0` to block requests immediately when payment is overdue. @@ -296,20 +296,20 @@ the RFC 7807 Problem Details format: Common error details: -| Condition | `detail` message | -| ------------------------------- | ------------------------------------------------------------------- | -| No auth header | `"No Authorization Header"` | -| Wrong auth scheme | `"Invalid Authorization Scheme"` | -| Empty key after the auth scheme | `"No key present"` | -| Cached invalid key or 401 | `"Authorization Failed"` | -| Invalid API key | `"API Key is invalid or does not have access to the API"` | -| Expired API key | `"API Key has expired."` | -| Expired subscription | `"API Key has an expired subscription."` | -| Subscription has no payment | `"Subscription payment status is not available."` | -| Payment not made | `"Payment has not been made."` | -| Payment overdue | `"Payment is overdue. Please update your payment method."` | -| Quota exhausted | `"API Key has exceeded the allowed limit for \"X\" meter."` | -| Meter not in subscription | `"API Key does not have \"X\" meter provided by the subscription."` | +| Condition | `detail` message | +| ---------------------------------- | ------------------------------------------------------------------- | +| No auth header | `"No Authorization Header"` | +| Wrong auth scheme | `"Invalid Authorization Scheme"` | +| No key after the auth scheme | `"No key present"` | +| Cached invalid key or upstream 401 | `"Authorization Failed"` | +| Invalid API key | `"API Key is invalid or does not have access to the API"` | +| Expired API key | `"API Key has expired."` | +| Expired subscription | `"API Key has an expired subscription."` | +| Missing payment status | `"Subscription payment status is not available."` | +| Payment not made | `"Payment has not been made."` | +| Payment overdue | `"Payment is overdue. Please update your payment method."` | +| Quota exhausted | `"API Key has exceeded the allowed limit for \"X\" meter."` | +| Meter not in subscription | `"API Key does not have \"X\" meter provided by the subscription."` | ## Pipeline ordering diff --git a/docs/articles/monetization/private-plans.md b/docs/articles/monetization/private-plans.md index 2dd1f59b1..992ee748b 100644 --- a/docs/articles/monetization/private-plans.md +++ b/docs/articles/monetization/private-plans.md @@ -109,10 +109,9 @@ Save the returned `id` — you need it to publish and invite users. :::note -The plan `id` is a 26-character ULID (regex -`^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$`), separate from the human-friendly -`key` you set on creation. The publish, invite, and other plan-scoped endpoints -require the `id`, not the `key`. +The plan `id` is a 26-character ULID. It's distinct from the human-friendly +`key` field. Use the `id` (not the `key`) when calling `/publish` and +`/plan-invites`. ::: diff --git a/docs/articles/monetization/stripe-integration.md b/docs/articles/monetization/stripe-integration.md index 99eb916c1..910f8b54a 100644 --- a/docs/articles/monetization/stripe-integration.md +++ b/docs/articles/monetization/stripe-integration.md @@ -58,6 +58,15 @@ specifically Customers, Checkout Sessions, Customer Portal Sessions, Invoices, and Tax Calculations. See [What Zuplo creates in Stripe](#what-zuplo-creates-in-stripe) for the full list. +:::tip + +To script the connection — for CI, infrastructure-as-code, or self-hosted +control planes — use the +[Stripe setup API endpoints](./api-access.mdx#stripe-setup-and-billing-readiness) +instead of the Portal flow. + +::: + ### Test mode vs. live mode Connect with a Stripe **test** key (`sk_test_...`) first to validate your diff --git a/docs/articles/monetization/subscription-lifecycle.md b/docs/articles/monetization/subscription-lifecycle.md index cdfa8ed7b..09c4ea3f0 100644 --- a/docs/articles/monetization/subscription-lifecycle.md +++ b/docs/articles/monetization/subscription-lifecycle.md @@ -57,17 +57,19 @@ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions \ -H "Authorization: Bearer {API_KEY}" \ -H "Content-Type: application/json" \ -d '{ - "plan": { "key": "pro" }, - "customerId": "01J9ZX2A8R0K8H6VG2C1A0K3WP" + "plan": { "key": "pro", "version": 1 }, + "customerKey": "user_external_id", + "timing": "immediate" }' ``` -`plan` references the target plan by its `key` (and optionally `version`). -Provide either `customerId` (the OpenMeter customer ULID, format -`^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$`) or `customerKey` (your own -identifier). Optional fields include `timing` (`"immediate"` by default, -`"next_billing_cycle"`, or an RFC 3339 timestamp), `startingPhase`, `name`, -`description`, `metadata`, `alignment`, and `billingAnchor`. +`plan` references the target plan by its `key` and (optionally) `version`. +Identify the customer with either `customerId` (the OpenMeter customer ULID) or +`customerKey` (your external customer identifier — for example, your Auth0 user +ID); `customerId` takes precedence when both are sent. Set `timing` to +`"immediate"` to start now, `"next_billing_cycle"` to defer activation, or an +RFC 3339 timestamp to schedule. Optional fields: `name`, `description`, +`metadata`, `billingAnchor`, `startingPhase`. ## Free trials @@ -198,12 +200,14 @@ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscri -H "Content-Type: application/json" \ -d '{ "timing": "immediate", - "plan": { "key": "enterprise" } + "plan": { "key": "enterprise", "version": 1 } }' ``` -`timing` accepts `"immediate"`, `"next_billing_cycle"`, or an RFC 3339 -timestamp. To preview the proration credit before committing, call +`timing` accepts `"immediate"`, `"next_billing_cycle"`, or an RFC 3339 datetime +for a scheduled change. The response includes both the closed-out (`current`) +and newly-started (`next`) subscriptions. To preview the proration credit before +committing, call `POST /v3/metering/{bucketId}/subscriptions/{subscriptionId}/change/estimate-credit` with the same body. @@ -270,9 +274,7 @@ Customers can cancel from the Developer Portal subscriptions page: curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscriptionId}/cancel \ -H "Authorization: Bearer {API_KEY}" \ -H "Content-Type: application/json" \ - -d '{ - "timing": "next_billing_cycle" - }' + -d '{ "timing": "next_billing_cycle" }' ``` `timing` controls when the cancellation takes effect: @@ -301,9 +303,10 @@ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscri -H "Authorization: Bearer {API_KEY}" ``` -This removes the pending cancellation. The subscription continues as normal. For -a subscription whose period has already ended, create a new subscription on the -same plan instead. +This removes the pending cancellation. The subscription continues as normal. + +If the subscription has already ended, create a new subscription rather than +restoring the old one. ## Multiple subscriptions