# SDK-Referenz

> Das Paket @ikiform/sdk: submit, challenge, getSchema, validate, createClient und verifyWebhook.

`@ikiform/sdk` ist ein kleines TypeScript-Paket zum Absenden von Formularen, Lesen von Daten aus der REST-API und Verifizieren von Webhooks. Es hat keine Abhängigkeiten und nutzt nur `fetch`, `FormData` und Web Crypto, daher läuft es in Browsern, Node 20+, Bun, Deno und Cloudflare Workers. Es wird als ESM mit Typdeklarationen ausgeliefert.

`Terminal`:

```bash
npm install @ikiform/sdk
```

| Import | Aus | Läuft in |
| --- | --- | --- |
| `submit` | `@ikiform/sdk` | Browser oder Server |
| `challenge`, `ChallengeError` | `@ikiform/sdk` | Browser |
| `getSchema`, `validate`, `validateField` | `@ikiform/sdk` | Browser oder Server |
| `createClient`, `IkiformError` | `@ikiform/sdk` | Nur Server |
| `verifyWebhook`, `WebhookVerificationError` | `@ikiform/sdk/webhooks` | Server |

## submit

`submit(formId, data, options?)` sendet an `/f/:formId` und liefert ein Ergebnis. Es wirft nie: HTTP-Fehler geben die Fehlerhülle zurück, Netzwerkfehler den Code `network_error` und Abbrüche `aborted`.

`SubmitResult`:

```ts
type SubmitResult =
  | { ok: true; id: string }
  | {
      ok: false;
      error: { code: string; message: string; fields?: Record<string, string[]> };
      status: number | null;
      requestId: string | null;
    };
```

| data | Gesendet als |
| --- | --- |
| `FormData` nur mit Text | `application/x-www-form-urlencoded` |
| `FormData` mit einer `File` | `multipart/form-data`, das Ikiform ablehnt, bis Uploads gebaut sind |
| `URLSearchParams` | `application/x-www-form-urlencoded` |
| Ein einfaches Objekt | `application/json` |

- Die Felder `$ACTION_` von React werden aus `FormData` entfernt.
- Der Rückgabewert erfüllt auch den Form-Action-Typ von React, daher funktioniert `action={(data) => submit(id, data)}`.
- `submit` sendet `Accept: application/json`. Es sendet keinen Header `Authorization`; nutze `fetch`, um mit einem API-Schlüssel zu senden.

| Option | Beschreibung |
| --- | --- |
| `baseUrl` | Standardmäßig `https://ikiform.com`. |
| `signal` | Ein `AbortSignal`, um den Request abzubrechen. |
| `idempotencyKey` | Wird als Header `Idempotency-Key` gesendet. |
| `turnstileToken` | Wird als `cf-turnstile-response` gesendet, in welchem Body-Format auch immer. |
| `fetch` | Eine eigene `fetch`-Implementierung. |

`submit.ts`:

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

const result = await submit("frm_xxxxxxxxxxxxxxxx", { email: "ada@example.com" }, {
  idempotencyKey: crypto.randomUUID(),
  signal: AbortSignal.timeout(10_000),
});

if (!result.ok) console.error(result.error.code, result.error.fields);
```

## challenge

Für Formulare, deren Spamprüfung **Ikiform** ist. `challenge(formId, options?)` bindet einen 300 Pixel breiten ikiform.com-Frame ein, der auf null Höhe bleibt, außer Turnstile braucht einen Klick, liefert ein Token und entfernt den Frame. Tokens sind einmalig gültig und leben 300 Sekunden, also ruf es einmal pro Absenden auf.

| Option | Beschreibung |
| --- | --- |
| `container` | Wo der Frame eingebunden wird. Standard ist `document.body`. |
| `mode` | `"frame"` (Standard) oder `"popup"`, für Browser, die den Frame blockieren. Das Popup muss aus einem Klick heraus geöffnet werden. |
| `timeoutMs` | Nach so vielen Millisekunden ablehnen. Standardmäßig kein Timeout. |
| `signal` | Ein `AbortSignal`. |
| `baseUrl` | Standardmäßig `https://ikiform.com`. |

Es lehnt mit `ChallengeError` ab, dessen `code` `challenge_failed`, `challenge_timeout` oder `aborted` ist.

`challenge.ts`:

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

