# Añade un formulario de feedback a tu sitio web o app

> Pon un formulario de feedback en cualquier página o dentro de tu app: una valoración, un comentario, la página de origen, filtro antispam y avisos cuando la nota es baja.

Publicado el 2026-10-25 · https://ikiform.com/es/guides/feedback-form

Añade un formulario corto con una valoración del 1 al 5, un comentario opcional y un email opcional, y envíalo a un backend de formularios para no tener que mantener un servidor. Manda la dirección de la página con cada respuesta, porque el navegador no te dará la URL completa por sí solo. Lleva las notas bajas al chat de tu equipo el mismo día y guarda todas las respuestas en un solo sitio para ver tendencias.

La mayoría de la gente nunca te dice qué falla en tu sitio. Simplemente se va. Un pequeño formulario de feedback atrapa a los pocos que sí lo dirán, en el momento en que lo notan, en la página que lo causó. Esta guía construye uno que encaja en cualquier sitio o app, envía la página de la que viene, deja fuera a los bots y pone las respuestas descontentas delante de tu equipo el mismo día.

## Cómo funciona un formulario de feedback

El formulario envía a Ikiform. Ikiform valida la nota, revisa el envío en busca de spam, lo guarda, avisa a tu equipo por email y manda un webhook firmado. Desde el webhook, las notas bajas van a un canal de chat y todas las respuestas pueden ir a una hoja de cálculo o una base de datos.

Diagrama: Cómo viaja un feedback desde tu página hasta tu equipo

1. Un visitante elige una nota, quizá escribe un comentario, y el formulario lo envía con la dirección de la página.
2. Ikiform comprueba que la nota sea un número entero del 1 al 5, pasa las comprobaciones antispam y guarda la respuesta.
3. La respuesta aparece en el panel, donde puedes filtrarla y exportarla como CSV.
4. Ikiform avisa a tu equipo por email. Si el visitante dejó un email, al responder le llega directamente a él.
5. Ikiform manda un webhook submission.created firmado con los campos y los metadatos del navegador.
6. Un pequeño relay publica solo las notas bajas en el chat de tu equipo, para que se lean hoy.

El relay es opcional. Empieza con el email y el panel, y añade el aviso por chat cuando las respuestas empiecen a acumularse.

## Formas de recoger feedback

Cada tipo de herramienta está pensado para un trabajo distinto. Elige según lo que necesitas ver cuando llega una respuesta, no por el aspecto del widget.

