# Ajouter un formulaire de feedback à votre site ou à votre application

> Placez un formulaire de feedback sur n'importe quelle page ou dans votre application : une note, un commentaire, la page d'origine, le filtrage du spam et des alertes quand une note est basse.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/feedback-form

Ajoutez un court formulaire avec une note de 1 à 5, un commentaire facultatif et un e-mail facultatif, et envoyez-le à un backend de formulaires pour ne pas gérer de serveur. Envoyez l'adresse de la page avec chaque réponse, car le navigateur ne vous donnera pas l'URL complète de lui-même. Faites remonter les notes basses dans le chat de votre équipe le jour même et gardez toutes les réponses au même endroit pour suivre les tendances.

La plupart des gens ne vous disent jamais ce qui ne va pas sur votre site. Ils partent. Un petit formulaire de feedback capte les quelques personnes qui le diront, au moment où elles le remarquent, sur la page en cause. Ce guide en construit un qui s'adapte à n'importe quel site ou application, envoie la page d'origine, bloque les bots et met les réponses mécontentes sous les yeux de votre équipe le jour même.

## Comment fonctionne un formulaire de feedback

Le formulaire envoie à Ikiform. Ikiform valide la note, vérifie que l'envoi n'est pas du spam, le stocke, envoie un e-mail à votre équipe et un webhook signé. À partir du webhook, les notes basses vont dans un canal de chat et chaque réponse peut aller dans un tableur ou une base de données.

Schéma : Le trajet d'un feedback, de votre page à votre équipe

1. Un visiteur choisit une note, écrit éventuellement un commentaire, et le formulaire l'envoie avec l'adresse de la page.
2. Ikiform vérifie que la note est un nombre entier de 1 à 5, lance les vérifications anti-spam et stocke la réponse.
3. La réponse apparaît dans le tableau de bord, où vous pouvez la filtrer et l'exporter en CSV.
4. Ikiform envoie un e-mail à votre équipe. Quand le visiteur a laissé son e-mail, Répondre lui écrit directement.
5. Ikiform envoie un webhook submission.created signé avec les champs et les métadonnées du navigateur.
6. Un petit relais ne publie que les notes basses dans le chat de votre équipe, pour qu'elles soient lues aujourd'hui.

Le relais est facultatif. Commencez par l'e-mail et le tableau de bord, et ajoutez l'alerte dans le chat quand les réponses commencent à s'accumuler.

## Les façons de recueillir du feedback

Chaque type d'outil sert un usage différent. Choisissez selon ce que vous devez voir quand une réponse arrive, pas selon l'apparence du widget.

