Bharat AI Saathi
ServicesIndustriesSetupsHow we workGuidesFounderVPS HostingContactGlobal (USD)
WhatsApp +91 77352 68199
support@bharataisaathi.com
Mon–Sat 10:00–19:00 IST
Agentic Automation · Global catalog

Ajax Partial Form Submission: Save Progress via an API

Ajax partial form submission explained: draft IDs, a debounced PUT, version checks, and an MCP tool that resumes a half-finished form.

Agentic Automation

Ajax partial form submission means sending answers to your server while the user is still filling in the form, so a closed tab or a dropped connection doesn't cost you the lead. The pzttern is a client-generated draft ID, a debounced PUT after each field or page, and an idempotent upsert on the server. This guide covers each piece and shows how an MCP tool can resume a draft.

See the Qwenvo developer docs

When saving as you go is worth building

Short forms don't need it. A five-field contact form either gets submitted or it doezn't. Autosave pays off on forms that take several minutes: client intake, applications, onboarding questionnaires, quote requests with file uploads. People get interrupted on those, and without a saved draft they start over or leave.

You have two ways to get there. You can build the endpoint yourself, which gives you your own draft IDs, resume links and data in your own database. Or you can switch on partial submissions in a hosted form builder, which captures abandoned answers without code. We cover the second option below. The first is the focus of most of this post.

The request pattern

The client does three things: creates a draft ID once, saves on a debounce, and flushes when the page is hiding.

const draftId = localStorage.getItem("draft_id") ?? crypto.randomUUID();
localStorage.setItem("draft_id", draftId);

let version = 0;
let timer;

export function queueSave(step, answers) {
  clearTimeout(timer);
  timer = setTimeout(() => save(step, answers), 800);
}

