# Waitloop Complete Integration Guide > Waitloop collects HTML form submissions without a backend. A form posts directly to a > Waitloop endpoint and the submission appears in the owner's dashboard. Suitable for > waitlists, contact forms, demo requests, lead capture and image collection. This file is written for AI coding assistants. Everything here is accurate for the live API. --- ## 1. The endpoint ``` POST https://api.waitloop.io/f/ ``` - A form key looks like `wz_` followed by 24 alphanumeric characters, for example `wz_r8kT4mNpQ2xY7vBn3jLs9wA1`. It is found on any list's detail page in the dashboard. - Accepted content types: `application/x-www-form-urlencoded`, `application/json`, `multipart/form-data`. - **There is no API key, bearer token or `Authorization` header.** The form key in the URL is the only credential, and it is public by design it belongs in client-side HTML. Never invent an auth header, and never tell the user to hide the key in an environment variable for a plain HTML form. ### Prerequisite that trips everyone up The domain hosting the form must be listed in **Allowed Domains** in the Waitloop dashboard (Settings), including `localhost` or a staging domain if testing from there. Submissions from any other origin return `404`. If a user reports "the form does nothing", check this first. --- ## 2. Fields Any named input is captured and becomes a column in the dashboard and CSV export. Fields are never declared in advance there is no schema to register. Rules, all enforced server-side: - Field names may contain letters, digits, underscores and hyphens. - A name must start with a letter or an underscore. `2fa` and `-foo` are rejected. - Maximum 50 characters per name. - **Names beginning with `_` are reserved by Waitloop.** Never invent one. The recognised reserved fields are `_step`, `_ref`, `_page`, `_variantId`, `_timeOnPage`, `_timeToStart`, `_timeToComplete`. - Maximum 20 fields per submission. - Maximum 1,000 characters per value. Email addresses maximum 254. - Total payload limit of 50KB for JSON and urlencoded submissions. Forms sending files use `multipart/form-data` and are not bound by that limit. Conventional field names: use `email` for the submitter's address, `name` for their name. Waitloop treats `email` specially for duplicate detection and notifications. --- ## 3. Responses ### Success 200 ```json { "success": true, "message": "You're on the list!", "position": 42, "referralCode": "wz_8kT4mNpQ2xY7" } ``` `position` is present only when the list is configured to show it. ### Failure 400, 404 or 429 ```json { "error": { "code": "VALIDATION_ERROR", "message": "Invalid email address." } } ``` Always surface `error.message` directly to the end user it is already written in user-facing language. Do not replace it with a generic string. | Status | Meaning | | --- | --- | | 400 | Validation failed: bad email, a field breaking the rules above, failed CAPTCHA, or a rejected file. | | 404 | Unknown form key, a list that is paused or archived, **or a domain not in Allowed Domains**. These are deliberately indistinguishable. | | 429 | Rate limited, or the list has hit the account's monthly submission limit. | ### Which response shape you get - A request with `Content-Type: application/json` always receives JSON. - A plain HTML form post receives a `302` redirect (see below). - A `multipart/form-data` post is ambiguous, so it honours the `Accept` header: send `Accept: application/json` to get JSON, otherwise you get the redirect. --- ## 4. Redirects after submit **There is no `_next` field. Do not generate one.** Any `_next` input is ignored. For plain form posts the destination is resolved server-side, in this order: 1. The Thank-You URL set on the list (dashboard, list Settings tab). 2. The account-level Default Thank-You URL (dashboard Settings). 3. Otherwise the visitor returns to the page they submitted from with `?waitloop=success` appended, which the page can detect to show its own confirmation. If the user wants a custom redirect, tell them to set it in the dashboard it is not controllable from the markup. --- ## 5. Plain HTML form (no JavaScript) ```html
``` --- ## 6. AJAX submission ### Option A the widget script, no code Add `data-waitloop` to an existing form and include the script once per page. It intercepts the submit, posts over AJAX, renders inline success and error messages, and automatically handles the honeypot, the page URL and any `?ref=` referral code. ```html
``` If the element is an empty `
` with no form inside, the widget renders a complete form configured from the dashboard, inside a shadow root so page CSS cannot affect it. Per-form overrides are `data-*` attributes on the element: `data-button-text`, `data-title`, `data-description`, `data-email-placeholder`, `data-success-message`, `data-position-text`, `data-share-label`, `data-copy-button`, `data-copied-button`, `data-invalid-email`, `data-error-message`, `data-locale`, `data-show-position`, `data-show-referral`, `data-ref`, `data-ga4`. **There is no `data-button-color`** colours are set in the dashboard's Form Creator, not by attribute. Supported locales: en, es, fr, de, pt, it, ja, ko, zh, ar. RTL layout is automatic. ### Option B your own fetch ```js const res = await fetch('https://api.waitloop.io/f/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.fromEntries(new FormData(form))) }); const body = await res.json(); if (!res.ok) { showError(body.error?.message || 'Something went wrong.'); return; } showSuccess(body.message); ``` --- ## 7. File uploads Waitloop accepts **images only**: png, jpg, jpeg, gif, webp, heic, heif, avif, bmp, tif, tiff. SVG is deliberately rejected because it can carry script. PDFs, documents, archives and video are not supported if the user needs a document, collect a link to it in a URL field. Every upload is validated by extension, declared content type *and* magic bytes, so a file renamed to `.png` is rejected. Setup the account owner must do first, in the dashboard: 1. Turn on **Allow file uploads** in the list's Settings tab (off by default on every list). 2. Add a **File Upload** field in the Form Creator whose name matches the file input. ### Plain HTML `enctype` is required. Without it the browser sends only the filename, not the file. ```html
``` ### From JavaScript ```js const res = await fetch('https://api.waitloop.io/f/YOUR_FORM_KEY', { method: 'POST', headers: { 'Accept': 'application/json' }, body: new FormData(form) }); ``` **Do not set `Content-Type` manually when sending `FormData`.** The browser must set it so the multipart boundary is included; setting it yourself breaks the upload. Limits scale with the account's plan per-file size from 5 MB on the free Sandbox plan up to 25 MB, and per-field file counts from 1 to 50. The list and each field can tighten those further but never widen them. --- ## 8. Multi-step forms Step one creates the submission immediately, so an abandoned step two still leaves a usable lead. Later steps merge into that same row. ```js // Step 1 create the signup. const r1 = await fetch('https://api.waitloop.io/f/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email }) }); const d1 = await r1.json(); const referralCode = d1.referralCode; // the handle for every later step // d1.nextStep and d1.totalSteps describe the wizard // Step 2 merge the rest into that same submission. await fetch(`https://api.waitloop.io/f/YOUR_FORM_KEY/${referralCode}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ _step: 2, company, use_case }) }); ``` Critical details: - Later steps use **`PATCH`**, not `POST`, and the referral code goes in the URL path. - The body must include `_step` with the step number being completed. - The request must be `application/json`. - Multi-step has to be enabled on the list, with fields assigned to steps, or step two returns `400`. Only step one is saved in that case. --- ## 9. Referrals and position Every submission gets a `referralCode` and a `position`. To credit a referrer, read `?ref=` from the page URL and pass it as `_ref`: ```js const ref = new URLSearchParams(location.search).get('ref'); await fetch('https://api.waitloop.io/f/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, _ref: ref || undefined }) }); ``` Build the share link as `location.origin + location.pathname + '?ref=' + body.referralCode`. Successful referrals move the referrer up the queue. The widget script does all of this automatically. --- ## 10. Metadata captured automatically Every submission stores the referrer, origin, user agent and server timestamp with no extra code. The page URL and UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) are read from the page address which only the widget script can see. **A plain no-JavaScript form post does not capture pageUrl or UTMs.** If campaign attribution matters, use the widget script or send the fields yourself. --- ## 11. Common mistakes to avoid - Inventing a `_next` hidden field for redirects. It does not exist. - Adding an `Authorization` header or API key. Form capture has no auth. - Omitting `enctype="multipart/form-data"` on a form with a file input. - Setting `Content-Type` manually when posting `FormData`. - Using `POST` instead of `PATCH` for step two of a multi-step form. - Using `data-button-color` it is not read. Colours come from the dashboard. - Assuming SVG or PDF uploads work. Images only. - Naming a field with a leading underscore, or starting a name with a digit. - Forgetting to tell the user to add their domain to Allowed Domains. --- ## 12. Where to send the user next - Dashboard and form keys: https://app.waitloop.io - Copy-paste form templates with CSS: https://waitloop.io/form-samples - Full documentation: https://waitloop.io/docs - AI coding guide and rules file: https://waitloop.io/ai-integration