Guides

Guides13 min read

Event registration and RSVP forms on your own site

Take RSVPs on your own event page: the form, guest limits, an honest headcount, closing when full and a check-in list, without a ticketing app.

Preet SutharFounder, Ikiform

Put an RSVP form on your event page that posts to a form backend, with a yes or no answer, a guest count capped on the server, and the details you need to plan. Count heads from the latest answer per email, using a CSV export or a short script, and pause the form when you're full. Ikiform stores and forwards every RSVP but has no ticketing, payments or automatic capacity cap, so use a ticketing platform when you need those.

Most events don't need a ticketing platform. A meetup, a workshop, a launch party, a reading group or a free conference day needs three things: a page that says what and when, a form that collects who is coming, and a number you can trust the day before. This guide builds that on your own site and is clear about the parts a form can't do.

How an RSVP travels

The form lives on your event page. Each RSVP posts to Ikiform, which checks it against your field rules, filters spam, stores it and tells you. From there it reaches whatever you plan with: the dashboard, a spreadsheet, or a script that counts heads.

How an RSVP gets from your event page to your headcountevent pageyour RSVP formIkiformchecks theguest limit,filters spamdashboardevery RSVP,CSV exportemail toorganisersReply goes tothe guestsignedwebhooksubmission.createdsheet orscriptlatest answerper emailRSVPcountHow an RSVP gets from your event page to your headcountevent pageyour RSVPformIkiformchecks theguest limit,filters spamdashboardevery RSVP, CSVexportemail toorganisersReply goes to theguestsigned webhooksubmission.createdsheet or scriptlatest answer peremailRSVPcount
  1. A guest fills in the RSVP form on your event page, and it posts to your Ikiform endpoint.
  2. Ikiform checks the fields, including the guest limit, and scores the post for spam before storing it.
  3. Every RSVP appears in the dashboard, where you can filter it and export it as CSV.
  4. The organisers get an email for each RSVP, and Reply goes straight to the guest.
  5. Ikiform sends a signed submission.created webhook.
  6. A sheet or a short script turns the RSVPs into a headcount, using each person's latest answer.
Ikiform stores answers; the headcount is a sum you run over them. That's the step a ticketing app would do for you.

What Ikiform doesn't do for events

Be clear about this before you choose. A form backend collects answers. It isn't a ticketing system:

  • No capacity cap. There's no setting that closes the form after a number of RSVPs or guests. You watch the count and pause the form yourself.
  • No tickets or payments. No QR codes, no check-in app, no paid tiers, no refunds.
  • No email to the guest. 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.
  • No waitlist. When you pause the form, new posts are refused. A waitlist would be a second form you put up in its place.
  • No deduplication. Ikiform doesn't merge or reject repeat submissions by email. An identical repeat within 24 hours only adds to the spam score. Deduplicate where the list lives, or when you export.

Each of these has a workable answer below. If several of them are deal-breakers, a ticketing platform is the better tool.

Ways to take RSVPs

ApproachGood atTrade-offs
Ticketing platform, such as Eventbrite or LumaTickets, payments, capacity limits, waitlists, confirmation emails and check-in, all includedThe event page and attendee list live on their platform with their branding. Paid tickets carry fees; check their pricing pages
Google FormsFree and quick. A switch to stop accepting responsesLooks like Google, not your event. A separate page, and no server-side guest limit
Calendar invite or group chatFine for twenty friendsNo numbers you can trust, no dietary needs, and no list on the day
Your own form and a form backend, such as IkiformYour page, your design, any fields. Guest limits checked on the server, spam filtered, organisers emailed, CSV and webhooksYou count heads, close registration and send confirmations yourself, or with a webhook to an email tool

For a paid event, use a ticketing platform. For a free event where the page and the list should stay yours, the rest of this guide is the setup. The Eventbrite page compares the two in more depth.

Fields that make planning possible

Ask what you'll act on and nothing else. Every question here changes something you order, book or arrange. Try it: the preview runs Ikiform's validator in your browser, so you can see the 422 a fourth guest would get.

Preview

Runs Ikiform's validator in your browser. Nothing is sent.

Can you make it?

Not counting you. Up to 3.

