Published: · Updated: · by kodewright
API Key Generator for Content Automation (2026)
Generate webhook secrets for Zapier, Make and n8n, turn on the auth they already have, and verify HMAC on the raw body. Tested in Node 22 and gitleaks.
An API key generator for content automation should give you a random secret of at least 128 bits from a cryptographic random source. You put that secret in a request header on both ends of the webhook (your Zapier, Make or n8n flow and the endpoint it calls), and the endpoint rejects every request that doesn't carry it. For anything that spends money or publishes, the endpoint should also check an HMAC signature computed over the raw request body.
I don't run content pipelines with API keys myself and I've never had an automation key abused, so this guide isn't built on war stories. It's built on things I ran for this update: a Node 22 test of the most common HMAC mistake, and a gitleaks scan that shows which automation secrets a scanner recognises and which it walks past. To make the secret itself, the Secret Key Generator does it in your browser tab with the Web Crypto API; the key is never sent to a server, including mine.
Which keys you generate and which you only protect
An automation stack holds two kinds of keys, and they fail in different ways.
Provider keys are the ones you're given: OpenAI, Anthropic, or a social platform's token. You can't make them stronger. Your whole job is keeping them out of places they leak to, and a leaked one usually shows up as a bill.
Webhook secrets are the ones you make. They're the shared string that proves a request hitting your endpoint came from your scenario and not from a scanner that found the URL. This is where an API key generator matters: you mint the value, paste it into the automation tool and into your endpoint, and anything without it gets a 401.
Make / Zapier / n8n Your endpoint
┌──────────────┐ ┌──────────────────────────┐
│ scenario │── POST ────────▶│ header != secret → 401 │
│ + secret │ X-Webhook- │ else → run the job │
└──────────────┘ Secret header └──────────────────────────┘
Generate the webhook secret: 128 bits from a CSPRNG
Whether a random secret can be guessed is plain entropy math:
H = L × log₂(R)
H is entropy in bits, L the length in characters, R the number of possible characters. A 32-character hex string (R = 16) is 32 × 4 = 128 bits. Even if an attacker got a SHA-256 hash of the secret and ran a single RTX 4090 at about 22 billion guesses per second (hashcat 6.2.6), the average search times look like this:
| Secret | Entropy | Average offline search, SHA-256, 1× RTX 4090 |
|---|---|---|
| 8 hex characters | 32 bits | ~0.1 seconds |
| 16 hex characters | 64 bits | ~13 years |
| 32 hex characters | 128 bits | ~2.5 × 10²⁰ years |
That holds only for truly random values. Summer25 or the project name plus a year falls to a dictionary attack in seconds. At 128 random bits, guessing is off the table and theft is the only way in, which is why most of this article is about theft.
The randomness has to come from a CSPRNG. Math.random() in V8 is xorshift128+, and its state can be recovered from a few outputs, so a "128-bit" key built on it is only as secret as those outputs. In code, crypto.randomBytes(32).toString('base64url') in Node and secrets.token_urlsafe(32) in Python both read from the operating system's generator. The Secret Key Generator uses crypto.getRandomValues(), and all three of its formats now carry the full bit count, alphanumeric included. One detail if you pick its Base64 format: it's standard Base64, so a 128-bit key is 24 characters ending in == and may contain + and /. That's fine in a header. In a URL, percent-encode it or pick hex.
Turn on the auth your platform already has
Before writing any verification code, check what the platform does for you. As of October 2026 the three big ones differ a lot.
| Platform | Incoming webhook auth built in | What to do |
|---|---|---|
| Make (Custom webhook) | Optional API key, sent in the x-make-apikey header; IP allowlist | Turn the API key on. Make strips that header, so it isn't passed into the scenario's data |
| n8n (Webhook node) | Basic auth, Header auth, JWT auth | Use Header auth with a generated secret; turn on Raw Body if you verify signatures yourself |
| Zapier (Catch Hook) | None that I found | The URL is the only secret. Use Catch Raw Hook, which includes the request headers, and stop the zap with a Filter step when the secret doesn't match |
The Make and n8n options come from their own docs (Make's Webhooks app, n8n's Webhook node). For Zapier I couldn't find a built-in check, and the question keeps coming up on Zapier's community forum without an official answer.
Your webhook URL is a secret, and scanners don't know it
On Zapier the catch-hook URL is the credential: anyone holding it can trigger the zap. On Make and n8n without auth turned on, the same is true. So I checked whether a secret scanner would notice one committed to a repo. I generated values in the right shapes (random, not real) for nine files and scanned them with gitleaks 8.30.1 and its default rules, five times with fresh values each time.
| What was in the file | Detected? | Rule |
|---|---|---|
OPENAI_API_KEY=sk-proj-… in .env | Yes, 5 of 5 | openai-api-key |
| Slack incoming webhook URL | Yes, 5 of 5 | slack-webhook-url |
const WEBHOOK_SECRET = "<32 hex>" | Yes, 5 of 5 | generic-api-key |
"X-Webhook-Secret": "<32 hex>" and "x-make-apikey": "<64 hex>" in a headers object | Yes, 5 of 5 | generic-api-key |
https://hooks.zapier.com/hooks/catch/<id>/<id>/ | No, 0 of 5 | none |
https://hook.eu1.make.com/<id> | No, 0 of 5 | none |
| n8n cloud webhook URL with a UUID path | No, 0 of 5 | none |
An exported scenario JSON with the secret in a {"name": …, "value": …} header pair | No, 0 of 5 | none |
The default rules know Slack and Teams webhook URLs but not Zapier, Make or n8n ones. The exported-JSON miss matters more than it looks: tools write headers as name/value pairs, so the word "secret" and the value end up on different lines, and the generic rule never sees them together. If you keep scenario exports in git, gitleaks won't save you there.
Adding a rule fixed the URL cases. With this in .gitleaks.toml, the Zapier URL, the Make URL and the Make URL inside the exported JSON were all flagged (3 of 3 runs). The secret value inside the JSON still wasn't, so keep the secret out of exports entirely.
[extend]
useDefault = true
[[rules]]
id = "automation-webhook-url"
description = "Zapier or Make incoming webhook URL"
regex = '''https://(?:hooks\.zapier\.com/hooks/catch/\d+/[a-z0-9]+|hook\.[a-z0-9]+\.make\.com/[a-z0-9]{20,})'''
keywords = ["hooks.zapier.com", "make.com"]
If one of these URLs has already been in a public repo, a screen recording or a support chat, regenerate the webhook in the platform. For keys in general, API key management best practices covers prefixes and scanning in more depth, and securing .env files covers where they should live.
HMAC: sign the raw body, not the parsed JSON
A static secret proves the sender knew a string. An HMAC signature also proves the body wasn't changed: the sender computes HMAC-SHA-256 over the body with the shared secret and sends it in a header, and the receiver recomputes it and compares.
The mistake to avoid is verifying against JSON.stringify(req.body) after a framework has already parsed the request. I tested what that does in Node 22 (v22.22 and v22.23, same results): sign a body exactly as a sender would serialize it, then verify once against the raw bytes and once against the re-serialized object.
| Body as sent | Verify raw bytes | Verify JSON.stringify(parsed) |
|---|---|---|
{"title":"Café menu","price":12} | match | match |
{"title":"Café menu","price":12.0} | match | no match (12.0 becomes 12) |
Python json.dumps default: spaces after : and ,, é as é | match | no match |
| Pretty-printed with newlines | match | no match |
Re-serializing only works when the sender happened to write JSON exactly the way JSON.stringify does, so the check passes in your test and fails against a different sender. Keep the raw body. Stripe's docs say the same about their signatures, and their scheme adds something worth copying: the signed string is timestamp.body, and their libraries reject anything older than 5 minutes, which blocks replays of a captured request.
The second trap is in the comparison. crypto.timingSafeEqual throws ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH when the two buffers differ in length, so a request with a short or empty signature header crashes the handler into a 500 instead of a clean 401. Check the length first. This is the function I ran:
const crypto = require('crypto')
function verify(rawBody, signatureHex, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest()
const received = Buffer.from(signatureHex, 'hex')
if (received.length !== expected.length) return false
return crypto.timingSafeEqual(received, expected)
}
On a 2 KB body it took about 3 to 4 µs per check over 200,000 runs, so speed is no reason to skip it. In Express, mount express.raw({ type: 'application/json' }) on the webhook route so req.body is a Buffer, verify, then JSON.parse it yourself. HMAC mode in the Hash Generator is handy for producing a signature for a test payload and comparing it with what your code computes.
A shared secret in a header is enough for most creator automations. I'd add signing once the webhook can trigger a payout, a mass email or a publish to production.
Where automation keys leak
The no-code platforms add leak paths that plain code doesn't have, because secrets pass through shared workspaces, run histories and screen recordings.
| Leak path | How it happens | Fix |
|---|---|---|
| Run history | Make and Zapier keep the data each step received and sent; a secret inside a payload sits there | Keep secrets in headers or connections, not in payload fields |
| Shared workspace | A freelancer added to the workspace can open every scenario and read every key | Limit access per folder or team; rotate when someone leaves |
| Recordings | A Loom walkthrough shows the key on screen for three seconds | Blur it, or rotate right after recording |
| Frontend code | Calling a provider API from a web page ships the key to every visitor | Call it from a server or edge function |
| AI chats | Pasting a flow with its key into an assistant to debug it | Strip secrets first; treat a pasted key as burned |
| Exported scenarios | Blueprint or workflow JSON committed to git with header values inside | Keep exports out of public repos; scanners miss the values (see above) |
Provider keys deserve one extra setting that no scanner replaces: a monthly spending limit in the provider's dashboard, so a stolen key costs you the limit and not more.
Rotate with an overlap window
No standard sets a rotation interval for API keys. Pick one you'll keep (every few months is common), and rotate right away when a collaborator leaves, a key shows up in a log or recording, or usage spikes.
Rotate without downtime by accepting two secrets for a short time: generate the new one, add it to the endpoint as a second accepted value, update the automation, confirm the flows still fire, then remove the old one and revoke it with the provider. Skipping the overlap breaks every live scenario at the moment of the swap. Stripe's dashboard does the same thing for its signing secrets, keeping the old one valid for up to 24 hours.
If a key or webhook URL has ever been in a recording, a chat or a shared scenario, start there today: generate a replacement, do the overlap swap, and revoke the old one.
FAQ
Can I just use a UUID as my webhook secret?
For low-stakes flows, yes. A v4 UUID from the UUID Generator carries 122 random bits, which nobody will guess. For anything that pays out, emails a list or publishes, use a dedicated secret of 128 bits or more and add HMAC signing, so you check the payload and not only the sender.
Do I still need a webhook secret if the endpoint uses HTTPS?
Yes. HTTPS stops people on the network from reading or changing the request, but it says nothing about who sent it. Anyone who knows your endpoint URL can send a perfectly valid HTTPS request to it. The secret, or an HMAC signature, is what tells your code the request came from your own scenario.