- Trigger
- Webhook, POST on the path /lead-magnet-signup
- Nodes
- 9
- Requires
- Postgres, Beehiiv (HTTP Header Auth)
- Category
- From the course
- Email and Newsletter Automation
- Published
- 2026-09-10
Node types used
- Webhook
- Postgres
- If
- Edit Fields (Set)
- HTTP Request
- Respond to Webhook
What it does
A public form endpoint gets found within days of going live. Left open it fills your list with addresses nobody typed, burns your sending quota and quietly damages your sender reputation. The two usual answers are a CAPTCHA the visitor has to solve or nothing at all, and neither is much good.
A Webhook node accepts POST on the path lead-magnet-signup with allowed origins set and the response deferred to a Respond to Webhook node. A Postgres node runs one statement that counts this IP address attempts in the last hour and records the current attempt in the same query, reading the caller address from cf-connecting-ip or x-forwarded-for. An If node then requires four things at once: the honeypot field named website must be empty, the recent attempt count must be under five, the Origin header must match the site, and the email must match a basic address pattern. Requests that pass go through a Set node that lowercases and trims the address and carries the first name and three UTM fields, then a Postgres upsert into newsletter_signups that keys on email and preserves an existing first name rather than blanking it, then an HTTP Request that POSTs to the Beehiiv v2 subscriptions endpoint with send_welcome_email false and the UTM values and first name attached, then a Postgres UPDATE that stores the status Beehiiv returned. Requests that fail any check drop to a second Respond to Webhook node that returns the same success message, so a bot cannot tell it was rejected.
The export sends no email of its own. There is no Resend, Postmark or SMTP node in it, so the confirmation the response promises has to come from the Beehiiv publication double opt-in setting or from a separate workflow, and nothing here delivers a lead magnet file.
Node by node
- 1
Webhook Accepts POST on the path lead-magnet-signup. Allowed origins are set on the node and responseMode is responseNode, so the reply comes from whichever Respond to Webhook node the run reaches.
- 2
Rate limit One Postgres statement with two CTEs: it counts rows in signup_attempts for this IP in the last hour and inserts a new attempt row at the same time, returning recent_attempts. The IP is the first entry of cf-connecting-ip or x-forwarded-for, falling back to 0.0.0.0.
- 3
Accept? An If node combining four conditions with AND: the body field website (the honeypot) is empty, recent_attempts is less than 5, the Origin header equals the allowed site, and the email matches ^[^@\s]+@[^@\s]+\.[^@\s]+$.
- 4
Clean signup A Set node that produces email (trimmed and lowercased), first_name (trimmed), and utm_source, utm_medium and utm_campaign copied straight from the body.
- 5
Record signup Inserts into newsletter_signups and on conflict on email updates first_name only when the incoming value is not empty, and bumps updated_at. Returns the row.
- 6
Create Beehiiv subscription POSTs to https://api.beehiiv.com/v2/publications/{publication id}/subscriptions with the cleaned email, send_welcome_email false, the three UTM values and a First Name custom field. Authenticates with a generic HTTP Header Auth credential.
- 7
Save Beehiiv status Updates newsletter_signups.beehiiv_status for that email with data.status from the API response.
- 8
Respond — check your email Returns JSON with ok true and the message "Check your email to confirm your subscription."
- 9
Respond — generic success The false branch of Accept?. Returns exactly the same JSON body, so a bot, a rate-limited caller and a real subscriber all see the same response.
Setup after import
- 1
Create a Postgres credential and select it on all three Postgres nodes.
- 2
Create signup_attempts (ip, attempted_at defaulting to now()) and newsletter_signups (email unique, first_name, utm_source, utm_medium, utm_campaign, beehiiv_status, updated_at).
- 3
Create an HTTP Header Auth credential holding your Beehiiv API key and select it on Create Beehiiv subscription.
- 4
Replace the publication id in the Beehiiv URL. The export ships with pub_00000000-0000-0000-0000-000000000000.
- 5
Replace https://example.com in two places: the allowedOrigins option on the Webhook node and the Origin condition inside Accept?. They must match the site the form is served from or every request fails the check.
- 6
Add a hidden input named website to your form and leave it empty. That field is the honeypot the first condition tests.
- 7
Decide whether Beehiiv or a workflow of your own sends the confirmation email. This one does not.
Limits
The rate limit is per IP address, five per hour, and the count is hardcoded in the SQL.
The Origin check only stops naive callers. Anything that sets its own headers can send whatever origin it likes.
No email is sent and no lead magnet file is delivered.
A Beehiiv error fails the run before the Respond node, so the visitor gets no response at all rather than a friendly one.
Download the workflow
The 9 node export as n8n reads it. Credential ids, personal values and real endpoints are replaced with placeholders, so nothing here can reach an account that is not yours. No email, no account.
Download newsletter-signup-form-to-beehiiv.jsonGet help with this in the community
If the import fails, if a node errors on the first run, or if you want to point this at a service it does not cover yet, post it in the free House of Loops community. Say which template it is and paste the error. Shannon Atkinson answers, and the answer stays there for the next person who hits the same thing.
Ask in the communityMore templates
Stripe webhook signature verification in n8n
A Webhook node with Raw Body on, a Code node that checks the Stripe-Signature HMAC, and a Postgres insert that ignores duplicate event ids.
- Trigger
- Stripe webhook, POST on the path /stripe
- Nodes
- 3
Stripe subscription lifecycle router in n8n
Verifies the Stripe signature, then routes customer.subscription events, invoice.paid and invoice.payment_failed to one branch per subscription status.
- Trigger
- Stripe webhook, POST on the path /stripe-subscription-lifecycle
- Nodes
- 15
Daily Stripe reconciliation to Postgres with a Slack alert
Runs at 02:00, pages through yesterday's Stripe balance transactions, finds the ids with no row in stripe_events, logs the result and alerts Slack on a gap.
- Trigger
- Schedule Trigger, daily at 02:00
- Nodes
- 7