| Enfoque | Punto fuerte | Inconvenientes |
| --- | --- | --- |
| Herramientas de feedback para webs, como Hotjar o Usersnap | Capturas, anotaciones y contexto de la página sin más código que una etiqueta script. | Un script de terceros en cada página, respuestas guardadas en su producto y precios fijados por sus planes. |
| El widget de un rastreador de errores, como Sentry User Feedback | Vincula un reporte al error y, con la reproducción de sesiones activada, a una grabación de lo que pasó. | Pensado para bugs, no para opiniones. Primero necesitas el SDK del rastreador en tu app. |
| Creadores de formularios, como [Typeform](https://ikiform.com/alternatives/typeform), [Tally](https://ikiform.com/alternatives/tally) o [Google Forms](https://ikiform.com/alternatives/google-forms) | Formularios pulidos sin código. Buenos para una encuesta que envías por enlace. | Un iframe o una página aparte, así que la respuesta pierde la página a la que se refería salvo que la pases en la URL. |
| Tableros de feedback, como Canny | Votación pública y actualizaciones de estado sobre peticiones de funcionalidades. | Pensados para ideas, no para reacciones rápidas. Las respuestas son públicas por diseño. |
| Tu propia ruta de API y base de datos | Control total sobre los datos y cualquier lógica. | Tú construyes el filtro antispam, los límites de frecuencia, el email de aviso y la exportación, y necesitas un servidor. |
| Un backend de formularios, como Ikiform | Tu propio markup en cualquier sitio o app. Validación, filtro antispam, almacenamiento, email y webhooks firmados sin servidor. | Sin capturas, porque no se admite la subida de archivos, y sin votaciones ni reproducción de sesiones. |

Si necesitas capturas en cada reporte, una herramienta de feedback o un rastreador de errores justifica su script. Si quieres que el formulario se parezca a tu sitio, que las respuestas sean portables y decidir tú adónde van, usa un backend de formularios. El resto de esta guía hace eso.

## El formulario

Bastan tres preguntas: una nota, un comentario y un email opcional. La nota es lo que la gente responde; el comentario es donde está el valor; el email es cómo cierras el círculo. Elige tu stack abajo; la elección se aplica a todos los ejemplos de código de estas guías.

**HTML**

Un bloque de markup y un script corto. Sin JavaScript, el formulario sigue enviándose y redirigiendo; con él, el visitante se queda en la página y ve el error del servidor junto al botón. El script rellena `page` con la dirección sin la query string, para que los tokens de las URLs nunca lleguen a tu bandeja.

`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 lee la cabecera `Referer` de su propio envío. El formulario y la action comparten origen, así que el navegador manda la URL completa de la página, y la action se queda solo con el origen y la ruta. No hace falta ningún input oculto.

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

¿Quieres profundizar con Next.js? Lee [Crea un formulario de feedback in-app en Next.js](https://ikiform.com/es/guides/feedback-form/nextjs.md).

**Astro**

La página conoce su propia dirección mientras se renderiza, así que `Astro.url` rellena el input oculto `page` en el servidor. Los inputs vacíos llegan a la action como `null`, que Ikiform trata como ausente. La página debe renderizarse bajo demanda para que se muestre el resultado de la action.

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

¿Quieres profundizar con Astro? Lee [Crea un formulario de feedback in-app en Astro](https://ikiform.com/es/guides/feedback-form/astro.md).

**TanStack Start**

Una server function POST comprueba la nota antes de que nada salga de tu servidor. El componente toma `page` de `window.location` al enviar, así que es correcta incluso tras una navegación en el cliente.

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

¿Quieres profundizar con TanStack Start? Lee [Crea un formulario de feedback in-app en TanStack Start](https://ikiform.com/es/guides/feedback-form/tanstack-start.md).

**React Router**

Una resource route contiene la action, y `useFetcher` envía a ella desde cualquier página sin navegar. Registra la ruta como `api/feedback` en `app/routes.ts`. `useLocation` aporta la ruta, y la action la resuelve contra su propia URL, que tiene el origen de tu sitio.

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

¿Quieres profundizar con React Router? Lee [Crea un formulario de feedback dentro de la app con React Router](https://ikiform.com/es/guides/feedback-form/react-router.md).

**SvelteKit**

Un endpoint `+server.ts` reenvía el envío, así que el componente puede estar en un layout y funcionar en todas las páginas. `page` de `$app/state` da la URL actual. Declara `IKIFORM_SECRET_KEY` en `src/env.ts` con `defineEnvVars` para que `$app/env/private` pueda leerla. El endpoint devuelve `Response.json()`, ya que SvelteKit 3 depreca el 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}
```

¿Quieres profundizar con SvelteKit? Lee [Crea un formulario de feedback dentro de la app en SvelteKit](https://ikiform.com/es/guides/feedback-form/sveltekit.md).

**Nuxt**

Una server route guarda la clave en `runtimeConfig.ikiformSecretKey`, que se rellena desde `NUXT_IKIFORM_SECRET_KEY`. `useRoute().path` y `useRequestURL()` construyen la dirección de la página tanto en el servidor como en el navegador.

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

¿Quieres profundizar con Nuxt? Lee [Crea un formulario de feedback dentro de la app en Nuxt](https://ikiform.com/es/guides/feedback-form/nuxt.md).

**React**

Sin servidor propio, el navegador envía directamente a Ikiform. `submit()` de `@ikiform/sdk` manda el `FormData` y devuelve `{ ok }` o los errores de campo. Nunca pongas aquí una clave secreta; **Allowed domains** (dominios permitidos) protege el formulario en su lugar.

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

El mismo envío desde el navegador en un componente de un solo archivo. La etiqueta del comentario cambia cuando la nota es 1 o 2, lo que consigue respuestas más concretas de los visitantes descontentos.

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

- Sustituye `frm_xxxxxxxxxxxxxxxx` por el id de tu formulario, que está en el panel.
- `rating` es un grupo de radios con cinco valores. Ikiform lo guarda como número, así que las medias no necesitan parseo.
- `email` es opcional. La gente escribe con más sinceridad cuando no tiene que identificarse, y quien quiera una respuesta lo dejará.
- El input oculto `_gotcha` es el honeypot. Las personas nunca lo ven; los bots que rellenan todos los inputs se marcan solos como spam.
- `page` registra a qué página se refiere la respuesta. La siguiente sección explica por qué la envías tú.

> Las versiones de servidor envían con una clave secreta, así que Ikiform se salta **Allowed domains** (dominios permitidos) y su verificación humana. Todos los envíos vienen entonces de la IP de tu servidor, y el límite Standard de 5 por minuto por visitante cuenta a todos tus visitantes juntos. Elige el [límite de frecuencia](https://ikiform.com/es/docs/rate-limits.md) Relaxed o Custom para un formulario que envías a través de un servidor, y guarda `IKIFORM_SECRET_KEY` en variables de entorno solo de servidor.

Las guías de frameworks de la documentación cubren la configuración con más detalle: [Next.js](https://ikiform.com/es/docs/guides/nextjs.md), [Astro](https://ikiform.com/es/docs/guides/astro.md), [SvelteKit](https://ikiform.com/es/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/es/docs/guides/nuxt.md), [React](https://ikiform.com/es/docs/guides/react.md), [Vue](https://ikiform.com/es/docs/guides/vue.md) y [HTML puro](https://ikiform.com/es/docs/html.md). La [plantilla de formulario de feedback](https://ikiform.com/forms/feedback) genera los mismos campos en HTML, React y una server action; las plantillas de [reporte de bugs](https://ikiform.com/forms/bug-report) y de [encuesta](https://ikiform.com/forms/survey) cubren los formularios más largos.

## Define los campos

Pega una definición en la pestaña **Fields** del formulario e Ikiform la aplica en cada envío, desde cualquier página. Pruébalo abajo: la vista previa ejecuta el validador de Ikiform en tu navegador, así que una nota de `0` o un email mal formado muestra el `422` que recibirías.

Formulario: Page feedback

- **¿Qué tan útil fue esta página?** (`rating`, rating, obligatorio)
- **¿Qué la haría mejor?** (`comment`, textarea)
- **Email, si quieres respuesta** (`email`, email)
- **Página** (`page`, url)

Un `rating` acepta un número entero del 1 a su `max`, aquí 5. Puede llegar a 10, pero cinco opciones se responden más rápido.

| Campo | Tipo | Por qué |
| --- | --- | --- |
| `rating` | `rating`, máx. 5 | Un clic. Se guarda como número, así que puedes promediarlo por página |
| `comment` | `textarea`, hasta 4.000 caracteres | El motivo de la nota |
| `email` | `email`, opcional | Responder a quien hace una pregunta. Pasa a ser el reply-to del aviso |
| `page` | `url`, opcional | A qué página se refiere la respuesta |

Con los campos definidos, cualquier otro nombre en el envío se rechaza como `Unknown field`. Si más adelante envías más contexto, como una versión de la app o el nombre de un plan, añade antes un campo para ello. Los ids de campo van en snake_case en minúsculas.

## Saber a qué página se refiere

Una nota sin página es difícil de aprovechar. Ikiform registra los metadatos del navegador con cada envío, pero uno de esos valores es menos útil de lo que parece:

| Valor | De dónde viene | Qué obtienes |
| --- | --- | --- |
| `meta.user_agent` | Lo registra Ikiform | El navegador y el sistema operativo, en un envío desde el navegador |
| `meta.origin` | Lo registra Ikiform | El origen de tu sitio, como `https://example.com` |
| `meta.referrer` | Lo registra Ikiform | Normalmente solo el origen. Los navegadores usan por defecto la política `strict-origin-when-cross-origin`, que quita la ruta al enviar a otro sitio |
| `meta.country` | Lo registra Ikiform | Un código de país de dos letras |
| `page` | Lo envía tu formulario | La ruta completa, para saber a qué página se refiere la respuesta |

Por eso todos los ejemplos de arriba envían `page` por su cuenta. Las versiones HTML y SPA leen `location`; las versiones de servidor leen la dirección que su framework ya conoce. Todas quitan la query string, que puede contener términos de búsqueda, ids de seguimiento o tokens de un solo uso que no quieres guardar.

> Las versiones de servidor envían desde tu servidor, así que `meta.user_agent` es el runtime de tu servidor, no el navegador del visitante. Para el feedback sobre páginas rara vez importa. Para los reportes de bugs sí, y la [guía de reportes de bugs](https://ikiform.com/es/guides/feedback-form/bug-report.md) reenvía el user agent del navegador como un campo.

## Dónde preguntar

El sitio donde está el formulario decide lo que oyes. Un formulario en el pie de cada página recibe un goteo de opiniones fuertes. Una pregunta al final de una tarea recibe respuestas constantes y concretas.

| Ubicación | Bueno para | Cuidado con |
| --- | --- | --- |
| Final de una página de documentación o un artículo | Encontrar páginas que no responden a la pregunta | Poco volumen por página. Léelo por sección, no por página |
| Tras una tarea en tu app, como una exportación o un primer proyecto | Cómo se siente un flujo concreto, mientras está reciente | Pregunta una vez por persona, no cada vez que terminan la tarea |
| Un botón flotante en cada pantalla | Cualquier cosa, en cualquier momento. Consulta el [widget de React](https://ikiform.com/es/guides/feedback-form/react-feedback-widget.md) | No debe tapar tus propios botones, banners de cookies ni burbujas de chat |
| Un enlace en un email | Notas que quieres seguir en el tiempo, como el [NPS](https://ikiform.com/es/guides/feedback-form/nps-survey.md) | El cliente de email no es tu página, así que el formulario necesita una página de destino en tu sitio |

Mantén el formulario pequeño esté donde esté. Pide primero la nota y muestra el cuadro de comentario junto a ella, no en un segundo paso. Un segundo paso pierde a la mayoría, y una nota sin motivo sigue valiendo la pena.

## Pon las notas bajas delante de las personas

Diagrama: Qué pasa con una respuesta según su nota

1. Todas las respuestas se guardan, sea cual sea su nota y la página de la que vengan.
2. Ikiform manda un webhook submission.created firmado a tu relay.
3. El relay lee la valoración. Las notas de 3 a 5 no necesitan nada más de momento.
4. Una nota de 1 o 2 va al chat de tu equipo con la página y el comentario.
5. Si el visitante dejó un email, alguien responde el mismo día.
6. Una vez por semana, la exportación muestra las medias por página, que es donde aparecen las tendencias.

Avisa solo de las notas malas. Un canal que suena por cada 5 de 5 acaba silenciado en una semana.

Los emails de aviso van a hasta 10 destinatarios confirmados por formulario y llegan con cada respuesta. Eso está bien para unas pocas respuestas al día. A partir de ahí, manda las notas bajas al chat con un relay como esta ruta de Next.js; el mismo código funciona en 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` comprueba la firma y el timestamp, así que nadie más puede publicar en tu canal a través de esta URL.
- Las notas por encima de 2 responden `204` y se detienen. Ikiform considera entregado cualquier 2xx.
- Una llamada fallida a Slack responde `502`, e Ikiform reintenta con el mismo `webhook-id`, así que una caída breve no hace perder un aviso.
- Los valores de los campos son lo que escribió el visitante. El relay recorta el comentario a 500 caracteres antes de que llegue al chat.

Añade el endpoint en **Webhooks** y copia su secreto `whsec_` en `IKIFORM_WEBHOOK_SECRET`. Un espacio de trabajo puede tener 3 endpoints. Para rutas sin código, consulta [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) y [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md). Para el feedback que debería convertirse en un ticket, [Linear y GitHub Issues](https://ikiform.com/blog/form-submissions-to-linear-and-github-issues.md) tiene un relay que crea uno por envío.

> No hay integraciones de un clic. Cada herramienta se alcanza mediante un [webhook](https://ikiform.com/es/docs/webhooks.md) firmado, directamente o a través de una herramienta de automatización como [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) o [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).

## Deja fuera a los bots

Un formulario de feedback en cada página es un formulario público en cada página. Los bots lo encuentran, y casi todo lo que envían son enlaces. Los ajustes por defecto de Ikiform se ocupan de la mayor parte:

| Defensa | Qué frena | Configuración |
| --- | --- | --- |
| Honeypot | Bots que rellenan todos los inputs | El input oculto `_gotcha` de los ejemplos |
| Puntuación de spam | Tres o más enlaces (+2), frases de spam conocidas (+2), emails desechables (+2), repeticiones idénticas (+3). Con 5 se guarda como spam | Ninguna |
| Límites de frecuencia | Scripts que envían en bucle | Activos por defecto: Standard, 5 por minuto por visitante y 300 por minuto para todo el formulario |
| Dominios permitidos | Envíos desde otros sitios | Añade tu dominio en los ajustes del formulario |
| Verificación humana | Bots que usan un navegador real | Pon **Spam check** en **Ikiform**. Los envíos con `fetch` desde el navegador necesitarán entonces un token de `challenge()` |

El spam recibe la misma respuesta de éxito que un envío real, no manda email ni webhook y no cuenta para el uso. Un visitante real que pegue tres enlaces en un comentario puede activar la regla de enlaces junto con una repetición, así que revisa de vez en cuando el filtro **Spam**. Marcar una respuesta como no spam envía entonces su webhook. La [documentación antispam](https://ikiform.com/es/docs/spam.md) explica cada comprobación.

## Lee las respuestas

Lee todos los comentarios las primeras semanas. Después, la vista útil es la nota media por página, de menor a mayor, con los comentarios al lado.

### Exportar

Abre **Submissions**, elige el formulario de feedback, escoge un rango de fechas y pulsa **Export CSV**. Las columnas son `id`, `created_at`, `form` y `status`, y luego una por campo, así que con los campos de arriba `rating` es la columna E y `page` la columna H. Una exportación admite hasta 100.000 filas; la [guía de exportación](https://ikiform.com/es/docs/guides/export.md) cubre la API para más.

Importa el archivo en Google Sheets y pon esto en una hoja vacía. Lista cada página con su nota media y el número de respuestas, de peor a mejor:

`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. Ignora las páginas con solo una o dos respuestas. Un visitante enfadado no es una tendencia.
2. Lee los comentarios de las tres páginas con peor nota. La solución suele estar en los primeros.
3. Cuando cambies una página, compara su media de las semanas anteriores y posteriores, no la cifra histórica.

> El feedback no es anónimo para Ikiform aunque no haya email: cada respuesta registra el user agent, el origen, el referrer y el país, y un hash de la IP que nunca se devuelve. Indica lo que recoges en tu aviso de privacidad.

## Elige tu caso

El formulario de arriba cubre reacciones rápidas en cualquier página. Estas páginas cubren feedback con otro propósito: un bug que reproducir, una petición que valorar, una nota que seguir o un cliente que se va.

- [Formulario de reporte de bugs](https://ikiform.com/es/guides/feedback-form/bug-report.md): Un formulario de reporte de bugs que pide a la gente los pasos y la gravedad, y deja que la página añada por su cuenta la URL, el navegador, el viewport y los errores recientes.
- [Formulario de peticiones de funcionalidades](https://ikiform.com/es/guides/feedback-form/feature-request.md): Un formulario de peticiones de funcionalidades que pregunta por el problema en lugar de la solución, guarda las peticiones por tema, cuenta personas y no filas, y les avisa por email cuando sale.
- [Widget de feedback en React](https://ikiform.com/es/guides/feedback-form/react-feedback-widget.md): Un botón flotante de feedback para una app React que abre un panel pequeño, envía la pantalla, la versión de la app y la cuenta con cada respuesta, y nunca pierde un borrador.
- [Feedback de beta testers](https://ikiform.com/es/guides/feedback-form/beta-tester-feedback.md): Un formulario de feedback para betas con un enlace personal por tester y build, dos escalas de valoración y un resumen semanal que muestra si cada build mejoró.
- [Encuesta NPS](https://ikiform.com/es/guides/feedback-form/nps-survey.md): Haz una encuesta NPS con una puntuación de 0 a 10 en el email, una pregunta de seguimiento en tu propia página que cambia según la puntuación y un script que calcula el NPS.
- [Encuesta de cancelación](https://ikiform.com/es/guides/feedback-form/cancellation-survey.md): Pregunta por qué cancelan tus clientes dentro del flujo de baja de tu SaaS, deja Omitir a un clic, guarda el motivo en Stripe e Ikiform y nunca retrases la baja.
- [Formulario de feedback en Next.js](https://ikiform.com/es/guides/feedback-form/nextjs.md): Un formulario de feedback in-app en Next.js: una server action que lee la sesión, toma la página de la petición y reenvía la nota con el usuario incluido.
- [Formulario de feedback en Astro](https://ikiform.com/es/guides/feedback-form/astro.md): Un formulario de feedback in-app en Astro: una action que toma el usuario de los locals del middleware, la página de la petición y la pantalla del patrón de ruta.
- [Formulario de feedback en TanStack Start](https://ikiform.com/es/guides/feedback-form/tanstack-start.md): Un formulario de feedback in-app en TanStack Start: una server function POST con middleware de sesión, el id de ruta como contexto y la página leída de las cabeceras.
- [Formulario de feedback en React Router](https://ikiform.com/es/guides/feedback-form/react-router.md): Un formulario de feedback en React Router 8: la action de una resource route lee la sesión de la cookie, un fetcher envía desde el layout y un envío sin JS redirige de vuelta.
- [Formulario de feedback en SvelteKit](https://ikiform.com/es/guides/feedback-form/sveltekit.md): Un formulario de feedback dentro de una app SvelteKit: un formulario en el layout envía a una única form action, que añade en el servidor el usuario desde locals y la página desde la petición.
- [Formulario de feedback en Nuxt](https://ikiform.com/es/guides/feedback-form/nuxt.md): Un formulario de feedback dentro de una app Nuxt: una server route que lee la sesión del usuario, toma la página del Referer y sigue funcionando cuando JavaScript no ha cargado.

## Comprueba que funciona

Repasa esto antes de poner el formulario en todas las páginas. Ikiform no tiene plan gratuito. Probar es gratis: un espacio de trabajo sin plan acepta 50 envíos de prueba al mes desde `localhost`. Para salir a producción, elige Pro desde 6 $/mes por 500 envíos, o paga una vez: 10.000 envíos por 49 $ o 50.000 envíos por 149 $, con créditos que nunca caducan. Consulta los [precios](https://ikiform.com/es/pricing).

1. En **Allowed domains** del formulario, añade el dominio de tu sitio, y `localhost` mientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier lugar.
2. Elige una nota, escribe un comentario y envía. La línea de agradecimiento sustituye al formulario, o el navegador llega a tu página de redirección en la versión de HTML puro.
3. Abre **Submissions** en el panel. La respuesta muestra `rating` como número y `page` con la ruta completa.
4. Envía una nota de `6` con `curl` o desde la consola del navegador. Ikiform responde `422` nombrando `rating`.
5. Revisa la bandeja de entrada de cada destinatario de las notificaciones. Cada uno recibe un email con los campos, y **Reply** va a quien envió el formulario cuando este tiene un campo `email`.
6. Envía una nota de 1. Tu relay la publica en el chat del equipo, y la entrega en **Webhooks** muestra `succeeded`.
7. Envía con el honeypot relleno, `_gotcha=bot`. Igualmente recibe una respuesta de éxito, pero el envío se guarda como spam y no envía email ni webhook.
8. ¿No llegó nada? Cambia el filtro de estado del panel a **Spam**. El spam recibe la misma respuesta de éxito que un envío real.

## Solución de problemas

| Problema | Qué hacer |
| --- | --- |
| `403 origin_not_allowed`, o todos los envíos acaban en spam | El dominio de la página no está en **Allowed domains** del formulario. Añádelo, y `localhost` para las pruebas locales. Los subdominios de un dominio de la lista están permitidos. |
| `422` con `Rating must be at most 5` | El formulario envía una nota mayor que el `max` del campo. Cambia los valores de los radios o sube `max` en la definición, hasta 10. |
| `422` con un Unknown field | El formulario envía un nombre que no está en la definición, a menudo un input oculto nuevo. Añade un campo con ese id o quita el input. |
| `page` está vacío | El script que lo rellena no se ejecutó, o se ejecutó antes de que existiera el formulario. Ponlo después del formulario, o asigna `page` al enviar como hacen los ejemplos SPA. |
| `meta.referrer` solo muestra tu dominio | Es lo esperado. Por defecto, los navegadores solo envían el origen en los envíos entre sitios. Usa el campo `page` en su lugar. |
| El envío está en el panel pero no llegó ningún email ni webhook | Se guardó como spam. El spam no envía nada. Ábrelo en el filtro **Spam** para ver los motivos y márcalo como no spam para enviar su email y sus webhooks. |
| `429 rate_limited` | Un visitante envió más de lo que permite el límite de frecuencia del formulario. Standard permite 5 por minuto por visitante. Espera lo que indique `Retry-After`, o elige Relaxed o Custom para formularios que envían desde una IP compartida. |
| `402 test_limit_reached` | El espacio de trabajo no tiene plan y ya usó sus 50 pruebas gratuitas desde `localhost` este mes. Elige un plan o usa **Send test** en el panel. |

## Preguntas

### ¿Cómo añado un formulario de feedback a un sitio web sin backend?

Apunta el action del formulario a un endpoint de un backend de formularios. El formulario puede ser HTML puro en un sitio estático; el backend valida, guarda y te envía por email cada respuesta, y manda un webhook si la quieres en otro sitio.

### ¿Cómo sé de qué página vino el feedback?

Envía la dirección de la página como un campo. Ikiform también registra el referrer, pero los navegadores suelen enviar solo tu dominio en un envío entre sitios, así que un campo de página rellenado desde location es la forma fiable.

### ¿Debería ser anónimo el feedback?

Haz que el email sea opcional. La gente es más franca sin él, y quien quiera respuesta lo dejará. Ikiform sigue registrando el navegador, el origen, el referrer y el país con cada respuesta.

### ¿Pueden los visitantes adjuntar una captura?

No como archivo. Ikiform rechaza los envíos con archivos con 422 files_not_supported. Pide un enlace a una captura, o usa una herramienta de feedback basada en capturas si necesitas una en cada reporte.

### ¿Puedo usar una escala del 0 al 10 en lugar de estrellas?

No con un campo rating, que va del 1 a como mucho 10. Una nota del 0 al 10 usa un campo number con min 0, max 10 e integer true. La guía de NPS muestra la configuración completa.

### ¿Cómo recibo avisos del feedback negativo?

Añade un webhook a un pequeño relay que compruebe la valoración y publique solo las notas bajas en Slack o Discord. Los emails de aviso cubren todas las respuestas; el relay cubre las que necesitan respuesta hoy.

### ¿Ikiform es gratis?

No, no hay plan gratuito. Probar es gratis: puedes enviar 50 envíos de prueba al mes desde localhost antes de pagar. Para pasar a producción, Pro empieza en 6 $/mes por 500 envíos, o puedes pagar una sola vez 49 $ por 10.000 envíos o 149 $ por 50.000 envíos, con créditos que nunca caducan.

## Más guías

- [Formulario de reporte de bugs](https://ikiform.com/es/guides/feedback-form/bug-report.md): Un formulario de reporte de bugs que pide a la gente los pasos y la gravedad, y deja que la página añada por su cuenta la URL, el navegador, el viewport y los errores recientes.
- [Formulario de peticiones de funcionalidades](https://ikiform.com/es/guides/feedback-form/feature-request.md): Un formulario de peticiones de funcionalidades que pregunta por el problema en lugar de la solución, guarda las peticiones por tema, cuenta personas y no filas, y les avisa por email cuando sale.
- [Widget de feedback en React](https://ikiform.com/es/guides/feedback-form/react-feedback-widget.md): Un botón flotante de feedback para una app React que abre un panel pequeño, envía la pantalla, la versión de la app y la cuenta con cada respuesta, y nunca pierde un borrador.
- [Feedback de beta testers](https://ikiform.com/es/guides/feedback-form/beta-tester-feedback.md): Un formulario de feedback para betas con un enlace personal por tester y build, dos escalas de valoración y un resumen semanal que muestra si cada build mejoró.
- [Encuesta NPS](https://ikiform.com/es/guides/feedback-form/nps-survey.md): Haz una encuesta NPS con una puntuación de 0 a 10 en el email, una pregunta de seguimiento en tu propia página que cambia según la puntuación y un script que calcula el NPS.
- [Encuesta de cancelación](https://ikiform.com/es/guides/feedback-form/cancellation-survey.md): Pregunta por qué cancelan tus clientes dentro del flujo de baja de tu SaaS, deja Omitir a un clic, guarda el motivo en Stripe e Ikiform y nunca retrases la baja.
- [Formulario de feedback en Next.js](https://ikiform.com/es/guides/feedback-form/nextjs.md): Un formulario de feedback in-app en Next.js: una server action que lee la sesión, toma la página de la petición y reenvía la nota con el usuario incluido.
- [Formulario de feedback en Astro](https://ikiform.com/es/guides/feedback-form/astro.md): Un formulario de feedback in-app en Astro: una action que toma el usuario de los locals del middleware, la página de la petición y la pantalla del patrón de ruta.
- [Formulario de feedback en TanStack Start](https://ikiform.com/es/guides/feedback-form/tanstack-start.md): Un formulario de feedback in-app en TanStack Start: una server function POST con middleware de sesión, el id de ruta como contexto y la página leída de las cabeceras.
- [Formulario de feedback en React Router](https://ikiform.com/es/guides/feedback-form/react-router.md): Un formulario de feedback en React Router 8: la action de una resource route lee la sesión de la cookie, un fetcher envía desde el layout y un envío sin JS redirige de vuelta.
- [Formulario de feedback en SvelteKit](https://ikiform.com/es/guides/feedback-form/sveltekit.md): Un formulario de feedback dentro de una app SvelteKit: un formulario en el layout envía a una única form action, que añade en el servidor el usuario desde locals y la página desde la petición.
- [Formulario de feedback en Nuxt](https://ikiform.com/es/guides/feedback-form/nuxt.md): Un formulario de feedback dentro de una app Nuxt: una server route que lee la sesión del usuario, toma la página del Referer y sigue funcionando cuando JavaScript no ha cargado.
- [Formulario de captación de leads](https://ikiform.com/es/guides/lead-capture.md): Un formulario de captación de leads en tu propio sitio que te avisa por email con reply-to, avisa al chat del equipo y alimenta tu CRM, para responder mientras el lead sigue ahí.
- [Formulario de suscripción a newsletter](https://ikiform.com/es/guides/newsletter-signup.md): Crea un formulario de suscripción a tu newsletter con tu propio markup, registra el consentimiento, bloquea los registros de bots y pasa cada suscriptor a Kit, MailerLite, beehiiv o cualquier herramienta.
- [Envío de formularios HTML](https://ikiform.com/es/guides/html-form-submission.md): Qué hace un navegador cuando se envía un formulario: qué campos envía, cómo los codifica, qué debería responder el servidor y cómo responde Ikiform.
