# Referência do SDK

> O pacote @ikiform/sdk: submit, challenge, getSchema, validate, createClient e verifyWebhook.

`@ikiform/sdk` é um pequeno pacote TypeScript para enviar formulários, ler dados da API REST e verificar webhooks. Ele não tem dependências e usa só `fetch`, `FormData` e Web Crypto, então roda em navegadores, Node 20+, Bun, Deno e Cloudflare Workers. Ele é distribuído como ESM com declarações de tipo.

`Terminal`:

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

| Import | De | Roda em |
| --- | --- | --- |
| `submit` | `@ikiform/sdk` | Navegador ou servidor |
| `challenge`, `ChallengeError` | `@ikiform/sdk` | Navegador |
| `getSchema`, `validate`, `validateField` | `@ikiform/sdk` | Navegador ou servidor |
| `createClient`, `IkiformError` | `@ikiform/sdk` | Só servidor |
| `verifyWebhook`, `WebhookVerificationError` | `@ikiform/sdk/webhooks` | Servidor |

## submit

`submit(formId, data, options?)` envia para `/f/:formId` e resolve para um resultado. Ele nunca lança exceção: erros HTTP retornam o envelope de erro, falhas de rede retornam o código `network_error` e cancelamentos retornam `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 | Enviado como |
| --- | --- |
| `FormData` só com texto | `application/x-www-form-urlencoded` |
| `FormData` com um `File` | `multipart/form-data`, que o Ikiform recusa até o upload de arquivos existir |
| `URLSearchParams` | `application/x-www-form-urlencoded` |
| Um objeto simples | `application/json` |

- Os campos `$ACTION_` do React são descartados do `FormData`.
- O valor retornado também satisfaz o tipo de form action do React, então `action={(data) => submit(id, data)}` funciona.
- `submit` envia `Accept: application/json`. Ele não envia um header `Authorization`; use `fetch` para enviar com uma chave de API.

| Opção | Descrição |
| --- | --- |
| `baseUrl` | O padrão é `https://ikiform.com`. |
| `signal` | Um `AbortSignal` para cancelar a requisição. |
| `idempotencyKey` | Enviado como o header `Idempotency-Key`. |
| `turnstileToken` | Enviado como `cf-turnstile-response` em qualquer formato de corpo usado. |
| `fetch` | Uma implementação personalizada de `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

Para formulários com a verificação de spam **Ikiform**. `challenge(formId, options?)` monta um frame de ikiform.com com 300 pixels de largura que fica com altura zero, a menos que o Turnstile precise de um clique, resolve com um token e remove o frame. Os tokens são de uso único e valem por 300 segundos, então chame uma vez por envio.

| Opção | Descrição |
| --- | --- |
| `container` | Onde montar o frame. O padrão é `document.body`. |
| `mode` | `"frame"` (padrão) ou `"popup"`, para navegadores que bloqueiam o frame. O popup precisa ser aberto a partir de um clique. |
| `timeoutMs` | Rejeita depois deste número de milissegundos. Sem timeout por padrão. |
| `signal` | Um `AbortSignal`. |
| `baseUrl` | O padrão é `https://ikiform.com`. |

Ele rejeita com `ChallengeError`, cujo `code` é `challenge_failed`, `challenge_timeout` ou `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?)` busca o schema público do formulário: os campos publicados, as configurações de exibição, o nome do honeypot e o modo de verificação. Assim como `submit`, ele nunca lança exceção e resolve para `{ ok: true, schema }` ou um resultado de erro. As opções são `baseUrl`, `signal` e `fetch`. A resposta está descrita na [página do shadcn/ui](https://ikiform.com/pt-br/docs/shadcn.md#schema-endpoint).

## validate

`validate(fields, data)` confere um `FormData` ou um objeto simples contra as definições de campo e retorna `Record<string, string>`, uma mensagem por campo inválido, ou um objeto vazio. Ele usa as mesmas regras e mensagens do servidor para os 12 tipos de campo. `validateField(field, value)` confere um valor e retorna uma mensagem ou `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? })` retorna um client para a [API REST](https://ikiform.com/pt-br/docs/api.md). Ele lança um `TypeError` se a chave não for secreta, então mantenha-o no servidor.

| Método | Requisição |
| --- | --- |
| `forms.list({ limit, cursor })` | `GET /forms` |
| `forms.get(formId)` | `GET /forms/:formId` |
| `forms.iterate({ limit })` | Todos os formulários, seguindo os cursores |
| `submissions.list({ formId, status, limit, cursor })` | `GET /submissions` |
| `submissions.get(id)` | `GET /submissions/:id` |
| `submissions.iterate({ formId, status, limit })` | Todos os envios, seguindo os cursores |
| `submissions.markSpam(id)` | `POST /submissions/:id/spam` |
| `submissions.markNotSpam(id)` | `POST /submissions/:id/not-spam` |
| `submissions.delete(id)` | `DELETE /submissions/:id` |

Falhas lançam `IkiformError` com `code`, `status`, `requestId`, `message`, mais `fields` quando presente e `retryAfter` em segundos em caso de `429`. Falhas de rede lançam com `code: "network_error"` e `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? })` verifica a assinatura de um webhook e retorna o evento já interpretado. Passe a string do corpo bruto. A tolerância padrão é de 300 segundos nas duas direções.

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

### Erros de verificação

| reason | Significado |
| --- | --- |
| `invalid_secret` | O segredo não é `whsec_` mais base64 |
| `missing_headers` | Falta `webhook-id`, `webhook-timestamp` ou `webhook-signature` |
| `invalid_timestamp` | `webhook-timestamp` não está em segundos Unix inteiros |
| `timestamp_too_old` | Assinado há mais de `toleranceSeconds`, por exemplo uma requisição repetida |
| `timestamp_too_new` | Assinado no futuro; confira o relógio do seu servidor |
| `no_matching_signature` | Nenhuma assinatura `v1` corresponde: segredo errado, ou o corpo foi alterado |
| `invalid_payload` | A assinatura correspondeu, mas o corpo não é JSON |

> Os tipos de evento são exportados como `WebhookEvent`, `SubmissionCreatedEvent` e `EndpointTestEvent`. Veja [Webhooks](https://ikiform.com/pt-br/docs/webhooks.md#payloads) para o payload completo.
