These calls need the Wix Forms app on the site (appDefId 225dd912-7dea-4738-8688-4b8c6955ffc2); a 428, a 400 naming the app, or an empty result on a site that should have content means it is not there. List Installed Apps shows what the site has; Install Wix Apps adds it by that id.
Standard call shape (every curl below). The <AUTH> placeholder is shorthand for Authorization: Bearer <TOKEN> only. Body-bearing requests also need Content-Type: application/json. Send wix-site-id: <SITE_ID> when the token is account-scoped.
Change what a form collects — add a field, relabel one, tighten a rule, reorder, retire one — on a form that already exists. Composing the field itself is in Create Form STEP 1: § Field types gives the common kinds' identifier / inputType / componentType and blocks, with copyable choice and checkbox fields, and § Silent breakers apply to an added field exactly as to a created one.
A PATCH, never a delete-and-recreate. An update keeps the formId everything downstream already holds — a frontend binding, an automation, a saved dashboard view — and spends no slot against the site's form cap.
The calls in this recipe are complete as written: send them as they are, with no docs or schema lookup. Read the form, send the PATCH, and run the two read-backs in § Verify, in that order.
Each call's method page and the response fields the next step reads are below. Cite these pages as the calls' docs sources; none needs a search. A new field's shape is in Create Form STEP 1.
| Call | Method page | Read from the response |
|---|---|---|
GET https://www.wixapis.com/form-schema-service/v4/forms?namespace=wix.form_app.form&fieldsets=METADATA (find it by name) | List Forms | forms[].id, forms[].name |
GET https://www.wixapis.com/form-schema-service/v4/forms?namespace=wix.form_app.form&formIds={formId} (read, and read-back 1) | List Forms | forms[0]: the whole form, with revision, formFields, steps, deletedFormFields |
PATCH https://www.wixapis.com/form-schema-service/v4/forms/{formId} | Update Form | form, with the new revision |
GET https://www.wixapis.com/form-schema-service/v4/forms/{formId}/summary (read-back 2) | Get Form Summary | formSummary.fields |
No formId yet ("my contact form")? List the forms and pick it by name: GET https://www.wixapis.com/form-schema-service/v4/forms?namespace=wix.form_app.form&fieldsets=METADATA returns only names and IDs. That call skips disabled forms; if the form isn't there, call again with &enabled=false.
namespace is a REQUIRED query parameter on the read, and this is what a revise run trips over. Without it the read returns 400 namespace has size 0, expected 10 or more, namespace must not be empty — a violation naming a field, so it reads like a body problem. Fix the call, don't reshape the payload. (The filter on POST .../v4/forms/query needs it too.)GET returned and send that as form. namespace is immutable and travels along; there is no fieldMask, and no revision outside form — inventing either is a silent partial write.fields array before you send. The read-back carries fields (the old field list) next to formFields. Send both and the old list wins: the PATCH returns 200 and bumps revision, but a field you added only to formFields is dropped, and only its layout item is kept. Spreading the whole form ({ ...form, formFields }) does exactly this. Remove fields and deletedFields first: const { fields, deletedFields, ...body } = form.formFields; never rebuild the entries you read. [...form.formFields, newField] works. Mapping the array to "clean up" each field — dropping fieldType, re-shaping an options block — fails with fieldType cannot be provided with any these fields: input_options, display_options on every field at once. The read-back's field objects are already a valid write shape: pass them through untouched and touch only the one field you are changing.formFields is replaced wholesale. Wix: "Any field that's missing from the array you send is moved to deletedFormFields, so resend every field you want to keep." Same for steps[].layout — miss an item and that field goes unplaced, which renders empty. Drops are recoverable from deletedFormFields, but nothing announces them.revision is rejected — re-GET and re-apply; never guess or increment it.Field count reached its limit of N on a PATCH is the site's premium cap: reduce or upgrade, with the choice put to the user (Create Form § Plan caps). Never split the form across schemas to dodge it.Build the new field from Create Form STEP 1: § Field types gives the identifier / inputType / componentType triple and the two block names — take all three from one row, and note that a dropdown or radio is inputType: "STRING". § Choice fields has a complete dropdown and multi choice to copy, and § Checkboxes has the agreement and email-subscription checkboxes. Composing a field from memory is what turns one PATCH into eight.
The field added most often is a newsletter or marketing-consent checkbox. It is a CONTACTS_SUBSCRIBE field, kept optional; a ticked box subscribes the submitter's email. For a box the visitor must tick to agree to terms, use "identifier": "CHECKBOX" with "required": true and no pii or contactMapping. The field below is complete; you do not need another page for it. The label is rich content, and validation sits in booleanOptions. For "at the end", give its layout item the row of SUBMIT_BUTTON and move the button one row down — never below the button:
Then four things are yours to generate:
id — a fresh lowercase GUID (uuidgen | tr 'A-Z' 'a-z'), unique in the form.target — the immutable submission key, unique across the form: snake_case plus a short random suffix (job_industry_7f2b). A duplicate target is accepted and silently stores nothing for every field but one.options[] each need their own, exactly like the field's; a readable slug is rejected (Create Form § Choice fields). Generate them with uuidgen, never by hand.id + value + label, and validation.enum must list every one of those values. An empty validation is a free-text field, not a dropdown; the general "always include validation, even as {}" rule does not apply here, and getting it wrong is accepted with a 200.steps[<n>].layout.large.items, mirroring into medium / small if the form has them. For "at the end", give it the row of SUBMIT_BUTTON and move the button one row down, so the field is not placed below the button:A field missing from the layout isn't dropped, but it sorts last and has no defined position in the owner's builder. To place it mid-form, insert at that row and bump every item at or after it — row restarts at 0 in each step.
A choice field's identifier is its kind — DROPDOWN, RADIO_GROUP, CHECKBOX_GROUP, TAGS — never TEXT_INPUT, which is accepted and stored as a plain text input (Create Form § Choice fields).
| Change | How |
|---|---|
| Label, placeholder, options, validation, order | Patch them and stop — nothing downstream needs updating |
| A field's component type, in place | Send that field with the choice identifier and only the new options block; leaving textInputOptions beside dropdownOptions reads back as the text-input fallback |
| Remove a field ("remove the phone field", "delete that question") | Drop it from formFields and drop its item from steps[].layout. It moves to deletedFormFields and can be restored from there; submissions already collected keep their value for it. Verified live on 2026-11-04: after the drop, a submission sent before it still returned the field's value, and /summary listed the field with deleted: true. So a form with submissions needs no hidden: true for a removal. The user's "remove" is the confirmation; do not ask again |
| Hide or retire a field ("stop asking", "hide that question") | Set hidden: true on the field and keep it in formFields and the layout: it stops rendering for visitors |
Rename a target | Never. It is the storage key, so a rename orphans every submission already collected under the old one. Change the label instead |
A 200 on the PATCH proves the body parsed, nothing about what was stored. Right after the PATCH, in the same run, send both of these reads. They are complete as written and need no lookup. Neither one alone is enough: the first shows the stored fields, deletedFormFields and layout; the second shows what the owner's dashboard renders.
Assert on these two read-backs, not on the PATCH response:
identifier and componentType you sent — a choice field with a non-empty options[], which is where a dropdown that degraded to a text input shows up;inputOptions.required still reads back as sent;deletedFormFields and has no layout item, and an added one has a layout item in the place you meant;formSummary.fields holds one entry with deleted: false per input field. A removed field stays there with deleted: true, so do not count it.These two reads are the whole verification; do not add more.
Then hand back the dashboard links (Forms Dashboard Navigation): the form builder at wix-forms/form/{formId}, and that form's submissions at wix-forms/form/{formId}/submissions. New submissions carry the new field; past ones don't.
| Error / symptom | Cause | Fix |
|---|---|---|
400 namespace has size 0, expected 10 or more / namespace must not be empty on a read | The namespace query parameter was omitted — it is required on every listing read, and the violation naming a field makes it look like a body problem | Add ?namespace=wix.form_app.form to the read; leave the payload alone |
fieldType cannot be provided with any these fields: input_options, display_options — on every field | The read-back's formFields were re-mapped or "cleaned" before sending | Send the fields exactly as read and append the new one: [...form.formFields, newField] |
inputType enum must be in [UNKNOWN_INPUT_TYPE, STRING, NUMBER, …] | An invented inputType such as ENUM (with an enumOptions block) for a choice field | A dropdown or radio is inputType: "STRING" with stringOptions.componentType; multi choice is ARRAY |
The PATCH is rejected for a revision mismatch | Someone (or an earlier call) updated the form since your read | Re-GET and re-apply the change; never increment the revision yourself |
The PATCH returns 200 and bumps revision, but the new field is missing from the read-back (its layout item is there) | The body still carried the legacy fields array from the read-back, and the server applied that old list | Remove fields and deletedFields from the body, re-GET, and send again. Don't retry the same body |
| Fields that were on the form are gone after the update | formFields was sent partial — the omitted fields moved to deletedFormFields | Re-send them in a further PATCH, from deletedFormFields in the read-back; always send the whole array |
| Surviving fields render empty for the owner | steps[].layout was sent without their items | Re-send every layout item, including SUBMIT_BUTTON's; re-verify with /summary |
The added dropdown/radio reads back as a text input with textInputOptions | identifier was TEXT_INPUT rather than the choice kind — componentType alone doesn't route the renderer | Set identifier to the choice kind and re-send. Deleting and re-adding the field with the same identifier reproduces it (Create Form § Choice fields) |
| The update lands but the field never appears in submissions | The new field shares a target with an existing one — the server keeps one and the rest store nothing | Give it a unique target and PATCH again |
Field count reached its limit of N | The site's premium field cap, counting INPUT fields | Reduce, or put the upgrade choice to the user (Create Form § Plan caps) |
formId yetPATCH contract and its replace semanticsrevision, hidden, deletedFormFields, steps[].layoutLast updated: 5 November 2026