---
title: "Gate API Capabilities by Subscription Plan"
description: "Not every plan difference is a usage limit. Sometimes a feature is included on paid plans and not on Free, full stop. Static features in Zuplo plans handle capability gating directly, without writing custom auth checks or stuffing flags into consumer metadata."
canonicalUrl: "https://zuplo.com/blog/2026/04/28/capability-gating-static-features"
pageType: "blog"
date: "2026-04-28"
authors: "martyn"
tags: "API Monetization, API Gateway"
image: "https://zuplo.com/og?text=Gate%20API%20Capabilities%20by%20Subscription%20Plan"
---
Most posts about API monetization talk about counting things. Requests per
month, tokens per minute, gigabytes per day. That's metering, and it's how
usage-based pricing works.

But plan differences aren't always about volume. Sometimes paid plans include a
capability that Free doesn't. Sometimes batch endpoints are paid-only. Sometimes
Enterprise is the only plan that can write data. None of that is a meter. It's a
yes-or-no capability that one plan gets and another doesn't.

The teams that run into this hardest are the ones who started with a single paid
plan and quietly grew to three or four. Somewhere around plan three, route
handlers are speckled with
`if (plan.name === "Pro" || plan.name === "Enterprise")` checks, the pricing
page is hand-maintained alongside the auth logic, and nobody wants to touch the
thing that decides who gets what.

In Zuplo, that's a **static feature**, and it lives next to your meters in the
plan definition.

<CalloutAudience
  variant="useIf"
  items={[
    "You have a paid plan that includes capabilities the free plan doesn't",
    "You're hard-coding plan checks in route handlers or stuffing flags into consumer metadata",
    "You want plan capabilities defined once and reflected automatically in your developer portal",
  ]}
/>

## Meters Count, Features Toggle

A meter answers "how much?". A feature answers "can they?".

If you charge $0.001 per API call with a 10,000 call free tier, that's a meter.
If only paid plans can write data, that's a feature. Different shapes of plan
logic, and treating them both as meters (or both as auth rules) ends in tears.

Zuplo splits them deliberately. Every plan can hold any number of:

- **Metered features**, which link to a meter and track consumption.
- **Static features**, which are non-metered entitlements.

"Static" in this post is the conceptual category, which is what the docs and
this post mean by the term. The dashboard exposes two shapes inside that
category: **Boolean (on/off)** for plain yes-or-no capabilities, and **Static
(config value)** for entitlements that carry a custom value (a number, a string)
rather than a yes-or-no. Capability gating wants the Boolean shape, so that's
what we'll use throughout.

## Create a Static Feature

Zuplo has native MCP server support, and "MCP access on paid plans only" is
exactly the kind of decision a static feature is built for. We'll use that as
the running example.

In your project, head to **Services → Monetization Service → Features** and
click **Add Feature**. The dialog is short:

- **Name**: `MCP Server Access`. The label shown in the developer portal.
- **Key**: auto-derived from the name (here, `mcp_server_access`). You can
  override it before saving if you want a shorter or differently-shaped
  identifier. This is what you'll read at runtime.
- **Linked Meter**: leave on `— No meter`. Without a meter, this becomes a
  non-metered entitlement that any plan can switch on or off.

Save, and the feature is available for any plan to include.

![The Add Feature dialog in the Zuplo dashboard. Name is set to "MCP Server Access", the auto-derived Key reads "mcp_server_access", Linked Meter is set to "— No meter", and a helper line below reads "Metered features track usage against a meter. Boolean features are simply on/off per plan."](/media/posts/2026-04-28-capability-gating-static-features/add-feature-dialog.png)

<CalloutTip>
  Whether you keep the auto-derived key or override it, choose with care:
  feature keys are immutable once saved. You can archive a feature, but you
  can't rename it. Pick a key you'll still want to read in code two years from
  now, and avoid baking a plan name into it (`pro_mcp` ages badly the moment you
  add an Enterprise tier that also gets MCP).
</CalloutTip>

## Add the Feature to a Plan

Open the plan in **Plans**, click **Add feature**, pick the feature you just
defined, and the form will ask for two things:

- **Pricing Model**: `Free`. The feature is part of what the plan already
  includes, not an upsell with its own price.
