RECIPE: Update a Wix Form

Download skillThe skill is a reference md and part of wix-manage skill. You can use the following command to add the full wix-manage skill to your project:
Copy

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 call

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.

CallMethod pageRead 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 Formsforms[].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 Formsforms[0]: the whole form, with revision, formFields, steps, deletedFormFields
PATCH https://www.wixapis.com/form-schema-service/v4/forms/{formId}Update Formform, with the new revision
GET https://www.wixapis.com/form-schema-service/v4/forms/{formId}/summary (read-back 2)Get Form SummaryformSummary.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.

Copy
  • ⚠️ 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.)
  • The read-back is the body. Apply your change to what the 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.
  • ⚠️ Delete the legacy 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.
  • ⚠️ Append to 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.
  • A stale revision is rejected — re-GET and re-apply; never guess or increment it.
  • Plan caps apply to an added field. 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.

Adding a field

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:

Copy

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.
  • one GUID per choice option — 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.
  • the choices, declared twice — every option needs 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.
  • the layout item — add it to 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:
Copy

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).


The other changes

ChangeHow
Label, placeholder, options, validation, orderPatch them and stop — nothing downstream needs updating
A field's component type, in placeSend 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 targetNever. It is the storage key, so a rename orphans every submission already collected under the old one. Change the label instead

Verify — an update regresses a form exactly as a create can

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.

Copy

Assert on these two read-backs, not on the PATCH response:

  • the changed field is there, carrying the 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;
  • every field you meant to keep is still present, and every inputOptions.required still reads back as sent;
  • a removed field is in deletedFormFields and has no layout item, and an added one has a layout item in the place you meant;
  • the summary's 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.


Troubleshooting

Error / symptomCauseFix
400 namespace has size 0, expected 10 or more / namespace must not be empty on a readThe namespace query parameter was omitted — it is required on every listing read, and the violation naming a field makes it look like a body problemAdd ?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 fieldThe read-back's formFields were re-mapped or "cleaned" before sendingSend 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 fieldA dropdown or radio is inputType: "STRING" with stringOptions.componentType; multi choice is ARRAY
The PATCH is rejected for a revision mismatchSomeone (or an earlier call) updated the form since your readRe-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 listRemove 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 updateformFields was sent partial — the omitted fields moved to deletedFormFieldsRe-send them in a further PATCH, from deletedFormFields in the read-back; always send the whole array
Surviving fields render empty for the ownersteps[].layout was sent without their itemsRe-send every layout item, including SUBMIT_BUTTON's; re-verify with /summary
The added dropdown/radio reads back as a text input with textInputOptionsidentifier was TEXT_INPUT rather than the choice kind — componentType alone doesn't route the rendererSet 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 submissionsThe new field shares a target with an existing one — the server keeps one and the rest store nothingGive it a unique target and PATCH again
Field count reached its limit of NThe site's premium field cap, counting INPUT fieldsReduce, or put the upgrade choice to the user (Create Form § Plan caps)

Last updated: 5 November 2026

Did this help?