# Referensi SDK

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

`@ikiform/sdk` adalah paket TypeScript kecil untuk mengirim formulir, membaca data dari REST API, dan memverifikasi webhook. Paket ini tanpa dependency dan hanya memakai `fetch`, `FormData`, dan Web Crypto, jadi berjalan di browser, Node 20+, Bun, Deno, dan Cloudflare Workers. Paket dirilis sebagai ESM dengan deklarasi tipe.

`Terminal`:

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

| Impor | Dari | Berjalan di |
| --- | --- | --- |
| `submit` | `@ikiform/sdk` | Browser atau server |
| `challenge`, `ChallengeError` | `@ikiform/sdk` | Browser |
| `getSchema`, `validate`, `validateField` | `@ikiform/sdk` | Browser atau server |
| `createClient`, `IkiformError` | `@ikiform/sdk` | Hanya server |
| `verifyWebhook`, `WebhookVerificationError` | `@ikiform/sdk/webhooks` | Server |

## submit

`submit(formId, data, options?)` mengirim ke `/f/:formId` dan resolve ke sebuah hasil. Fungsi ini tidak pernah melempar error: error HTTP mengembalikan envelope error, kegagalan jaringan mengembalikan kode `network_error`, dan pembatalan mengembalikan `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 | Dikirim sebagai |
| --- | --- |
| `FormData` yang hanya berisi teks | `application/x-www-form-urlencoded` |
| `FormData` dengan `File` | `multipart/form-data`, yang ditolak Ikiform sampai fitur unggah tersedia |
| `URLSearchParams` | `application/x-www-form-urlencoded` |
| Objek biasa | `application/json` |

- Field `$ACTION_` milik React dibuang dari `FormData`.
- Nilai kembaliannya juga memenuhi tipe form action React, jadi `action={(data) => submit(id, data)}` berfungsi.
- `submit` mengirim `Accept: application/json`. Fungsi ini tidak mengirim header `Authorization`; gunakan `fetch` untuk mengirim dengan kunci API.

| Pilihan | Deskripsi |
| --- | --- |
| `baseUrl` | Default-nya `https://ikiform.com`. |
| `signal` | `AbortSignal` untuk membatalkan request. |
| `idempotencyKey` | Dikirim sebagai header `Idempotency-Key`. |
| `turnstileToken` | Dikirim sebagai `cf-turnstile-response` dalam format body apa pun yang dipakai. |
| `fetch` | Implementasi `fetch` kustom. |

`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

Untuk formulir yang pemeriksaan spamnya **Ikiform**. `challenge(formId, options?)` memasang frame ikiform.com selebar 300 piksel yang tetap bertinggi nol kecuali Turnstile butuh klik, resolve dengan token, lalu menghapus frame. Token hanya sekali pakai dan berlaku 300 detik, jadi panggil sekali per kiriman.

| Pilihan | Deskripsi |
| --- | --- |
| `container` | Tempat memasang frame. Default-nya `document.body`. |
| `mode` | `"frame"` (default) atau `"popup"`, untuk browser yang memblokir frame. Popup harus dibuka dari sebuah klik. |
| `timeoutMs` | Reject setelah sekian milidetik. Tanpa timeout secara default. |
| `signal` | Sebuah `AbortSignal`. |
| `baseUrl` | Default-nya `https://ikiform.com`. |

Fungsi ini me-reject dengan `ChallengeError`, yang `code`-nya `challenge_failed`, `challenge_timeout`, atau `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?)` mengambil skema publik formulir: field yang dipublikasikan, pengaturan tampilan, nama honeypot, dan mode challenge. Seperti `submit`, fungsi ini tidak pernah melempar error, dan resolve ke `{ ok: true, schema }` atau hasil error. Opsinya adalah `baseUrl`, `signal`, dan `fetch`. Responsnya dijelaskan di [halaman shadcn/ui](https://ikiform.com/id/docs/shadcn.md#schema-endpoint).

## validate

`validate(fields, data)` memeriksa `FormData` atau objek biasa terhadap definisi field dan mengembalikan `Record<string, string>`, satu pesan per field yang tidak valid, atau objek kosong. Fungsi ini memakai aturan dan pesan yang sama seperti server untuk ke-12 tipe field. `validateField(field, value)` memeriksa satu nilai dan mengembalikan pesan atau `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? })` mengembalikan client untuk [REST API](https://ikiform.com/id/docs/api.md). Fungsi ini melempar `TypeError` jika key bukan secret key, jadi simpan di server.

| Metode | Request |
| --- | --- |
| `forms.list({ limit, cursor })` | `GET /forms` |
| `forms.get(formId)` | `GET /forms/:formId` |
| `forms.iterate({ limit })` | Setiap formulir, mengikuti cursor |
| `submissions.list({ formId, status, limit, cursor })` | `GET /submissions` |
| `submissions.get(id)` | `GET /submissions/:id` |
| `submissions.iterate({ formId, status, limit })` | Setiap kiriman, mengikuti cursor |
| `submissions.markSpam(id)` | `POST /submissions/:id/spam` |
| `submissions.markNotSpam(id)` | `POST /submissions/:id/not-spam` |
| `submissions.delete(id)` | `DELETE /submissions/:id` |

Kegagalan melempar `IkiformError` dengan `code`, `status`, `requestId`, `message`, plus `fields` jika ada dan `retryAfter` dalam detik pada `429`. Kegagalan jaringan melempar dengan `code: "network_error"` dan `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? })` memverifikasi tanda tangan webhook dan mengembalikan event yang sudah diurai. Berikan string body mentah. Toleransi default-nya 300 detik ke dua arah.

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

### Error verifikasi

| reason | Arti |
| --- | --- |
| `invalid_secret` | Secret-nya bukan `whsec_` ditambah base64 |
| `missing_headers` | `webhook-id`, `webhook-timestamp`, atau `webhook-signature` tidak ada |
| `invalid_timestamp` | `webhook-timestamp` bukan detik Unix bulat |
| `timestamp_too_old` | Ditandatangani lebih dari `toleranceSeconds` yang lalu, misalnya request yang dikirim ulang (replay) |
| `timestamp_too_new` | Ditandatangani di masa depan; periksa jam server Anda |
| `no_matching_signature` | Tidak ada tanda tangan `v1` yang cocok: secret salah, atau body sudah diubah |
| `invalid_payload` | Tanda tangan cocok tetapi body-nya bukan JSON |

> Tipe event diekspor sebagai `WebhookEvent`, `SubmissionCreatedEvent`, dan `EndpointTestEvent`. Lihat [Webhook](https://ikiform.com/id/docs/webhooks.md#payloads) untuk payload lengkapnya.
