Newsletter signup form

Newsletter signup form6 min read

Send your own newsletter form's signups to Kit

Connect your own newsletter form to Kit with a relay that creates the subscriber, adds them to a Kit form for the incentive email, and tags the placement.

Preet SutharFounder, Ikiform

Post your own form to Ikiform, and point an Ikiform webhook at a relay that makes up to three Kit v4 API calls: create the subscriber, add them to a Kit form, and tag where they signed up. Create them as inactive and add them to a form whose incentive email is on, and Kit should send its own confirmation, which you test once with an address you own because Kit documents it only for its bulk endpoint. The relay holds the Kit API key; your page never sees it.

What's different with Kit

Kit splits a signup into separate objects. A subscriber is a person. A form is where they came from, and it carries the confirmation email, which Kit also calls the incentive email: its double opt-in step. A tag is a label you segment by. The v4 API has one endpoint for each, and both the form and the tag endpoints need the subscriber to exist first. So one Ikiform signup becomes a short sequence of calls, in order:

The Kit API calls the relay makes for one signupIkiformwebhookverified by therelayPOST/v4/subscribersstate: inactivePOST/v4/forms/{id}/subscriberssends incentiveemailPOST/v4/tags/{id}/subscriberstags theplacement123The Kit API calls the relay makes for one signupIkiform webhookverified by the relayPOST /v4/subscribersstate: inactivePOST/v4/forms/{id}/subscriberssends incentive emailPOST/v4/tags/{id}/subscriberstags the placement123
  1. The relay receives Ikiform's signed webhook and verifies it.
  2. It creates the subscriber in Kit as inactive. If the address already exists, Kit updates the first name and leaves the state alone.
  3. It adds the subscriber to your Kit form. If that form's incentive email is on, Kit sends the confirmation.
  4. If the signup's source has a tag, it tags the subscriber. This step is optional.
Each call depends on the one before it, so the relay stops at the first failure and lets Ikiform retry.
CallKit answersMeaning
Create subscriber201New subscriber, created in the state you sent
Create subscriber200Already exists. First name updated, state unchanged
Add to form201 or 200Added, or already on the form. Kit's bulk form docs say people already on a form don't get the incentive email again
Any call401Wrong or missing API key
Add to form or tag404The form or tag id doesn't exist in this account
Any call429Over the API key rate limit

Set up Kit

  1. In Kit, create a form for these signups, such as "Website signup". You won't embed it; it's the record of where subscribers came from and the home of the incentive email.
  2. Open the form's settings and turn the incentive email on if you want double opt-in. Write the email and set where the confirmation link goes.
  3. Find the form's numeric id, in the Kit app or with the API's list forms endpoint. Create a tag per placement if you want them, and note their ids.
  4. Create a v4 API key in Kit's developer settings and keep it for the relay's environment.
Kit's API docs say API access depends on the account's plan being eligible, and its pricing page lists apps and integrations from the Creator plan up. Check your plan can create a v4 key before you build on it.

The relay

A Next.js route handler receives the webhook and a small module talks to Kit. The same two files run on any Node or edge host that gives you a public HTTPS URL.

lib/kit.ts
const KIT = "https://api.kit.com/v4";

const TAGS: Record<string, number> = {
  footer: 1111111,
  post: 2222222,
};

function call(path: string, body: Record<string, unknown>) {
  return fetch(`${KIT}${path}`, {
    method: "POST",
    headers: {
      "X-Kit-Api-Key": process.env.KIT_API_KEY!,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify(body),
  });
}

export async function addToKit(signup: { email: string; firstName?: string; source: string; referrer?: string }) {
  const created = await call("/subscribers", {
    email_address: signup.email,
    first_name: signup.firstName ?? null,
    state: "inactive",
  });
  if (!created.ok) return created;

  const joined = await call(`/forms/${process.env.KIT_FORM_ID}/subscribers`, {
    email_address: signup.email,
    referrer: signup.referrer ?? null,
  });
  if (!joined.ok) return joined;

  const tag = TAGS[signup.source];
  if (!tag) return joined;
  return call(`/tags/${tag}/subscribers`, { email_address: signup.email });
}
app/api/kit/route.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { addToKit } from "@/lib/kit";

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.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 answer = await addToKit({
    email,
    firstName: typeof first_name === "string" ? first_name : undefined,
    source: typeof source === "string" ? source : "unknown",
    referrer: typeof event.data.meta.referrer === "string" ? event.data.meta.referrer : undefined,
  });

  if (answer.status === 429) return new Response("Kit rate limit", { status: 503, headers: { "retry-after": "60" } });
  if (!answer.ok) return new Response(`Kit answered ${answer.status}: ${await answer.text()}`, { status: 502 });
  return new Response(null, { status: 204 });
}
.env.local
IKIFORM_WEBHOOK_SECRET=whsec_...
KIT_API_KEY=your-v4-api-key
KIT_FORM_ID=1234567
  • X-Kit-Api-Key is how Kit's v4 API takes a personal API key. OAuth is for apps other people install; a relay for your own list doesn't need it.
  • Sending state: "inactive" is what keeps a new subscriber unconfirmed until they click. Leave it out and Kit creates them as active, and the state can't be changed later through the same endpoint.
  • The page's referrer goes to Kit as the form's referrer, so Kit shows which article the reader came from.
  • Kit's error body goes into the 502, so the delivery log in Ikiform shows Kit's own message.
  • Map each source value to a tag id in TAGS. A source with no tag skips the third call.

Add the route's URL under Webhooks in the Ikiform dashboard, pick the newsletter form, and copy the whsec_ secret into IKIFORM_WEBHOOK_SECRET. Every rule for the URL and each answer is on the webhooks docs.

Rate limits and retries

Kit allows 120 requests per rolling 60 seconds for an API key. Each signup costs two or three, so roughly 40 to 60 signups a minute fit before Kit answers 429. That's plenty for a footer form, and close on the day a post goes viral.

The relay answers 503 with Retry-After when Kit says 429, and Ikiform waits before it tries again. A retry runs the whole sequence from the start. That is safe: create returns 200 for an existing subscriber, and adding someone already on the form returns 200. Kit's bulk form docs say people already on a form don't get the incentive email again; the single endpoint doesn't say, so test a retry with an address you own. Your own server shows how to record each webhook-id if you'd rather skip retries you've already finished.

Gotchas

  • Custom fields must exist first. If you add fields to the create call, use each field's key, such as last_name, and create the field in Kit before you send it.
  • Active subscribers stay active. Because create never changes an existing subscriber's state, a confirmed reader who signs up again isn't asked to confirm again.
  • Unsubscribed readers. A reader who unsubscribed and signs up again exists already, so create returns 200 and doesn't change their state. Check in Kit how you want to handle them.
  • One form, many placements. Use one Kit form for the website and tags for placements. Several Kit forms means several incentive emails to keep in step.
  • No email from Ikiform. Ikiform doesn't email the person who fills in the form. Notification emails go to you and your team only, so there is no autoresponder, confirmation email or double opt-in. Send those from your email tool, triggered by a webhook.

Check it works

  1. Choose Send test on the endpoint in Ikiform. The delivery shows 204, and nothing changes in Kit.
  2. Subscribe on your page with an address you own. The delivery shows 204.
  3. In Kit, the subscriber shows on your form as unconfirmed, with the placement's tag.
  4. The incentive email arrives. Click the link and the subscriber becomes confirmed.
  5. Subscribe again with the same address. The delivery shows 204 and no second incentive email arrives.
  6. Set a wrong KIT_FORM_ID for a moment. The delivery shows 502 with Kit's 404 message.

Questions

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