Builder Guide
The builder side of the public API lets you create and edit forms programmatically with an API key. All builder endpoints live under /papi/v1/forms and authenticate with the x-api-key header.
Read the Overview first if you have not — every response uses the standard envelope.
Authentication
Builder requests use an API key sent in the x-api-key header. Create a key in the app under Settings > API Keys (see API Keys).
Scopes:
forms:read— read forms (GETendpoints).forms:write— create and modify forms (POSTandPATCHendpoints).
Grant the smallest scope set the integration needs.
List Forms
curl https://api.kinoforms.com/papi/v1/forms \
-H "x-api-key: $KINOFORMS_API_KEY"{
"data": [
{ "id": "f4d3a2e1-8b7c-4d6e-9a1f-2c3b4d5e6f70", "title": "Contact us", "version": 4 }
],
"apiVersion": "v1",
"_docs": "https://docs.kinoforms.com/developer/api-reference"
}Get a Form
curl https://api.kinoforms.com/papi/v1/forms/f4d3a2e1-8b7c-4d6e-9a1f-2c3b4d5e6f70 \
-H "x-api-key: $KINOFORMS_API_KEY"The data object includes the form's metadata, its current version, and its fields. Hold on to version if you intend to use optimistic concurrency on a later PATCH.
Create a Form
curl -X POST https://api.kinoforms.com/papi/v1/forms \
-H "x-api-key: $KINOFORMS_API_KEY" \
-H "content-type: application/json" \
-d '{
"title": "Contact us",
"fields": [
{ "key": "email", "type": "email", "label": "Email", "required": true }
]
}'Requires forms:write. The response data is the created form, including its new id.
Edit a Form (Diffs-Only PATCH)
You do not replace a whole form to edit it. PATCH /papi/v1/forms/{form_id} takes a diffs-only ops array — a list of operations applied in order. This keeps edits small, intent-revealing, and concurrency-safe.
Requires forms:write.
curl -X PATCH https://api.kinoforms.com/papi/v1/forms/f4d3a2e1-8b7c-4d6e-9a1f-2c3b4d5e6f70 \
-H "x-api-key: $KINOFORMS_API_KEY" \
-H "content-type: application/json" \
-d '{
"expectedVersion": 4,
"ops": [
{ "op": "setMeta", "title": "Contact our team" },
{ "op": "addField", "field": { "key": "phone", "type": "tel", "label": "Phone" } }
]
}'Request Shape
| Field | Required | Description |
|---|---|---|
ops | yes | An ordered array of operations to apply. |
expectedVersion | no | If set, the PATCH only applies when the form's current version matches. See Optimistic concurrency. |
Operations Reference
Each op object has an op field naming the operation, plus its arguments.
setMeta
Update form-level metadata (title, description, settings).
{ "op": "setMeta", "title": "Contact our team", "description": "We reply within a day." }addField
Append a new field to the form.
{ "op": "addField", "field": { "key": "phone", "type": "tel", "label": "Phone", "required": false } }updateField
Change properties of an existing field, identified by its key.
{ "op": "updateField", "key": "phone", "patch": { "required": true, "label": "Mobile phone" } }removeField
Delete a field by key.
{ "op": "removeField", "key": "phone" }moveField
Reorder a field to a new position (zero-based index).
{ "op": "moveField", "key": "email", "index": 0 }How Ops Apply
- Ops apply in array order. Later ops see the result of earlier ops in the same request.
- The whole array is applied atomically: if any op is invalid, the form is left unchanged and you get a
validation_failederror describing the bad op. - A successful PATCH returns the updated form (including a bumped
version) indata.
Optimistic Concurrency
To avoid clobbering a concurrent edit, send expectedVersion with the version you last read:
{ "expectedVersion": 4, "ops": [ /* ... */ ] }- If the form's current version is still
4, the PATCH applies and the version becomes5. - If someone else already edited the form (current version is
5), the PATCH is rejected. Re-fetch the form, rebase your ops on the new state, and retry with the newexpectedVersion.
Omit expectedVersion for last-write-wins behavior.
Errors
Builder errors use the standard envelope. Common codes:
unauthorized— missing or invalid API key.missing_scope— the key lacksforms:read/forms:writefor this call.validation_failed— a bad op or invalid field definition; see theerrorsarray.rate_limited— slow down and retry after a short delay.
See the Validation reference for the full code list and per-field codes.
