# Référence du SDK

> Le package @ikiform/sdk : submit, challenge, getSchema, validate, createClient et verifyWebhook.

`@ikiform/sdk` est un petit package TypeScript pour soumettre des formulaires, lire des données via l'API REST et vérifier les webhooks. Il n'a aucune dépendance et n'utilise que `fetch`, `FormData` et Web Crypto, donc il tourne dans les navigateurs, Node 20+, Bun, Deno et Cloudflare Workers. Il est distribué en ESM avec ses déclarations de types.

`Terminal`:

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

| Import | Depuis | S'exécute dans |
| --- | --- | --- |
| `submit` | `@ikiform/sdk` | Navigateur ou serveur |
| `challenge`, `ChallengeError` | `@ikiform/sdk` | Navigateur |
| `getSchema`, `validate`, `validateField` | `@ikiform/sdk` | Navigateur ou serveur |
| `createClient`, `IkiformError` | `@ikiform/sdk` | Serveur uniquement |
| `verifyWebhook`, `WebhookVerificationError` | `@ikiform/sdk/webhooks` | Serveur |

## submit

`submit(formId, data, options?)` envoie vers `/f/:formId` et se résout en un résultat. Il ne lève jamais d'exception : les erreurs HTTP renvoient l'enveloppe d'erreur, les pannes réseau renvoient le code `network_error` et les interruptions renvoient `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 | Envoyé en |
| --- | --- |
| `FormData` avec uniquement du texte | `application/x-www-form-urlencoded` |
| `FormData` avec un `File` | `multipart/form-data`, refusé par Ikiform tant que l'envoi de fichiers n'existe pas |
| `URLSearchParams` | `application/x-www-form-urlencoded` |
| Un objet simple | `application/json` |

- Les champs `$ACTION_` de React sont retirés du `FormData`.
- La valeur renvoyée satisfait aussi le type d'action de formulaire de React, donc `action={(data) => submit(id, data)}` fonctionne.
- `submit` envoie `Accept: application/json`. Il n'envoie pas d'en-tête `Authorization` ; utilisez `fetch` pour envoyer avec une clé API.

| Option | Description |
| --- | --- |
| `baseUrl` | Par défaut `https://ikiform.com`. |
| `signal` | Un `AbortSignal` pour annuler la requête. |
| `idempotencyKey` | Envoyé dans l'en-tête `Idempotency-Key`. |
| `turnstileToken` | Envoyé en `cf-turnstile-response`, quel que soit le format du corps. |
| `fetch` | Une implémentation personnalisée de `fetch`. |

`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

Pour les formulaires dont le contrôle anti-spam est **Ikiform**. `challenge(formId, options?)` monte une frame ikiform.com de 300 pixels de large qui garde une hauteur nulle sauf si Turnstile a besoin d'un clic, se résout avec un token et retire la frame. Les tokens sont à usage unique et valables 300 secondes : appelez-le une fois par envoi.

| Option | Description |
| --- | --- |
| `container` | Où monter la frame. Par défaut `document.body`. |
| `mode` | `"frame"` (par défaut) ou `"popup"`, pour les navigateurs qui bloquent la frame. La popup doit être ouverte depuis un clic. |
| `timeoutMs` | Rejette après ce nombre de millisecondes. Pas de timeout par défaut. |
| `signal` | Un `AbortSignal`. |
| `baseUrl` | Par défaut `https://ikiform.com`. |

Il est rejeté avec une `ChallengeError`, dont le `code` vaut `challenge_failed`, `challenge_timeout` ou `aborted`.

`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?)` récupère le schéma public du formulaire : ses champs publiés, ses réglages d'affichage, le nom du honeypot et le mode de vérification. Comme `submit`, il ne lève jamais d'exception et se résout en `{ ok: true, schema }` ou en résultat d'erreur. Les options sont `baseUrl`, `signal` et `fetch`. La réponse est décrite sur la [page shadcn/ui](https://ikiform.com/fr/docs/shadcn.md#schema-endpoint).

## validate

`validate(fields, data)` vérifie un `FormData` ou un objet simple par rapport aux définitions de champs et renvoie un `Record<string, string>`, avec un message par champ invalide, ou un objet vide. Il utilise les mêmes règles et messages que le serveur pour les 12 types de champs. `validateField(field, value)` vérifie une seule valeur et renvoie un message ou `undefined`.

`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? })` renvoie un client pour l'[API REST](https://ikiform.com/fr/docs/api.md). Il lève une `TypeError` si la clé n'est pas une clé secrète : gardez-le donc côté serveur.

| Méthode | Requête |
| --- | --- |
| `forms.list({ limit, cursor })` | `GET /forms` |
| `forms.get(formId)` | `GET /forms/:formId` |
| `forms.iterate({ limit })` | Tous les formulaires, en suivant les curseurs |
| `submissions.list({ formId, status, limit, cursor })` | `GET /submissions` |
| `submissions.get(id)` | `GET /submissions/:id` |
| `submissions.iterate({ formId, status, limit })` | Toutes les soumissions, en suivant les curseurs |
| `submissions.markSpam(id)` | `POST /submissions/:id/spam` |
| `submissions.markNotSpam(id)` | `POST /submissions/:id/not-spam` |
| `submissions.delete(id)` | `DELETE /submissions/:id` |

Les échecs lèvent une `IkiformError` avec `code`, `status`, `requestId`, `message`, plus `fields` le cas échéant et `retryAfter` en secondes sur un `429`. Les pannes réseau lèvent une erreur avec `code: "network_error"` et `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? })` vérifie une signature de webhook et renvoie l'événement analysé. Passez la chaîne du corps brut. La tolérance par défaut est de 300 secondes dans les deux sens.

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

### Erreurs de vérification

| reason | Signification |
| --- | --- |
| `invalid_secret` | Le secret n'est pas `whsec_` suivi de base64 |
| `missing_headers` | `webhook-id`, `webhook-timestamp` ou `webhook-signature` est absent |
| `invalid_timestamp` | `webhook-timestamp` n'est pas un nombre entier de secondes Unix |
| `timestamp_too_old` | Signé il y a plus de `toleranceSeconds`, par exemple une requête rejouée |
| `timestamp_too_new` | Signé dans le futur ; vérifiez l'horloge de votre serveur |
| `no_matching_signature` | Aucune signature `v1` ne correspond : mauvais secret, ou corps modifié |
| `invalid_payload` | La signature correspond mais le corps n'est pas du JSON |

> Les types d'événements sont exportés sous les noms `WebhookEvent`, `SubmissionCreatedEvent` et `EndpointTestEvent`. Voir [Webhooks](https://ikiform.com/fr/docs/webhooks.md#payloads) pour le payload complet.
