Lead capture form6 min read
Build a quote request form without file uploads
A request-a-quote form that collects briefs as shared links instead of uploads, puts a due date on every request, and keeps link-heavy leads out of spam.
Ask for the brief as a link to a shared folder in a url field, because Ikiform refuses file uploads with 422 files_not_supported. Keep service and budget as fixed choices so every request arrives in the same shape, and promise a turnaround in the success message. Then send each request to the team with its due date attached, so the promise gets kept.
What's different about a quote request
A quote request comes with material: a brief, a floor plan, a spreadsheet of parts, last year's design. The usual answer is a file input. That doesn't work here, and it is worth knowing why before you design around it.
File uploads aren't supported yet. A post with a file gets 422 files_not_supported. The whole post is refused, not only the file, so a visitor who attaches a PDF loses everything they typed unless your form shows the error and keeps their input. A shared link avoids the problem and has advantages of its own: the client can add files after sending, there is no size limit, and nothing large sits in your inbox.
- With a file input, the visitor attaches brief.pdf and the browser sends a multipart post.
- Ikiform refuses the whole post with 422 files_not_supported.
- Nothing is stored, so no email or webhook goes out.
- With a link field, the visitor pastes a link to a shared folder.
- Ikiform checks that it is a valid https URL and stores the request.
- The link arrives in the notification email and in chat, where anyone on the team can open it.
The fields
The quote request template has the full field set with phone, company and timeline. The version below trims it and adds the link field. Try an http: link, or a bare drive.google.com link, to see how the url field answers.
Preview
Runs Ikiform's validator in your browser. Nothing is sent.
files_url is a url field with https_only, so a bare drive.google.com/... is stored as https://drive.google.com/... and an http: link is refused.| Field | Why it's shaped this way |
|---|---|
files_url | One link to a folder, not one field per file. The client controls what's inside and can add to it later |
service | Decides who prices it. Values stay stable for routing even if you rename the labels |
budget | Optional, with a Not sure yet choice that sends nothing. A required budget field loses leads |
details | Required, at least 20 characters. A quote needs scope, and this is where it comes from |
The form
Plain HTML. type="url" brings up the right keyboard on phones, and the help text says how to share the folder, which saves a round trip when the first link turns out to be private.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Work email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="service">What do you need?</label>
<select id="service" name="service" required>
<option value="design">Design</option>
<option value="development">Development</option>
<option value="both">Design and development</option>
</select>
<label for="budget">Budget</label>
<select id="budget" name="budget">
<option value="">Not sure yet</option>
<option value="under_5k">Under $5,000</option>
<option value="5k_20k">$5,000 to $20,000</option>
<option value="over_20k">Over $20,000</option>
</select>
<label for="files_url">Link to your files</label>
<input id="files_url" name="files_url" type="url" inputmode="url" placeholder="https://drive.google.com/..." aria-describedby="files-help" />
<p id="files-help">A shared folder with the brief, mockups or specs. Set it so anyone with the link can view.</p>
<label for="details">What should the quote cover?</label>
<textarea id="details" name="details" rows="5" minlength="20" required></textarea>
<input type="hidden" name="_redirect" value="https://example.com/quote/thanks" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Request a quote</button>
</form><input type="file"> you inherited from an old form. An empty one is skipped. With a file chosen, a multipart or FormData post fails as a whole, and a plain urlencoded form sends only the file's name. Either way the file never arrives.Links and the spam score
A quote form is the one place where real leads send links on purpose, and links are a spam signal. Three or more links across all fields add 2 to the score. A url field is stored as a full https:// link, so it counts.
| The request contains | Score | Result |
|---|---|---|
| The folder link | 0 | Received |
The folder link and two more links in details | +2 | Received |
| The same three links, sent again within 24 hours with the same fields | +2 and +3 | Spam, at 5 |
The third row is a real person who thought the first submit failed. That's why the form should have one link field, not three, and why the success state must be unmistakable. Check the Spam filter weekly; marking a request as not spam sends its email and webhook then.
Put a due date on every request
The success message promises a quote within two working days. This Cloudflare Worker turns that promise into a date the team can see. It verifies the webhook, works out the due date from created_at, and posts the request to a quotes channel with the files link:
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
function addWorkingDays(from: Date, days: number) {
const date = new Date(from);
while (days > 0) {
date.setUTCDate(date.getUTCDate() + 1);
const day = date.getUTCDay();
if (day !== 0 && day !== 6) days -= 1;
}
return date.toISOString().slice(0, 10);
}
export default {
async fetch(request: Request, env: { IKIFORM_WEBHOOK_SECRET: string; SLACK_QUOTES_WEBHOOK: string }) {
const payload = await request.text();
let event;
try {
event = await verifyWebhook({ payload, headers: request.headers, secret: env.IKIFORM_WEBHOOK_SECRET });
} catch (error) {
if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
throw error;
}
if (event.type !== "submission.created") return new Response(null, { status: 204 });
const quote = event.data.fields;
const due = addWorkingDays(new Date(event.data.created_at), 2);
const lines = [
`Quote due *${due}*: ${quote.service} for ${quote.name} (${quote.budget ?? "budget not given"})`,
quote.files_url ? `Files: ${quote.files_url}` : "No files linked. Ask for them in the first reply.",
`Reply: mailto:${quote.email}`,
];
const response = await fetch(env.SLACK_QUOTES_WEBHOOK, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ text: lines.join("\n") }),
});
if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
return new Response(null, { status: 204 });
},
};- Working days skip Saturday and Sunday in UTC. Add your holidays if you quote around them.
- A missing
files_urlproduces a reminder to ask for files in the first reply, rather than an empty line. - Answering
502when Slack fails makes Ikiform retry the same delivery, so a request isn't dropped.
Add the Worker's URL under Webhooks, copy the whsec_ secret into IKIFORM_WEBHOOK_SECRET, and store the Slack URL as a secret. The Slack guide covers creating the incoming webhook.
Check it works
- Submit with a Google Drive or Dropbox folder link. Open the link from the notification email in a private window to confirm it's viewable.
- Submit with an
http:link. The post is refused with422and the message namesfiles_url. - Search your templates for
type="file"and remove every match from quote forms that post to Ikiform. - Check the quotes channel. The message shows the due date two working days out and the files link.
- Press Reply on the notification email. It writes to the address on the form.
Gotchas
- Private links. The most common failure is a link only its owner can open. Say how to share it in the help text, and ask for access in the first reply instead of waiting for it.
- Clients with files and no folder. Tell them to reply to your first email with attachments. Reply-to puts their files in a normal email thread with your team.
- Long scopes. Notification emails show values up to 500 characters. The full
detailstext is in the dashboard and the webhook. - No acknowledgement email. Ikiform doesn't email the person who fills in the form. The on-page message is the acknowledgement, so make the turnaround concrete.
Questions
Not yet. A post that contains a file is refused with 422 files_not_supported. Ask for a link to the file instead, such as a shared drive URL.
Ask for one link to a shared folder in a url field, and say in the help text that the folder must be viewable by anyone with the link. The client can add files to the folder after sending the request.
Three or more links add 2 to the spam score, and the same fields sent again within 24 hours add 3, which reaches the threshold of 5. Keep one link field, and mark the request as not spam to send its email and webhook.
Yes. Leave required off and add a first option with an empty value, such as Not sure yet. An empty choice sends nothing, and Ikiform stores the request without a budget.
Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.