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.
/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.
| Kind | Prefix | Use |
|---|---|---|
| Secret | ik_sk_ | For servers. Calls the REST API with the scopes you choose, and can create submissions. |
| Publishable | ik_pub_ | For browsers. Can only submit forms, and only from the domains you list on the key. |
Scopes
| Scope | Grants |
|---|---|
forms:read | GET /forms, GET /forms/:formId |
submissions:read | GET /submissions, GET /submissions/:id |
submissions:write | DELETE /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.
curl https://ikiform.com/api/v1/forms \
-H "Authorization: Bearer $IKIFORM_SECRET_KEY"- A missing, malformed, unknown, revoked or expired key gets
401 unauthorizedwithWWW-Authenticate: Bearer. - A publishable key gets
403 forbidden(Publishable keys can only submit forms.). A key without the needed scope gets403 forbiddenwith the scope named in the message. - Responses are sent with
Cache-Control: no-store.
The SDK examples below share one client:
import { createClient } from "@ikiform/sdk";
const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });Request composer
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
| Method | Path | Scope | Returns |
|---|---|---|---|
GET | /forms | forms:read | List forms |
GET | /forms/:formId | forms:read | Get a form |
GET | /submissions | submissions:read | List submissions |
GET | /submissions/:id | submissions:read | Get a submission |
POST | /submissions/:id/spam | submissions:write | Mark as spam |
POST | /submissions/:id/not-spam | submissions:write | Mark as not spam |
DELETE | /submissions/:id | submissions:write | Delete a submission |
GET | /openapi.json | None | The OpenAPI document |
GET | /health | None | { "ok": true } |
List forms
GET /forms with the forms:read scope.
- Newest first. Takes
limit(1 to 100, default 25) andcursor.
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.
| Parameter | Values | Default |
|---|---|---|
form_id | A form id | All forms in the workspace |
status | received or spam | Both |
limit | 1 to 100 | 25 |
cursor | next_cursor from the previous page | The first page |
- Newest first. A
form_idfrom 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_countand 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.createdto 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 field | Description |
|---|---|
status | active, paused or disabled |
submission_count | Received submissions, not spam |
| Submission field | Description |
|---|---|
status | received or spam |
fields | The stored values |
meta | user_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.reasons | The spam score and reasons, such as honeypot or origin_mismatch |
Pagination
Lists are newest first and use opaque cursors.
- Request without a cursor.
- If
has_moreis true, sendnext_cursorascursorwith the same filters. - Stop when
next_cursorisnull.
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.
{
"ok": false,
"error": { "code": "forbidden", "message": "This API key is missing the forms:read scope." },
"request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | limit, status, form_id or cursor is invalid; the message names it |
| 401 | unauthorized | Missing or invalid key |
| 403 | forbidden | Publishable key, or a missing scope |
| 404 | not_found | Unknown route or id, or an id in another workspace |
| 405 | method_not_allowed | A browser preflight |
| 429 | rate_limited | Over the per-key rate limit |
| 500 | server_error | Something 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.