zuplo 6.69.6 → 6.69.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,6 +60,136 @@ curl \
60
60
  --header "Authorization: Bearer $ZAPI_KEY"
61
61
  ```
62
62
 
63
+ ## Bucket monetization configuration
64
+
65
+ Each bucket has an optional `MonetizationConfiguration` record that holds
66
+ bucket-wide defaults. The configuration is read by the runtime and the Developer
67
+ Portal — it is not stored in OpenMeter.
68
+
69
+ | Method | Path |
70
+ | -------- | ---------------------------------------------------- |
71
+ | `GET` | `/v3/metering/{bucketId}/monetization-configuration` |
72
+ | `PUT` | `/v3/metering/{bucketId}/monetization-configuration` |
73
+ | `DELETE` | `/v3/metering/{bucketId}/monetization-configuration` |
74
+
75
+ The `PUT` endpoint upserts the record. At least one of the four fields below
76
+ must be present. Pass any combination — fields that are omitted retain their
77
+ previous value.
78
+
79
+ | Field | Type | Description |
80
+ | ------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
+ | `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. |
82
+ | `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. |
83
+ | `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. |
84
+ | `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). |
85
+
86
+ ```bash
87
+ curl -X PUT "https://dev.zuplo.com/v3/metering/$BUCKET_ID/monetization-configuration" \
88
+ --header "Authorization: Bearer $ZAPI_KEY" \
89
+ --header "Content-Type: application/json" \
90
+ --data '{
91
+ "planOrder": ["free", "developer", "pro"],
92
+ "planSettings": {
93
+ "pro": { "visiblePhases": ["default"] }
94
+ },
95
+ "maxPaymentOverdueDays": 7
96
+ }'
97
+ ```
98
+
99
+ `DELETE` removes the record entirely; `GET` on a bucket with no record returns
100
+ the schema defaults (`multipleSubscriptionsEnabled: false`, `planOrder: []`,
101
+ `planSettings: {}`, `maxPaymentOverdueDays: 3`).
102
+
103
+ ## Stripe setup and billing readiness
104
+
105
+ These endpoints script the Stripe integration that the
106
+ [Monetization Service UI](./stripe-integration.md#connecting-your-stripe-account)
107
+ runs interactively. They live on the Zuplo developer API (not OpenMeter), so the
108
+ request shape is documented here.
109
+
110
+ ### Connect a Stripe app
111
+
112
+ ```http
113
+ POST /v3/metering/{bucketId}/setup/stripe
114
+ ```
115
+
116
+ Installs a Stripe app on the bucket and creates the default billing profile
117
+ linked to that app.
118
+
119
+ | Field | Type | Description |
120
+ | ------------- | --------- | ------------------------------------------------------------------------------------------------ |
121
+ | `apiKey` | `string` | Stripe secret or restricted key. Required. |
122
+ | `name` | `string` | Display name for the app. Required. |
123
+ | `taxEnabled` | `boolean` | Initial value for `workflow.tax.enabled` on the billing profile. Optional; defaults to `false`. |
124
+ | `taxEnforced` | `boolean` | Initial value for `workflow.tax.enforced` on the billing profile. Optional; defaults to `false`. |
125
+ | `country` | `string` | ISO 3166-1 alpha-2 supplier country for the billing profile. Optional; defaults to `"US"`. |
126
+
127
+ The request fails if the key prefix does not match the bucket environment:
128
+
129
+ - Working-copy or preview buckets accept `sk_test_*` or `rk_test_*`.
130
+ - Production buckets accept `sk_live_*` or `rk_live_*`.
131
+
132
+ ```bash
133
+ curl -X POST "https://dev.zuplo.com/v3/metering/$BUCKET_ID/setup/stripe" \
134
+ --header "Authorization: Bearer $ZAPI_KEY" \
135
+ --header "Content-Type: application/json" \
136
+ --data '{
137
+ "apiKey": "rk_test_...",
138
+ "name": "Zuplo Monetization (test)",
139
+ "taxEnabled": false,
140
+ "country": "US"
141
+ }'
142
+ ```
143
+
144
+ ### Read the connected Stripe app
145
+
146
+ ```http
147
+ GET /v3/metering/{bucketId}/setup/stripe
148
+ ```
149
+
150
+ Returns a summary of the connected Stripe app, the matched billing profile, and
151
+ connection-test status. Use this to confirm the integration is wired up before
152
+ continuing.
153
+
154
+ ### Add a billing profile to a Stripe app
155
+
156
+ ```http
157
+ POST /v3/metering/{bucketId}/setup/stripe/{stripeAppId}/billing-profile
158
+ ```
159
+
160
+ Creates an additional billing profile against an already-installed Stripe app.
161
+ This is rarely needed — the default profile is created during initial setup. Use
162
+ this endpoint to create per-supplier-country profiles.
163
+
164
+ ### Check billing readiness
165
+
166
+ ```http
167
+ GET /v3/metering/{bucketId}/billing-readiness
168
+ ```
169
+
170
+ Returns:
171
+
172
+ ```json
173
+ {
174
+ "hasStripeApp": true,
175
+ "stripeAppId": "app_01H...",
176
+ "hasDefaultBillingProfile": true,
177
+ "defaultBillingProfileId": "bp_01H..."
178
+ }
179
+ ```
180
+
181
+ Use this in setup wizards to gate the UI on whether Stripe is connected.
182
+
183
+ ### Update an app
184
+
185
+ ```http
186
+ PUT /v3/metering/{bucketId}/apps/{appId}
187
+ ```
188
+
189
+ Replaces an app's configuration (name, description, metadata, and — for Stripe
190
+ apps — `secretAPIKey`). The same key-prefix validation as `POST /setup/stripe`
191
+ applies: a Stripe key must match the bucket environment.
192
+
63
193
  ## API Reference
64
194
 
65
195
  For complete API operations, see the API Reference documentation:
@@ -44,7 +44,8 @@ feature structure carefully before creating them.
44
44
 
45
45
  ## Creating a Feature
46
46
 
47
- Create a metered feature linked to an API requests meter:
47
+ Create a metered feature linked to the `api_requests` meter. The feature's `key`
48
+ must match the meter's `slug`:
48
49
 
49
50
  ```shell