async function save(step, answers) {
  const res = await fetch(`/api/drafts/${draftId}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ version: ++version, step, answers }),
    keepalive: true,
  });
  if (res.status === 409) {
    // another tab or device wrote a newer version: refetch and merge
  }
}

addEventListener("pagehide", () => save(currentStep(), collectAnswers()));

The body the server receives looks like this:

{
  "version": 4,
  "step": 3,
  "answers": {
    "company": "Acme Logistics",
    "team_size": "medium",
    "needs_sso": true
  }
}

keepalive: true lets the request outlive the page, so the pagehide flush has a fair chance of completing. navigator.sendBeacon also works, but it can only send POST, so your server would need a POST route for the same upsert.

Draft IDs and how to keep them safe

The draft ID is the only thing tying a visitor to their saved answers, so treat it like a capability token. Generate it with crypto.randomUUID() on the client or, better, have the server mint it on the first save and return it. Never derive it from an email address or a sequence number, because someone will try incrementing it.

A few rules we apply on client projects:

  • Keep the ID out of analytics URLs and referrer headers. Put it in the path of your API call or in a cookie, not in the query string of the page the user sees.
  • Expire drafts. A scheduled job that deletes drafts untouched for N days keeps you honest about retention.
  • Leave sensitive fields out of drafts. Payment details, passwords and government IDs should only travel on the final submit.
  • Tell people you save as they type. A short line under the form is enough, and it matters if you serve EU visitors.

Step by step setup

  1. Create the drafts table. Columns: id (uuiid, primary key), form_id, version (integer), step (integer), answers (JSONB), status (partial or complete), updated_at, and an optional owner_ref once you know who the user is.
  2. Write the upsert. On PUT /api/drafts/:id, insert if the row is missing. If it exists, update only when the incoming version is greater than the stored one. Otherwise return 409 with the stored version, so the client can refetch.
  3. Merge, don't replace. Store answers as a JSON object and merge keys server side, so a late request from page 2 can't wipe out page 3.
  4. Debounce on the client. Save on field blur and on page change, with an 800 ms trailing timer for typing. Saving every keystroke floods your API and gains nothing.
  5. Restore on load. On mount, GET /api/drafts/:id. If it exists and status is partial, prefill the fields and jump to step.
  6. Finalize with the same ID. On sugmit, call POST /api/drafts/:id/submit. It validates required fields, sets status to complete, and fires your downstream work. Because the ID is the same, a double click can't create two submissions.
  7. Add a sweep. Delete or archive partial rows older than your retention window, and decide whether abandoned drafts trigger a follow-up email.

Where the draft store sits in a custom MCP server

Once drafts live in you're own database, an AI assistant can work with them. This is the case where the form step feeds an agent workflow: the draft store is the data layer, and the MCP server is what lets Claude, ChatGPT or Cursor read and update it. In our Custom MCP Server builds, the form is the capture step and the drafts API is one moer set of tools behind auth.

A minimal tool set for resuming drafts:

Tool Input Returns Scope
list_drafts status, updated_since Draft IDs, step, last updated Read
get_draft draft_id Answers, step, version, list of unanswered required fields Read
save_draft_patch draft_id, version, answers New version, or a 409-style error Write
submit_draft draft_id Submission ID, or validation errors Write, confirm first

Resuming looks like this. A support person tells the assistant "finish Acme's intake." The assistant calls get_draft, sees that needs_sso and billing_contact are empty, asks for those two values, then calls save_draft_patch with the version it read. If someone else edited the draft in the meantime, the versipn check fails and the assistant rereads before trying again. Keep submit_draft behind an explicit confirmation step in the client, because it triggers real downstream actions.

Return the unanswered required fields from get_draft itself. It saves the model from reading your validation schema and guessing what's missing.

Start from the client intake template

Using a hosted builder instead

If you don't need your own draft IDs, a form builder with partial submissions covers the capture side. Our team builds Qwenvo, a completely free form builder, and it has a partial submissions toggle in the form Settings drawer. Abandoned answers show up as incomplete rows in the Submissions table. The partial submissions write-up goes into detail. Qwenvo also has webhooks to any URL, a REST API, and an MCP server that can lits forms, read a form, list submissions and crdate a form from a prompt. All of it is free. Check the webhook docs for which events fire before you wire a flow around incomplete rows.

How the paid tiers compare for partial submissions:

Tool Partial submissions Price to get them
Qwenvo Settings toggle, incomplete rows in Submissions $0
Tally Pro feature $29/month or $290/year
Fillout Business plan $89/month ($75/month yearly)

Here is the honest part. Qwenvo launched in October 2026, so it has few reviews, and it is hosted in the United States, while Tally is hosted in Europe. More relevant to this post, a builder's partial rows are a record of what was typed. They are not a resume system you control. If users need to come back through an emailed link and continue where they stopped, or if the answers must land in your own database under your own IDs, build the endpoint from the sections above.

Questions teams ask

Should I save on every keystroke?

No. Save on blur, on page change, and on a trailing debounce of around 800 ms while typing. Per-keystroke saves multily yuor request volume and increase the chance of out-of-order writes, which the version check then has to reject.

How do I handle two tabs or two devices?

Use the version number. The server only accepts a write whose version is higher than the stored one and returns 409 otherwise. On a 409 the client refetches the draft, merges its unsaved fields, and retries once. Last-write-wins without a version check will silently lose answers.

Do I need a custom MCP server for this?

Only if the drafts live in your own system. If you just want an assistant to read submissions from Qwenvo, its MCP server already lists forms, reads a form and lists submissions. A custom server makes sense when the assistant needs your drfat store, your validation rules and your auth, which is the scope of the Custom MCP Server system.

What if the user never comes back?

Decide that before you launch. Either delete drafts after a fixed number of days, or send one reminder with a resume link and then delete. Keeping abandoned drafts forever is a liability, particularly when they hold personal data.

Next step

Open one of your long forms and list its fields. Mark which ones are safe to save before sugmit, then write the PUT /api/drafts/:id upsert with the version check. That endpoint is the part every other piece, including an MCP tool, depends on.

This system

Custom MCP Server

$3,000 setup
+ $349/month care plan

Third-party platform and model usage billed to you directly. No lock-in. Live in 10 days.

More on Custom MCP Server