# Formulários de inscrição e RSVP para eventos no seu próprio site

> Receba confirmações de presença na página do seu evento: formulário, limite de convidados, contagem honesta, encerramento ao lotar e lista de check-in, sem app de ingressos.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/event-registration

Coloque um formulário de RSVP na página do evento que envia para um backend de formulários, com resposta sim ou não, número de convidados limitado no servidor e os detalhes de que você precisa para planejar. Conte as pessoas a partir da resposta mais recente de cada e-mail, usando uma exportação CSV ou um script curto, e pause o formulário quando lotar. O Ikiform armazena e encaminha cada RSVP, mas não tem venda de ingressos, pagamentos nem limite automático de vagas; use uma plataforma de ingressos quando precisar disso.

A maioria dos eventos não precisa de uma plataforma de ingressos. Um meetup, um workshop, uma festa de lançamento, um clube de leitura ou um dia de conferência gratuito precisam de três coisas: uma página que diga o quê e quando, um formulário que registre quem vem e um número em que você possa confiar na véspera. Este guia monta isso no seu próprio site e deixa claro o que um formulário não consegue fazer.

## O caminho de um RSVP

O formulário fica na página do evento. Cada RSVP é enviado ao Ikiform, que o confere com as regras dos seus campos, filtra spam, armazena e avisa você. Dali ele chega a onde você planeja: o painel, uma planilha ou um script que conta as pessoas.

Diagrama: Como um RSVP sai da página do evento e chega à sua contagem

1. Um convidado preenche o formulário de RSVP na página do evento, e ele é enviado ao seu endpoint do Ikiform.
2. O Ikiform confere os campos, incluindo o limite de convidados, e dá uma pontuação de spam ao envio antes de armazená-lo.
3. Cada RSVP aparece no painel, onde você pode filtrá-lo e exportá-lo como CSV.
4. Os organizadores recebem um e-mail para cada RSVP, e o Reply vai direto para o convidado.
5. O Ikiform envia um webhook submission.created assinado.
6. Uma planilha ou um script curto transforma os RSVPs em uma contagem, usando a resposta mais recente de cada pessoa.

O Ikiform armazena as respostas; a contagem é uma soma que você faz sobre elas. Esse é o passo que um app de ingressos faria por você.

## O que o Ikiform não faz por eventos

Tenha isto claro antes de escolher. Um backend de formulários coleta respostas. Ele não é um sistema de ingressos:

