# SDK 레퍼런스

> @ikiform/sdk 패키지: submit, challenge, getSchema, validate, createClient, verifyWebhook.

`@ikiform/sdk`는 폼 제출, REST API 데이터 조회, 웹훅 검증을 위한 작은 TypeScript 패키지입니다. 의존성이 없고 `fetch`, `FormData`, Web Crypto만 사용하므로 브라우저, Node 20+, Bun, Deno, Cloudflare Workers에서 실행됩니다. 타입 선언과 함께 ESM으로 배포됩니다.

`Terminal`:

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

| 가져올 항목 | 패키지 | 실행 환경 |
| --- | --- | --- |
| `submit` | `@ikiform/sdk` | 브라우저 또는 서버 |
| `challenge`, `ChallengeError` | `@ikiform/sdk` | 브라우저 |
| `getSchema`, `validate`, `validateField` | `@ikiform/sdk` | 브라우저 또는 서버 |
| `createClient`, `IkiformError` | `@ikiform/sdk` | 서버 전용 |
| `verifyWebhook`, `WebhookVerificationError` | `@ikiform/sdk/webhooks` | 서버 |

## submit

`submit(formId, data, options?)`는 `/f/:formId`로 POST하고 결과로 resolve됩니다. 예외를 던지지 않습니다. HTTP 오류는 오류 형식을 반환하고, 네트워크 오류는 `network_error` 코드를, 중단은 `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 | 전송 형식 |
| --- | --- |
| 텍스트만 있는 `FormData` | `application/x-www-form-urlencoded` |
| `File`이 있는 `FormData` | `multipart/form-data`, 업로드 기능이 나오기 전까지 Ikiform이 거부 |
| `URLSearchParams` | `application/x-www-form-urlencoded` |
| 일반 객체 | `application/json` |

- `FormData`에서 React의 `$ACTION_` 필드는 제거됩니다.
- 반환 값은 React의 폼 액션 타입도 만족하므로 `action={(data) => submit(id, data)}`가 작동합니다.
- `submit`은 `Accept: application/json`을 보냅니다. `Authorization` 헤더는 보내지 않으므로, API 키로 POST하려면 `fetch`를 사용하세요.

| 옵션 | 설명 |
| --- | --- |
| `baseUrl` | 기본값은 `https://ikiform.com`입니다. |
| `signal` | 요청을 취소하는 `AbortSignal`. |
| `idempotencyKey` | `Idempotency-Key` 헤더로 전송됩니다. |
| `turnstileToken` | 어떤 본문 형식을 쓰든 `cf-turnstile-response`로 전송됩니다. |
| `fetch` | 커스텀 `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

스팸 검사가 **Ikiform**인 폼용입니다. `challenge(formId, options?)`는 너비 300픽셀의 ikiform.com 프레임을 마운트하며, Turnstile에 클릭이 필요할 때까지 높이가 없는 상태로 유지됩니다. 토큰으로 resolve된 뒤 프레임을 제거합니다. 토큰은 한 번만 쓸 수 있고 300초 동안 유효하므로 제출마다 한 번씩 호출하세요.

| 옵션 | 설명 |
| --- | --- |
| `container` | 프레임을 마운트할 위치. 기본값은 `document.body`입니다. |
| `mode` | `"frame"`(기본값) 또는 프레임을 막는 브라우저용 `"popup"`. 팝업은 클릭으로 열어야 합니다. |
| `timeoutMs` | 이 밀리초가 지나면 reject합니다. 기본값은 시간 제한 없음입니다. |
| `signal` | `AbortSignal`. |
| `baseUrl` | 기본값은 `https://ikiform.com`입니다. |

`code`가 `challenge_failed`, `challenge_timeout`, `aborted` 중 하나인 `ChallengeError`로 reject됩니다.

`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?)`는 폼의 공개 스키마를 가져옵니다. 게시된 필드, 표시 설정, 허니팟 이름, 챌린지 모드가 포함됩니다. `submit`처럼 예외를 던지지 않으며 `{ ok: true, schema }` 또는 오류 결과로 resolve됩니다. 옵션은 `baseUrl`, `signal`, `fetch`입니다. 응답은 [shadcn/ui 페이지](https://ikiform.com/ko/docs/shadcn.md#schema-endpoint)에 설명되어 있습니다.

## validate

`validate(fields, data)`는 `FormData`나 일반 객체를 필드 정의와 비교해 확인하고, 유효하지 않은 필드마다 메시지 하나씩 담은 `Record<string, string>` 또는 빈 객체를 반환합니다. 12가지 필드 타입 모두에서 서버와 같은 규칙과 메시지를 사용합니다. `validateField(field, value)`는 값 하나를 확인하고 메시지나 `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? })`는 [REST API](https://ikiform.com/ko/docs/api.md)용 클라이언트를 반환합니다. 키가 시크릿 키가 아니면 `TypeError`를 던지므로 서버에서만 사용하세요.

| 메서드 | 요청 |
| --- | --- |
| `forms.list({ limit, cursor })` | `GET /forms` |
| `forms.get(formId)` | `GET /forms/:formId` |
| `forms.iterate({ limit })` | 커서를 따라가며 모든 폼 |
| `submissions.list({ formId, status, limit, cursor })` | `GET /submissions` |
| `submissions.get(id)` | `GET /submissions/:id` |
| `submissions.iterate({ formId, status, limit })` | 커서를 따라가며 모든 제출 |
| `submissions.markSpam(id)` | `POST /submissions/:id/spam` |
| `submissions.markNotSpam(id)` | `POST /submissions/:id/not-spam` |
| `submissions.delete(id)` | `DELETE /submissions/:id` |

실패하면 `code`, `status`, `requestId`, `message`, 그리고 있으면 `fields`, `429`에서는 초 단위의 `retryAfter`가 담긴 `IkiformError`를 던집니다. 네트워크 오류는 `code: "network_error"`와 `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? })`는 웹훅 서명을 검증하고 파싱된 이벤트를 반환합니다. 원본 본문 문자열을 전달하세요. 기본 허용 오차는 양방향 300초입니다.

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

### 검증 오류

| reason | 의미 |
| --- | --- |
| `invalid_secret` | 시크릿이 `whsec_` 뒤에 base64가 붙는 형식이 아님 |
| `missing_headers` | `webhook-id`, `webhook-timestamp`, `webhook-signature` 중 하나가 없음 |
| `invalid_timestamp` | `webhook-timestamp`가 정수 Unix 초가 아님 |
| `timestamp_too_old` | `toleranceSeconds`보다 더 전에 서명됨. 예: 재생된 요청 |
| `timestamp_too_new` | 미래 시각으로 서명됨. 서버 시계를 확인하세요 |
| `no_matching_signature` | 일치하는 `v1` 서명이 없음. 시크릿이 틀렸거나 본문이 바뀜 |
| `invalid_payload` | 서명은 일치하지만 본문이 JSON이 아님 |

> 이벤트 타입은 `WebhookEvent`, `SubmissionCreatedEvent`, `EndpointTestEvent`로 export됩니다. 전체 페이로드는 [웹훅](https://ikiform.com/ko/docs/webhooks.md#payloads)을 참고하세요.
