@businessflow/leads
v2.5.0
Published
Marketing website lead collection and form submission utilities with server-side API route handlers for BusinessFlow CRM
Maintainers
Readme
@businessflow/marketing-sites
Marketing website lead collection and form submission utilities for React and NextJS applications that integrate with BusinessFlow CRM.
Features
- 🚀 Framework Agnostic: Works with React, NextJS, and vanilla JavaScript
- 🔒 Secure: Server-side API key handling, reCAPTCHA integration
- 📱 Responsive: Mobile-friendly form components
- 🎨 Customizable: Flexible styling and field configuration
- 📝 TypeScript: Full type safety and IntelliSense support
- ⚡ Modern: Built with latest React patterns and NextJS features
- 🏢 BusinessFlow Ready: Pre-configured for BusinessFlow CRM integration
Installation
npm install @businessflow/marketing-sitesQuick Start
1. Simple BusinessFlow Integration
// app/api/lead/route.ts
import { createBusinessFlowHandler } from '@businessflow/marketing-sites/server';
export const POST = createBusinessFlowHandler({
apiUrl: process.env.BUSINESS_FLOW_API_URL!,
apiKey: process.env.BUSINESS_FLOW_API_KEY!,
sourceUrl: process.env.SITE_URL!,
recaptchaSecret: process.env.RECAPTCHA_SECRET_KEY!,
});2. Even Simpler with Environment Variables
// app/api/lead/route.ts
import { createSimpleBusinessFlowHandler } from '@businessflow/marketing-sites/server';
// Uses BUSINESS_FLOW_API_URL, BUSINESS_FLOW_API_KEY, and RECAPTCHA_SECRET_KEY from env
export const POST = createSimpleBusinessFlowHandler();3. Custom Integration (Advanced)
// app/api/lead/route.ts
import { createLeadHandler } from '@businessflow/marketing-sites/server';
export const POST = createLeadHandler({
onSubmit: async (data) => {
// Your custom business logic here
const response = await fetch('https://your-api.com/leads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.YOUR_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return {
success: response.ok,
message: response.ok ? 'Lead submitted successfully' : 'Submission failed',
id: response.ok ? await response.json().then(r => r.id) : undefined
};
},
recaptcha: {
secretKey: process.env.RECAPTCHA_SECRET_KEY!
}
});2. Use in Your Components
Pre-built Component
import { ContactForm } from '@businessflow/leads/react';
export default function ContactPage() {
return (
<ContactForm
recaptcha={{ siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY! }}
onSuccess={(response) => console.log('Success!', response)}
onError={(error) => console.error('Error:', error)}
/>
);
}Custom Hook for Custom Forms
import { useContactForm } from '@businessflow/leads/react';
export default function CustomContactForm() {
const {
formData,
errors,
state,
handleChange,
handleSubmit,
handleRetry,
resetForm
} = useContactForm({
endpoint: '/api/lead',
recaptcha: {
siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
action: 'contact_form'
},
onSuccess: (response) => console.log('Success!', response),
onError: (error) => console.error('Error:', error),
maxRetries: 3
});
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input
type="text"
placeholder="First Name"
value={formData.firstName}
onChange={(e) => handleChange('firstName', e.target.value)}
/>
{errors.firstName && <span>{errors.firstName}</span>}
<input
type="text"
placeholder="Last Name"
value={formData.lastName}
onChange={(e) => handleChange('lastName', e.target.value)}
/>
{errors.lastName && <span>{errors.lastName}</span>}
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
{state.error && state.canRetry && (
<button onClick={handleRetry} disabled={state.isSubmitting}>
Retry ({state.retryCount}/{3})
</button>
)}
{state.isSuccess && <div>Form submitted successfully!</div>}
{state.error && <div>Error: {state.error}</div>}
</form>
);
}File attachments (v2.5+)
Lets a visitor attach plans or documents to a lead. Files go direct from the browser to the BusinessFlow API, not through your Next.js function — that clears Vercel's 4.5 MB request-body cap. Your server route only mints a short-lived ticket, so the API key never reaches the browser.
Server-enforced limits: 10 MB per file (decimal), 5 files per ticket,
extensions .pdf .jpg .jpeg .png .dwg. The API also sniffs the leading bytes, so a
renamed executable is rejected regardless of its extension. Uploaded files are
malware-scanned; the dashboard only serves a file once it comes back clean.
1. Add the ticket route
// app/api/lead-upload-ticket/route.ts
import { createUploadTicketHandler } from '@businessflow/leads/server';
export const POST = createUploadTicketHandler();Requires BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY (throws at construction if
either is missing). The handler throttles per visitor IP — without that, one scripted
visitor would spend your site's entire shared API-key budget, because the API sees only
your host's egress address.
2. Upload from the form
import { uploadLeadAttachments } from '@businessflow/leads/client';
const result = await uploadLeadAttachments(files, {
ticketUrl: '/api/lead-upload-ticket',
apiUrl: process.env.NEXT_PUBLIC_BUSINESS_FLOW_API_URL!,
// Thread the session's ticket back in on every later batch — see below.
existingTicket: ticket ? { token: ticket, usedSlots } : undefined,
onProgress: (fileIndex, pct) => setRowProgress(fileIndex, pct),
});Keep the whole form session on one ticket. Mint on the first file selection, then
pass existingTicket into every subsequent call. The API binds attachments to a lead
per ticket, so a submission carrying ids from two different tickets is rejected.
It never throws. Everything that fails comes back in result.failures, each entry
carrying index (maps to your UI row), a user-facing error, and permanent — true
for oversize/wrong-type/over-cap (offer no retry), false for network and 5xx (offer a
retry). Submit the lead even when every upload failed; losing the enquiry is far worse
than losing the file.
3. Submit the lead
await fetch('/api/lead', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name, email, phone, comments, token: recaptchaToken,
// Send all three together, or omit all three.
...(result.attachmentIds.length > 0 && {
attachmentIds: result.attachmentIds,
uploadTicket: result.ticket,
popiaConsentVersion: 'your-site-2026-07-v1',
}),
}),
});Consent is mandatory whenever attachmentIds is non-empty — the API rejects the
submission otherwise. Gate your consent checkbox on there being at least one file, and
clear it when the last file is removed.
Documentation
License
MIT
