Newsletter signup form

Newsletter signup form6 min read

Send your own newsletter form's signups to beehiiv

Connect your own signup form to beehiiv: a relay creates each subscription with double opt-in on, the placement as UTM source and the page as referrer.

Preet SutharFounder, Ikiform

Post your own form to Ikiform and point the webhook at a relay that calls beehiiv's create subscription endpoint. Send double_opt_override: "on" so beehiiv emails the confirmation, pass the form's placement as utm_source and the page as referring_site, and keep reactivate_existing false so a signup never quietly undoes an unsubscribe. One API call per signup, with the key kept in the relay.

What's different with beehiiv

beehiiv's API is organised around publications. A subscription is an email address subscribed to one publication, and you create it with one POST to /v2/publications/{publicationId}/subscriptions. Everything about the signup travels in that one request: whether to confirm, whether to send the welcome email, where the reader came from, and custom fields. There's no separate tag or form step as in Kit.

beehiiv also reports acquisition in UTM terms. Its subscription records carry utm_source, utm_medium, utm_campaign and referring_site. Map your form's fields onto those, and each subscriber in beehiiv shows which placement and which article brought them in:

How the relay maps an Ikiform submission to beehiiv subscriptionIkiform eventbeehiiv subscriptionfields.emailemailfields.sourceutm_sourcemeta.referrerreferring_sitefields.first_namecustom_fields: First Namedouble_opt_override: "on"reactivate_existing: falseHow the relay maps an Ikiform submission to beehiiv subscriptionIkiform eventbeehiiv subscriptionfields.emailemailfields.sourceutm_sourcemeta.referrerreferring_sitefields.first_namecustom_fields:First Namedouble_opt_override:"on"reactivate_existing:false
  1. fields.email becomes email.
  2. fields.source becomes utm_source.
  3. meta.referrer becomes referring_site.
  4. fields.first_name becomes custom_fields: First Name.
  5. double_opt_override: "on" is added by the relay.
  6. reactivate_existing: false is added by the relay.
The first four come from the Ikiform event. The last two are fixed choices the relay makes for every signup.

Set up beehiiv

  1. In beehiiv, open Settings, then API under Workspace Settings, and choose Create New API Key. Copy it right away; beehiiv won't show it again.
  2. On a paid plan, restrict the key to the one publication the form feeds. A publication-scoped key gets 404 for any other publication, which limits the damage if it leaks.
  3. Find the publication's id, the prefixed id beehiiv's API uses in the path. The API's list publications endpoint returns it.
  4. Create a custom field named First Name if you want to store first names. beehiiv discards custom fields that don't exist yet.
  5. Set up the double opt-in and welcome emails in the publication's settings. The relay turns them on per request; their content lives in beehiiv.
beehiiv's pricing page lists API access on every plan, with lower API rate limits on cheaper plans. Its Send API, for sending email through the API, is limited to higher plans, but this relay only creates subscriptions and doesn't need it.

The relay

This is a Netlify Function, which suits a site already on Netlify: it runs on your own domain, next to the static site, with no separate service. Install @netlify/functions for the types, and set the three variables in the site's environment settings.

