# Formulário de newsletter sem o embed do Mailchimp

> Crie um formulário de newsletter no seu próprio markup, registre o consentimento, barre inscrições de bots e passe cada assinante para Kit, MailerLite, beehiiv ou qualquer ferramenta.

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

Escreva o formulário de inscrição no seu próprio HTML e aponte-o para um backend de formulários em vez de colar o código de embed do seu provedor de e-mail. O backend valida o endereço e a caixa de consentimento, filtra bots e envia um webhook assinado, e um pequeno relay adiciona o assinante pela API da sua ferramenta de e-mail. A ferramenta de e-mail então envia o e-mail de confirmação, a newsletter e os links de descadastro, então trocar de ferramenta depois significa mudar o relay, não cada formulário.

Toda ferramenta de e-mail oferece um embed: um bloco de markup gerado que você cola na sua página. Funciona, mas o formulário passa a pertencer à ferramenta. Ele envia direto para aquele provedor, carrega o markup do provedor e muitas vezes os estilos e scripts dele, e no dia em que você muda de ferramenta precisa caçar cada cópia dele pelo site. Este guia monta um formulário de inscrição que é seu, no markup e no framework que você já usa, e o conecta à ferramenta que envia a sua newsletter.

## Como a inscrição viaja

Há dois trabalhos aqui, e eles ficam em lugares diferentes. Coletar uma inscrição é trabalho de formulário: validar o endereço, conferir a caixa de consentimento, manter os bots fora, registrar quando aconteceu. Enviar uma newsletter é trabalho de e-mail: confirmar o endereço, enviar as edições, cuidar de descadastros e bounces. O Ikiform faz o primeiro. Sua ferramenta de e-mail faz o segundo. Um webhook assinado liga os dois.

Diagrama: Como uma inscrição na newsletter vai da sua página até a sua ferramenta de e-mail

1. Um leitor preenche o formulário de inscrição na sua página, e ele envia ao seu endpoint do Ikiform.
2. O Ikiform confere o endereço, a caixa de consentimento obrigatória, o honeypot, a pontuação de spam e o limite de taxa.
3. A inscrição aceita é armazenada no painel com data e hora, origem e referrer.
4. O Ikiform pode avisar você por e-mail a cada inscrição. Ele nunca envia e-mail ao leitor.
5. O Ikiform envia um webhook submission.created assinado.
6. Um relay verifica a assinatura e adiciona o leitor à sua ferramenta de e-mail, que envia a confirmação e, depois, cada edição.

O relay é a única parte que sabe qual ferramenta de e-mail você usa. O formulário, o filtro de spam e o registro armazenado não mudam quando a ferramenta muda.

## Quatro formas de coletar inscrições na newsletter