- **Entitlement**: `Boolean (on/off)`. The dropdown also offers
  `Static (config value)` for entitlements that carry a custom value, but
  capability gating needs only the on/off form, so that's what we pick. Saving
  with this set flips `hasAccess` to `true` for anyone on this plan.

Save, then repeat for each plan that should include the feature. Skip it on
plans that shouldn't. That's the whole configuration.

![A plan in the Zuplo dashboard with two features attached. The "Requests" feature uses Flat fee pricing with a Metered (track usage) entitlement and a 5,000 usage limit. The "MCP Server Access" feature uses Free pricing with a Boolean (on/off) entitlement.](/media/posts/2026-04-28-capability-gating-static-features/plan-with-static-access.png)

<CalloutDoc
  title="Plan Features Reference"
  description="Full reference for static and metered features, including the entitlement template options and the API for managing features programmatically."
  href="https://zuplo.com/docs/articles/monetization/features"
  icon="book"
/>

## Check Entitlements at Runtime

This assumes you've already wired up the `monetization-inbound` policy, which
resolves the caller's API key into a subscription (see
[Part 2 of the monetized-API series](/blog/2026/04/01/building-a-monetized-api-part-2)
if you haven't). Your gating policy doesn't replace it, it reads from the data
already populated on `context`.

Static features show up in the same `entitlements` object that meters do.
`MonetizationInboundPolicy.getSubscriptionData(context)` returns the active
subscription, and `entitlements[<feature_key>].hasAccess` tells you whether the
plan includes it.

A custom inbound policy at `modules/check-mcp-access.ts` looks like this:

```typescript
import {
  MonetizationInboundPolicy,
  ZuploContext,
  ZuploRequest,
} from "@zuplo/runtime";

export default async function policy(
  request: ZuploRequest,
  context: ZuploContext,
  options: never,
  policyName: string,
) {
  const subscription = MonetizationInboundPolicy.getSubscriptionData(context);

  if (!subscription?.entitlements?.mcp_server_access?.hasAccess) {
    return new Response(
      JSON.stringify({
        error: "MCP access requires a Starter or Pro plan.",
      }),
      { status: 403, headers: { "Content-Type": "application/json" } },
    );
  }

  return request;
}
```

Add it as a
[custom-code-inbound](https://zuplo.com/docs/policies/custom-code-inbound)
policy on your `/mcp` route, either through the Route Designer or by adding the
policy to `config/policies.json` and pointing it at the module. It runs after
`monetization-inbound` on the same route, so unauthenticated requests have
already been rejected upstream and `subscription` is the active subscriber's
record. Free callers get a clean 403 pointing at the upgrade path, paid callers
go through, and there's no plan-name string comparison anywhere in your code.

![The Policies panel in the Zuplo dashboard for the MCP Server route. The Request chain shows monetization-inbound first, followed by check-mcp-access. The Response chain is empty.](/media/posts/2026-04-28-capability-gating-static-features/request-chain.png)

The end-to-end version of this lives in
[Part 3 of the monetized-API series](/blog/2026/04/02/building-a-monetized-api-part-3),
where the same primitive gates MCP access on a fully-built monetized API. The
piece worth dwelling on, and the reason this post exists, is the dashboard side:
a static feature is two dropdowns on a plan, and once it's there, the policy
code is the same shape every time. Swap `mcp_server_access` for any other
feature key and you have your next capability gate.

<CalloutDoc
  title="Custom Code Inbound Policy"
  description="Reference for the policy type used here: where modules live, the function signature, and how options flow through to the handler."
  href="https://zuplo.com/docs/policies/custom-code-inbound"
  icon="code"
/>

## When to Use a Static Feature

Three good signals:

1. **The capability is yes-or-no**, not counted. "Can call this endpoint" is
   static. "Can call it 1,000 times" is metered.
2. **You want it on the pricing page**. Static features show up there
   automatically; hidden capability flags belong in consumer metadata, not plan
   features.
3. **You'd otherwise hard-code a plan check**. Anywhere you're tempted to write
   `if (plan.name === "Pro")` is somewhere a static feature belongs.

Metering still matters, and most APIs need both. But every time you reach for a
plan check that has nothing to do with usage, you're describing a feature, and
features are a primitive Zuplo already gives you.