netlify/functions/beehiiv.mts
import type { Config } from "@netlify/functions";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export default async (request: Request) => {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: Netlify.env.get("IKIFORM_WEBHOOK_SECRET")!,
    });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const { email, first_name, consent, source } = event.data.fields;
  if (consent !== true || typeof email !== "string") return new Response("Skipped", { status: 200 });

  const publication = Netlify.env.get("BEEHIIV_PUBLICATION_ID");
  const answer = await fetch(`https://api.beehiiv.com/v2/publications/${publication}/subscriptions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${Netlify.env.get("BEEHIIV_API_KEY")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email,
      double_opt_override: "on",
      send_welcome_email: true,
      reactivate_existing: false,
      utm_source: typeof source === "string" ? source : "website",
      utm_medium: "signup_form",
      referring_site: event.data.meta.referrer ?? undefined,
      custom_fields: typeof first_name === "string" && first_name ? [{ name: "First Name", value: first_name }] : [],
    }),
  });

  if (answer.status === 429) {
    const reset = Number(answer.headers.get("ratelimit-reset"));
    const wait = reset ? Math.max(1, reset - Math.floor(Date.now() / 1000)) : 60;
    return new Response("beehiiv rate limit", { status: 503, headers: { "retry-after": String(wait) } });
  }
  if (!answer.ok) return new Response(`beehiiv answered ${answer.status}: ${await answer.text()}`, { status: 502 });
  return new Response(null, { status: 204 });
};

export const config: Config = {
  path: "/hooks/beehiiv",
};
  • double_opt_override: "on" sends the confirmation email whatever the publication's default is. Use "not_set" to follow the publication setting instead, and never "off" for a public form.
  • send_welcome_email defaults to false in the API. Set it to true if you want API signups to get the welcome email that embed signups get.
  • reactivate_existing: false is the default too. Spell it out so nobody flips it later without thinking; it reactivates people who unsubscribed.
  • beehiiv answers 200 with the subscription, and its status starts as validating while beehiiv checks the address, then pending until the reader confirms.
  • On 429, the relay reads beehiiv's RateLimit-Reset, a Unix time, and turns it into a Retry-After delay for Ikiform.

The function's URL is https://your-site.com/hooks/beehiiv. Add it under Webhooks in Ikiform and copy the whsec_ secret into IKIFORM_WEBHOOK_SECRET. The webhooks docs list the URL rules, and your own server shows how to skip a delivery you've already handled.

Choosing UTM values

beehiiv's UTM fields are free text, so decide the values once and keep them consistent, or the records split into near-duplicates such as footer and Footer. The form's hidden source input is the natural utm_source. Keep utm_medium fixed, so every signup from your own forms can be told apart from beehiiv's hosted pages and from imports:

Placementsource inpututm_sourceutm_medium
Site footerfooterfootersignup_form
End of a postpostpostsignup_form
Newsletter pagenewsletter-pagenewsletter-pagesignup_form

If you run a campaign that links to a page with the form, read the campaign from the page URL in the relay. meta.referrer holds the full URL of the page, query string included when the browser sends it, so the relay can copy a utm_campaign parameter from it into the request.

Rate limits by plan

beehiiv measures its limit per API key in fixed 60-second windows, and the limit depends on the plan. Each signup is one request:

beehiiv planRequests per minute
Free30
Lite180
Pro500
Enterprise800, higher on request

Thirty a minute is enough for most footer forms, but a launch post on a free publication can pass it. The relay's 503 makes Ikiform wait and retry rather than drop the signup. Plan names and limits change, so check beehiiv's rate limiting page if yours isn't listed.

Gotchas

  • Custom field names are exact. beehiiv matches custom fields by name and drops unknown ones with a warning in the response, not an error. A typo means first names quietly go missing.
  • referring_site can be empty. meta.referrer is null when the browser didn't send a referrer, and the relay leaves the field out.
  • Existing subscribers. beehiiv's docs say the call creates or updates the subscription, but not what changes for an active subscriber. Test with your own confirmed address before you put the form on every page.
  • Keys are workspace-wide by default. A key without a publication restriction can reach every publication in the workspace. Scope it.
  • Paid tiers stay in beehiiv. Leave tier out so signups join without a paid subscription. Upgrades happen in beehiiv's own checkout.

Check it works

  1. Choose Send test on the endpoint in Ikiform. The delivery shows 204 and beehiiv gets no request.
  2. Subscribe from your page with an address you own. The delivery shows 204.
  3. In beehiiv, the subscriber appears as pending, with your placement as UTM source and the article as referring site.
  4. The double opt-in email arrives. Confirm it, and the welcome email follows.
  5. Unsubscribe that address in beehiiv and sign up again. The subscriber stays inactive, because reactivate_existing is false.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.