A Webflow Cloud route can receive each form submission and create the person in Pipedrive, so nobody re-keys a lead and weekend submissions reach the CRM the moment they arrive.
Webflow form leads often reach Pipedrive the slow way. Someone on sales copies the name and email by hand, or an automation platform sits in the middle, with your Pipedrive token stored in its field-mapping screen.
Instead, you can use one Next.js Route Handler deployed on Webflow Cloud. It receives Webflow's form submission webhook and, once a shared secret checks out, creates the person through Pipedrive's v2 API in a single call. Let’s see how.
What do you need to send form leads to Pipedrive in Webflow?
You need these six things:
- Webflow form: A published form whose field names are set in the Designer, because those names become the keys the handler maps to Pipedrive.
- Webflow Cloud environment: One environment on that site; the free Starter plan covers a mount without a custom domain, and a custom-domain mount requires Premium or higher.
- Pipedrive API token: A Pipedrive user whose personal API token you can copy, plus your company domain, the
yourcompanypart ofyourcompany.pipedrive.com. - Node.js and Next.js: Node.js 22 or later and Next.js 15 or higher with the App Router, per Webflow's bring-your-own-app docs.
- npm: Webflow Cloud supports only npm, so
pnpmandyarninstall steps fail; every command in this guide uses npm or npx. - Shared secret: A random string generated on your machine with
openssl rand -hex 32, which the handler expects as the key in the webhook URL.
With those in hand, the order is Pipedrive first (so you can test the API call in isolation), then the app, then the Webflow webhook that feeds it.
5 steps to send Webflow form leads to Pipedrive in Webflow Cloud
The build is one POST Route Handler on Webflow Cloud that sits between a Webflow form submission webhook and Pipedrive's API, and the mount path decides the URL the webhook must call.
A curl request against the deployed route confirms the mount path and the Pipedrive call before any real form fires.
1. Copy your Pipedrive API token
In Pipedrive, open your profile menu, then Personal preferences, then the API tab, and copy your personal API token. Note your company domain too, because the handler calls Pipedrive's v2 persons endpoint on that domain with the token in an x-api-token header.
Pipedrive moved its v1 create-person endpoint out of support in August 2026, so older tutorials that post a flat email field to /v1/persons no longer match the current API.
The token never goes into Webflow page custom code: anything in the published page is readable by anyone who opens the source, and this token grants API access to your Pipedrive account.
At the end of this step, you have two strings: the token and your company domain, and nothing has changed in Pipedrive yet.
2. Scaffold the Next.js app for Webflow Cloud
Create a fresh Next.js project with the App Router, using npm, and leave the default configuration alone. Webflow Cloud's configuration docs state, "You don't need to add an adapter, a base path, or an output mode," so the starter project needs no changes.
Do not add export const runtime = 'edge' to any route; the OpenNext Cloudflare adapter Webflow Cloud deploys through does not support it, so the build fails.
Run these three commands to scaffold the project and confirm it serves locally:
npx create-next-app@15 webflow-pipedrive
cd webflow-pipedrive
npm run dev
You now have a project that serves the default Next.js page on localhost, with an app directory ready for app/api/pipedrive/route.ts. Keep the mount path you intend to use in mind (this build uses /leads), because the route's public URL will be that path plus /api/pipedrive once deployed.
3. Write the Route Handler that creates the Pipedrive contact
Add a POST handler at app/api/pipedrive/route.ts that checks a shared secret in the query string. Once the key passes, it reads the form fields from the webhook body and creates a person in Pipedrive, with the email and phone sent as the arrays the v2 API expects.
It returns 401 on a bad key, 500 when a Pipedrive variable is missing, 400 when the Email field is missing, and 502 when Pipedrive is unreachable or rejects the record, so the status code alone tells you which end failed. A 200 with { ok: true } means the person exists.
Route Handler code
Save this as the complete handler file:
// app/api/pipedrive/route.ts
import { NextResponse } from 'next/server';
type Fields = Record<string, unknown>;
function extractFields(body: unknown): Fields {
if (!body || typeof body !== 'object') return {};
const b = body as { payload?: { data?: Fields }; data?: Fields };
return b.payload?.data ?? b.data ?? (b as Fields);
}
function text(fields: Fields, key: string): string {
const value = fields[key];
if (typeof value === 'string') return value.trim();
if (typeof value === 'number') return String(value);
return '';
}
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}
export async function POST(request: Request) {
const key = new URL(request.url).searchParams.get('key') ?? '';
const secret = process.env.WEBHOOK_SHARED_SECRET ?? '';
if (!secret || !safeEqual(key, secret)) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
const domain = process.env.PIPEDRIVE_COMPANY_DOMAIN;
const token = process.env.PIPEDRIVE_API_TOKEN;
if (!domain || !token) {
return NextResponse.json({ error: 'pipedrive not configured' }, { status: 500 });
}
const body = await request.json().catch(() => null);
if (process.env.DEBUG_PAYLOAD === '1') {
console.log('webflow payload', JSON.stringify(body));
}
const fields = extractFields(body);
const email = text(fields, 'Email');
const name = text(fields, 'Name') || email;
const phone = text(fields, 'Phone');
if (!email) {
return NextResponse.json({ error: 'missing Email field' }, { status: 400 });
}
let response: Response;
try {
response = await fetch(`https://${domain}.pipedrive.com/api/v2/persons`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-token': token,
},
body: JSON.stringify({
name,
emails: [{ value: email, primary: true, label: 'work' }],
...(phone ? { phones: [{ value: phone, primary: true, label: 'work' }] } : {}),
}),
signal: AbortSignal.timeout(8000),
});
} catch (error) {
console.error('pipedrive unreachable', error);
return NextResponse.json({ error: 'pipedrive unreachable' }, { status: 502 });
}
if (!response.ok) {
console.error('pipedrive error', response.status, await response.text());
return NextResponse.json({ error: 'pipedrive rejected the contact' }, { status: 502 });
}
return NextResponse.json({ ok: true });
}
The field keys Name, Email and Phone are the names set on the form fields in the Designer, and they are case-sensitive; if your form names them differently, change the three text() calls to match.
Webflow's webhook docs show form fields under payload.data, keyed by each field's name, with phone numbers sometimes arriving as numbers rather than strings, which is why text() accepts both.
The DEBUG_PAYLOAD gate lets you confirm your own form's exact keys. The secret comparison checks every character instead of stopping at the first mismatch, and it is written by hand because crypto.subtle.timingSafeEqual exists on the deployed Workers runtime but not under local next dev.
After saving the file, you now have a complete handler that compiles with npm run dev and returns the 401 JSON body for a POST to the local route without a key.
4. Add the environment variables and deploy
From the site's Webflow Cloud panel, create a project linked to the repository that holds the app. Inside that project, create an environment and set its mount path to /leads. Add the four variables below in that environment's settings, then deploy.
Mark the token and the secret as secrets. Webflow's environment variables docs state that "Both secret and non-secret environment variables are available to your application's build process and to the deployed application at runtime," and secrets are redacted from build logs, so a mistyped token cannot be read back from the log; you check it by behavior instead.
Environment variable roles
Each variable has one job in the handler:
- WEBHOOK_SHARED_SECRET: The random hex string from
openssl rand -hex 32; use the same value in the environment and the webhook URL, and rotating it means changing both places. - PIPEDRIVE_API_TOKEN: The Pipedrive API token copied from Pipedrive's Personal preferences API tab, stored as a secret so it is redacted from build logs and never committed to the repository.
- PIPEDRIVE_COMPANY_DOMAIN: Your Pipedrive company domain without
.pipedrive.com, for exampleacme. - DEBUG_PAYLOAD: Set to
1for the first deploy to log the raw webhook body, then remove it so lead data stops landing in logs.
Once the deploy finishes, the handler is live at /leads/api/pipedrive with all four variables available at build and runtime.
Testing the Handler
Swap in your secret and domain, then send this request to the deployed route:
curl -i -X POST "https://www.example.com/leads/api/pipedrive?key=YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"Name":"Test Lead","Email":"test@example.com","Phone":"+1 555 0100"}'
The www.example.com host stands in for a custom domain, which needs Premium; without a custom domain, substitute the domain for your environment and keep /leads/api/pipedrive.
A 200 response with {"ok":true} and a new person named Test Lead in Pipedrive means both halves of the integration work. A 500 here means one of the Pipedrive variables is missing, a 502 points at the endpoint URL, header name or token, and a 404 points at the mount path in the URL.
5. Point the Webflow form webhook at the route
Point a Form submission webhook at https://www.example.com/leads/api/pipedrive?key=YOUR_SECRET with your own domain and secret in place to send each form submission to the handler. In the site's webhook settings, add a webhook with Form submission as its trigger, and check that the URL keeps the mount path before /api/pipedrive.
Webflow signs only webhooks created through its API, and a webhook added in site settings arrives unsigned, so the query key is the only check on the caller here. Treat it like a password because the URL carries the secret in plain text, and query strings can land in request logs along the way.
On client sites, I tend to rotate the secret when an editor with settings access leaves, which is a two-field change.
Then open the published site, fill in the form with a real-looking email, and submit it. The same name and email should appear as a person in Pipedrive. With DEBUG_PAYLOAD set, the webflow payload log line shows the envelope the handler received; if it differs from the nested payload.data shape the handler expects, adjust extractFields to match.
What causes Webflow to Pipedrive lead sync to fail?
Most failures trace to one of four causes, and the curl test's status code points to which one.
Webflow Cloud build fails on a route that compiled locally
Cause: The route contains export const runtime = 'edge', which the OpenNext Cloudflare adapter behind Webflow Cloud does not support. The word "edge" means two different things on the same docs page.
Webflow Cloud runs on Cloudflare Workers, an edge platform, and the bring-your-own-app page describes that deployment as using the Edge runtime. The same page still tells you to add the Next.js edge runtime directive to API routes, and following it produces a route that cannot build.
Fix: Delete the export const runtime = 'edge' line from every route and page file and redeploy. If you later move the secret check into middleware, keep the file named middleware.ts.
Webflow's framework customization docs state, "Node.js runtime middleware isn't supported. Only Edge runtime middleware works on the Workers runtime," and Next 16's renamed proxy.ts runs on the Node runtime with no way to opt into Edge.
Form submissions never reach the handler, and the live URL returns 404
Cause: The webhook URL is missing the mount path. Webflow Cloud injects basePath at build time, so a route written at app/api/pipedrive/route.ts is public at the mount path plus /api/pipedrive, never at the site root. A URL copied from a local next dev session, where the route answered at /api/pipedrive, 404s on the live domain.
The same mismatch bites in the other direction if you add a browser-side form on the app itself, because Webflow's docs are explicit that "Client-side fetch calls must manually include the base path."
Fix: Edit the webhook URL to include the mount segment (/leads/api/pipedrive in this build) and resubmit the form. For any client-side fetch inside the app, set NEXT_PUBLIC_BASE_PATH to the mount path as an environment variable and prefix the request with it rather than reading basePath from next.config.
A quick way to confirm the right path before touching the webhook is the curl test against the deployed route: a 401 proves the route is reachable, and only the key is wrong, while a 404 proves the path itself is off.
The deployed route returns 500 or 502, but the same request works from your laptop
Cause: The deployed app has a missing or wrong Pipedrive value. If PIPEDRIVE_API_TOKEN or PIPEDRIVE_COMPANY_DOMAIN is missing from the environment the app is deployed from, the handler returns 500 with "pipedrive not configured" and never calls Pipedrive.
A value that lives only in .env.local is absent from the deployed environment. A wrong token or domain reaches Pipedrive and fails there; the caller sees a 502, and Pipedrive's status appears in the console.error line.
Fix: Open the environment the live app is deployed from, add both variables there, and redeploy. To check the configuration without printing the token, log token.length and domain once behind the DEBUG_PAYLOAD gate; the length shows whether the secret is empty, and the domain should match your Pipedrive address. Once the error clears, remove the extra log line.
The deployed route returns 401 even though the key looks right
Cause: The key value in the webhook URL differs from WEBHOOK_SHARED_SECRET in the environment, so the comparison fails before Pipedrive is ever called. Usually, the secret was rotated in one place but not the other, or stray whitespace was picked up when pasting.
A key containing characters that need URL encoding causes the same mismatch, which the hex output of openssl rand -hex 32 avoids.
Fix: Re-copy the same secret into both the WEBHOOK_SHARED_SECRET variable and the webhook URL, redeploy, and rerun the curl test against the deployed route. A 200 confirms the two values now match; a 401 means they still differ.
What you can build next with Pipedrive and Webflow
You can extend this one handler to look up an existing person before creating one, or to add a lead or deal linked to the new person so it lands in the sales pipeline. The same webhook pattern also sends leads to HubSpot without a middle service.
For deeper customization beyond what the form webhook handles natively, Webflow's developer docs cover the form submission event and the rest of the Data API.
Frequently asked questions
Can I send Webflow form leads to Pipedrive without writing code?
Yes. Pipedrive's web forms can be embedded in a Code Embed element, which needs a paid Site plan, and automation tools such as Zapier and Make connect a native Webflow form to Pipedrive; Webflow's Pipedrive integration page lists the options. The Route Handler in this guide suits teams that want to keep the Pipedrive token in their own environment rather than in a third-party service.
What happens to a lead if Pipedrive is down when the form is submitted?
The lead isn't lost because the form webhook runs alongside Webflow's form handling, and the submission stays in the site's form submissions. The handler returns a 502 to Webflow and logs Pipedrive's status, or the network error if Pipedrive never answered. To recover, re-send the stored submission to the route with the curl test.
Will a repeat submission create a duplicate person in Pipedrive?
Yes. The handler creates a new person on every call, so the same email submitted twice produces two records. To avoid that, call Pipedrive's person search endpoint, GET /api/v2/persons/search with fields=email and exact_match=true, before creating, then update or skip the match.
What happens if an editor renames a form field?
The text() keys stop matching. A renamed Email field returns 400 with "missing Email field" on every submission, and a renamed Phone arrives empty. A renamed Name field fails silently, since the handler falls back to the email as the name; checking a fresh submission in Pipedrive after any Designer edit to the form catches it.





