---
title: "Monetize an n8n AI Workflow with Zuplo"
description: "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."
canonicalUrl: "https://zuplo.com/blog/2026/04/30/monetize-an-n8n-ai-workflow-with-zuplo"
pageType: "blog"
date: "2026-04-30"
authors: "martyn"
tags: "API Monetization, AI, API Gateway"
image: "https://zuplo.com/og?text=Monetize%20an%20n8n%20AI%20Workflow%20with%20Zuplo"
---
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.

<CalloutAudience
  variant="useIf"
  items={[
    `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.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/n8n-workflow.png)

<CalloutTip>
  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.
</CalloutTip>

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.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/architecture.png)

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}.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/zuplo-portal-policy-chain.png)

<CalloutDoc
  title="Monetization Quickstart"
  description="The complete Zuplo walkthrough for setting up monetization, Stripe, and plans."
  href="https://zuplo.com/docs/articles/monetization/quickstart"
  icon="book"
/>

## 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`.

<CalloutTip variant="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.
</CalloutTip>

## 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.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/portal-files-pane.png)

   The full module:

   ```typescript
   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.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/custom-code-config.png)

   The portal commits this to `config/policies.json`:

   ```json
   {
     "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}.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/policy-chain-custom-code-highlighted.png)

   The chain ends up as:

   ```json
   "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.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/n8n-webhook-node.png)

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.](/blog-images/2026-04-30-monetize-an-n8n-ai-workflow-with-zuplo/n8n-edit-fields.png)

## 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](https://zuplo.com/docs/articles/monetization/stripe-integration),
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.

<CalloutDoc
  title="Zuplo Developer Portal"
  description="How to wire up signup, pricing, plan selection, and Stripe Checkout for your customers."
  href="https://zuplo.com/docs/dev-portal/introduction"
  icon="book"
/>

## 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](https://zuplo.com/signup), 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.

<CalloutDoc
  title="Monetization Policy Reference"
  description="Full reference for meters, meterOnStatusCodes, plan setup, and the Stripe integration."
  href="https://zuplo.com/docs/policies/monetization-inbound"
  icon="book"
/>