# REST API

> シークレットキーで、フォームと送信データの読み取り、送信データのスパムへの移動と解除、削除を行います。

REST API を使うと、サーバーからフォームと送信データを読み取り、送信データをスパムに移動または解除し、削除できます。`https://ikiform.com/api/v1` で HTTPS 経由の JSON を使います。OpenAPI 3.1 のドキュメントは [/api/v1/openapi.json](https://ikiform.com/api/v1/openapi.json) にあります。

> 送信データの作成は `/api/v1` には含まれません。代わりに、シークレットキーを付けてフォームのエンドポイントに送信してください。[サーバーから送信する](https://ikiform.com/ja/docs/guides/server.md)を参照してください。

## API キー

キーはダッシュボードの **API keys** で作成します。キーは 1 つのワークスペースに属し、そのワークスペースしか参照できません。キー全体が表示されるのは 1 回だけで、Ikiform はハッシュしか保存しません。

| 種類 | プレフィックス | 用途 |
| --- | --- | --- |
| シークレット | `ik_sk_` | サーバー用。選んだスコープで REST API を呼び出せ、送信データも作成できます。 |
| 公開可能 | `ik_pub_` | ブラウザー用。フォームの送信しかできず、キーに登録したドメインからしか使えません。 |

### スコープ

| スコープ | 許可される操作 |
| --- | --- |
| `forms:read` | `GET /forms`, `GET /forms/:formId` |
| `submissions:read` | `GET /submissions`, `GET /submissions/:id` |
| `submissions:write` | `DELETE /submissions/:id`、`POST /submissions/:id/spam` と `/not-spam`、`/f/:formId` への送信 |

## 認証

シークレットキーを Bearer トークンとして送ります。API はサーバー間専用です。CORS ヘッダーを送らず、ブラウザーのプリフライトには `405` で応答するため、シークレットキーが Web ページから使えることはありません。

`ターミナル`:

```bash
curl https://ikiform.com/api/v1/forms \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

- キーがない、不正な形式、存在しない、無効化された、期限切れの場合は、`WWW-Authenticate: Bearer` 付きの `401 unauthorized` が返ります。
- 公開可能キーには `403 forbidden`（`Publishable keys can only submit forms.`）が返ります。必要なスコープのないキーには、メッセージにスコープ名を含む `403 forbidden` が返ります。
- レスポンスは `Cache-Control: no-store` 付きで送られます。

以下の SDK の例では、1 つのクライアントを共有します:

`ikiform.ts`:

```ts
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });
```

## リクエストコンポーザー

このページのインタラクティブなリクエストコンポーザーは、エンドポイント、フィルター、ID から REST API のリクエストを組み立て、curl、fetch、SDK のコードとして表示します。何も送信しません。REST API はサーバー間専用で、設計上ブラウザーからのリクエストを拒否します。

## エンドポイント

| メソッド | パス | スコープ | 返すもの |
| --- | --- | --- | --- |
| `GET` | [`/forms`](#list-forms) | `forms:read` | フォームの一覧 |
| `GET` | [`/forms/:formId`](#get-form) | `forms:read` | フォームを取得 |
| `GET` | [`/submissions`](#list-submissions) | `submissions:read` | 送信データの一覧 |
| `GET` | [`/submissions/:id`](#get-submission) | `submissions:read` | 送信データを取得 |
| `POST` | [`/submissions/:id/spam`](#mark-spam) | `submissions:write` | スパムとしてマーク |
| `POST` | [`/submissions/:id/not-spam`](#mark-not-spam) | `submissions:write` | スパムではないとマーク |
| `DELETE` | [`/submissions/:id`](#delete-submission) | `submissions:write` | 送信データを削除 |
| `GET` | `/openapi.json` | なし | OpenAPI のドキュメント |
| `GET` | `/health` | なし | `{ "ok": true }` |

### フォームの一覧

`forms:read` スコープで `GET /forms`。

- 新しい順。`limit`（1〜100、デフォルト 25）と `cursor` を受け取ります。

**curl** (`ターミナル`)

```bash
curl "https://ikiform.com/api/v1/forms?limit=10" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const page = await ikiform.forms.list({ limit: 10 });
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/forms?limit=10", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{
  "object": "list",
  "data": [
    {
      "object": "form",
      "id": "frm_xxxxxxxxxxxxxxxx",
      "name": "Contact",
      "status": "active",
      "submission_count": 42,
      "created_at": "2026-10-01T09:30:00.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c3ViXzAxazd2MmM5dDNlOG01cTR4NnowYTFiMmMz"
}
```

### フォームを取得

`forms:read` スコープで `GET /forms/:formId`。

- 別のワークスペースのフォーム ID は、存在しない ID とまったく同じく `404` を返します。

**curl** (`ターミナル`)

```bash
curl https://ikiform.com/api/v1/forms/frm_xxxxxxxxxxxxxxxx \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const form = await ikiform.forms.get("frm_xxxxxxxxxxxxxxxx");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/forms/frm_xxxxxxxxxxxxxxxx", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{
  "object": "form",
  "id": "frm_xxxxxxxxxxxxxxxx",
  "name": "Contact",
  "status": "active",
  "submission_count": 42,
  "created_at": "2026-10-01T09:30:00.000Z"
}
```

### 送信データの一覧

`submissions:read` スコープで `GET /submissions`。

| パラメーター | 値 | デフォルト |
| --- | --- | --- |
| `form_id` | フォーム ID | ワークスペースのすべてのフォーム |
| `status` | `received` または `spam` | 両方 |
| `limit` | 1〜100 | 25 |
| `cursor` | 前のページの `next_cursor` | 最初のページ |

- 新しい順。別のワークスペースの `form_id` はエラーではなく空の一覧を返すので、ID を探られることはありません。

**curl** (`ターミナル`)

```bash
curl "https://ikiform.com/api/v1/submissions?form_id=frm_xxxxxxxxxxxxxxxx&status=received&limit=50" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const page = await ikiform.submissions.list({
  formId: "frm_xxxxxxxxxxxxxxxx",
  status: "received",
  limit: 50,
});
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions?form_id=frm_xxxxxxxxxxxxxxxx&status=received&limit=50", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{
  "object": "list",
  "data": [
    {
      "object": "submission",
      "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
      "form_id": "frm_xxxxxxxxxxxxxxxx",
      "status": "received",
      "fields": { "name": "Ada", "email": "ada@example.com" },
      "meta": {
        "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
        "origin": "https://example.com",
        "referrer": "https://example.com/contact",
        "country": "GB",
        "subject": null,
        "reply_to": "ada@example.com"
      },
      "spam": { "score": 0, "reasons": [] },
      "created_at": "2026-10-17T12:00:00.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c3ViXzAxazd2MmM5dDNlOG01cTR4NnowYTFiMmMz"
}
```

### 送信データを取得

`submissions:read` スコープで `GET /submissions/:id`。

- 別のワークスペースの ID は、存在しない ID とまったく同じく `404` を返します。

**curl** (`ターミナル`)

```bash
curl https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3 \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const submission = await ikiform.submissions.get("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{
  "object": "submission",
  "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
  "form_id": "frm_xxxxxxxxxxxxxxxx",
  "status": "received",
  "fields": { "name": "Ada", "email": "ada@example.com" },
  "meta": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "origin": "https://example.com",
    "referrer": "https://example.com/contact",
    "country": "GB",
    "subject": null,
    "reply_to": "ada@example.com"
  },
  "spam": { "score": 0, "reasons": [] },
  "created_at": "2026-10-17T12:00:00.000Z"
}
```

### スパムとしてマーク

`submissions:write` スコープで `POST /submissions/:id/spam`。

- 受信済みの送信データをスパムに移動し、フォームの `submission_count` を減らし、保留中の Webhook の配信をキャンセルします。
- すでにスパムの送信データに対して呼び出すと、変更せずにそのまま返します。

**curl** (`ターミナル`)

```bash
curl -X POST https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/spam \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const submission = await ikiform.submissions.markSpam("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/spam", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{
  "object": "submission",
  "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
  "form_id": "frm_xxxxxxxxxxxxxxxx",
  "status": "spam",
  "fields": { "name": "Ada", "email": "ada@example.com" },
  "meta": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "origin": "https://example.com",
    "referrer": "https://example.com/contact",
    "country": "GB",
    "subject": null,
    "reply_to": "ada@example.com"
  },
  "spam": { "score": 0, "reasons": [] },
  "created_at": "2026-10-17T12:00:00.000Z"
}
```

### スパムではないとマーク

`submissions:write` スコープで `POST /submissions/:id/not-spam`。

- 受信済みに戻し、スパムスコアと理由を消去し、一致するすべての Webhook エンドポイントに `submission.created` を送ります。
- 受信済みの送信データに対して呼び出すと、変更せずにそのまま返します。

**curl** (`ターミナル`)

```bash
curl -X POST https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/not-spam \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const submission = await ikiform.submissions.markNotSpam("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/not-spam", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{
  "object": "submission",
  "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
  "form_id": "frm_xxxxxxxxxxxxxxxx",
  "status": "received",
  "fields": { "name": "Ada", "email": "ada@example.com" },
  "meta": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "origin": "https://example.com",
    "referrer": "https://example.com/contact",
    "country": "GB",
    "subject": null,
    "reply_to": "ada@example.com"
  },
  "spam": { "score": 0, "reasons": [] },
  "created_at": "2026-10-17T12:00:00.000Z"
}
```

### 送信データを削除

`submissions:write` スコープで `DELETE /submissions/:id`。

- 送信データとその Webhook の配信を完全に削除します。元に戻すことはできません。もう一度削除すると `404` が返ります。

**curl** (`ターミナル`)

```bash
curl -X DELETE https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3 \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const result = await ikiform.submissions.delete("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**レスポンス** (`200 OK`)

```json
{ "object": "submission", "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3", "deleted": true }
```

## オブジェクト

オブジェクトは `object` フィールドを持つ snake_case の JSON です。タイムスタンプはミリ秒付きの ISO 8601（UTC）です。送信 ID は時刻順（`sub_` に続く 26 文字）、フォーム ID はランダム（`frm_` に続く 16 文字）です。

| フォームのフィールド | 説明 |
| --- | --- |
| `status` | `active`、`paused`、`disabled` のいずれか |
| `submission_count` | 受信済みの送信データ数（スパムを除く） |

| 送信データのフィールド | 説明 |
| --- | --- |
| `status` | `received` または `spam` |
| `fields` | 保存された値 |
| `meta` | `user_agent`、`origin`、`referrer`、`country`、`subject`、`reply_to`。すべてのキーが存在し、どの値も `null` になりえます。IP ハッシュが返されることはありません |
| `spam.score`, `spam.reasons` | スパムスコアと理由（`honeypot`、`origin_mismatch` など） |

## ページネーション

一覧は新しい順で、不透明なカーソルを使います。

1. カーソルなしでリクエストします。
2. `has_more` が true なら、同じフィルターで `next_cursor` を `cursor` として送ります。
3. `next_cursor` が `null` になったら終了します。

新しい送信データが届いても、ページは安定しています。新しいものは最初のページに表示され、後ろのページで重複することはありません。カーソルを自分で組み立てたり解析したりしないでください。無効なカーソルは `400 invalid_request` を返します。

**SDK** (`export.ts`)

```ts
for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx" })) {
  console.log(submission.id, submission.fields);
}
```

**fetch** (`export.js`)

```js
let cursor = null;

do {
  const url = new URL("https://ikiform.com/api/v1/submissions");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
  });
  const page = await response.json();
  for (const submission of page.data) console.log(submission.id);
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
```

## エラー

エラーはフォームの送信と同じ形式を使います。すべてのレスポンスに `X-Request-Id` ヘッダーが付き、エラーではそれが `request_id` として繰り返されます。

`403 Forbidden`:

```json
{
  "ok": false,
  "error": { "code": "forbidden", "message": "This API key is missing the forms:read scope." },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

| ステータス | コード | タイミング |
| --- | --- | --- |
| 400 | `invalid_request` | `limit`、`status`、`form_id`、`cursor` のいずれかが無効。メッセージにその名前が入ります |
| 401 | `unauthorized` | キーがない、または無効 |
| 403 | `forbidden` | 公開可能キー、またはスコープ不足 |
| 404 | `not_found` | 存在しないルートや ID、または別のワークスペースの ID |
| 405 | `method_not_allowed` | ブラウザーのプリフライト |
| 429 | `rate_limited` | キーごとのレート制限を超えた |
| 500 | `server_error` | こちら側で問題が発生。`request_id` を伝えてください |

## レート制限

各キーは 60 秒あたり 100 リクエストまで送れます。上限を超えると、API は秒数を示す `Retry-After` ヘッダー付きで `429 rate_limited` を返します。SDK ではこれを `error.retryAfter` として参照できます。
