# Event registration without Eventbrite, for free community events

> Run registration for a free community event on your own site: session choices, a confirmation email from your own tool, room counts and a check-in list.

Published 2026-10-25 · https://ikiform.com/guides/event-registration/without-eventbrite

For a free event, a registration form on your own site can replace Eventbrite's signup: it posts to Ikiform, which stores each registration and emails your team. Add a webhook that sends the confirmation email from your own email provider, and count seats per session from the latest registration per email. Keep Eventbrite, or another ticketing platform, for paid tickets, hard capacity caps and check-in scanning.

## What's different without a ticketing platform

Eventbrite does five jobs at once, and it's easy to forget the ones you'll now do yourself. For a free community day, a meetup series or a workshop, most of them are small. Here's the split, honestly:

Diagram: Which jobs move from Eventbrite to your own setup, and who does each

1. The event page moves from Eventbrite's page to your own site, with your design.
2. Registration moves from Eventbrite's checkout to your form, which posts to Ikiform.
3. The confirmation email is no longer sent for you. A webhook sends it from your own email provider.
4. Capacity is no longer a hard cap. You count registrations per session and pause the form when you're full.
5. Check-in is no longer an app with QR scanning. You print a list from the CSV export.

The first three are better on your own site. The last two are worse, and for a large or paid event they're the reason to stay on a ticketing platform.

Eventbrite's pricing changes, so check its own pricing page before you compare costs. For free events the reasons to leave are usually not fees: the attendee list sits on someone else's platform, the page looks like theirs, and registrants hand their details to another company with its own account and email settings. For paid events, stay. Ikiform has no payments.

## A registration form with session choices

A community day often has parallel sessions, and rooms differ in size. Ask which sessions people plan to attend, so you can move a popular workshop to a bigger room before the day. This definition caps choices at three of four, since two run at the same time:

Form: Community day registration

- **Name for your badge** (`name`, text, required)
- **Email** (`email`, email, required)
- **Sessions you'll attend** (`sessions`, multi_select, required)
- **Company or group** (`organisation`, text)
- **Lunch** (`lunch`, radio, required)
- **Email me about future community days** (`updates`, checkbox)

`sessions` is a `multi_select`, stored as an array. In HTML, name the checkboxes `sessions[]` so a single choice is still stored as a list.

- `name` is labelled for the badge, which gets you the name people want to be called.
- `lunch` is a `radio` with a `No lunch` option, so you order for the people who'll eat.
- `updates` is an unticked checkbox, stored as `true` or `false` on a submission that keeps its own `created_at`. Only add people to your newsletter when it's `true`.
- Use the [RSVP form from the main guide](https://ikiform.com/guides/event-registration.md#form) for the markup in your stack, with these fields swapped in.

> If a registration form also offers future updates, keep that checkbox unticked by default and say exactly what you'll send. The checkbox is your record of consent; the confirmation email below isn't marketing and doesn't need it.

## Send the confirmation from your own email tool

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](https://ikiform.com/docs/webhooks.md). For an event that matters, because people forward the confirmation to their manager and search for it on the morning. This route handler receives Ikiform's signed webhook and sends the email through Resend's API. Any provider with a send endpoint works the same way.

`app/api/registered/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const TITLES: Record<string, string> = {
  keynote: "Opening talk, 10:00",
  workshop_a: "Workshop A, 11:30",
  workshop_b: "Workshop B, 11:30",
  panel: "Closing panel, 15:00",
};

function escape(text: string) {
  return text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
}

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") return new Response(null, { status: 204 });

    const fields = event.data.fields;
    const email = String(fields.email ?? "");
    const chosen = (Array.isArray(fields.sessions) ? fields.sessions : []).map((id) => TITLES[String(id)] ?? String(id));
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": request.headers.get("webhook-id") ?? event.data.submission_id,
      },
      body: JSON.stringify({
        from: "Community Day <hello@community.example>",
        to: email,
        reply_to: "hello@community.example",
        subject: "You're registered for Community Day, 14 March",
        html: [
          `<p>Hi ${escape(String(fields.name ?? ""))},</p>`,
          "<p>You're on the list for Community Day on 14 March at The Old Library. Doors open at 9:30.</p>",
          `<p>Your sessions: ${chosen.map(escape).join(", ")}.</p>`,
          '<p><a href="https://community.example/day.ics">Add it to your calendar</a>. Can\'t come after all? Reply to this email.</p>',
        ].join(""),
      }),
    });
    if (!response.ok) return new Response(`Email API answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

- `verifyWebhook` checks the signature, so nobody else can make your domain send email.
- `Idempotency-Key` uses the delivery's `webhook-id`, which stays the same on every automatic retry and on a manual **Retry** of a failed delivery, so a delivery whose answer got lost doesn't send a second email.
- The name and session titles are escaped before they go into HTML. Treat field values as untrusted input.
- A `502` makes Ikiform retry, so a short outage at the email provider doesn't lose a confirmation. Spam never triggers the webhook, so bots don't make you email strangers.

Add the route under **Webhooks** in the dashboard and copy its secret into `IKIFORM_WEBHOOK_SECRET`. Ikiform retries a failed delivery for about 3 days, and the delivery log shows each attempt. Without code, an automation tool such as [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) or [Make](https://ikiform.com/blog/form-submissions-to-make.md) can send the same email.

## Seats per session

Ikiform won't stop registrations for one session when its room is full, and it can't hide a full option. Count instead, using each person's newest registration, and run it whenever you want a number:

`seats.ts`:

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

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });
const ROOMS: Record<string, number> = { keynote: 200, workshop_a: 30, workshop_b: 30, panel: 200 };
const latest = new Map<string, string[]>();

