# JavaScript und fetch

> Der Einsendungs-Endpunkt im Detail: Body-Formate, Header, CORS, Antworten und Idempotenz.

Jeder Code, der eine HTTP-Anfrage senden kann, kann an Ikiform senden. Sende JSON, `FormData` oder urlencoded Daten an `https://ikiform.com/f/<formId>` und lies eine JSON-Antwort. Das [SDK](https://ikiform.com/de/docs/sdk.md) verpackt das in einer Funktion, ist aber optional. Für eine Schritt-für-Schritt-Einrichtung auf einem bestimmten Stack siehe [Anleitungen nach Plattform](#guides).

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

## Ausprobieren

Der interaktive Request-Builder setzt eine Einsendung aus Feldzeilen zusammen, zeigt den curl-, fetch- und SDK-Code dafür und kann einen echten POST senden, wenn du deine eigene Formular-ID eingibst. Nichts wird automatisch gesendet.

## Endpunkte

| Methode | Pfad | Was er macht |
| --- | --- | --- |
| `POST` | `/f/<formId>` | Eine Einsendung erstellen |
| `OPTIONS` | `/f/<formId>` | CORS-Preflight, immer `204` |
| `GET` | `/f/<formId>/schema` | Die öffentliche Felddefinition des Formulars, siehe [shadcn/ui](https://ikiform.com/de/docs/shadcn.md#schema-endpoint) |
| `GET` | `/f/<formId>/thanks` | Die gehostete Dankeseite |
| `GET` | `/f/<formId>/challenge` | Der Frame der Ikiform-Menschlichkeitsprüfung, vom SDK verwendet |

Wenn du `/f/<formId>` selbst in einem Browser-Tab öffnest, ist das ein `GET`, der mit `404 not_found` beantwortet wird. Das ist so gewollt: Der Endpunkt nimmt nur Posts an.

## Body-Formate

| Content-Type | Struktur | Hinweise |
| --- | --- | --- |
| `application/json` und `+json`-Typen | Ein JSON-Objekt | Werte können Strings, Zahlen, Booleans, Arrays und Objekte sein, höchstens 3 Ebenen tief verschachtelt, das oberste Objekt mitgezählt |
| `application/x-www-form-urlencoded` | Name-Wert-Paare | Wiederholte Namen und `name[]` werden zu Arrays |
| `multipart/form-data` | Name-Wert-Paare | Wie urlencoded; echte Dateien werden abgelehnt |
| Alles andere |  | `415 unsupported_media_type` |

Bodies müssen UTF-8 sein. Ein anderes `charset` als `utf-8` bekommt `415 unsupported_charset`, und Bytes, die kein gültiges UTF-8 sind, bekommen denselben Code. Ein JSON-Array oder ein fehlerhafter Body bekommt `400 invalid_body`.

## Header

### Request-Header

| Header | Pflicht | Wirkung |
| --- | --- | --- |
| `Content-Type` | Ja | Wählt den Body-Parser, siehe oben |
| `Accept` | Nein | `application/json` erzwingt eine JSON-Antwort, siehe [JSON oder eine Weiterleitung](https://ikiform.com/de/docs/how-it-works.md#response-modes) |
| `Idempotency-Key` | Nein | Macht erneute Versuche sicher, bis zu 255 Zeichen, siehe [Idempotenz](#idempotency) |
| `Authorization` | Nein | `Bearer ik_sk_...` oder `Bearer ik_pub_...`, siehe [Von einem Server](https://ikiform.com/de/docs/guides/server.md) |
| `Origin`, `Referer` | Von Browsern gesendet | Wird mit den erlaubten Domains des Formulars abgeglichen |

### Response-Header

| Header | Wann | Wert |
| --- | --- | --- |
| `X-Request-Id` | Jede Antwort | `req_...`, in Fehler-Bodies als `request_id` wiederholt |
| `Retry-After` | `429` | Sekunden, bis der nächste Post erlaubt ist |
| `Location` | `303` Browser-Erfolg | Deine Weiterleitung oder die gehostete Dankeseite |
| `Access-Control-Allow-Origin` | Wenn die Anfrage einen `Origin` hat | Der eigene Origin der Anfrage |
| `Access-Control-Expose-Headers` | Mit CORS | `X-Request-Id, Retry-After`, damit Browser-Code beide lesen kann |
| `Cache-Control` | Erfolg | `no-store` |

## CORS

Der Einsendungs-Endpunkt akzeptiert Cross-Origin-Posts von jeder Website. Preflights sind immer erfolgreich und geben den Origin der Anfrage zurück. Es werden keine Cookies oder Zugangsdaten verwendet, also schützt nicht CORS dein Formular, sondern die **erlaubten Domains**. Sind erlaubte Domains gesetzt, wird ein Post von einer anderen Website je nach Einstellung des Formulars als Spam gespeichert oder abgelehnt. Siehe [Erlaubte Domains](https://ikiform.com/de/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
```

> Die [REST API](https://ikiform.com/de/docs/api.md) unter `/api/v1` ist anders: Sie sendet keine CORS-Header und beantwortet Browser-Preflights mit `405`, sodass ein Secret Key nie von einer Webseite aus genutzt werden kann.

## Antworten

Erfolg ist `201` mit der neuen Einsendungs-ID. Fehler verwenden überall dasselbe Format, mit Feldfehlern nach Feld-ID, wenn die Validierung fehlschlägt. Alle Codes stehen unter [Fehler](https://ikiform.com/de/docs/errors.md).

**201** (`Erstellt`)

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

**422** (`Validierung fehlgeschlagen`)

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

> Ein Post, der als Spam bewertet wird, bekommt trotzdem `201`. Spam wird separat gespeichert und erreicht nie deine Webhooks oder deinen Posteingang.

## Idempotenz

Sende einen `Idempotency-Key`-Header oder ein `_idempotency`-Feld, um erneute Versuche sicher zu machen. Schlüssel sind bis zu 255 Zeichen lang und gelten 24 Stunden lang für das Formular. Werden beide gesendet, gewinnt der Header.

| Erneuter Versuch mit | Ergebnis |
| --- | --- |
| Demselben Schlüssel und denselben Feldern | Die erste Antwort wird erneut abgespielt. Es wird keine zweite Einsendung erstellt. |
| Demselben Schlüssel und anderen Feldern | `422 idempotency_mismatch` |
| Demselben Schlüssel, während die erste Anfrage noch läuft | `409 idempotency_in_progress` |

Posts, die fehlschlagen, bevor die Einsendung gespeichert ist, etwa bei Validierungsfehlern, behalten den Schlüssel nicht, sodass du die Daten korrigieren und es mit demselben Schlüssel erneut versuchen kannst. Eine sichere Retry-Schleife:

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

## Anleitungen nach Plattform

### Frameworks

- [Next.js](https://ikiform.com/de/docs/guides/nextjs.md): Eine Client-Komponente oder eine Server Action.
- [React](https://ikiform.com/de/docs/guides/react.md): Lade-, Fehler- und Erfolgszustände.
- [Vue](https://ikiform.com/de/docs/guides/vue.md): Eine Single-File-Komponente.
- [Nuxt](https://ikiform.com/de/docs/guides/nuxt.md): Posts vor der Hydration und danach mit `$fetch`.
- [Svelte](https://ikiform.com/de/docs/guides/svelte.md): Eine Svelte-5-Komponente und eine Server Action.
- [SvelteKit](https://ikiform.com/de/docs/guides/sveltekit.md): Eine vorgerenderte Seite mit Progressive Enhancement.
- [Astro](https://ikiform.com/de/docs/guides/astro.md): Ein Formular ohne JavaScript, optional erweitert.
- [Gatsby](https://ikiform.com/de/docs/guides/gatsby.md): Ein statisches Formular oder eine Gatsby Function.
- [Reines JavaScript](https://ikiform.com/de/docs/guides/javascript.md): `fetch` mit Inline-Fehlern.

### Statische Website-Generatoren

- [Hugo](https://ikiform.com/de/docs/guides/hugo.md): Ein Partial und ein Shortcode für Inhaltsdateien.
- [Jekyll](https://ikiform.com/de/docs/guides/jekyll.md): Ein Include und die Einrichtung für GitHub Pages.
- [Eleventy](https://ikiform.com/de/docs/guides/eleventy.md): Ein Shortcode für Nunjucks, Liquid und Markdown.

### Website-Baukästen

- [Webflow](https://ikiform.com/de/docs/guides/webflow.md): Der Form Block mit einer eigenen Action.
- [Framer](https://ikiform.com/de/docs/guides/framer.md): Ein Formular-Webhook oder eine Code-Komponente.
- [WordPress](https://ikiform.com/de/docs/guides/wordpress.md): Der Custom-HTML-Block oder ein Shortcode.
- [Wix](https://ikiform.com/de/docs/guides/wix.md): Ein HTML-Embed, das mit `fetch` sendet.
- [Squarespace](https://ikiform.com/de/docs/guides/squarespace.md): Ein Code-Block, mit oder ohne JavaScript.
- [Carrd](https://ikiform.com/de/docs/guides/carrd.md): Ein eigenes Formular, das an eine URL sendet.
