Integrate SharpSpring (now part of Constant Contact) with Bolt.new using their JSON-RPC style REST API. Get API credentials from SharpSpring Settings → API Settings, then use a Next.js API route to capture leads from Bolt forms, manage pipeline stages, and sync contact data. The SharpSpring API uses a unique account ID plus secret key authentication pattern — store both credentials server-side and never in client code.
Connect Bolt.new Forms and Dashboards to SharpSpring CRM
SharpSpring is a marketing automation platform that has found its strongest foothold among SMBs and marketing agencies — it offers the depth of enterprise platforms like Marketo and HubSpot at a significantly lower price point. In 2022, Constant Contact acquired SharpSpring and merged their platforms, giving SharpSpring users access to a broader email and digital marketing toolkit. If your clients or team is already using SharpSpring, building Bolt.new integrations that push leads and contact data directly into SharpSpring can significantly reduce manual data entry and improve lead response times.
SharpSpring's API is notable for its JSON-RPC style — unlike typical REST APIs where different endpoints represent different resources, SharpSpring uses a single endpoint and identifies the operation via a method field in the request body. This unusual pattern requires slightly different code structure than standard REST integrations, but it is well-documented and fully functional when called from a Next.js API route.
The integration covers two main flows. Capture flow: a Bolt.new form collects lead information from your landing page or app, and an API route immediately creates or updates a corresponding lead record in SharpSpring — ensuring no leads are lost and response times are minimized. Reporting flow: your Bolt app reads pipeline data from SharpSpring to display deal stages, lead counts, and conversion metrics in a custom dashboard that supplements or replaces SharpSpring's own reporting UI.
Integration method
SharpSpring's REST API uses a JSON-RPC style request format where all operations go to a single endpoint with method and params in the body. All API calls from Bolt must go through a Next.js API route using your accountID and secretKey stored in server-side environment variables. The API supports creating and updating leads, reading pipeline data, and managing contact fields. Webhook-based triggers for SharpSpring automation events require a deployed Netlify URL.
Prerequisites
- A SharpSpring account at sharpspring.com — API access requires a SharpSpring subscription
- SharpSpring API credentials: go to Settings → API Settings to find your Account ID and Secret Key
- A Bolt.new account with a new Next.js project open
- Familiarity with SharpSpring's lead fields — download the field list from SharpSpring's API documentation for your account's custom fields
- A Netlify account for deployment if you plan to use SharpSpring webhook notifications
Step-by-step guide
Get SharpSpring API credentials and understand the JSON-RPC request format
Get SharpSpring API credentials and understand the JSON-RPC request format
SharpSpring's API has an unusual design that can be confusing if you approach it expecting a standard REST API. All requests — regardless of the operation — go to the same URL, and the operation is specified by a `method` field in the request body. This is called a JSON-RPC style API, and SharpSpring uses it consistently across all their endpoints. The API endpoint URL includes your Account ID and Secret Key as query parameters. This means the credentials are in the URL itself, not in headers: `https://api.sharpspring.com/pubapi/v1/?accountID=YOUR_ID&secretKey=YOUR_KEY`. Because the entire URL is a secret, you absolutely must call this from a server-side API route and never from client-side React code. If a user inspects your network requests, they would see the secret key in the URL. To get your credentials: in SharpSpring, go to Settings (gear icon) → API Settings. You will find your Account ID (a UUID-style string) and Secret Key. If you do not see API Settings, check with your SharpSpring administrator — some accounts require admin access to view API credentials. The request body format is always: `{ method: 'methodName', params: { objects: [...], limit: 500 }, id: 'request-id' }`. The response format is: `{ result: { data: [...], total: 100 }, error: null, id: 'request-id' }`. When there is an error, `result` is null and `error` has code and message fields. Always check for both `result === null` and `error !== null` in your response handling.
Set up SharpSpring API integration. Create .env.local with SHARPSPRING_ACCOUNT_ID=your-account-id and SHARPSPRING_SECRET_KEY=your-secret-key. Create lib/sharpspring.ts with: a base apiRequest function that accepts method (string) and params (object), constructs the SharpSpring JSON-RPC URL from env vars, sends a POST with the method/params/id body, and returns the result.data array. Add TypeScript interfaces for SharpSpringLead (with all standard fields: leadID, firstName, lastName, emailAddress, companyName, phoneNumber, leadStatus, ownerID). Export helper functions createLeads(leads[]), getLeads(where?, limit?), updateLeads(leads[]), and getLeadByEmail(email) that all use apiRequest.
Paste this in Bolt.new chat
1// lib/sharpspring.ts2export interface SharpSpringLead {3 leadID?: string;4 firstName: string;5 lastName: string;6 emailAddress: string;7 companyName?: string;8 phoneNumber?: string;9 leadStatus?: string;10 ownerID?: string;11 description?: string;12 [key: string]: string | undefined;13}1415interface ApiResponse<T> {16 result: { [key: string]: T[] } | null;17 error: { code: number; message: string } | null;18 id: string;19}2021async function apiRequest<T>(method: string, params: Record<string, unknown>): Promise<T[]> {22 const accountID = process.env.SHARPSPRING_ACCOUNT_ID;23 const secretKey = process.env.SHARPSPRING_SECRET_KEY;2425 if (!accountID || !secretKey) {26 throw new Error('SharpSpring credentials not configured');27 }2829 const url = `https://api.sharpspring.com/pubapi/v1/?accountID=${accountID}&secretKey=${secretKey}`;30 const requestId = `bolt-${Date.now()}`;3132 const response = await fetch(url, {33 method: 'POST',34 headers: { 'Content-Type': 'application/json' },35 body: JSON.stringify({ method, params, id: requestId }),36 });3738 if (!response.ok) {39 throw new Error(`SharpSpring HTTP error: ${response.status}`);40 }4142 const data: ApiResponse<T> = await response.json();4344 if (data.error) {45 throw new Error(`SharpSpring API error ${data.error.code}: ${data.error.message}`);46 }4748 if (!data.result) {49 return [];50 }5152 // SharpSpring wraps results in a key matching the data type (e.g., result.leads)53 const resultKey = Object.keys(data.result)[0];54 return (data.result[resultKey] as T[]) || [];55}5657export async function createLeads(leads: Partial<SharpSpringLead>[]): Promise<SharpSpringLead[]> {58 return apiRequest<SharpSpringLead>('createLeads', { objects: leads });59}6061export async function getLeads(62 where?: Record<string, string>,63 limit = 10064): Promise<SharpSpringLead[]> {65 const params: Record<string, unknown> = { limit, offset: 0 };66 if (where) params.where = where;67 return apiRequest<SharpSpringLead>('getLeads', params);68}6970export async function updateLeads(leads: Array<{ leadID: string } & Partial<SharpSpringLead>>): Promise<SharpSpringLead[]> {71 return apiRequest<SharpSpringLead>('updateLeads', { objects: leads });72}7374export async function getLeadByEmail(email: string): Promise<SharpSpringLead | null> {75 const leads = await getLeads({ emailAddress: email }, 1);76 return leads[0] || null;77}Pro tip: SharpSpring's API request ID field (the id property in the JSON-RPC body) is used to correlate requests and responses. Use a unique ID per request — a timestamp-based ID like bolt-${Date.now()} works well. The response will echo back the same ID, which is useful for debugging when multiple requests are in flight.
Expected result: A lib/sharpspring.ts helper with typed functions for creating, reading, and updating leads using SharpSpring's JSON-RPC API, with credentials in .env.local.
Build the lead capture API route
Build the lead capture API route
The lead capture route is the most common and immediately valuable SharpSpring integration. When a user submits a contact form, request a demo, or sign up for a trial in your Bolt app, this route captures their information and creates a corresponding lead record in SharpSpring. This eliminates manual data entry and ensures your sales team sees new leads instantly without checking multiple systems. SharpSpring field names follow camelCase conventions. The standard fields are: `firstName`, `lastName`, `emailAddress`, `phoneNumber`, `companyName`, `leadStatus`, `description`, and `ownerID`. Custom fields created in your SharpSpring account have their own field names visible in SharpSpring's field manager. Look up your account's custom field names in SharpSpring under Settings → Lead Fields. For the `leadStatus` field, use SharpSpring's predefined stage names exactly as configured in your account — these vary by SharpSpring configuration. Common defaults include 'Open - Not Contacted', 'Working - Contacted', and 'Closed - Converted'. Mismatching the status name means the lead may not appear in expected pipeline views. The create-or-update pattern (check if the email already exists, then create if new or update if existing) prevents duplicate lead records when the same person submits a form multiple times. Use `getLeadByEmail` to check for an existing record before calling `createLeads`.
Create a SharpSpring lead capture API route at app/api/sharpspring/leads/route.ts using functions from lib/sharpspring.ts. Accept POST with: firstName (required), lastName (required), email (required, validate format), company (optional), phone (optional), message (optional), source (optional, for tracking which page). Check if a lead with that email already exists using getLeadByEmail. If yes, update the existing lead. If no, create a new lead. Map message to the description field and source to a custom field if SHARPSPRING_SOURCE_FIELD env var is set. Return 201 for new leads, 200 for updates. Also create a ContactForm React component with fields for first name, last name, email, company, phone, and message, with Zod validation and loading/success/error states.
Paste this in Bolt.new chat
1// app/api/sharpspring/leads/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { createLeads, updateLeads, getLeadByEmail } from '@/lib/sharpspring';45const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;67export async function POST(request: NextRequest) {8 const body = await request.json();9 const { firstName, lastName, email, company, phone, message, source } = body;1011 // Validation12 if (!firstName?.trim()) {13 return NextResponse.json({ error: 'First name is required' }, { status: 400 });14 }15 if (!lastName?.trim()) {16 return NextResponse.json({ error: 'Last name is required' }, { status: 400 });17 }18 if (!email || !emailRegex.test(email)) {19 return NextResponse.json({ error: 'Valid email is required' }, { status: 400 });20 }2122 try {23 const existingLead = await getLeadByEmail(email);2425 const leadData: Record<string, string> = {26 firstName: firstName.trim(),27 lastName: lastName.trim(),28 emailAddress: email.toLowerCase().trim(),29 };3031 if (company) leadData.companyName = company.trim();32 if (phone) leadData.phoneNumber = phone.trim();33 if (message) leadData.description = message.trim();3435 // Add source tracking if custom field is configured36 const sourceField = process.env.SHARPSPRING_SOURCE_FIELD;37 if (source && sourceField) {38 leadData[sourceField] = source;39 }4041 if (existingLead?.leadID) {42 await updateLeads([{ leadID: existingLead.leadID, ...leadData }]);43 return NextResponse.json({ success: true, action: 'updated', leadID: existingLead.leadID });44 } else {45 leadData.leadStatus = 'Open - Not Contacted';46 const created = await createLeads([leadData]);47 const newLeadId = (created[0] as Record<string, string>)?.id;48 return NextResponse.json({ success: true, action: 'created', leadID: newLeadId }, { status: 201 });49 }50 } catch (error) {51 console.error('SharpSpring lead capture error:', error);52 return NextResponse.json(53 { error: 'Failed to save lead information' },54 { status: 500 }55 );56 }57}Pro tip: The leadStatus value must exactly match the stage names configured in your SharpSpring account. If 'Open - Not Contacted' is not your account's default first stage name, new leads may not appear in your pipeline view. Check your SharpSpring admin under Settings → Stages to find the exact stage names for your account.
Expected result: A lead capture API route that creates or updates SharpSpring lead records from Bolt.new form submissions, with duplicate prevention via email lookup.
Fetch pipeline data for a sales dashboard
Fetch pipeline data for a sales dashboard
A custom pipeline dashboard in Bolt.new can combine SharpSpring lead data with other data sources — Supabase user data, Stripe subscription metrics, or custom application events — in a single view that SharpSpring's own reporting does not provide. Even without combining data sources, a Bolt-built dashboard can present SharpSpring pipeline data in a layout tailored to your team's daily review process. SharpSpring's `getLeads` API returns leads with their current pipeline status and associated fields. For a pipeline funnel, group leads by `leadStatus` and count them at each stage. For opportunity-based pipelines (where deals have monetary values), SharpSpring's `getOpportunities` API provides deal stage and value data. Pagination is important for accounts with large numbers of leads. SharpSpring's API supports `limit` and `offset` parameters in the params object. For a summary dashboard showing counts per stage, you can use a single API call with a high limit. For detailed lead lists, implement pagination with a next/previous interface. Caching the SharpSpring pipeline data in Supabase is strongly recommended for team-facing dashboards that multiple people access throughout the day. A sync job that pulls fresh data from SharpSpring every 15-30 minutes is better than hitting the SharpSpring API on every page load from every user.
Create a SharpSpring pipeline summary API route at app/api/sharpspring/pipeline/route.ts. Fetch all leads using getLeads from lib/sharpspring.ts with limit 500. Group leads by leadStatus field and count them. Also group by ownerID. Return: stages (array of {name, count}), byOwner (array of {ownerID, count}), totalLeads. Cache the result in Supabase table sharpspring_pipeline_cache (id, data jsonb, synced_at timestamp) and use cached data if it is less than 30 minutes old. Create a PipelineSummary React component showing a horizontal bar chart (Recharts) of leads by stage and a table of leads by owner. Add a Refresh button that forces a fresh SharpSpring API call.
Paste this in Bolt.new chat
1// app/api/sharpspring/pipeline/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { getLeads } from '@/lib/sharpspring';4import { createClient } from '@supabase/supabase-js';56const CACHE_TTL_MINUTES = 30;78interface PipelineData {9 stages: Array<{ name: string; count: number }>;10 byOwner: Array<{ ownerID: string; count: number }>;11 totalLeads: number;12 syncedAt: string;13}1415async function fetchFromSharpSpring(): Promise<PipelineData> {16 const leads = await getLeads(undefined, 500);1718 const stageCounts = new Map<string, number>();19 const ownerCounts = new Map<string, number>();2021 for (const lead of leads) {22 const stage = lead.leadStatus || 'Unknown';23 stageCounts.set(stage, (stageCounts.get(stage) || 0) + 1);2425 if (lead.ownerID) {26 ownerCounts.set(lead.ownerID, (ownerCounts.get(lead.ownerID) || 0) + 1);27 }28 }2930 return {31 stages: Array.from(stageCounts.entries()).map(([name, count]) => ({ name, count })),32 byOwner: Array.from(ownerCounts.entries()).map(([ownerID, count]) => ({ ownerID, count })),33 totalLeads: leads.length,34 syncedAt: new Date().toISOString(),35 };36}3738export async function GET(request: NextRequest) {39 const { searchParams } = new URL(request.url);40 const forceRefresh = searchParams.get('refresh') === 'true';4142 const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;43 const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;44 const supabase = supabaseUrl && serviceKey ? createClient(supabaseUrl, serviceKey) : null;4546 // Try cache first47 if (supabase && !forceRefresh) {48 const { data: cached } = await supabase49 .from('sharpspring_pipeline_cache')50 .select('data, synced_at')51 .order('synced_at', { ascending: false })52 .limit(1)53 .single();5455 if (cached) {56 const ageMinutes = (Date.now() - new Date(cached.synced_at).getTime()) / 60000;57 if (ageMinutes < CACHE_TTL_MINUTES) {58 return NextResponse.json({ ...cached.data, fromCache: true });59 }60 }61 }6263 try {64 const data = await fetchFromSharpSpring();6566 if (supabase) {67 await supabase.from('sharpspring_pipeline_cache').insert({ data, synced_at: data.syncedAt });68 }6970 return NextResponse.json(data);71 } catch (error) {72 return NextResponse.json(73 { error: (error as Error).message },74 { status: 500 }75 );76 }77}Pro tip: SharpSpring's getLeads API returns up to 500 records per call. For accounts with more than 500 leads, implement pagination by incrementing the offset parameter in additional calls until you have fetched all records. Use the total count returned by the API to know when you have retrieved everything.
Expected result: A pipeline summary API route with Supabase caching that returns lead counts by stage and owner, ready for dashboard visualizations.
Deploy to Netlify and configure SharpSpring webhook notifications
Deploy to Netlify and configure SharpSpring webhook notifications
SharpSpring API calls work in Bolt.new's WebContainer during development since they are standard outbound HTTP requests. You can build and test all lead capture and pipeline reading features in the Bolt preview before deploying. Deployment to Netlify is required for two reasons: your .env.local credentials are not included in the deployed build, and any SharpSpring webhook features (receiving notifications when lead status changes) require a publicly accessible URL that Bolt's WebContainer cannot provide. After deploying to Netlify via Bolt.new's deploy button, go to the Netlify dashboard → Site Configuration → Environment Variables and add: SHARPSPRING_ACCOUNT_ID, SHARPSPRING_SECRET_KEY, and if you use Supabase caching: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY. Trigger a redeploy. For SharpSpring webhook notifications: in SharpSpring, go to Settings → API Settings → Webhooks. Enter your Netlify URL (https://your-app.netlify.app/api/sharpspring/webhook) and select the events to subscribe to — lead status changes, new form submissions, or lead score changes. SharpSpring sends a POST request to your webhook URL when these events occur. Always return a 200 response from your webhook handler quickly to acknowledge receipt. SharpSpring retries webhooks that receive error responses, which can cause duplicate processing. Log errors internally but return 200 to prevent retries.
Create a SharpSpring webhook handler at app/api/sharpspring/webhook/route.ts. Accept POST from SharpSpring. Parse the payload and extract event type and lead data. Handle lead status change events by updating the corresponding record in a Supabase leads_sync table if present. Log the event type and affected lead email for debugging. Return 200 immediately for all events. Also add netlify.toml with Next.js build config. Add a comment noting that webhooks require a deployed public URL and cannot be received in Bolt's WebContainer preview.
Paste this in Bolt.new chat
1// app/api/sharpspring/webhook/route.ts2// NOTE: SharpSpring webhooks require a public URL.3// This endpoint cannot receive events in Bolt's WebContainer preview.4// Deploy to Netlify first, then register your URL in SharpSpring Settings → API Settings → Webhooks.5import { NextRequest, NextResponse } from 'next/server';67interface SharpSpringWebhookPayload {8 event: string;9 lead?: {10 leadID: string;11 emailAddress: string;12 leadStatus: string;13 firstName: string;14 lastName: string;15 };16}1718export async function POST(request: NextRequest) {19 let payload: SharpSpringWebhookPayload;2021 try {22 payload = await request.json();23 } catch {24 return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });25 }2627 const { event, lead } = payload;2829 console.log(`SharpSpring webhook: ${event}`, {30 leadID: lead?.leadID,31 email: lead?.emailAddress,32 status: lead?.leadStatus,33 });3435 switch (event) {36 case 'lead.status_changed':37 // TODO: Sync updated status to your Supabase database38 console.log(`Lead ${lead?.emailAddress} status changed to: ${lead?.leadStatus}`);39 break;4041 case 'lead.created':42 console.log(`New SharpSpring lead: ${lead?.firstName} ${lead?.lastName} <${lead?.emailAddress}>`);43 break;4445 case 'lead.score_changed':46 console.log(`Lead score changed for: ${lead?.emailAddress}`);47 break;4849 default:50 console.log(`Unhandled SharpSpring event: ${event}`);51 }5253 // Always return 200 — SharpSpring retries on non-20054 return NextResponse.json({ received: true });55}Pro tip: SharpSpring's webhook payload format should be verified against their current API documentation — the exact structure and event type names may vary based on your SharpSpring version and configuration. Test the webhook by using SharpSpring's webhook test tool or by manually changing a lead's status after setting up the webhook endpoint.
Expected result: A deployed Netlify app with SharpSpring lead capture and pipeline reading working in production, with a webhook handler for lead event notifications.
Common use cases
Lead Capture Form to SharpSpring CRM
When a visitor fills out a contact or demo request form on your Bolt.new landing page or marketing site, automatically create a lead record in SharpSpring with their contact information and assign them to the appropriate lead owner and pipeline stage. Eliminates manual lead entry and ensures instant CRM visibility.
Create a demo request form that sends leads to SharpSpring. Build an API route at app/api/sharpspring/leads/route.ts that accepts POST with firstName, lastName, email, company, phone, and message fields. Use SHARPSPRING_ACCOUNT_ID and SHARPSPRING_SECRET_KEY env vars to call SharpSpring's JSON-RPC API at https://api.sharpspring.com/pubapi/v1/?accountID={id}&secretKey={key} with method createLeads. Pass the lead data in the objects array with appropriate SharpSpring field names. Create a DemoRequestForm React component with a clean multi-field form, validation, and success message showing 'We will be in touch within 1 business day'. Return 400 for validation errors, 500 for API failures.
Copy this prompt to try it in Bolt.new
Pipeline Stage Dashboard
Build an internal sales dashboard that fetches current lead pipeline data from SharpSpring and displays deal counts and values by stage. Useful for sales managers who want a quick overview without navigating SharpSpring's interface, or for combining SharpSpring pipeline data with metrics from other sources.
Create a SharpSpring pipeline dashboard. Build an API route at app/api/sharpspring/pipeline/route.ts that calls SharpSpring's getLeads and getOpportunities methods using SHARPSPRING_ACCOUNT_ID and SHARPSPRING_SECRET_KEY. Group leads by their lead status field and opportunities by stage. Return summary data with counts and deal values per stage. Create a PipelineDashboard React component displaying a horizontal funnel visualization using Recharts, showing the count and total value of leads at each stage. Add a refresh button and display last synced timestamp.
Copy this prompt to try it in Bolt.new
Contact Update from In-App Actions
When a user takes a key action in your app — completing onboarding, upgrading their subscription, or canceling — automatically update their SharpSpring contact record with the new status and trigger relevant SharpSpring automation rules. Keeps your CRM data in sync with actual user behavior.
Create a SharpSpring contact sync utility. Build a lib/sharpspring.ts helper with functions: updateLead(email, fields) that calls SharpSpring's updateLeads method to change field values, and createOrUpdateLead(contactData) that first searches for an existing lead by email using getLeads, then creates if not found or updates if found. Call updateLead from my Supabase user lifecycle hooks: when a user upgrades their plan, update their SharpSpring leadStatus field to 'Customer'; when they cancel, update to 'Churned'. Store the SharpSpring lead ID in Supabase users table for efficient future updates.
Copy this prompt to try it in Bolt.new
Troubleshooting
SharpSpring API returns error code 301 — 'Method not found'
Cause: The method name in the JSON-RPC request body does not match an available API method. SharpSpring method names are case-sensitive and must exactly match the documented names (e.g., 'createLeads' not 'CreateLeads' or 'create_leads').
Solution: Verify the exact method name in SharpSpring's API documentation. Common methods: createLeads, getLeads, updateLeads, createOpportunities, getOpportunities, updateOpportunities, getLeadFields. Check that the method name is spelled correctly and matches the documented capitalization exactly.
1// Correct method names2body: JSON.stringify({3 method: 'createLeads', // correct4 params: { objects: [leadData] },5 id: requestId,6})7// Not: 'CreateLeads', 'create_leads', or 'CreateLead'SharpSpring API returns error code 102 — 'Authentication failed'
Cause: The accountID or secretKey is incorrect, or the URL-encoded credentials are malformed. The credentials are query parameters in the URL, not headers, and must be exact matches to your SharpSpring account's values.
Solution: Verify both SHARPSPRING_ACCOUNT_ID and SHARPSPRING_SECRET_KEY match exactly what is shown in SharpSpring Settings → API Settings. Check there are no extra spaces or newlines in the environment variable values. Confirm the URL format is correct: https://api.sharpspring.com/pubapi/v1/?accountID={id}&secretKey={key}.
Lead status is not updating or new leads do not appear in the SharpSpring pipeline
Cause: The leadStatus value being sent does not match the exact stage names configured in your SharpSpring account. SharpSpring's stage names are configurable and may differ from the default examples in documentation.
Solution: Fetch your account's actual stage names from the SharpSpring API using the getLeadFields method or by checking Settings → Stages in the SharpSpring dashboard. Replace the hardcoded 'Open - Not Contacted' status with the exact stage name from your account. Stage names are case-sensitive.
1// Fetch valid lead status values from SharpSpring2const fields = await apiRequest('getLeadFields', {});3// Look for the 'leadStatus' field and its available optionsSharpSpring API calls work in Bolt.new preview but the API key is undefined after deploying to Netlify
Cause: .env.local variables are never deployed — they only exist in the local Bolt development environment. Netlify has its own environment variable storage that must be configured separately.
Solution: In the Netlify dashboard → Site Configuration → Environment Variables, add SHARPSPRING_ACCOUNT_ID and SHARPSPRING_SECRET_KEY with their values. Click Trigger Deploy → Deploy site to redeploy with the new variables. These server-side variables are never exposed to the browser, so do not add NEXT_PUBLIC_ prefix.
Best practices
- Never expose your SharpSpring API credentials in client-side code — the accountID and secretKey appear in the API URL itself, which must only be constructed server-side in Next.js API routes
- Use the create-or-update pattern for lead capture to prevent duplicate records — always check for an existing lead by email before creating a new one
- Map lead source information (which page, which campaign, which product) to SharpSpring custom fields at capture time — this data is far harder to reconstruct after the fact
- Implement a Supabase cache layer for pipeline data reads to avoid hitting SharpSpring's API on every page load when multiple team members use the dashboard
- Use SharpSpring's exact stage name strings from your account configuration — hardcoding default stage names that may not match your setup causes silent failures where leads are created but do not appear in the expected pipeline view
- Always return HTTP 200 from SharpSpring webhook handlers regardless of processing outcome — SharpSpring retries on non-200 responses, causing duplicate event processing
- Test API calls in Bolt.new's WebContainer preview before deploying — outbound SharpSpring API calls work in the preview, allowing you to validate the JSON-RPC request format before going to production
- Add request ID logging to your API routes to correlate SharpSpring API calls with specific form submissions for debugging
Alternatives
Marketo is an enterprise-grade marketing automation platform with deeper ABM and attribution features — significantly more expensive than SharpSpring but better for large organizations with complex multi-touch revenue attribution needs.
HubSpot has a better free tier, a larger developer community, and more developer-friendly REST API with clearer documentation — easier to integrate from Bolt.new and better for teams new to CRM integrations.
Pardot (Salesforce Marketing Cloud Account Engagement) is tightly integrated with Salesforce CRM — better for organizations already in the Salesforce ecosystem, whereas SharpSpring is a standalone CRM-plus-automation solution.
Oracle Eloqua is a large-enterprise marketing automation platform with complex multi-channel campaign orchestration — SharpSpring is more accessible for SMBs that do not need enterprise-scale campaign complexity.
Frequently asked questions
Can SharpSpring API calls work in Bolt.new's WebContainer during development?
Yes. SharpSpring API calls are standard outbound HTTP POST requests from your Next.js API routes. These work in Bolt's WebContainer preview without any issues. You can build and test all lead capture and pipeline read features in the Bolt preview. The only SharpSpring feature that requires deployment is incoming webhook delivery, which needs a publicly accessible URL.
Why does SharpSpring use a JSON-RPC style API instead of standard REST?
SharpSpring's API was designed before REST APIs became the universal standard for B2B SaaS platforms. The JSON-RPC pattern uses a single endpoint for all operations, with the operation name in the request body. This is functionally equivalent to REST — all operations are available, the format is consistent, and authentication works the same way. It just requires learning a slightly different request structure compared to typical REST APIs.
How do I find the correct field names for SharpSpring custom fields?
Call the SharpSpring getLeadFields API method with empty params to retrieve all available lead fields for your account, including custom fields created by your admin. The response includes each field's systemName (the API field name to use in create/update calls), label (the display name), and dataType. Custom fields typically have system names like customField_abc123 that you must use exactly in API calls.
What is the difference between SharpSpring leads and opportunities in the API?
SharpSpring uses a two-object model: Leads (contacts, either individuals or companies) and Opportunities (deals linked to leads, with stages and monetary values). The leads API manages contact information and lead status. The opportunities API manages the sales pipeline with stages and deal values. For lead capture forms, you typically only interact with the leads API. For a full sales pipeline dashboard, you use both.
Is SharpSpring still available as a standalone product after the Constant Contact acquisition?
Yes. SharpSpring continues to operate as a distinct product with its own branding, pricing, and feature set at sharpspring.com. The Constant Contact acquisition in 2022 added marketing email capabilities to SharpSpring's existing CRM and marketing automation platform. Existing SharpSpring customers can continue using it as before, and the API remains the same.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation