# JavaScript e fetch

> O endpoint de envio em detalhe: formatos de corpo, headers, CORS, respostas e idempotência.

Qualquer código que consiga fazer uma requisição HTTP pode enviar para o Ikiform. Envie JSON, `FormData` ou dados urlencoded para `https://ikiform.com/f/<formId>` e leia uma resposta JSON. O [SDK](https://ikiform.com/pt-br/docs/sdk.md) encapsula isso em uma função, mas é opcional. Para uma configuração passo a passo em uma stack específica, veja [Guias por plataforma](#guides).

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

## Experimente

O construtor de requisições interativo monta um envio a partir de linhas de campos, mostra o código curl, fetch e SDK correspondente e pode enviar um POST real quando você informa o id do seu formulário. Nada é enviado automaticamente.

## Endpoints

| Método | Caminho | O que faz |
| --- | --- | --- |
| `POST` | `/f/<formId>` | Cria um envio |
| `OPTIONS` | `/f/<formId>` | Preflight de CORS, sempre `204` |
| `GET` | `/f/<formId>/schema` | A definição pública de campos do formulário, veja [shadcn/ui](https://ikiform.com/pt-br/docs/shadcn.md#schema-endpoint) |
| `GET` | `/f/<formId>/thanks` | A página de agradecimento hospedada |
| `GET` | `/f/<formId>/challenge` | O frame de verificação humana do Ikiform, usado pelo SDK |

Abrir o próprio `/f/<formId>` em uma aba do navegador é um `GET`, que responde `404 not_found`. Isso é esperado: o endpoint só aceita envios.

## Formatos de corpo

| Content-Type | Formato | Observações |
| --- | --- | --- |
| `application/json` e tipos `+json` | Um objeto JSON | Os valores podem ser strings, números, booleanos, arrays e objetos, com no máximo 3 níveis de aninhamento contando o objeto de topo |
| `application/x-www-form-urlencoded` | Pares de nome e valor | Nomes repetidos e `name[]` viram arrays |
| `multipart/form-data` | Pares de nome e valor | Igual a urlencoded; arquivos de verdade são recusados |
| Qualquer outro |  | `415 unsupported_media_type` |

O corpo precisa ser UTF-8. Um `charset` diferente de `utf-8` recebe `415 unsupported_charset`, e bytes que não são UTF-8 válido recebem o mesmo código. Um array JSON ou um corpo malformado recebe `400 invalid_body`.

## Headers

### Headers da requisição

| Header | Obrigatório | Efeito |
| --- | --- | --- |
| `Content-Type` | Sim | Escolhe o parser do corpo, veja acima |
| `Accept` | Não | `application/json` força uma resposta JSON, veja [JSON ou redirecionamento](https://ikiform.com/pt-br/docs/how-it-works.md#response-modes) |
| `Idempotency-Key` | Não | Torna as novas tentativas seguras, até 255 caracteres, veja [Idempotência](#idempotency) |
| `Authorization` | Não | `Bearer ik_sk_...` ou `Bearer ik_pub_...`, veja [A partir de um servidor](https://ikiform.com/pt-br/docs/guides/server.md) |
| `Origin`, `Referer` | Enviados pelos navegadores | Comparados aos domínios permitidos do formulário |

### Headers da resposta

| Header | Quando | Valor |
| --- | --- | --- |
| `X-Request-Id` | Toda resposta | `req_...`, repetido como `request_id` nos corpos de erro |
| `Retry-After` | `429` | Segundos até o próximo envio ser permitido |
| `Location` | `303` sucesso no navegador | Seu redirecionamento ou a página de agradecimento hospedada |
| `Access-Control-Allow-Origin` | Quando a requisição tem um `Origin` | A própria origem da requisição |
| `Access-Control-Expose-Headers` | Com CORS | `X-Request-Id, Retry-After`, para o código no navegador conseguir ler os dois |
| `Cache-Control` | Sucesso | `no-store` |

## CORS

O endpoint de envio aceita envios cross-origin de qualquer site. Preflights sempre dão certo e devolvem a origem da requisição. Nenhum cookie ou credencial é usado, então não é o CORS que protege seu formulário: são os **domínios permitidos**. Com domínios permitidos definidos, um envio de outro site é armazenado como spam ou recusado, conforme a configuração do formulário. Veja [Domínios permitidos](https://ikiform.com/pt-br/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
```

> A [API REST](https://ikiform.com/pt-br/docs/api.md) em `/api/v1` é diferente: ela não envia headers de CORS e responde preflights do navegador com `405`, então uma chave secreta nunca pode ser usada a partir de uma página web.

## Respostas

Sucesso é `201` com o id do novo envio. Os erros usam o mesmo envelope em todo lugar, com erros de campo indexados pelo id do campo quando a validação falha. Todos os códigos estão em [Erros](https://ikiform.com/pt-br/docs/errors.md).

**201** (`Created`)

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

**422** (`Validação falhou`)

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

> Um envio pontuado como spam ainda recebe `201`. Spam é armazenado à parte e nunca chega aos seus webhooks nem à sua caixa de entrada.

## Idempotência

Envie um header `Idempotency-Key`, ou um campo `_idempotency`, para tornar as novas tentativas seguras. As chaves têm até 255 caracteres e valem por formulário durante 24 horas. O header vence quando os dois são enviados.

| Nova tentativa com | Resultado |
| --- | --- |
| A mesma chave e os mesmos campos | A primeira resposta é repetida. Nenhum segundo envio é criado. |
| A mesma chave e campos diferentes | `422 idempotency_mismatch` |
| A mesma chave enquanto a primeira requisição ainda está rodando | `409 idempotency_in_progress` |

Requisições que falham antes de o envio ser armazenado, como erros de validação, não guardam a chave, então você pode corrigir os dados e tentar de novo com a mesma chave. Um loop seguro de novas tentativas:

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

## Guias por plataforma

### Frameworks

- [Next.js](https://ikiform.com/pt-br/docs/guides/nextjs.md): Um client component ou uma server action.
- [React](https://ikiform.com/pt-br/docs/guides/react.md): Estados de carregamento, erro e sucesso.
- [Vue](https://ikiform.com/pt-br/docs/guides/vue.md): Um single-file component.
- [Nuxt](https://ikiform.com/pt-br/docs/guides/nuxt.md): Envios antes da hidratação e com `$fetch` depois.
- [Svelte](https://ikiform.com/pt-br/docs/guides/svelte.md): Um componente Svelte 5 e uma server action.
- [SvelteKit](https://ikiform.com/pt-br/docs/guides/sveltekit.md): Uma página pré-renderizada com progressive enhancement.
- [Astro](https://ikiform.com/pt-br/docs/guides/astro.md): Um formulário sem JavaScript, com melhoria opcional.
- [Gatsby](https://ikiform.com/pt-br/docs/guides/gatsby.md): Um formulário estático ou uma Gatsby Function.
- [JavaScript puro](https://ikiform.com/pt-br/docs/guides/javascript.md): `fetch` com erros inline.

### Geradores de sites estáticos

- [Hugo](https://ikiform.com/pt-br/docs/guides/hugo.md): Um partial e um shortcode para arquivos de conteúdo.
- [Jekyll](https://ikiform.com/pt-br/docs/guides/jekyll.md): Um include e a configuração do GitHub Pages.
- [Eleventy](https://ikiform.com/pt-br/docs/guides/eleventy.md): Um shortcode para Nunjucks, Liquid e Markdown.

### Criadores de sites

- [Webflow](https://ikiform.com/pt-br/docs/guides/webflow.md): O Form Block com um action personalizado.
- [Framer](https://ikiform.com/pt-br/docs/guides/framer.md): Um webhook de formulário ou um code component.
- [WordPress](https://ikiform.com/pt-br/docs/guides/wordpress.md): O bloco Custom HTML ou um shortcode.
- [Wix](https://ikiform.com/pt-br/docs/guides/wix.md): Um embed HTML que envia com `fetch`.
- [Squarespace](https://ikiform.com/pt-br/docs/guides/squarespace.md): Um code block, com ou sem JavaScript.
- [Carrd](https://ikiform.com/pt-br/docs/guides/carrd.md): Um formulário personalizado que envia para uma URL.
