Skip to guide
form koi.Start free ↗

Submission
API.

Send to POST /f/{publicKey} on your form’s API origin. The public key accepts messages; reading them requires a signed-in workspace or a scoped management token.

1. Send a submission

The endpoint accepts application/json, application/x-www-form-urlencoded and multipart/form-data. No account credential is needed. Copy the exact URL from the connection guide.

JSON request — illustrative staging endpoint
curl 'https://api-staging.formkoi.com/f/YOUR_PUBLIC_FORM_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'Idempotency-Key: YOUR_NEW_UNIQUE_REQUEST_ID' \
  --data '{
    "name": "Alex",
    "email": "alex@example.com",
    "message": "Could we talk about a project?",
    "interests": ["Design", "Development"]
  }'

Replace the endpoint and request ID. This example assumes the form has no origin restriction and Turnstile is not enabled. A protected website must submit a fresh widget response; see the protection contract. Origin restrictions also apply to server calls.

For browser requests, use fetch(endpoint, { method: "POST", body: new FormData(form) }). Let the browser set the multipart boundary; do not set the multipart Content-Type yourself. Send neither cookies nor management credentials. Supported CORS headers are Content-Type and Idempotency-Key.

For native HTML and React examples, start with the website connection guide.

2. Field names and limits

JSON values may be strings, finite numbers, booleans or arrays of those values. Values become strings in the inbox. Repeated HTML field names become arrays. Nested objects and null values are not accepted.

PartLimit or behavior
Text body64 KiB; at most 50 named text fields and 100 values.
Values10,000 characters per text value; at most 20 scalar values per array.
Field names1–64 characters, starting with a letter or underscore. Letters, numbers, underscores, dots, brackets and hyphens are accepted. Reserved object-property names are rejected.
emailOptional, but must be valid when supplied. Used as the owner notification’s Reply-To.
nameOptional display name in the inbox. At least one non-empty business field or accepted file is required.
FilesOpt-in multipart uploads: three files, 5 MiB each, 10 MiB total. Text is still independently limited. See supported formats.

The configured honeypot, _idempotency_key, access_key and cf-turnstile-response are control fields, removed from stored message content. The endpoint URL selects the form; access_key does not replace that URL. Fields named subject, redirect, to, cc or bcc are ordinary data and cannot change notification settings.

3. Receipt versus delivery

201 — new message stored
{
  "success": true,
  "submissionId": "MESSAGE_ID",
  "duplicate": false,
  "message": "Your message has been received."
}

A JSON acknowledgement confirms durable receipt. Notifications and webhooks run asynchronously; inspect their histories in the workspace. Spam messages receive the same public acknowledgement and are held for review without owner notification.

A request advertising Accept: text/html gets a 303 redirect to the form’s configured HTTPS thank-you URL. With no redirect configured, it gets a built-in confirmation page with status 200. AJAX clients receive JSON unless they explicitly request HTML. Use a success message such as “Your message has been received,” rather than claiming email delivery is complete.

4. Idempotent retries

Send an Idempotency-Key header containing 8–128 letters, numbers, dots, underscores, colons or hyphens. A native form can instead submit a hidden _idempotency_key. Generate a unique value per intended submission, not one fixed value in the page template.

Keep the key when retrying unchanged data after an uncertain connection. An existing form/key/payload returns 200 with the original ID and duplicate: true, without a second charge or notification. Editing fields or files requires a new key. Reusing a key with different data returns 409 idempotency_conflict.

The fingerprint includes file names, field names, sizes, content hashes and file order. Turnstile responses do not affect it. A stored duplicate does not consume another challenge; if verification succeeded but storage did not, retry with a fresh challenge and the same unchanged-data key.

Idempotency follows the stored message.

Permanent deletion removes its record. A later retry can become a new message. Requests without keys are separate submissions, and these guarantees do not apply to general management writes.

5. Errors and responses

JSON error shape
{
  "error": {
    "code": "invalid_email",
    "message": "Enter a valid email address.",
    "requestId": "REQUEST_ID"
  }
}

Use the returned message to help the visitor and retain the request ID for support. Infrastructure failures can return a different body, so handle non-JSON responses too. HTML requests get a readable error page.

StatusMeaningNext step
400Malformed JSON or form dataCheck that JSON is an object and the body matches Content-Type.
403Origin not allowedUse a configured exact website origin. A restrictive allowlist also rejects a missing Origin.
404Form unavailableCheck the full endpoint and environment. The form may have been deleted or its key rotated.
409Paused form or idempotency conflictRead error.code. Resume the form, or use a new key only if this is a new or edited message.
413 / 415Body too large / unsupported formatReduce the payload or use one of the supported content types.
422Invalid fields, files or verificationShow the returned message, preserve input and correct the issue. Refresh an expired Turnstile check.
429Rate, submission or storage limitHonor Retry-After when present. Monthly and storage limits need owner action, such as upgrading or turning on extra usage; repeated retries will not resolve them.
503Verification temporarily unavailableKeep input and try later with a fresh check. The server cannot accept a new protected message without verification.

Do not automatically retry every failure. Back off after transient limits, preserve an unchanged request key and show a useful retry action. Quota, configuration and validation errors need the relevant problem corrected first.

6. Message retention

Form settings let you keep new messages until you delete them, or set automatic expiry from 1–365 days after receipt. The default is no automatic expiry. The management API uses messageRetentionDays: an integer for automatic expiry, or null to turn it off. Each message records its own expiresAt timestamp.

Changes apply to new submissions only. Existing messages keep their original lifetime, even if you turn automatic expiry off. Moving a message to archive, reviewing spam or retrying the same submission does not extend its expiry. Download an export before expiry if you need your own copy.

At expiry the message is unavailable through the inbox, API, exports and attachment downloads. Pending notification and webhook work stops when a worker checks the expired message. A send already in progress may still complete. Scheduled cleanup permanently removes the stored message and its notification and webhook history, then removes its private files. Cleanup delays can extend physical storage; this is not an instantaneous erasure guarantee.

Usage counts, unsubscribe preferences and email suppressions remain independent of message history. Copies already emailed, exported or received by your webhook destination follow those systems’ retention. Provider event receipts and database recovery copies also have separate retention. Deletion cannot recall those copies. Once a message is physically removed, its idempotency record is removed too.