# JavaScript et fetch

> L'endpoint de soumission en détail : formats de corps, en-têtes, CORS, réponses et idempotence.

Tout code capable d'envoyer une requête HTTP peut soumettre à Ikiform. Envoyez du JSON, du `FormData` ou des données urlencoded à `https://ikiform.com/f/<formId>` et lisez une réponse JSON. Le [SDK](https://ikiform.com/fr/docs/sdk.md) enveloppe tout cela dans une seule fonction, mais il est facultatif. Pour une mise en place pas à pas sur une stack précise, voir [Guides par plateforme](#guides).

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

## Essayez

Le constructeur de requête interactif compose une soumission à partir de lignes de champs, affiche le code curl, fetch et SDK correspondant, et peut envoyer un vrai POST quand vous saisissez l'id de votre formulaire. Rien n'est envoyé automatiquement.

## Endpoints

| Méthode | Chemin | Rôle |
| --- | --- | --- |
| `POST` | `/f/<formId>` | Créer une soumission |
| `OPTIONS` | `/f/<formId>` | Preflight CORS, toujours `204` |
| `GET` | `/f/<formId>/schema` | La définition publique des champs du formulaire, voir [shadcn/ui](https://ikiform.com/fr/docs/shadcn.md#schema-endpoint) |
| `GET` | `/f/<formId>/thanks` | La page de remerciement hébergée |
| `GET` | `/f/<formId>/challenge` | La frame de vérification humaine d'Ikiform, utilisée par le SDK |

Ouvrir `/f/<formId>` dans un onglet de navigateur envoie un `GET`, qui répond `404 not_found`. C'est normal : l'endpoint n'accepte que des POST.

## Formats de corps

| Content-Type | Forme | Remarques |
| --- | --- | --- |
| `application/json` et les types `+json` | Un seul objet JSON | Les valeurs peuvent être des chaînes, des nombres, des booléens, des tableaux et des objets, imbriqués sur 3 niveaux au plus en comptant l'objet racine |
| `application/x-www-form-urlencoded` | Paires nom et valeur | Les noms répétés et `name[]` deviennent des tableaux |
| `multipart/form-data` | Paires nom et valeur | Comme urlencoded ; les vrais fichiers sont refusés |
| Tout le reste |  | `415 unsupported_media_type` |

Les corps doivent être en UTF-8. Un `charset` autre que `utf-8` reçoit `415 unsupported_charset`, tout comme des octets qui ne sont pas de l'UTF-8 valide. Un tableau JSON ou un corps mal formé reçoit `400 invalid_body`.

## En-têtes

### En-têtes de requête

| En-tête | Obligatoire | Effet |
| --- | --- | --- |
| `Content-Type` | Oui | Choisit l'analyseur du corps, voir ci-dessus |
| `Accept` | Non | `application/json` force une réponse JSON, voir [JSON ou redirection](https://ikiform.com/fr/docs/how-it-works.md#response-modes) |
| `Idempotency-Key` | Non | Rend les nouvelles tentatives sûres, jusqu'à 255 caractères, voir [Idempotence](#idempotency) |
| `Authorization` | Non | `Bearer ik_sk_...` ou `Bearer ik_pub_...`, voir [Depuis un serveur](https://ikiform.com/fr/docs/guides/server.md) |
| `Origin`, `Referer` | Envoyé par les navigateurs | Comparé aux domaines autorisés du formulaire |

### En-têtes de réponse

| En-tête | Quand | Valeur |
| --- | --- | --- |
| `X-Request-Id` | Chaque réponse | `req_...`, repris en `request_id` dans les corps d'erreur |
| `Retry-After` | `429` | Secondes avant que le prochain envoi soit autorisé |
| `Location` | Succès `303` pour un navigateur | Votre redirection ou la page de remerciement hébergée |
| `Access-Control-Allow-Origin` | Quand la requête a un `Origin` | L'origine de la requête elle-même |
| `Access-Control-Expose-Headers` | Avec CORS | `X-Request-Id, Retry-After`, pour que le code navigateur puisse lire les deux |
| `Cache-Control` | Succès | `no-store` |

## CORS

L'endpoint de soumission accepte les envois cross-origin depuis n'importe quel site. Les preflights réussissent toujours et renvoient l'origine de la requête. Aucun cookie ni identifiant n'est utilisé, donc ce n'est pas CORS qui protège votre formulaire, ce sont les **domaines autorisés**. Avec des domaines autorisés définis, un envoi depuis un autre site est stocké comme spam ou refusé, selon le réglage du formulaire. Voir [Domaines autorisés](https://ikiform.com/fr/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
```

> L'[API REST](https://ikiform.com/fr/docs/api.md) sous `/api/v1` est différente : elle n'envoie aucun en-tête CORS et répond aux preflights des navigateurs avec `405`, donc une clé secrète ne peut jamais être utilisée depuis une page web.

## Réponses

Un succès renvoie `201` avec l'id de la nouvelle soumission. Les erreurs utilisent partout la même enveloppe, avec les erreurs de champ indexées par id de champ quand la validation échoue. Tous les codes sont listés sur [Erreurs](https://ikiform.com/fr/docs/errors.md).

**201** (`Soumission créée`)

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

**422** (`Échec de la validation`)

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

> Un envoi classé comme spam reçoit quand même `201`. Le spam est stocké à part et n'atteint jamais vos webhooks ni votre boîte de réception.

## Idempotence

Envoyez un en-tête `Idempotency-Key`, ou un champ `_idempotency`, pour rendre les nouvelles tentatives sûres. Les clés font jusqu'à 255 caractères et sont limitées au formulaire pendant 24 heures. L'en-tête l'emporte si les deux sont envoyés.

| Nouvelle tentative avec | Résultat |
| --- | --- |
| La même clé et les mêmes champs | La première réponse est rejouée. Aucune seconde soumission n'est créée. |
| La même clé et des champs différents | `422 idempotency_mismatch` |
| La même clé pendant que la première requête est encore en cours | `409 idempotency_in_progress` |

Les envois qui échouent avant le stockage de la soumission, comme les erreurs de validation, ne conservent pas la clé : vous pouvez corriger les données et réessayer avec la même clé. Une boucle de nouvelles tentatives sûre :

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

## Guides par plateforme

### Frameworks

- [Next.js](https://ikiform.com/fr/docs/guides/nextjs.md): Un composant client, ou une server action.
- [React](https://ikiform.com/fr/docs/guides/react.md): États d'attente, d'erreur et de succès.
- [Vue](https://ikiform.com/fr/docs/guides/vue.md): Un composant monofichier.
- [Nuxt](https://ikiform.com/fr/docs/guides/nuxt.md): Envoie avant l'hydratation, puis avec `$fetch`.
- [Svelte](https://ikiform.com/fr/docs/guides/svelte.md): Un composant Svelte 5 et une server action.
- [SvelteKit](https://ikiform.com/fr/docs/guides/sveltekit.md): Une page prérendue avec amélioration progressive.
- [Astro](https://ikiform.com/fr/docs/guides/astro.md): Un formulaire sans JavaScript, amélioré si vous le souhaitez.
- [Gatsby](https://ikiform.com/fr/docs/guides/gatsby.md): Un formulaire statique, ou une Gatsby Function.
- [JavaScript pur](https://ikiform.com/fr/docs/guides/javascript.md): `fetch` avec erreurs affichées en ligne.

### Générateurs de sites statiques

- [Hugo](https://ikiform.com/fr/docs/guides/hugo.md): Un partial et un shortcode pour les fichiers de contenu.
- [Jekyll](https://ikiform.com/fr/docs/guides/jekyll.md): Un include, et la configuration GitHub Pages.
- [Eleventy](https://ikiform.com/fr/docs/guides/eleventy.md): Un shortcode pour Nunjucks, Liquid et Markdown.

### Créateurs de sites

- [Webflow](https://ikiform.com/fr/docs/guides/webflow.md): Le Form Block avec une action personnalisée.
- [Framer](https://ikiform.com/fr/docs/guides/framer.md): Un webhook de formulaire, ou un composant de code.
- [WordPress](https://ikiform.com/fr/docs/guides/wordpress.md): Le bloc Custom HTML, ou un shortcode.
- [Wix](https://ikiform.com/fr/docs/guides/wix.md): Un embed HTML qui envoie avec `fetch`.
- [Squarespace](https://ikiform.com/fr/docs/guides/squarespace.md): Un bloc de code, avec ou sans JavaScript.
- [Carrd](https://ikiform.com/fr/docs/guides/carrd.md): Un formulaire personnalisé qui envoie vers une URL.
