The problem: form data lives in email, work lives in ClickUp
Here's the actual situation most teams hit. Your contact form on the marketing site fires off an email to a shared inbox. Someone reads it, copies the name, pastes it into ClickUp, copies the email, pastes that, retypes the message into the task description, then forgets to tag the right list. Multiply that by every form submission, every day, forever.
The fix is a webhook. splitforms catches the form submission, fires a POST to a URL of your choosing, and that URL turns the payload into a ClickUp task with the right title, description, custom fields, priority, and status. No copy-paste. No missed leads. The submission shows up in your team's ClickUp inbox roughly 2 seconds after the visitor hits Send.
This guide covers two paths. Path one is the developer route: splitforms webhook directly to the ClickUp REST API. Path two is the no-code route through Make or Zapier. Both work. The developer route is free and faster. The no-code route is easier if you've never touched an API.
What you need before you start
Five things, in order:
- A splitforms account with at least one form set up. If you don't have one, grab a free access key — 1,000 submissions/month free, webhooks included on the free tier.
- A ClickUp workspace where you have permission to create tasks and generate an API token.
- A ClickUp Personal API Token (starts with
pk_). Get it from Settings → Apps → API Token. - The List ID of the ClickUp List you want submissions to land in. You can read it from the URL of the List in your browser — the trailing 9-digit number.
- A place to host a webhook handler. A Vercel function, a Cloudflare Worker, a Next.js API route, a Deno Deploy script — anything that can receive a POST and make an outgoing fetch. If you don't have one, skip to the Make/Zapier path further down.
Read the send form data to webhook guide if you haven't configured splitforms webhooks before — that post covers the dashboard setup, signature verification, and retry behavior.
Path 1: splitforms webhook → ClickUp REST API (recommended)
This is the path I recommend for anyone who can write or paste 30 lines of JavaScript. It's free, fast, transparent, and you own the integration end-to-end. No middleman, no monthly subscription, no surprise rate limits.
1. Configure the splitforms webhook
In the splitforms dashboard, open your form, click Integrations → Webhooks → Add webhook. Paste the URL of your handler (we'll write it next) and save. Now every successful submission fires a POST with this rough shape:
POST https://your-app.com/api/clickup-webhook
Content-Type: application/json
X-Splitforms-Signature: sha256=<hmac>
{
"id": "sub_8f2a1c0e",
"form_id": "frm_marketing_contact",
"submitted_at": "2026-05-11T18:23:11Z",
"data": {
"name": "Priya Shah",
"email": "priya@example.com",
"company": "Acme Co",
"message": "Need a quote for 50 seats",
"budget": "10k-25k"
},
"files": []
}2. Write the handler
Here's a complete Next.js API route. Drop it at app/api/clickup-webhook/route.ts and set CLICKUP_TOKEN and CLICKUP_LIST_ID in your environment.
// app/api/clickup-webhook/route.ts
export async function POST(req: Request) {
const payload = await req.json();
const d = payload.data ?? {};
const task = {
name: `New lead: ${d.name ?? "Unknown"} — ${d.company ?? ""}`.trim(),
description: [
`**Email:** ${d.email ?? "—"}`,
`**Company:** ${d.company ?? "—"}`,
`**Budget:** ${d.budget ?? "—"}`,
"",
"**Message:**",
d.message ?? "",
"",
`_Submitted via splitforms · ${payload.submitted_at}_`,
].join("\n"),
status: "to do",
priority: d.budget === "10k-25k" ? 2 : 3, // 1=urgent 2=high 3=normal 4=low
tags: ["inbound-lead", "splitforms"],
};
const res = await fetch(
`https://api.clickup.com/api/v2/list/${process.env.CLICKUP_LIST_ID}/task`,
{
method: "POST",
headers: {
Authorization: process.env.CLICKUP_TOKEN!,
"Content-Type": "application/json",
},
body: JSON.stringify(task),
}
);
if (!res.ok) {
const err = await res.text();
console.error("ClickUp error", res.status, err);
return new Response("ClickUp create failed", { status: 502 });
}
return Response.json({ ok: true });
}That's the whole thing. Deploy it, paste the public URL into the splitforms webhook config, and submit a test through your form. You should see a new ClickUp task in your target List within 2–3 seconds.
Map form fields to ClickUp custom fields
The task description is fine for unstructured prose, but if you want filtering, sorting, and reporting in ClickUp, you need custom fields. ClickUp's API treats these as a separate concern from the task body — and this is where most people get tripped up.
1. Find the custom field UUIDs
Custom fields are referenced by UUID, not name. Run this once and save the IDs:
curl -H "Authorization: pk_XXXXX" \
https://api.clickup.com/api/v2/list/<LIST_ID>/field
# Response:
# {
# "fields": [
# { "id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "Email",
# "type": "email" },
# { "id": "650e8400-...",
# "name": "Company",
# "type": "short_text" },
# { "id": "750e8400-...",
# "name": "Budget",
# "type": "drop_down",
# "type_config": { "options": [
# { "id": "abc-123", "name": "Under 10k" },
# { "id": "def-456", "name": "10k-25k" },
# { "id": "ghi-789", "name": "25k+" }
# ]}}
# ]
# }2. Send custom fields in the create payload
Once you have the UUIDs, attach them to the task at creation time:
const task = {
name: `New lead: ${d.name}`,
description: d.message,
status: "to do",
custom_fields: [
{ id: "550e8400-e29b-41d4-a716-446655440000", value: d.email },
{ id: "650e8400-...", value: d.company },
// dropdown fields take the OPTION id, not the option name:
{ id: "750e8400-...", value: "def-456" /* 10k-25k */ },
],
};Dropdown, label, and status-type custom fields take the option ID, not the option text. This is the single most common ClickUp API mistake. If you pass the human-readable name, the API returns 200 but the field stays empty. Map your form's values to ClickUp's option IDs in a small lookup object at the top of your handler.
ClickUp's quirky required fields and edge cases
A few ClickUp behaviors that aren't in the official docs but you'll hit on day one.
- Required custom fields block task creation. If a List has any custom field marked Required, the API will reject your create call with 400 unless you include that field. The error message is usually clear (
Required field missing) but the fix isn't: either send a default value from your handler or un-require the field in ClickUp. - Priority is an integer, not a string. 1 = Urgent, 2 = High, 3 = Normal, 4 = Low. If you send
"high", ClickUp ignores it. - Due dates are Unix milliseconds. Not ISO strings, not seconds — milliseconds.
Date.now() + 86400000for a 24-hour SLA. - Assignees take user IDs, not emails. Hit
GET /team/<team_id>/memberonce and hardcode the user IDs for your intake team. - Tags must already exist on the Space. Tags you reference must be pre-created at the Space level, otherwise they're silently dropped. Create your
inbound-leadandsplitformstags manually first. - Status names are case-insensitive but spelling-sensitive.
To Do,to do, andTO DOall work.Tododoes not.
Most people don't realize the API rate limit is 100 requests per minute per token. If you're creating tasks, attaching files, and setting custom fields in separate calls per submission, that's 3 requests each, so you cap out at ~33 submissions per minute per token. For higher volumes, batch your custom fields into the create call (as shown above) and use a second token for attachment uploads.
Attaching the form's file uploads
If your form accepts file uploads (resume, logo, screenshot of the bug), splitforms stores the files and includes their URLs in the webhook payload. To attach them to the ClickUp task you have to download from splitforms and re-upload to ClickUp as multipart form-data.
async function attachFiles(taskId: string, files: Array<{url: string, name: string}>) {
for (const file of files) {
const blob = await (await fetch(file.url)).blob();
const fd = new FormData();
fd.append("attachment", blob, file.name);
await fetch(
`https://api.clickup.com/api/v2/task/${taskId}/attachment`,
{
method: "POST",
headers: { Authorization: process.env.CLICKUP_TOKEN! },
body: fd,
}
);
}
}Call this after the task is created with the returned task.id. ClickUp's attachment endpoint accepts files up to 100 MB per upload on paid plans, less on Free Forever. If a visitor uploads a 200 MB video, your handler should reject early or stream-truncate.
Path 2: splitforms → Make or Zapier → ClickUp (no-code)
If you don't want to host a webhook handler, both Make (formerly Integromat) and Zapier have native ClickUp modules. The flow is identical in both:
- In Make/Zapier, create a new scenario/Zap with a Webhook trigger. Both platforms generate a unique URL.
- Paste that URL into your splitforms form's webhook config.
- Trigger one test submission so the platform captures the payload schema.
- Add a ClickUp → Create Task step. Authenticate with your ClickUp account (OAuth).
- Map fields visually: drag
data.nameinto Task Name,data.emailinto a custom field, etc. - Activate the scenario.
The big win is the visual mapper — you see the splitforms payload on the left and the ClickUp task on the right, and you connect them with arrows. The downside is cost. Zapier's entry tier (Free) is 100 tasks/month and one Zap. Their Starter is $19.99/month for 750 tasks. Make is more generous: the free tier is 1,000 operations/month, which translates to roughly 500 form-to-ClickUp runs since each scenario uses ~2 ops.
If you're processing more than a hundred submissions a month, Path 1 (your own handler) saves you $20–60/month indefinitely. If you're processing fewer than 50 and you don't want to think about deployments, Make's free tier is fine.
Status mapping and routing logic
The naive setup dumps every submission into one List with status to do. That works for tiny teams. Once you have real volume, you want routing: hot leads to one List, support questions to another, recruiter spam to a triage column.
The cleanest pattern is one splitforms form per ClickUp List. Your marketing site has /contact pointing at the Sales form, /support pointing at the Support form, /careers pointing at the Recruiting form. Each form has its own webhook URL. Each webhook handler hardcodes the destination List ID. Zero conditional logic, zero bugs.
If you must route from a single form, branch in the handler by a form field. For example, a category select-dropdown:
const LIST_BY_CATEGORY: Record<string, string> = {
"sales": "901234567",
"support": "901234568",
"billing": "901234569",
"partners": "901234570",
};
const listId = LIST_BY_CATEGORY[d.category] ?? LIST_BY_CATEGORY["sales"];
await fetch(`https://api.clickup.com/api/v2/list/${listId}/task`, ...);For status, set status based on urgency signals from the form. A budget field over $10k? Land in qualified not to do. A message under 30 characters? Probably spam — land in triage instead. Use splitforms' AI spam score (included free) to route low-confidence submissions to a quarantine List.
How to test and verify the integration works
Before you trust the pipeline with real leads, run through this checklist:
- Send a test from the splitforms dashboard. Each form has a Test Webhook button that fires a synthetic payload. You should see the ClickUp task created within 5 seconds.
- Submit a real form on your live site. Use your own email so you also get the notification. Confirm the ClickUp task name and description match the submission.
- Check every custom field. Open the created task in ClickUp and verify all custom fields populated. Empty fields almost always mean a UUID typo or a dropdown option ID mismatch.
- Test a file upload. If your form accepts files, submit one and verify the attachment shows up on the ClickUp task with the correct filename.
- Force a failure. Temporarily change the List ID to a fake value. Submit. Confirm splitforms shows a 502 in the webhook delivery log and that you have an alerting plan. Then revert the ID.
- Test the retry. splitforms retries failed webhooks 3 times with exponential backoff. Take your handler offline for a minute, submit, bring it back — the submission should arrive after the retry succeeds.
The splitforms webhook delivery log is at Dashboard → Webhooks → Deliveries. It shows every POST, the response status, the response body, and a Replay button. If something looks wrong, replay the last delivery while watching your handler logs — that's the fastest debug loop. Pair this with the broader webhook setup guide for verification and signing patterns.
Why splitforms is the right form backend for ClickUp pipelines
Form backends are interchangeable on the happy path. They diverge on edge cases, and the edges matter when your sales team relies on the pipeline.
- Webhooks on the free tier. Most competitors paywall webhooks. Formspree wants $10/month for them. splitforms includes them on the free 1,000-submission tier.
- AI spam scoring included. Each submission carries a spam score so you can route low-confidence ones to a triage List instead of pinging your sales team at 3 AM about a crypto scam.
- Signed webhooks. The
X-Splitforms-Signatureheader lets you verify the request actually came from splitforms before you trust the payload. Required for production. - Retry on failure. If ClickUp is having an outage, splitforms retries your handler 3 times with backoff so you don't lose leads to transient API errors.
- Delivery log with replay. Every webhook delivery is logged with payload and response. Replay any failed one with one click.
- Pricing that doesn't scale punishingly. Free tier is 1,000 submissions/month. Pro is $5/month for 5,000. The $59 four-year plan averages out to $1.23/month if you commit. No per-zap, per-task, per-anything pricing.
If you're still picking a backend, see the best free form backends 2026 roundup. If you're on Formspree and want to switch, the 5-minute migration guide covers the cutover. Framework users: Next.js, React, Astro.
Ship it
Pick a path. If you can host a 30-line function, take Path 1 — it's free forever and you own the integration. If you can't, take Path 2 through Make (cheaper than Zapier at the free tier). Either way:
- Get a splitforms access key (free, 1,000 submissions/month).
- Add the webhook URL to your form in the splitforms dashboard.
- Either deploy the handler from Path 1 or wire up Make/Zapier from Path 2.
- Send a test submission. Confirm the ClickUp task appears.
- Wire up custom fields one by one (don't try to map all 15 at once — one at a time, test each).
Reference material: /docs for the splitforms request/response contract, /api-reference for the webhook envelope and signature scheme, /faq for plan and security questions, and /blog for more integration patterns. Need a starter form? Grab a free contact form template pre-wired to splitforms.
FAQ
Where do I get my ClickUp API token?
Open ClickUp, click your avatar bottom-left, choose Settings, then Apps in the left rail. You'll see a Personal API Token at the top — click Generate or Copy. The token starts with `pk_` and never expires unless you regenerate. Personal tokens act as your own user, so any task you create shows up as authored by you. For team automations, generate an OAuth app instead so the tasks aren't tied to one person's account.
How do I find a ClickUp List ID for the API?
Open the List in ClickUp's web app and look at the URL. The pattern is `/v/li/<LIST_ID>` or `/li/<LIST_ID>`. That number is the List ID — it's a 9-digit integer. You can also call `GET https://api.clickup.com/api/v2/team/<team_id>/space` to walk the tree (team → space → folder → list) if you're building tooling that discovers lists. Either way, hardcode the ID in your webhook handler — it doesn't change unless someone deletes and recreates the list.
Why does my ClickUp task create but the custom fields are empty?
Custom fields aren't part of the main `POST /list/<id>/task` payload. You set them in a separate `POST /task/<task_id>/field/<field_id>` call after the task is created, OR you include a `custom_fields` array in the create payload with `{id, value}` objects. The most common bug is sending the field name (`Email`) instead of the field's UUID. Always look up the UUID via `GET /list/<id>/field` once, cache it in your code, and reference by UUID.
Do I need Zapier or Make, or can I do this with just a webhook?
If you can run any backend code — a Next.js API route, a Cloudflare Worker, a Vercel function, a tiny Express server — you don't need Zapier or Make. The splitforms webhook posts JSON to your URL, you transform it, you call the ClickUp API. That's free forever and roughly 30 lines of code. Zapier and Make exist for non-developers and for teams that want a visual audit trail. Pick the path that matches your skill, not the marketing.
How do I attach the form's file uploads to the ClickUp task?
Two-step. First create the task with `POST /list/<id>/task`. Then for each file URL in the splitforms submission, call `POST /task/<task_id>/attachment` with `multipart/form-data` containing the file binary. You'll need to fetch the file from splitforms' storage URL, stream it into a FormData object, and POST it. ClickUp returns an attachment object with a CDN URL you can ignore — the file is already linked to the task.
What ClickUp status should new form submissions land in?
Whatever your team's intake status is — usually `to do`, `open`, `new`, or a custom `triage` status. Pass it as the `status` string in the create-task payload. The status name must match the List's status set exactly (case-insensitive, but spelling matters). If you omit `status`, ClickUp uses the List's default first status. Tip: don't hardcode `to do` — many ClickUp Lists rename it. Make the status configurable via env var.
How do I avoid duplicate tasks if splitforms retries the webhook?
splitforms retries on 5xx but not on 2xx, so the simplest fix is: respond 200 as soon as you've queued the work. If you're worried about double-submission within the same retry window, dedupe on the submission ID (every splitforms payload has a unique `id`). Store the last 1,000 IDs you've processed in a small KV (Cloudflare KV, Upstash Redis, or even an in-memory Map for low volume) and skip any ID you've already seen.
Can I create the ClickUp task in a specific Folder or Space?
Tasks live inside Lists, not directly in Folders or Spaces. If you want submissions organized by Folder, create one List per Folder and route by the form's intent. For example: a sales form posts to the `Sales Inbox` List inside the `Sales` Folder; a support form posts to the `Support Tickets` List inside `Support`. The splitforms dashboard lets you set per-form webhooks, so you can have one form per List with zero conditional logic in your handler.