Paste this definition on the form's Fields tab and Ikiform enforces it on every post, from any page. The event RSVP template has a shorter version with generated code.
FieldWhyKeep in mind
attendingA No is information too: it stops you chasing peopleA radio with yes and no, so the count is a filter
guestsThe number you order food and chairs forA number with min: 0, max: 3, integer: true. Ikiform refuses 5 and 1.5 on the server
dietaryCatering countsA select adds up cleanly. Free text doesn't
access_needsStep-free access, seating, a quiet roomOptional. Read every one before the day
emailTelling people about changes, and matching changed answersAlso the reply-to of every notification email
Ask only for what the event needs. Dietary and access answers can be health information; say on the form why you ask, keep the export somewhere private, and delete the submissions once the event is over.

The RSVP form

Pick your stack. Each version thanks a Yes and a No differently, hides the guest questions for a No, and sends guests as 0 when someone isn't coming, so your sums stay right.

Paste the form into the event page. It posts the plain way and redirects to your confirmation page, so it works without JavaScript. The short script hides the guest questions when someone answers No and resets the guest count to 0.

event.html
<form id="rsvp" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required maxlength="120">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>
  <fieldset>
    <legend>Can you make it?</legend>
    <label><input type="radio" name="attending" value="yes" required> Yes</label>
    <label><input type="radio" name="attending" value="no"> No</label>
  </fieldset>
  <div id="if-going">
    <label for="guests">Guests you're bringing</label>
    <input id="guests" name="guests" type="number" min="0" max="3" step="1" value="0">
    <label for="dietary">Dietary needs</label>
    <select id="dietary" name="dietary">
      <option value="none">None</option>
      <option value="vegetarian">Vegetarian</option>
      <option value="vegan">Vegan</option>
      <option value="gluten_free">Gluten free</option>
      <option value="other">Other, tell us below</option>
    </select>
    <label for="access_needs">Access needs or anything else</label>
    <textarea id="access_needs" name="access_needs" rows="3" maxlength="1000"></textarea>
  </div>
  <input type="hidden" name="_redirect" value="https://example.com/meetup/registered">
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
  <button type="submit">Send RSVP</button>
</form>
<script>
  const rsvp = document.getElementById("rsvp");
  const extra = document.getElementById("if-going");
  rsvp.addEventListener("change", () => {
    const no = rsvp.elements.attending.value === "no";
    extra.hidden = no;
    if (no) rsvp.elements.guests.value = "0";
  });
</script>
The server-side versions post with a secret key, so Ikiform skips Allowed domains and its human check. Every post then comes from your server's IP, and the Standard limit of 5 a minute per visitor counts all your visitors together. Choose the Relaxed or Custom rate limit for a form you post through a server, and keep IKIFORM_SECRET_KEY in server-only environment variables. The server versions also pass on the browser's Origin header when there is one, because a server's own fetch sends none and Ikiform counts free localhost test submissions by it.

Framework guides in the docs go deeper: Next.js, Astro, SvelteKit, Nuxt, React, Vue and plain HTML. On a site builder, the HTML version pastes into an embed block.

Count heads from the latest answer

People change their minds. Someone says Yes with two guests on Monday and No on Thursday; that's two submissions, and the second is the truth. So the rule is: one answer per email, the newest one, then add one for the person plus their guests.

Why the headcount uses each person's latest RSVPAda: yes2 guestsBen: yes0 guestsAda: nochanged hermind1 personBen onlylatest winsMondayWednesdayThursdayheadcountWhy the headcount uses each person's latest RSVPAda:yes2guestsBen:yes0guestsAda:nochangedhermind1personBenonlylatest winsMondayWednesdayThursdayheadcount
  1. Monday: Ada RSVPs yes with 2 guests. Adding every submission would count 3 people.
  2. Wednesday: Ben RSVPs yes with no guests.
  3. Thursday: Ada sends the form again and says no. Ikiform stores it as a second submission from her.
  4. Taking only each email's newest submission, Ada is a no, so the headcount is 1: Ben.
Adding every row would count Ada's party of three. Counting the newest row per email gets it right.

In a spreadsheet

  1. In Submissions, pick the RSVP form, set the status to Received and the date range to All time, then Export CSV.
  2. Import it into Google Sheets and sort by created_at, newest first.
  3. Select the data and use Data > Data cleanup > Remove duplicates on the email column. Sheets keeps the first row it sees, which is now the newest.
  4. Count, with attending in column G and guests in column H. Adjust the letters to your export:
