ID do formulário

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
npm install @ikiform/sdk
ImportDeRoda em
submit@ikiform/sdkNavegador ou servidor
challenge, ChallengeError@ikiform/sdkNavegador
getSchema, validate, validateField@ikiform/sdkNavegador ou servidor
createClient, IkiformError@ikiform/sdkSó servidor
verifyWebhook, WebhookVerificationError@ikiform/sdk/webhooksServidor

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
type SubmitResult =
  | { ok: true; id: string }
  | {
      ok: false;
      error: { code: string; message: string; fields?: Record<string, string[]> };
      status: number | null;
      requestId: string | null;
    };
dataEnviado como
FormData só com textoapplication/x-www-form-urlencoded
FormData com um Filemultipart/form-data, que o Ikiform recusa até o upload de arquivos existir
URLSearchParamsapplication/x-www-form-urlencoded
Um objeto simplesapplication/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çãoDescrição
baseUrlO padrão é https://ikiform.com.
signalUm AbortSignal para cancelar a requisição.
idempotencyKeyEnviado como o header Idempotency-Key.
turnstileTokenEnviado como cf-turnstile-response em qualquer formato de corpo usado.
fetchUma implementação personalizada 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 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çãoDescrição
containerOnde 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.
timeoutMsRejeita depois deste número de milissegundos. Sem timeout por padrão.
signalUm AbortSignal.
baseUrlO padrão é https://ikiform.com.

Ele rejeita com ChallengeError, cujo code é challenge_failed, challenge_timeout ou 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?) 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.

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
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. Ele lança um TypeError se a chave não for secreta, então mantenha-o no servidor.

MétodoRequisiçã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
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
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

reasonSignificado
invalid_secretO segredo não é whsec_ mais base64
missing_headersFalta webhook-id, webhook-timestamp ou webhook-signature
invalid_timestampwebhook-timestamp não está em segundos Unix inteiros
timestamp_too_oldAssinado há mais de toleranceSeconds, por exemplo uma requisição repetida
timestamp_too_newAssinado no futuro; confira o relógio do seu servidor
no_matching_signatureNenhuma assinatura v1 corresponde: segredo errado, ou o corpo foi alterado
invalid_payloadA assinatura correspondeu, mas o corpo não é JSON
Os tipos de evento são exportados como WebhookEvent, SubmissionCreatedEvent e EndpointTestEvent. Veja Webhooks para o payload completo.