ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
    OverviewQuickstart
    Concepts
    Guides
      Stripe IntegrationDeveloper PortalMonetization PolicySubscription LifecyclePrivate PlansTax CollectionGoing to Production
    Reference
    TroubleshootingThird-Party IntegrationsCustom Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Guides

Monetization Policy Reference

The MonetizationInboundPolicy is the gateway enforcement mechanism. It runs on every request to a protected route, authenticates the API key, checks the customer's subscription and payment status, enforces quota, meters the request, and allows or blocks access.

Basic configuration

Add the policy to your policies.json:

Code
{ "name": "monetization-inbound", "policyType": "monetization-inbound", "handler": { "export": "MonetizationInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "meters": { "api_requests": 1 } } } }

Then reference it in your route's inbound policy pipeline:

Code
{ "x-zuplo-route": { "policies": { "inbound": ["monetization-inbound"] } } }

The MonetizationInboundPolicy handles API key authentication internally. It reads the API key from the Authorization header, validates it, and sets request.user. You do not need a separate API key authentication policy (api-key-inbound) on monetized routes — the monetization policy replaces it.

Configuration options

OptionTypeDefaultDescription
metersobject(none)Map of meter keys to increment values
meterOnStatusCodesstring or number[]"200-299"Status code range to meter
authHeaderstring"authorization"Header to read the API key from
authSchemestring"Bearer"Expected auth scheme prefix
cacheTtlSecondsnumber60How long to cache subscription data (minimum 60s)

meters

The meters option defines which meters to increment and by how much when a request is processed. Values must be non-negative numbers.

If meters is omitted, the policy still authenticates the API key and validates the subscription's payment status, but no usage is recorded. If meters is provided, it must contain at least one entry — an empty object throws a configuration error. To track usage at runtime instead of from static config, see Dynamic metering.

Code
// Increment the api_requests meter by 1 per request { "meters": { "api_requests": 1 } } // Increment multiple meters simultaneously { "meters": { "api_requests": 1, "api_credits": 5 } } // Increment by a fixed amount per request (expensive endpoint) { "meters": { "api_credits": 10 } }

meterOnStatusCodes

Controls which responses count toward metering. By default, only successful responses (2xx) are metered.

Code
// Only meter successful responses (default) { "meterOnStatusCodes": "200-299" } // Only meter 200 OK { "meterOnStatusCodes": "200" } // Meter success and redirects { "meterOnStatusCodes": "200-399" } // Comma-separated ranges { "meterOnStatusCodes": "200, 201, 300-304" } // Array of specific status codes { "meterOnStatusCodes": [200, 201, 202] }

The wildcard "*" is not a valid value for meterOnStatusCodes and throws a configuration error. Use a specific range like "200-599" if you want to meter most responses.

This is important for fairness: if your backend returns a 500 error, you probably don't want to charge the customer for that request.

authHeader

The header to read the API key from. Defaults to "authorization".

authScheme

The expected auth scheme prefix. Defaults to "Bearer". The policy expects the header value in the format {authScheme} {apiKey}.

cacheTtlSeconds

How long to cache subscription and entitlement data, in seconds. Defaults to 60 with a minimum of 60. Increasing this value reduces calls to the gateway service but means entitlement changes take longer to propagate.

Subscription and payment validation

The policy checks payment status on every request. Access is granted when:

  • The subscription is active and not expired
  • Payment status is paid or not_required (free plans)

When payment fails, a configurable grace period (default 3 days) allows continued access while retries are attempted. After the grace period, access is blocked until payment succeeds.

The grace period resolves in this order, with each level overriding the one 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. Default — 3 days

Set the value to 0 to block requests immediately when payment is overdue.

Multiple policies for different routes

Different routes can have different metering configurations. Define multiple policy instances in policies.json:

Code
[ { "name": "monetization-standard", "policyType": "monetization-inbound", "handler": { "export": "MonetizationInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "meters": { "api_requests": 1 } } } }, { "name": "monetization-ai", "policyType": "monetization-inbound", "handler": { "export": "MonetizationInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "meters": { "api_requests": 1, "tokens": 1 } } } }, { "name": "monetization-premium", "policyType": "monetization-inbound", "handler": { "export": "MonetizationInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "meters": { "api_credits": 10 } } } } ]

Apply each to the appropriate routes:

Code
// Simple lookup -> 1 request meter "/api/v1/search": { "inbound": ["monetization-standard"] } // AI endpoint -> 1 request + token metering "/api/v1/analyze": { "inbound": ["monetization-ai"] } // Premium endpoint -> 10 credits "/api/v1/bulk-export": { "inbound": ["monetization-premium"] }

Dynamic metering

For AI APIs and other variable-cost endpoints, you may need to meter based on the response — for example, counting the number of tokens returned by an LLM.

Use the setMeters and addMeters static methods to set meter values programmatically at runtime from a custom policy or handler:

Code
import { MonetizationInboundPolicy } from "@zuplo/runtime"; // In a custom outbound policy, set meters based on the response export default async function meterTokens(response, request, context) { if (response.ok) { const body = await response.json(); const tokens = body.usage?.total_tokens ?? 0; MonetizationInboundPolicy.setMeters(context, { tokens_used: tokens, }); } return response; }

You can also use addMeters to add to existing meter values rather than replacing them:

Code
MonetizationInboundPolicy.addMeters(context, { api_credits: creditsConsumed, });

You can also read the current runtime meter values at any point:

Code
const meters = MonetizationInboundPolicy.getMeters(context); // { tokens_used: 150 }

How meter values are merged

The final metering hook combines static and runtime values before usage is sent:

  • options.meters provides the static base values
  • setMeters replaces the current runtime meter map, overriding matching static keys
  • addMeters accumulates into the runtime meter map, then combines additively with static values
  • If both static and runtime maps are empty, metering is skipped

For a meter key like api with options.meters.api = 1:

  • setMeters(context, { api: 50 }) sends api: 50 (replaces static value)
  • addMeters(context, { api: 50 }) sends api: 51 (adds to static value)

Error responses

The policy returns 403 Forbidden for all error conditions. Responses follow the RFC 7807 Problem Details format:

Code
{ "type": "https://httpproblems.com/http-status/403", "title": "Forbidden", "status": 403, "detail": "API Key has exceeded the allowed limit for \"api_requests\" meter.", "instance": "/api/v1/resource", "trace": { "timestamp": "2026-01-15T10:00:00Z", "requestId": "req_abc123", "buildId": "build_xyz" } }

Common error details:

Conditiondetail 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."
Subscription has no entitlements"Subscription entitlements are not available."
Meter not in subscription"API Key does not have \"X\" meter provided by the subscription."
Quota exhausted"API Key has exceeded the allowed limit for \"X\" meter."
Meter access denied"API Key does not have access to \"X\" meter."

Pipeline ordering

The monetization policy should be the first policy in the inbound pipeline since it handles authentication:

Code
1. monetization-inbound → Authenticates, checks subscription, enforces quota, meters usage 2. rate-limiting → (Optional) Per-second/per-minute spike protection 3. caching → (Optional) Response caching 4. → Route handler → Your API logic

If you still want per-second or per-minute rate limiting on top of monthly quotas, add a standalone rate-limiting policy after the monetization policy. These serve different purposes: monetization enforces billing quotas, while rate limiting protects against traffic spikes.

Edit this page
Last modified on May 24, 2026
Developer PortalSubscription Lifecycle
On this page
  • Basic configuration
  • Configuration options
    • meters
    • meterOnStatusCodes
    • authHeader
    • authScheme
    • cacheTtlSeconds
  • Subscription and payment validation
  • Multiple policies for different routes
  • Dynamic metering
    • How meter values are merged
  • Error responses
  • Pipeline ordering
JSON
JSON
JSON
JSON
JSON
JSON
TypeScript
TypeScript
TypeScript
JSON