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.
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.
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 deFormData. - El valor devuelto también cumple el tipo de form action de React, así que
action={(data) => submit(id, data)}funciona. submitenvíaAccept: application/json. No envía la cabeceraAuthorization; usafetchpara 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. |
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.
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.
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é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.
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.
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 |
WebhookEvent, SubmissionCreatedEvent y EndpointTestEvent. Consulta Webhooks para ver el payload completo.