50
51
  curl \
@@ -54,8 +55,8 @@ curl \
54
55
  --header "Content-Type: application/json" \
55
56
  --data @- << EOF
56
57
  {
57
- "key": "api_calls",
58
- "name": "API Calls",
58
+ "key": "api_requests",
59
+ "name": "API Requests",
59
60
  "meterSlug": "api_requests"
60
61
  }
61
62
  EOF
@@ -66,17 +67,19 @@ The response includes the created feature:
66
67
  ```json
67
68
  {
68
69
  "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
69
- "key": "api_calls",
70
- "name": "API Calls",
70
+ "key": "api_requests",
71
+ "name": "API Requests",
71
72
  "meterSlug": "api_requests",
72
- "createdAt": "2024-01-01T01:01:01.001Z",
73
- "updatedAt": "2024-01-01T01:01:01.001Z"
73
+ "createdAt": "2026-01-01T01:01:01.001Z",
74
+ "updatedAt": "2026-01-01T01:01:01.001Z"
74
75
  }
75
76
  ```
76
77
 
77
78
  ### Creating a Static Feature
78
79
 
79
- For features without usage tracking, omit the `meterSlug`:
80
+ For features without usage tracking, omit the `meterSlug`. A plan's rate card
81
+ attaches a `boolean` entitlement that determines whether the customer has access
82
+ to the capability:
80
83
 
81
84
  ```shell
82
85
  curl \
@@ -92,6 +95,34 @@ curl \
92
95
  EOF
93
96
  ```
94
97
 
