Inbound lead → enriched CRM row
HMAC-verified webhook → Claude enrichment → HubSpot upsert with score.
Thanks for the note — sharing a 2-min Loom that shows the exact locker rotation flow you asked about, plus a 14-day trial link scoped to your workspace only.
Raw form submissions arrive without company size, industry, or fit score — your AEs waste the first ten minutes researching every lead by hand.
Every lead lands in HubSpot already enriched (industry, headcount, ICP fit 0–100). AEs work the queue top-down.
Ingredients & skills
- ANTHROPIC_API_KEY
- HUBSPOT_TOKEN
- LEAD_WEBHOOK_SECRET
- Anthropic
- HubSpot
- fetch-mcp
How it works
A public `/api/public/lead` endpoint receives marketing-form posts, verifies the HMAC signature, asks Claude to enrich and score the lead, and upserts into HubSpot.
1 — Expose the public route
Anything under `/api/public/*` skips auth on the published site — perfect for inbound webhooks. Always verify before processing.
import { createFileRoute } from "@tanstack/react-router";
import { createHmac, timingSafeEqual } from "node:crypto";
export const Route = createFileRoute("/api/public/lead")({
server: { handlers: { POST: async ({ request }) => {
const sig = request.headers.get("x-signature") ?? "";
const body = await request.text();
const expected = createHmac("sha256", process.env.LEAD_WEBHOOK_SECRET!).update(body).digest("hex");
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return new Response("bad sig", { status: 401 });
const lead = JSON.parse(body);
const enriched = await enrich(lead);
await upsertHubspot(enriched);
return new Response("ok");
} } },
});2 — Enrichment prompt
Claude returns strict JSON. We force the schema via a tool definition.
export async function enrich(lead: { email: string; company?: string }) {
const r = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "x-api-key": process.env.ANTHROPIC_API_KEY!, "anthropic-version": "2023-06-01", "content-type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 400,
tools: [{ name: "enriched", input_schema: { type: "object", properties: { industry: { type: "string" }, headcount: { type: "integer" }, icp_fit: { type: "integer", minimum: 0, maximum: 100 } }, required: ["industry", "headcount", "icp_fit"] } }],
tool_choice: { type: "tool", name: "enriched" },
messages: [{ role: "user", content: `Enrich this lead: ${JSON.stringify(lead)}` }],
}),
}).then((r) => r.json());
return { ...lead, ...r.content[0].input };
}3 — Upsert into HubSpot
Idempotent on email. ICP fit becomes a custom property.
async function upsertHubspot(lead: any) {
await fetch("https://api.hubapi.com/crm/v3/objects/contacts/" + encodeURIComponent(lead.email) + "?idProperty=email", {
method: "PATCH",
headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`, "content-type": "application/json" },
body: JSON.stringify({ properties: { email: lead.email, industry: lead.industry, headcount: lead.headcount, icp_fit: lead.icp_fit } }),
});
}The button above runs the same command with your saved config. This is the raw CLI form.
npx claudeloops deploy lead-enricher