Architecting an Offline Conversion Pipeline: ERPNext to Google Ads via Next.js 15 for B2B Automated Bidding
Induji Technical Team
Content Strategy
Table of Contents
Key Takeaways
- The B2B Bidding Problem: Standard Google Ads bidding optimizes for top-of-funnel actions (like form fills), not bottom-line revenue, which is a critical flaw for businesses with long sales cycles.
- The Architectural Solution: A closed-loop system that captures the Google Click ID (GCLID) on lead submission, stores it in ERPNext, and uses a webhook to fire a conversion back to the Google Ads API only when a deal is marked as "Won."
- Next.js 15 as Middleware: Next.js 15 API Routes provide a perfect, scalable, and serverless environment to act as the crucial middleware, handling form submissions and processing webhooks from ERPNext.
- Unlocking Smart Bidding: This pipeline enables the use of powerful, value-based bidding strategies like Target CPA (based on qualified leads) and Target ROAS (based on actual deal value), aligning ad spend directly with revenue.
- Technical Implementation: The process involves creating custom fields in ERPNext, configuring webhooks, building two distinct Next.js API routes, and authenticating securely with the Google Ads API using OAuth2.
The Fundamental Flaw in Standard B2B Ad Bidding
For B2B enterprises, the journey from an ad click to a closed deal can span weeks, if not months. This protracted sales cycle creates a massive data chasm for digital advertising platforms. Google Ads, by default, optimizes for immediate, observable conversions—typically, a "Thank You" page view after a form submission.
This leads to a dangerous optimization fallacy: the algorithm treats a lead from a Fortune 500 CTO with a $250,000 budget identically to a lead from a student researching a project. Both are just "one conversion." As a result, your Cost Per Acquisition (CPA) might look great, but your pipeline is filled with low-quality leads, and your actual Return On Ad Spend (ROAS) is abysmal.
The solution is not to abandon automated bidding but to feed it better data. We need to inform Google which clicks actually resulted in revenue. This is achieved through Offline Conversion Imports (OCI), and the technical backbone for this system is a robust data pipeline connecting your single source of truth—your ERP—to the ad platform. This guide provides the definitive architectural blueprint for building this pipeline using ERPNext, Next.js 15, and the Google Ads API.
Blueprint for a Resilient Offline Conversion Pipeline
The goal is to create a seamless, automated flow of information. When a prospect clicks a Google Ad, they are tagged with a unique Google Click Identifier (GCLID). Our architecture must capture this ID, shepherd it through the entire sales process within ERPNext, and then use it to report a conversion back to Google Ads at the precise moment a deal is won.
The core components of this architecture are:
- Frontend (Next.js 15 App): The user-facing website or landing page with the lead generation form. Its primary job is to capture the
gclidparameter from the URL. - Ingestion API (Next.js API Route): A server-side endpoint that receives the form submission, including the hidden
gclid. It validates the data and uses the ERPNext REST API to create a newLead. - ERPNext Instance: Your central business management system. We will customize it to store the
gclidand trigger an action upon a change in deal status. - ERPNext Webhook: An automated trigger configured to fire when an
Opportunitystatus is updated toWon. This webhook sends a payload of critical data to our processing middleware. - Processing Middleware (Next.js API Route): A secure, serverless endpoint dedicated to receiving the webhook from ERPNext. It authenticates the request, transforms the data into the format required by Google Ads, and executes the API call.
- Google Ads API: The final destination. It receives the conversion data, matches the
gclidto the original ad click, and attributes the revenue value correctly, thereby enriching the bidding algorithm's dataset.
Step 1: Capturing and Storing the GCLID with Next.js and ERPNext
This first phase is about ensuring the gclid is captured reliably and permanently associated with the lead record in your ERP.
Frontend Logic in Next.js 15
In your Next.js application, the lead generation form component needs to read the gclid from the URL query parameters. With the App Router in Next.js 15 and React 19, the useSearchParams hook makes this straightforward.
// app/components/LeadForm.tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function LeadForm() {
const searchParams = useSearchParams();
const [gclid, setGclid] = useState('');
useEffect(() => {
const gclidParam = searchParams.get('gclid');
if (gclidParam) {
setGclid(gclidParam);
}
}, [searchParams]);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch('/api/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to submit lead');
}
// Handle success (e.g., redirect to thank you page)
window.location.href = '/thank-you';
} catch (error) {
console.error(error);
// Handle error
}
};
return (
<form onSubmit={handleSubmit}>
{/* Your form fields: name, email, company, etc. */}
<input type="text" name="full_name" required />
<input type="email" name="email_id" required />
{/* Hidden field to pass the GCLID */}
<input type="hidden" name="gclid" value={gclid} />
<button type="submit">Submit</button>
</form>
);
}
Backend Handling and ERPNext Integration
When the form is submitted, it hits a Next.js API Route. This route is responsible for communicating with ERPNext.
First, prepare ERPNext:
- Navigate to
Setup > Customize Form. - Select the
LeadDocType. - Add a new custom field:
- Label: GCLID
- Fieldname:
custom_gclid(ERPNext automatically prefixes withcustom_) - Type: Data
- Permissions: Ensure it's writable via the API.
- Repeat this process for the
OpportunityDocType, and ensure the field is mapped to be carried over when a Lead is converted to an Opportunity.
Next, build the API Route:
// app/api/leads/route.ts
import { NextResponse } from 'next/server';
// Assuming you have a helper/SDK for Frappe/ERPNext
import { FrappeClient } from 'frappe-client-ts';
const client = new FrappeClient(
process.env.ERPNEXT_URL!,
{
useToken: true,
token: () => process.env.ERPNEXT_API_KEY!,
}
);
export async function POST(request: Request) {
try {
const body = await request.json();
const { full_name, email_id, gclid } = body;
if (!full_name || !email_id) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
const leadDoc = {
doctype: 'Lead',
lead_name: full_name,
email_id: email_id,
// Pass the GCLID to your custom field
custom_gclid: gclid || null,
};
const newLead = await client.createDoc('Lead', leadDoc);
return NextResponse.json({ success: true, lead: newLead.name }, { status: 201 });
} catch (error) {
console.error('ERPNext Lead Creation Failed:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
Step 2: Triggering Conversions from ERPNext with Webhooks
With the gclid safely stored, we now wait for the sales team to do their work. When they finally close a deal by updating the Opportunity status to Won in ERPNext, we need to trigger our pipeline.
Configuring Webhooks in ERPNext
In your ERPNext desk, search for and navigate to "Webhook".
Click "Add Webhook".
Webhook DocType: Select
Opportunity.Webhook Event: Choose
On Update.Condition: This is crucial to prevent firing on every update. Set the condition to:
doc.status == 'Won' and doc.get_doc_before_save().status != 'Won'This ensures the webhook only fires on the specific transition to "Won".
Request URL: Enter the URL of the Next.js middleware API route you will build in the next step (e.g.,
https://your-app.com/api/google-ads-conversion).Webhook Data: Check "Send Webhook Data" and define the fields to be sent. You only need a few:
custom_gclid(the GCLID from the original Lead)grand_total(the value of the deal)transaction_date(the date the deal was won)currency
Request Headers: Add a header for security, like
X-ERPNext-Secret: YOUR_SECRET_KEY.
Step 3: Processing and Forwarding Conversions to Google Ads API
This is the final, most critical link in the chain. Our middleware receives the payload from ERPNext and communicates with Google.
Building the Next.js Middleware API Route
This API route acts as a secure and robust processor. It validates the incoming request, formats the data, and handles the Google Ads API communication.
// app/api/google-ads-conversion/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { GoogleAdsApi, enums } from '@google-ads/google-ads-api';
export async function POST(request: NextRequest) {
// 1. Authenticate the webhook request
const secret = request.headers.get('x-erpnext-secret');
if (secret !== process.env.ERPNEXT_WEBHOOK_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json();
const { custom_gclid, grand_total, transaction_date, currency } = body;
// 2. Validate essential data
if (!custom_gclid || !grand_total || !transaction_date) {
console.log('Webhook received with missing data:', body);
// Return 200 to prevent ERPNext from retrying a bad request
return NextResponse.json({ message: 'Missing required conversion data.' });
}
// 3. Authenticate with Google Ads API
const client = new GoogleAdsApi({
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
developer_token: process.env.GOOGLE_DEVELOPER_TOKEN!,
});
const customer = client.Customer({
customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID!,
login_customer_id: process.env.GOOGLE_ADS_MANAGER_ID!, // If using MCC
refresh_token: process.env.GOOGLE_REFRESH_TOKEN!,
});
// 4. Construct and Upload the Conversion
const conversionUploadService = customer.services.ConversionUpload;
const clickConversion = {
gclid: custom_gclid,
conversion_action: `customers/${process.env.GOOGLE_ADS_CUSTOMER_ID}/conversionActions/${process.env.CONVERSION_ACTION_ID}`,
conversion_date_time: new Date(transaction_date).toISOString().replace('T', ' ').split('.')[0], // Format: 'YYYY-MM-DD HH:MM:SS'
conversion_value: grand_total,
currency_code: currency,
};
const response = await conversionUploadService.uploadClickConversions({
customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID!,
conversions: [clickConversion],
partial_failure: true, // Recommended
});
console.log('Successfully uploaded conversion:', response);
return NextResponse.json({ success: true, result: response.results[0] });
} catch (error) {
console.error('Google Ads Conversion Upload Failed:', error);
// Log the error for debugging
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
Unlocking True B2B Performance with Automated Bidding
With this pipeline active, the data flowing into your Google Ads account is transformed. You are no longer telling the algorithm "get me more form fills." You are now providing explicit instructions: "get me more clicks that result in high-value, closed deals."
This enables a strategic shift:
- From
Maximize ConversionstoTarget CPA: You can now set a realistic Target Cost Per Qualified Opportunity, not just a raw lead. - To the Holy Grail:
Target ROAS: By sending thegrand_totalas the conversion value, you can directly instruct Google's AI to optimize for a specific Return On Ad Spend. If you tell Google you want a 5x ROAS, it will actively adjust bids to find users whose behavior patterns correlate with past high-value customers, even if it means a higher cost-per-click or a lower top-of-funnel conversion rate.
Be patient. Google's bidding algorithms require data. Expect a learning period of several weeks after implementation. A steady flow of 30-50 offline conversions per month is generally recommended for these strategies to perform optimally.
Frequently Asked Questions (FAQ)
Q1: What are the prerequisites in Google Ads for this to work?
You must have auto-tagging enabled in your Google Ads account settings. You also need to create a new Conversion Action with the source set to "Import from clicks." This will provide you with the CONVERSION_ACTION_ID needed in the API call.
Q2: How do I handle leads that come from channels without a GCLID?
The architecture is resilient to this. The custom_gclid field in ERPNext should not be a required field. The ingestion API should be coded to handle cases where the gclid is null or undefined. The conversion pipeline will simply not trigger for these non-Google Ads leads, which is the correct behavior.
Q3: Can this be done without Next.js? Absolutely. The middleware can be built with any server-side technology (e.g., Express.js, Python/Flask, PHP/Laravel). However, Next.js API Routes, particularly when deployed on serverless platforms like Vercel, offer an excellent combination of developer experience, scalability, and cost-efficiency for this type of event-driven task.
Q4: What's the typical delay between the ERPNext event and the conversion appearing in Google Ads? The API call from your Next.js middleware is nearly instantaneous. However, Google Ads can take anywhere from a few hours to 24 hours to process and attribute the imported conversion in its reporting interface.
Bridge the Gap Between Ad Spend and Revenue
Architecting a closed-loop conversion pipeline is a high-leverage engineering effort that directly impacts marketing efficiency and business profitability. It transforms your ad budget from an expense into a precision-guided investment in revenue.
This is not a trivial task. It requires expertise across system architecture, DevOps, ERP customization, and third-party API integration. If you are ready to unlock the true potential of your B2B marketing with a data-driven bidding strategy, the team at Induji Technologies is here to help.
Ready to Transform Your Business?
Partner with Induji Technologies to leverage cutting-edge solutions tailored to your unique challenges. Let's build something extraordinary together.
