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

Programmatic Monetization

Pricing rules often depend on who's calling or what your API returns. A few examples:

  • Restrict bulk export to the Enterprise plan.
  • Offer advanced search only to plans that include it.
  • Bill an AI endpoint by the tokens it returns, and a search endpoint by the rows it matches — not a flat amount per call.

You can enforce rules and meter usage declaratively with features and entitlements on your plans, in code with custom policies, or both together. This guide covers the code path, for decisions that need runtime logic. It shows two techniques, then combines them:

  • Gate operations by plan — read the caller's subscription and allow or block the request based on their plan or entitlements.
  • Meter from the response — compute usage from the response body, report it, and enforce a quota on it.

Both build on the MonetizationInboundPolicy: the subscription data it exposes and its dynamic metering methods. Add that policy to your monetized routes first.

Gate operations by plan

To make an operation available on some plans but not others, read the subscription in a custom code inbound policy and block the request when the plan doesn't qualify. Place the policy after monetization-inbound so the subscription data exists on the context.

Code
// modules/plan-gate.ts import { MonetizationInboundPolicy, HttpProblems, ZuploContext, ZuploRequest, } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const subscription = MonetizationInboundPolicy.getSubscriptionData(context); if (!subscription) { return HttpProblems.forbidden(request, context, { detail: "No active subscription", }); } // Bulk export is an Enterprise-only operation if (subscription.plan.key !== "enterprise") { return HttpProblems.forbidden(request, context, { detail: "Bulk export requires the Enterprise plan", }); } return request; }

To gate on a specific feature instead of the whole plan, check its entitlement:

Code
const advancedSearch = subscription.entitlements["advanced_search"]; if (!advancedSearch?.hasAccess) { return HttpProblems.forbidden(request, context, { detail: "Your plan does not include advanced search", }); }

Register the policy and apply it after monetization-inbound on the routes you want to protect:

Code
// config/policies.json { "name": "plan-gate", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/plan-gate)" } }
Code
// On the route { "x-zuplo-route": { "policies": { "inbound": ["monetization-inbound", "plan-gate"] } } }

Meter from the response

For variable-cost endpoints, meter a request by something you only know once the backend responds — the number of records returned, items processed, or tokens generated. Compute the value in a custom code outbound policy and report it with MonetizationInboundPolicy.addMeters.

Code
// modules/count-records.ts import { MonetizationInboundPolicy, ZuploContext, ZuploRequest, } from "@zuplo/runtime"; export default async function ( response: Response, request: ZuploRequest, context: ZuploContext, ) { if (!response.ok) { return response; } // Reading the body consumes it, so rebuild the response afterward const data = (await response.json()) as { records: unknown[] }; const recordCount = data.records?.length ?? 0; MonetizationInboundPolicy.addMeters(context, { records: recordCount }); return new Response(JSON.stringify(data), { status: response.status, headers: response.headers, }); }

addMeters accumulates with any static meters and earlier calls; use setMeters to replace the runtime value instead. The policy sends the combined total once the response goes out, and only for the configured status codes. See Dynamic Metering for the full API and merge rules.

Block on a response-derived meter

addMeters runs in an outbound policy — it sees the response only after the handler returns. By then it's too late to block the current request, and because the meter never appeared in the policy's static config, the inbound quota check never ran for it. A caller who is already over their limit still gets through.

To enforce the quota, declare the meter in the policy's meters option with a value of 0:

Code
// config/policies.json { "name": "monetization-inbound", "policyType": "monetization-inbound", "handler": { "export": "MonetizationInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "meters": { "records": 0 } } } }

This does two things:

  1. Enforces the quota up front. At request time the policy checks the records entitlement and returns 403 Forbidden when the balance has run out — before the request reaches your backend.
  2. Avoids double-counting. The static 0 contributes nothing to the total. Runtime values add to the static base (0 + n = n), so addMeters reports the exact amount to bill.

The quota check runs at the start of each request and lets through any caller who still has balance; the policy charges usage after the response. So a request whose cost exceeds the remaining balance still completes, the balance goes negative, and the policy blocks the next request. An increment of 1 enforces the quota exactly — a caller overshoots only when a single request meters more than 1 against a partial balance.

Putting it together

A complete setup for a metered, plan-gated, response-counted route:

Code
// config/policies.json { "policies": [ { "name": "monetization-inbound", "policyType": "monetization-inbound", "handler": { "export": "MonetizationInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "meters": { "records": 0 } } } }, { "name": "plan-gate", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/plan-gate)" } }, { "name": "count-records", "policyType": "custom-code-outbound", "handler": { "export": "default", "module": "$import(./modules/count-records)" } } ] }
Code
// On the route { "x-zuplo-route": { "policies": { "inbound": ["monetization-inbound", "plan-gate"], "outbound": ["count-records"] } } }

The request flows through the monetization policy (authenticates, checks the records quota, blocks if the quota has run out), then the plan gate (allows the operation only on qualifying plans), then your handler, then the outbound policy (counts the records and reports them with addMeters).

Next steps

  • Reading Subscription Data — the full subscription object you can read in code.
  • Dynamic Metering — the full setMeters / addMeters / getMeters API and how static and runtime values merge.
  • Monetization Policy Reference — every configuration option.
  • Meters — defining the meters your policies increment.
Edit this page
Last modified on June 20, 2026
Dynamic MeteringSubscription Lifecycle
On this page
  • Gate operations by plan
  • Meter from the response
  • Block on a response-derived meter
  • Putting it together
  • Next steps
TypeScript
TypeScript
JSON
JSON
TypeScript
JSON
JSON
JSON