How-to › Operate integrations in production

How to bring Zapier and n8n calls under your outbound policy#

Put automation workflows under the egress policy your services follow: self-host n8n behind the proxy, scope hosted keys, or rewrite as a service.

Audience
Platform team
Level
intermediate
Topic
Control outbound integrations from one place
Verified

Read first: Route outbound API calls through an egress proxy

Your egress proxy logs every call your services make to the billing vendor, yet the vendor’s dashboard lists invoices that no line in that log explains. They come from a Zap someone in finance built, calling from Zapier’s network with the account’s admin key pasted into its connection. Your allowlist, your budget, and your audit trail cover none of those calls.

What you get

You will end up with each automation workflow behind your egress proxy or rewritten as a service, a scoped key per workflow, and the retries and duplicate detection the platform provided. This is for you if automation platforms call your vendors.

Short answer

To bring Zapier or n8n calls under your outbound policy, move each workflow to where the policy runs. Self-host n8n inside your network with HTTP_PROXY pointing at your egress proxy, or rewrite it as a small service that calls through the proxy. Where Zapier or Make stays, register the platform’s published egress addresses with the vendor and give each workflow a key scoped to its job. A rewrite loses the platform’s retries and duplicate detection, so send an Idempotency-Key.

You will need

Node 22 or later, an egress proxy your services already use, and a vendor that issues scoped keys. The proxy here is the one from routing outbound API calls through an egress proxy, reduced to a method rule per vendor host. Verified 2026-10-21 against Node 22.22.0. Zapier, Make, and n8n did not run. The workflow engine is a stand-in that skips a repeated trigger and calls from wherever it runs, and the vendor and the proxy are loopback stand-ins.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A service you write yourselfA workflow of a few steps whose calls must obey the same allowlist, budget, and audit trail as your servicesRetries, idempotency, and trigger handling become your code, and every change to the workflow waits for a developerThe people who change the workflow every week are not developers
MakeWorkflows that stay hosted, calling a vendor that can restrict a key to source addressesOne published list of egress addresses per zone, shared by every customer in it, so the vendor admits Make rather than your workflowThe calls must appear in your egress log or count against your budget
Self-hosted n8nA visual workflow builder that has to run inside your network, behind the same proxy as everything elseA stateful service to run, patch, back up, and scale, holding every workflow’s credentials in its own databaseNobody on the team will own one more stateful service
ZapierHosted workflows built outside engineering, where the speed of a change matters more than controlCalls leave from a broad AWS range unless the integration has static addresses, and bypass your proxy, your budget, and your logThe vendor or your auditors need to see each call

Self-hosted n8n keeps the visual builder and puts every call through your proxy, at the price of a stateful service with credentials in its database. A rewritten service puts the call under the same review as any other code, and takes on the retries and duplicate detection the platform gave away. Zapier and Make stay the fastest to change, but an allowlist of their addresses admits every customer of the platform.

Run the workflow where your proxy can see it

Your egress log is the test of whether a workflow is under policy. The demo runs one workflow, triggered twice by the same order, on a hosted platform and on a self-hosted n8n with HTTP_PROXY set. The engine stand-in does the two things that matter here:

/**
 * A stand-in for an automation platform's workflow engine, reduced to the two facts this page
 * depends on. It skips a trigger it has already processed, as Zapier's polling triggers and n8n's
 * Remove Duplicates node do. And it calls the vendor from wherever it runs: hosted, from the
 * platform's network with no proxy in the path; self-hosted, as n8n can be, through the
 * HTTP_PROXY you set.
 */
export async function runWorkflow({ triggers, key, vendorUrl, proxy = null }) {
  const seen = new Set()
  const outcome = []
  for (const trigger of triggers) {
    if (seen.has(trigger.id)) {
      outcome.push(`${trigger.id}: duplicate trigger, skipped by the platform`)
      continue
    }
    seen.add(trigger.id)
    const res = await send(`${vendorUrl}/invoices`, { method: 'POST', proxy, headers: { 'x-api-key': key }, body: trigger })
    outcome.push(`${trigger.id}: ${res.status} ${res.body.error ?? res.body.id}`)
  }
  return outcome
}
node demo.mjs
order o_1001, delivered twice

hosted platform, connection holding the admin key
  o_1001: 201 inv_1
  o_1001: duplicate trigger, skipped by the platform
  your egress log: nothing
  billco: 1 call with the admin key, 1 invoice

