# JavaScript と fetch

> 送信エンドポイントの詳細: ボディの形式、ヘッダー、CORS、レスポンス、冪等性。

HTTP リクエストを送れるコードなら何でも Ikiform に送信できます。JSON、`FormData`、urlencoded のデータを `https://ikiform.com/f/<formId>` に POST し、JSON の応答を読み取ります。[SDK](https://ikiform.com/ja/docs/sdk.md) はこれを 1 つの関数にまとめていますが、使うかどうかは任意です。特定のスタックでの手順は[プラットフォーム別ガイド](#guides)を参照してください。

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

## 試してみる

インタラクティブなリクエストビルダーは、フィールドの行から送信データを組み立て、その curl、fetch、SDK のコードを表示します。自分のフォーム ID を入力すると、実際の POST を 1 件送れます。自動では何も送られません。

## エンドポイント

| メソッド | パス | 内容 |
| --- | --- | --- |
| `POST` | `/f/<formId>` | 送信データを作成 |
| `OPTIONS` | `/f/<formId>` | CORS のプリフライト。常に `204` |
| `GET` | `/f/<formId>/schema` | フォームの公開フィールド定義。[shadcn/ui](https://ikiform.com/ja/docs/shadcn.md#schema-endpoint) を参照 |
| `GET` | `/f/<formId>/thanks` | ホストされたサンクスページ |
| `GET` | `/f/<formId>/challenge` | SDK が使う、Ikiform の人間確認フレーム |

ブラウザーのタブで `/f/<formId>` そのものを開くと `GET` になり、`404 not_found` が返ります。これは想定どおりの動作で、エンドポイントは POST しか受け付けません。

## ボディの形式

| Content-Type | 形式 | メモ |
| --- | --- | --- |
| `application/json` と `+json` 系のタイプ | 1 つの JSON オブジェクト | 値には文字列、数値、真偽値、配列、オブジェクトを使えます。ネストはトップレベルのオブジェクトを含めて最大 3 階層です |
| `application/x-www-form-urlencoded` | 名前と値のペア | 繰り返される名前と `name[]` は配列になります |
| `multipart/form-data` | 名前と値のペア | urlencoded と同じ。実際のファイルは拒否されます |
| それ以外 |  | `415 unsupported_media_type` |

ボディは UTF-8 である必要があります。`utf-8` 以外の `charset` は `415 unsupported_charset` になり、UTF-8 として無効なバイトも同じコードになります。JSON の配列や不正な形式のボディは `400 invalid_body` になります。

## ヘッダー

### リクエストヘッダー

| ヘッダー | 必須 | 効果 |
| --- | --- | --- |
| `Content-Type` | はい | ボディのパーサーを決めます。上記を参照 |
| `Accept` | いいえ | `application/json` で JSON の応答を強制します。[JSON かリダイレクトか](https://ikiform.com/ja/docs/how-it-works.md#response-modes)を参照 |
| `Idempotency-Key` | いいえ | リトライを安全にします。最大 255 文字。[冪等性](#idempotency)を参照 |
| `Authorization` | いいえ | `Bearer ik_sk_...` または `Bearer ik_pub_...`。[サーバーから送信する](https://ikiform.com/ja/docs/guides/server.md)を参照 |
| `Origin`, `Referer` | ブラウザーが送信 | フォームの許可ドメインと照合されます |

### レスポンスヘッダー

| ヘッダー | タイミング | 値 |
| --- | --- | --- |
| `X-Request-Id` | すべてのレスポンス | `req_...`。エラーのボディでは `request_id` として繰り返されます |
| `Retry-After` | `429` | 次の送信が許可されるまでの秒数 |
| `Location` | ブラウザーでの成功時の `303` | リダイレクト先、またはホストされたサンクスページ |
| `Access-Control-Allow-Origin` | リクエストに `Origin` がある場合 | リクエスト自身のオリジン |
| `Access-Control-Expose-Headers` | CORS の場合 | `X-Request-Id, Retry-After`。ブラウザーのコードから両方を読み取れます |
| `Cache-Control` | 成功時 | `no-store` |

## CORS

送信エンドポイントは、どのサイトからのクロスオリジンの送信も受け付けます。プリフライトは常に成功し、リクエストのオリジンをそのまま返します。Cookie や認証情報は使わないため、フォームを守るのは CORS ではなく **Allowed domains**（許可ドメイン）です。許可ドメインを設定すると、他のサイトからの送信はフォームの設定に応じてスパムとして保存されるか、拒否されます。[許可ドメイン](https://ikiform.com/ja/docs/spam.md#allowed-domains)を参照してください。

`プリフライト`:

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

> `/api/v1` 以下の [REST API](https://ikiform.com/ja/docs/api.md) は異なります。CORS ヘッダーを送らず、ブラウザーのプリフライトには `405` で応答するため、シークレットキーが Web ページから使われることはありません。

## レスポンス

成功すると、新しい送信 ID 付きの `201` を返します。エラーはどこでも同じ形式を使い、バリデーションに失敗した場合はフィールド ID ごとにフィールドエラーを返します。すべてのコードは[エラー](https://ikiform.com/ja/docs/errors.md)にあります。

**201** (`Created`)

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

**422** (`バリデーション失敗`)

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

> スパムと判定された送信にも `201` を返します。スパムは別に保存され、Webhook や受信トレイに届くことはありません。

## 冪等性

リトライを安全にするには、`Idempotency-Key` ヘッダーか `_idempotency` フィールドを送ります。キーは最大 255 文字で、フォーム単位で 24 時間有効です。両方送った場合はヘッダーが優先されます。

| リトライの内容 | 結果 |
| --- | --- |
| 同じキーで同じフィールド | 最初の応答を再送します。2 件目の送信データは作成されません。 |
| 同じキーで異なるフィールド | `422 idempotency_mismatch` |
| 最初のリクエストの処理中に同じキー | `409 idempotency_in_progress` |

バリデーションエラーなど、送信データが保存される前に失敗した送信はキーを保持しないので、データを直して同じキーでリトライできます。安全なリトライのループ:

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

## プラットフォーム別ガイド

### フレームワーク

- [Next.js](https://ikiform.com/ja/docs/guides/nextjs.md): クライアントコンポーネント、または Server Action。
- [React](https://ikiform.com/ja/docs/guides/react.md): 送信中、エラー、成功の状態。
- [Vue](https://ikiform.com/ja/docs/guides/vue.md): 単一ファイルコンポーネント。
- [Nuxt](https://ikiform.com/ja/docs/guides/nuxt.md): ハイドレーション前は通常の送信、後は `$fetch` で送信。
- [Svelte](https://ikiform.com/ja/docs/guides/svelte.md): Svelte 5 のコンポーネントと Server Action。
- [SvelteKit](https://ikiform.com/ja/docs/guides/sveltekit.md): プログレッシブエンハンスメント付きのプリレンダリングページ。
- [Astro](https://ikiform.com/ja/docs/guides/astro.md): JavaScript なしのフォーム。必要に応じて拡張。
- [Gatsby](https://ikiform.com/ja/docs/guides/gatsby.md): 静的なフォーム、または Gatsby Function。
- [素の JavaScript](https://ikiform.com/ja/docs/guides/javascript.md): インラインエラー付きの `fetch`。

### 静的サイトジェネレーター

- [Hugo](https://ikiform.com/ja/docs/guides/hugo.md): パーシャルと、コンテンツファイル用のショートコード。
- [Jekyll](https://ikiform.com/ja/docs/guides/jekyll.md): インクルードと、GitHub Pages の設定。
- [Eleventy](https://ikiform.com/ja/docs/guides/eleventy.md): Nunjucks、Liquid、Markdown 用のショートコード。

### サイトビルダー

- [Webflow](https://ikiform.com/ja/docs/guides/webflow.md): 独自の action を設定した Form Block。
- [Framer](https://ikiform.com/ja/docs/guides/framer.md): フォームの Webhook、またはコードコンポーネント。
- [WordPress](https://ikiform.com/ja/docs/guides/wordpress.md): カスタム HTML ブロック、またはショートコード。
- [Wix](https://ikiform.com/ja/docs/guides/wix.md): `fetch` で送信する HTML 埋め込み。
- [Squarespace](https://ikiform.com/ja/docs/guides/squarespace.md): コードブロック。JavaScript ありでもなしでも。
- [Carrd](https://ikiform.com/ja/docs/guides/carrd.md): URL に送信するカスタムフォーム。
