Management
API.
Read messages, provision forms and connect workflows from your server. Every API token belongs to one workspace, one environment and a set of permissions you choose.
1. Authenticate
For exact request and response shapes, view the OpenAPI specification or follow the copyable payload examples. The specification lists the required scope for every management operation.
- Open API tokens in your workspace. Give the integration a recognizable name, choose permissions and set an expiry.
- Create the token and save the one-time value in your server’s secret store.
- Copy the API origin shown in the workspace. Staging currently uses
https://api-staging.formkoi.com. - Set
FORMKOI_API_URLandFORMKOI_API_TOKENon your server, then inspect the token:
curl "$FORMKOI_API_URL/v1/token" \
-H "Authorization: Bearer $FORMKOI_API_TOKEN"{
"data": {
"id": "TOKEN_ID",
"name": "Reporting dashboard",
"scopes": ["forms:read", "submissions:read"],
"expiresAt": 1799000000000,
"workspaceId": "WORKSPACE_ID",
"environment": "staging"
}
}IDs and timestamps above are illustrative. Timestamps are epoch milliseconds. Send the token only in the Authorization: Bearer header. Server requests must go directly to the API origin, without browser cookies or an Origin header. The Vercel application uses cookie sessions and rejects bearer credentials.
Do not embed them in frontend code, URLs, public repositories or form HTML. A form’s public submission key is different: it receives messages through POST /f/:publicKey and does not grant management access.
2. Permissions
Read and write permissions are independent. A write request can return the object it changed, but write permission does not grant unrelated list or read endpoints. Tokens cannot create more tokens, revoke tokens, access account sessions or call the guided owner receiver-test endpoint.
| Permission | What it allows |
|---|---|
forms:read | List forms and read their settings. |
forms:write | Create, edit or delete forms; rotate public keys; configure or disconnect Turnstile. Form deletion also deletes its messages. |
submissions:read | List and read messages, export CSV, and read webhook delivery history and payloads. |
submissions:write | Move or delete messages, retry owner email notifications and failed webhooks. |
files:read | Download private, unexpired attachments. Message-read access alone cannot download them. |
recipients:read | List workspace recipients and their verification status. |
recipients:write | Add recipients or resend verification. The recipient must still verify their address. |
integrations:read | Read webhook destinations, routing and visitor-reply settings; preview routing and reply drafts. |
integrations:write | Configure workflows, rotate webhook secrets and send synthetic webhook tests. |
usage:read | Read monthly submission counts, folder totals and storage usage. |
For a reporting dashboard, start with forms:read and submissions:read. For deployment automation, use forms:read, forms:write and recipients:read. Add other permissions only when the integration needs them.
Workflow settings and private submission payloads have separate permissions. Reading a webhook destination does not allow reading its delivery payload. Private file bytes also require their own permission.
3. Read messages and create forms
curl "$FORMKOI_API_URL/v1/submissions?folder=inbox&unread=true&limit=25" \
-H "Authorization: Bearer $FORMKOI_API_TOKEN"List responses contain a short preview. Read /v1/submissions/:id for full fields, attachment metadata, notification routing, the visitor-reply decision and delivery attempts. Search is a case-insensitive substring over submitted field values. Folder values are inbox, archive, spam or all; the default is inbox.
curl "$FORMKOI_API_URL/v1/forms" \
-H "Authorization: Bearer $FORMKOI_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"name": "Website contact",
"recipientIds": ["VERIFIED_RECIPIENT_ID"],
"settings": {
"subject": "A new website inquiry",
"allowedOrigins": ["https://www.example.com"]
}
}'Use an actual verified recipient ID from GET /v1/recipients. Form creation returns HTTP 201 and the form under data, including its public key. Accepted recipients are always checked against the token’s workspace.
curl "$FORMKOI_API_URL/v1/submissions/$SUBMISSION_ID" \
-X PATCH \
-H "Authorization: Bearer $FORMKOI_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"folder":"archive","read":true}'Form updates accept name, status, recipientIds and settings. Use an empty recipientIds: [] array to disable default notification emails; submissions still appear in your inbox. If you provide settings, it merges only the supplied members, including nested upload settings. Omitted members keep their saved values. A successful delete returns 204 with no JSON body.
Attachments are streamed with forced-download headers. Respect expiry and store downloads privately. Files are not malware-scanned.
4. Pagination
Forms, messages and webhook delivery lists return {"data": [...], "nextCursor": "..."}. Pass the cursor unchanged in the next request while retaining the same filters. Stop when it is null. Newer records appear first; the cursor combines a timestamp and unique ID.
const base = process.env.FORMKOI_API_URL;
const token = process.env.FORMKOI_API_TOKEN;
let cursor = null;
do {
const url = new URL("/v1/submissions", base);
url.searchParams.set("folder", "inbox");
url.searchParams.set("limit", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
const failure = await response.json();
throw new Error(failure.error.message);
}
const page = await response.json();
for (const message of page.data) {
await storeMessageById(message.id, message);
}
cursor = page.nextCursor;
} while (cursor);storeMessageById represents your own persistence function. Key records by ID. Cursor pagination avoids offset shifts from newly inserted records, but it is not a frozen snapshot: deletion, edits and folder changes during a traversal can affect results. Recipient lists contain all current addresses because a workspace is limited to 20.
5. Endpoint reference
All paths below are relative to your API origin. Use JSON for request bodies and include Content-Type: application/json. Management bodies are limited to 32 KiB.
| Method / path | Permission | Behavior |
|---|---|---|
GET/v1/token | Any valid token | Inspect this token’s permissions, environment, workspace and expiry. |
GET/v1/forms | forms:read | List forms. limit 1–100 (default 25); cursor. |
POST/v1/forms | forms:write | Create a form: name, recipientIds, optional settings. |
GET/v1/forms/:id | forms:read | Read one form, recipient IDs and public Turnstile configuration. |
PATCH / DELETE/v1/forms/:id | forms:write | Update or permanently delete a form. |
POST/v1/forms/:id/rotate-key | forms:write | Replace its public submission key; update your website afterward. |
POST / DELETE/v1/forms/:id/turnstile | forms:write | Save or disconnect a customer-owned widget. |
GET/v1/submissions | submissions:read | List messages. limit 1–50 (default 25), cursor, folder, formId, search, unread. |
GET/v1/submissions/export | submissions:read | Download CSV for matching filters, up to 1,000 messages. |
GET / PATCH / DELETE/v1/submissions/:id | submissions:read / submissions:write | Read with GET; update folder/read state or delete with write permission. |
GET/v1/submissions/:id/attachments/:attachmentId | files:read | Stream a private file with forced-download headers. |
POST/v1/submissions/:id/deliveries/:deliveryId/retry | submissions:write | Retry eligible owner notifications. Acknowledge possible duplicates for uncertain sends. |
GET / POST/v1/recipients | recipients:read / recipients:write | List all recipients (maximum 20), or add an email address. |
GET/v1/usage | usage:read | Get month, accepted/spam counts, allowance, folders and storage. |
GET / POST/v1/forms/:id/routing | integrations:read / integrations:write | Read or save ordered routing rules; saves send the read shape plus its revision. |
POST/v1/forms/:id/routing/preview | integrations:read | Preview draft config and example fields without sending. |
GET / POST/v1/forms/:id/autoresponder | integrations:read / integrations:write | Read or save a visitor reply with a revision. |
POST/v1/forms/:id/autoresponder/preview | integrations:read | Render a draft reply without sending. |
GET / POST / DELETE/v1/forms/:id/webhook | integrations:read / integrations:write | Read, configure or remove the webhook endpoint. |
POST/v1/forms/:id/webhook/rotate-secret | integrations:write | Rotate the signing secret using the current revision. |
POST/v1/forms/:id/webhook/test | integrations:write | Queue a synthetic test event. No request body is needed. |
GET/v1/forms/:id/webhook/deliveries | submissions:read | List deliveries. limit 1–50 (default 20); cursor. |
GET/v1/forms/:id/webhook/deliveries/:deliveryId | submissions:read | Read the saved payload and attempt history. |
POST/v1/forms/:id/webhook/deliveries/:deliveryId/retry | submissions:write | Retry an eligible failed delivery. No request body is needed. |
The routing guide, visitor-reply guide and webhook guide explain their configuration and delivery behavior. When saving routing or visitor replies, send the configuration fields at the top level with revision: null for first creation, then use the revision returned by the previous save. Webhook settings use a top-level revision, omitted for initial creation.
Turnstile setup accepts siteKey, a matching secretKey for new widgets, hostnames and enabled. Remote environments require a real production widget. Secrets are never included in form read responses.
6. Rate limits and errors
{
"error": {
"code": "insufficient_scope",
"message": "This request requires the forms:write permission.",
"requestId": "REQUEST_ID"
}
}Keep the request ID for support. Errors rejected before the application router may not have one. Responses use Cache-Control: no-store; do not cache private API responses in a public CDN.
| Status | Next step |
|---|---|
| 401 | Check token expiry, revocation and environment. Create a replacement if needed. |
| 403 | Check the required permission and API origin. Remove browser cookies and Origin headers from server calls. Account-only endpoints cannot use tokens. |
| 404 | Check the resource ID and workspace. Resources in another workspace are not exposed. |
| 409 | Reload current configuration or delivery state before retrying a conflicting change. |
| 413 / 415 / 422 | Correct body size, content type or validation errors. |
| 429 | Honor Retry-After when present. Slow down requests; inspect any specific quota or cooldown code. |
| 500 / 503 | Use bounded backoff for reads. Check whether a mutation took effect before repeating it. |
Rate limiting applies per connecting IP, per token and across a workspace’s tokens, each configured for 60 requests per minute. These limits are approximate, apply per Cloudflare location, and are not a billing quota. Resource-specific cooldowns and quotas also apply.
Management writes do not have a general idempotency-key contract. Do not automatically repeat form creation or secret rotation after a lost response. Public form submissions have their own idempotency support. Owner notification retry accepts {"acknowledgePossibleDuplicate": true} when the previous send is uncertain; visitor replies cannot be manually resent.
7. Concurrent updates
Tokens expire after 1–365 days; the workspace defaults to 90. You can have 20 active tokens and create up to 50 in 24 hours. Expired and revoked tokens stay visible in history, with creation, expiry and recent-use information. Last-used timestamps update at most once every five minutes.
To rotate a token or change permissions, create a replacement in the workspace, update your integration’s secret, verify a request, then revoke the old token. Revocation prevents new authorization checks from succeeding; requests already authorized can finish. Tokens cannot change their own permissions or manage other tokens.
A token’s full value is shown only when created. If you lose it, revoke it and create another. Form Koi stores its hash, a short identifying prefix and configuration, not a retrievable copy of the secret.
Ready to connect your first tool?
Create a scoped API token ↗