98
+ ### Creating a Billing-Only Feature
99
+
100
+ Some plans charge a flat subscription fee that isn't tied to any entitlement — a
101
+ monthly access fee, a setup fee, or a fixed retainer. Model these the same way
102
+ as a static feature (no `meterSlug`), then attach them to a plan via a
103
+ `flat_fee` rate card with no `entitlementTemplate` and a price whose
104
+ `paymentTerm` is `"in_advance"`. Zuplo issues a Stripe Invoice for the fee at
105
+ the start of each billing period.
106
+
107
+ ```shell
108
+ curl \
109
+ https://dev.zuplo.com/v3/metering/$BUCKET_ID/features \
110
+ --request POST \
111
+ --header "Authorization: Bearer $ZAPI_KEY" \
112
+ --header "Content-Type: application/json" \
113
+ --data @- << EOF
114
+ {
115
+ "key": "monthly_fee",
116
+ "name": "Monthly Fee"
117
+ }
118
+ EOF
119
+ ```
120
+
121
+ Because the rate card carries no entitlement, the developer portal's
122
+ feature-comparison matrix doesn't render a row for the feature — the price rolls
123
+ into the plan's headline cost. See the Pro plan example in [Plans](./plans.mdx)
124
+ for the rate-card shape.
125
+
95
126
  ## API Reference
96
127
 
97
128
  For complete API operations (list, get, archive), see the
@@ -28,8 +28,8 @@ and invoices come out wrong.
28
28
 
29
29
  Zuplo eliminates this by making the gateway the single source of truth. Every
30
30
  request that hits your API is metered, enforced, and billed through one system.
31
- When a customer's quota runs out, they get a `429` immediately — not five
32
- minutes later when a batch job catches up.
31
+ When a customer's quota runs out, they get a `403 Forbidden` immediately — not
32
+ five minutes later when a batch job catches up.
33
33
 
34
34
  ## Core concepts
35
35
 
@@ -73,10 +73,11 @@ with automatic conversion to paid, and multiple subscriptions per customer.
73
73
  - **Built-in metering** — Count requests, tokens, bytes, or any custom
74
74
  dimension. No external metering service required.
75
75
  - **Real-time quota enforcement** — Customers hitting their limit get a
76
- `429 Too Many Requests` response instantly, not after a batch sync.
77
- - **Stripe billing integration** — Subscriptions, invoicing, and payment
78
- collection handled by Stripe. Zuplo keeps access state synchronized
79
- automatically.
76
+ `403 Forbidden` response instantly, not after a batch sync.
77
+ - **Stripe billing integration** — Stripe handles checkout, payment, the billing
78
+ portal, and tax. Zuplo issues Stripe Invoices for fixed fees and metered usage
79
+ at the end of each billing period, and the gateway revokes access
80
+ automatically when a payment goes overdue past the configured grace period.
80
81
  - **Self-serve Developer Portal** — Pricing page, plan selection, checkout,
81
82
  subscription management, usage dashboards, and API key management built into
82
83
  the Zudoku-powered portal.
@@ -93,21 +94,21 @@ pricing, wire up Stripe, and configure your gateway to enforce quotas.
93
94
 
94
95
  ## Documentation map
95
96
 
