# How to Integrate Bolt.new with SharpSpring

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
// lib/sharpspring.ts
export interface SharpSpringLead {
  leadID?: string;
  firstName: string;
  lastName: string;
  emailAddress: string;
  companyName?: string;
  phoneNumber?: string;
  leadStatus?: string;
  ownerID?: string;
  description?: string;
  [key: string]: string | undefined;
}

interface ApiResponse<T> {
  result: { [key: string]: T[] } | null;
  error: { code: number; message: string } | null;
  id: string;
}

async function apiRequest<T>(method: string, params: Record<string, unknown>): Promise<T[]> {
  const accountID = process.env.SHARPSPRING_ACCOUNT_ID;
  const secretKey = process.env.SHARPSPRING_SECRET_KEY;

  if (!accountID || !secretKey) {
    throw new Error('SharpSpring credentials not configured');
  }

  const url = `https://api.sharpspring.com/pubapi/v1/?accountID=${accountID}&secretKey=${secretKey}`;
  const requestId = `bolt-${Date.now()}`;

  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ method, params, id: requestId }),
  });

  if (!response.ok) {
    throw new Error(`SharpSpring HTTP error: ${response.status}`);
  }

  const data: ApiResponse<T> = await response.json();

  if (data.error) {
    throw new Error(`SharpSpring API error ${data.error.code}: ${data.error.message}`);
  }

  if (!data.result) {
    return [];
  }

  // SharpSpring wraps results in a key matching the data type (e.g., result.leads)
  const resultKey = Object.keys(data.result)[0];
  return (data.result[resultKey] as T[]) || [];
}

export async function createLeads(leads: Partial<SharpSpringLead>[]): Promise<SharpSpringLead[]> {
  return apiRequest<SharpSpringLead>('createLeads', { objects: leads });
}

export async function getLeads(
  where?: Record<string, string>,
  limit = 100
): Promise<SharpSpringLead[]> {
  const params: Record<string, unknown> = { limit, offset: 0 };
  if (where) params.where = where;
  return apiRequest<SharpSpringLead>('getLeads', params);
}

export async function updateLeads(leads: Array<{ leadID: string } & Partial<SharpSpringLead>>): Promise<SharpSpringLead[]> {
  return apiRequest<SharpSpringLead>('updateLeads', { objects: leads });
}

export async function getLeadByEmail(email: string): Promise<SharpSpringLead | null> {
  const leads = await getLeads({ emailAddress: email }, 1);
  return leads[0] || null;
}
```

**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.

### 2. 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`.

```
// app/api/sharpspring/leads/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createLeads, updateLeads, getLeadByEmail } from '@/lib/sharpspring';

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { firstName, lastName, email, company, phone, message, source } = body;

  // Validation
  if (!firstName?.trim()) {
    return NextResponse.json({ error: 'First name is required' }, { status: 400 });
  }
  if (!lastName?.trim()) {
    return NextResponse.json({ error: 'Last name is required' }, { status: 400 });
  }
  if (!email || !emailRegex.test(email)) {
    return NextResponse.json({ error: 'Valid email is required' }, { status: 400 });
  }

  try {
    const existingLead = await getLeadByEmail(email);

    const leadData: Record<string, string> = {
      firstName: firstName.trim(),
      lastName: lastName.trim(),
      emailAddress: email.toLowerCase().trim(),
    };

    if (company) leadData.companyName = company.trim();
    if (phone) leadData.phoneNumber = phone.trim();
    if (message) leadData.description = message.trim();

    // Add source tracking if custom field is configured
    const sourceField = process.env.SHARPSPRING_SOURCE_FIELD;
    if (source && sourceField) {
      leadData[sourceField] = source;
    }

    if (existingLead?.leadID) {
      await updateLeads([{ leadID: existingLead.leadID, ...leadData }]);
      return NextResponse.json({ success: true, action: 'updated', leadID: existingLead.leadID });
    } else {
      leadData.leadStatus = 'Open - Not Contacted';
      const created = await createLeads([leadData]);
      const newLeadId = (created[0] as Record<string, string>)?.id;
      return NextResponse.json({ success: true, action: 'created', leadID: newLeadId }, { status: 201 });
    }
  } catch (error) {
    console.error('SharpSpring lead capture error:', error);
    return NextResponse.json(
      { error: 'Failed to save lead information' },
      { status: 500 }
    );
  }
}
```

**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.

### 3. 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.

