Zuplo
API Monetization

Monetize an n8n AI Workflow with Zuplo

Martyn DaviesMartyn Davies
April 30, 2026
11 min read

You've built a workflow with n8n and now you want to charge for it. Here's how to turn it into a billable API with keys, tiered rate limits, and Stripe-backed metering, without writing a billing pipeline from scratch.

You’ve built something useful in n8n: it could be a document summariser, a lead qualifier, a content classifier, an invoice extractor, whatever. It works great.

Now a few people have asked if they can pay to use it, and you’re staring at the gap between “a working n8n workflow” and “an API with customers billed every month.”

The DIY version is possible but the process is pretty miserable. A keys table. A Stripe webhook handler. Quota logic hand-rolled in a Function node. Rate limiting you have little control over. A login form so customers can manage their own keys, glued to the rest with whatever you have time for.

In the end, your n8n workflow is 9 nodes but the billing pipeline around it is bigger than the workflow itself.

Zuplo sits in front of n8n and does this layer for you. You keep the workflow and get keys, quotas, metering, and Stripe implemented without writing any of the infrastructure.

Use this approach if you're:
  • You have a working n8n AI workflow you want to sell access to
  • You don't want to build a billing pipeline from scratch
  • You need tiered plans (free / pro / enterprise) with real quota enforcement

Our example n8n workflow

The post uses a document summariser as a running example, but the pattern is the same for any workflow you want to charge for: a webhook in, some processing in the middle, structured JSON out. Swap nodes to match what your workflow actually does.

End-to-end the workflow runs like this. The Webhook node receives a document URL. Anthropic’s Analyze Document node fetches and summarises the file in one step. A Supabase node stores the result. An Edit Fields node shapes the response. A final Respond to Webhook node returns JSON with the summary and the row id. Five nodes, no glue code.

An n8n workflow canvas: a Webhook node (POST) feeds an Anthropic Analyze Document node, then a Supabase Create a Row node, then an Edit Fields node that shapes the response, ending in a Respond to Webhook node that returns JSON.

Pro tip:

This synchronous request/response shape works fine for workflows that finish in seconds. For workflows that take longer than ~30 seconds, you’ll start hitting timeouts somewhere between the caller and n8n. The fix is async polling: return a job_id immediately and have the caller poll for completion. The Zuplo and n8n wiring stays identical; only the workflow shape and the response convention change.

Ultimately, the workflow is what you’re “selling”. n8n makes it easy to build, run, and iterate on, but it doesn’t turn the workflow into a product: no managed API keys, no quota enforcement, no Stripe integration, no customer portal. The Webhook node accepts anyone who sends an HTTP request. That’s where Zuplo comes in.

What Zuplo adds

Five things, all configured once in the Zuplo portal:

  1. Monetization: validates each caller’s API key, identifies them, then meters successful requests against a Stripe subscription plan that enforces quota and bills usage. Key validation comes built in, so you don’t add a separate API Key policy.
  2. Per-consumer rate limiting: each customer gets their own limit window, automatically tracked by their key.
  3. Secret masking (outbound): redacts credentials if your workflow ever echoes something sensitive in a response.
  4. Prompt injection detection (outbound): scans responses for injected instructions before they reach whoever called your API (especially relevant if the caller is another AI agent).
  5. Schema validation (optional): drop an OpenAPI validator if you want malformed payloads rejected before they reach n8n. The walkthrough below skips this, but it’s a one-policy add if you have an OpenAPI spec for your route.

You configure all of these by clicking policies onto a route in the Zuplo portal, a drag-and-drop policy chain editor. The portal saves every change and deploys it for you. Power users can edit the underlying JSON directly, and optionally wire the project up to a Git repo for version control. Pretty much everything you can do in code in Zuplo, you can also do through the UI, so pick whichever path fits how you like to build.

Here’s how a request flows once those policies are in place:

Architecture: an API caller sends requests to Zuplo, which applies the monetization and rate limit policies before forwarding to the n8n workflow, then runs secret masking and prompt injection checks on the response.

Inbound policies (key validation, rate limiting, metering) run before the request reaches n8n. Outbound policies (secret masking, prompt injection checks) run on the response on the way back to the caller. n8n stays focused on the actual work: fetch, summarise, store. Everything that turns it into a paid API lives at the gateway.

What you’ll click

Five steps, all in the Zuplo portal:

  1. Sign up at Zuplo and create a project. Point it at your n8n webhook URL as the upstream.
  2. Add the inbound policies. From the policy library, drop Rate Limit and Monetization onto your route. Monetization handles API key validation as part of its job, so you don’t need a separate auth policy.
  3. Add the outbound policies. Drop Secret Masking and Prompt Injection Detection on the outbound side of the same route.
  4. Connect Stripe. In Settings → Payment Providers, paste your Stripe secret key.
  5. Create your plans. In Monetization, define Free / Pro / Enterprise tiers with quotas and prices. Zuplo creates the matching Stripe products for you.

That’s the whole shape of it. The rest of the post is what those clicks produce, in case you ever want to read or edit the underlying config.

