# JavaScript y fetch

> El endpoint de envíos en detalle: formatos de cuerpo, cabeceras, CORS, respuestas e idempotencia.

Cualquier código que pueda enviar una petición HTTP puede enviar a Ikiform. Envía JSON, `FormData` o datos urlencoded a `https://ikiform.com/f/<formId>` y lee una respuesta JSON. El [SDK](https://ikiform.com/es/docs/sdk.md) lo envuelve en una función, pero es opcional. Para una configuración paso a paso en un stack concreto, consulta [Guías por plataforma](#guides).

## Con fetch

**JSON** (`submit.js`)

```js
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({
    name: "Ada Lovelace",
    email: "ada@example.com",
    message: "Hello",
  }),
});

const result = await response.json();
if (!result.ok) console.error(result.error.code, result.error.fields);
```

**FormData** (`form.js`)

```js
const form = document.querySelector("form");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  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.replaceWith("Thanks, we got it.");
});
```

**SDK** (`submit.ts`)

```ts
import { submit } from "@ikiform/sdk";

const result = await submit("frm_xxxxxxxxxxxxxxxx", {
  name: "Ada Lovelace",
  email: "ada@example.com",
});

if (result.ok) console.log("Stored", result.id);
else console.error(result.error.code, result.error.fields);
```

## Pruébalo

El constructor de peticiones interactivo arma un envío a partir de filas de campos, muestra su código curl, fetch y SDK, y puede enviar un POST real cuando escribes el id de tu formulario. No se envía nada automáticamente.

## Endpoints

| Método | Ruta | Qué hace |
| --- | --- | --- |
| `POST` | `/f/<formId>` | Crea un envío |
| `OPTIONS` | `/f/<formId>` | Preflight de CORS, siempre `204` |
| `GET` | `/f/<formId>/schema` | La definición pública de los campos del formulario, consulta [shadcn/ui](https://ikiform.com/es/docs/shadcn.md#schema-endpoint) |
| `GET` | `/f/<formId>/thanks` | La página de agradecimiento alojada |
| `GET` | `/f/<formId>/challenge` | El frame de verificación humana de Ikiform, que usa el SDK |

Abrir `/f/<formId>` en una pestaña del navegador es un `GET`, que responde `404 not_found`. Es lo esperado: el endpoint solo acepta envíos.

## Formatos de cuerpo

| Content-Type | Forma | Notas |
| --- | --- | --- |
| `application/json` y tipos `+json` | Un objeto JSON | Los valores pueden ser cadenas, números, booleanos, arrays y objetos, con un anidamiento máximo de 3 niveles contando el objeto principal |
| `application/x-www-form-urlencoded` | Pares de nombre y valor | Los nombres repetidos y `name[]` se convierten en arrays |
| `multipart/form-data` | Pares de nombre y valor | Igual que urlencoded; los archivos reales se rechazan |
| Cualquier otro |  | `415 unsupported_media_type` |

Los cuerpos deben ser UTF-8. Un `charset` distinto de `utf-8` recibe `415 unsupported_charset`, y los bytes que no son UTF-8 válido reciben el mismo código. Un array JSON o un cuerpo mal formado recibe `400 invalid_body`.

## Cabeceras

### Cabeceras de la petición

| Cabecera | Obligatoria | Efecto |
| --- | --- | --- |
| `Content-Type` | Sí | Elige el parser del cuerpo, consulta arriba |
| `Accept` | No | `application/json` fuerza una respuesta JSON, consulta [JSON o una redirección](https://ikiform.com/es/docs/how-it-works.md#response-modes) |
| `Idempotency-Key` | No | Hace que los reintentos sean seguros, hasta 255 caracteres, consulta [Idempotencia](#idempotency) |
| `Authorization` | No | `Bearer ik_sk_...` o `Bearer ik_pub_...`, consulta [Desde un servidor](https://ikiform.com/es/docs/guides/server.md) |
| `Origin`, `Referer` | Los envían los navegadores | Se comparan con los dominios permitidos del formulario |

### Cabeceras de la respuesta

| Cabecera | Cuándo | Valor |
| --- | --- | --- |
| `X-Request-Id` | Cada respuesta | `req_...`, repetido como `request_id` en los cuerpos de error |
| `Retry-After` | `429` | Segundos hasta que se permite el siguiente envío |
| `Location` | `303` éxito en el navegador | Tu redirección o la página de agradecimiento alojada |
| `Access-Control-Allow-Origin` | Cuando la petición tiene un `Origin` | El origen de la propia petición |
| `Access-Control-Expose-Headers` | Con CORS | `X-Request-Id, Retry-After`, para que el código del navegador pueda leer ambas |
| `Cache-Control` | Éxito | `no-store` |

## CORS

El endpoint de envíos acepta envíos cross-origin desde cualquier sitio. Los preflights siempre tienen éxito y devuelven el origen de la petición. No se usan cookies ni credenciales, así que CORS no es lo que protege tu formulario: lo protegen los **dominios permitidos**. Con dominios permitidos configurados, un envío desde otro sitio se guarda como spam o se rechaza, según el ajuste del formulario. Consulta [Dominios permitidos](https://ikiform.com/es/docs/spam.md#allowed-domains).

`Preflight`:

```http
OPTIONS /f/frm_xxxxxxxxxxxxxxxx HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, Accept, Idempotency-Key, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
```

> La [API REST](https://ikiform.com/es/docs/api.md) bajo `/api/v1` es distinta: no envía cabeceras CORS y responde a los preflights del navegador con `405`, así que una clave secreta nunca se puede usar desde una página web.

## Respuestas

El éxito es `201` con el id del nuevo envío. Los errores usan el mismo formato en todas partes, con los errores de campo indexados por id de campo cuando falla la validación. Todos los códigos están en [Errores](https://ikiform.com/es/docs/errors.md).

**201** (`Created`)

```json
{ "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" }
```

**422** (`Validación fallida`)

```json
{
  "ok": false,
  "error": {
    "code": "validation_failed",
    "message": "Some fields are invalid.",
    "fields": { "email": ["Enter a valid email address"] }
  },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

> Un envío que puntúa como spam igualmente recibe `201`. El spam se guarda aparte y nunca llega a tus webhooks ni a tu bandeja de entrada.

## Idempotencia

Envía una cabecera `Idempotency-Key`, o un campo `_idempotency`, para que los reintentos sean seguros. Las claves tienen hasta 255 caracteres y valen para el formulario durante 24 horas. Si se envían ambas, gana la cabecera.

| Reintento con | Resultado |
| --- | --- |
| La misma clave y los mismos campos | Se repite la primera respuesta. No se crea un segundo envío. |
| La misma clave y campos distintos | `422 idempotency_mismatch` |
| La misma clave mientras la primera petición sigue en curso | `409 idempotency_in_progress` |

Los envíos que fallan antes de guardarse, como los errores de validación, no conservan la clave, así que puedes corregir los datos y reintentar con la misma clave. Un bucle de reintentos seguro:

`retry.js`:

```js
const key = crypto.randomUUID();

async function send(attempt = 1) {
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: { "Content-Type": "application/json", "Idempotency-Key": key },
    body: JSON.stringify({ email: "ada@example.com" }),
  }).catch(() => null);
  if (response && response.status < 500 && response.status !== 429) return response.json();
  if (attempt === 3) throw new Error("Ikiform is unreachable");
  const wait = Number(response?.headers.get("Retry-After")) || attempt * 2;
  await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  return send(attempt + 1);
}
```

## Guías por plataforma

### Frameworks

- [Next.js](https://ikiform.com/es/docs/guides/nextjs.md): Un componente de cliente o una server action.
- [React](https://ikiform.com/es/docs/guides/react.md): Estados de carga, error y éxito.
- [Vue](https://ikiform.com/es/docs/guides/vue.md): Un componente de archivo único.
- [Nuxt](https://ikiform.com/es/docs/guides/nuxt.md): Envía antes de la hidratación y con `$fetch` después.
- [Svelte](https://ikiform.com/es/docs/guides/svelte.md): Un componente de Svelte 5 y una server action.
- [SvelteKit](https://ikiform.com/es/docs/guides/sveltekit.md): Una página prerenderizada con mejora progresiva.
- [Astro](https://ikiform.com/es/docs/guides/astro.md): Un formulario sin JavaScript, opcionalmente mejorado.
- [Gatsby](https://ikiform.com/es/docs/guides/gatsby.md): Un formulario estático o una Gatsby Function.
- [JavaScript puro](https://ikiform.com/es/docs/guides/javascript.md): `fetch` con errores en línea.

### Generadores de sitios estáticos

- [Hugo](https://ikiform.com/es/docs/guides/hugo.md): Un partial y un shortcode para los archivos de contenido.
- [Jekyll](https://ikiform.com/es/docs/guides/jekyll.md): Un include y la configuración de GitHub Pages.
- [Eleventy](https://ikiform.com/es/docs/guides/eleventy.md): Un shortcode para Nunjucks, Liquid y Markdown.

### Creadores de sitios

- [Webflow](https://ikiform.com/es/docs/guides/webflow.md): El Form Block con un action personalizado.
- [Framer](https://ikiform.com/es/docs/guides/framer.md): Un webhook de formulario o un code component.
- [WordPress](https://ikiform.com/es/docs/guides/wordpress.md): El bloque Custom HTML o un shortcode.
- [Wix](https://ikiform.com/es/docs/guides/wix.md): Un embed HTML que envía con `fetch`.
- [Squarespace](https://ikiform.com/es/docs/guides/squarespace.md): Un bloque de código, con o sin JavaScript.
- [Carrd](https://ikiform.com/es/docs/guides/carrd.md): Un formulario personalizado que envía a una URL.
