On this page

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

  1. Post with enctype="multipart/form-data" and one or more <input type="file"> fields. The input name becomes the sheet column header.
  2. On first upload for a form, Forms2sheet creates a Drive folder named {form name} — uploads in the connected Google account and reuses it afterward.
  3. Each file is uploaded into that folder (filename prefixed with a short submission id). The cell value is the file's Drive view link.
  4. 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 planMax request body
Free / Pro100 MB
Business200 MB
Enterprise500 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)

TokenValue
{{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

TokenValue
{{submission_id}}Submission id for this request (sub_…)
{{form_id}}Form id
{{form_name}}Form display name

Request / client

TokenValue
{{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.

TokenValue
{{country}}CF-IPCountry
{{city}}City header when present
{{region}}Region header when present
{{colo}}Cloudflare colo from CF-Ray when parseable

Generated

TokenValue
{{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.

Email

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 large
  • 403 — domain or CAPTCHA failure
  • 404 — unknown form
  • 429 — rate limited
  • 502 — Google Sheets write failed

Also read Google Sheets limits.