The Zuplo portal route editor showing the full policy chain. The Request column lists monetization-inbound, rate-limit-inbound, and custom-code-inbound in order. The Response column shows prompt-injection-outbound and secret-masking-outbound. The Request Handler below is set to URL Rewrite with the rewrite URL $env.N8N_WEBHOOK_URL.

Monetization Quickstart

The complete Zuplo walkthrough for setting up monetization, Stripe, and plans.

How each policy works

A quick read of the four policies you just added:

  • Rate limit: caps each customer at 60 requests per minute. Tracked per key, not per IP.
  • Monetization: validates the API key on the way in (no separate auth policy needed), then increments a meter called api_requests by 1 for every successful request. Errors and retries don’t burn the customer’s quota.
  • Secret masking: scrubs any credentials that accidentally end up in a response.
  • Prompt injection detection: scans the response for injected instructions. Default flags and logs. Set strict: true to block flagged responses outright.

Two things to set in the portal’s environment variables before any of this runs: N8N_WEBHOOK_URL (the full URL of your n8n webhook) and OPENAI_API_KEY (used by the prompt injection check).

One choice on the route is worth flagging: in the Request Handler, pick URL Rewrite, not URL Forward. Forward appends the inbound path onto a base URL, which is wrong for n8n: webhook URLs are complete endpoints, so the request would land at /webhook/abc/v1/summarizer and 404. URL Rewrite uses a fixed pattern, so the request goes to exactly the n8n endpoint you’ve set in N8N_WEBHOOK_URL.

Common mistake:

Leaving the n8n webhook URL publicly reachable after you put Zuplo in front of it. A caller who finds the raw webhook can bypass the gateway entirely, which means no key, no quota, no billing. The fix is a shared secret header that Zuplo attaches on every forward and n8n requires on every webhook hit. The “Telling n8n who the caller is” section below shows the pattern. Or put n8n on a private network that only Zuplo can reach.

Telling n8n who the caller is

The policies above will get you authenticated, rate-limited, metered API calls. But they leave one thing on the table: the n8n workflow doesn’t know who the caller is. That’s fine for the processing step itself, but the moment you want to store results per customer (so each customer only sees their own data), n8n needs the consumer id.

Once the monetization policy validates a key, it attaches the subscription data to the request context, including the customer id. You read it with MonetizationInboundPolicy.getSubscriptionData(context) and forward it to n8n on every request, alongside a shared secret that prevents anyone from calling the n8n webhook directly.

Three steps in the Zuplo portal:

  1. Write a small module that reads subscription data and sets headers. This is the one piece of code you write yourself. Create modules/set-consumer-headers.ts in the portal’s file editor.

    The Zuplo portal Code view, with the file tree on the left showing config, modules, schemas, docs, and tests folders. The modules folder is expanded and set-consumer-headers.ts is open in the editor on the right, showing the imports from @zuplo/runtime and the default export that reads the subscription, sets x-gateway-secret and x-consumer-id, and returns the request.

    The full module:

    TypeScripttypescript
    import {
      MonetizationInboundPolicy,
      ZuploContext,
      ZuploRequest,
    } from "@zuplo/runtime";
    
    export default async function (
      request: ZuploRequest,
      context: ZuploContext,
    ) {
      const subscription =
        MonetizationInboundPolicy.getSubscriptionData(context);
      request.headers.set("x-gateway-secret", context.env.GATEWAY_SECRET);
      request.headers.set("x-consumer-id", subscription.customerId);
      return request;
    }
  2. Drop a Custom Code Inbound policy onto the route and point it at the module. From the policy library, find Custom Code Inbound, drag it onto the route’s inbound chain, and set the module path to your new file.

    The Zuplo portal's policy configuration dialog for the Custom Code Inbound policy named custom-code-inbound. The Policy panel shows the JSON config with the module path "$import(./modules/set-consumer-headers)" highlighted on line 3.

    The portal commits this to config/policies.json:

    JSONjson
    {
      "name": "custom-code-inbound",
      "policyType": "custom-code-inbound",
      "handler": {
        "export": "default",
        "module": "$import(./modules/set-consumer-headers)",
        "options": {}
      }
    }
  3. Slot it at the bottom of the inbound chain. The custom policy reads subscription data from the context, which only exists once monetization has run, so as long as it sits below monetization the order works. Putting it last keeps the chain easy to scan.

    The Zuplo portal policy chain with custom-code-inbound highlighted as the third inbound policy, sitting below monetization-inbound and rate-limit-inbound. The Response column shows prompt-injection-outbound and secret-masking-outbound. The Request Handler is URL Rewrite pointing at $env.N8N_WEBHOOK_URL.

    The chain ends up as:

    JSONjson
    "inbound": [
      "monetization-inbound",
      "rate-limit-inbound",
      "custom-code-inbound"
    ]

One more environment variable to add in the portal: GATEWAY_SECRET. Generate any random string and set it in Settings → Environment Variables. n8n will require the same value on every webhook hit, so the two sides match.

