Form ID

JavaScript and fetch

The submission endpoint in detail: body formats, headers, CORS, responses and idempotency.

Any code that can send an HTTP request can submit to Ikiform. Post JSON, FormData or urlencoded data to https://ikiform.com/f/<formId> and read a JSON answer. The SDK wraps this in one function, but it is optional. For framework walkthroughs, see the guides for Next.js, React, Vue, Svelte and Astro.

With fetch

const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({
    name: "Ada Lovelace",
    email: "ada@example.com",
    message: "Hello",
  }),
});

const result = await response.json();
if (!result.ok) console.error(result.error.code, result.error.fields);

Try it

Form ID
Fields
Body
Idempotency key
curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada Lovelace","email":"ada@example.com","message":"Hello from the Ikiform docs."}'

Enter your own form ID to send this as a real request.

Endpoints

MethodPathWhat it does
POST/f/<formId>Create a submission
OPTIONS/f/<formId>CORS preflight, always 204
GET/f/<formId>/schemaThe form's public field definition, see shadcn/ui
GET/f/<formId>/thanksThe hosted thank-you page
GET/f/<formId>/challengeThe Ikiform human check frame, used by the SDK

Opening /f/<formId> itself in a browser tab is a GET, which answers 404 not_found. That is expected: the endpoint only takes posts.

Body formats

Content-TypeShapeNotes
application/json and +json typesOne JSON objectValues can be strings, numbers, booleans, arrays and objects, nested at most 3 levels counting the top object
application/x-www-form-urlencodedName and value pairsRepeated names and name[] become arrays
multipart/form-dataName and value pairsSame as urlencoded; real files are refused
Anything else415 unsupported_media_type

Bodies must be UTF-8. A charset other than utf-8 gets 415 unsupported_charset, and bytes that aren't valid UTF-8 get the same code. A JSON array or a malformed body gets 400 invalid_body.

Headers

Request headers

HeaderRequiredEffect
Content-TypeYesPicks the body parser, see above
AcceptNoapplication/json forces a JSON answer, see JSON or a redirect
Idempotency-KeyNoMakes retries safe, up to 255 characters, see Idempotency
AuthorizationNoBearer ik_sk_... or Bearer ik_pub_..., see From a server
Origin, RefererSent by browsersChecked against the form's allowed domains

Response headers

HeaderWhenValue
X-Request-IdEvery responsereq_..., repeated as request_id in error bodies
Retry-After429Seconds until the next post is allowed
Location303 browser successYour redirect or the hosted thank-you page
Access-Control-Allow-OriginWhen the request has an OriginThe request's own origin
Access-Control-Expose-HeadersWith CORSX-Request-Id, Retry-After, so browser code can read both
Cache-ControlSuccessno-store

CORS

The submission endpoint accepts cross-origin posts from any site. Preflights always succeed and echo the request's origin. No cookies or credentials are used, so CORS is not what protects your form: allowed domains are. With allowed domains set, a post from another site is stored as spam or refused, depending on the form's setting. See Allowed domains.

Preflight
OPTIONS /f/frm_xxxxxxxxxxxxxxxx HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, Accept, Idempotency-Key, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
The REST API under /api/v1 is different: it sends no CORS headers and answers browser preflights with 405, so a secret key can never be used from a web page.

Responses

Success is 201 with the new submission id. Errors use one envelope everywhere, with field errors keyed by field id when validation fails. Every code is listed on Errors.

{ "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" }
A post that scores as spam still gets 201. Spam is stored separately and never reaches your webhooks or inbox.

Idempotency

Send an Idempotency-Key header, or an _idempotency field, to make retries safe. Keys are up to 255 characters and are scoped to the form for 24 hours. The header wins when both are sent.

Retry withResult
The same key and the same fieldsThe first response is replayed. No second submission is created.
The same key and different fields422 idempotency_mismatch
The same key while the first request is still running409 idempotency_in_progress

Posts that fail before the submission is stored, such as validation errors, don't keep the key, so you can fix the data and retry with the same key. A safe retry loop:

retry.js
const key = crypto.randomUUID();

async function send(attempt = 1) {
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: { "Content-Type": "application/json", "Idempotency-Key": key },
    body: JSON.stringify({ email: "ada@example.com" }),
  }).catch(() => null);
  if (response && response.status < 500 && response.status !== 429) return response.json();
  if (attempt === 3) throw new Error("Ikiform is unreachable");
  const wait = Number(response?.headers.get("Retry-After")) || attempt * 2;
  await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  return send(attempt + 1);
}