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
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
| Method | Path | What it does |
|---|---|---|
POST | /f/<formId> | Create a submission |
OPTIONS | /f/<formId> | CORS preflight, always 204 |
GET | /f/<formId>/schema | The form's public field definition, see shadcn/ui |
GET | /f/<formId>/thanks | The hosted thank-you page |
GET | /f/<formId>/challenge | The 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-Type | Shape | Notes |
|---|---|---|
application/json and +json types | One JSON object | Values can be strings, numbers, booleans, arrays and objects, nested at most 3 levels counting the top object |
application/x-www-form-urlencoded | Name and value pairs | Repeated names and name[] become arrays |
multipart/form-data | Name and value pairs | Same as urlencoded; real files are refused |
| Anything else | 415 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
| Header | Required | Effect |
|---|---|---|
Content-Type | Yes | Picks the body parser, see above |
Accept | No | application/json forces a JSON answer, see JSON or a redirect |
Idempotency-Key | No | Makes retries safe, up to 255 characters, see Idempotency |
Authorization | No | Bearer ik_sk_... or Bearer ik_pub_..., see From a server |
Origin, Referer | Sent by browsers | Checked against the form's allowed domains |
Response headers
| Header | When | Value |
|---|---|---|
X-Request-Id | Every response | req_..., repeated as request_id in error bodies |
Retry-After | 429 | Seconds until the next post is allowed |
Location | 303 browser success | Your redirect or the hosted thank-you page |
Access-Control-Allow-Origin | When the request has an Origin | The request's own origin |
Access-Control-Expose-Headers | With CORS | X-Request-Id, Retry-After, so browser code can read both |
Cache-Control | Success | no-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.
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/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" }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 with | Result |
|---|---|
| The same key and the same fields | The first response is replayed. No second submission is created. |
| The same key and different fields | 422 idempotency_mismatch |
| The same key while the first request is still running | 409 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:
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);
}