Zuplo
API Monetization

Meter Only Successful API Responses, Not Errors

Martyn DaviesMartyn Davies
April 15, 2026
4 min read

Your gateway is counting every 500, timeout, and retry against your customers' quotas. Here's how Zuplo does it with a single line of config, and why most gateways make you write custom policy code to get the same behaviour.

Your API has a bad afternoon. A downstream service flaps, 3% of requests return 500s for an hour, and your customers retry with exponential backoff. Every retry is a fresh inbound request that hits your gateway, so by end of day they’ve burned half their monthly quota on errors you caused. The next morning, your support inbox is full of “why am I rate-limited when none of my requests worked?”

This is a metering bug, and on most gateways it’s the default behaviour.

Use this approach if you're:
  • You bill API customers based on request count or usage tiers
  • Your gateway meters requests before the response status is known
  • You've had a customer ask why their quota burned during an outage they didn't cause

Why Your Gateway Meters Errors

Most metering happens on the request, not the response. The gateway sees the request, increments the counter, and forwards to your backend. 500? Timeout? Doesn’t matter, the counter already moved.

That’s fine for capacity planning. It’s wrong if you’re billing customers on it. Customers pay for successful calls. A 500 is not a successful call.

To bill correctly you need metering to fire after the response, and only when the response was useful to the caller.

How Zuplo Does It

Zuplo’s monetization-inbound policy has a meterOnStatusCodes option that controls which responses count. Add meterOnStatusCodes: "200-299" to your policy options (in config/policies.json or via the UI), and only successful responses get metered. No custom logic, no post-response hooks, no extra wiring. One line:

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

The meters object maps a meter key (api_requests) to the increment per qualifying response, and the meterOnStatusCodes range decides what counts as qualifying. With that one line in your policy, 500s don’t meter, 4xx errors don’t meter, and retries on a broken backend don’t eat the customer’s quota.

If you’ve ever widened the range, change it back.

Common mistake:

The most common way this gets widened is during debugging. Someone is troubleshooting a “why isn’t my meter firing?” issue, opens the range up to confirm the meter is being called at all, and then forgets to put it back. Worth a quick audit of your meterOnStatusCodes setting if you’ve ever debugged a meter in production.

You can also pass explicit codes if you want tighter control:

JSONjson
{
  "options": {
    "meters": { "api_requests": 1 },
    "meterOnStatusCodes": "200, 201, 202, 204"
  }
}

Edge Cases Worth Thinking About

429 rate limits. Don’t meter. The customer hit a limit, got no value, and metering a 429 on top of rate-limiting is double jeopardy. Setting meterOnStatusCodes: "200-299" handles this because 429 is in the 4xx range.

401 and 403. Don’t meter. The request was rejected before it reached your business logic. The same "200-299" setting covers this.

207 Multi-Status. Genuinely ambiguous, and only you know the answer for your API. If a batch call returned 207 with 6 successes and 4 errors, did the customer get value? Probably, but maybe not the value they paid for. Consider metering 207s with a meter that reflects actual successful sub-operations, not one flat tick per request.

Streaming responses that die mid-flight. Status 200 is sent before the stream starts. If it fails halfway, you’ve already metered. For streaming responses, meter on what you can measure at the end: tokens returned, bytes sent, records streamed. See the meters documentation for how to configure meters on extracted event values rather than a flat increment.

What Other Gateways Make You Do

On most gateways, you get there with custom code, whether that’s a post-request hook, a Lambda, or a policy that runs after the response, pulls the status, and conditionally fires an increment against your billing backend. You write it, test it, maintain it, and debug it the next time someone adds an endpoint and forgets to wire it up.

None of that is hard, but it’s all work nobody asked for. A gateway that bills your customers for your own 500s, and where the documented fix is “write a custom policy”, is a gateway problem, not a you problem.

Before and After

Before: customer retries through your 500, burns quota, opens a ticket.

After: the request fails, the meter doesn’t move, the customer never knows.

That second outcome is what Zuplo’s monetization-inbound policy gives you with one line of config: meterOnStatusCodes: "200-299". If you’re already on Zuplo, that’s the only line you need to add (or confirm is still in place, if someone widened it during a debugging session and forgot to roll it back). If you’re on something else, you’ve got a custom policy to write and probably a billing report to apologise for.

Monetization Policy Reference

Full reference for the monetization-inbound policy: every meterOnStatusCodes format, the complete options surface, and the underlying meter and feature model.