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

Source: https://voxgig.com/howto/bring-automation-platform-calls-under-policy

- Audience: platform-team
- Level: intermediate
- Verified: 2026-10-21
- Published: 2026-10-20

## 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](/howto/route-outbound-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

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A service you write yourself](https://nodejs.org/api/http.html#httprequestoptions-callback) | A workflow of a few steps whose calls must obey the same allowlist, budget, and audit trail as your services | Retries, idempotency, and trigger handling become your code, and every change to the workflow waits for a developer | The people who change the workflow every week are not developers |
| [Make](https://help.make.com/allow-connections-to-and-from-make-ip-addresses) | Workflows that stay hosted, calling a vendor that can restrict a key to source addresses | One published list of egress addresses per zone, shared by every customer in it, so the vendor admits Make rather than your workflow | The calls must appear in your egress log or count against your budget |
| [Self-hosted n8n](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/deployment) | A visual workflow builder that has to run inside your network, behind the same proxy as everything else | A stateful service to run, patch, back up, and scale, holding every workflow's credentials in its own database | Nobody on the team will own one more stateful service |
| [Zapier](https://help.zapier.com/hc/en-us/articles/8496216020877-Zapier-IP-addresses-and-allowlisting) | Hosted workflows built outside engineering, where the speed of a change matters more than control | Calls leave from a broad AWS range unless the integration has static addresses, and bypass your proxy, your budget, and your log | The 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:

```js title="platform.mjs"
/**
 * 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
}
```

```bash
node demo.mjs
```

```text output
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](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/deployment)
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](https://help.make.com/allow-connections-to-and-from-make-ip-addresses),
and Zapier offers [fixed ranges](https://docs.zapier.com/integrations/build/static-ip) 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](https://docs.stripe.com/keys/restricted-api-keys) are
built:[^2]

```js title="stands.mjs"
/** 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](https://docs.zapier.com/integrations/build/deduplication) and can
[replay failed runs](https://help.zapier.com/hc/en-us/articles/8496241726989-Replay-Zap-runs).
n8n has a [Remove Duplicates](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.removeduplicates)
node and a [Retry On Fail](https://docs.n8n.io/build/understand-workflows/workflow-components/work-with-nodes)
setting, and Make keeps [incomplete executions](https://help.make.com/error-handling) to retry.
The service has to do both, and its retry is safe only with a key the vendor can match:

```js title="service.mjs"
/**
 * 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:

```bash
node demo.mjs
```

```text output
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.

```bash
node --test policy.test.mjs
```

```text output
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:

```bash
node pitfall.mjs
```

```text output
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:

```bash
node pitfall.mjs
```

```text output
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](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest)
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.

## Related how-tos

- [Make queue consumer writes safe under redelivery](/howto/queue-consumer-writes-under-at-least-once)

- [Retry fetch calls with exponential backoff in Node.js](/howto/retry-fetch-with-backoff-in-node)

- [Route outbound API calls through an egress proxy](/howto/route-outbound-calls-through-an-egress-proxy)

- [Give a vendor a stable egress IP to allowlist](/howto/give-vendors-a-stable-egress-ip)

- [Choose between an egress gateway and an in-process library](/howto/choose-between-an-egress-gateway-and-a-library)

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

[^1]: Zapier's [static ranges](https://docs.zapier.com/integrations/build/static-ip) 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](https://help.zapier.com/hc/en-us/articles/8496216020877-Zapier-IP-addresses-and-allowlisting)
suggests allowing requests that carry `User-Agent: Zapier` instead, a header any HTTP client can be
configured to send.

[^2]: Stripe's [restricted key guide](https://docs.stripe.com/keys/restricted-api-keys) 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](https://web.mit.edu/Saltzer/www/publications/protection/Basic.html) 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.

[^3]: n8n reads its proxy variables through the `proxy-from-env` package, and its
[documentation](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/deployment)
notes that the lowercase names take precedence when both forms are set. curl
[accepts `http_proxy` in lowercase only](https://everything.curl.dev/usingcurl/proxies/env.html),
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.