| Abordagem | Bom em | Contras |
| --- | --- | --- |
| O embed da ferramenta de e-mail, do Mailchimp, Kit, MailerLite ou beehiiv | Nada para rodar. Confirmação, tags e o e-mail de boas-vindas são configurados nas próprias opções da ferramenta. | O markup e o comportamento são da ferramenta. O Mailchimp avisa que seus formulários incorporados herdam os estilos do seu site, então podem ficar diferentes na sua página, e ligar o reCAPTCHA dele num formulário existente significa gerar e colar um código novo. Cada cópia fica presa a um provedor. |
| Sua própria rota no servidor que chama a API da ferramenta | Controle total do formulário e da requisição, com a chave de API guardada no seu servidor. | Você mesmo constrói a validação, o filtro de bots, os limites de taxa e o registro de consentimento, e precisa de um servidor mesmo que o site seja estático. |
| Um criador de formulários hospedado mais uma automação, como [Typeform](https://ikiform.com/alternatives/typeform) com Zapier | Nenhum código, e a ferramenta de automação conhece todo provedor de e-mail. | Um iframe que não combina com a sua página, mais duas assinaturas. Webhooks e marca dependem do plano. |
| Um backend de formulários, como o Ikiform, mais um relay | Seu próprio markup em qualquer site, filtro de spam, um registro de consentimento armazenado e webhooks assinados, sem servidor para o formulário. | O Ikiform não envia e-mail ao leitor. A confirmação e cada newsletter saem da sua ferramenta de e-mail, por um pequeno relay ou uma automação. |

Está feliz com um site, uma ferramenta e o embed dela? Mantenha. Construa o seu quando o formulário precisar combinar com o seu design, estiver em vários sites ou puder durar mais que a ferramenta.

## O formulário

Um bom formulário de newsletter é pequeno: um e-mail, um primeiro nome opcional para a saudação e uma caixa de consentimento cujo rótulo é a promessa que você está fazendo. Um input oculto `source` registra em que lugar do site a inscrição aconteceu. Escolha sua stack; a escolha vale para todos os exemplos de código destes guias.

**HTML**

Cole o formulário no rodapé, na barra lateral ou no fim de um post. Ele envia do jeito tradicional e cai na sua própria página de agradecimento, então funciona sem JavaScript.

`footer.html`:

```html
<form id="newsletter" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="nl-email">Email</label>
  <input id="nl-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" />

  <label for="nl-name">First name (optional)</label>
  <input id="nl-name" name="first_name" type="text" maxlength="80" autocomplete="given-name" />

  <label>
    <input name="consent" type="checkbox" value="true" required />
    Email me the newsletter. I can unsubscribe at any time.
  </label>

  <input type="hidden" name="source" value="footer" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />

  <button>Subscribe</button>
  <p id="nl-status" role="status" hidden></p>
</form>
```

Para manter o leitor no artigo, adicione este script depois do formulário. Ele envia com `fetch`, troca o formulário por uma frase pedindo para conferir a caixa de entrada e mostra a mensagem do servidor para o e-mail ou a caixa de seleção.

`newsletter.js`:

```js
const form = document.querySelector("#newsletter");
const status = document.querySelector("#nl-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const body = new FormData(form);
  button.disabled = true;
  status.hidden = false;
  status.textContent = "Subscribing";
  try {
    const response = await fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      form.replaceChildren(status);
      status.textContent = "Almost done. Open the email we sent you and confirm your address.";
      return;
    }
    const fields = result.error.fields ?? {};
    status.textContent = fields.email?.[0] ?? fields.consent?.[0] ?? result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

**Next.js**

Uma server action encaminha a inscrição com a sua chave secreta. Ela devolve erros por campo, então a mensagem do e-mail fica embaixo do input de e-mail e a do consentimento embaixo da caixa. O componente recebe uma prop `source`, então o rodapé e uma caixa dentro do artigo registram valores diferentes.

`app/newsletter/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface SignupState {
  done: boolean;
  errors: { email?: string; consent?: string; form?: string };
  email: string;
}

export async function subscribe(_previous: SignupState, formData: FormData): Promise<SignupState> {
  const email = String(formData.get("email") ?? "").trim();
  const consent = formData.get("consent") === "true";
  if (!consent) return { done: false, errors: { consent: "Tick the box so we know you want the emails." }, email };

  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: (await headers()).get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      first_name: String(formData.get("first_name") ?? "").trim() || undefined,
      consent,
      source: String(formData.get("source") ?? "footer"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { done: true, errors: {}, email };
  const fields = result?.error?.fields ?? {};
  return {
    done: false,
    errors: {
      email: fields.email?.[0],
      consent: fields.consent?.[0],
      form: fields.email || fields.consent ? undefined : (result?.error?.message ?? "Couldn't subscribe. Try again."),
    },
    email,
  };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type SignupState, subscribe } from "./actions";

const initial: SignupState = { done: false, errors: {}, email: "" };

export function NewsletterForm({ source = "footer" }: { source?: string }) {
  const [state, action, pending] = useActionState(subscribe, initial);

  if (state.done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form action={action} noValidate>
      <label htmlFor="nl-email">Email</label>
      <input
        id="nl-email"
        name="email"
        type="email"
        required
        autoComplete="email"
        defaultValue={state.email}
        aria-invalid={Boolean(state.errors.email)}
        aria-describedby="nl-email-error"
      />
      <p id="nl-email-error">{state.errors.email}</p>
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} autoComplete="given-name" />
      <label>
        <input name="consent" type="checkbox" value="true" aria-describedby="nl-consent-error" />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      <p id="nl-consent-error">{state.errors.consent}</p>
      <input type="hidden" name="source" value={source} />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.errors.form ? <p role="alert">{state.errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

Quer ir além com Next.js? Leia [Crie um formulário de inscrição em newsletter no Next.js](https://ikiform.com/pt-br/guides/newsletter-signup/nextjs.md).

**Astro**

Uma Astro Action com `accept: "form"` interpreta a caixa de seleção como booleano e o e-mail com `z.email()`. `isInputError` distingue um endereço inválido de um envio recusado. Form actions precisam de uma página sob demanda, então adicione um adapter e defina `export const prerender = false` em toda página que renderiza o componente; o export não faz nada dentro de um componente.

`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 = {
  subscribe: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      first_name: z.string().max(80).nullish(),
      consent: z.boolean(),
      source: z.string().default("footer"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      if (!input.consent) {
        throw new ActionError({ code: "BAD_REQUEST", message: "Tick the box so we know you want the emails." });
      }
      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({ ...input, first_name: input.first_name ?? undefined }),
      });
      const result = await response.json();
      if (!result.ok) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: result.error.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { subscribed: true };
    },
  }),
};
```

`src/components/NewsletterForm.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

const result = Astro.getActionResult(actions.subscribe);
const fields = isInputError(result?.error) ? result.error.fields : {};
---

{result?.data?.subscribed ? (
  <p role="status">Almost done. Open the email we sent you and confirm your address.</p>
) : (
  <form method="POST" action={actions.subscribe}>
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    {fields.email && <p>Enter a valid email address.</p>}
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
    <button>Subscribe</button>
  </form>
)}
```

Quer ir além com Astro? Leia [Crie um formulário de inscrição em newsletter no Astro](https://ikiform.com/pt-br/guides/newsletter-signup/astro.md).

**TanStack Start**

Uma função POST `createServerFn` transforma o `FormData` em JSON em `.validator()` e devolve os erros de campo como dados em vez de lançar exceção, para que o componente posicione cada mensagem ao lado do seu input.

`src/components/newsletter-form.tsx`:

```tsx
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

type Errors = { email?: string; consent?: string; form?: string };

const subscribe = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      email: String(data.get("email") ?? "").trim(),
      first_name: String(data.get("first_name") ?? "").trim() || undefined,
      consent: data.get("consent") === "true",
      source: String(data.get("source") ?? "footer"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }): Promise<{ ok: boolean; errors: Errors }> => {
    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 { ok: true, errors: {} };
    const fields = result.error.fields ?? {};
    return {
      ok: false,
      errors: { email: fields.email?.[0], consent: fields.consent?.[0], form: result.error.message },
    };
  });

export function NewsletterForm() {
  const send = useServerFn(subscribe);
  const [done, setDone] = useState(false);
  const [errors, setErrors] = useState<Errors>({});
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      const result = await send({ data: new FormData(event.currentTarget) });
      setDone(result.ok);
      setErrors(result.errors);
    } catch {
      setErrors({ form: "Couldn't subscribe. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {errors.email ? <p>{errors.email}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {errors.consent ? <p>{errors.consent}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form && !errors.email && !errors.consent ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

Quer ir além com TanStack Start? Leia [Crie um formulário de inscrição em newsletter no TanStack Start](https://ikiform.com/pt-br/guides/newsletter-signup/tanstack-start.md).

**React Router**

Um formulário no rodapé não deve tirar a pessoa da página que ela está lendo. Coloque a action numa resource route em `/newsletter` e envie para ela com `useFetcher`, que envia sem navegação e mantém o resultado na página atual.

`app/routes/newsletter.tsx`:

```ts
import { data } from "react-router";
import type { Route } from "./+types/newsletter";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  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: request.headers.get("origin") ?? "",
    },
    body: JSON.stringify({
      email: String(formData.get("email") ?? "").trim(),
      first_name: String(formData.get("first_name") ?? "").trim() || undefined,
      consent: formData.get("consent") === "true",
      source: String(formData.get("source") ?? "footer"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true as const };
  return data(
    { ok: false as const, fields: (result.error.fields ?? {}) as Record<string, string[]>, message: String(result.error.message) },
    { status: response.status },
  );
}
```

`app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [index("routes/home.tsx"), route("newsletter", "routes/newsletter.tsx")] satisfies RouteConfig;
```

`app/components/newsletter-form.tsx`:

```tsx
import { useFetcher } from "react-router";
import type { action } from "~/routes/newsletter";

export function NewsletterForm() {
  const fetcher = useFetcher<typeof action>();
  const result = fetcher.data;
  const busy = fetcher.state !== "idle";

  if (result?.ok) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <fetcher.Form method="post" action="/newsletter">
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {result?.fields.email ? <p>{result.fields.email[0]}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {result?.fields.consent ? <p>{result.fields.consent[0]}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {result && !result.fields.email && !result.fields.consent ? <p role="alert">{result.message}</p> : null}
      <button disabled={busy}>{busy ? "Subscribing" : "Subscribe"}</button>
    </fetcher.Form>
  );
}
```

Quer ir além com React Router? Leia [Crie um formulário de inscrição em newsletter no React Router](https://ikiform.com/pt-br/guides/newsletter-signup/react-router.md).

**SvelteKit**

Actions ficam num `+page.server.ts`, nunca num layout, então a action fica numa página `/newsletter` e o rodapé envia para `/newsletter?/subscribe`. O callback personalizado de `use:enhance` trata o resultado por conta própria em vez de chamar `update()`, então o leitor continua no artigo. Declare `IKIFORM_SECRET_KEY` em `src/env.ts` com `defineEnvVars` para que `$app/env/private` consiga lê-la.

`src/routes/newsletter/+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 = {
  subscribe: async ({ request, fetch, url }) => {
    const form = await request.formData();
    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({
        email: String(form.get("email") ?? "").trim(),
        first_name: String(form.get("first_name") ?? "").trim() || undefined,
        consent: form.get("consent") === "true",
        source: String(form.get("source") ?? "footer"),
        _gotcha: String(form.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields = result.error.fields ?? {};
      return fail(response.status, { message: String(fields.email?.[0] ?? fields.consent?.[0] ?? result.error.message) });
    }
    return { subscribed: true };
  },
} satisfies Actions;
```

`src/lib/NewsletterForm.svelte`:

```svelte
<script lang="ts">
  import { applyAction, enhance } from "$app/forms";

  let done = $state(false);
  let message = $state("");
  let pending = $state(false);
</script>

{#if done}
  <p role="status">Almost done. Open the email we sent you and confirm your address.</p>
{:else}
  <form
    method="POST"
    action="/newsletter?/subscribe"
    use:enhance={() => {
      pending = true;
      return async ({ result }) => {
        pending = false;
        if (result.type === "success") done = true;
        else if (result.type === "failure") message = String(result.data?.message ?? "Couldn't subscribe.");
        else await applyAction(result);
      };
    }}
  >
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" value="true" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if message}
      <p role="alert">{message}</p>
    {/if}
    <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
  </form>
{/if}
```

Quer ir além com SvelteKit? Leia [Crie um formulário de inscrição em newsletter no SvelteKit](https://ikiform.com/pt-br/guides/newsletter-signup/sveltekit.md).

**Nuxt**

Uma server route mantém a chave secreta longe do navegador e devolve os erros de campo do Ikiform no `data` do erro. Defina `NUXT_IKIFORM_SECRET_KEY` e declare `ikiformSecretKey` em `runtimeConfig` no `nuxt.config.ts`.

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

```ts
interface Signup {
  email?: string;
  first_name?: string;
  consent?: boolean;
  source?: string;
  _gotcha?: string;
}

export default defineEventHandler(async (event) => {
  const body = await readBody<Signup>(event);
  const config = useRuntimeConfig(event);
  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({
      email: body.email?.trim() ?? "",
      first_name: body.first_name?.trim() || undefined,
      consent: body.consent === true,
      source: body.source ?? "footer",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.message),
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { subscribed: true };
});
```

`app/components/NewsletterForm.vue`:

```vue
<script setup lang="ts">
const email = ref("");
const firstName = ref("");
const consent = ref(false);
const trap = ref("");
const status = ref<"idle" | "sending" | "done">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function subscribe() {
  status.value = "sending";
  errors.value = {};
  message.value = "";
  try {
    await $fetch("/api/newsletter", {
      method: "POST",
      body: { email: email.value, first_name: firstName.value, consent: consent.value, source: "footer", _gotcha: trap.value },
    });
    status.value = "done";
  } catch (failure) {
    const data = (failure as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    errors.value = data?.data?.fields ?? {};
    message.value = data?.message ?? "Couldn't subscribe. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'done'" role="status">Almost done. Open the email we sent you and confirm your address.</p>
  <form v-else @submit.prevent="subscribe">
    <label for="nl-email">Email</label>
    <input id="nl-email" v-model="email" name="email" type="email" required autocomplete="email" />
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" v-model="firstName" name="first_name" type="text" maxlength="80" />
    <label>
      <input v-model="consent" name="consent" type="checkbox" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <p v-if="errors.consent">{{ errors.consent[0] }}</p>
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="message && !errors.email && !errors.consent" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Subscribing" : "Subscribe" }}</button>
  </form>
</template>
```

Quer ir além com Nuxt? Leia [Crie um formulário de inscrição em newsletter no Nuxt](https://ikiform.com/pt-br/guides/newsletter-signup/nuxt.md).

**React**

Uma single-page app envia pelo navegador. `submit()` do `@ikiform/sdk` envia o `FormData` e sempre resolve com `ok` e, em caso de falha, os erros de campo, então não há bloco `try` para escrever. Nunca coloque uma chave secreta aqui; quem protege o formulário é **Allowed domains**.

`src/NewsletterForm.tsx`:

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, useState } from "react";

type FieldErrors = Record<string, string[]>;

export function NewsletterForm() {
  const [done, setDone] = useState(false);
  const [errors, setErrors] = useState<FieldErrors>({});
  const [message, setMessage] = useState("");
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    setErrors({});
    setMessage("");
    const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.currentTarget));
    setPending(false);
    if (result.ok) {
      setDone(true);
      return;
    }
    setErrors(result.error.fields ?? {});
    setMessage(result.error.message);
  }

  if (done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" onSubmit={onSubmit}>
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {errors.email ? <p>{errors.email[0]}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {errors.consent ? <p>{errors.consent[0]}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {message && !errors.email && !errors.consent ? <p role="alert">{message}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

**Vue**

A mesma chamada `submit()` num single-file component do Vue. O formulário mantém o `action`, então ainda envia se o script não carregar.

`src/components/NewsletterForm.vue`:

```vue
<script setup lang="ts">
import { submit } from "@ikiform/sdk";
import { ref } from "vue";

const status = ref<"idle" | "sending" | "done">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function subscribe(event: SubmitEvent) {
  status.value = "sending";
  const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.target as HTMLFormElement));
  if (result.ok) {
    status.value = "done";
    return;
  }
  errors.value = result.error.fields ?? {};
  message.value = result.error.message;
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'done'" role="status">Almost done. Open the email we sent you and confirm your address.</p>
  <form v-else action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" @submit.prevent="subscribe">
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" value="true" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <p v-if="errors.consent">{{ errors.consent[0] }}</p>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="message && !errors.email && !errors.consent" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Subscribing" : "Subscribe" }}</button>
  </form>
</template>
```

- Troque `frm_xxxxxxxxxxxxxxxx` pelo id do seu formulário, que está no painel.
- Mantenha o name do input de e-mail como `email`. O Ikiform o valida, coloca o domínio em minúsculas e o confere contra domínios de e-mail descartáveis.
- O input oculto `_gotcha` é o honeypot. Pessoas nunca o veem, e bots que preenchem todo input se marcam como spam.
- A caixa de seleção envia `true` quando marcada. Desmarcada, não envia nada, e uma caixa obrigatória é recusada com `This box must be checked`.
- A frase de sucesso pede para conferir a caixa de entrada, porque sua ferramenta de e-mail envia uma confirmação. Se você desligou o double opt-in, troque por um simples obrigado.
- As versões no servidor repassam o header `Origin` do navegador, porque o Ikiform reconhece por ele os testes gratuitos em `localhost` e o `fetch` do próprio servidor não envia nenhum.

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

A configuração de cada framework está na documentação: [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 simples](https://ikiform.com/pt-br/docs/html.md). O [modelo de formulário de newsletter](https://ikiform.com/forms/newsletter-signup) gera os mesmos campos em HTML, React e uma server action do Next.js.

## Campos e a definição

Cole a definição abaixo na aba **Fields** do formulário e o Ikiform a aplica no servidor, não importa o que a página envie. A prévia roda o mesmo validador no seu navegador: deixe a caixa desmarcada e envie para ver o erro que um bot ou uma página quebrada receberia.

Formulário: Newsletter

- **E-mail** (`email`, email, obrigatório)
- **Primeiro nome** (`first_name`, text)
- **Quero receber a newsletter por e-mail. Posso cancelar a inscrição a qualquer momento.** (`consent`, checkbox, obrigatório)
- **Local da inscrição** (`source`, text)

Na sua página, `source` é um input oculto. Aqui ele é um campo de texto visível só para você poder testar.

| Campo | Por quê | Lembre-se |
| --- | --- | --- |
| `email` | O assinante | Obrigatório. `type="email"` abre o teclado certo no celular |
| `first_name` | Uma saudação em cada edição | Opcional. A maioria das ferramentas de e-mail tem um campo de primeiro nome para mapeá-lo |
| `consent` | Um registro de que a pessoa pediu os e-mails | Caixa de seleção obrigatória. Armazenada como `true` |
| `source` | Qual formulário do site funcionou: rodapé, post, popup | Oculto. Use um valor diferente por posição e repasse-o como tag ou campo |

Com os campos definidos, o Ikiform recusa qualquer input que não conhece com `unknown_field`, então adicione o campo à definição antes de adicionar o input à página. Nomes que começam com `_`, como `_redirect`, são instruções para o Ikiform e nunca contam como campos.

## Registrando o consentimento

Uma newsletter é um e-mail que alguém pediu. A caixa de seleção deixa o pedido explícito, e o envio armazenado o registra. É isto que o Ikiform guarda para cada inscrição aceita:

`data.fields`:

```json
{
  "email": "ada@example.com",
  "first_name": "Ada",
  "consent": true,
  "source": "footer"
}
```

Além dos campos, cada envio tem o timestamp `created_at`, a origem e o referrer da página, o país e o user agent do navegador. Juntos, mostram quando e onde a pessoa marcou a caixa. Dois hábitos tornam esse registro útil depois:

1. **Escreva o rótulo como a promessa.** "Quero receber a newsletter por e-mail. Posso cancelar a inscrição a qualquer momento." diz o que a pessoa recebe. Se você também envia ofertas, diga isso no rótulo.
2. **Anote a data quando o texto mudar.** Um `true` armazenado diz que a pessoa concordou, não com qual texto. Mantenha um pequeno histórico do rótulo com datas, para que cada inscrição possa ser ligada às palavras que viu.

O Ikiform guarda o registro; ele não sabe dizer o que as regras do lugar onde você e seus leitores vivem exigem. Confira isso com alguém que as conheça.

## Os e-mails de confirmação saem da sua ferramenta de e-mail

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). Essa divisão é proposital. A ferramenta que envia sua newsletter já registra quem confirmou, quem se descadastrou e quais endereços dão bounce. Se um segundo sistema enviasse as confirmações, esses registros sairiam de sincronia.

Por isso o relay adiciona o leitor num estado que faz a ferramenta de e-mail enviar a própria confirmação, e a ferramenta o marca como ativo quando ele clica. Cada ferramenta liga isso de um jeito: uma opção do formulário, uma chave da conta ou uma flag na requisição da API. O [guia de double opt-in](https://ikiform.com/pt-br/guides/newsletter-signup/double-opt-in.md) cobre os três, e o que acontece quando um assinante conhecido se inscreve de novo.

## O relay

Um relay é uma função curta que recebe o webhook do Ikiform, confere a assinatura e chama a API da sua ferramenta de e-mail. Ele precisa de uma URL HTTPS pública e de um segredo de cada lado. Este é um route handler do Next.js; o mesmo código roda em qualquer runtime com `fetch`.

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

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { addSubscriber } from "@/lib/email-tool";

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const fields = event.data.fields;
  if (fields.consent !== true) return new Response("No consent, skipped", { status: 200 });

  const answer = await addSubscriber({
    email: String(fields.email),
    firstName: typeof fields.first_name === "string" ? fields.first_name : undefined,
    source: typeof fields.source === "string" ? fields.source : "unknown",
    referrer: typeof event.data.meta.referrer === "string" ? event.data.meta.referrer : undefined,
  });
  if (answer.status === 429) {
    return new Response("Email tool rate limit", {
      status: 503,
      headers: { "retry-after": answer.headers.get("retry-after") ?? "60" },
    });
  }
  if (!answer.ok) return new Response(`Email tool answered ${answer.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` confere a assinatura HMAC e o timestamp. Sem ele, qualquer um que descubra a URL pode adicionar endereços à sua lista.
- Eventos `endpoint.test`, do **Send test** no painel, são respondidos com `204` e ignorados.
- A checagem de consentimento é uma segunda trava atrás da definição do formulário.
- `addSubscriber` é a única parte que depende da ferramenta. O corpo dela para cada ferramenta está nas páginas de [Kit](https://ikiform.com/pt-br/guides/newsletter-signup/kit.md), [MailerLite](https://ikiform.com/pt-br/guides/newsletter-signup/mailerlite.md) e [beehiiv](https://ikiform.com/pt-br/guides/newsletter-signup/beehiiv.md).

O que o relay responde decide o que o Ikiform faz em seguida:

Diagrama: O que o relay responde ao Ikiform para cada resposta da ferramenta de e-mail

1. A ferramenta de e-mail adiciona ou atualiza o assinante: o relay responde 204 e a entrega é marcada como bem-sucedida.
2. A ferramenta de e-mail está com limite de taxa: o relay responde 503 com Retry-After, e o Ikiform espera esse tempo antes da próxima tentativa.
3. A ferramenta de e-mail recusa a requisição ou está fora do ar: o relay responde 502, e o Ikiform tenta de novo no seu cronograma com o mesmo webhook-id.

Cada tentativa fica no log de entregas do endpoint com a resposta do seu relay, então um 502 que cita o status da ferramenta diz o que corrigir.

Adicione a URL do relay em **Webhooks** no painel, escolha o formulário da newsletter e copie o segredo `whsec_` para `IKIFORM_WEBHOOK_SECRET`. Um workspace tem até 3 endpoints. A URL precisa ser HTTPS pública na porta 443; hostnames `workers.dev` e `pages.dev` são recusados, então dê um domínio personalizado ao Worker. Tentativas, payloads e o esquema de assinatura estão na [documentação de webhooks](https://ikiform.com/pt-br/docs/webhooks.md), e [envios de formulário para o seu próprio servidor](https://ikiform.com/blog/form-submissions-to-your-own-server.md) tem uma versão que armazena cada entrega uma única vez.

> Sem código? Aponte o webhook para uma ferramenta de automação e use a ação dela para a sua ferramenta de e-mail em vez de um relay. Veja [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). Não há integrações de um clique. Toda ferramenta é alcançada por um [webhook](https://ikiform.com/pt-br/docs/webhooks.md) assinado, diretamente ou por uma ferramenta de automação como [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).

## Mantenha bots e list bombing longe

Inscrições de bots prejudicam mais uma newsletter do que a maioria dos formulários. Um endereço real inscrito por um bot recebe e-mails que nunca pediu, e reclamações dessas pessoas prejudicam o domínio de onde você envia. Alguns bots inscrevem o endereço de uma vítima em centenas de formulários de uma vez para soterrar a caixa de entrada dela, e todo formulário com e-mail de confirmação participa disso.

| Defesa | O que ela barra | Configuração |
| --- | --- | --- |
| Honeypot | Bots que preenchem todos os inputs | O input oculto `_gotcha` |
| Consentimento obrigatório | Scripts que enviam só um e-mail | A caixa de seleção `consent` com `required: true` na definição |
| Pontuação de spam | Endereços descartáveis (+2), repetições idênticas (+3), links e frases de spam. Com 5, a inscrição é armazenada como spam | Nenhuma |
| Limites de taxa | Um visitante enviando em loop | Ligado por padrão: Standard, 5 por minuto por visitante |
| Verificação humana | Bots que rodam um navegador de verdade | Defina **Spam check** como **Ikiform** para usar o Cloudflare Turnstile sem chaves para gerenciar |
| Domínios permitidos | O endpoint do seu formulário copiado em outros sites | Liste todo domínio em que o formulário aparece |

Spam recebe a mesma resposta de sucesso que uma inscrição real e não envia webhook, então nunca chega à sua ferramenta de e-mail nem dispara um e-mail de confirmação. Se um leitor real cair em spam, marque como não spam e o webhook dele sai nesse momento. A [documentação de spam](https://ikiform.com/pt-br/docs/spam.md) explica cada checagem e sua pontuação.

> Um limite por visitante rígido combina com um formulário de newsletter, porque a pessoa se inscreve uma vez. Se o seu público lê de escritórios ou campi que compartilham um IP, o Standard os conta juntos; ali, Relaxed, com 20 por minuto por visitante, é a escolha mais segura. Veja [limites de taxa](https://ikiform.com/pt-br/docs/rate-limits.md).

## Um formulário em muitas páginas

Um endpoint pode ficar no rodapé de toda página, no fim de cada post e numa página `/newsletter`. Dê a cada posição seu próprio valor de `source` para saber qual traz inscrições:

- `footer`: visto em toda página. Deixe só o e-mail e a caixa de seleção.
- `post`: leitores que terminaram um artigo, muitas vezes os que mais convertem.
- `newsletter-page`: para onde os perfis sociais apontam. Espaço para uma edição de exemplo.

Cada envio também registra o referrer, que é a página exata em que o leitor estava, então `source` diz a posição e o referrer diz o artigo. Adicione cada domínio que mostra o formulário em **Allowed domains**, até 20 por formulário; subdomínios de um domínio listado são permitidos automaticamente.

### O mesmo leitor duas vezes

O Ikiform não mescla nem rejeita envios repetidos por e-mail. Uma repetição idêntica dentro de 24 horas só soma à pontuação de spam. Remova duplicados onde a lista mora, ou na exportação. Para uma newsletter isso raramente é problema: Kit, MailerLite e beehiiv identificam os assinantes pelo e-mail, então uma repetição atualiza um contato em vez de criar um segundo.

## Trocar de ferramenta de e-mail depois

É aqui que ser dono do formulário compensa. Mudar de uma ferramenta de e-mail para outra normalmente significa trocar cada embed do site. Aqui o formulário não sabe qual ferramenta está por trás dele:

Diagrama: O que muda quando você passa para outra ferramenta de e-mail

1. Com um embed, toda página carrega código que envia direto para a ferramenta antiga, então mudar significa trocar cada cópia.
2. Com o seu próprio formulário, toda página continua enviando ao Ikiform, e o Ikiform continua enviando o mesmo webhook.
3. Só a função addSubscriber do relay muda, para chamar a API da nova ferramenta.

Se quiser, rode as duas ferramentas por uma semana: dois endpoints de webhook, dois relays, um formulário. Depois remova o endpoint antigo.

Migre os assinantes existentes com a exportação da própria ferramenta antiga, já que só ela sabe quem confirmou e quem saiu. A exportação CSV do Ikiform, de até 100.000 linhas por arquivo, serve para conferir os números, não para importar.

## Escolha o seu caso

O formulário acima é o mesmo em todo lugar. Estas páginas cobrem e-mails de confirmação, sites sem servidor e as chamadas exatas de API para três ferramentas de e-mail.

- [Double opt-in](https://ikiform.com/pt-br/guides/newsletter-signup/double-opt-in.md): O Ikiform não envia e-mails de confirmação, então o double opt-in roda na sua ferramenta de e-mail: um relay adiciona cada inscrição como não confirmada e a ferramenta envia o link.
- [Site estático](https://ikiform.com/pt-br/guides/newsletter-signup/static-site.md): Adicione um formulário de newsletter a um site estático em Hugo, Jekyll, Eleventy ou Astro, mantenha a chave de API da ferramenta de e-mail fora da página e confirme na ferramenta.
- [Kit (ConvertKit)](https://ikiform.com/pt-br/guides/newsletter-signup/kit.md): Conecte seu próprio formulário de newsletter ao Kit com um relay que cria o assinante, o adiciona a um formulário do Kit para o incentive email e marca a posição com uma tag.
- [MailerLite](https://ikiform.com/pt-br/guides/newsletter-signup/mailerlite.md): Conecte seu próprio formulário de inscrição ao MailerLite: um relay adiciona novos leitores como não confirmados para o double opt-in e nunca pede a um assinante que confirme duas vezes.
- [beehiiv](https://ikiform.com/pt-br/guides/newsletter-signup/beehiiv.md): Conecte seu próprio formulário de inscrição ao beehiiv: um relay cria cada subscription com double opt-in ligado, a posição como UTM source e a página como referrer.
- [Formulário de newsletter no Next.js](https://ikiform.com/pt-br/guides/newsletter-signup/nextjs.md): Um formulário de newsletter no rodapé do Next.js: uma server action com checagem de consentimento e Turnstile, enviando ao Ikiform, e um route handler que adiciona leitores ao Kit.
- [Formulário de newsletter no Astro](https://ikiform.com/pt-br/guides/newsletter-signup/astro.md): Um formulário de newsletter no Astro que funciona em páginas pré-renderizadas: uma Astro Action confere o consentimento, envia ao Ikiform, e um endpoint adiciona leitores ao MailerLite.
- [Formulário de newsletter no TanStack Start](https://ikiform.com/pt-br/guides/newsletter-signup/tanstack-start.md): Um formulário de newsletter no TanStack Start: uma server function para inscrições na página, uma server route como fallback sem JavaScript, e inscrições reais enviadas ao beehiiv.
- [Formulário de newsletter no React Router](https://ikiform.com/pt-br/guides/newsletter-signup/react-router.md): Um formulário de newsletter no rodapé com React Router: useFetcher envia a uma rota /newsletter que também funciona sem JavaScript, e uma resource route alimenta o MailerLite.
- [Formulário de newsletter no SvelteKit](https://ikiform.com/pt-br/guides/newsletter-signup/sveltekit.md): Um formulário de newsletter no layout do SvelteKit 3: uma form action com consentimento e limite por IP, use:enhance que fica na página e uma rota de webhook para o Kit.
- [Formulário de newsletter no Nuxt](https://ikiform.com/pt-br/guides/newsletter-signup/nuxt.md): Um formulário de newsletter no Nuxt que funciona com ou sem JavaScript: uma server route com verificação Turnstile envia ao Ikiform, e uma rota Nitro alimenta o beehiiv.

## Confira se funciona

Repasse isto antes de adicionar o formulário a toda página. O Ikiform não tem plano gratuito. Testar é gratuito: 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 [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 o formulário com a caixa desmarcada pelas ferramentas de desenvolvedor do navegador, ou remova o `required` por um momento. Você recebe `422` com `This box must be checked` e nada é armazenado.
3. Envie direito, com um endereço seu. Você vê a frase pedindo para conferir a caixa de entrada, ou cai na sua página de agradecimento.
4. Abra **Submissions** no painel. O novo envio está lá com todos os campos que você enviou.
5. Abra o endpoint em **Webhooks**. A entrega mostra `succeeded` e a resposta do seu relay.
6. Abra sua ferramenta de e-mail. O assinante está lá, não confirmado se o double opt-in estiver ligado, e o e-mail de confirmação está na sua caixa de entrada.
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 |
| --- | --- |
| `422 validation_failed` com `consent` | A caixa não foi marcada, ou o `name` do input não é `consent`. Uma versão no servidor precisa enviar `true`, `"true"` ou `"on"`; uma caixa desmarcada não envia nada. |
| `422 validation_failed` com `unknown_field` | A página envia um input que a definição não lista, muitas vezes um `source` oculto ou um input de rastreamento adicionado depois. Adicione-o à definição ou remova o input. |
| `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. |
| As inscrições chegam no Ikiform, mas não na ferramenta de e-mail | Leia o log de entregas em **Webhooks**. `400` significa que o segredo do relay está errado. `502` traz o status da ferramenta de e-mail: geralmente uma chave de API, id de lista ou id de formulário errado. |
| O assinante está na ferramenta de e-mail, mas não recebeu e-mail de confirmação | O double opt-in está desligado na ferramenta, ou o relay criou o assinante como já confirmado. Veja [double opt-in](https://ikiform.com/pt-br/guides/newsletter-signup/double-opt-in.md) para a chave de cada ferramenta. |
| 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

### Posso criar um formulário de newsletter sem o código de embed do Mailchimp?

Sim. Escreva o formulário no seu próprio HTML, envie-o a um backend de formulários como o Ikiform e encaminhe cada inscrição por webhook ao Mailchimp ou a qualquer outra ferramenta de e-mail pela API dela. O formulário então tem a cara do resto do seu site e não muda quando a ferramenta de e-mail muda.

### O Ikiform envia o e-mail de confirmação ou a newsletter?

Não. O Ikiform coleta, valida e armazena as inscrições e as repassa por webhook. Ele nunca envia e-mail ao assinante, então o e-mail de confirmação, cada edição e o link de descadastro saem da sua ferramenta de e-mail.

### Com quais ferramentas de e-mail isso funciona?

Qualquer ferramenta com uma API para adicionar assinantes, ou qualquer ferramenta suportada por um serviço de automação como Zapier, Make ou n8n. Não há integrações de um clique; o relay ou a automação faz a conexão. Kit, MailerLite e beehiiv têm páginas passo a passo neste guia.

### Onde guardo a chave de API da minha ferramenta de e-mail?

Nas variáveis de ambiente do relay, no servidor, nunca na página. A chave de API de uma ferramenta de e-mail geralmente consegue ler e alterar a lista inteira, então uma chave no JavaScript do navegador é uma chave que qualquer um pode copiar.

### Como impeço bots de se inscreverem na minha newsletter?

Mantenha o honeypot oculto _gotcha e a caixa de consentimento obrigatória, liste seus domínios em Allowed domains e defina a verificação de spam como Ikiform para ter uma checagem com Cloudflare Turnstile. Inscrições armazenadas como spam nunca enviam webhook, então nunca disparam um e-mail de confirmação.

### O Ikiform remove envios duplicados?

Não. Todo envio aceito é armazenado. Uma repetição idêntica em até 24 horas aumenta a pontuação de spam, mas não é spam por si só, então remova os duplicados por e-mail na sua lista, no banco de dados ou na exportação CSV.

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

- [Double opt-in](https://ikiform.com/pt-br/guides/newsletter-signup/double-opt-in.md): O Ikiform não envia e-mails de confirmação, então o double opt-in roda na sua ferramenta de e-mail: um relay adiciona cada inscrição como não confirmada e a ferramenta envia o link.
- [Site estático](https://ikiform.com/pt-br/guides/newsletter-signup/static-site.md): Adicione um formulário de newsletter a um site estático em Hugo, Jekyll, Eleventy ou Astro, mantenha a chave de API da ferramenta de e-mail fora da página e confirme na ferramenta.
- [Kit (ConvertKit)](https://ikiform.com/pt-br/guides/newsletter-signup/kit.md): Conecte seu próprio formulário de newsletter ao Kit com um relay que cria o assinante, o adiciona a um formulário do Kit para o incentive email e marca a posição com uma tag.
- [MailerLite](https://ikiform.com/pt-br/guides/newsletter-signup/mailerlite.md): Conecte seu próprio formulário de inscrição ao MailerLite: um relay adiciona novos leitores como não confirmados para o double opt-in e nunca pede a um assinante que confirme duas vezes.
- [beehiiv](https://ikiform.com/pt-br/guides/newsletter-signup/beehiiv.md): Conecte seu próprio formulário de inscrição ao beehiiv: um relay cria cada subscription com double opt-in ligado, a posição como UTM source e a página como referrer.
- [Formulário de newsletter no Next.js](https://ikiform.com/pt-br/guides/newsletter-signup/nextjs.md): Um formulário de newsletter no rodapé do Next.js: uma server action com checagem de consentimento e Turnstile, enviando ao Ikiform, e um route handler que adiciona leitores ao Kit.
- [Formulário de newsletter no Astro](https://ikiform.com/pt-br/guides/newsletter-signup/astro.md): Um formulário de newsletter no Astro que funciona em páginas pré-renderizadas: uma Astro Action confere o consentimento, envia ao Ikiform, e um endpoint adiciona leitores ao MailerLite.
- [Formulário de newsletter no TanStack Start](https://ikiform.com/pt-br/guides/newsletter-signup/tanstack-start.md): Um formulário de newsletter no TanStack Start: uma server function para inscrições na página, uma server route como fallback sem JavaScript, e inscrições reais enviadas ao beehiiv.
- [Formulário de newsletter no React Router](https://ikiform.com/pt-br/guides/newsletter-signup/react-router.md): Um formulário de newsletter no rodapé com React Router: useFetcher envia a uma rota /newsletter que também funciona sem JavaScript, e uma resource route alimenta o MailerLite.
- [Formulário de newsletter no SvelteKit](https://ikiform.com/pt-br/guides/newsletter-signup/sveltekit.md): Um formulário de newsletter no layout do SvelteKit 3: uma form action com consentimento e limite por IP, use:enhance que fica na página e uma rota de webhook para o Kit.
- [Formulário de newsletter no Nuxt](https://ikiform.com/pt-br/guides/newsletter-signup/nuxt.md): Um formulário de newsletter no Nuxt que funciona com ou sem JavaScript: uma server route com verificação Turnstile envia ao Ikiform, e uma rota Nitro alimenta o beehiiv.
- [Envio de formulários HTML](https://ikiform.com/pt-br/guides/html-form-submission.md): O que o navegador faz quando um formulário é enviado: quais campos manda, como os codifica, o que o servidor deve responder e como o Ikiform responde.
- [Inscrição em eventos](https://ikiform.com/pt-br/guides/event-registration.md): 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.
- [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.
