SheetLink Forms beta

Next.js forms to Google Sheets

A route handler, a fetch, or a plain form action - every submission lands as a row in Google Sheets or Excel, with no automation tool in the middle.

Next.js gives you several honest ways to move a form submission into a spreadsheet, and none of them should involve a third-party automation tool replaying webhooks. SheetLink Forms gives every form a permanent endpoint at https://sheetlinkforms.com/f/{token}. It accepts JSON, urlencoded, or multipart POSTs (bodies up to 256KB), and files each accepted submission as a row in Google Sheets or an Excel Online table.

Delivery is direct: an asynchronous worker writes to the Google Sheets API or Microsoft Graph with retries at 5 minutes, 30 minutes, and 2 hours, and every attempt is visible in a delivery log. Field names that match your column headers map automatically (case and punctuation insensitive), so the JSON keys you already use in your form state usually just work. The full pipeline is described on how it works.

The product is free during beta and invite-gated - join the waitlist - and there are no per-submission fees, ever.

The best path for Next.js

The best path in Next.js is a JSON POST from a route handler. Your form component posts to /api/contact, the handler forwards the payload to your SheetLink endpoint, and the endpoint answers {"ok":true,"id":"..."}, which is trivial to relay back to the client. Running the call server-side means there is no CORS to think about and you can validate or enrich the payload before it leaves your app.

Two lighter alternatives are also fully supported: a client-side fetch straight from the component (the endpoint is built for cross-origin browser posts - send Accept: application/json to get JSON back instead of the 303 redirect meant for classic HTML forms), or a plain <form action> with no JavaScript at all, which uses the 303-redirect thank-you flow.

// app/api/contact/route.ts (App Router) export async function POST(req: Request) { const fields = await req.json(); const res = await fetch("https://sheetlinkforms.com/f/slf_yourtoken", { method: "POST", headers: { "Content-Type": "application/json" }, // _slhp is the honeypot: include it, leave it empty body: JSON.stringify({ ...fields, _slhp: "" }), }); // SheetLink answers JSON posts with {"ok":true,"id":"..."} const result = await res.json(); return Response.json(result, { status: res.status }); } // In your client component: // await fetch("/api/contact", { // method: "POST", // headers: { "Content-Type": "application/json" }, // body: JSON.stringify({ Name: name, Email: email, Message: message }), // });

1. Connect a sheet and get your endpoint

Connect Google Sheets with one-click OAuth (the drive.file scope means the product only sees sheets you pick or create - never the rest of your Drive) or connect Excel Online and pick a table. You get a permanent endpoint of the form https://sheetlinkforms.com/f/{token}. On an empty sheet the header row is seeded for you; rows then append under it.

2. Create the route handler

Use the snippet above in app/api/contact/route.ts. On the Pages Router the same logic goes in pages/api/contact.ts with req.body and res.status().json() - anything that can fetch works. Keep the JSON keys named after your sheet's column headers so mapping is automatic; explicit per-field mapping exists in the dashboard for the cases where they differ.

3. Wire your form component to it

In a client component, prevent default on submit and fetch("/api/contact") with a JSON body built from your form state. Relay the endpoint's ok flag into your UI state for success and error rendering. Nothing about this couples you to a rendering mode - static, SSR, and ISR pages all post the same way.

4. Or post from the client directly

If you do not want the extra hop, fetch https://sheetlinkforms.com/f/{token} straight from the browser. Send Content-Type: application/json (or an Accept: application/json header with form-encoded bodies) and you get the {"ok":true,"id":"..."} JSON response instead of a 303 redirect. The endpoint supports cross-origin browser posts, and you can restrict them with the per-form origin allowlist so only your domains may submit.

5. Or skip JavaScript entirely

A plain <form action="https://sheetlinkforms.com/f/{token}" method="POST"> works in any server-rendered page with zero client code. Include the off-screen honeypot input (<input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">) and the browser follows a 303 redirect to a thank-you page or your custom URL after submitting.

6. Test and read the delivery log

Send a test submission and watch the row land - or try the live demo first to see the flow end to end. The dashboard's delivery log records each attempt, and the docs list the error responses worth handling: 404 unknown token, 410 paused form, 403 origin not allowed, 413 payload too large, 429 rate limited, 400 unreadable body.

FAQ

Route handler or client fetch - which should I use?

Use the route handler when you want server-side validation, enrichment, or a single place to change the destination later. Use a direct client fetch when the form is simple and you want less code - the endpoint is designed for cross-origin browser posts, and the per-form origin allowlist can pin it to your domains. Both receive the same {"ok":true,"id":"..."} response.

Does this work with the Pages Router or older Next.js versions?

Yes. The integration is one HTTP POST, so an API route in pages/api, a route handler in app/api, a server action, or a plain client fetch all work identically. There is no SDK to install and nothing version-specific.

Where does the honeypot go in a JSON post?

Include "_slhp": "" as a key in the JSON body and leave it empty, as the snippet does. The honeypot is the one signal that marks spam outright; if a bot fills it, the submission is marked spam. Everything else suspicious is quarantined for one-click review rather than dropped.

Can I keep ad attribution if I post from the server?

The gclid, fbclid, and utm_* values live in the visitor's browser, so either forward them yourself as fields in the JSON body, or put the sl.js embed on the page - it captures utm_* plus gclid, wbraid, gbraid, fbclid, and msclkid, persists them in localStorage across pages, and binds forms marked data-sheetlink, with SPA navigation handled via MutationObserver.

What happens if the Google Sheets API is briefly down?

Acceptance and delivery are decoupled. The endpoint accepts the submission, then an asynchronous worker writes the row and retries at 5 minutes, 30 minutes, and 2 hours if needed. The delivery log in the dashboard shows the outcome of every attempt.

Also on: React · Plain HTML · Webflow

Request an invite See the live demo