for await (const submission of ikiform.submissions.iterate({ formId: process.argv[2], status: "received", limit: 100 })) {
  const email = String(submission.fields.email ?? "").trim().toLowerCase();
  const chosen = Array.isArray(submission.fields.sessions) ? submission.fields.sessions.map(String) : [];
  if (email && !latest.has(email)) latest.set(email, chosen);
}

for (const [session, seats] of Object.entries(ROOMS)) {
  const taken = [...latest.values()].filter((chosen) => chosen.includes(session)).length;
  console.log(`${session.padEnd(12)} ${String(taken).padStart(4)} of ${seats}${taken >= seats ? "  FULL" : ""}`);
}
```

When a workshop fills, edit the page to mark it full and remove its option from the form and the field definition, so new posts can't choose it. When the whole day fills, choose **Pause form**: posts get `403 form_paused`, and the page should say registration has closed.

| Situation | What to do |
| --- | --- |
| One workshop full | Remove its option from the form and the field definition, and say so on the page |
| The whole day full | **Pause form** and replace the form with a waitlist email address |
| Someone cancels | They reply to the confirmation email. Delete their submission so the count frees the seat |
| No-shows expected | Free events often see a share of no-shows. Track your own rate and over-register a little next time |

## At the door

1. The evening before, export **Submissions** as CSV and remove duplicate emails, keeping the newest.
2. Sort by name, keep `name`, `organisation` and `lunch`, and print two copies for the desk.
3. Print badges from the same sheet with a mail merge, using the `name` column.
4. Write in walk-ins by hand. After the event, add them to your own list only if they agree.

## Check it before you announce

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. Register with your own email, choosing two sessions.
3. The confirmation email arrives from your domain, listing both sessions.
4. If you added a webhook, open the endpoint in **Webhooks**. The delivery shows `succeeded` with your server's answer.
5. Run the seat count. Each of your two sessions shows one seat taken.
6. Nothing arrived? Switch the dashboard's status filter to **Spam**. Spam gets the same success answer as a real submission.

## Gotchas

| Problem | What to do |
| --- | --- |
| `422` saying to choose at most 3 options | Someone picked all four sessions. That's the `max_items` rule working; show Ikiform's message next to the checkboxes. |
| Only one session is stored when someone picks one | Name the checkboxes `sessions[]`. With the field defined as `multi_select`, a single value is still stored as a list. |
| No confirmation email, but the registration is in Ikiform | Open the delivery in the endpoint's log. A `502` with the provider's status means it refused the email, often an unverified sending domain. |
| The submission is in the dashboard but no email or webhook arrived | It 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. |
| `429 rate_limited` | One 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. |

## Questions

### Is a registration form enough instead of Eventbrite?

For a free event where you can count seats and print a list, yes. For paid tickets, hard capacity limits, waitlists or QR check-in, a ticketing platform does those and Ikiform doesn't.

### How do registrants get a confirmation email?

Send it yourself. Add an Ikiform webhook that calls your email provider's API, or connects through an automation tool, and send the confirmation from your own domain.

### Can I limit seats in each workshop?

Not automatically. Count registrations per session with a short script or a sheet, and remove a full workshop's option from the form when it fills.

### Does Ikiform remove duplicate submissions?

No. Every accepted post is stored. An identical repeat within 24 hours adds to the spam score but isn't spam on its own, so deduplicate by email in your list, database or CSV export.

### Is Ikiform free?

No, there is no free plan. Testing is free: you can send 50 test submissions a month from localhost before you pay. To go live, Pro starts at $6/mo for 500 submissions, or you can pay once for 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire.

## More guides

- [Event registration](https://ikiform.com/guides/event-registration.md): 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.
- [Volunteer signup](https://ikiform.com/guides/event-registration/volunteer-signup.md): A volunteer signup form where people pick shifts, with a phone number for the day, and a script that shows which shifts are short and builds the rota.
- [Wedding RSVP](https://ikiform.com/guides/event-registration/wedding-rsvp.md): An RSVP form for a wedding website you built yourself: one reply per invitation, a meal per named guest, a deadline and a list of who hasn't replied.