```
// app/api/sharpspring/pipeline/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getLeads } from '@/lib/sharpspring';
import { createClient } from '@supabase/supabase-js';

const CACHE_TTL_MINUTES = 30;

interface PipelineData {
  stages: Array<{ name: string; count: number }>;
  byOwner: Array<{ ownerID: string; count: number }>;
  totalLeads: number;
  syncedAt: string;
}

async function fetchFromSharpSpring(): Promise<PipelineData> {
  const leads = await getLeads(undefined, 500);

  const stageCounts = new Map<string, number>();
  const ownerCounts = new Map<string, number>();

  for (const lead of leads) {
    const stage = lead.leadStatus || 'Unknown';
    stageCounts.set(stage, (stageCounts.get(stage) || 0) + 1);

    if (lead.ownerID) {
      ownerCounts.set(lead.ownerID, (ownerCounts.get(lead.ownerID) || 0) + 1);
    }
  }

  return {
    stages: Array.from(stageCounts.entries()).map(([name, count]) => ({ name, count })),
    byOwner: Array.from(ownerCounts.entries()).map(([ownerID, count]) => ({ ownerID, count })),
    totalLeads: leads.length,
    syncedAt: new Date().toISOString(),
  };
}

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const forceRefresh = searchParams.get('refresh') === 'true';

  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
  const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
  const supabase = supabaseUrl && serviceKey ? createClient(supabaseUrl, serviceKey) : null;

  // Try cache first
  if (supabase && !forceRefresh) {
    const { data: cached } = await supabase
      .from('sharpspring_pipeline_cache')
      .select('data, synced_at')
      .order('synced_at', { ascending: false })
      .limit(1)
      .single();

    if (cached) {
      const ageMinutes = (Date.now() - new Date(cached.synced_at).getTime()) / 60000;
      if (ageMinutes < CACHE_TTL_MINUTES) {
        return NextResponse.json({ ...cached.data, fromCache: true });
      }
    }
  }

  try {
    const data = await fetchFromSharpSpring();

    if (supabase) {
      await supabase.from('sharpspring_pipeline_cache').insert({ data, synced_at: data.syncedAt });
    }

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: (error as Error).message },
      { status: 500 }
    );
  }
}
```

**Expected result:** A pipeline summary API route with Supabase caching that returns lead counts by stage and owner, ready for dashboard visualizations.

### 4. 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.

```
// app/api/sharpspring/webhook/route.ts
// NOTE: SharpSpring webhooks require a public URL.
// This endpoint cannot receive events in Bolt's WebContainer preview.
// Deploy to Netlify first, then register your URL in SharpSpring Settings → API Settings → Webhooks.
import { NextRequest, NextResponse } from 'next/server';

interface SharpSpringWebhookPayload {
  event: string;
  lead?: {
    leadID: string;
    emailAddress: string;
    leadStatus: string;
    firstName: string;
    lastName: string;
  };
}

export async function POST(request: NextRequest) {
  let payload: SharpSpringWebhookPayload;

  try {
    payload = await request.json();
  } catch {
    return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
  }

  const { event, lead } = payload;

  console.log(`SharpSpring webhook: ${event}`, {
    leadID: lead?.leadID,
    email: lead?.emailAddress,
    status: lead?.leadStatus,
  });

  switch (event) {
    case 'lead.status_changed':
      // TODO: Sync updated status to your Supabase database
      console.log(`Lead ${lead?.emailAddress} status changed to: ${lead?.leadStatus}`);
      break;

    case 'lead.created':
      console.log(`New SharpSpring lead: ${lead?.firstName} ${lead?.lastName} <${lead?.emailAddress}>`);
      break;

    case 'lead.score_changed':
      console.log(`Lead score changed for: ${lead?.emailAddress}`);
      break;

    default:
      console.log(`Unhandled SharpSpring event: ${event}`);
  }

  // Always return 200 — SharpSpring retries on non-200
  return NextResponse.json({ received: true });
}
```

**Expected result:** A deployed Netlify app with SharpSpring lead capture and pipeline reading working in production, with a webhook handler for lead event notifications.

## 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

## 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

## 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.

```
// Correct method names
body: JSON.stringify({
  method: 'createLeads',  // correct
  params: { objects: [leadData] },
  id: requestId,
})
// 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.

```
// Fetch valid lead status values from SharpSpring
const fields = await apiRequest('getLeadFields', {});
// Look for the 'leadStatus' field and its available options
```

### SharpSpring 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.

## 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.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/sharpspring
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/sharpspring
