Setting the file. One moment.
Wix Forms Submissions · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 22.10
Post Detail
Previous
Reference Wix Form Schema Utils
references/forms/app/rest/ wix-forms-submissions.js
JavaScript · 238 lines · 12 KB
11
* listed under the owner scope `SCOPE.DC-FORMS.MANAGE-SUBMISSIONS`, and in practice returns 200 on
12 * an anonymous visitor token: Wix grants implicit visitor access so a published site can submit its
13 * own forms. Reaching for a backend here is the most common wrong turn on this vertical.
14 *
15 * ⚠️ SUBMISSIONS ARE WRITE-ONLY FROM THE BROWSER. Reading them back
16 * (`querySubmissionsByNamespace`, `getSubmission`, `countSubmission`) genuinely requires the owner
17 * scope `WIX_FORMS.SUBMISSION_READ_ANY` and 403s for a visitor. The resolved `createSubmission`
18 * promise IS the success signal — show a thank-you; the submission appears in the owner's dashboard.
19 * A site that must LIST what visitors submitted needs the `cms` vertical instead.
20 *
21 * Submit values: https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/about-submission-values.md
22 * Upload URL: https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/get-media-upload-url.md
23 * Create: https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/create-submission.md
24 */
25
26 /**
27 * Submission statuses that mean the submission EXISTS — show the thank-you. `CONFIRMED` is recorded,
28 * `PENDING` is created but not recorded yet, and `PAYMENT_WAITING` is created on a form that also
29 * collects payment. See `createSubmission` for why this is an allowlist rather than a catch-all.
30 */
31 export const SUBMITTED_OK = new Set ([ "CONFIRMED" , "PENDING" , "PAYMENT_WAITING" ]);
32
33 /**
34 * Get a signed URL to upload one file for a form submission — the FORMS-scoped wrapper around the
35 * Media Manager's upload URL, so a visitor can attach a file without the Manage-Media credential a
36 * direct `site-media` call would demand (that one 403s for a visitor; this one is listed under the
37 * same `SCOPE.FORMS.VIEW-FORM` as the schema read).
38 * Reference: https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/get-media-upload-url.md
39 *
40 * @param {string} formId
41 * @param {string} filename Including the extension — Wix reads the type from it.
42 * @param {string} mimeType e.g. "image/png". A browser gives you `file.type`.
43 * @returns {Promise<string>} The signed upload URL.
44 */
45 export async function getMediaUploadUrl ( formId , filename , mimeType ) {
46 const res = await wixApiRequest ( "/form-submission-service/v4/submissions/media-upload-url" , {
47 method: "POST" ,
48 body: { formId, filename, mimeType },
49 });
50 if ( ! res?.uploadUrl) throw new Error ( `Could not get an upload URL for "${ filename }".` );
51 return res.uploadUrl;
52 }
53
54 /**
55 * Upload one `File` and return the value to submit for its field.
56 *
57 * ⚠️ The submission value IS the generated `uploadUrl` — not the CDN URL the upload responds with,
58 * and not a file id. That's Wix's own contract (see the "media file" example on Create Submission
59 * and the "Submit a form with media" sample flow): get the URL, PUT the bytes to it, then send that
60 * same URL as the field's value.
61 *
62 * The PUT goes straight to Wix's upload host on a signed URL, so it uses plain `fetch` — NOT
63 * `wixApiRequest`: adding the visitor's Authorization header to a pre-signed URL is what turns a
64 * working upload into a 400.
65 *
66 * @param {string} formId
67 * @param {File} file From an `<input type="file">`.
68 * @returns {Promise<string>} The value to put in the submission for this field.
69 */
70 export async function uploadSubmissionFile ( formId , file ) {
71 // A browser leaves `type` empty for extensions it doesn't recognize; the generic type keeps the
72 // upload-URL call valid (Media Manager rejects a mime type that contradicts the extension).
73 const mimeType = file.type || "application/octet-stream" ;
74 const uploadUrl = await getMediaUploadUrl (formId, file.name, mimeType);
75 const res = await fetch ( `${ uploadUrl }?filename=${ encodeURIComponent ( file . name ) }` , {
76 method: "PUT" ,
77 headers: { "Content-Type" : mimeType },
78 body: file,
79 });
80 if ( ! res.ok) {
81 // Media Manager's own codes land here: FILE_SIZE_OVER_LIMIT, UNSUPPORTED_FILE_FORMAT,
82 // MISMATCH_MIME_TYPE, ZERO_FILE_SIZE, SITE_QUOTA_EXCEEDED.
83 throw new Error ( `Could not upload "${ file . name }" (${ res . status }). Check its size and file type.` );
84 }
85 return uploadUrl;
86 }
87
88 /**
89 * Upload every `File` sitting in the form's state and return a copy of `values` with each file
90 * field replaced by its submitted value(s). Run this BEFORE `toSubmissionValues` — a `File` object
91 * is not something the submission API accepts.
92 *
93 * @param {string} formId
94 * @param {object[]} fields The render model.
95 * @param {Record<string, unknown>} values
96 * @returns {Promise<Record<string, unknown>>}
97 */
98 export async function uploadFormFiles ( formId , fields , values ) {
99 const next = { ... values };
100 for ( const field of fields) {
101 if ( ! isFile (field)) continue ;
102 const target = targetOf (field);
103 const picked = []. concat (values[target] ?? []). filter (( f ) => f instanceof File );
104 const uploaded = [];
105 // Sequential on purpose: a visitor's uplink is the bottleneck, and a failed file should stop
106 // the submit rather than race three more uploads it will throw away.
107 for ( const file of picked) uploaded. push ( await uploadSubmissionFile (formId, file));
108 next[target] = uploaded;
109 }
110 return next;
111 }
112
113 /**
114 * Turn the form's state into the `submissions` map `createSubmission` expects — keyed by each
115 * field's `target`, which is the same key the controls are bound to, so the keys come out right by
116 * construction with no hand-maintained list to drift.
117 *
118 * Walks the SCHEMA, not the state object: a stray key in `values` can never reach the API, and a
119 * field the owner added shows up the moment the schema does.
120 *
121 * The three value shapes and their per-`inputType` rules
122 * (https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/about-submission-values.md):
123 * • flat value — text / dropdown / radio / date / time. NUMBER submits a number and PHONE the
124 * normalized E.164 string, never the raw control text.
125 * • ARRAY — a multi-choice field, holding the checked option values.
126 * • OBJECT — an ADDRESS, keyed by subfield (`city`, `country`, …), which is also the shape
127 * behind the server's nested `address/city` error paths.
128 * An empty optional field is OMITTED rather than sent as "" — the server validates what it's given.
129 *
130 * @param {object[]} fields The render model, from `hooks/useWixForm.js`.
131 * @param {Record<string, unknown>} values The form's state, keyed by `target`.
132 * @returns {Record<string, unknown>}
133 */
134 export function toSubmissionValues ( fields , values ) {
135 const filled = ( v ) => v !== undefined && v !== null && String (v). trim () !== "" ;
136 const out = {};
137 for ( const field of fields) {
138 const target = targetOf (field);
139 const raw = values?.[target];
140
141 if ( isAddress (field)) {
142 const parts = {};
143 for ( const { sub } of addressPartsOf (field)) {
144 if ( filled (raw?.[sub])) parts[sub] = String (raw[sub]). trim ();
145 }
146 if (Object. keys (parts). length ) out[target] = parts;
147 continue ;
148 }
149
150 if ( isMulti (field)) {
151 const picked = (Array. isArray (raw) ? raw : []). filter (filled);
152 if (picked. length ) out[target] = picked;
153 continue ;
154 }
155
156 // A file field carries what `uploadFormFiles` produced — the upload URL(s). One file submits the
157 // bare string (the shape Wix's own example uses); several submit an array. A stray `File` that
158 // never went through the upload is dropped rather than sent, since it would 400 the whole form.
159 if ( isFile (field)) {
160 const urls = []. concat (raw ?? []). filter (( v ) => typeof v === "string" && v);
161 if (urls. length ) out[target] = urls. length === 1 ? urls[ 0 ] : urls;
162 continue ;
163 }
164
165 if ( ! filled (raw)) continue ;
166 const value = typeof raw === "string" ? raw. trim () : raw;
167 out[target] =
168 inputTypeOf (field) === "NUMBER" ? Number (value) :
169 validationOf (field).format === "PHONE" ? normalizePhone (value) : value;
170 }
171 return out;
172 }
173
174 /**
175 * Create a submission. This is the write — and the ONLY confirmation you get, since a visitor can't
176 * read submissions back.
177 * Reference: https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/create-submission.md
178 *
179 * ⚠️ EVERY status this resolves on is a created submission — `CONFIRMED` (recorded), `PENDING`
180 * (created, not recorded yet) and `PAYMENT_WAITING` (created on a form that also collects payment).
181 * Show the thank-you for all three. Treating one as a failure invites the visitor to submit again,
182 * which costs the owner duplicate entries for a submission that already exists.
183 *
184 * `PAYMENT_WAITING` still leaves the visitor a payment step, but the SUBMIT succeeded — surface the
185 * payment inside the success state, never as a failed submission. A form only reaches this status if
186 * the UI rendered a PAYMENT field, so read the returned `status` when you build one.
187 * Success stays an ALLOWLIST rather than a catch-all, so a status added to the enum later can't
188 * silently render a thank-you for something that isn't a submission.
189 *
190 * ⚠️ REST returns `submission.id` — the SDK's normalized `_id` does not exist here.
191 *
192 * @param {string} formId
193 * @param {Record<string, unknown>} values Map of `target` → value, from `toSubmissionValues`.
194 * @returns {Promise<{ id: string, status: string }>}
195 */
196 export async function createSubmission ( formId , values ) {
197 const res = await wixApiRequest ( "/form-submission-service/v4/submissions" , {
198 method: "POST" ,
199 body: { submission: { formId, submissions: values } },
200 });
201 const submission = res?.submission;
202 if ( ! submission?.id) throw new Error ( "Submission failed (no submission returned)." );
203 if ( ! SUBMITTED_OK . has (submission.status)) {
204 throw new Error (
205 `Submission status is "${ submission . status }" — not one of the statuses that mean the ` +
206 `submission was created (${ [ ... SUBMITTED_OK ]. join ( ", " ) }), so don't show a success state. ` +
207 `Check the Submission status docs before adding it to SUBMITTED_OK.` ,
208 );
209 }
210 return { id: submission.id, status: submission.status };
211 }
212
213 /**
214 * Pull the per-field violations out of a failed `createSubmission`.
215 *
216 * The documented entries (`errorPath`, `errorType`, `errorMessage`, `params`) arrive under
217 * `details.validationError.fieldViolations[]`, each with its own nested `data.errors[]` array —
218 * two levels deeper than the docs' shape. This flattens that; turning a violation into words a
219 * visitor should read is the UI's job (`mapSubmissionErrors` in `hooks/useWixForm.js`).
220 *
221 * `errorPath` is the field's `target`, or a nested path like `address/subdivision` or
222 * `attachments/0/fileId` — which is exactly how the controls are named, so it maps straight across.
223 *
224 * Reference: https://dev.wix.com/docs/api-reference/crm/forms/form-submissions/introduction#validation-errors
225 *
226 * @param {Error & { body?: object }} err From `createSubmission`; the REST body is on `err.body`.
227 * @returns {{ errorPath: string, errorType: string, errorMessage?: string }[]} Empty when the
228 * failure wasn't a validation error at all (a network drop, a 500).
229 */
230 export function submissionViolations ( err ) {
231 const violations =
232 err?.body?.details?.validationError?.fieldViolations ??
233 err?.details?.validationError?.fieldViolations ??
234 [];
235 return violations
236 . flatMap (( violation ) => violation.data?.errors ?? [violation])
237 . filter (( entry ) => entry?.errorPath);
238 }