Anleitungen13 Min. Lesezeit
Eventanmeldungen und RSVP-Formulare auf deiner eigenen Website
Nimm Zusagen auf deiner eigenen Eventseite entgegen: Formular, Begleitpersonen-Limit, ehrliche Teilnehmerzahl, Schließen bei voller Kapazität und Check-in-Liste, ohne Ticketing-App.
Setze ein RSVP-Formular auf deine Eventseite, das an ein Formular-Backend sendet, mit einer Ja-oder-Nein-Antwort, einer serverseitig begrenzten Zahl an Begleitpersonen und den Angaben, die du für die Planung brauchst. Zähle die Teilnehmer anhand der neuesten Antwort pro E-Mail-Adresse, per CSV-Export oder mit einem kurzen Skript, und pausiere das Formular, wenn du voll bist. Ikiform speichert und leitet jede Zusage weiter, hat aber kein Ticketing, keine Zahlungen und keine automatische Kapazitätsgrenze; nutze eine Ticketing-Plattform, wenn du das brauchst.
Die meisten Events brauchen keine Ticketing-Plattform. Ein Meetup, ein Workshop, eine Launch-Party, ein Lesekreis oder ein kostenloser Konferenztag braucht drei Dinge: eine Seite, die sagt, was und wann, ein Formular, das sammelt, wer kommt, und eine Zahl, auf die du dich am Tag davor verlassen kannst. Diese Anleitung baut genau das auf deiner eigenen Site und sagt klar, was ein Formular nicht kann.
Der Weg einer Zusage
Das Formular liegt auf deiner Eventseite. Jede Zusage wird an Ikiform gesendet, das sie gegen deine Feldregeln prüft, Spam filtert, sie speichert und dich benachrichtigt. Von dort gelangt sie zu dem, womit du planst: ins Dashboard, in eine Tabelle oder in ein Skript, das die Teilnehmer zählt.
- Ein Gast füllt das RSVP-Formular auf deiner Eventseite aus, und es wird an deinen Ikiform-Endpunkt gesendet.
- Ikiform prüft die Felder, einschließlich des Begleitpersonen-Limits, und bewertet den Post auf Spam, bevor er gespeichert wird.
- Jede Zusage erscheint im Dashboard, wo du sie filtern und als CSV exportieren kannst.
- Die Organisatoren bekommen für jede Zusage eine E-Mail, und eine Antwort darauf geht direkt an den Gast.
- Ikiform sendet einen signierten submission.created-Webhook.
- Eine Tabelle oder ein kurzes Skript macht aus den Zusagen eine Teilnehmerzahl, anhand der neuesten Antwort jeder Person.
Was Ikiform bei Events nicht macht
Sei dir darüber im Klaren, bevor du dich entscheidest. Ein Formular-Backend sammelt Antworten. Es ist kein Ticketing-System:
- Keine Kapazitätsgrenze. Es gibt keine Einstellung, die das Formular nach einer bestimmten Zahl an Zusagen oder Gästen schließt. Du behältst die Zahl im Blick und pausierst das Formular selbst.
- Keine Tickets oder Zahlungen. Keine QR-Codes, keine Check-in-App, keine kostenpflichtigen Stufen, keine Erstattungen.
- Keine E-Mail an den Gast. Ikiform schickt der Person, die das Formular ausfüllt, keine E-Mail. Benachrichtigungs-E-Mails gehen nur an dich und dein Team, es gibt also keinen Autoresponder, keine Bestätigungs-E-Mail und kein Double-Opt-in. Verschicke sie aus deinem E-Mail-Tool, ausgelöst durch einen Webhook.
- Keine Warteliste. Wenn du das Formular pausierst, werden neue Posts abgelehnt. Eine Warteliste wäre ein zweites Formular, das du an seine Stelle setzt.
- Keine Deduplizierung. Ikiform führt wiederholte Einsendungen per E-Mail-Adresse weder zusammen noch lehnt es sie ab. Eine identische Wiederholung innerhalb von 24 Stunden erhöht nur den Spam-Score. Dedupliziere dort, wo die Liste liegt, oder beim Export.
Für jeden dieser Punkte gibt es unten eine praktikable Lösung. Wenn mehrere davon K.-o.-Kriterien sind, ist eine Ticketing-Plattform das bessere Werkzeug.
Wege, Zusagen zu sammeln
| Ansatz | Stark bei | Nachteile |
|---|---|---|
| Ticketing-Plattform wie Eventbrite oder Luma | Tickets, Zahlungen, Kapazitätsgrenzen, Wartelisten, Bestätigungs-E-Mails und Check-in, alles inklusive | Eventseite und Teilnehmerliste liegen auf ihrer Plattform, mit ihrem Branding. Bezahlte Tickets kosten Gebühren; sieh auf ihren Preisseiten nach |
| Google Forms | Kostenlos und schnell. Ein Schalter, um keine Antworten mehr anzunehmen | Sieht nach Google aus, nicht nach deinem Event. Eine separate Seite und kein serverseitiges Gäste-Limit |
| Kalendereinladung oder Gruppenchat | Reicht für zwanzig Freunde | Keine verlässlichen Zahlen, keine Ernährungswünsche und keine Liste am Tag selbst |
| Dein eigenes Formular und ein Formular-Backend wie Ikiform | Deine Seite, dein Design, beliebige Felder. Gäste-Limits serverseitig geprüft, Spam gefiltert, Organisatoren per E-Mail informiert, CSV und Webhooks | Teilnehmer zählen, Anmeldung schließen und Bestätigungen senden machst du selbst oder per Webhook an ein E-Mail-Tool |
Für ein kostenpflichtiges Event nimm eine Ticketing-Plattform. Für ein kostenloses Event, bei dem Seite und Liste dir gehören sollen, ist der Rest dieser Anleitung die Einrichtung. Die Eventbrite-Seite vergleicht beides ausführlicher.
Felder, die Planung möglich machen
Frag nur, worauf du auch reagierst. Jede Frage hier ändert etwas, das du bestellst, buchst oder organisierst. Probier es aus: Die Vorschau führt den Validator von Ikiform in deinem Browser aus, so siehst du den 422, den ein vierter Gast bekäme.
Vorschau
Führt den Validator von Ikiform in deinem Browser aus. Es wird nichts gesendet.
| Feld | Warum | Beachte |
|---|---|---|
attending | Auch ein Nein ist eine Information: Du musst niemandem hinterherlaufen | Ein radio mit yes und no, damit die Zählung ein Filter ist |
guests | Die Zahl, für die du Essen und Stühle bestellst | Ein number mit min: 0, max: 3, integer: true. Ikiform lehnt 5 und 1.5 serverseitig ab |
dietary | Zahlen fürs Catering | Ein select lässt sich sauber summieren. Freitext nicht |
access_needs | Stufenloser Zugang, Sitzplätze, ein ruhiger Raum | Optional. Lies jede Angabe vor dem Event |
email | Um über Änderungen zu informieren und geänderte Antworten zuzuordnen | Außerdem die Reply-To-Adresse jeder Benachrichtigungs-E-Mail |
Das RSVP-Formular
Wähle deinen Stack. Jede Version bedankt sich für ein Ja anders als für ein Nein, blendet die Fragen zu Begleitpersonen bei einem Nein aus und sendet guests als 0, wenn jemand nicht kommt, damit deine Summen stimmen.
Füge das Formular in die Eventseite ein. Es sendet auf klassischem Weg und leitet auf deine Bestätigungsseite weiter, funktioniert also ohne JavaScript. Das kurze Skript blendet die Fragen zu Begleitpersonen aus, wenn jemand mit Nein antwortet, und setzt die Zahl der Begleitpersonen auf 0 zurück.
<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>Eine Server Action leitet die Zusage mit deinem Secret Key weiter und setzt guests bei einem Nein auf 0, egal was der Browser gesendet hat. Die Komponente ersetzt das Formular je nach Antwort durch eine andere Zeile.
"use server";
import { headers } from "next/headers";
export interface RsvpState {
status: "idle" | "going" | "declined" | "error";
message: string;
}
export async function sendRsvp(_previous: RsvpState, formData: FormData): Promise<RsvpState> {
const attending = formData.get("attending") === "no" ? "no" : "yes";
const origin = (await headers()).get("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify({
name: String(formData.get("name") ?? "").trim(),
email: String(formData.get("email") ?? "").trim(),
attending,
guests: attending === "yes" ? Number(formData.get("guests") || 0) : 0,
dietary: String(formData.get("dietary") ?? "none"),
access_needs: String(formData.get("access_needs") ?? ""),
_gotcha: String(formData.get("_gotcha") ?? ""),
}),
});
const result = await response.json().catch(() => null);
if (result?.ok) {
return attending === "yes"
? { status: "going", message: "You're registered. See you on 14 March at 6:30 pm." }
: { status: "declined", message: "Thanks for letting us know. We'll miss you." };
}
const fields: Record<string, string[]> = result?.error?.fields ?? {};
const [field, problems] = Object.entries(fields)[0] ?? [];
return {
status: "error",
message: field ? `${field}: ${problems?.[0]}` : (result?.error?.message ?? "Couldn't save your RSVP. Try again."),
};
}"use client";
import { useActionState, useState } from "react";
import { type RsvpState, sendRsvp } from "./actions";
const initial: RsvpState = { status: "idle", message: "" };
export function RsvpForm() {
const [state, action, pending] = useActionState(sendRsvp, initial);
const [attending, setAttending] = useState("yes");
if (state.status === "going" || state.status === "declined") return <p role="status">{state.message}</p>;
return (
<form action={action}>
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required maxLength={120} />
<label htmlFor="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" defaultChecked onChange={() => setAttending("yes")} /> Yes
</label>
<label>
<input type="radio" name="attending" value="no" onChange={() => setAttending("no")} /> No
</label>
</fieldset>
{attending === "yes" ? (
<>
<label htmlFor="guests">Guests you're bringing</label>
<input id="guests" name="guests" type="number" min={0} max={3} step={1} defaultValue={0} />
<label htmlFor="dietary">Dietary needs</label>
<select id="dietary" name="dietary" defaultValue="none">
<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 htmlFor="access_needs">Access needs or anything else</label>
<textarea id="access_needs" name="access_needs" rows={3} maxLength={1000} />
</>
) : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{state.status === "error" ? <p role="alert">{state.message}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Send RSVP"}</button>
</form>
);
}Eine Astro Action mit accept: "form" prüft die Antwort mit Zod, einschließlich des Gäste-Limits, bevor sie etwas weiterleitet. z.coerce.number() macht aus dem Text des Formulars eine Zahl. Die Seite wird bei Bedarf gerendert, also behalte prerender = false und einen Adapter bei.
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";
export const server = {
rsvp: defineAction({
accept: "form",
input: z.object({
name: z.string().trim().min(1).max(120),
email: z.email(),
attending: z.enum(["yes", "no"]),
guests: z.coerce.number().int().min(0).max(3).default(0),
dietary: z.enum(["none", "vegetarian", "vegan", "gluten_free", "other"]).default("none"),
access_needs: z.string().max(1000).default(""),
_gotcha: z.string().nullish(),
}),
handler: async (input, context) => {
const origin = context.request.headers.get("origin");
const body = { ...input, guests: input.attending === "yes" ? input.guests : 0 };
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify(body),
});
const result = await response.json();
if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
return { attending: input.attending };
},
}),
};---
import { actions, isInputError } from "astro:actions";
export const prerender = false;
const result = Astro.getActionResult(actions.rsvp);
const fieldErrors = result?.error && isInputError(result.error) ? result.error.fields : {};
---
{result?.data ? (
<p role="status">{result.data.attending === "yes" ? "You're registered. See you on 14 March at 6:30 pm." : "Thanks for letting us know. We'll miss you."}</p>
) : (
<form method="POST" action={actions.rsvp}>
<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" checked /> Yes</label>
<label><input type="radio" name="attending" value="no" /> No</label>
</fieldset>
<label for="guests">Guests you're bringing</label>
<input id="guests" name="guests" type="number" min="0" max="3" step="1" value="0" />
{fieldErrors.guests && <p role="alert">{fieldErrors.guests.join(" ")}</p>}
<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>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
{result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
<button>Send RSVP</button>
</form>
)}Eine POST-Serverfunktion prüft die Zahl der Begleitpersonen in .validator() und setzt sie bei einem Nein auf null. Die Komponente blendet die Fragen zu Begleitpersonen aus, bis jemand Ja sagt.
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";
const sendRsvp = createServerFn({ method: "POST" })
.validator((data: unknown) => {
if (!(data instanceof FormData)) throw new Error("Expected form data");
const attending = data.get("attending") === "no" ? "no" : "yes";
const guests = attending === "yes" ? Number(data.get("guests") || 0) : 0;
if (!Number.isInteger(guests) || guests < 0 || guests > 3) throw new Error("Bring up to 3 guests.");
return {
name: String(data.get("name") ?? "").trim(),
email: String(data.get("email") ?? "").trim(),
attending,
guests,
dietary: String(data.get("dietary") ?? "none"),
access_needs: String(data.get("access_needs") ?? ""),
_gotcha: String(data.get("_gotcha") ?? ""),
};
})
.handler(async ({ data }) => {
const origin = getRequestHeader("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!result.ok) return { done: false, message: String(result.error.message) };
return { done: true, message: data.attending === "yes" ? "You're registered. See you on 14 March at 6:30 pm." : "Thanks for letting us know. We'll miss you." };
});
export const Route = createFileRoute("/rsvp")({ component: Rsvp });
function Rsvp() {
const send = useServerFn(sendRsvp);
const [attending, setAttending] = useState("yes");
const [state, setState] = useState({ done: false, message: "" });
const [pending, setPending] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setPending(true);
try {
setState(await send({ data: new FormData(event.currentTarget) }));
} catch (error) {
setState({ done: false, message: error instanceof Error ? error.message : "Couldn't save your RSVP." });
} finally {
setPending(false);
}
}
if (state.done) return <p role="status">{state.message}</p>;
return (
<form method="post" onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required maxLength={120} />
<label htmlFor="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" defaultChecked onChange={() => setAttending("yes")} /> Yes
</label>
<label>
<input type="radio" name="attending" value="no" onChange={() => setAttending("no")} /> No
</label>
</fieldset>
{attending === "yes" ? (
<>
<label htmlFor="guests">Guests you're bringing</label>
<input id="guests" name="guests" type="number" min={0} max={3} step={1} defaultValue={0} />
<label htmlFor="dietary">Dietary needs</label>
<select id="dietary" name="dietary" defaultValue="none">
<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 htmlFor="access_needs">Access needs or anything else</label>
<textarea id="access_needs" name="access_needs" rows={3} maxLength={1000} />
</>
) : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{state.message ? <p role="alert">{state.message}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Send RSVP"}</button>
</form>
);
}Die action der Route leitet die Zusage weiter und gibt die Antwort zurück, sodass sich die Komponente für ein Ja anders bedanken kann als für ein Nein. data() behält bei Fehlern den Status von Ikiform bei.
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/rsvp";
export async function action({ request }: Route.ActionArgs) {
const form = await request.formData();
const attending = form.get("attending") === "no" ? "no" : "yes";
const origin = request.headers.get("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify({
name: String(form.get("name") ?? "").trim(),
email: String(form.get("email") ?? "").trim(),
attending,
guests: attending === "yes" ? Number(form.get("guests") || 0) : 0,
dietary: String(form.get("dietary") ?? "none"),
access_needs: String(form.get("access_needs") ?? ""),
_gotcha: String(form.get("_gotcha") ?? ""),
}),
});
const result = await response.json();
if (result.ok) return { attending, error: "" };
const fields: Record<string, string[]> = result.error.fields ?? {};
const first = Object.values(fields)[0]?.[0];
return data({ attending: null, error: String(first ?? result.error.message) }, { status: response.status });
}
export default function Rsvp({ actionData }: Route.ComponentProps) {
const sending = useNavigation().state === "submitting";
if (actionData?.attending === "yes") return <p role="status">You're registered. See you on 14 March at 6:30 pm.</p>;
if (actionData?.attending === "no") return <p role="status">Thanks for letting us know. We'll miss you.</p>;
return (
<Form method="post">
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required maxLength={120} />
<label htmlFor="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" defaultChecked /> Yes</label>
<label><input type="radio" name="attending" value="no" /> No</label>
</fieldset>
<label htmlFor="guests">Guests you're bringing</label>
<input id="guests" name="guests" type="number" min={0} max={3} step={1} defaultValue={0} />
<label htmlFor="dietary">Dietary needs</label>
<select id="dietary" name="dietary" defaultValue="none">
<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>
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{actionData?.error ? <p role="alert">{actionData.error}</p> : null}
<button disabled={sending}>{sending ? "Sending" : "Send RSVP"}</button>
</Form>
);
}Eine Standard-Form-Action prüft das Gäste-Limit mit fail(), bevor sie weiterleitet, und behält bei Fehlern den eingegebenen Namen und die E-Mail-Adresse. bind:group zeigt die Fragen zu Begleitpersonen nur bei einem Ja. Deklariere IKIFORM_SECRET_KEY in src/env.ts mit defineEnvVars, damit $app/env/private ihn lesen kann.
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";
export const actions = {
default: async ({ request, fetch, url }) => {
const form = await request.formData();
const attending = form.get("attending") === "no" ? "no" : "yes";
const guests = attending === "yes" ? Number(form.get("guests") || 0) : 0;
const name = String(form.get("name") ?? "").trim();
const email = String(form.get("email") ?? "").trim();
if (!Number.isInteger(guests) || guests < 0 || guests > 3) {
return fail(400, { name, email, message: "You can bring up to 3 guests." });
}
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
Origin: request.headers.get("origin") ?? url.origin,
},
body: JSON.stringify({
name,
email,
attending,
guests,
dietary: String(form.get("dietary") ?? "none"),
access_needs: String(form.get("access_needs") ?? ""),
_gotcha: String(form.get("_gotcha") ?? ""),
}),
});
const result = await response.json();
if (!result.ok) return fail(response.status, { name, email, message: String(result.error.message) });
return { attending };
},
} satisfies Actions;<script lang="ts">
import { enhance } from "$app/forms";
import type { PageProps } from "./$types";
let { form }: PageProps = $props();
let attending = $state("yes");
</script>
{#if form?.attending === "yes"}
<p role="status">You're registered. See you on 14 March at 6:30 pm.</p>
{:else if form?.attending === "no"}
<p role="status">Thanks for letting us know. We'll miss you.</p>
{:else}
<form method="POST" use:enhance>
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required maxlength="120" value={form?.name ?? ""} />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required value={form?.email ?? ""} />
<fieldset>
<legend>Can you make it?</legend>
<label><input type="radio" name="attending" value="yes" bind:group={attending} /> Yes</label>
<label><input type="radio" name="attending" value="no" bind:group={attending} /> No</label>
</fieldset>
{#if attending === "yes"}
<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>
{/if}
<label for="access_needs">Access needs or anything else</label>
<textarea id="access_needs" name="access_needs" rows="3" maxlength="1000"></textarea>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
{#if form?.message}<p role="alert">{form.message}</p>{/if}
<button>Send RSVP</button>
</form>
{/if}Eine Server-Route hält den Secret Key; setze NUXT_IKIFORM_SECRET_KEY und deklariere ikiformSecretKey unter runtimeConfig in nuxt.config.ts. Die Komponente sendet ein reaktives Objekt mit $fetch.
interface RsvpBody {
name?: string;
email?: string;
attending?: string;
guests?: number;
dietary?: string;
_gotcha?: string;
}
export default defineEventHandler(async (event) => {
const body = await readBody<RsvpBody>(event);
const config = useRuntimeConfig(event);
const attending = body.attending === "no" ? "no" : "yes";
const origin = getRequestHeader(event, "origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${config.ikiformSecretKey}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify({
name: body.name?.trim() ?? "",
email: body.email?.trim() ?? "",
attending,
guests: attending === "yes" ? Number(body.guests ?? 0) : 0,
dietary: body.dietary ?? "none",
_gotcha: body._gotcha ?? "",
}),
});
const result = await response.json();
if (!result.ok) {
throw createError({
status: response.status,
message: String(result.error.message),
});
}
return { attending };
});<script setup lang="ts">
const rsvp = reactive({ name: "", email: "", attending: "yes", guests: 0, dietary: "none", _gotcha: "" });
const answer = ref<"yes" | "no" | null>(null);
const error = ref("");
const sending = ref(false);
async function send() {
sending.value = true;
error.value = "";
try {
const result = await $fetch<{ attending: "yes" | "no" }>("/api/rsvp", { method: "POST", body: rsvp });
answer.value = result.attending;
} catch (failure) {
error.value = (failure as { data?: { message?: string } }).data?.message ?? "Couldn't save your RSVP.";
}
sending.value = false;
}
</script>
<template>
<p v-if="answer === 'yes'" role="status">You're registered. See you on 14 March at 6:30 pm.</p>
<p v-else-if="answer === 'no'" role="status">Thanks for letting us know. We'll miss you.</p>
<form v-else @submit.prevent="send">
<label for="name">Name</label>
<input id="name" v-model="rsvp.name" autocomplete="name" required maxlength="120" />
<label for="email">Email</label>
<input id="email" v-model="rsvp.email" type="email" autocomplete="email" required />
<fieldset>
<legend>Can you make it?</legend>
<label><input v-model="rsvp.attending" type="radio" value="yes" /> Yes</label>
<label><input v-model="rsvp.attending" type="radio" value="no" /> No</label>
</fieldset>
<template v-if="rsvp.attending === 'yes'">
<label for="guests">Guests you're bringing</label>
<input id="guests" v-model.number="rsvp.guests" type="number" min="0" max="3" step="1" />
<label for="dietary">Dietary needs</label>
<select id="dietary" v-model="rsvp.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</option>
</select>
</template>
<input v-model="rsvp._gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="sending">{{ sending ? "Sending" : "Send RSVP" }}</button>
</form>
</template>Eine Single-Page-App sendet direkt aus dem Browser. Leg hier niemals einen Secret Key ab; Allowed domains schützt das Formular. Ein Nein setzt guests vor dem Senden zurück, und die max-Regel von Ikiform fängt alles andere ab.
import { type FormEvent, useState } from "react";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
export function RsvpForm() {
const [attending, setAttending] = useState("yes");
const [answer, setAnswer] = useState<"yes" | "no" | null>(null);
const [error, setError] = useState("");
const [sending, setSending] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const body = new FormData(event.currentTarget);
if (body.get("attending") === "no") body.set("guests", "0");
setSending(true);
setError("");
try {
const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
const result = await response.json();
if (result.ok) setAnswer(body.get("attending") === "no" ? "no" : "yes");
else setError(result.error.fields?.guests?.[0] ?? result.error.message);
} catch {
setError("Couldn't reach the server. Try again.");
}
setSending(false);
}
if (answer === "yes") return <p role="status">You're registered. See you on 14 March at 6:30 pm.</p>;
if (answer === "no") return <p role="status">Thanks for letting us know. We'll miss you.</p>;
return (
<form action={ENDPOINT} method="post" onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required maxLength={120} />
<label htmlFor="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" defaultChecked onChange={() => setAttending("yes")} /> Yes
</label>
<label>
<input type="radio" name="attending" value="no" onChange={() => setAttending("no")} /> No
</label>
</fieldset>
<div hidden={attending === "no"}>
<label htmlFor="guests">Guests you're bringing</label>
<input id="guests" name="guests" type="number" min={0} max={3} step={1} defaultValue={0} />
<label htmlFor="dietary">Dietary needs</label>
<select id="dietary" name="dietary" defaultValue="none">
<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</option>
</select>
</div>
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{error ? <p role="alert">{error}</p> : null}
<button disabled={sending}>{sending ? "Sending" : "Send RSVP"}</button>
</form>
);
}Derselbe browserseitige Post in einer Vue-Single-File-Component, mit den Fragen zu Begleitpersonen hinter v-show.
<script setup lang="ts">
import { ref } from "vue";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const attending = ref("yes");
const answer = ref<"yes" | "no" | null>(null);
const error = ref("");
const sending = ref(false);
async function send(event: Event) {
const body = new FormData(event.target as HTMLFormElement);
if (attending.value === "no") body.set("guests", "0");
sending.value = true;
error.value = "";
try {
const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
const result = await response.json();
if (result.ok) answer.value = attending.value === "no" ? "no" : "yes";
else error.value = result.error.fields?.guests?.[0] ?? result.error.message;
} catch {
error.value = "Couldn't reach the server. Try again.";
}
sending.value = false;
}
</script>
<template>
<p v-if="answer === 'yes'" role="status">You're registered. See you on 14 March at 6:30 pm.</p>
<p v-else-if="answer === 'no'" role="status">Thanks for letting us know. We'll miss you.</p>
<form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
<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 v-model="attending" type="radio" name="attending" value="yes" /> Yes</label>
<label><input v-model="attending" type="radio" name="attending" value="no" /> No</label>
</fieldset>
<div v-show="attending === 'yes'">
<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</option>
</select>
</div>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="sending">{{ sending ? "Sending" : "Send RSVP" }}</button>
</form>
</template>IKIFORM_SECRET_KEY in reinen Server-Umgebungsvariablen. Die Server-Versionen reichen außerdem den Origin-Header des Browsers weiter, falls vorhanden, weil das eigene fetch eines Servers keinen sendet und Ikiform kostenlose localhost-Testeinsendungen daran erkennt.Die Framework-Anleitungen in der Dokumentation gehen tiefer: Next.js, Astro, SvelteKit, Nuxt, React, Vue und einfaches HTML. In einem Website-Baukasten fügst du die HTML-Version in einen Embed-Block ein.
Teilnehmer anhand der neuesten Antwort zählen
Menschen ändern ihre Meinung. Jemand sagt am Montag Ja mit zwei Begleitpersonen und am Donnerstag Nein; das sind zwei Einsendungen, und die zweite gilt. Die Regel lautet also: eine Antwort pro E-Mail-Adresse, die neueste, und dann eins für die Person plus ihre Begleitpersonen.
- Montag: Ada sagt zu, mit 2 Begleitpersonen. Würde man jede Einsendung addieren, kämen 3 Personen heraus.
- Mittwoch: Ben sagt zu, ohne Begleitpersonen.
- Donnerstag: Ada sendet das Formular erneut und sagt ab. Ikiform speichert das als zweite Einsendung von ihr.
- Nimmt man nur die neueste Einsendung jeder E-Mail-Adresse, hat Ada abgesagt, die Teilnehmerzahl ist also 1: Ben.
In einer Tabelle
- Wähle unter Submissions das RSVP-Formular, setze den Status auf Received und den Zeitraum auf All time, dann Export CSV.
- Importiere die Datei in Google Sheets und sortiere nach
created_at, neueste zuerst. - Markiere die Daten und nutze Data > Data cleanup > Remove duplicates auf der Spalte
email. Sheets behält die erste Zeile, die es sieht, und das ist jetzt die neueste. - Zähle, mit
attendingin Spalte G undguestsin Spalte H. Passe die Buchstaben an deinen Export an:
=COUNTIF(G2:G, "yes") + SUMIF(G2:G, "yes", H2:H)Der Export enthält zuerst id, created_at, form und status, danach eine Spalte pro Feld. Eine Datei fasst bis zu 100.000 Zeilen.
Mit einem Skript
Für eine Zahl, die du jederzeit neu berechnen kannst, lies die Zusagen über die REST API. Listen kommen neueste zuerst, also ist die erste Einsendung, die du pro E-Mail-Adresse siehst, die aktuellste. Führe das mit Node und einem geheimen API-Schlüssel aus:
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));Die Essens-Tabelle zählt Zusagen, nicht Personen, weil ein Formular nicht sagen kann, welcher Gast was isst. Wenn das Essen der Gäste wichtig ist, etwa bei einer Hochzeit, frag pro Person; die Seite zu Hochzeits-RSVPs zeigt, wie.
Anmeldung schließen, wenn du voll bist
Ikiform stoppt das Formular nicht bei einer bestimmten Zahl, also richte zwei Dinge ein: eine Warnung, bevor du voll bist, und eine Möglichkeit zu schließen. Schließe an zwei Stellen, im Formular und auf der Seite:
- Wähle in den Einstellungen des Formulars Pause form. Ab dann werden Posts mit
403 form_pausedabgelehnt, sodass ein alter Tab oder ein Bot nichts mehr hinzufügen kann. - Ersetze das Formular auf deiner Eventseite durch eine Zeile wie
Registration is closed. Email us to join the waitlist.Besucher sollten den Fehler nie zu sehen bekommen. - Wenn du wieder öffnest, wähle Resume form und setze das Formular zurück auf die Seite.
Die Warnung kann ein Webhook-Handler sein, der bei jedem Ja neu zählt und an einen Chat-Webhook postet, etwa den von Slack, sobald du 90 % der Kapazität überschreitest. Das hier ist ein Next.js-Route-Handler; dieselbe Logik läuft auf jedem Server:
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;
}
}- Er zählt jedes Mal über die API neu, sodass eine geänderte Antwort berücksichtigt wird. Jeder Schlüssel kann 100 API-Requests pro Minute stellen, was für mehrere tausend Zusagen pro Zählung reicht.
- Die Warnung sagt einem Menschen, dass er pausieren soll. Pausieren ist eine Aktion im Dashboard, also schließt nichts überraschend.
- Füge den Endpunkt unter Webhooks hinzu und kopiere sein Secret in
IKIFORM_WEBHOOK_SECRET. Ein Workspace kann 3 Endpunkte haben; die Webhook-Dokumentation behandelt Wiederholungsversuche.
- Solange die Anmeldung offen ist, wird jede Zusage an Ikiform gesendet.
- Dein Webhook-Handler zählt bei jedem Ja die neuesten Antworten neu und warnt dich bei 90 % der Kapazität.
- Du entscheidest, wann du voll bist, und wählst Pause form, das neue Posts mit 403 form_paused ablehnt.
- Du ersetzt das Formular auf der Seite durch einen Hinweis, dass die Anmeldung geschlossen ist, und eine Möglichkeit, sich auf die Warteliste zu setzen.
Bestätigungen ohne Bestätigungs-E-Mail
Ikiform schickt dem Gast keine E-Mail, also gib ihm auf der Dankeseite, was er braucht: Datum, Uhrzeit, Adresse und eine Kalenderdatei. Eine statische .ics-Datei neben der Seite funktioniert in jeder Kalender-App. Verlinke sie von der Bestätigungsseite aus als Add to calendar:
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:VCALENDARZeiten mit Z am Ende sind UTC, daher zeigen Kalender-Apps sie in der Zeitzone des jeweiligen Gastes an. Wenn du eine echte Bestätigungs-E-Mail willst, schick jede Zusage per Webhook an dein E-Mail-Tool, direkt oder über Zapier, Make oder n8n, und versende sie von dort. Dieselbe Liste bekommt dann auch die Erinnerung am Vortag.
Geänderte Antworten und Absagen
Sag den Leuten, wie sie ihre Antwort ändern: Plans changed? Send the form again with the same email. Deine Zählung nutzt bereits die neueste Antwort, mehr ist also nicht nötig. Benachrichtigungen zeigen jede Änderung, sobald sie passiert, was in der Woche des Events nützlich ist.
Zwei Personen im selben WLAN teilen sich eine IP-Adresse, und das Rate-Limit zählt pro IP. Das Standard-Preset mit 5 pro Minute und Besucher reicht für eine Eventseite. Setze kein Custom-Limit von einem Post pro Tag, um Wiederholungen zu verhindern: Das blockiert ein zweites Haushaltsmitglied im selben Netz, und die Regel der neuesten Antwort erledigt Wiederholungen ohnehin.
Am Tag selbst
Exportiere die Zusagen, entferne Duplikate wie oben, filtere auf attending = yes, sortiere nach Name und druck die Liste aus. Füge Spalten zum Abhaken und für erschienene Begleitpersonen hinzu. Eine gedruckte Liste funktioniert auch, wenn der Empfang am Veranstaltungsort schlecht ist. Behalte die Spalte zur Barrierefreiheit auf der Kopie für das Einlassteam und die Spalte zur Ernährung auf der für das Catering.
Spam erreicht deine Liste nicht: Honeypot, Spam-Bewertung und Rate-Limits filtern ihn, bevor er zählt, und Spam löst weder E-Mail noch Webhook aus. Schau vor dem Drucken einmal in den Filter Spam, falls eine echte Zusage mit einer ungewöhnlichen Nachricht dort gelandet ist. Wegwerfadressen erhöhen den Spam-Score um 2.
Wähle deinen Fall
Eine Hochzeit, ein kostenloses Community-Event und ein Helfer-Dienstplan zählen jeweils anders. Diese Seiten zeigen, was sich ändert.
- Hochzeits-RSVPEin RSVP-Formular für eine selbst gebaute Hochzeitswebsite: eine Antwort pro Einladung, ein Menü pro Gast, eine Frist und eine Liste, wer noch nicht geantwortet hat.
- Ohne EventbriteNimm Anmeldungen für ein kostenloses Community-Event auf deiner eigenen Site entgegen: Sessionwahl, Bestätigungsmail aus deinem eigenen Tool, Raumzahlen und Check-in-Liste.
- HelferanmeldungEin Anmeldeformular für Helfer, in dem man Schichten wählt, mit Telefonnummer für den Tag, und ein Skript, das unterbesetzte Schichten zeigt und den Dienstplan erstellt.
Prüfen, ob es funktioniert
Teste den ganzen Ablauf eine Woche, bevor du das Event ankündigst. Ikiform hat keinen kostenlosen Tarif. Testen ist kostenlos: Ein Workspace ohne Tarif nimmt 50 Testeinsendungen pro Monat von localhost an. Für den Livebetrieb wähle Pro ab $6/Monat für 500 Einsendungen oder zahle einmalig: 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt. Siehe Preise.
- Füge unter Allowed domains des Formulars die Domain deiner Site hinzu, plus
localhost, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen. - Sende ein Ja mit zwei Begleitpersonen und ein Nein von verschiedenen E-Mail-Adressen. Beide kommen an, und das Nein zeigt
guestsals0. - Sende drei Begleitpersonen, dann vier. Die vierte wird mit
Must be at most 3abgelehnt. - Prüfe das Postfach jedes Benachrichtigungsempfängers. Jeder bekommt eine E-Mail mit den Feldern, und Reply geht an den Absender, wenn das Formular ein
email-Feld hat. - Führe das Zählskript oder die Tabellenformel aus. Das Ergebnis sollte dem entsprechen, was du gesendet hast.
- Pausiere das Formular und sende noch einmal. Du bekommst
403 form_paused. Setze es danach fort. - Sende einen Post mit ausgefülltem Honeypot,
_gotcha=bot. Er bekommt trotzdem eine Erfolgsantwort, aber die Einsendung wird als Spam gespeichert und löst weder E-Mail noch Webhook aus. - Nichts angekommen? Stell den Statusfilter im Dashboard auf Spam. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.
Fehlerbehebung
| Problem | Was zu tun ist |
|---|---|
| Die Teilnehmerzahl ist zu hoch | Du addierst jede Einsendung. Behalte nur die neueste pro E-Mail-Adresse und prüfe, ob ein Nein guests als 0 sendet. |
403 form_paused auf der Live-Seite | Das Formular ist im Dashboard pausiert. Setze es fort oder ersetze das Formular auf der Seite durch einen Hinweis, dass geschlossen ist. |
422 validation_failed mit Bezug auf guests | Mehr Begleitpersonen als das max des Felds oder eine Dezimalzahl. Zeig die Meldung von Ikiform neben dem Feld an, wie es die Beispiele tun. |
403 origin_not_allowed, oder jeder Post landet im Spam | Die Domain der Seite steht nicht unter Allowed domains des Formulars. Füge sie hinzu, plus localhost für lokale Tests. Subdomains einer eingetragenen Domain sind erlaubt. |
429 rate_limited | Ein Besucher hat mehr gesendet, als das Rate-Limit des Formulars erlaubt. Standard erlaubt 5 pro Minute und Besucher. Warte Retry-After ab oder wähle Relaxed oder Custom für Formulare, die von einer gemeinsamen IP aus senden. |
| Die Einsendung ist im Dashboard, aber keine E-Mail und kein Webhook kam an | Sie wurde als Spam gespeichert. Spam löst nichts aus. Öffne sie unter dem Filter Spam, um die Gründe zu sehen, und markiere sie als kein Spam, um ihre E-Mail und Webhooks zu senden. |
| Der Browser landet auf der Dankeseite von Ikiform statt auf deiner | _redirect wird ignoriert, außer seine Domain steht unter Allowed domains und es ist eine https:-URL. Füge die Domain hinzu oder setze stattdessen Redirect after submit in den Einstellungen des Formulars. |
Fragen
Nicht automatisch. Es gibt keine Kapazitätseinstellung. Behalte die Teilnehmerzahl im Blick oder lass dich per Webhook warnen, und wähle dann Pause form in den Einstellungen des Formulars. Ein pausiertes Formular lehnt neue Posts mit 403 form_paused ab.
Nein. Ikiform hat keine Zahlungen, Tickets oder Check-in. Nutze eine Ticketing-Plattform für kostenpflichtige Events und ein Formular auf deiner eigenen Seite für kostenlose.
Mach die Begleitpersonen zu einem Zahlenfeld mit min 0, max und integer. Ikiform setzt diese Regeln serverseitig durch, daher wird ein Post mit zu vielen Gästen mit einem Feldfehler abgelehnt, den du anzeigen kannst.
Beide Einsendungen werden gespeichert. Zähle nur die neueste Einsendung pro E-Mail-Adresse, in einer Tabelle nach dem Entfernen der Duplikate oder in einem Skript, damit eine geänderte Antwort die alte ersetzt.
Nein. Ikiform schickt E-Mails an dich und dein Team, nicht an den Absender, es gibt also keinen Autoresponder und keine Bestätigungs-E-Mail. Sende einen Webhook an dein E-Mail-Tool und verschicke die Bestätigung von dort.
Nein, es gibt keinen kostenlosen Tarif. Testen ist kostenlos: Du kannst 50 Testeinsendungen pro Monat von localhost senden, bevor du zahlst. Für den Livebetrieb startet Pro bei $6/Monat für 500 Einsendungen, oder du zahlst einmalig 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.