- **Sem limite de vagas.** Não existe uma configuração que feche o formulário depois de um número de RSVPs ou de convidados. Você acompanha a contagem e pausa o formulário por conta própria.
- **Sem ingressos nem pagamentos.** Sem QR codes, sem app de check-in, sem categorias pagas, sem reembolsos.
- **Sem e-mail para o convidado.** O Ikiform não envia e-mail para quem preenche o formulário. Os e-mails de notificação vão só para você e sua equipe, então não há resposta automática, e-mail de confirmação nem double opt-in. Envie esses e-mails pela sua ferramenta de e-mail, disparados por um [webhook](https://ikiform.com/pt-br/docs/webhooks.md).
- **Sem lista de espera.** Quando você pausa o formulário, novos envios são recusados. Uma lista de espera seria um segundo formulário colocado no lugar dele.
- **Sem deduplicação.** O Ikiform não mescla nem rejeita envios repetidos pelo e-mail. Uma repetição idêntica em até 24 horas só aumenta a pontuação de spam. Remova os duplicados onde a lista fica, ou na hora de exportar.

Cada um desses pontos tem uma solução viável mais abaixo. Se vários deles forem decisivos para você, uma plataforma de ingressos é a ferramenta certa.

## Formas de receber RSVPs

| Abordagem | Bom em | Contras |
| --- | --- | --- |
| Plataforma de ingressos, como Eventbrite ou Luma | Ingressos, pagamentos, limite de vagas, listas de espera, e-mails de confirmação e check-in, tudo incluído | A página do evento e a lista de participantes ficam na plataforma deles, com a marca deles. Ingressos pagos têm taxas; confira as páginas de preços |
| Google Forms | Gratuito e rápido. Um botão para parar de aceitar respostas | Parece o Google, não o seu evento. Uma página separada e nenhum limite de convidados no servidor |
| Convite de calendário ou grupo de mensagens | Serve para vinte amigos | Nenhum número confiável, nada de restrições alimentares e nenhuma lista no dia |
| Seu próprio formulário e um backend de formulários, como o Ikiform | Sua página, seu design, quaisquer campos. Limite de convidados conferido no servidor, spam filtrado, organizadores avisados por e-mail, CSV e webhooks | Você conta as pessoas, encerra as inscrições e envia as confirmações por conta própria, ou com um webhook para uma ferramenta de e-mail |

Para um evento pago, use uma plataforma de ingressos. Para um evento gratuito em que a página e a lista devem continuar suas, o resto deste guia é a configuração. A [página sobre o Eventbrite](https://ikiform.com/pt-br/guides/event-registration/without-eventbrite.md) compara os dois com mais detalhes.

## Campos que tornam o planejamento possível

Pergunte só o que vai fazer você agir. Cada pergunta aqui muda algo que você encomenda, reserva ou organiza. Experimente: a prévia roda o validador do Ikiform no seu navegador, então você vê o `422` que um quarto convidado receberia.

Formulário: Meetup RSVP

- **Nome** (`name`, text, obrigatório)
- **E-mail** (`email`, email, obrigatório)
- **Você vai conseguir ir?** (`attending`, radio, obrigatório)
- **Convidados que você vai levar** (`guests`, number)
- **Restrições alimentares** (`dietary`, select)
- **Necessidades de acessibilidade ou qualquer outra coisa** (`access_needs`, textarea)

Cole esta definição na aba **Fields** do formulário e o Ikiform a aplica em todo envio, vindo de qualquer página. O [modelo de RSVP para eventos](https://ikiform.com/forms/event-rsvp) tem uma versão mais curta com código gerado.

| Campo | Por quê | Lembre-se |
| --- | --- | --- |
| `attending` | Um Não também é informação: evita que você fique cobrando as pessoas | Um `radio` com `yes` e `no`, para a contagem ser um filtro |
| `guests` | O número para o qual você encomenda comida e cadeiras | Um `number` com `min: 0`, `max: 3`, `integer: true`. O Ikiform recusa `5` e `1.5` no servidor |
| `dietary` | Contagem para o buffet | Um `select` soma de forma limpa. Texto livre não |
| `access_needs` | Acesso sem degraus, assentos, uma sala silenciosa | Opcional. Leia todas antes do dia |
| `email` | Avisar as pessoas sobre mudanças e casar respostas alteradas | Também é o reply-to de cada e-mail de notificação |

> Peça só o que o evento precisa. Respostas sobre alimentação e acessibilidade podem ser informações de saúde; diga no formulário por que você pergunta, guarde a exportação em um lugar privado e exclua os envios quando o evento terminar.

## O formulário de RSVP

Escolha sua stack. Cada versão agradece um Sim e um Não de forma diferente, esconde as perguntas sobre convidados para um Não e envia `guests` como `0` quando alguém não vem, para as suas somas continuarem certas.

**HTML**

Cole o formulário na página do evento. Ele envia do jeito tradicional e redireciona para a sua página de confirmação, então funciona sem JavaScript. O script curto esconde as perguntas sobre convidados quando alguém responde Não e volta o número de convidados para `0`.

`event.html`:

```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>
```

**Next.js**

Uma server action encaminha o RSVP com sua chave secreta e define `guests` como `0` para um Não, seja lá o que o navegador tenha enviado. O componente troca o formulário por uma frase diferente conforme a resposta.

`app/rsvp/actions.ts`:

```ts
"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."),
  };
}
```

`app/rsvp/rsvp-form.tsx`:

```tsx
"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>
  );
}
```

**Astro**

Uma Astro Action com `accept: "form"` confere a resposta com Zod, incluindo o limite de convidados, antes de encaminhar qualquer coisa. `z.coerce.number()` transforma o texto do formulário em número. A página é renderizada sob demanda, então mantenha `prerender = false` e um adapter.

`src/actions/index.ts`:

```ts
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 };
    },
  }),
};
```

`src/pages/rsvp.astro`:

```astro
---
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>
)}
```

**TanStack Start**

Uma server function POST confere o número de convidados em `.validator()` e o zera para um Não. O componente esconde as perguntas sobre convidados até alguém dizer Sim.

`src/routes/rsvp.tsx`:

```tsx
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>
  );
}
```

**React Router**

A `action` da rota encaminha o RSVP e devolve a resposta, para o componente agradecer um Sim e um Não de forma diferente. `data()` mantém o status do Ikiform nos erros.

`app/routes/rsvp.tsx`:

```tsx
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>
  );
}
```

**SvelteKit**

Uma form action padrão confere o limite de convidados com `fail()` antes de encaminhar e mantém o nome e o e-mail digitados nos erros. `bind:group` mostra as perguntas sobre convidados só para um Sim. Declare `IKIFORM_SECRET_KEY` em `src/env.ts` com `defineEnvVars` para que `$app/env/private` consiga lê-la.

`src/routes/rsvp/+page.server.ts`:

```ts
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;
```

`src/routes/rsvp/+page.svelte`:

```svelte
<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}
```

**Nuxt**

Uma server route guarda a chave secreta; defina `NUXT_IKIFORM_SECRET_KEY` e declare `ikiformSecretKey` em `runtimeConfig` no `nuxt.config.ts`. O componente envia um objeto reativo com `$fetch`.

`server/api/rsvp.post.ts`:

```ts
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 };
});
```

`app/components/RsvpForm.vue`:

```vue
<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>
```

**React**

Uma single-page app envia direto do navegador. Nunca coloque uma chave secreta aqui; **Allowed domains** (domínios permitidos) protege o formulário. Um Não zera `guests` antes do envio, e a regra `max` do Ikiform pega todo o resto.

`src/RsvpForm.tsx`:

```tsx
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>
  );
}
```

**Vue**

O mesmo envio pelo navegador em um single-file component do Vue, com as perguntas sobre convidados atrás de `v-show`.

`src/components/RsvpForm.vue`:

```vue
<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>
```

> As versões no servidor enviam com uma chave secreta, então o Ikiform pula **Allowed domains** e a verificação humana. Todo envio passa a vir do IP do seu servidor, e o limite Standard de 5 por minuto por visitante conta todos os seus visitantes juntos. Escolha o [limite de taxa](https://ikiform.com/pt-br/docs/rate-limits.md) Relaxed ou Custom para um formulário que você envia por um servidor, e mantenha `IKIFORM_SECRET_KEY` em variáveis de ambiente só do servidor. As versões no servidor também repassam o header `Origin` do navegador quando existe, porque o `fetch` do próprio servidor não envia nenhum e o Ikiform usa esse header para contar os envios de teste gratuitos de `localhost`.

Os guias de frameworks na documentação vão mais a fundo: [Next.js](https://ikiform.com/pt-br/docs/guides/nextjs.md), [Astro](https://ikiform.com/pt-br/docs/guides/astro.md), [SvelteKit](https://ikiform.com/pt-br/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/pt-br/docs/guides/nuxt.md), [React](https://ikiform.com/pt-br/docs/guides/react.md), [Vue](https://ikiform.com/pt-br/docs/guides/vue.md) e [HTML puro](https://ikiform.com/pt-br/docs/html.md). Em um criador de sites, a versão HTML vai colada em um bloco de embed.

## Conte as pessoas pela resposta mais recente

As pessoas mudam de ideia. Alguém diz Sim com dois convidados na segunda e Não na quinta; são dois envios, e o segundo é o que vale. Então a regra é: uma resposta por e-mail, a mais nova, e depois some um pela pessoa mais os convidados dela.

Diagrama: Por que a contagem usa o RSVP mais recente de cada pessoa

1. Segunda: Ada confirma presença com 2 convidados. Somar todos os envios contaria 3 pessoas.
2. Quarta: Ben confirma presença sem convidados.
3. Quinta: Ada envia o formulário de novo e diz não. O Ikiform armazena isso como um segundo envio dela.
4. Pegando só o envio mais novo de cada e-mail, Ada é um não, então a contagem é 1: Ben.

Somar todas as linhas contaria o grupo de três da Ada. Contar a linha mais nova por e-mail acerta.

### Em uma planilha

1. Em **Submissions**, escolha o formulário de RSVP, defina o status como **Received** e o período como **All time**, depois clique em **Export CSV**.
2. Importe no Google Sheets e ordene por `created_at`, do mais novo para o mais antigo.
3. Selecione os dados e use **Dados** > **Limpeza de dados** > **Remover cópias** na coluna `email`. O Sheets mantém a primeira linha que encontra, que agora é a mais nova.
4. Conte, com `attending` na coluna G e `guests` na coluna H. Ajuste as letras à sua exportação:

`Google Sheets`:

```text
=COUNTIF(G2:G, "yes") + SUMIF(G2:G, "yes", H2:H)
```

A exportação traz primeiro `id`, `created_at`, `form` e `status`, e depois uma coluna por campo. Um arquivo tem até 100.000 linhas.

### Com um script

Para ter um número que você pode recalcular a qualquer momento, leia os RSVPs pela [API REST](https://ikiform.com/pt-br/docs/api.md). As listas vêm da mais nova para a mais antiga, então o primeiro envio visto para cada e-mail é o mais recente. Rode isto com Node e uma chave de API secreta:

`headcount.ts`:

```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));
```

A tabela de refeições conta RSVPs, não pessoas, porque um único formulário não diz qual convidado come o quê. Se as refeições dos convidados importam, como em um casamento, pergunte por pessoa; a [página de RSVP de casamento](https://ikiform.com/pt-br/guides/event-registration/wedding-rsvp.md) mostra como.

## Encerrando as inscrições quando lotar

O Ikiform não para o formulário em um número, então prepare duas coisas: um alerta antes de lotar e um jeito de encerrar. Encerre em dois lugares, no formulário e na página:

1. Nas configurações do formulário, escolha **Pause form**. A partir daí, os envios são recusados com `403 form_paused`, então uma aba antiga ou um bot não conseguem adicionar mais ninguém.
2. Troque o formulário na página do evento por uma frase como `Registration is closed. Email us to join the waitlist.` Os visitantes nunca devem dar de cara com o erro.
3. Se reabrir, escolha **Resume form** e coloque o formulário de volta.

O alerta pode ser um handler de webhook que refaz a contagem a cada Sim e envia uma mensagem para um webhook de chat, como o do Slack, quando você passa de 90% da capacidade. Este é um route handler do Next.js; a mesma lógica roda em qualquer servidor:

`app/api/rsvp-alert/route.ts`:

```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;
  }
}
```

- Ele refaz a contagem pela API a cada vez, então uma resposta alterada é levada em conta. Cada chave pode fazer 100 requisições à API por minuto, o que cobre vários milhares de RSVPs por contagem.
- O alerta diz a uma pessoa que é hora de pausar. Pausar é uma ação no painel, então nada fecha de surpresa.
- Adicione o endpoint em **Webhooks** e copie o segredo dele para `IKIFORM_WEBHOOK_SECRET`. Um workspace pode ter 3 endpoints; a [documentação de webhooks](https://ikiform.com/pt-br/docs/webhooks.md) explica as novas tentativas.

Diagrama: Das inscrições abertas ao encerramento, sem nada automático

1. Enquanto as inscrições estão abertas, cada RSVP é enviado ao Ikiform.
2. Seu handler de webhook refaz a contagem das respostas mais recentes a cada Sim e avisa você ao atingir 90% da capacidade.
3. Você decide quando lotou e escolhe Pause form, que recusa novos envios com 403 form_paused.
4. Você troca o formulário na página por uma mensagem de encerramento e um jeito de entrar numa lista de espera.

Uma pessoa decide encerrar. Isso é mais lento que o limite rígido de um app de ingressos, e significa que uma mudança de planos de última hora nunca deixa de fora alguém que você teria aceitado.

## Confirmações sem e-mail de confirmação

O Ikiform não envia e-mail ao convidado, então dê a ele o que precisa na página de agradecimento: data, horário, endereço e um arquivo de calendário. Um arquivo `.ics` estático ao lado da página funciona em todo app de calendário. Coloque um link para ele na página de confirmação como `Add to calendar`:

`meetup.ics`:

```text
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
```

Horários terminados em `Z` estão em UTC, então os apps de calendário mostram cada um no fuso horário do próprio convidado. Se você quer um e-mail de confirmação de verdade, envie cada RSVP por webhook para sua ferramenta de e-mail, direto ou via [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md), [Make](https://ikiform.com/blog/form-submissions-to-make.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md), e mande o e-mail de lá. A mesma lista depois recebe o lembrete na véspera.

## Respostas alteradas e cancelamentos

Diga às pessoas como mudar a resposta: `Plans changed? Send the form again with the same email.` Sua contagem já usa a resposta mais nova, então não é preciso mais nada. As notificações mostram cada mudança no momento em que acontece, o que é útil na semana do evento.

Duas pessoas no mesmo Wi-Fi compartilham um endereço IP, e o limite de taxa conta por IP. O padrão Standard, de 5 por minuto por visitante, serve para uma página de evento. Não defina um limite Custom de um envio por dia para barrar repetições: isso bloqueia uma segunda pessoa da mesma casa na mesma rede, e a regra da resposta mais recente já cuida das repetições.

## No dia

Exporte os RSVPs, remova os duplicados como acima, filtre por `attending` = `yes`, ordene por nome e imprima. Adicione colunas para marcar presença e para os convidados que chegaram. Uma lista impressa funciona quando o sinal do local não funciona. Deixe a coluna de acessibilidade na cópia da equipe da porta e a coluna de alimentação na do buffet.

Spam não chega à sua lista: o honeypot, a pontuação de spam e os limites de taxa filtram antes da contagem, e spam não envia e-mail nem webhook. Confira o filtro **Spam** uma vez antes de imprimir, caso um RSVP real com uma mensagem incomum tenha ido parar lá. Endereços descartáveis somam 2 à pontuação de spam.

## Escolha o seu caso

Um casamento, um evento comunitário gratuito e uma escala de voluntários contam de formas diferentes. Estas páginas mostram o que muda.

- [RSVP de casamento](https://ikiform.com/pt-br/guides/event-registration/wedding-rsvp.md): Um formulário de RSVP para um site de casamento feito por você: uma resposta por convite, uma refeição por convidado, um prazo e a lista de quem não respondeu.
- [Sem Eventbrite](https://ikiform.com/pt-br/guides/event-registration/without-eventbrite.md): Faça as inscrições de um evento comunitário gratuito no seu próprio site: escolha de sessões, e-mail de confirmação pela sua ferramenta, contagem por sala e lista de check-in.
- [Inscrição de voluntários](https://ikiform.com/pt-br/guides/event-registration/volunteer-signup.md): Um formulário de inscrição de voluntários em que as pessoas escolhem turnos, com telefone para o dia, e um script que mostra os turnos desfalcados e monta a escala.

## Confira se funciona

Teste o ciclo completo uma semana antes de anunciar o evento. O Ikiform não tem plano gratuito. Testar é grátis: um workspace sem plano aceita 50 envios de teste por mês a partir de `localhost`. Para entrar no ar, escolha o Pro a partir de US$ 6/mês para 500 envios, ou pague uma vez: 10.000 envios por US$ 49 ou 50.000 envios por US$ 149, com créditos que nunca expiram. Veja os [preços](https://ikiform.com/pt-br/pricing).

1. Em **Allowed domains** do formulário, adicione o domínio do seu site, mais `localhost` enquanto desenvolve. Deixe vazio para aceitar envios de qualquer lugar.
2. Envie um Sim com dois convidados e um Não a partir de e-mails diferentes. Os dois chegam, e o Não mostra `guests` como `0`.
3. Envie três convidados, depois quatro. O quarto é recusado com `Must be at most 3`.
4. Confira a caixa de entrada de cada destinatário das notificações. Cada um recebe um e-mail com os campos, e **Reply** vai para quem enviou quando o formulário tem um campo `email`.
5. Rode o script de contagem ou a fórmula da planilha. O resultado deve bater com o que você enviou.
6. Pause o formulário e envie mais uma vez. Você recebe `403 form_paused`. Retome o formulário.
7. Envie com o honeypot preenchido, `_gotcha=bot`. Ele ainda recebe uma resposta de sucesso, mas o envio é armazenado como spam e não dispara e-mail nem webhook.
8. Não chegou nada? Mude o filtro de status do painel para **Spam**. Spam recebe a mesma resposta de sucesso de um envio real.

## Solução de problemas

| Problema | O que fazer |
| --- | --- |
| A contagem está alta demais | Você está somando todos os envios. Mantenha só o mais novo por e-mail e confira se um Não envia `guests` como `0`. |
| `403 form_paused` na página publicada | O formulário está pausado no painel. Retome-o ou troque o formulário na página por uma mensagem de encerramento. |
| `422 validation_failed` mencionando `guests` | Mais convidados que o `max` do campo, ou um número decimal. Mostre a mensagem do Ikiform ao lado do campo, como os exemplos fazem. |
| `403 origin_not_allowed`, ou todo envio cai no spam | O domínio da página não está em **Allowed domains** do formulário. Adicione-o, mais `localhost` para testes locais. Subdomínios de um domínio listado são permitidos. |
| `429 rate_limited` | Um visitante enviou mais do que o limite de taxa do formulário permite. O Standard permite 5 por minuto por visitante. Espere o `Retry-After`, ou escolha Relaxed ou Custom para formulários que enviam a partir de um único IP compartilhado. |
| O envio está no painel, mas nenhum e-mail ou webhook chegou | Ele foi armazenado como spam. Spam não envia nada. Abra-o no filtro **Spam** para ver os motivos e marque-o como não spam para enviar o e-mail e os webhooks dele. |
| O navegador cai na página de agradecimento do Ikiform em vez da sua | O `_redirect` é ignorado, a menos que o domínio dele esteja em **Allowed domains** e ele seja uma URL `https:`. Adicione o domínio, ou defina **Redirect after submit** nas configurações do formulário. |

## Perguntas

### O Ikiform consegue parar de aceitar RSVPs quando meu evento lotar?

Não automaticamente. Não existe configuração de capacidade. Acompanhe a contagem, ou crie um alerta a partir de um webhook, e então escolha Pause form nas configurações do formulário. Um formulário pausado recusa novos envios com 403 form_paused.

### Posso vender ingressos com um formulário do Ikiform?

Não. O Ikiform não tem pagamentos, ingressos nem check-in. Use uma plataforma de ingressos para eventos pagos e um formulário na sua própria página para os gratuitos.

### Como evito que as pessoas levem convidados demais?

Transforme os convidados em um campo numérico com min 0, max e integer definidos. O Ikiform aplica essas regras no servidor, então um envio com convidados demais é recusado com um erro de campo que você pode mostrar.

### E se alguém confirmar presença duas vezes?

Os dois envios são armazenados. Conte só o envio mais novo por e-mail, numa planilha depois de remover os duplicados ou num script, para que uma resposta alterada substitua a antiga.

### O Ikiform pode enviar e-mail para quem preencheu o formulário?

Não. O Ikiform envia e-mail para você e sua equipe, não para quem enviou, então não há resposta automática nem e-mail de confirmação. Envie um webhook para sua ferramenta de e-mail e mande a confirmação de lá.

### O Ikiform é gratuito?

Não, não há plano gratuito. Testar é grátis: você pode enviar 50 envios de teste por mês a partir de localhost antes de pagar. Para entrar no ar, o Pro começa em US$ 6/mês para 500 envios, ou você pode pagar uma vez 10.000 envios por US$ 49 ou 50.000 envios por US$ 149, com créditos que nunca expiram.

## Mais guias

- [RSVP de casamento](https://ikiform.com/pt-br/guides/event-registration/wedding-rsvp.md): Um formulário de RSVP para um site de casamento feito por você: uma resposta por convite, uma refeição por convidado, um prazo e a lista de quem não respondeu.
- [Sem Eventbrite](https://ikiform.com/pt-br/guides/event-registration/without-eventbrite.md): Faça as inscrições de um evento comunitário gratuito no seu próprio site: escolha de sessões, e-mail de confirmação pela sua ferramenta, contagem por sala e lista de check-in.
- [Inscrição de voluntários](https://ikiform.com/pt-br/guides/event-registration/volunteer-signup.md): Um formulário de inscrição de voluntários em que as pessoas escolhem turnos, com telefone para o dia, e um script que mostra os turnos desfalcados e monta a escala.
- [Verificar assinaturas de webhook](https://ikiform.com/pt-br/guides/webhook-signature-verification.md): Confira se um webhook de formulário veio mesmo do Ikiform: o esquema HMAC-SHA256 do Standard Webhooks, um vetor de teste, corpos brutos, timestamps e rotação de segredo.
- [Lista de espera para um novo SaaS](https://ikiform.com/pt-br/guides/waitlist.md): Monte uma lista de espera pré-lançamento na sua própria landing page: o formulário, proteção contra spam, duplicados, e-mails de confirmação e uma exportação limpa no dia do lançamento.
- [Formulário de contato em site estático](https://ikiform.com/pt-br/guides/contact-form-static-site.md): Coloque um formulário de contato funcionando num site estático sem servidor: para onde vai o POST, spam, respostas por e-mail e código para todo host e framework.
