TURN A KEAP TAG INTO A BUILT CLICKUP PROJECT
A tag gets applied, and thirty seconds later the client's whole ClickUp structure exists: space, list, board, and a first task carrying the discovery notes and a link back to their CRM record. Nobody clicked anything.
THE GAP BETWEEN SOLD AND STARTED
A deal closes. Somewhere between the CRM knowing that and the work actually beginning, a human has to open a project tool, make a space, name it after the client, add a list, add a board, create the first task, and copy the discovery notes across from wherever they live.
It takes ten minutes and it always happens late. It happens on a Monday when you get to it, or the evening you remember, or not at all, and then someone starts work with no notes because the notes are in an email.
None of that is judgement. Every one of those steps is mechanical, and mechanical work that only happens when a human remembers is work that mostly does not happen.
THE TAG IS THE TRIGGER
If you followed guide 2, the recap already landed on the contact and a tag got applied. That tag is not a record. It is an event, and this is the thing that subscribes to it.
In Keap, a tag applied is an automation trigger, and an automation can POST anywhere. So: tag fires, HTTP POST to your endpoint, with a shared secret header and just enough to identify the client.
POST /api/clickup-onboard
x-webhook-secret: <shared secret>
{
"company_name": "Acme Co", // names the Space
"first_name": "Jane", // fallback name when there is no company
"last_name": "Doe",
"contact_id": "12345" // so the task can link back, and we can read the notes
}
FIND OR CREATE, AT EVERY STEP
This is the whole design and it is worth more than the rest of the guide combined.
The naive version creates a space, then a list, then a task. It works exactly once. Then the tag gets re-applied, or someone re-runs the automation, or a client comes back for a second project, and now there are two spaces called Acme Co and nobody knows which one has the work in it.
Look before you create. Every time.
// Reuse a Space matching EITHER the company or the contact name.
const { spaces } = await cu("GET", `/team/${TEAM_ID}/space?archived=false`);
let space = null;
for (const candidate of [companyName, personName].filter(Boolean)) {
space = spaces.find((s) => sameName(s.name, candidate)); // trim + lowercase
if (space) break;
}
if (!space) space = await cu("POST", `/team/${TEAM_ID}/space`, { name: spaceName });
// then the same shape again for the list, and again for the board view
Matching on both the company name and the person's name matters more than it looks. Half your clients are a company and half are a person who is also the company, and the CRM does not always know which. Checking both names before creating is the difference between one space per client and two.
A webhook you cannot safely re-run is a webhook you will be afraid of. And being afraid of your own automation is worse than not having it: you stop trusting it, you check its work, and then you are doing the job manually with extra steps. Idempotency is not a nicety, it is the thing that lets you leave it alone.
THE AUTH GOTCHA
This one costs everybody an hour exactly once, so here it is for free.
// ClickUp personal tokens go in RAW. No "Bearer", no scheme, nothing.
headers: { Authorization: token }
// NOT this, which is what every other API this month wanted:
// headers: { Authorization: "Bearer " + token }
You will write Bearer out of muscle memory, get a 401, assume the token is wrong, regenerate it, get another
401, and start reading the docs from the top. The token was fine the whole time.
THE VIEW THAT IS NOT IN VIEWS
Ask ClickUp for a list's views and you get back a views array. Sensible. But the default views that every
list already has are not in it. They are in required_views, a separate object.
So a naive "does a board view exist?" check looks in views, finds nothing, creates a board, and now the list
has two: the default one nobody asked about and yours. Check both.
const pool = [];
if (Array.isArray(data.views)) pool.push(...data.views);
if (data.required_views) { // the defaults live here, not in views
for (const v of Object.values(data.required_views)) {
Array.isArray(v) ? pool.push(...v) : pool.push(v);
}
}
const board = pool.find((v) => v && String(v.type).toLowerCase() === "board");
FOLD THE CRM CONTEXT INTO THE TASK
An empty task called "Onboarding" helps nobody. The task should carry the discovery notes, so read them back out of the CRM at task build time and fold them into the body, with a link to the contact record underneath.
Two rules make this safe.
Never build the JSON by hand. The notes are multi-line free text written by a human or an AI. They contain
newlines, quotes, apostrophes, and eventually an emoji. Put the raw string on an object property and let
JSON.stringify serialise the payload. Every escape is handled and you never think about it again. Hand
assembled JSON works until the first person types "don't".
Link back to the contact. Whoever opens this task next will want the customer record, and if finding it takes three clicks they will stop bothering. One URL in the task body closes the loop in both directions.
DECIDE WHAT THE JOB ACTUALLY IS
Once the ClickUp structure exists, the job is done. Everything after that, the confirmation note back to the CRM, the "onboarded" tag, is bookkeeping.
So let it be best effort. If the note fails to write, log it and return success anyway.
Returning an error because the bookkeeping failed invites a retry, and the retry recreates nothing (you built it find or create) while telling everyone something is broken when the actual work succeeded. Be precise about what success means, and let the rest be quiet.
Quiet is not the same as invisible, though. Log the failure. A best effort write that fails forever with nobody watching is just a bug with good manners.
WHAT YOU END UP WITH
- A call gets booked, and the CRM knows exactly who booked it. Guide 1.
- The call happens, the AI writes the recap, and it lands on the contact record. Guide 2.
- That tags the contact, which builds the project, with the notes already in it. This guide.
- Somebody opens ClickUp on Monday and the work is there, set up, with context, waiting.
Nobody typed anything. That is the entire point, and none of the three pieces is difficult on its own.
COMMON QUESTIONS
Bearer. Personal API tokens go in the Authorization header raw, with no
prefix. It is the single most common ClickUp integration mistake and it looks exactly like a bad token.JSON.stringify on the payload. Never concatenate
JSON strings by hand. The first apostrophe will end you.Booking, recap, CRM, project built, nobody touching anything. We have been connecting Keap to the rest of the stack since 2010. If the gap between sold and started is where your week goes, that is a conversation worth having.
Book a Free Consult