Then in n8n, require both headers in the Webhook node and use them in the workflow:

  • x-gateway-secret is checked by the Webhook node itself. Set Authentication to Header Auth and create a Header Auth credential with name x-gateway-secret and value matching GATEWAY_SECRET in the Zuplo portal. n8n will reject any call with a missing or wrong value before the workflow runs, which closes the “raw webhook URL is reachable” loophole the earlier callout warned about.
  • x-consumer-id is the value your workflow uses to scope data per customer. Pass it as the customer_id (or user_id, or whatever you call it) on every write, and as the filter on every read. The example uses Supabase, but the same applies to Postgres, Airtable, Notion, or any storage node n8n connects to.

The n8n Webhook node configuration. Parameters set HTTP Method to POST, Authentication to Header Auth, and Respond to "Using 'Respond to Webhook' Node". The Output panel on the right shows the request headers including x-consumer-id (the customer id from monetization) and x-gateway-secret (the shared secret), with an arrow pointing to x-gateway-secret.

The response back to Zuplo is built in two steps. First, an Edit Fields (Set) node assembles the fields the caller will receive: id, summary, source_url, customer_id, and created_at. Then a Respond to Webhook node at the end of the workflow sends them out.

There’s a subtle gotcha here worth knowing about. The Respond to Webhook node has a JSON body field where you can paste a template with expression placeholders, but n8n treats it as a string template. If your summary contains line breaks or quotes (and most LLM output does), the result is invalid JSON and the customer gets a broken response.

The Edit Fields node sidesteps this entirely. It builds a real object, field by field, and n8n serialises it correctly when it leaves the workflow.

To wire it up, set the entry Webhook node’s Respond option to Using ‘Respond to Webhook’ Node, then set the Respond to Webhook node’s Respond With option to First Incoming Item. Edit Fields hands its object straight through, and the caller gets clean JSON every time.

The n8n Edit Fields (Set) node configuration. The Input panel on the left shows the incoming fields id, customer_id, source_url, summary, and created_at from the Create a row step. The Parameters panel in the middle uses Manual Mapping to assemble each field by referencing the matching json property. The Output panel on the right shows the final assembled object that will be handed to the Respond to Webhook node.

Where Stripe fits in

The monetization policy you added defines the meter. The bit that turns a meter into money is the plan configuration, which you set up in the Zuplo portal’s Monetization section. Connect a Stripe secret key once in the portal’s Stripe integration settings, then create plans in Zuplo that map onto Stripe products and prices for you. Example plan set:

  • Free: 100 api_requests per day, $0/month flat, hard quota.
  • Pro: 10,000 api_requests per day, $49/month flat, soft quota with $0.005 per extra request billed as Stripe metered usage.
  • Enterprise: custom quota, negotiated price, optionally a private plan that isn’t visible on the public pricing page.

Zuplo supports both flat subscriptions with hard quotas and tiered usage-based billing where overage requests bill through Stripe’s metered pricing. You pick the shape per plan.

Customers subscribe through Zuplo’s managed developer portal, which is a built-in product. Wire in an auth provider like Auth0 or Clerk for signup, and the portal handles the pricing page, plan selection, Stripe Checkout, and subscription management out of the box.

Once a customer subscribes, Zuplo creates an API key for them and links the subscription to that key. From here on, every request they make carries that key.

On each request, the monetization policy looks up their plan and current usage, then either allows the call or rejects it with a quota error. Successful calls increment the meter, and Stripe reads the meter at invoice time.

At no point does your n8n workflow talk to Stripe. It knows the consumer id (so it can scope data per customer), but by default it doesn’t know which plan they’re on, doesn’t know how close to their quota they are, and never sees the API key. Unless you want it to: all the data is available for you to pass over to n8n if you require it for some aspect of your workflow. Get creative! Plans, quotas, and billing stay on Zuplo’s side; n8n just runs the workflow.

Zuplo Developer Portal

How to wire up signup, pricing, plan selection, and Stripe Checkout for your customers.

Your n8n workflow is now a paid API

Your n8n workflow now has paying customers. They sign up through a dev portal, pick a plan, get an API key, and start making calls. Their usage is metered per request, quotas are enforced at the edge, billing happens on Stripe’s normal invoice cycle, errors don’t burn their quota, secrets in the response get scrubbed on the way out, and injection attempts get flagged (or blocked, if you set strict: true). The workflow itself didn’t change.

What you’ve actually built is a pattern, not a product. The five Zuplo policies, the small custom policy that forwards the consumer id, the Edit Fields → Respond to Webhook response shape: all of these apply to any n8n workflow you want to charge for. Swap the Anthropic node for whatever your workflow does. Swap Supabase for whatever you store in. The boundary between Zuplo and n8n stays the same.

If you have a workflow you’ve been meaning to charge for, spin up a free Zuplo project, point it at your n8n webhook URL, and walk the five steps. The free tier is enough to build and test all of this end-to-end; you’ll need a paid plan when you’re ready to take real money from real customers.

Monetization Policy Reference

Full reference for meters, meterOnStatusCodes, plan setup, and the Stripe integration.