96
- | Document | What it covers |
97
- | ---------------------------------------------------------------------- | -------------------------------------------------------- |
98
- | [Quickstart](./monetization/quickstart.md) | End-to-end setup in 30 minutes |
99
- | [Meters](./monetization/meters.mdx) | How meters track usage dimensions |
100
- | [Features](./monetization/features.mdx) | Connecting meters to your product catalog |
101
- | [Plans](./monetization/plans.mdx) | Plan structure, phases, lifecycle, and publishing |
102
- | [Rate Cards](./monetization/rate-cards.mdx) | Pricing and entitlements within plans |
103
- | [Pricing Models](./monetization/pricing-models.mdx) | Flat, per-unit, tiered, volume, and package pricing |
104
- | [Billing Models Guide](./monetization/billing-models.md) | Choosing the right pricing strategy for your business |
105
- | [Stripe Integration](./monetization/stripe-integration.md) | Connecting Stripe and managing payments |
106
- | [Developer Portal Setup](./monetization/developer-portal.md) | Configuring the self-serve portal for your customers |
107
- | [Monetization Policy Reference](./monetization/monetization-policy.md) | Configuring the `MonetizationInboundPolicy` per-route |
108
- | [API Access](./monetization/api-access.mdx) | Authentication, buckets, and API reference links |
109
- | [Subscription Lifecycle](./monetization/subscription-lifecycle.md) | Managing trials, upgrades, downgrades, and cancellations |
110
- | [Private Plans](./monetization/private-plans.md) | Invite-only plans for specific users |
111
- | [Tax Collection](./monetization/tax-collection.md) | Enabling VAT, sales tax, or GST via Stripe Tax |
112
- | [Plan Examples](./monetization/plan-examples.mdx) | Real-world plan configurations |
113
- | [Troubleshooting](./monetization/troubleshooting.md) | Common issues, debugging, and FAQ |
97
+ | Document | What it covers |
98
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
99
+ | [Quickstart](./monetization/quickstart.md) | End-to-end setup in 30 minutes |
100
+ | [Meters](./monetization/meters.mdx) | How meters track usage dimensions |
101
+ | [Features](./monetization/features.mdx) | Connecting meters to your product catalog |
102
+ | [Plans](./monetization/plans.mdx) | Plan structure, phases, lifecycle, and publishing |
103
+ | [Rate Cards](./monetization/rate-cards.mdx) | Pricing and entitlements within plans |
104
+ | [Pricing Models](./monetization/pricing-models.mdx) | Flat, per-unit, tiered, volume, and package pricing |
105
+ | [Billing Models Guide](./monetization/billing-models.md) | Choosing the right pricing strategy for your business |
106
+ | [Stripe Integration](./monetization/stripe-integration.md) | Connecting Stripe and managing payments |
107
+ | [Developer Portal Setup](./monetization/developer-portal.md) | Configuring the self-serve portal for your customers |
108
+ | [Monetization Policy Reference](./monetization/monetization-policy.md) | Configuring the `MonetizationInboundPolicy` per-route |
109
+ | [API Access](./monetization/api-access.mdx) | Authentication, buckets, bucket configuration, Stripe setup APIs, and reference links |
110
+ | [Subscription Lifecycle](./monetization/subscription-lifecycle.md) | Managing trials, upgrades, downgrades, and cancellations |
111
+ | [Private Plans](./monetization/private-plans.md) | Invite-only plans for specific users |
112
+ | [Tax Collection](./monetization/tax-collection.md) | Enabling VAT, sales tax, or GST via Stripe Tax |
113
+ | [Plan Examples](./monetization/plan-examples.mdx) | Real-world plan configurations |
114
+ | [Troubleshooting](./monetization/troubleshooting.md) | Common issues, debugging, and FAQ |
@@ -25,7 +25,7 @@ A meter is configured with:
25
25
  - **Aggregation** - How to combine values (typically `SUM`)
26
26
 
27
27
  When events are ingested, the meter matches events by type, extracts the
28
- specified value, and aggregates it per customer over time.
28
+ specified value, and aggregates it per customer subscription over time.
29
29
 
30
30
  ## Common Examples
31
31
 
@@ -52,7 +52,7 @@ Each event contains the `subscription` ID linking it to a subscription and a
52
52
  "specversion": "1.0",
53
53
  "type": "api_requests",
54
54
  "source": "monetization-policy",
55
- "subject": "customer-id",
55
+ "subject": "01KNVXHQG356VA7T7W0V9N21GH",
56
56
  "subscription": "01KNVXHQG356VA7T7W0V9N21GH",
57
57
  "data": {
58
58
  "total": 1
@@ -60,6 +60,14 @@ Each event contains the `subscription` ID linking it to a subscription and a
60
60
  }
61
61
  ```
62
62
 
63
+ :::note
64
+
65
+ Events emitted by the `MonetizationInboundPolicy` always set `subject` and
66
+ `subscription` to the same subscription ULID. See
67
+ [Monetization Policy](./monetization-policy.md) for how usage is recorded.
68
+
69
+ :::
70
+
63
71
  ### Token Usage
64
72
 
65
73
  Track token consumption for AI applications:
@@ -74,8 +82,13 @@ Track token consumption for AI applications:
74
82
  }
75
83
  ```
