Using Webhooks and Credentials in n8n: Public URLs, OAuth Redirects and Testing
n8n builds every webhook and OAuth callback URL from the WEBHOOK_URL environment variable. Set it to your public HTTPS address, for example https://n8n.example.com/. Test URLs (/webhook-test/...) only work while the editor is listening. Production URLs (/webhook/...) only work once the workflow is active. OAuth2 apps at Google, Microsoft or Slack must list https://n8n.example.com/rest/oauth2-credential/callback as the redirect URI.
This guide covers how to make webhooks reachable, how to secure them, how n8n stores credentials, and how to test everything with curl. It applies to Cloudesta n8n hosting and to any self-hosted n8n running in Docker on Ubuntu 22.04 or 24.04.
Prerequisites
- An n8n instance running in Docker with Docker Compose, and root SSH access to the server.
- A domain or subdomain, for example
n8n.example.com, with DNS you can edit. - A reverse proxy in front of n8n that terminates HTTPS. The examples below use Nginx.
curlon your workstation or the server.
1. How does n8n build webhook URLs?
Every Webhook node has a path, for example order-created. n8n combines that path with WEBHOOK_URL to produce two URLs:
- Test URL:
https://n8n.example.com/webhook-test/order-created. This URL is live only after you click Listen for test event or Execute workflow in the editor. It accepts a single call, then stops listening, and also times out after about two minutes. Use it while you build the workflow. - Production URL:
https://n8n.example.com/webhook/order-created. This URL is live only while the workflow is active (labelled Publish in newer n8n versions). Executions run in the background and appear under Executions.
If WEBHOOK_URL is missing, n8n shows URLs such as http://localhost:5678/webhook/.... Those addresses are useless to external services. To fix this, open the folder that holds your docker-compose.yml. If you do not know where it is, find it like this:
docker ps --format '{{.Names}}'
docker inspect <container-name> --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'
Add or adjust these variables in the n8n service's environment: section:
environment:
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
- N8N_EDITOR_BASE_URL=https://n8n.example.com/
- N8N_PROXY_HOPS=1
- N8N_PAYLOAD_SIZE_MAX=32
Here is what each of the less obvious variables does:
N8N_PROXY_HOPS=1tells n8n to trust theX-Forwarded-*headers from one reverse proxy.N8N_PAYLOAD_SIZE_MAXis the maximum request body in MiB. The default is 16.
Apply the changes by recreating the container. A plain restart does not reload the environment.
cd /path/to/compose-folder
docker compose up -d
docker compose exec n8n printenv WEBHOOK_URL
Replace n8n with your service name if it is different.
2. How do I make an n8n webhook reachable from the internet?
DNS and HTTPS
Create an A record pointing n8n.example.com to your server IP, for example 203.0.113.10. Once it resolves, issue a certificate:
dig +short n8n.example.com
sudo apt update && sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d n8n.example.com
Firewall
Open only SSH, HTTP and HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
"127.0.0.1:5678:5678" in docker-compose.yml so port 5678 is reachable only through the proxy, never directly from the internet.Proxy timeouts and body size
Nginx has two defaults that break webhooks:
- It rejects request bodies over 1 MB with a 413 error.
- It gives up on slow responses after 60 seconds.
In your site file, for example /etc/nginx/sites-available/n8n.example.com, use settings like these inside the server block that listens on 443:
client_max_body_size 32m;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
sudo nginx -t && sudo systemctl reload nginx
Keep client_max_body_size in line with N8N_PAYLOAD_SIZE_MAX.
3. How do I return a custom response with Respond to Webhook?
The Webhook node's Respond setting controls what the caller receives:
- Immediately: returns right away, while the workflow continues in the background. This is best for senders with short timeouts. Slack, for example, expects a reply within 3 seconds.
- When Last Node Finishes: returns the output of the final node.
- Using 'Respond to Webhook' Node: you place a Respond to Webhook node wherever you want in the workflow. You choose what it returns (JSON, text, binary, redirect, first or all incoming items, or no data), the response code and any headers. For example, you can return
{"status":"received","id":"{{ $json.order_id }}"}with code 200, and send a 400 from another branch when validation fails.
4. How do I secure an n8n webhook?
In the Webhook node, set Authentication to Header Auth or Basic Auth and create a credential for it:
- Header Auth: for example, name
X-Webhook-Tokenwith a long random value. Generate a value withopenssl rand -hex 32. Requests without the matching header get a 403 error. - Basic Auth: a username and password the sender must supply.
If the sender publishes its IP ranges, you can also restrict an individual webhook path at the proxy. Add this above the general location / block:
location = /webhook/erp-sync {
allow 198.51.100.0/24;
deny all;
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
For services that sign their payloads, such as Stripe or GitHub, also verify the signature header inside the workflow.
5. Where does n8n store credentials and how are they encrypted?
n8n stores credentials in its database. That is SQLite inside the /home/node/.n8n volume by default, or PostgreSQL if you configured it. Credentials are encrypted with an encryption key, which comes from one of two places:
- The
N8N_ENCRYPTION_KEYenvironment variable, if you set it. - Otherwise, a key generated on first start and saved in
/home/node/.n8n/config.
To view the stored key:
docker compose exec n8n cat /home/node/.n8n/config
6. What OAuth redirect URL should I use for Google, Microsoft and Slack?
When you create an OAuth2 credential, n8n displays an OAuth Redirect URL. With the settings above, it is:
https://n8n.example.com/rest/oauth2-credential/callback
Paste it exactly into the provider's app settings:
- Google: Google Cloud Console → APIs & Services → Credentials → your OAuth client ID (type Web application) → Authorized redirect URIs. Also enable the APIs you need, such as Gmail or Sheets.
- Microsoft: Microsoft Entra admin center → App registrations → your app → Authentication → Add a platform → Web → Redirect URIs.
- Slack: api.slack.com/apps → your app → OAuth & Permissions → Redirect URLs.
Copy the client ID and secret into n8n, then click Connect.
Service accounts and API keys
For server-to-server access, OAuth redirects are not involved:
- Google service accounts: use the Google Service Account credential type and paste the service account email and private key.
- Plain API keys you manage outside the UI: add them to the compose file, for example
- CRM_API_KEY=yourapikey, rundocker compose up -d, and reference them in an HTTP Request node as{{ $env.CRM_API_KEY }}.
If $env returns an access error, set N8N_BLOCK_ENV_ACCESS_IN_NODE=false. Do this only if everyone who can edit workflows is allowed to read those variables.
Verify it works
Click Listen for test event on the Webhook node, then send a test request:
curl -i -X POST https://n8n.example.com/webhook-test/order-created \
-H "Content-Type: application/json" \
-H "X-Webhook-Token: yoursecret" \
-d '{"order_id": 1234, "total": 250}'
The data should appear in the editor. Next, activate the workflow and repeat the request against /webhook/order-created. For Basic Auth, use -u user:yourpassword instead of the header. Then check two more things:
- A request without the token returns 403.
- A new execution appears under Executions.
Common problems
- 404 "The requested webhook ... is not registered": the workflow is inactive, the test listener has expired, or the HTTP method differs from the node setting. Activate the workflow, or click Listen again, and match GET/POST.
- OAuth "redirect_uri_mismatch" or "invalid redirect": the provider's redirect URI does not exactly match what n8n shows. Common causes are
httpvshttps,localhost, or a different subdomain. SetWEBHOOK_URLandN8N_EDITOR_BASE_URL, recreate the container, and copy the URL again. - 413 Request Entity Too Large: raise
client_max_body_sizein Nginx andN8N_PAYLOAD_SIZE_MAXin n8n. - 504 Gateway Timeout on long executions: set the Webhook node to respond Immediately and process in the background, or raise
proxy_read_timeout. Remember that the sender's own timeout still applies. - Webhook URLs show
localhost:5678:WEBHOOK_URLis not set inside the container. Check it withdocker compose exec n8n printenv WEBHOOK_URL.
Frequently asked questions
Can I use the test webhook URL in production?
No. The test URL only responds while the editor is listening, and only for one call. Always give external services the /webhook/ production URL and keep the workflow active.
Do I need to reconnect OAuth credentials if I change my n8n domain?
Yes. Update WEBHOOK_URL and the redirect URI at each provider, then reconnect every OAuth2 credential. External services also need the new webhook URLs.
Is it safe to store API keys in n8n credentials?
Yes, as long as you protect the encryption key, because credentials are encrypted at rest. Limit who has editor access, since editors can use any shared credential in a workflow.
Need help? Open a ticket at https://cloudesta.com/submitticket.php with your n8n domain and the exact error. You can also browse more guides in the Cloudesta knowledgebase.