Form ID

REST API

Read forms and submissions, move submissions in and out of spam, and delete them with a secret key.

The REST API lets your server read forms and submissions, move submissions in and out of spam, and delete them. It is JSON over HTTPS at https://ikiform.com/api/v1. The OpenAPI 3.1 document is at /api/v1/openapi.json.

Creating submissions is not part of /api/v1. Post to the form endpoint with a secret key instead; see From a server.

API keys

Create keys in the dashboard under API keys. A key belongs to one workspace and only sees that workspace. The full key is shown once; Ikiform stores only its hash.

KindPrefixUse
Secretik_sk_For servers. Calls the REST API with the scopes you choose, and can create submissions.
Publishableik_pub_For browsers. Can only submit forms, and only from the domains you list on the key.

Scopes

ScopeGrants
forms:readGET /forms, GET /forms/:formId
submissions:readGET /submissions, GET /submissions/:id
submissions:writeDELETE /submissions/:id, POST /submissions/:id/spam and /not-spam, and posting to /f/:formId

Authentication

Send the secret key as a Bearer token. The API is server to server only: it sends no CORS headers and answers browser preflights with 405, so a secret key can never work from a web page.

Terminal
curl https://ikiform.com/api/v1/forms \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
  • A missing, malformed, unknown, revoked or expired key gets 401 unauthorized with WWW-Authenticate: Bearer.
  • A publishable key gets 403 forbidden (Publishable keys can only submit forms.). A key without the needed scope gets 403 forbidden with the scope named in the message.
  • Responses are sent with Cache-Control: no-store.

The SDK examples below share one client:

ikiform.ts
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });

Request composer

Form
Form ID
curl "https://ikiform.com/api/v1/submissions?form_id=frm_xxxxxxxxxxxxxxxx&status=received&limit=50" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Run this from your server. The REST API refuses browser requests, so this page never sends it.

Endpoints

MethodPathScopeReturns
GET/formsforms:readList forms
GET/forms/:formIdforms:readGet a form
GET/submissionssubmissions:readList submissions
GET/submissions/:idsubmissions:readGet a submission
POST/submissions/:id/spamsubmissions:writeMark as spam
POST/submissions/:id/not-spamsubmissions:writeMark as not spam
DELETE/submissions/:idsubmissions:writeDelete a submission
GET/openapi.jsonNoneThe OpenAPI document
GET/healthNone{ "ok": true }

List forms

GET /forms with the forms:read scope.

  • Newest first. Takes limit (1 to 100, default 25) and cursor.
curl "https://ikiform.com/api/v1/forms?limit=10" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Get a form

GET /forms/:formId with the forms:read scope.

  • A form id from another workspace returns 404, exactly like one that doesn't exist.
curl https://ikiform.com/api/v1/forms/frm_xxxxxxxxxxxxxxxx \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

List submissions

GET /submissions with the submissions:read scope.

ParameterValuesDefault
form_idA form idAll forms in the workspace
statusreceived or spamBoth
limit1 to 10025
cursornext_cursor from the previous pageThe first page
  • Newest first. A form_id from another workspace returns an empty list rather than an error, so ids can't be probed.
curl "https://ikiform.com/api/v1/submissions?form_id=frm_xxxxxxxxxxxxxxxx&status=received&limit=50" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Get a submission

GET /submissions/:id with the submissions:read scope.

  • Ids from another workspace return 404, exactly like ids that don't exist.
curl https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3 \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Mark as spam

POST /submissions/:id/spam with the submissions:write scope.

  • Moves a received submission to spam, lowers the form's submission_count and cancels its pending webhook deliveries.
  • Calling it on a submission that is already spam returns it unchanged.
curl -X POST https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/spam \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Mark as not spam

POST /submissions/:id/not-spam with the submissions:write scope.

  • Moves it back to received, clears its spam score and reasons, and sends submission.created to every matching webhook endpoint.
  • Calling it on a received submission returns it unchanged.
curl -X POST https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/not-spam \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Delete a submission

DELETE /submissions/:id with the submissions:write scope.

  • Permanently removes the submission and its webhook deliveries. There is no undo. Deleting again returns 404.
curl -X DELETE https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3 \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"

Objects

Objects are snake_case JSON with an object field. Timestamps are ISO 8601 in UTC with milliseconds. Submission ids are ordered by time (sub_ plus 26 characters); form ids are random (frm_ plus 16 characters).

Form fieldDescription
statusactive, paused or disabled
submission_countReceived submissions, not spam
Submission fieldDescription
statusreceived or spam
fieldsThe stored values
metauser_agent, origin, referrer, country, subject, reply_to. Every key is present and any value can be null. The IP hash is never returned
spam.score, spam.reasonsThe spam score and reasons, such as honeypot or origin_mismatch

Pagination

Lists are newest first and use opaque cursors.

  1. Request without a cursor.
  2. If has_more is true, send next_cursor as cursor with the same filters.
  3. Stop when next_cursor is null.

Pages are stable while new submissions arrive: new ones appear on the first page, never as duplicates further down. Don't build or parse cursors yourself; an invalid one returns 400 invalid_request.

for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx" })) {
  console.log(submission.id, submission.fields);
}

Errors

Errors use the same envelope as form submissions. Every response has an X-Request-Id header, which errors repeat as request_id.

403 Forbidden
{
  "ok": false,
  "error": { "code": "forbidden", "message": "This API key is missing the forms:read scope." },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
StatusCodeWhen
400invalid_requestlimit, status, form_id or cursor is invalid; the message names it
401unauthorizedMissing or invalid key
403forbiddenPublishable key, or a missing scope
404not_foundUnknown route or id, or an id in another workspace
405method_not_allowedA browser preflight
429rate_limitedOver the per-key rate limit
500server_errorSomething failed on our side; quote request_id

Rate limit

Each key can make 100 requests per 60 seconds. Over the limit, the API returns 429 rate_limited with a Retry-After header in seconds. The SDK exposes it as error.retryAfter.