ID del formulario

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
npm install @ikiform/sdk
ImportaciónDesdeFunciona en
submit@ikiform/sdkNavegador o servidor
challenge, ChallengeError@ikiform/sdkNavegador
getSchema, validate, validateField@ikiform/sdkNavegador o servidor
createClient, IkiformError@ikiform/sdkSolo servidor
verifyWebhook, WebhookVerificationError@ikiform/sdk/webhooksServidor

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
type SubmitResult =
  | { ok: true; id: string }
  | {
      ok: false;
      error: { code: string; message: string; fields?: Record<string, string[]> };
      status: number | null;
      requestId: string | null;
    };
dataSe envía como
FormData solo con textoapplication/x-www-form-urlencoded
FormData con un Filemultipart/form-data, que Ikiform rechaza hasta que exista la subida de archivos
URLSearchParamsapplication/x-www-form-urlencoded
Un objeto simpleapplication/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ónDescripción
baseUrlPor defecto, https://ikiform.com.
signalUn AbortSignal para cancelar la petición.
idempotencyKeySe envía como la cabecera Idempotency-Key.
turnstileTokenSe envía como cf-turnstile-response en el formato de cuerpo que se use.
fetchUna implementación propia de fetch.
submit.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ónDescripción
containerDó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.
timeoutMsRechaza pasados estos milisegundos. Sin timeout por defecto.
signalUn AbortSignal.
baseUrlPor defecto, https://ikiform.com.

Se rechaza con ChallengeError, cuyo code es challenge_failed, challenge_timeout o aborted.

challenge.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.

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
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. Lanza un TypeError si la clave no es secreta, así que mantenlo en el servidor.

MétodoPetició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
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
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

reasonSignificado
invalid_secretEl secreto no es whsec_ más base64
missing_headersFalta webhook-id, webhook-timestamp o webhook-signature
invalid_timestampwebhook-timestamp no es un número entero de segundos Unix
timestamp_too_oldSe firmó hace más de toleranceSeconds, por ejemplo en una petición repetida
timestamp_too_newSe firmó en el futuro; revisa el reloj de tu servidor
no_matching_signatureNinguna firma v1 coincide: el secreto es incorrecto o el cuerpo cambió
invalid_payloadLa firma coincidió, pero el cuerpo no es JSON
Los tipos de evento se exportan como WebhookEvent, SubmissionCreatedEvent y EndpointTestEvent. Consulta Webhooks para ver el payload completo.