self-hosted n8n in your network, HTTP_PROXY set, workflow key
  o_1001: 201 inv_1
  o_1001: duplicate trigger, skipped by the platform
  your egress log: 1 line, allowed POST 127.0.0.1:39979/invoices
  billco: 1 call with the workflow key, 1 invoice

The hosted run created the invoice with the admin key and left your egress log empty. The self-hosted run did the same work through your proxy, under its method rule, with the workflow key, and left one line. n8n takes its proxy from HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY, as its deployment variables document, so the move is configuration rather than workflow edits. Both runs skipped the second delivery of the order, which is the platform doing its job.

Scope what stays hosted

Where Zapier or Make stays, shrink what a pasted key can do, since nothing you run will see the call. If the vendor can restrict a key to source addresses, register the platform’s published egress addresses. Make lists three per zone, and Zapier offers fixed ranges for an integration whose developers turned them on.1 Then give each workflow its own key, scoped to its one job, the way Stripe’s restricted keys are built:2

/** The two keys BillCo issues: the admin key its dashboard shows first, and one scoped to a workflow. */
export const KEYS = {
  sk_admin_billco: { label: 'admin key', scopes: ['*'] },
  sk_wf_invoices: { label: 'workflow key', scopes: ['invoices:write'] },
}

The workflow key can create invoices and do nothing else. A copy that leaks from a workflow export or a shared screen can only create invoices, and the vendor’s audit log names the key that did. Neither control puts the call in your egress log or under your budget, because the traffic never reaches your network.

Rebuild what the platform did for you

A rewrite puts the call under your proxy and gives up two things the platform supplied without being asked. Zapier skips a polling trigger’s items by their unique id and can replay failed runs. n8n has a Remove Duplicates node and a Retry On Fail setting, and Make keeps incomplete executions to retry. The service has to do both, and its retry is safe only with a key the vendor can match:

/**
 * The workflow rewritten as a service of your own: create the invoice for an order at BillCo,
 * through your egress proxy, with the key scoped to this workflow. Two things the platform did for
 * you are written down here. The Idempotency-Key comes from the order id, so a second delivery of
 * the same order, or a retry after a lost response, gets the first invoice back instead of a new
 * one. The loop retries a 5xx, a dropped connection, or a response that has not come within
 * `timeoutMs`, with that same key, backing off between attempts, and stops at the cap.
 */
export async function createInvoice(order, { vendorUrl, egress, key, attempts = 3, delayMs = 200, timeoutMs = 10_000 }) {
  // Without an id every order would share the key order-undefined and get the first invoice back.
  if (order.id == null || order.id === '') throw new TypeError('createInvoice needs the order id for its Idempotency-Key')
  const headers = { 'x-api-key': key, 'idempotency-key': `order-${order.id}` }
  let last
  for (let i = 1; i <= attempts; i++) {
    try {
      const res = await send(`${vendorUrl}/invoices`, { method: 'POST', proxy: egress, headers, body: order, timeoutMs })
      if (res.status < 500) return { ...res, attempts: i }
      last = res
    } catch (err) {
      last = err // no response, or the connection dropped: the invoice may exist, so retry with the same key
    }
    if (i < attempts) await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** (i - 1)))
  }
  if (last instanceof Error) throw last
  return { ...last, attempts }
}

Each attempt gets timeoutMs to answer, so a response that never comes counts as lost and is retried rather than holding the service forever. The key comes from the order, not from the attempt, so a retry after a lost response and a second delivery of the same order both carry it. The last two runs of the demo show both:

node demo.mjs
your own service through the proxy, workflow key, Idempotency-Key per order
  o_1001: 201 inv_1 after 1 attempt
  o_1001: 200 inv_1 replayed after 1 attempt
  your egress log: 2 lines, allowed POST 127.0.0.1:33783/invoices
  billco: 2 calls with the workflow key, 1 invoice

the same service when billco commits the first invoice and the response is lost
  o_1001: 200 inv_1 replayed after 2 attempts
  o_1001: 200 inv_1 replayed after 1 attempt
  your egress log: 3 lines, allowed POST 127.0.0.1:39741/invoices
  billco: 3 calls with the workflow key, 1 invoice

