Documentation
Forms2sheet gives every form a public POST endpoint. No API keys — protect with fair use, domain allowlists, and optional CAPTCHA.
Endpoint
POST /api/forms/:formId/submit
HTML form
<form action="https://forms2sheet.com/api/forms/frm_abc123/submit" method="POST" enctype="multipart/form-data" > <input name="name" placeholder="Name" required /> <input name="email" type="email" placeholder="Email" required /> <input name="resume" type="file" multiple /> <button type="submit">Send</button> </form>
File uploads
Forms2sheet accepts file fields on multipart/form-data submissions. Each file is stored in the form owner's Google Drive; the matching sheet column receives a clickable Drive link instead of the binary. JSON bodies cannot carry files — use a plain HTML form or FormData.
How it works
- Post with
enctype="multipart/form-data"and one or more<input type="file">fields. The inputnamebecomes the sheet column header. - On first upload for a form, Forms2sheet creates a Drive folder named
{form name} — uploadsin the connected Google account and reuses it afterward. - Each file is uploaded into that folder (filename prefixed with a short submission id). The cell value is the file's Drive view link.
- Multiple files on the same field (
multiple) are written as newline-separated links in one cell. Separate file inputs become separate columns.
Empty file inputs (no file chosen) are ignored. Files stay private in your Drive — only people with Drive access can open the links.
HTML example
<form action="https://forms2sheet.com/api/forms/frm_abc123/submit" method="POST" enctype="multipart/form-data" > <input name="name" placeholder="Name" required /> <input name="email" type="email" placeholder="Email" required /> <input name="resume" type="file" accept=".pdf,.doc,.docx" /> <input name="photos" type="file" accept="image/*" multiple /> <button type="submit">Send</button> </form>
In the sheet, resume gets one Drive URL; photos gets one URL per image, each on its own line.
JavaScript (FormData)
<form id="contact">
<input name="name" />
<input name="email" type="email" />
<input name="resume" type="file" />
<button type="submit">Send</button>
</form>
<script>
document.getElementById("contact").addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch(
"https://forms2sheet.com/api/forms/frm_abc123/submit",
{ method: "POST", body: new FormData(e.target) }
);
console.log(await res.json());
});
</script>Do not set Content-Type manually when sending FormData — the browser adds the multipart boundary.
Size limits
Forms2sheet accepts bodies up to 100 MB (Cloudflare Free/Pro request body limit). Larger requests may be rejected by Cloudflare with HTTP 413 before they reach Forms2sheet:
| Cloudflare plan | Max request body |
|---|---|
| Free / Pro | 100 MB |
| Business | 200 MB |
| Enterprise | 500 MB (default) |
Workers isolates have 128 MB of memory. Very large multipart bodies buffered in memory can fail even under the body limit — keep total attachments well under 100 MB in practice (a few files totaling tens of MB is safest).
Automatic fields
Opt into server-filled columns with a hidden input whose entire value is a magic token. The column name is yours; Forms2sheet replaces only exact token matches before writing the sheet. Plain strings (including a forged timestamp) are left unchanged. Tokens are case-sensitive.
<form
action="https://forms2sheet.com/api/forms/frm_abc123/submit"
method="POST"
>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<input type="hidden" name="submitted_at" value="{{timestamp}}" />
<input type="hidden" name="country" value="{{country}}" />
<button type="submit">Send</button>
</form>Missing geo or request headers resolve to an empty string; the column is still written. Tokens such as {{ip}}, user agent, and geo land in your Google Sheet only when you include them.
Time (UTC)
| Token | Value |
|---|---|
{{timestamp}} | ISO 8601 UTC (e.g. 2026-07-12T17:54:01.234Z) |
{{iso}} | Alias of {{timestamp}} |
{{now}} | Alias of {{timestamp}} |
{{unix}} | Unix seconds |
{{unix_ms}} | Unix milliseconds |
{{date}} | YYYY-MM-DD UTC |
{{time}} | HH:mm:ss UTC |
{{year}} | YYYY |
{{month}} | MM |
{{day}} | DD |
{{weekday}} | Short English weekday (Sun…Sat) UTC |
Submission / form
| Token | Value |
|---|---|
{{submission_id}} | Submission id for this request (sub_…) |
{{form_id}} | Form id |
{{form_name}} | Form display name |
Request / client
| Token | Value |
|---|---|
{{ip}} | Client IP (cf-connecting-ip or X-Forwarded-For) |
{{user_agent}} | User-Agent header |
{{origin}} | Origin / Referer hostname when present |
{{referer}} | Referer header (alias {{referrer}}) |
{{accept_language}} | Accept-Language header |
Geo / edge
Best-effort from Cloudflare headers when available; empty otherwise.
| Token | Value |
|---|---|
{{country}} | CF-IPCountry |
{{city}} | City header when present |
{{region}} | Region header when present |
{{colo}} | Cloudflare colo from CF-Ray when parseable |
Generated
| Token | Value |
|---|---|
{{uuid}} | New UUID v4 per resolved field |
{{random}} | 12-character URL-safe random string |
UTM and page URL (client-side)
Page URL and UTM params are not available as magic tokens. Capture them in your page before submit:
<input type="hidden" name="page_url" id="page_url" />
<input type="hidden" name="utm_source" id="utm_source" />
<script>
const params = new URLSearchParams(window.location.search);
document.getElementById("page_url").value = window.location.href;
document.getElementById("utm_source").value =
params.get("utm_source") || "";
</script>After submit
Configure a custom redirect URL or a success message. HTML form posts redirect to your URL when set; otherwise they land on /thanks/:formId showing your message. JSON / fetch clients still receive { success: true }.
Success response
{
"success": true,
"submissionId": "sub_..."
}Domain allowlist
If you configure allowed domains, the request Origin or Referer host must match exactly (e.g. app.example.com). An empty allowlist accepts all origins — fine for testing, risky in production.
CAPTCHA
When enabled, include the provider token as g-recaptcha-response, cf-turnstile-response, captchaToken, or header X-Captcha-Token.
Configure SMTP on each form for owner notifications and an optional auto responder to the submitter. Auto-replies use HTML bodies (rich text or paste raw HTML). Use {{field}} placeholders for any submitted field, plus {{form_name}}, {{form_id}}, and {{submission_id}}. The submitter address comes from a configured email field name, then common fallbacks like email.
Errors
400— invalid body / too large403— domain or CAPTCHA failure404— unknown form429— rate limited502— Google Sheets write failed
Also read Google Sheets limits.