# Referencia del SDK

> El paquete @ikiform/sdk: submit, challenge, getSchema, validate, createClient y verifyWebhook.

`@ikiform/sdk` es un paquete pequeño de TypeScript para enviar formularios, leer datos de la API REST y verificar webhooks. No tiene dependencias y solo usa `fetch`, `FormData` y Web Crypto, así que funciona en navegadores, Node 20+, Bun, Deno y Cloudflare Workers. Se distribuye como ESM con declaraciones de tipos.

`Terminal`:

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

| Importación | Desde | Funciona en |
| --- | --- | --- |
| `submit` | `@ikiform/sdk` | Navegador o servidor |
| `challenge`, `ChallengeError` | `@ikiform/sdk` | Navegador |
| `getSchema`, `validate`, `validateField` | `@ikiform/sdk` | Navegador o servidor |
| `createClient`, `IkiformError` | `@ikiform/sdk` | Solo servidor |
| `verifyWebhook`, `WebhookVerificationError` | `@ikiform/sdk/webhooks` | Servidor |

## submit

`submit(formId, data, options?)` envía a `/f/:formId` y se resuelve con un resultado. Nunca lanza excepciones: los errores HTTP devuelven el formato de error, los fallos de red devuelven el código `network_error` y las cancelaciones devuelven `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 | Se envía como |
| --- | --- |
| `FormData` solo con texto | `application/x-www-form-urlencoded` |
| `FormData` con un `File` | `multipart/form-data`, que Ikiform rechaza hasta que exista la subida de archivos |
| `URLSearchParams` | `application/x-www-form-urlencoded` |
| Un objeto simple | `application/json` |

- Los campos `$ACTION_` de React se descartan de `FormData`.
- El valor devuelto también cumple el tipo de form action de React, así que `action={(data) => submit(id, data)}` funciona.
- `submit` envía `Accept: application/json`. No envía la cabecera `Authorization`; usa `fetch` para enviar con una clave de API.

| Opción | Descripción |
| --- | --- |
| `baseUrl` | Por defecto, `https://ikiform.com`. |
| `signal` | Un `AbortSignal` para cancelar la petición. |
| `idempotencyKey` | Se envía como la cabecera `Idempotency-Key`. |
| `turnstileToken` | Se envía como `cf-turnstile-response` en el formato de cuerpo que se use. |
| `fetch` | Una implementación propia 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 formularios con la verificación antispam en **Ikiform**. `challenge(formId, options?)` monta un frame de ikiform.com de 300 píxeles de ancho que se queda con altura cero salvo que Turnstile necesite un clic, se resuelve con un token y elimina el frame. Los tokens son de un solo uso y duran 300 segundos, así que llámalo una vez por envío.

| Opción | Descripción |
| --- | --- |
| `container` | Dónde montar el frame. Por defecto, `document.body`. |
| `mode` | `"frame"` (por defecto) o `"popup"`, para los navegadores que bloquean el frame. El popup debe abrirse desde un clic. |
| `timeoutMs` | Rechaza pasados estos milisegundos. Sin timeout por defecto. |
| `signal` | Un `AbortSignal`. |
| `baseUrl` | Por defecto, `https://ikiform.com`. |

Se rechaza con `ChallengeError`, cuyo `code` es `challenge_failed`, `challenge_timeout` o `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?)` obtiene el esquema público del formulario: sus campos publicados, los ajustes de visualización, el nombre del honeypot y el modo de verificación. Igual que `submit`, nunca lanza excepciones y se resuelve con `{ ok: true, schema }` o con un resultado de error. Las opciones son `baseUrl`, `signal` y `fetch`. La respuesta se describe en la [página de shadcn/ui](https://ikiform.com/es/docs/shadcn.md#schema-endpoint).

## validate

`validate(fields, data)` comprueba `FormData` o un objeto simple contra las definiciones de campos y devuelve `Record<string, string>`, un mensaje por cada campo no válido, o un objeto vacío. Usa las mismas reglas y mensajes que el servidor para los 12 tipos de campo. `validateField(field, value)` comprueba un valor y devuelve un mensaje o `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? })` devuelve un cliente para la [API REST](https://ikiform.com/es/docs/api.md). Lanza un `TypeError` si la clave no es secreta, así que mantenlo en el servidor.

| Método | Petición |
| --- | --- |
| `forms.list({ limit, cursor })` | `GET /forms` |
| `forms.get(formId)` | `GET /forms/:formId` |
| `forms.iterate({ limit })` | Todos los formularios, siguiendo los cursores |
| `submissions.list({ formId, status, limit, cursor })` | `GET /submissions` |
| `submissions.get(id)` | `GET /submissions/:id` |
| `submissions.iterate({ formId, status, limit })` | Todos los envíos, siguiendo los cursores |
| `submissions.markSpam(id)` | `POST /submissions/:id/spam` |
| `submissions.markNotSpam(id)` | `POST /submissions/:id/not-spam` |
| `submissions.delete(id)` | `DELETE /submissions/:id` |

Los fallos lanzan `IkiformError` con `code`, `status`, `requestId`, `message`, además de `fields` cuando existe y `retryAfter` en segundos ante un `429`. Los fallos de red lanzan con `code: "network_error"` y `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 la firma de un webhook y devuelve el evento ya analizado. Pasa la cadena del cuerpo sin procesar. La tolerancia por defecto es de 300 segundos en ambos sentidos.

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

### Errores de verificación

| reason | Significado |
| --- | --- |
| `invalid_secret` | El secreto no es `whsec_` más base64 |
| `missing_headers` | Falta `webhook-id`, `webhook-timestamp` o `webhook-signature` |
| `invalid_timestamp` | `webhook-timestamp` no es un número entero de segundos Unix |
| `timestamp_too_old` | Se firmó hace más de `toleranceSeconds`, por ejemplo en una petición repetida |
| `timestamp_too_new` | Se firmó en el futuro; revisa el reloj de tu servidor |
| `no_matching_signature` | Ninguna firma `v1` coincide: el secreto es incorrecto o el cuerpo cambió |
| `invalid_payload` | La firma coincidió, pero el cuerpo no es JSON |

> Los tipos de evento se exportan como `WebhookEvent`, `SubmissionCreatedEvent` y `EndpointTestEvent`. Consulta [Webhooks](https://ikiform.com/es/docs/webhooks.md#payloads) para ver el payload completo.
