Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 136 additions & 82 deletions docs/articles/monetization/api-access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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

Expand Down
7 changes: 4 additions & 3 deletions docs/articles/monetization/meters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

:::

Expand Down
34 changes: 17 additions & 17 deletions docs/articles/monetization/monetization-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
7 changes: 3 additions & 4 deletions docs/articles/monetization/private-plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

:::

Expand Down
9 changes: 9 additions & 0 deletions docs/articles/monetization/stripe-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading