# Ikiform documentation > The complete Ikiform docs as markdown, generated from the same source as https://ikiform.com/docs. --- Source: https://ikiform.com/docs # Quickstart > Point any HTML form at Ikiform and receive your first submission in two minutes. Ikiform is the backend for every form. You keep your own markup, framework and styles. Point the form at an Ikiform endpoint and Ikiform validates each post, filters spam, stores it in your workspace, emails you and delivers it to your systems by signed webhook. Code samples in these docs use the sample form id `frm_xxxxxxxxxxxxxxxx`. Replace it with your own form's id, which you find on the form's overview in the dashboard. ## Pick how you send Every form gets one public endpoint, `https://ikiform.com/f/`. Anything that can send an HTTP POST can use it. | You send from | Use | Guide | | --- | --- | --- | | A plain HTML page or a site builder | `
` | [HTML forms](https://ikiform.com/docs/html.md) | | React, Next.js, Vue, Svelte, Astro | `fetch` or `submit` from the SDK | [Guides](https://ikiform.com/docs/guides/nextjs.md) | | A backend, script or integration | JSON over HTTPS with a secret key | [From a server](https://ikiform.com/docs/guides/server.md) | | A shadcn/ui project | The registry component | [shadcn/ui component](https://ikiform.com/docs/shadcn.md) | ## Quickstart ### Step 1: Create a form Sign in to the [dashboard](https://ikiform.com/dashboard), create a workspace and add a form. Its overview shows the endpoint URL and ready-made snippets for HTML, JavaScript and React. ### Step 2: Point your form at the endpoint Set the form's `action` to the endpoint and `method` to `post`. Every input with a `name` becomes a field of the submission. `contact.html`: ```html
``` The hidden `_gotcha` input is a honeypot. People never see it; bots that fill it in are stored as spam. See [Spam protection](https://ikiform.com/docs/spam.md#honeypot). ### Step 3: Submit it Open the page and send the form. The browser lands on Ikiform's thank-you page, or on your own page if you set a redirect. You can also post JSON from a terminal: `Terminal`: ```bash curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \ -H "Content-Type: application/json" \ -d '{"name":"Ada","email":"ada@example.com","message":"Hello"}' ``` A JSON request gets a JSON answer with status `201`: `201 Created`: ```json { "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" } ``` ### Step 4: See it arrive The submission shows up in the dashboard right away. From there, add a [webhook](https://ikiform.com/docs/webhooks.md) to send each one to your app, or read them with the [REST API](https://ikiform.com/docs/api.md). ## Try it The interactive request builder on this page composes a submission from field rows and shows the matching curl, fetch and SDK code. With the sample form id it only shows code; with your own form id it can send one real POST to `https://ikiform.com/f/` and print the status and JSON response. The request it builds looks like this: ```bash curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \ -H "Content-Type: application/json" \ -d '{"name":"Ada","email":"ada@example.com","message":"Hello"}' ``` ## Next steps - [How it works](https://ikiform.com/docs/how-it-works.md) walks through every check a post goes through. - [Define fields](https://ikiform.com/docs/fields.md) so Ikiform rejects bad data with clear field errors. - [Turn on a human check](https://ikiform.com/docs/spam.md#human-check) and restrict the form to your domains. - [Verify webhook signatures](https://ikiform.com/docs/webhooks.md#verify) in your backend. --- Source: https://ikiform.com/docs/how-it-works # How it works > Every check a submission goes through, in order, and what each one can return. Each post to `/f/` runs through the same pipeline. Cheap checks run first, so a flood or an unknown form costs almost nothing. The first check that fails decides the response. The pipeline, in order: 1. IP flood guard: at most 600 posts a minute from one IP across every form, before the form is looked up. Fails with `429 rate_limited`. 2. Form lookup: unknown, deleted and disabled forms fail with `404 not_found`. 3. Form rate limits: the form's preset, counted per visitor IP and per form. Fails with `429 rate_limited`. 4. Paused forms fail with `403 form_paused`. 5. Body parsing: urlencoded, multipart or JSON, at most 256 KB. Fails with `415`, `400 invalid_body`, `413 payload_too_large`, `422 too_deep` or `422 files_not_supported`. 6. API key, when an `Authorization` header is sent. Fails with `403 forbidden` or `403 origin_not_allowed`. 7. Allowed domains: a post from another site is stored as spam, or fails with `403 origin_not_allowed`. 8. Human check: fails with `403 challenge_required` or `403 captcha_failed`. Browser form posts get a check page instead. 9. Field validation: fails with `422 validation_failed`. 10. Spam scoring: a score of 5 or more is stored as spam but still answered as a success. 11. Idempotency: a repeated key replays the first response, or fails with `409` or `422`. 12. Monthly usage: far over the limit fails with `429 quota_exceeded`. 13. Store: `201` with the submission id, or a `303` redirect for browser posts. Webhooks, notification emails and the live dashboard update follow. ## What each stage does ### Rate limits before anything else Rate limits run before the body is read. The flood guard (600 a minute per IP) runs even before the form lookup, so a flood never reaches the database. The form's own rules run right after the lookup. Rejected requests don't use up the allowance. Exact numbers are on [Rate limits](https://ikiform.com/docs/rate-limits.md). ### Parsing The body is parsed as urlencoded, multipart or JSON. Text is decoded as UTF-8, normalized to NFC, CRLF becomes LF and control characters are removed. Names that start with `_` are reserved instructions, such as `_redirect` and `_subject`, and are never stored as answers. See [HTML forms](https://ikiform.com/docs/html.md#reserved-fields). ### Who sent it An `Authorization: Bearer` header is optional. A secret key (`ik_sk_`) with the `submissions:write` scope skips the allowed-domains check and the Ikiform human check. A publishable key (`ik_pub_`) only works from the domains listed on the key. Without a key, the post is judged by its `Origin`. ### Spam Spam is never an error. A post that fails the allowed-domains policy in spam mode, or scores 5 or more, is stored with status `spam` and gets the same success response as a real one, so bots learn nothing. Spam sends no webhooks or emails and doesn't count toward usage. See [Spam protection](https://ikiform.com/docs/spam.md). ### Store and deliver The submission, its webhook deliveries and its notification email are written in one database batch, then handed to a queue. Webhook delivery and email never slow down the response, and a submission is never stored without its deliveries. ## JSON or a redirect The same endpoint answers browsers with pages and code with JSON. Ikiform decides per request: | Request | Answer | | --- | --- | | `Accept` includes `application/json` | JSON | | The body is JSON | JSON | | `Sec-Fetch-Mode: navigate` (a normal form post) | Redirect or hosted page | | Any other `Sec-Fetch-Mode`, such as `cors` from `fetch` | JSON | | No `Sec-Fetch-Mode`, and `Accept` includes `text/html` | Redirect or hosted page | | Anything else, such as curl | JSON | In JSON mode success is `201 { "ok": true, "id": "sub_..." }`. In page mode success is a `303` redirect, and errors render a hosted error page that lists field problems by label. ## After a submission is stored | What | When | Details | | --- | --- | --- | | Webhooks | Seconds later, retried for about 3 days | [Webhooks](https://ikiform.com/docs/webhooks.md) | | Notification email | Seconds later, if notifications are on for the form | Capped at 50 emails per form per hour (10 for workspaces younger than 7 days), then one hourly digest | | Dashboard | Immediately, live | Spam shows under the spam filter | | REST API | Immediately | [REST API](https://ikiform.com/docs/api.md) | > Forms whose submissions contain password, card or seed-phrase style field names are flagged and their notification emails held for review. Don't collect credentials with a form backend. ## Ids | Id | Looks like | Notes | | --- | --- | --- | | Form | `frm_xxxxxxxxxxxxxxxx` | `frm_` plus 16 random characters. Public: it is in your form's action URL. | | Submission | `sub_01k7v2c9t3e8m5q4x6z0a1b2c3` | `sub_` plus 26 characters, ordered by time. | | Webhook delivery | `dlv_...` | Sent as `webhook-id`; stays the same across retries. | | Request | `req_...` | Every response has an `X-Request-Id` header. Quote it when you contact support. | **201** (`JSON success`) ```json { "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" } ``` **303** (`Browser success`) ```http HTTP/1.1 303 See Other Location: https://example.com/thanks X-Request-Id: req_01k7v2c9t3e8m5q4x6z0a1b2c3 ``` --- Source: https://ikiform.com/docs/html # HTML forms > Post any HTML form to Ikiform with no JavaScript: redirects, reserved fields, arrays and the honeypot. A plain HTML form is the whole integration. No script, no iframe and no embed: set the `action` to your form's endpoint and keep your own markup and CSS. This also works in site builders that let you paste custom HTML; see [Site builders](https://ikiform.com/docs/guides/site-builders.md). ## A basic form `contact.html`: ```html
``` - `method` must be `post`. The browser sends `application/x-www-form-urlencoded`, or `multipart/form-data` if you set `enctype`. Both work. - Every control with a `name` becomes a field. Controls without a name, and disabled controls, are not sent by the browser. - Values are stored as UTF-8 text, normalized to NFC, with CRLF line endings turned into LF and control characters removed. - Browser validation such as `required` and `type="email"` helps people, but it is not a security check. Define [fields](https://ikiform.com/docs/fields.md) to have Ikiform enforce rules on the server. ## After submit A successful browser post is answered with a `303` redirect. Ikiform picks the target in this order: 1. The form's **Redirect after submit** setting in the dashboard. Any `http:` or `https:` URL works here, because only workspace members can set it. 2. A `_redirect` (or `_next`) field in the post, if it passes the rules below. 3. Otherwise Ikiform's hosted thank-you page at `/f//thanks`. When the form has allowed domains and the post came from an `https:` page on one of them, the page links back to it. ### Rules for _redirect A `_redirect` value comes from the visitor's browser, so Ikiform only follows it when it is an `https:` URL with no username, password or custom port, on one of the form's allowed domains or a subdomain of one. Forms without allowed domains ignore `_redirect` entirely. With allowed domains set to `example.com`: | _redirect | Followed | Why | | --- | --- | --- | | `https://example.com/thanks` | Yes | Allowed domain | | `https://app.example.com/done` | Yes | Subdomain of an allowed domain | | `http://example.com/thanks` | No | Not `https:` | | `https://example.com:8443/thanks` | No | Custom port | | `https://user@example.com/` | No | Contains a username | | `https://example.com.evil.dev/` | No | Not the domain or a subdomain of it | A `_redirect` that isn't followed falls through to the hosted thank-you page; it never causes an error. JSON requests ignore redirects and get `201` with the submission id. ### When a post is rejected If a post is rejected, for example because a required field is missing, the browser sees a hosted error page with the HTTP status of the error. It lists the problems by field label and links back to the form. See [Errors](https://ikiform.com/docs/errors.md) for every code. ## Field names and arrays Repeated names and names ending in `[]` become arrays. Use `[]` for checkbox groups and ` Billing ``` Checking both topics and the newsletter box stores: `Stored fields`: ```json { "topics": ["billing", "api"], "plan": "team", "newsletter": "on" } ``` - Names in brackets such as `address[city]` are kept literally; they are not turned into nested objects. Post JSON if you want nested values. - An unchecked checkbox is not sent at all. With a `checkbox` [field](https://ikiform.com/docs/fields.md#types) defined, a missing box is stored as `false` and a checked one as `true`. - A submission can have up to 100 fields, names up to 128 characters and values up to 16 KB. See [Limits](https://ikiform.com/docs/limits.md). > File uploads are not built yet. An empty file input is ignored; a post that contains a real file is refused with `422 files_not_supported`. ## Reserved fields Names that start with `_` are instructions for Ikiform. They are never stored as answers. | Field | Effect | | --- | --- | | `_gotcha` | Honeypot. Leave it empty and hidden; a filled-in value adds 5 to the spam score, which is enough to store the post as spam. The name can be changed per form. | | `_redirect`, `_next` | Where to send the browser after a successful post. See [Rules for _redirect](#redirect-rules). | | `_subject` | Subject for notification emails. Whitespace is collapsed and it is cut to 200 characters. | | `_replyto` | Reply-to address for notification emails. Ignored unless it is one valid email address. | | `_idempotency` | Idempotency key, the same as the `Idempotency-Key` header. See [Idempotency](https://ikiform.com/docs/frameworks.md#idempotency). | | `_ikiform_ts` | Signed render time, set by the SDK and the shadcn/ui component. A post sent under 2 seconds after render scores as spam. | | `cf-turnstile-response` | The Turnstile token, added by the Turnstile widget or the SDK. | | `_ikiform_via` | Set by Ikiform's own human check page. Don't send it yourself. | Any other field starting with `_` is also dropped, so you can use names like `_source` for your own hidden markers without storing them. `demo.html`: ```html
``` ## The honeypot The default honeypot name is `_gotcha`. You can change it per form under **Honeypot field** in the form's settings; whatever name you pick is treated as reserved and never stored. A plain `hidden` attribute is enough for most bots. If you see spam getting through, hide the input off-screen instead, because some bots skip inputs marked `hidden`: `honeypot.html`: ```html ``` Keep `tabindex="-1"` and `autocomplete="off"` so keyboard users and password managers never fill it in. ## Adding a human check Forms with the spam check set to **Ikiform** need no change in your HTML. When a browser posts without a token, Ikiform answers with a short "Checking you're human" page on ikiform.com. It runs Cloudflare Turnstile, then posts the original fields again with the token. Your visitor ends on the same thank-you page as before. See [Spam protection](https://ikiform.com/docs/spam.md#human-check) for the full flow. ### With your own Turnstile keys If the form uses **Your own Turnstile keys**, render the widget yourself. It adds the `cf-turnstile-response` field to the post. Posts without a valid token are rejected with `403 captcha_failed`. `turnstile.html`: ```html
``` --- Source: https://ikiform.com/docs/frameworks # 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/` and read a JSON answer. The [SDK](https://ikiform.com/docs/sdk.md) wraps this in one function, but it is optional. For framework walkthroughs, see the guides for [Next.js](https://ikiform.com/docs/guides/nextjs.md), [React](https://ikiform.com/docs/guides/react.md), [Vue](https://ikiform.com/docs/guides/vue.md), [Svelte](https://ikiform.com/docs/guides/svelte.md) and [Astro](https://ikiform.com/docs/guides/astro.md). ## With fetch **JSON** (`submit.js`) ```js 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); ``` **FormData** (`form.js`) ```js const form = document.querySelector("form"); form.addEventListener("submit", async (event) => { event.preventDefault(); const response = await fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, body: new FormData(form), }); const result = await response.json(); if (result.ok) form.replaceWith("Thanks, we got it."); }); ``` **SDK** (`submit.ts`) ```ts import { submit } from "@ikiform/sdk"; const result = await submit("frm_xxxxxxxxxxxxxxxx", { name: "Ada Lovelace", email: "ada@example.com", }); if (result.ok) console.log("Stored", result.id); else console.error(result.error.code, result.error.fields); ``` ## Try it The interactive request builder composes a submission from field rows, shows the curl, fetch and SDK code for it, and can send one real POST when you enter your own form id. Nothing is sent automatically. ## Endpoints | Method | Path | What it does | | --- | --- | --- | | `POST` | `/f/` | Create a submission | | `OPTIONS` | `/f/` | CORS preflight, always `204` | | `GET` | `/f//schema` | The form's public field definition, see [shadcn/ui](https://ikiform.com/docs/shadcn.md#schema-endpoint) | | `GET` | `/f//thanks` | The hosted thank-you page | | `GET` | `/f//challenge` | The Ikiform human check frame, used by the SDK | Opening `/f/` 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](https://ikiform.com/docs/how-it-works.md#response-modes) | | `Idempotency-Key` | No | Makes retries safe, up to 255 characters, see [Idempotency](#idempotency) | | `Authorization` | No | `Bearer ik_sk_...` or `Bearer ik_pub_...`, see [From a server](https://ikiform.com/docs/guides/server.md) | | `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](https://ikiform.com/docs/spam.md#allowed-domains). `Preflight`: ```http 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](https://ikiform.com/docs/api.md) 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](https://ikiform.com/docs/errors.md). **201** (`Created`) ```json { "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" } ``` **422** (`Validation failed`) ```json { "ok": false, "error": { "code": "validation_failed", "message": "Some fields are invalid.", "fields": { "email": ["Enter a valid email address"] } }, "request_id": "req_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 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: `retry.js`: ```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); } ``` --- Source: https://ikiform.com/docs/shadcn # shadcn/ui component > Install a form component that renders your Ikiform fields with your own shadcn/ui components. If your project uses shadcn/ui, you can install a ready-made form instead of writing the markup. It is one option among several: the same form keeps working with [plain HTML](https://ikiform.com/docs/html.md) and [fetch](https://ikiform.com/docs/frameworks.md). ## Install `Terminal`: ```bash npx shadcn@latest add https://ikiform.com/r/form.json ``` This copies `components/ikiform/form.tsx` into your project and installs the shadcn/ui components it uses: `button`, `checkbox`, `input`, `label`, `radio-group` and `textarea`. The file is yours to edit. Selects use a native ` {errors.email ?

{errors.email[0]}

: null}

{{ message }}

``` For forms with the **Ikiform** human check, call `challenge(formId)` before `submit` and pass the token as `{ turnstileToken }`, as in the [React guide](https://ikiform.com/docs/guides/react.md#component). ## Nuxt server route To post from the server instead, add a server route that forwards the body with a secret key. The page then posts JSON to your own route. `server/api/contact.post.ts`: ```ts export default defineEventHandler(async (event) => { const body = await readBody(event); return $fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", { method: "POST", headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` }, body, ignoreResponseError: true, }); }); ``` > Posts sent from your server all come from your server's IP address, so Ikiform's per-visitor rate limits count every visitor together. The Standard preset allows 5 posts a minute per IP and form, which a busy site can hit. Post from the browser when you can, or choose the Relaxed or Custom [rate limit](https://ikiform.com/docs/rate-limits.md) for forms you proxy through a server. --- Source: https://ikiform.com/docs/guides/svelte # Svelte and SvelteKit > A Svelte 5 component that posts from the browser, and a SvelteKit form action that posts with a secret key. ## Svelte component Install the SDK with `npm install @ikiform/sdk`, then post the form from the browser: `ContactForm.svelte`: ```svelte {#if status === "sent"}

Thanks, we got it.

{:else}
{#if errors.email}

{errors.email[0]}

{/if} {#if status === "error" && !errors.email}

{message}

{/if}
{/if} ``` ## SvelteKit form action A form action works without JavaScript and is enhanced when it loads. The action posts to Ikiform with a secret key stored as `IKIFORM_SECRET_KEY`. `src/routes/contact/+page.server.js`: ```js import { env } from "$env/dynamic/private"; import { fail } from "@sveltejs/kit"; export const actions = { default: async ({ request }) => { const data = await request.formData(); const body = new URLSearchParams(); for (const [name, value] of data) if (typeof value === "string") body.append(name, value); const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", { method: "POST", headers: { Accept: "application/json", Authorization: `Bearer ${env.IKIFORM_SECRET_KEY}` }, body, }); const result = await response.json(); if (!result.ok) return fail(response.status, { message: result.error.message, fields: result.error.fields ?? {} }); return { sent: true }; }, }; ``` `src/routes/contact/+page.svelte`: ```svelte {#if form?.sent}

Thanks, we got it.

{:else}
{#if form?.fields?.email}

{form.fields.email[0]}

{/if} {#if form?.message && !form?.fields?.email}

{form.message}

{/if}
{/if} ``` > Posts sent from your server all come from your server's IP address, so Ikiform's per-visitor rate limits count every visitor together. The Standard preset allows 5 posts a minute per IP and form, which a busy site can hit. Post from the browser when you can, or choose the Relaxed or Custom [rate limit](https://ikiform.com/docs/rate-limits.md) for forms you proxy through a server. --- Source: https://ikiform.com/docs/guides/astro # Astro > A zero-JavaScript Astro form, and the same form enhanced with a small client script. Astro renders plain HTML by default, which is all Ikiform needs. Start with the static form and add the script only if you want to stay on the page after submit. ## Static form `src/components/Contact.astro`: ```astro --- const endpoint = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx"; ---
``` `_redirect` only works when the form has allowed domains that include your site; otherwise the browser lands on the hosted thank-you page. See [HTML forms](https://ikiform.com/docs/html.md#redirect-rules). ## Enhanced with a script Astro bundles ` ``` --- Source: https://ikiform.com/docs/guides/javascript # Plain JavaScript > Progressively enhance an HTML form with fetch: inline field errors, a status message and no page reload. This page needs no build step and no package. The form works as a normal HTML post when JavaScript is off; with it on, the script posts with `fetch` and shows the result inline. `contact.html`: ```html

``` - `Accept: application/json` makes Ikiform answer with JSON instead of a redirect. - `result.error.fields` is keyed by field id; the script writes the first message under each field. - `fetch` only rejects on network failures. HTTP errors still resolve, with the error envelope in the body. - A `429` response carries `Retry-After`, which browser code can read because Ikiform exposes it through CORS. ## With the Ikiform human check The interstitial check page only appears for normal browser navigations. A `fetch` post to a form with the **Ikiform** check gets `403 challenge_required` instead. Use `challenge` from the [SDK](https://ikiform.com/docs/sdk.md#challenge) through your bundler, or let the form post without JavaScript. --- Source: https://ikiform.com/docs/guides/server # From a server > Create submissions from a backend, script or integration with a secret API key. Backends, cron jobs and integrations can post to the same endpoint as browsers. Add a secret API key so Ikiform knows the post is yours. ## Create a secret key 1. In the dashboard, open **API keys** and create a **secret** key. 2. Give it the `submissions:write` scope. Add `submissions:read` and `forms:read` only if the same key also reads data from the [REST API](https://ikiform.com/docs/api.md). 3. Copy the key. It starts with `ik_sk_` and is shown once; Ikiform stores only its hash. ## Send a submission **Node** (`submit.ts`) ```ts const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`, "Idempotency-Key": order.id, }, body: JSON.stringify({ email: order.email, plan: order.plan }), }); const result = await response.json(); if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message} (${result.request_id})`); ``` **curl** (`Terminal`) ```bash curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \ -H "Authorization: Bearer $IKIFORM_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1234" \ -d '{"email":"ada@example.com","plan":"team"}' ``` **Python** (`submit.py`) ```python import os import requests response = requests.post( "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", json={"email": "ada@example.com", "plan": "team"}, headers={ "Authorization": f"Bearer {os.environ['IKIFORM_SECRET_KEY']}", "Idempotency-Key": "order_1234", }, timeout=10, ) result = response.json() if not result["ok"]: raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}") ``` ## What a secret key changes | Check | Without a key | With a secret key | | --- | --- | --- | | Allowed domains | Checked against `Origin` or `Referer` | Skipped | | Ikiform human check | Required when turned on | Skipped | | Your own Turnstile keys | Token required | Token still required | | Rate limits | Per visitor IP and form | Still apply, per your server's IP | | Field validation and spam scoring | Run | Run | A key from another workspace, a revoked key, or a secret key without `submissions:write` gets `403 forbidden`. A malformed `Authorization` header is also refused rather than ignored, so a typo never silently downgrades to an anonymous post. > Posts sent from your server all come from your server's IP address, so Ikiform's per-visitor rate limits count every visitor together. The Standard preset allows 5 posts a minute per IP and form, which a busy site can hit. Post from the browser when you can, or choose the Relaxed or Custom [rate limit](https://ikiform.com/docs/rate-limits.md) for forms you proxy through a server. ## Publishable keys Publishable keys (`ik_pub_`) are for browsers. They can only submit, and only from the domains you list on the key. A post with a publishable key from any other site gets `403 origin_not_allowed`. They don't skip the human check or the form's own allowed domains. > Never put a secret key in browser code, a mobile app or a public repository. Revoke it in the dashboard if it leaks. ## Retries Servers retry. Send an `Idempotency-Key` that is stable for the thing you are submitting, such as an order id, and a retry will replay the first response instead of creating a duplicate. See [Idempotency](https://ikiform.com/docs/frameworks.md#idempotency). --- Source: https://ikiform.com/docs/guides/site-builders # Site builders > Use Ikiform from no-code site builders that let you paste custom HTML, such as an embed or code block. Ikiform works anywhere you can put a real HTML `
` on the page, because a plain form post needs no script. Most site builders offer an embed, code or custom HTML block. Paste a form into it and point the `action` at your endpoint. > This only works when the builder renders your HTML as-is. A builder's own form widget usually posts to the builder's servers instead; use it only if it lets you set a custom action URL with the POST method. ## The embed `Custom HTML block`: ```html
``` - Builders often place custom code inside an iframe. `target="_top"` makes the thank-you page open in the whole window instead of inside the small frame. If the builder's frame doesn't allow top navigation, the result shows inside the frame. - Set **Redirect after submit** on the form to send people back to a thank-you page on your site. - Styles inside the block may not inherit your site's styles; the `