Skip to guide
form koi.Manage API tokens ↗

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.

  1. Open API tokens in your workspace. Give the integration a recognizable name, choose permissions and set an expiry.
  2. Create the token and save the one-time value in your server’s secret store.
  3. Copy the API origin shown in the workspace. Staging currently uses https://api-staging.formkoi.com.
  4. Set FORMKOI_API_URL and FORMKOI_API_TOKEN on 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.

Keep management tokens on your server.

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.

PermissionWhat it allows
forms:readList forms and read their settings.
forms:writeCreate, edit or delete forms; rotate public keys; configure or disconnect Turnstile. Form deletion also deletes its messages.
submissions:readList and read messages, export CSV, and read webhook delivery history and payloads.
submissions:writeMove or delete messages, retry owner email notifications and failed webhooks.
files:readDownload private, unexpired attachments. Message-read access alone cannot download them.
recipients:readList workspace recipients and their verification status.
recipients:writeAdd recipients or resend verification. The recipient must still verify their address.
integrations:readRead webhook destinations, routing and visitor-reply settings; preview routing and reply drafts.
integrations:writeConfigure workflows, rotate webhook secrets and send synthetic webhook tests.
usage:readRead 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 / pathPermissionBehavior
GET
/v1/token
Any valid tokenInspect this token’s permissions, environment, workspace and expiry.
GET
/v1/forms
forms:readList forms. limit 1–100 (default 25); cursor.
POST
/v1/forms
forms:writeCreate a form: name, recipientIds, optional settings.
GET
/v1/forms/:id
forms:readRead one form, recipient IDs and public Turnstile configuration.
PATCH / DELETE
/v1/forms/:id
forms:writeUpdate or permanently delete a form.
POST
/v1/forms/:id/rotate-key
forms:writeReplace its public submission key; update your website afterward.
POST / DELETE
/v1/forms/:id/turnstile
forms:writeSave or disconnect a customer-owned widget.
GET
/v1/submissions
submissions:readList messages. limit 1–50 (default 25), cursor, folder, formId, search, unread.
GET
/v1/submissions/export
submissions:readDownload CSV for matching filters, up to 1,000 messages.
GET / PATCH / DELETE
/v1/submissions/:id
submissions:read / submissions:writeRead with GET; update folder/read state or delete with write permission.
GET
/v1/submissions/:id/attachments/:attachmentId
files:readStream a private file with forced-download headers.
POST
/v1/submissions/:id/deliveries/:deliveryId/retry
submissions:writeRetry eligible owner notifications. Acknowledge possible duplicates for uncertain sends.
GET / POST
/v1/recipients
recipients:read / recipients:writeList all recipients (maximum 20), or add an email address.
GET
/v1/usage
usage:readGet month, accepted/spam counts, allowance, folders and storage.
GET / POST
/v1/forms/:id/routing
integrations:read / integrations:writeRead or save ordered routing rules; saves send the read shape plus its revision.
POST
/v1/forms/:id/routing/preview
integrations:readPreview draft config and example fields without sending.
GET / POST
/v1/forms/:id/autoresponder
integrations:read / integrations:writeRead or save a visitor reply with a revision.
POST
/v1/forms/:id/autoresponder/preview
integrations:readRender a draft reply without sending.
GET / POST / DELETE
/v1/forms/:id/webhook
integrations:read / integrations:writeRead, configure or remove the webhook endpoint.
POST
/v1/forms/:id/webhook/rotate-secret
integrations:writeRotate the signing secret using the current revision.
POST
/v1/forms/:id/webhook/test
integrations:writeQueue a synthetic test event. No request body is needed.
GET
/v1/forms/:id/webhook/deliveries
submissions:readList deliveries. limit 1–50 (default 20); cursor.
GET
/v1/forms/:id/webhook/deliveries/:deliveryId
submissions:readRead the saved payload and attempt history.
POST
/v1/forms/:id/webhook/deliveries/:deliveryId/retry
submissions:writeRetry 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.

StatusNext step
401Check token expiry, revocation and environment. Create a replacement if needed.
403Check the required permission and API origin. Remove browser cookies and Origin headers from server calls. Account-only endpoints cannot use tokens.
404Check the resource ID and workspace. Resources in another workspace are not exposed.
409Reload current configuration or delivery state before retrying a conflicting change.
413 / 415 / 422Correct body size, content type or validation errors.
429Honor Retry-After when present. Slow down requests; inspect any specific quota or cooldown code.
500 / 503Use 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 ↗