Google Sheets
=COUNTIF(G2:G, "yes") + SUMIF(G2:G, "yes", H2:H)

The export has id, created_at, form and status first, then one column per field. One file holds up to 100,000 rows.

With a script

For a number you can rerun any time, read the RSVPs through the REST API. Lists come newest first, so the first submission seen for each email is the latest. Run this with Node and a secret API key:

headcount.ts
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });
const latest = new Map<string, Record<string, unknown>>();

for await (const submission of ikiform.submissions.iterate({
  formId: "frm_xxxxxxxxxxxxxxxx",
  status: "received",
  limit: 100,
})) {
  const email = String(submission.fields.email ?? "").trim().toLowerCase();
  if (email && !latest.has(email)) latest.set(email, submission.fields);
}

let people = 0;
const meals = new Map<string, number>();
for (const fields of latest.values()) {
  if (fields.attending !== "yes") continue;
  people += 1 + Number(fields.guests ?? 0);
  const meal = String(fields.dietary ?? "none");
  meals.set(meal, (meals.get(meal) ?? 0) + 1);
}

console.log(`${latest.size} people replied. ${people} are coming, guests included.`);
console.table(Object.fromEntries(meals));

The meal table counts RSVPs, not people, because one form can't say which guest eats what. If guests' meals matter, as at a wedding, ask per person; the wedding RSVP page shows how.

Closing registration when you're full

Ikiform won't stop the form at a number, so set up two things: an alert before you're full, and a way to close. Close in two places, the form and the page:

  1. In the form's settings, choose Pause form. From then on, posts are refused with 403 form_paused, so a stale tab or a bot can't add more.
  2. Replace the form on your event page with a line such as Registration is closed. Email us to join the waitlist. Visitors should never meet the error.
  3. If you reopen, choose Resume form and put the form back.

The alert can be a webhook handler that recounts on every Yes and posts to a chat webhook, such as Slack's, once you pass 90% of capacity. This is a Next.js route handler; the same logic runs on any server:

app/api/rsvp-alert/route.ts
import { createClient } from "@ikiform/sdk";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const CAPACITY = 80;
const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });

async function peopleComing(formId: string) {
  const seen = new Set<string>();
  let people = 0;
  for await (const submission of ikiform.submissions.iterate({ formId, status: "received", limit: 100 })) {
    const email = String(submission.fields.email ?? "").trim().toLowerCase();
    if (!email || seen.has(email)) continue;
    seen.add(email);
    if (submission.fields.attending === "yes") people += 1 + Number(submission.fields.guests ?? 0);
  }
  return people;
}

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    if (event.type !== "submission.created" || event.data.fields.attending !== "yes") {
      return new Response(null, { status: 204 });
    }
    const people = await peopleComing(event.data.form.id);
    if (people >= CAPACITY * 0.9) {
      await fetch(process.env.ALERT_WEBHOOK_URL!, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          text: `${people} of ${CAPACITY} places taken. Pause the RSVP form in Ikiform when you're full.`,
        }),
      });
    }
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
  • It recounts through the API each time, so a changed answer is reflected. Each key can make 100 API requests a minute, which covers several thousand RSVPs per count.
  • The alert tells a person to pause. Pausing is a dashboard action, so nothing closes by surprise.
  • Add the endpoint under Webhooks and copy its secret into IKIFORM_WEBHOOK_SECRET. A workspace can have 3 endpoints; the webhooks docs cover retries.
From open registration to closed, with nothing automaticopenform on thepage90% alertwebhookhandlerrecountsPauseform403form_pausedpage saysclosedwaitlist byemailRSVPsyou decideFrom open registration to closed, with nothing automaticopenform on the page90% alertwebhook handler recountsPause form403 form_pausedpage says closedwaitlist by emailRSVPsyou decide
  1. While registration is open, every RSVP posts to Ikiform.
  2. Your webhook handler recounts the latest answers on every Yes and alerts you at 90% of capacity.
  3. You decide when you're full and choose Pause form, which refuses new posts with 403 form_paused.
  4. You replace the form on the page with a closed message and a way to join a waitlist.
