Formular-ID

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
npm install @ikiform/sdk
ImportAusLäuft in
submit@ikiform/sdkBrowser oder Server
challenge, ChallengeError@ikiform/sdkBrowser
getSchema, validate, validateField@ikiform/sdkBrowser oder Server
createClient, IkiformError@ikiform/sdkNur Server
verifyWebhook, WebhookVerificationError@ikiform/sdk/webhooksServer

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
type SubmitResult =
  | { ok: true; id: string }
  | {
      ok: false;
      error: { code: string; message: string; fields?: Record<string, string[]> };
      status: number | null;
      requestId: string | null;
    };
dataGesendet als
FormData nur mit Textapplication/x-www-form-urlencoded
FormData mit einer Filemultipart/form-data, das Ikiform ablehnt, bis Uploads gebaut sind
URLSearchParamsapplication/x-www-form-urlencoded
Ein einfaches Objektapplication/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.
OptionBeschreibung
baseUrlStandardmäßig https://ikiform.com.
signalEin AbortSignal, um den Request abzubrechen.
idempotencyKeyWird als Header Idempotency-Key gesendet.
turnstileTokenWird als cf-turnstile-response gesendet, in welchem Body-Format auch immer.
fetchEine eigene fetch-Implementierung.
submit.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.

OptionBeschreibung
containerWo 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.
timeoutMsNach so vielen Millisekunden ablehnen. Standardmäßig kein Timeout.
signalEin AbortSignal.
baseUrlStandardmäßig https://ikiform.com.

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

challenge.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 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
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 zurück. Es wirft einen TypeError, wenn der Schlüssel kein Secret Key ist, also lass ihn auf dem Server.

MethodeAnfrage
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
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
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

reasonBedeutung
invalid_secretDas Secret ist nicht whsec_ plus Base64
missing_headerswebhook-id, webhook-timestamp oder webhook-signature fehlt
invalid_timestampwebhook-timestamp ist keine ganze Zahl von Unix-Sekunden
timestamp_too_oldVor mehr als toleranceSeconds signiert, zum Beispiel ein wiederholt eingespielter Request
timestamp_too_newIn der Zukunft signiert; prüf die Uhr deines Servers
no_matching_signatureKeine v1-Signatur passt: falsches Secret, oder der Body wurde verändert
invalid_payloadDie 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.