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:
{
"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/submit/{access_key}?partial=true 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=trueoncemissingRequiredis 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 accessKey. Issue a challenge with POST /papi/v1/challenge and respond correctly, then retry the original request.
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/challenge (issue) and POST /papi/v1/challenge/respond (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/challenge.
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.