A person makes the call to close. That's slower than a ticketing app's hard cap, and it means a late change of plans never locks out someone you'd have let in.

Confirmations without a confirmation email

Ikiform doesn't email the guest, so give them what they need on the thank-you page: the date, time, address and a calendar file. A static .ics file next to the page works in every calendar app. Link to it from the confirmation page as Add to calendar:

meetup.ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Meetup//RSVP//EN
BEGIN:VEVENT
UID:meetup-2027-03-14@example.com
DTSTAMP:20261025T090000Z
DTSTART:20270314T183000Z
DTEND:20270314T210000Z
SUMMARY:Example meetup
LOCATION:The Old Library, 12 High Street, London
DESCRIPTION:Doors at 6:30 pm. Reply to the organiser if your plans change.
END:VEVENT
END:VCALENDAR

Times ending in Z are UTC, so calendar apps show them in each guest's own time zone. If you want a real confirmation email, send each RSVP by webhook to your email tool, directly or through Zapier, Make or n8n, and send it from there. The same list then gets the reminder the day before.

Changed answers and cancellations

Tell people how to change their answer: Plans changed? Send the form again with the same email. Your count already uses the newest answer, so nothing else is needed. Notifications show every change as it happens, which is useful the week of the event.

Two people on the same Wi-Fi share an IP address, and the rate limit counts per IP. The default Standard, 5 a minute per visitor is fine for an event page. Don't set a Custom limit of one post per day to stop repeats: it blocks a second household member on the same network, and the latest-answer rule already handles repeats.

On the day

Export the RSVPs, remove duplicates as above, filter to attending = yes, sort by name and print it. Add columns for a tick and for guests who arrived. A printed list works when the venue's signal doesn't. Keep the access needs column on the copy the door team uses, and the dietary column on the caterer's.

Spam doesn't reach your list: the honeypot, spam scoring and rate limits filter it before it counts, and spam sends no email or webhook. Check the Spam filter once before you print, in case a real RSVP with an unusual message ended up there. Disposable addresses add 2 to the spam score.

Pick your situation

A wedding, a free community event and a volunteer rota each count differently. These pages cover what changes.

Check it works

Test the whole loop a week before you announce the event. Ikiform has no free plan. Testing is free: a workspace without a plan accepts 50 test submissions a month from localhost. To go live, pick Pro from $6/mo for 500 submissions, or pay once: 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire. See pricing.

  1. Under the form's Allowed domains, add your site's domain, plus localhost while you develop. Leave it empty to accept posts from anywhere.
  2. Send a Yes with two guests and a No from different emails. Both arrive, and the No shows guests as 0.
  3. Send three guests, then four. The fourth is refused with Must be at most 3.
  4. Check the inbox of each notification recipient. Each gets an email with the fields, and Reply goes to the submitter when the form has an email field.
  5. Run the headcount script or the sheet formula. It should match what you sent.
  6. Pause the form and submit once more. You get 403 form_paused. Resume it.
  7. Post with the honeypot filled in, _gotcha=bot. It still gets a success answer, but the submission is stored as spam and sends no email or webhook.
  8. Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.

Troubleshooting

ProblemWhat to do
The headcount is too highYou're adding every submission. Keep only the newest per email, and check a No sends guests as 0.
403 form_paused on the live pageThe form is paused in the dashboard. Resume it, or replace the form on the page with a closed message.
422 validation_failed mentioning guestsMore guests than the field's max, or a decimal. Show Ikiform's message next to the field, as the samples do.
403 origin_not_allowed, or every post lands in spamThe page's domain isn't in the form's Allowed domains. Add it, plus localhost for local tests. Subdomains of a listed domain are allowed.
429 rate_limitedOne visitor sent more than the form's rate limit allows. Standard allows 5 a minute per visitor. Wait for Retry-After, or choose Relaxed or Custom for forms that post from one shared IP.
The submission is in the dashboard but no email or webhook arrivedIt was stored as spam. Spam sends nothing. Open it under the Spam filter to see the reasons, and mark it as not spam to send its email and webhooks.
The browser lands on Ikiform's thank-you page instead of yours_redirect is ignored unless its domain is in Allowed domains and it is an https: URL. Add the domain, or set Redirect after submit in the form's settings instead.

Questions

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