The second delivery came back as the first invoice, marked replayed. In the last run the vendor committed the invoice and lost the response, so the service retried with the same key and got that invoice back. Three calls for one order left one invoice.

Check it worked

Nine tests pin the page’s claims. A hosted run leaves your egress log empty, and a self-hosted run leaves a line. The platform and the service each turn a repeated order into one invoice. The workflow key cannot delete a customer, and the proxy refuses a DELETE whatever key it carries. Two lost responses in a row still leave one invoice, and so does a response that never comes. An order with no id is refused, since its key would match every other order without one. The last two hold the stand-ins to the names they were given. A key or a host named constructor is unknown, a request in origin form is refused, and Host goes upstream as the host the rule checked.

node --test policy.test.mjs
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0

# pass 9 is the result to expect. A failure in the fifth test means the retry created a second invoice, so the key is missing or changes with each attempt. A failure in the sixth means a call waited on a response that never came.

When it goes wrong

A key pasted into a hosted connection does whatever the vendor allows that key, from an address your proxy never sees. The run below sends the same DELETE with the admin key and with the workflow key:

node pitfall.mjs
the connection key leaks, and somebody sends DELETE /customers/c_7 with it
  sk_admin_billco  200  customer c_7 deleted
  sk_wf_invoices   403  key lacks scope customers:delete
  your egress log: nothing, the call never crossed your network
  through your proxy, with the admin key: 403 DELETE is not allowed for billco

The admin key deleted the customer, and the vendor refused the workflow key. Through your proxy, the method rule refused even the admin key. Take the admin key out of every hosted connection, rotate it, and issue a scoped key per workflow.

The rewrite creates a second invoice. A retry without an Idempotency-Key is a second request as far as the vendor can tell:

node pitfall.mjs
the rewrite without an Idempotency-Key, first response lost
  attempt 1: 503 upstream timeout
  attempt 2: 201 inv_2
  invoices billco holds for order o_1001: 2

Derive the key from the trigger’s own id, as createInvoice does, so a retry and a redelivery both match the first invoice.

A self-hosted workflow goes around the proxy. The n8n HTTP Request node takes a proxy of its own, which takes precedence over the environment variables, and n8n reads http_proxy before HTTP_PROXY when both are set.3 A host listed in NO_PROXY goes direct as well. Let only the egress proxy out of the network n8n runs in, so a node configured to go elsewhere fails rather than leaving.

When not to do this

Do not move a workflow that calls no vendor you police. A Zap that posts a summary to a team chat gains nothing from your proxy, and moving it costs a service somebody has to run.

Do not self-host n8n to save the subscription. The cost moves to your on-call rotation, along with a database of credentials to protect.

Do not rewrite a workflow that changes every week. A service turns each change into a pull request, which is right when the calls move money and wrong when they send a newsletter.

Do not register a platform’s egress addresses and call the vendor locked down. The list admits every customer of that platform, so the key’s scope is still the only limit on a leaked copy.

Last verified

Verified 2026-10-21 against Node 22.22.0. Every output block is what the command preceding it printed. Zapier, Make, and n8n did not run: the workflow engine is a stand-in that skips a repeated trigger and calls from wherever it runs. The platforms’ features are read from their documentation.

Footnotes

  1. Zapier’s static ranges are two /28 blocks, one in us-east-1 and one in us-west-2, 32 addresses between them. They carry requests only from paid Zapier accounts, so an allowlist built from them admits every paying customer whose Zap uses that integration. Zapier’s allowlisting article suggests allowing requests that carry User-Agent: Zapier instead, a header any HTTP client can be configured to send. ↩︎ Back to text

  2. Stripe’s restricted key guide asks you to configure each key in line with the principle of least privilege, holding the minimum permissions its job needs. Jerome Saltzer and Michael Schroeder set the principle down in a paper whose manuscript was received in October 1974. Their version gives the reason this page cares about: when a privilege is misused, fewer programs have to be audited. A hosted connection holding the admin key is one more program on that list, in somebody else’s data center. ↩︎ Back to text

  3. n8n reads its proxy variables through the proxy-from-env package, and its documentation notes that the lowercase names take precedence when both forms are set. curl accepts http_proxy in lowercase only, because CGI hands a script each request header as an uppercase variable prefixed HTTP_. A header named Proxy would arrive as HTTP_PROXY. A platform team that sets only the uppercase name is relying on nobody having set the other. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.