76
84
 
77
- Each event reports a fixed quantity per request. For example, if a meter is
78
- configured to report 50 tokens per request:
85
+ The meter aggregates events from the gateway; the per-request quantity comes
86
+ from the `MonetizationInboundPolicy`. Set a fixed cost per request in the
87
+ policy's `meters` option, or call
88
+ [`MonetizationInboundPolicy.setMeters`](./monetization-policy.md#dynamic-metering)
89
+ from a custom outbound policy to report a value derived from the response — for
90
+ example, the actual token count an LLM returned. An event for a request that
91
+ consumed 50 tokens looks like this:
79
92
 
80
93
  ```json
81
94
  {
@@ -83,7 +96,7 @@ configured to report 50 tokens per request:
83
96
  "specversion": "1.0",
84
97
  "type": "tokens",
85
98
  "source": "monetization-policy",
86
- "subject": "customer-id",
99
+ "subject": "01KNVXHQG356VA7T7W0V9N21GH",
87
100
  "subscription": "01KNVXHQG356VA7T7W0V9N21GH",
88
101
  "data": {
89
102
  "total": 50
@@ -113,7 +126,7 @@ Each event reports the number of bytes transferred:
113
126
  "specversion": "1.0",
114
127
  "type": "data_transfer",
115
128
  "source": "monetization-policy",
116
- "subject": "customer-id",
129
+ "subject": "01KNVXHQG356VA7T7W0V9N21GH",
117
130
  "subscription": "01KNVXHQG356VA7T7W0V9N21GH",
118
131
  "data": {
119
132
  "total": 4096
@@ -53,8 +53,8 @@ Then reference it in your route's inbound policy pipeline:
53
53
 
54
54
  The `MonetizationInboundPolicy` handles API key authentication internally. It
55
55
  reads the API key from the `Authorization` header, validates it, and sets
56
- `request.user`. You do not need a separate `api-key-auth` policy on monetized
57
- routes — the monetization policy replaces it.
56
+ `request.user`. You do not need a separate API key authentication policy
57
+ (`api-key-inbound`) on monetized routes — the monetization policy replaces it.
58
58
 
59
59
  :::
60
60
 
@@ -62,7 +62,7 @@ routes — the monetization policy replaces it.
62
62
 
63
63
  | Option | Type | Default | Description |
64
64
  | -------------------- | ------------------ | ----------------- | ------------------------------------------------- |
65
- | `meters` | object | (required) | Map of meter keys to increment values |
65
+ | `meters` | object | _(none)_ | Map of meter keys to increment values |
66
66
  | `meterOnStatusCodes` | string or number[] | `"200-299"` | Status code range to meter |
67
67
  | `authHeader` | string | `"authorization"` | Header to read the API key from |
68
68
  | `authScheme` | string | `"Bearer"` | Expected auth scheme prefix |
@@ -71,7 +71,13 @@ routes — the monetization policy replaces it.
71
71
  ### `meters`
72
72
 
73
73
  The `meters` option defines which meters to increment and by how much when a
74
- request is processed. Values must be positive numbers.
74
+ request is processed. Values must be non-negative numbers.
75
+
76
+ If `meters` is omitted, the policy still authenticates the API key and validates
77
+ the subscription's payment status, but no usage is recorded. If `meters` is
78
+ provided, it must contain at least one entry — an empty object throws a
79
+ configuration error. To track usage at runtime instead of from static config,
80
+ see [Dynamic metering](#dynamic-metering).
75
81
 
76
82
  ```json
77
83
  // Increment the api_requests meter by 1 per request
@@ -143,16 +149,22 @@ When payment fails, a configurable grace period (default 3 days) allows
143
149
  continued access while retries are attempted. After the grace period, access is
144
150
  blocked until payment succeeds.
145
151
 
146
- The grace period is configurable via metadata on the plan or customer:
152
+ The grace period resolves in this order, with each level overriding the one
153
+ below it:
154
+
155
+ 1. **Customer metadata** — `zuplo_max_payment_overdue_days` on the customer
156
+ 2. **Plan metadata** — `zuplo_max_payment_overdue_days` on the plan
157
+ 3. **Bucket configuration** — `maxPaymentOverdueDays` on the bucket's
158
+ monetization configuration (PUT
159
+ `/v3/metering/{bucketId}/monetization-configuration`)
160
+ 4. **Default** — `3` days
147
161
 
148
- - Plan metadata key: `zuplo_max_payment_overdue_days`
149
- - Customer metadata key: `zuplo_max_payment_overdue_days`
150
- - Default: `3` (days)
162
+ Set the value to `0` to block requests immediately when payment is overdue.
151
163
 
152
164
  ## Multiple policies for different routes
153
165
 
154
166
  Different routes can have different metering configurations. Define multiple
155
- policy instances:
167
+ policy instances in `policies.json`:
156
168
 
157
169
  ```json
158
170
  [
@@ -284,17 +296,20 @@ the RFC 7807 Problem Details format:
284
296
 
285
297
  Common error details:
286
298
 
287
- | Condition | `detail` message |
288
- | ------------------------- | ------------------------------------------------------------------- |
289
- | No auth header | `"No Authorization Header"` |
290
- | Wrong auth scheme | `"Invalid Authorization Scheme"` |
291
- | Invalid API key | `"API Key is invalid or does not have access to the API"` |
292
- | Expired API key | `"API Key has expired."` |
293
- | Expired subscription | `"API Key has an expired subscription."` |
294
- | Payment not made | `"Payment has not been made."` |
295
- | Payment overdue | `"Payment is overdue. Please update your payment method."` |
296
- | Quota exhausted | `"API Key has exceeded the allowed limit for \"X\" meter."` |
297
- | Meter not in subscription | `"API Key does not have \"X\" meter provided by the subscription."` |
299
+ | Condition | `detail` message |
300
+ | ------------------------------- | ------------------------------------------------------------------- |
301
+ | No auth header | `"No Authorization Header"` |
302
+ | Wrong auth scheme | `"Invalid Authorization Scheme"` |
303
+ | Empty key after the auth scheme | `"No key present"` |
304
+ | Cached invalid key or 401 | `"Authorization Failed"` |
305
+ | Invalid API key | `"API Key is invalid or does not have access to the API"` |
306
+ | Expired API key | `"API Key has expired."` |
307
+ | Expired subscription | `"API Key has an expired subscription."` |
308
+ | Subscription has no payment | `"Subscription payment status is not available."` |
309
+ | Payment not made | `"Payment has not been made."` |
310
+ | Payment overdue | `"Payment is overdue. Please update your payment method."` |
311
+ | Quota exhausted | `"API Key has exceeded the allowed limit for \"X\" meter."` |
312
+ | Meter not in subscription | `"API Key does not have \"X\" meter provided by the subscription."` |
298
313
 
299
314
  ## Pipeline ordering
300
315
 
@@ -73,9 +73,11 @@ Plans move through a defined lifecycle:
73
73
  This example creates a Pro plan with:
74
74
 
75
75
  - A 1-week free trial phase with 1,000 API calls
76
- - A default phase with $99/month subscription and 10,000 included API calls
77
- - Overage pricing at $0.01 per additional call
78
- - Static features for premium access
76
+ - A `monthly_fee` flat-fee rate card on the default phase that charges $99 in
77
+ advance at the start of each billing period
78
+ - A `usage_based` rate card with 10,000 included API requests per billing period
79
+ and $0.01 per request overage in arrears
80
+ - A `priority_support` static feature granted on both phases
79
81
 
80
82
  For step-by-step examples building plans from simple to complex, see
81
83
  [Plan Examples](./plan-examples.mdx).
@@ -132,6 +134,18 @@ curl \
132
134
  "name": "Default",
133
135
  "duration": null,
134
136
  "rateCards": [
137
+ {
138
+ "type": "flat_fee",
139
+ "key": "monthly_fee",
140
+ "name": "Monthly Fee",
141
+ "featureKey": "monthly_fee",
142
+ "billingCadence": "P1M",
143
+ "price": {
144
+ "type": "flat",
145
+ "amount": "99.00",
146
+ "paymentTerm": "in_advance"
147
+ }
148
+ },
135
149
  {
136
150
  "type": "usage_based",
137
151
  "key": "api_requests",
@@ -150,8 +164,8 @@ curl \
150
164
  "tiers": [
151
165
  {
152
166
  "upToAmount": "10000",
153
- "flatPrice": { "type": "flat", "amount": "99.00" },
154
- "unitPrice": null
167
+ "flatPrice": null,
168
+ "unitPrice": { "type": "unit", "amount": "0.00" }
155
169
  },
156
170
  {
157
171
  "flatPrice": null,
@@ -186,9 +200,12 @@ EOF
186
200
  | Trial | 1 week | 1,000 (hard limit) | Free |
187
201
  | Default | Ongoing | 10,000 included | $99/month |
188
202
 
189
- After the trial ends, customers automatically move to the default phase. In the
190
- default phase, the first 10,000 API requests are included in the $99 monthly
191
- fee. Additional requests are billed at $0.01 each (soft limit allows overage).
203
+ After the trial ends, customers automatically move to the default phase. The $99
204
+ monthly subscription fee is charged in advance at the start of each billing
205
+ period via the `monthly_fee` flat-fee rate card. The plan includes 10,000 API
206
+ requests per period; additional requests are billed at $0.01 each in arrears
207
+ (the soft limit on the metered entitlement allows overage to flow through to the
208
+ next invoice).
192
209
 
193
210
  ## Plan Properties
194
211
 
@@ -107,6 +107,15 @@ curl -X POST "https://dev.zuplo.com/v3/metering/${ZUPLO_BUCKET_ID}/plans" \
107
107
 
108
108
  Save the returned `id` — you need it to publish and invite users.
109
109
 
110
+ :::note
111
+
112
+ The plan `id` is a 26-character ULID (regex
113
+ `^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$`), separate from the human-friendly
114
+ `key` you set on creation. The publish, invite, and other plan-scoped endpoints
115
+ require the `id`, not the `key`.
116
+
117
+ :::
118
+
110
119
  The key difference from a public plan is `metadata.zuplo_private_plan` set to
111
120
  `"true"`. Everything else (rate cards, entitlements, pricing) works the same as
112
121
  public plans.
@@ -284,22 +284,27 @@ directory.
284
284
  3. In the **Meters** configuration field, set the key to `api_requests` with a
285
285
  value of `1` to match the meter you created in _Step 4_. This field maps the
286
286
  meter slug to the number of units each request consumes.
287
+ 4. Click on **Create Policy**.
288
+
289
+ The Portal saves the policy to `policies.json` as a complete entry:
287
290
 
288
291
  ```json
289
292
  {
290
- "export": "MonetizationInboundPolicy",
291
- "module": "$import(@zuplo/runtime)",
292
- "options": {
293
- "cacheTtlSeconds": 60,
294
- "meters": {
295
- "api_requests": 1
293
+ "name": "monetization-inbound",
294
+ "policyType": "monetization-inbound",
295
+ "handler": {
296
+ "export": "MonetizationInboundPolicy",
297
+ "module": "$import(@zuplo/runtime)",
298
+ "options": {
299
+ "cacheTtlSeconds": 60,
300
+ "meters": {
301
+ "api_requests": 1
302
+ }
296
303
  }
297
304
  }
298
305
  }
299
306
  ```
300
307
 
301
- 4. Click on **Create Policy**.
302
-
303
308
  ### Apply the policy to routes
304
309
 
305
310
  Next, you need to apply the Monetization policy to some or all of your routes.
@@ -317,7 +322,8 @@ Next, you need to apply the Monetization policy to some or all of your routes.
317
322
  :::note
318
323
 
319
324
  The `MonetizationInboundPolicy` handles API key authentication internally. You
320
- do not need a separate `api-key-auth` policy on monetized routes.
325
+ do not need a separate API key authentication policy (`api-key-inbound`) on
326
+ monetized routes.
321
327
 
322
328
  :::
323
329