Skip to content

Validation Reference ​

This page is the canonical reference for every validation code the public API can return. Each per-field error carries a help URL that deep-links straight to the relevant anchor on this page (for example https://docs.kinoforms.com/developer/api/validation#format).

This page is the canonical source of truth. Every top-level error code the public API returns has a matching anchor here, so the _guide pointer embedded in every error response (e.g. https://docs.kinoforms.com/developer/api/validation#{code}) always resolves to a live section. When you add a new error code in papi/error.rs or papi/turnstile.rs, add its anchor here first.

For the error-object shape and the validate-vs-submit severity model, see Parsing validation responses.

How Codes Are Used ​

A per-field problem looks like:

json
{
  "field": "email",
  "code": "format",
  "message": "Value is not a valid email address.",
  "help": "https://docs.kinoforms.com/developer/api/validation#format"
}

Branch on code — it is stable. The codes below are the ones that can appear on a submit (and, with the severity rules noted, on validate).

Submit Validation Codes ​

required ​

When it fires: a field marked required has no value. On submit this is an error (422 validation_failed). On validate the missing key is reported in missingRequired instead. In partial mode (?partial=true) it is reported in missingRequired and does not error.

How to fix: supply a value for the field. Use missingRequired from a validate call as your checklist.

type_mismatch ​

When it fires: the value's JSON type does not match the field type (e.g. a string where a number is expected).

How to fix: send the value in the correct type — 42 not "42", true not "true".

not_an_object ​

When it fires: a field that expects an object (for example a structured/group field) received a non-object value.

How to fix: send a JSON object { ... } for that field, not a scalar or array.

unknown_field ​

When it fires: the payload contains a key that is not a field on this form. On submit this is an error (422). On validate it is a warning and the field is ignored.

How to fix: remove the extra key, or map it to a real field key. Resolve all unknown_field warnings on validate before you submit.

too_short ​

When it fires: a text value is shorter than the field's minimum length.

How to fix: provide a longer value that meets the minimum length.

too_long ​

When it fires: a text value exceeds the field's maximum length.

How to fix: shorten the value to within the maximum length.

too_small ​

When it fires: a numeric value is below the field's minimum.

How to fix: send a number at or above the minimum.

too_large ​

When it fires: a numeric value is above the field's maximum.

How to fix: send a number at or below the maximum.

pattern ​

When it fires: a text value does not match the field's required regular-expression pattern.

How to fix: format the value to match the field's pattern (see the field's help text or label for the expected shape).

format ​

When it fires: a value fails a named format check (email, URL, etc.).

How to fix: send a correctly formatted value for the field's format, e.g. a valid [email protected] for an email field.

not_in_options ​

When it fires: a select/radio/choice value is not one of the field's allowed options.

How to fix: send one of the field's defined option values exactly.

too_few_items ​

When it fires: a multi-value field (e.g. checkboxes, multi-select) has fewer selections than the minimum.

How to fix: select at least the minimum number of items.

too_many_items ​

When it fires: a multi-value field has more selections than the maximum.

How to fix: reduce the selections to within the maximum.

file_too_large ​

When it fires: an uploaded file exceeds the field's maximum file size.

How to fix: upload a smaller file within the size limit.

file_type ​

When it fires: an uploaded file's type is not in the field's accepted types.

How to fix: upload a file of an accepted type (see the field's accepted formats).

date_before ​

When it fires: a date value is earlier than the field's allowed minimum date.

How to fix: send a date on or after the minimum allowed date.

date_after ​

When it fires: a date value is later than the field's allowed maximum date.

How to fix: send a date on or before the maximum allowed date.

The Partial Contract ​

POST /papi/v1/submitter/form/submissions?partial=true with the form credential in x-form-access-key saves an incomplete response:

  • The row is saved with status=partial.
  • Missing required fields do not error. They are reported in data.missingRequired.
  • Fields you did send are still validated — a malformed value (e.g. format, type_mismatch) is still an error.
  • Finalize by submitting again without ?partial=true once missingRequired is empty.

See the Submitter guide for examples.

Top-Level Error Codes ​

These appear as the top-level code on an error envelope (not as per-field codes).

Request / Limits ​

bad_request ​

HTTP 400. The request is malformed or missing required parameters. Check the error message for specifics; fix the request body, URL, or headers accordingly.

forbidden ​

HTTP 403. The caller is recognized but not permitted to perform this action. See the specific sub-codes below for remediation — forbidden itself indicates an unexpected denial; report it if you see it in production.

not_found ​

HTTP 404. The requested resource (form, agent, grant, key, challenge) does not exist or is not visible in the caller's tenant. Check the error message for the resource type.

conflict ​

HTTP 409. The request conflicts with the current state of the resource — for example, a version mismatch during a diff-patch update. Re-fetch the resource and retry.

rate_limited ​

HTTP 429. Too many requests. Submit is limited to 1 per 5 seconds per agent; validate is throttled generously so you can self-correct. Back off and retry.

internal_error ​

HTTP 500. Something went wrong on our end. Retry with exponential backoff. If it persists, report the error message and the x-request-id header to support.

invalid_body ​

HTTP 400. The request body is not valid JSON (syntax error, truncated payload, or empty where a body is required). Send a well-formed JSON body.

invalid_content_type ​

HTTP 400. The request is missing a JSON content type. Send content-type: application/json on requests that carry a body.

method_not_allowed ​

HTTP 405. The path exists but does not support this HTTP method. Check the API reference for the allowed methods on the route.

idempotency_key_too_long ​

HTTP 400. The Idempotency-Key header exceeds the 255-character limit. Use a shorter opaque key (a UUID is ideal).

error ​

Generic fallback code for an unexpected, redacted failure. Treat it like internal_error: retry with backoff and report the x-request-id if it persists.

Authentication & Session ​

unauthorized ​

Missing, invalid, or expired credentials — no usable API key (x-api-key) or session token (x-respondent-token). Re-authenticate. For agents, mint a fresh token with POST /papi/v1/auth/token.

anonymous_not_allowed ​

HTTP 403. This form does not accept anonymous submissions (it has respondentAuthMethod set to something other than "none"). Authenticate as a known respondent or agent before submitting.

Authorization & Grants ​

tenant_mismatch ​

The form belongs to a different tenant than the agent's binding. You cannot act on a form outside your tenant.

workspace_mismatch ​

The form belongs to a different workspace than the agent's binding.

no_grant ​

No grant exists for this agent on this form. An operator must grant the agent scopes on the form first (see the Agent quickstart).

missing_scope ​

The agent has a grant but it lacks the scope this call needs (for example calling submit without agent:submit, or a builder call without forms:write). The operator must extend the grant.

binding_required ​

The call requires the agent to be bound to a specific form/respondent context and that binding is missing.

respondent_not_assigned ​

The form (or grant) is bound to a specific respondent and the agent is not the assigned respondent. The operator must assign the correct respondentId on the grant.

submit_not_grantable_by_api_key ​

An API-key caller tried to grant agent:submit. The agent:submit scope can only be granted by an operator (UI / session), never by an API key. Have a human operator issue the grant.

insufficient_workspace_role ​

HTTP 403. You need admin or editor role on the form's workspace to grant agent access. Contact a workspace admin to grant the required scopes.

respondent_binding_requires_session ​

HTTP 403. Binding an agent to a specific respondent slot requires an operator session. An API key alone cannot set respondentId on a grant. Use a human operator session instead.

Submitter Policy ​

These codes are returned to submitter-owned agents (Track C signed path) when the form's agentPolicy rejects them. They never appear for tenant-owned agents (which go through the grant path above).

submitter_agents_not_accepted ​

HTTP 403. The form's agentPolicy.acceptSubmitterAgents is false. This form does not accept submissions from submitter-owned agents at all.

agent_denied ​

HTTP 403. The form's ACL explicitly denies this agent or its owning submitter. An operator must add an allow entry in form_agent_acl.

insufficient_trust ​

HTTP 403. The agent's trust score is below the form's agentPolicy.minTrust threshold (default 0.5). Improve trust by signing more requests with consistent keys, having long-lived agent accounts, and passing challenges.

challenge_required ​

HTTP 403. The form's agentPolicy.requireChallenge is true and the agent has no valid challenge pass. The error payload includes a _challenge pointer with the challenge endpoint and required header name. Issue a challenge with POST /papi/v1/submitter/form/challenges, sending the same x-form-access-key, and respond correctly before retrying.

respondent_binding_required ​

HTTP 403. This form requires the submitter agent to be bound to an assigned respondent (the form has respondentAuthMethod set to something other than "none"), but:

  • The agent has no owning submitter account (submitter-owned agents are created without a linked submitter), or
  • The submitter has no email-linked respondent assigned to this form.

Link a submitter account to the agent or ask the operator to assign the submitter's linked respondent to the form.

Agent & Identity Management ​

agent_inactive ​

The agent identity is disabled or revoked. An operator must re-enable or re-register it.

agent_not_found ​

HTTP 404. The requested agent does not exist or is not owned by the caller's tenant. Check the agent ID and try again.

agent_revoked ​

HTTP 409. The agent has been revoked (via DELETE), which is terminal — it cannot be patched back to active or disabled. Register a new agent instead.

agent_not_managed_by_key ​

HTTP 403. An API-key caller tried to manage (rotate, patch, delete, or register keys on) an agent it did not itself provision. API keys may only manage the agents they created; use an operator session for agents created elsewhere.

invalid_name ​

HTTP 400. The agent name is empty or whitespace-only. Provide a non-empty name.

invalid_status ​

HTTP 400. The agent status must be "active" or "disabled". Use DELETE to revoke an agent permanently.

invalid_scopes ​

HTTP 400. The scopes array is empty. Provide at least one scope from: agent:read, agent:validate, agent:submit.

invalid_scope ​

HTTP 400. One of the scopes in the scopes array is not a recognized agent scope. Valid scopes are: agent:read, agent:validate, agent:submit.

invalid_public_key ​

HTTP 400. The publicKeyPem value is not a valid SPKI PEM (missing BEGIN PUBLIC KEY / END PUBLIC KEY delimiters). Provide a valid Ed25519 public key in SPKI PEM format.

key_conflict ​

HTTP 400. The provided public key is already registered to another agent. Each key pair can belong to only one agent. Generate a fresh key pair.

key_not_found ​

HTTP 404. No active key with the given keyId exists for this agent. Check the key ID or register a new key with PUT /papi/v1/identity/agents/{id}/keys.

grant_not_found ​

HTTP 404. The requested grant does not exist for this agent/form pair. Check the grant ID or create a new grant.

Validation ​

validation_failed ​

The submitted data is invalid. Returned 422 on submit, with a per-field errors array using the submit codes above.

CAPTCHA / Turnstile ​

Codes specific to Cloudflare Turnstile verification during anonymous session mint (POST /papi/v1/auth/anonymous).

captcha_misconfigured ​

HTTP 500. The server's TURNSTILE_SECRET environment variable is not set. An operator must configure Turnstile secrets on the server before CAPTCHA can be used. Until resolved, forms with CAPTCHA enabled will deny anonymous sessions fail-closed.

captcha_required ​

HTTP 403. The form requires CAPTCHA (settings.captcha.enabled) but no turnstileToken was provided or it was empty. Include the Turnstile widget token in the request body.

captcha_failed ​

HTTP 403. The Turnstile token was invalid or expired. The reader was likely a bot, or the token expired before submission. Re-render the widget and try again with a fresh token.

captcha_unavailable ​

HTTP 503. The Turnstile verification service is unreachable (network error contacting Cloudflare). Retry later. This is a transient server-side issue, not a client problem.

Challenge ​

Codes specific to POST /papi/v1/submitter/form/challenges (issue) and POST /papi/v1/submitter/challenges/{challenge_id}/responses (respond).

no_checkable_field ​

HTTP 400. The form has no field with constraints (min/max length, pattern, format, etc.) that the challenge system can verify against. At least one constrained field is required to issue a challenge. Add a constrained field to the form first.

challenge_not_found ​

HTTP 404. The challenge ID does not exist or is not visible in the caller's scope. Check the challenge ID from the _challenge pointer in the challenge_required error.

challenge_not_issuable ​

HTTP 400. The challenge exists but is no longer issuable — it has already been resolved (passed or failed) or expired. Issue a fresh challenge with POST /papi/v1/submitter/form/challenges.

challenge_expired ​

HTTP 403. The challenge's TTL has elapsed. Issue a fresh challenge and respond before expiry.

challenge_failed ​

HTTP 403. The maximum number of attempts for this challenge has been exceeded. Issue a fresh challenge and try again.

signature_required ​

HTTP 403. Challenge respond requires an HTTP Message Signature (Track C). The request is missing a valid Signature and Signature-Input header pair. Sign the challenge response request with the agent's Ed25519 key.

form_not_found ​

HTTP 404. The form associated with this challenge could not be found. This typically means the form was deleted after the challenge was issued. Issue a fresh challenge on the current form.

KinoForms documentation