async function send(form) {
  try {
    const turnstileToken = await challenge("frm_xxxxxxxxxxxxxxxx", { timeoutMs: 60_000 });
    return submit("frm_xxxxxxxxxxxxxxxx", new FormData(form), { turnstileToken });
  } catch (error) {
    if (!(error instanceof ChallengeError) || error.code !== "challenge_failed") throw error;
    const turnstileToken = await challenge("frm_xxxxxxxxxxxxxxxx", { mode: "popup" });
    return submit("frm_xxxxxxxxxxxxxxxx", new FormData(form), { turnstileToken });
  }
}
```

## getSchema

`getSchema(formId, options?)` ruft das öffentliche Schema des Formulars ab: seine veröffentlichten Felder, Anzeigeeinstellungen, den Honeypot-Namen und den Challenge-Modus. Wie `submit` wirft es nie und liefert `{ ok: true, schema }` oder ein Fehlerergebnis. Optionen sind `baseUrl`, `signal` und `fetch`. Die Antwort ist auf der [shadcn/ui-Seite](https://ikiform.com/de/docs/shadcn.md#schema-endpoint) beschrieben.

## validate

`validate(fields, data)` prüft `FormData` oder ein einfaches Objekt gegen Felddefinitionen und gibt `Record<string, string>` zurück, eine Meldung pro ungültigem Feld, oder ein leeres Objekt. Es nutzt für alle 12 Feldtypen dieselben Regeln und Meldungen wie der Server. `validateField(field, value)` prüft einen Wert und gibt eine Meldung oder `undefined` zurück.

`validate.ts`:

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

const result = await getSchema("frm_xxxxxxxxxxxxxxxx");

if (result.ok) {
  const errors = validate(result.schema.fields, new FormData(form));
  if (Object.keys(errors).length === 0) {
    await submit("frm_xxxxxxxxxxxxxxxx", new FormData(form));
  }
}
```

## createClient

`createClient({ apiKey, baseUrl?, fetch? })` gibt einen Client für die [REST-API](https://ikiform.com/de/docs/api.md) zurück. Es wirft einen `TypeError`, wenn der Schlüssel kein Secret Key ist, also lass ihn auf dem Server.

| Methode | Anfrage |
| --- | --- |
| `forms.list({ limit, cursor })` | `GET /forms` |
| `forms.get(formId)` | `GET /forms/:formId` |
| `forms.iterate({ limit })` | Jedes Formular, den Cursorn folgend |
| `submissions.list({ formId, status, limit, cursor })` | `GET /submissions` |
| `submissions.get(id)` | `GET /submissions/:id` |
| `submissions.iterate({ formId, status, limit })` | Jede Einsendung, den Cursorn folgend |
| `submissions.markSpam(id)` | `POST /submissions/:id/spam` |
| `submissions.markNotSpam(id)` | `POST /submissions/:id/not-spam` |
| `submissions.delete(id)` | `DELETE /submissions/:id` |

Fehler werfen `IkiformError` mit `code`, `status`, `requestId`, `message`, dazu `fields`, falls vorhanden, und `retryAfter` in Sekunden bei `429`. Netzwerkfehler werfen mit `code: "network_error"` und `status: 0`.

`client.ts`:

```ts
import { createClient, IkiformError } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });

const forms = await ikiform.forms.list({ limit: 10 });
const form = await ikiform.forms.get("frm_xxxxxxxxxxxxxxxx");

for await (const submission of ikiform.submissions.iterate({ formId: form.id, status: "received" })) {
  console.log(submission.id, submission.fields);
}

try {
  await ikiform.submissions.delete("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
} catch (error) {
  if (error instanceof IkiformError && error.status === 429) {
    console.log(`Retry in ${error.retryAfter} seconds`);
  }
}
```

## verifyWebhook

`verifyWebhook({ payload, headers, secret, toleranceSeconds? })` verifiziert eine Webhook-Signatur und gibt das geparste Event zurück. Übergib den rohen Body-String. Die Standardtoleranz beträgt 300 Sekunden in beide Richtungen.

`webhook.ts`:

```ts
import { verifyWebhook, type WebhookEvent } from "@ikiform/sdk/webhooks";

const event = await verifyWebhook<WebhookEvent>({
  payload: rawBody,
  headers: request.headers,
  secret: process.env.IKIFORM_WEBHOOK_SECRET!,
  toleranceSeconds: 300,
});
```

### Verifizierungsfehler

| reason | Bedeutung |
| --- | --- |
| `invalid_secret` | Das Secret ist nicht `whsec_` plus Base64 |
| `missing_headers` | `webhook-id`, `webhook-timestamp` oder `webhook-signature` fehlt |
| `invalid_timestamp` | `webhook-timestamp` ist keine ganze Zahl von Unix-Sekunden |
| `timestamp_too_old` | Vor mehr als `toleranceSeconds` signiert, zum Beispiel ein wiederholt eingespielter Request |
| `timestamp_too_new` | In der Zukunft signiert; prüf die Uhr deines Servers |
| `no_matching_signature` | Keine `v1`-Signatur passt: falsches Secret, oder der Body wurde verändert |
| `invalid_payload` | Die Signatur passt, aber der Body ist kein JSON |

> Die Event-Typen werden als `WebhookEvent`, `SubmissionCreatedEvent` und `EndpointTestEvent` exportiert. Den vollständigen Payload findest du unter [Webhooks](https://ikiform.com/de/docs/webhooks.md#payloads).