| Approche | Points forts | Compromis |
| --- | --- | --- |
| Outils de feedback pour sites web, comme Hotjar ou Usersnap | Captures d'écran, annotations et contexte de la page, sans autre code qu'une balise script. | Un script tiers sur chaque page, des réponses stockées dans leur produit et des prix fixés par leurs offres. |
| Le widget d'un outil de suivi d'erreurs, comme Sentry User Feedback | Relie un signalement à l'erreur et, avec le session replay activé, à un enregistrement de ce qui s'est passé. | Conçu pour les bugs, pas pour les avis. Il faut d'abord le SDK de l'outil dans votre application. |
| Les créateurs de formulaires, comme [Typeform](https://ikiform.com/alternatives/typeform), [Tally](https://ikiform.com/alternatives/tally) ou [Google Forms](https://ikiform.com/alternatives/google-forms) | Des formulaires soignés sans code. Bien pour une enquête envoyée par lien. | Une iframe ou une page à part : la réponse perd donc la page dont elle parle, sauf si vous la passez dans l'URL. |
| Les tableaux de feedback, comme Canny | Vote public et suivi de l'avancement des demandes de fonctionnalités. | Conçu pour les idées, pas pour les réactions rapides. Les réponses sont publiques par principe. |
| Votre propre route API et votre base de données | Contrôle total sur les données et toute la logique. | Vous construisez vous-même le filtrage du spam, les limites de débit, l'e-mail de notification et l'export, et il vous faut un serveur. |
| Un backend de formulaires, comme Ikiform | Votre propre markup sur n'importe quel site ou application. Validation, filtrage du spam, stockage, e-mail et webhooks signés, sans serveur. | Pas de captures d'écran, car l'envoi de fichiers n'est pas pris en charge, ni de vote ou de session replay. |

Si vous avez besoin de captures d'écran sur chaque signalement, un outil de feedback ou de suivi d'erreurs justifie son script. Si vous voulez un formulaire à l'image de votre site, des réponses portables et décider vous-même où elles vont, utilisez un backend de formulaires. C'est ce que fait la suite de ce guide.

## Le formulaire

Trois questions suffisent : une note, un commentaire et un e-mail facultatif. La note, c'est ce à quoi les gens répondent ; le commentaire, c'est là qu'est la valeur ; l'e-mail, c'est ce qui vous permet de leur répondre. Choisissez votre stack ci-dessous ; ce choix s'applique à tous les exemples de code de ces guides.

**HTML**

Un seul bloc de markup et un court script. Sans JavaScript, le formulaire envoie quand même et redirige ; avec, le visiteur reste sur la page et voit l'erreur du serveur à côté du bouton. Le script remplit `page` avec l'adresse sans les paramètres d'URL, pour que les tokens présents dans les URL n'arrivent jamais dans votre boîte de réception.

`feedback.html`:

```html
<form id="feedback" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <fieldset>
    <legend>How useful was this page?</legend>
    <label><input type="radio" name="rating" value="1" required /> 1</label>
    <label><input type="radio" name="rating" value="2" /> 2</label>
    <label><input type="radio" name="rating" value="3" /> 3</label>
    <label><input type="radio" name="rating" value="4" /> 4</label>
    <label><input type="radio" name="rating" value="5" /> 5</label>
  </fieldset>
  <label for="comment">What would make it better?</label>
  <textarea id="comment" name="comment" rows="4" maxlength="4000"></textarea>
  <label for="feedback-email">Email, if you'd like a reply</label>
  <input id="feedback-email" name="email" type="email" />
  <input type="hidden" name="page" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send feedback</button>
  <p id="feedback-status" role="status"></p>
</form>

<script>
  const form = document.getElementById("feedback");
  const status = document.getElementById("feedback-status");
  form.elements.page.value = location.origin + location.pathname;

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const button = form.querySelector("button");
    button.disabled = true;
    status.textContent = "Sending";
    try {
      const response = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      const result = await response.json();
      if (result.ok) {
        form.replaceChildren(Object.assign(document.createElement("p"), { textContent: "Thanks. The team reads every note." }));
        return;
      }
      const fields = result.error.fields ?? {};
      status.textContent = fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message;
    } catch {
      status.textContent = "Couldn't send it. Try again in a moment.";
    }
    button.disabled = false;
  });
</script>
```

**Next.js**

La server action lit l'en-tête `Referer` de son propre envoi. Le formulaire et l'action partagent la même origine, donc le navigateur envoie l'URL complète de la page, et l'action n'en garde que l'origine et le chemin. Aucun input masqué n'est nécessaire.

`app/feedback/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface FeedbackState {
  sent: boolean;
  error: string;
}

export async function sendFeedback(_previous: FeedbackState, formData: FormData): Promise<FeedbackState> {
  const rating = Number(formData.get("rating"));
  if (!Number.isInteger(rating) || rating < 1 || rating > 5) return { sent: false, error: "Pick a score from 1 to 5." };

  const incoming = await headers();
  const referer = incoming.get("referer");
  const origin = incoming.get("origin");
  const page = referer ? new URL(referer) : null;

  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({
      rating,
      comment: String(formData.get("comment") ?? ""),
      email: String(formData.get("email") ?? ""),
      page: page ? page.origin + page.pathname : "",
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, error: "" };
  const fields = result?.error?.fields ?? {};
  return { sent: false, error: fields.comment?.[0] ?? fields.email?.[0] ?? "Couldn't send it. Try again." };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type FeedbackState, sendFeedback } from "./actions";

const initial: FeedbackState = { sent: false, error: "" };

export function FeedbackForm() {
  const [state, action, pending] = useActionState(sendFeedback, initial);

  if (state.sent) return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <form action={action}>
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

Pour aller plus loin avec Next.js, lisez [Créer un formulaire de feedback dans l'application avec Next.js](https://ikiform.com/fr/guides/feedback-form/nextjs.md).

**Astro**

La page connaît sa propre adresse pendant le rendu, donc `Astro.url` remplit l'input masqué `page` côté serveur. Les inputs vides arrivent à l'action sous la forme `null`, qu'Ikiform traite comme absents. La page doit être rendue à la demande pour que le résultat de l'action s'affiche.

`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 = {
  sendFeedback: defineAction({
    accept: "form",
    input: z.object({
      rating: z.coerce.number().int().min(1).max(5),
      comment: z.string().max(4000).nullish(),
      email: z.email().nullish(),
      page: z.string().nullish(),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.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 ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify(input),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { sent: true };
    },
  }),
};
```

`src/components/FeedbackForm.astro`:

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

const result = Astro.getActionResult(actions.sendFeedback);
const page = Astro.url.origin + Astro.url.pathname;
---

{result?.data?.sent ? (
  <p role="status">Thanks. The team reads every note.</p>
) : (
  <form method="POST" action={actions.sendFeedback}>
    <fieldset>
      <legend>How useful was this page?</legend>
      {[1, 2, 3, 4, 5].map((score) => (
        <label><input type="radio" name="rating" value={score} required /> {score}</label>
      ))}
    </fieldset>
    <label for="comment">What would make it better?</label>
    <textarea id="comment" name="comment" rows="4" maxlength="4000"></textarea>
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" name="email" type="email" />
    <input type="hidden" name="page" value={page} />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && (
      <p role="alert">{isInputError(result.error) ? "Pick a score and check your email address." : result.error.message}</p>
    )}
    <button>Send feedback</button>
  </form>
)}
```

Pour aller plus loin avec Astro, lisez [Créer un formulaire de feedback dans l'application avec Astro](https://ikiform.com/fr/guides/feedback-form/astro.md).

**TanStack Start**

Une server function POST vérifie la note avant que quoi que ce soit ne quitte votre serveur. Le composant renseigne `page` depuis `window.location` au moment de l'envoi, pour qu'elle soit juste même après une navigation côté client.

`src/components/FeedbackForm.tsx`:

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

const sendFeedback = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const rating = Number(data.get("rating"));
    if (!Number.isInteger(rating) || rating < 1 || rating > 5) throw new Error("Pick a score from 1 to 5.");
    return {
      rating,
      comment: String(data.get("comment") ?? ""),
      email: String(data.get("email") ?? ""),
      page: String(data.get("page") ?? ""),
      _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 { sent: true, error: "" };
    const fields = result.error.fields ?? {};
    return { sent: false, error: String(fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message) };
  });

export function FeedbackForm() {
  const send = useServerFn(sendFeedback);
  const [state, setState] = useState({ sent: false, error: "" });
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    data.set("page", window.location.origin + window.location.pathname);
    setPending(true);
    try {
      setState(await send({ data }));
    } catch (error) {
      setState({ sent: false, error: error instanceof Error ? error.message : "Couldn't send it. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (state.sent) return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

Pour aller plus loin avec TanStack Start, lisez [Créer un formulaire de feedback dans l'application avec TanStack Start](https://ikiform.com/fr/guides/feedback-form/tanstack-start.md).

**React Router**

Une resource route contient l'action, et `useFetcher` y envoie depuis n'importe quelle page sans navigation. Déclarez la route comme `api/feedback` dans `app/routes.ts`. `useLocation` fournit le chemin, et l'action le résout par rapport à sa propre URL, qui a l'origine de votre site.

`app/routes/api.feedback.ts`:

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  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({
      rating: String(formData.get("rating") ?? ""),
      comment: String(formData.get("comment") ?? ""),
      email: String(formData.get("email") ?? ""),
      page: new URL(String(formData.get("path") ?? "/"), request.url).href,
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { sent: true, error: "" };
  const fields = result.error.fields ?? {};
  return data(
    { sent: false, error: String(fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message) },
    { status: response.status },
  );
}
```

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

```tsx
import { useFetcher, useLocation } from "react-router";
import type { action } from "../routes/api.feedback";

export function FeedbackForm() {
  const fetcher = useFetcher<typeof action>();
  const { pathname } = useLocation();
  const pending = fetcher.state !== "idle";

  if (fetcher.data?.sent) return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <fetcher.Form method="post" action="/api/feedback">
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input type="hidden" name="path" value={pathname} />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {fetcher.data?.error ? <p role="alert">{fetcher.data.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </fetcher.Form>
  );
}
```

Pour aller plus loin avec React Router, lisez [Créer un formulaire de feedback dans l'application avec React Router](https://ikiform.com/fr/guides/feedback-form/react-router.md).

**SvelteKit**

Un endpoint `+server.ts` transmet l'envoi, pour que le composant puisse vivre dans un layout et fonctionner sur toutes les pages. `page` depuis `$app/state` donne l'URL courante. Déclarez `IKIFORM_SECRET_KEY` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse la lire. L'endpoint renvoie `Response.json()`, car SvelteKit 3 rend obsolète le helper `json()`.

`src/routes/api/feedback/+server.ts`:

```ts
import { error } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { RequestHandler } from "./$types";

export const POST: RequestHandler = async ({ request, fetch, url }) => {
  const body = await request.json();
  const rating = Number(body.rating);
  if (!Number.isInteger(rating) || rating < 1 || rating > 5) error(400, "Pick a score from 1 to 5.");

  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({
      rating,
      comment: String(body.comment ?? ""),
      email: String(body.email ?? ""),
      page: String(body.page ?? ""),
      _gotcha: String(body._gotcha ?? ""),
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    const fields = result.error.fields ?? {};
    error(response.status, String(fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message));
  }
  return Response.json({ sent: true });
};
```

`src/lib/FeedbackForm.svelte`:

```svelte
<script lang="ts">
  import { page } from "$app/state";

  let rating = $state(0);
  let comment = $state("");
  let email = $state("");
  let trap = $state("");
  let status = $state<"idle" | "sending" | "sent">("idle");
  let message = $state("");

  async function send(event: SubmitEvent) {
    event.preventDefault();
    status = "sending";
    message = "";
    const response = await fetch("/api/feedback", {
      method: "POST",
      headers: { Accept: "application/json", "Content-Type": "application/json" },
      body: JSON.stringify({ rating, comment, email, page: page.url.origin + page.url.pathname, _gotcha: trap }),
    });
    if (response.ok) {
      status = "sent";
      return;
    }
    message = (await response.json().catch(() => null))?.message ?? "Couldn't send it. Try again.";
    status = "idle";
  }
</script>

{#if status === "sent"}
  <p role="status">Thanks. The team reads every note.</p>
{:else}
  <form onsubmit={send}>
    <fieldset>
      <legend>How useful was this page?</legend>
      {#each [1, 2, 3, 4, 5] as score}
        <label><input type="radio" name="rating" value={score} bind:group={rating} required /> {score}</label>
      {/each}
    </fieldset>
    <label for="comment">What would make it better?</label>
    <textarea id="comment" name="comment" rows="4" maxlength="4000" bind:value={comment}></textarea>
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" name="email" type="email" bind:value={email} />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden bind:value={trap} />
    {#if message}
      <p role="alert">{message}</p>
    {/if}
    <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send feedback"}</button>
  </form>
{/if}
```

Pour aller plus loin avec SvelteKit, lisez [Créer un formulaire de feedback dans l'application avec SvelteKit](https://ikiform.com/fr/guides/feedback-form/sveltekit.md).

**Nuxt**

Une route serveur garde la clé dans `runtimeConfig.ikiformSecretKey`, alimentée par `NUXT_IKIFORM_SECRET_KEY`. `useRoute().path` et `useRequestURL()` construisent l'adresse de la page aussi bien côté serveur que dans le navigateur.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ rating?: number; comment?: string; email?: string; page?: string; _gotcha?: string }>(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({
      rating: body.rating,
      comment: body.comment ?? "",
      email: body.email ?? "",
      page: body.page ?? "",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    const fields = result.error.fields ?? {};
    throw createError({
      status: response.status,
      message: String(fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message),
    });
  }
  return { sent: true };
});
```

`app/components/FeedbackForm.vue`:

```vue
<script setup lang="ts">
const route = useRoute();
const requestUrl = useRequestURL();
const rating = ref<number | null>(null);
const comment = ref("");
const email = ref("");
const trap = ref("");
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");

async function send() {
  status.value = "sending";
  error.value = "";
  try {
    await $fetch("/api/feedback", {
      method: "POST",
      body: {
        rating: rating.value,
        comment: comment.value,
        email: email.value,
        page: requestUrl.origin + route.path,
        _gotcha: trap.value,
      },
    });
    status.value = "sent";
  } catch (failure) {
    error.value = (failure as { data?: { message?: string } }).data?.message ?? "Couldn't send it. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks. The team reads every note.</p>
  <form v-else @submit.prevent="send">
    <fieldset>
      <legend>How useful was this page?</legend>
      <label v-for="score in 5" :key="score">
        <input v-model="rating" type="radio" name="rating" :value="score" required /> {{ score }}
      </label>
    </fieldset>
    <label for="comment">What would make it better?</label>
    <textarea id="comment" v-model="comment" name="comment" rows="4" maxlength="4000" />
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" v-model="email" name="email" type="email" />
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send feedback" }}</button>
  </form>
</template>
```

Pour aller plus loin avec Nuxt, lisez [Créer un formulaire de feedback dans l'application avec Nuxt](https://ikiform.com/fr/guides/feedback-form/nuxt.md).

**React**

Sans serveur à vous, le navigateur envoie directement à Ikiform. `submit()` de `@ikiform/sdk` envoie le `FormData` et renvoie `{ ok }` ou les erreurs de champ. Ne mettez jamais de clé secrète ici ; c'est **Allowed domains** (domaines autorisés) qui protège le formulaire.

`src/FeedbackForm.tsx`:

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

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";

export function FeedbackForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    data.set("page", window.location.origin + window.location.pathname);
    setStatus("sending");
    setError("");
    const result = await submit(FORM_ID, data);
    if (result.ok) {
      setStatus("sent");
      return;
    }
    const fields = result.error.fields ?? {};
    setError(fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message);
    setStatus("idle");
  }

  if (status === "sent") return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <form onSubmit={onSubmit}>
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

**Vue**

Le même envoi côté navigateur dans un composant monofichier. Le libellé du commentaire change quand la note est 1 ou 2, ce qui obtient des réponses plus précises des visiteurs mécontents.

`src/components/FeedbackForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const rating = ref(0);
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");

async function send(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  body.set("page", window.location.origin + window.location.pathname);
  status.value = "sending";
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      status.value = "sent";
      return;
    }
    const fields = result.error.fields ?? {};
    error.value = fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message;
  } catch {
    error.value = "Couldn't send it. Try again in a moment.";
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks. The team reads every note.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <fieldset>
      <legend>How useful was this page?</legend>
      <label v-for="score in 5" :key="score">
        <input v-model="rating" type="radio" name="rating" :value="score" required /> {{ score }}
      </label>
    </fieldset>
    <label for="comment">{{ rating && rating <= 2 ? "What went wrong?" : "What would make it better?" }}</label>
    <textarea id="comment" name="comment" rows="4" maxlength="4000" />
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" name="email" type="email" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send feedback" }}</button>
  </form>
</template>
```

- Remplacez `frm_xxxxxxxxxxxxxxxx` par l'id de votre formulaire, visible dans le tableau de bord.
- `rating` est un groupe de cinq boutons radio. Ikiform le stocke comme un nombre, donc les moyennes ne demandent aucun parsing.
- `email` est facultatif. Les gens écrivent plus franchement quand ils n'ont pas à s'identifier, et ceux qui veulent une réponse le laisseront.
- L'input masqué `_gotcha` est le honeypot. Les humains ne le voient jamais ; les bots qui remplissent tous les inputs se signalent eux-mêmes comme spam.
- `page` enregistre la page dont parle la réponse. La section suivante explique pourquoi vous l'envoyez vous-même.

> Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur.

Les guides par framework de la documentation détaillent la mise en place : [Next.js](https://ikiform.com/fr/docs/guides/nextjs.md), [Astro](https://ikiform.com/fr/docs/guides/astro.md), [SvelteKit](https://ikiform.com/fr/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/fr/docs/guides/nuxt.md), [React](https://ikiform.com/fr/docs/guides/react.md), [Vue](https://ikiform.com/fr/docs/guides/vue.md) et [HTML simple](https://ikiform.com/fr/docs/html.md). Le [modèle de formulaire de feedback](https://ikiform.com/forms/feedback) génère les mêmes champs en HTML, en React et en server action ; les modèles de [signalement de bug](https://ikiform.com/forms/bug-report) et d'[enquête](https://ikiform.com/forms/survey) couvrent les formulaires plus longs.

## Définir les champs

Collez une définition dans l'onglet **Fields** du formulaire et Ikiform l'applique à chaque envoi, depuis n'importe quelle page. Essayez ci-dessous : l'aperçu exécute le validateur d'Ikiform dans votre navigateur, donc une note de `0` ou un e-mail mal formé affiche le `422` que vous recevriez.

Formulaire : Page feedback

- **Cette page vous a-t-elle été utile ?** (`rating`, rating, obligatoire)
- **Qu'est-ce qui l'améliorerait ?** (`comment`, textarea)
- **Votre e-mail, si vous souhaitez une réponse** (`email`, email)
- **Page** (`page`, url)

Un `rating` accepte un nombre entier de 1 à son `max`, ici 5. Il peut aller jusqu'à 10, mais cinq choix sont plus rapides.

| Champ | Type | Pourquoi |
| --- | --- | --- |
| `rating` | `rating`, max 5 | Un clic. Stocké comme un nombre, pour faire la moyenne par page |
| `comment` | `textarea`, jusqu'à 4 000 caractères | La raison derrière la note |
| `email` | `email`, facultatif | Répondre aux personnes qui posent une question. Devient l'adresse de réponse de la notification |
| `page` | `url`, facultatif | La page dont parle la réponse |

Une fois les champs définis, tout autre nom dans l'envoi est refusé avec `Unknown field`. Si plus tard vous envoyez plus de contexte, comme une version d'application ou un nom d'offre, ajoutez d'abord un champ pour lui. Les id de champ sont en snake_case minuscule.

## Savoir de quelle page il s'agit

Une note sans page est difficile à exploiter. Ikiform enregistre les métadonnées du navigateur avec chaque soumission, mais l'une de ces valeurs est moins utile qu'elle n'en a l'air :

| Valeur | D'où elle vient | Ce que vous obtenez |
| --- | --- | --- |
| `meta.user_agent` | Enregistré par Ikiform | Le navigateur et le système d'exploitation, depuis un envoi du navigateur |
| `meta.origin` | Enregistré par Ikiform | L'origine de votre site, comme `https://example.com` |
| `meta.referrer` | Enregistré par Ikiform | Généralement l'origine seule. Les navigateurs appliquent par défaut la politique `strict-origin-when-cross-origin`, qui supprime le chemin lors d'un envoi vers un autre site |
| `meta.country` | Enregistré par Ikiform | Un code pays à deux lettres |
| `page` | Envoyé par votre formulaire | Le chemin complet, pour savoir de quelle page parle la réponse |

C'est pourquoi chaque exemple ci-dessus envoie `page` lui-même. Les versions HTML et SPA lisent `location` ; les versions serveur lisent l'adresse que leur framework connaît déjà. Toutes suppriment les paramètres d'URL, qui peuvent contenir des termes de recherche, des id de suivi ou des tokens à usage unique que vous ne voulez pas stocker.

> Les versions serveur envoient depuis votre serveur, donc `meta.user_agent` est le runtime de votre serveur, pas le navigateur du visiteur. Pour le feedback sur une page, c'est rarement important. Pour les signalements de bug, ça l'est, et le [guide du signalement de bug](https://ikiform.com/fr/guides/feedback-form/bug-report.md) transmet le user agent du navigateur dans un champ.

## Où poser la question

L'emplacement du formulaire détermine ce que vous entendez. Un formulaire dans le pied de page de chaque page reçoit un filet d'avis tranchés. Une question à la fin d'une tâche reçoit des réponses régulières et précises.

| Emplacement | Idéal pour | Points de vigilance |
| --- | --- | --- |
| Fin d'une page de documentation ou d'un article | Repérer les pages qui ne répondent pas à la question | Peu de réponses par page. Lisez-les par section, pas par page |
| Après une tâche dans votre application, comme un export ou un premier projet | Le ressenti d'un parcours précis, tant qu'il est frais | Demandez une fois par personne, pas à chaque fois qu'elle termine la tâche |
| Un bouton flottant sur chaque écran | N'importe quoi, n'importe quand. Voir le [widget React](https://ikiform.com/fr/guides/feedback-form/react-feedback-widget.md) | Il ne doit pas masquer vos propres boutons, bannières de cookies ou bulles de chat |
| Un lien dans un e-mail | Les scores à suivre dans le temps, comme le [NPS](https://ikiform.com/fr/guides/feedback-form/nps-survey.md) | Le client e-mail n'est pas votre page : le formulaire a donc besoin d'une page d'atterrissage sur votre site |

Gardez le formulaire court, où qu'il soit. Demandez d'abord la note et affichez le champ de commentaire avec, pas dans une deuxième étape. Une deuxième étape fait fuir la plupart des gens, et une note sans raison reste utile.

## Mettre les notes basses sous les yeux de quelqu'un

Schéma : Ce qui arrive à une réponse selon sa note

1. Chaque réponse est stockée, quelle que soit sa note et la page d'où elle vient.
2. Ikiform envoie un webhook submission.created signé à votre relais.
3. Le relais lit la note. Les notes de 3 à 5 ne demandent rien de plus dans l'immédiat.
4. Une note de 1 ou 2 part dans le chat de votre équipe avec la page et le commentaire.
5. Si le visiteur a laissé son e-mail, quelqu'un lui répond le jour même.
6. Une fois par semaine, l'export donne les moyennes par page, et c'est là que les tendances apparaissent.

N'alertez que sur les mauvaises notes. Un canal qui sonne pour chaque 5 sur 5 est mis en sourdine en une semaine.

Les e-mails de notification vont à 10 destinataires confirmés au plus par formulaire et arrivent pour chaque réponse. Ça convient pour quelques réponses par jour. Au-delà, envoyez les notes basses dans le chat avec un relais comme cette route Next.js ; le même code tourne dans un Cloudflare Worker :

`app/api/feedback-alerts/route.ts`:

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

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    const { rating, comment, page, email } = event.data.fields;
    if (Number(rating) > 2) return new Response(null, { status: 204 });

    const lines = [
      `*${rating}/5* on ${page ?? event.data.meta.origin ?? "an unknown page"}`,
      comment ? `> ${String(comment).slice(0, 500)}` : "_No comment._",
      email ? `Reply to ${email}` : "Anonymous",
    ];
    const response = await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ text: lines.join("\n") }),
    });
    if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

- `verifyWebhook` vérifie la signature et l'horodatage, pour que personne d'autre ne puisse publier dans votre canal via cette URL.
- Les notes supérieures à 2 répondent `204` et s'arrêtent là. Ikiform considère tout 2xx comme livré.
- Un appel Slack en échec répond `502`, et Ikiform réessaie avec le même `webhook-id` : une courte panne ne fait donc pas perdre d'alerte.
- Les valeurs des champs sont ce que le visiteur a tapé. Le relais coupe le commentaire à 500 caractères avant qu'il n'arrive dans le chat.

Ajoutez l'endpoint sous **Webhooks** et copiez son secret `whsec_` dans `IKIFORM_WEBHOOK_SECRET`. Un espace de travail peut avoir 3 endpoints. Pour des routes sans code, voyez [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) et [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md). Pour un feedback qui doit devenir un ticket, [Linear et GitHub Issues](https://ikiform.com/blog/form-submissions-to-linear-and-github-issues.md) propose un relais qui en crée un par soumission.

> Il n'y a pas d'intégrations en un clic. Chaque outil est atteint par un [webhook](https://ikiform.com/fr/docs/webhooks.md) signé, directement ou via un outil d'automatisation comme [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).

## Tenir les bots à l'écart

Un formulaire de feedback sur chaque page, c'est un formulaire public sur chaque page. Les bots le trouvent, et ils envoient surtout des liens. Les réglages par défaut d'Ikiform en gèrent l'essentiel :

| Défense | Ce qu'elle arrête | Configuration |
| --- | --- | --- |
| Honeypot | Les bots qui remplissent tous les inputs | L'input masqué `_gotcha` des exemples |
| Score de spam | Trois liens ou plus (+2), phrases de spam connues (+2), e-mails jetables (+2), répétitions identiques (+3). À 5, c'est stocké comme spam | Aucun |
| Limites de débit | Les scripts qui envoient en boucle | Activée par défaut : Standard, 5 par minute et par visiteur, et 300 par minute pour l'ensemble du formulaire |
| Domaines autorisés | Les envois depuis d'autres sites | Ajoutez votre domaine dans les réglages du formulaire |
| Vérification humaine | Les bots qui pilotent un vrai navigateur | Réglez **Spam check** sur **Ikiform**. Les envois `fetch` du navigateur ont alors besoin d'un token de `challenge()` |

Le spam reçoit la même réponse de succès qu'un vrai envoi, ne déclenche ni e-mail ni webhook et ne compte pas dans l'utilisation. Un vrai visiteur qui colle trois liens dans un commentaire peut déclencher la règle des liens en même temps qu'une répétition : jetez donc un œil au filtre **Spam** de temps en temps. Marquer une réponse comme non-spam envoie alors son webhook. La [documentation anti-spam](https://ikiform.com/fr/docs/spam.md) explique chaque vérification.

## Lire les réponses

Lisez chaque commentaire les premières semaines. Ensuite, la vue utile est la note moyenne par page, la plus basse en premier, avec les commentaires à côté.

### Export

Ouvrez **Submissions**, choisissez le formulaire de feedback, une période, puis **Export CSV**. Les colonnes sont `id`, `created_at`, `form` et `status`, puis une par champ : avec les champs ci-dessus, `rating` est la colonne E et `page` la colonne H. Un export contient jusqu'à 100 000 lignes ; le [guide d'export](https://ikiform.com/fr/docs/guides/export.md) présente l'API pour aller au-delà.

Importez le fichier dans Google Sheets et mettez ceci dans une feuille vide. Il liste chaque page avec sa note moyenne et son nombre de réponses, de la pire à la meilleure :

`Google Sheets`:

```text
=QUERY(A2:H, "select H, avg(E), count(E) where D = 'received' and E is not null group by H order by avg(E) asc label avg(E) 'average', count(E) 'answers'", 0)
```

1. Ignorez les pages qui n'ont qu'une ou deux réponses. Un visiteur en colère ne fait pas une tendance.
2. Lisez les commentaires des trois pages les moins bien notées. La correction se trouve généralement dans les premiers.
3. Après avoir modifié une page, comparez sa moyenne sur les semaines d'avant et d'après, pas la moyenne globale.

> Le feedback n'est pas anonyme pour Ikiform, même sans e-mail : chaque réponse enregistre le user agent, l'origine, le referrer et le pays, ainsi qu'un hash de l'IP qui n'est jamais renvoyé. Indiquez ce que vous collectez dans votre politique de confidentialité.

## Choisissez votre cas

Le formulaire ci-dessus couvre les réactions rapides sur n'importe quelle page. Ces pages-ci couvrent le feedback qui a un autre rôle : un bug à reproduire, une demande à évaluer, un score à suivre ou un client sur le départ.

- [Formulaire de signalement de bug](https://ikiform.com/fr/guides/feedback-form/bug-report.md): Un formulaire de signalement de bug qui demande les étapes et la gravité, et laisse la page ajouter elle-même l'URL, le navigateur, le viewport et les erreurs récentes.
- [Formulaire de demande de fonctionnalité](https://ikiform.com/fr/guides/feedback-form/feature-request.md): Un formulaire de demande de fonctionnalité qui demande le problème plutôt que la solution, range les demandes par thème, compte les personnes et non les lignes, et les prévient quand c'est livré.
- [Widget de feedback React](https://ikiform.com/fr/guides/feedback-form/react-feedback-widget.md): Un bouton de feedback flottant pour une application React qui ouvre un petit panneau, envoie l'écran, la version et le compte avec chaque réponse, et ne perd jamais un brouillon.
- [Feedback des bêta-testeurs](https://ikiform.com/fr/guides/feedback-form/beta-tester-feedback.md): Un formulaire de feedback bêta avec un lien personnel par testeur et par build, deux échelles de notation et un résumé hebdomadaire qui montre si chaque build s'est amélioré.
- [Enquête NPS](https://ikiform.com/fr/guides/feedback-form/nps-survey.md): Menez une enquête NPS avec un score de 0 à 10 dans l'e-mail, une question de suivi sur votre propre page qui change selon le score, et un script qui calcule le NPS.
- [Enquête de résiliation](https://ikiform.com/fr/guides/feedback-form/cancellation-survey.md): Demandez pourquoi vos clients résilient dans le parcours d'annulation de votre SaaS, gardez Passer à un clic, enregistrez la raison dans Stripe et Ikiform, sans jamais retarder la résiliation.
- [Formulaire de feedback Next.js](https://ikiform.com/fr/guides/feedback-form/nextjs.md): Un formulaire de feedback dans une application Next.js : une server action qui lit la session, tire la page de la requête et transmet une note avec l'utilisateur joint.
- [Formulaire de feedback Astro](https://ikiform.com/fr/guides/feedback-form/astro.md): Un formulaire de feedback dans une application Astro : une action qui prend l'utilisateur dans les locals du middleware, la page dans la requête et l'écran dans le motif de route.
- [Formulaire de feedback TanStack Start](https://ikiform.com/fr/guides/feedback-form/tanstack-start.md): Un formulaire de feedback dans une application TanStack Start : une server function POST avec un middleware de session, l'id de route comme contexte et la page lue dans les en-têtes.
- [Formulaire de feedback React Router](https://ikiform.com/fr/guides/feedback-form/react-router.md): Un formulaire de feedback React Router 8 : l'action d'une resource route lit la session cookie, un fetcher envoie depuis le layout et un envoi sans JS redirige en retour.
- [Formulaire de feedback SvelteKit](https://ikiform.com/fr/guides/feedback-form/sveltekit.md): Un formulaire de feedback dans une application SvelteKit : un formulaire de layout envoie à une seule form action, qui ajoute côté serveur l'utilisateur depuis locals et la page depuis la requête.
- [Formulaire de feedback Nuxt](https://ikiform.com/fr/guides/feedback-form/nuxt.md): Un formulaire de feedback dans une application Nuxt : une route serveur qui lit la session utilisateur, prend la page dans Referer et fonctionne même quand JavaScript n'est pas chargé.

## Vérifiez que tout fonctionne

Faites ces vérifications avant de mettre le formulaire sur chaque page. Ikiform n'a pas d'offre gratuite. Les tests sont gratuits : un espace de travail sans offre accepte 50 soumissions de test par mois depuis `localhost`. Pour passer en production, choisissez Pro à partir de 6 $/mois pour 500 soumissions, ou payez une seule fois : 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais. Voir les [tarifs](https://ikiform.com/fr/pricing).

1. Dans **Allowed domains** du formulaire, ajoutez le domaine de votre site, plus `localhost` pendant le développement. Laissez vide pour accepter les envois de partout.
2. Choisissez une note, écrivez un commentaire et envoyez. La ligne de remerciement remplace le formulaire, ou le navigateur arrive sur votre page de redirection pour la version HTML simple.
3. Ouvrez **Submissions** dans le tableau de bord. La réponse affiche `rating` comme un nombre et `page` avec le chemin complet.
4. Envoyez une note de `6` avec `curl` ou la console du navigateur. Ikiform répond `422` en nommant `rating`.
5. Vérifiez la boîte de réception de chaque destinataire des notifications. Chacun reçoit un e-mail avec les champs, et **Reply** répond à l'expéditeur quand le formulaire a un champ `email`.
6. Envoyez une note de 1. Votre relais la publie dans le chat de l'équipe, et la livraison sous **Webhooks** affiche `succeeded`.
7. Envoyez avec le honeypot rempli, `_gotcha=bot`. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.
8. Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur **Spam**. Le spam reçoit la même réponse de succès qu'une vraie soumission.

## Dépannage

| Problème | Que faire |
| --- | --- |
| `403 origin_not_allowed`, ou tous les envois finissent en spam | Le domaine de la page n'est pas dans les **Allowed domains** du formulaire. Ajoutez-le, plus `localhost` pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés. |
| `422` avec `Rating must be at most 5` | Le formulaire envoie une note supérieure au `max` du champ. Changez les valeurs des boutons radio ou augmentez `max` dans la définition, jusqu'à 10. |
| `422` avec un Unknown field | Le formulaire envoie un nom absent de la définition, souvent un nouvel input masqué. Ajoutez un champ avec cet id, ou supprimez l'input. |
| `page` est vide | Le script qui le remplit ne s'est pas exécuté, ou s'est exécuté avant que le formulaire n'existe. Placez-le après le formulaire, ou renseignez `page` au moment de l'envoi comme le font les exemples SPA. |
| `meta.referrer` n'affiche que votre domaine | C'est normal. Par défaut, les navigateurs n'envoient que l'origine sur les envois cross-site. Utilisez plutôt le champ `page`. |
| La soumission est dans le tableau de bord mais aucun e-mail ni webhook n'est arrivé | Elle a été stockée comme spam. Le spam n'envoie rien. Ouvrez-la sous le filtre **Spam** pour voir les raisons, et marquez-la comme non-spam pour envoyer son e-mail et ses webhooks. |
| `429 rate_limited` | Un visiteur a envoyé plus que ce que permet la limite de débit du formulaire. Standard autorise 5 envois par minute et par visiteur. Attendez la durée de `Retry-After`, ou choisissez Relaxed ou Custom pour les formulaires qui envoient depuis une seule IP partagée. |
| `402 test_limit_reached` | L'espace de travail n'a pas d'offre et a utilisé ses 50 tests gratuits depuis `localhost` ce mois-ci. Choisissez une offre, ou utilisez **Send test** dans le tableau de bord. |

## Questions

### Comment ajouter un formulaire de feedback à un site sans backend ?

Faites pointer l'action du formulaire vers l'endpoint d'un backend de formulaires. Le formulaire peut être du HTML simple sur un site statique ; le backend valide, stocke et envoie par e-mail chaque réponse, et envoie un webhook si vous la voulez ailleurs.

### Comment savoir de quelle page vient le feedback ?

Envoyez l'adresse de la page dans un champ. Ikiform enregistre aussi le referrer, mais les navigateurs n'envoient généralement que votre domaine sur un envoi cross-site : un champ de page rempli depuis location est donc la méthode fiable.

### Le feedback doit-il être anonyme ?

Rendez l'e-mail facultatif. Les gens sont plus francs sans, et ceux qui veulent une réponse le laisseront. Ikiform enregistre quand même le navigateur, l'origine, le referrer et le pays avec chaque réponse.

### Les visiteurs peuvent-ils joindre une capture d'écran ?

Pas sous forme de fichier. Ikiform refuse les envois avec fichiers avec 422 files_not_supported. Demandez plutôt un lien vers une capture, ou utilisez un outil de feedback centré sur les captures d'écran s'il vous en faut une sur chaque signalement.

### Puis-je utiliser une échelle de 0 à 10 au lieu d'étoiles ?

Pas avec un champ rating, qui va de 1 à 10 au plus. Un score de 0 à 10 utilise un champ number avec min 0, max 10 et integer true. Le guide NPS montre la configuration complète.

### Comment être alerté d'un mauvais feedback ?

Ajoutez un webhook vers un petit relais qui vérifie la note et ne publie que les notes basses dans Slack ou Discord. Les e-mails de notification couvrent chaque réponse ; le relais couvre celles qui demandent une réponse aujourd'hui.

### Ikiform est-il gratuit ?

Non, il n'y a pas d'offre gratuite. Les tests sont gratuits : vous pouvez envoyer 50 soumissions de test par mois depuis localhost avant de payer. Pour passer en production, Pro commence à 6 $/mois pour 500 soumissions, ou vous pouvez payer une seule fois 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais.

## Autres guides

- [Formulaire de signalement de bug](https://ikiform.com/fr/guides/feedback-form/bug-report.md): Un formulaire de signalement de bug qui demande les étapes et la gravité, et laisse la page ajouter elle-même l'URL, le navigateur, le viewport et les erreurs récentes.
- [Formulaire de demande de fonctionnalité](https://ikiform.com/fr/guides/feedback-form/feature-request.md): Un formulaire de demande de fonctionnalité qui demande le problème plutôt que la solution, range les demandes par thème, compte les personnes et non les lignes, et les prévient quand c'est livré.
- [Widget de feedback React](https://ikiform.com/fr/guides/feedback-form/react-feedback-widget.md): Un bouton de feedback flottant pour une application React qui ouvre un petit panneau, envoie l'écran, la version et le compte avec chaque réponse, et ne perd jamais un brouillon.
- [Feedback des bêta-testeurs](https://ikiform.com/fr/guides/feedback-form/beta-tester-feedback.md): Un formulaire de feedback bêta avec un lien personnel par testeur et par build, deux échelles de notation et un résumé hebdomadaire qui montre si chaque build s'est amélioré.
- [Enquête NPS](https://ikiform.com/fr/guides/feedback-form/nps-survey.md): Menez une enquête NPS avec un score de 0 à 10 dans l'e-mail, une question de suivi sur votre propre page qui change selon le score, et un script qui calcule le NPS.
- [Enquête de résiliation](https://ikiform.com/fr/guides/feedback-form/cancellation-survey.md): Demandez pourquoi vos clients résilient dans le parcours d'annulation de votre SaaS, gardez Passer à un clic, enregistrez la raison dans Stripe et Ikiform, sans jamais retarder la résiliation.
- [Formulaire de feedback Next.js](https://ikiform.com/fr/guides/feedback-form/nextjs.md): Un formulaire de feedback dans une application Next.js : une server action qui lit la session, tire la page de la requête et transmet une note avec l'utilisateur joint.
- [Formulaire de feedback Astro](https://ikiform.com/fr/guides/feedback-form/astro.md): Un formulaire de feedback dans une application Astro : une action qui prend l'utilisateur dans les locals du middleware, la page dans la requête et l'écran dans le motif de route.
- [Formulaire de feedback TanStack Start](https://ikiform.com/fr/guides/feedback-form/tanstack-start.md): Un formulaire de feedback dans une application TanStack Start : une server function POST avec un middleware de session, l'id de route comme contexte et la page lue dans les en-têtes.
- [Formulaire de feedback React Router](https://ikiform.com/fr/guides/feedback-form/react-router.md): Un formulaire de feedback React Router 8 : l'action d'une resource route lit la session cookie, un fetcher envoie depuis le layout et un envoi sans JS redirige en retour.
- [Formulaire de feedback SvelteKit](https://ikiform.com/fr/guides/feedback-form/sveltekit.md): Un formulaire de feedback dans une application SvelteKit : un formulaire de layout envoie à une seule form action, qui ajoute côté serveur l'utilisateur depuis locals et la page depuis la requête.
- [Formulaire de feedback Nuxt](https://ikiform.com/fr/guides/feedback-form/nuxt.md): Un formulaire de feedback dans une application Nuxt : une route serveur qui lit la session utilisateur, prend la page dans Referer et fonctionne même quand JavaScript n'est pas chargé.
- [Formulaire de capture de leads](https://ikiform.com/fr/guides/lead-capture.md): Un formulaire de capture de leads sur votre propre site, qui vous écrit avec le lead en adresse de réponse, alerte le chat de l'équipe et alimente votre CRM, pour répondre tant que le lead est encore là.
- [Formulaire d'inscription à la newsletter](https://ikiform.com/fr/guides/newsletter-signup.md): Créez un formulaire d'inscription à la newsletter avec votre propre markup, enregistrez le consentement, bloquez les inscriptions de bots et transmettez chaque abonné à Kit, MailerLite, beehiiv ou n'importe quel outil.
- [Envoi de formulaire HTML](https://ikiform.com/fr/guides/html-form-submission.md): Ce que fait un navigateur quand un formulaire est envoyé : quels champs il transmet, comment il les encode, ce que le serveur doit répondre, et comment Ikiform répond.
