# JavaScript dan fetch

> Endpoint kiriman secara rinci: format body, header, CORS, respons, dan idempotensi.

Kode apa pun yang bisa mengirim request HTTP bisa mengirim ke Ikiform. Kirim JSON, `FormData`, atau data urlencoded ke `https://ikiform.com/f/<formId>` dan baca jawaban JSON-nya. [SDK](https://ikiform.com/id/docs/sdk.md) membungkus ini dalam satu fungsi, tetapi sifatnya opsional. Untuk penyiapan langkah demi langkah di stack tertentu, lihat [Panduan per platform](#guides).

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

## Coba sekarang

Pembuat request interaktif menyusun kiriman dari baris-baris field, menampilkan kode curl, fetch, dan SDK-nya, dan bisa mengirim satu POST sungguhan saat Anda memasukkan ID formulir Anda sendiri. Tidak ada yang dikirim secara otomatis.

## Endpoint

| Metode | Path | Fungsinya |
| --- | --- | --- |
| `POST` | `/f/<formId>` | Membuat kiriman |
| `OPTIONS` | `/f/<formId>` | Preflight CORS, selalu `204` |
| `GET` | `/f/<formId>/schema` | Definisi field publik formulir, lihat [shadcn/ui](https://ikiform.com/id/docs/shadcn.md#schema-endpoint) |
| `GET` | `/f/<formId>/thanks` | Halaman terima kasih hosted |
| `GET` | `/f/<formId>/challenge` | Frame pemeriksaan manusia Ikiform, dipakai oleh SDK |

Membuka `/f/<formId>` langsung di tab browser adalah `GET`, yang dijawab `404 not_found`. Itu memang seharusnya: endpoint ini hanya menerima kiriman.

## Format body

| Content-Type | Bentuk | Catatan |
| --- | --- | --- |
| `application/json` dan tipe `+json` | Satu objek JSON | Nilai bisa berupa string, angka, boolean, array, dan objek, bertingkat maksimal 3 level termasuk objek teratas |
| `application/x-www-form-urlencoded` | Pasangan nama dan nilai | Nama yang berulang dan `name[]` menjadi array |
| `multipart/form-data` | Pasangan nama dan nilai | Sama seperti urlencoded; file sungguhan ditolak |
| Selain itu |  | `415 unsupported_media_type` |

Body harus UTF-8. `charset` selain `utf-8` mendapat `415 unsupported_charset`, dan byte yang bukan UTF-8 valid mendapat kode yang sama. Array JSON atau body yang rusak mendapat `400 invalid_body`.

## Header

### Header request

| Header | Wajib | Efek |
| --- | --- | --- |
| `Content-Type` | Ya | Menentukan parser body, lihat di atas |
| `Accept` | Tidak | `application/json` memaksa jawaban JSON, lihat [JSON atau redirect](https://ikiform.com/id/docs/how-it-works.md#response-modes) |
| `Idempotency-Key` | Tidak | Membuat percobaan ulang aman, hingga 255 karakter, lihat [Idempotensi](#idempotency) |
| `Authorization` | Tidak | `Bearer ik_sk_...` atau `Bearer ik_pub_...`, lihat [Dari server](https://ikiform.com/id/docs/guides/server.md) |
| `Origin`, `Referer` | Dikirim oleh browser | Dicocokkan dengan domain yang diizinkan formulir |

### Header respons

| Header | Kapan | Nilai |
| --- | --- | --- |
| `X-Request-Id` | Setiap respons | `req_...`, diulang sebagai `request_id` di body error |
| `Retry-After` | `429` | Detik sampai kiriman berikutnya diizinkan |
| `Location` | Sukses browser `303` | Redirect Anda atau halaman terima kasih hosted |
| `Access-Control-Allow-Origin` | Saat request punya `Origin` | Origin milik request itu sendiri |
| `Access-Control-Expose-Headers` | Dengan CORS | `X-Request-Id, Retry-After`, agar kode browser bisa membaca keduanya |
| `Cache-Control` | Sukses | `no-store` |

## CORS

Endpoint kiriman menerima kiriman lintas origin dari situs mana pun. Preflight selalu berhasil dan memantulkan origin request. Tidak ada cookie atau kredensial yang dipakai, jadi bukan CORS yang melindungi formulir Anda, melainkan **domain yang diizinkan**. Jika domain yang diizinkan diatur, kiriman dari situs lain disimpan sebagai spam atau ditolak, tergantung pengaturan formulir. Lihat [Domain yang diizinkan](https://ikiform.com/id/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
```

> [REST API](https://ikiform.com/id/docs/api.md) di bawah `/api/v1` berbeda: API ini tidak mengirim header CORS dan menjawab preflight browser dengan `405`, jadi secret key tidak pernah bisa dipakai dari halaman web.

## Respons

Sukses berupa `201` dengan ID kiriman baru. Error memakai satu format envelope di mana pun, dengan error field yang diberi key berdasarkan ID field saat validasi gagal. Setiap kode tercantum di [Error](https://ikiform.com/id/docs/errors.md).

**201** (`Created`)

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

**422** (`Validasi gagal`)

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

> Kiriman yang dinilai sebagai spam tetap mendapat `201`. Spam disimpan terpisah dan tidak pernah sampai ke webhook atau inbox Anda.

## Idempotensi

Kirim header `Idempotency-Key`, atau field `_idempotency`, agar percobaan ulang aman. Key bisa hingga 255 karakter dan berlaku untuk formulir tersebut selama 24 jam. Header yang dipakai jika keduanya dikirim.

| Coba ulang dengan | Hasil |
| --- | --- |
| Key yang sama dan field yang sama | Respons pertama dikembalikan lagi. Tidak ada kiriman kedua yang dibuat. |
| Key yang sama dan field yang berbeda | `422 idempotency_mismatch` |
| Key yang sama saat request pertama masih berjalan | `409 idempotency_in_progress` |

Kiriman yang gagal sebelum disimpan, seperti error validasi, tidak menyimpan key-nya, jadi Anda bisa memperbaiki data dan mencoba ulang dengan key yang sama. Loop percobaan ulang yang aman:

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

## Panduan per platform

### Framework

- [Next.js](https://ikiform.com/id/docs/guides/nextjs.md): Client component, atau server action.
- [React](https://ikiform.com/id/docs/guides/react.md): State pending, error, dan sukses.
- [Vue](https://ikiform.com/id/docs/guides/vue.md): Single-file component.
- [Nuxt](https://ikiform.com/id/docs/guides/nuxt.md): Mengirim sebelum hidrasi dan dengan `$fetch` setelahnya.
- [Svelte](https://ikiform.com/id/docs/guides/svelte.md): Komponen Svelte 5 dan server action.
- [SvelteKit](https://ikiform.com/id/docs/guides/sveltekit.md): Halaman prerender dengan progressive enhancement.
- [Astro](https://ikiform.com/id/docs/guides/astro.md): Formulir tanpa JavaScript, dengan peningkatan opsional.
- [Gatsby](https://ikiform.com/id/docs/guides/gatsby.md): Formulir statis, atau Gatsby Function.
- [JavaScript biasa](https://ikiform.com/id/docs/guides/javascript.md): `fetch` dengan error inline.

### Static site generator

- [Hugo](https://ikiform.com/id/docs/guides/hugo.md): Partial dan shortcode untuk file konten.
- [Jekyll](https://ikiform.com/id/docs/guides/jekyll.md): Include, dan penyiapan GitHub Pages.
- [Eleventy](https://ikiform.com/id/docs/guides/eleventy.md): Shortcode untuk Nunjucks, Liquid, dan Markdown.

### Pembuat situs

- [Webflow](https://ikiform.com/id/docs/guides/webflow.md): Form Block dengan action kustom.
- [Framer](https://ikiform.com/id/docs/guides/framer.md): Webhook formulir, atau code component.
- [WordPress](https://ikiform.com/id/docs/guides/wordpress.md): Blok Custom HTML, atau shortcode.
- [Wix](https://ikiform.com/id/docs/guides/wix.md): Embed HTML yang mengirim dengan `fetch`.
- [Squarespace](https://ikiform.com/id/docs/guides/squarespace.md): Code block, dengan atau tanpa JavaScript.
- [Carrd](https://ikiform.com/id/docs/guides/carrd.md): Formulir kustom yang mengirim ke URL.
