Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Freshsales

To integrate Replit with Freshsales, generate an API key from your Freshsales profile settings, store it in Replit Secrets (lock icon πŸ”’), and call the Freshsales REST API from your Python or Node.js server to manage leads, contacts, deals, and sales activities. Your Freshsales subdomain is part of every API URL, so store it as a Secret too.

What you'll learn

  • How to generate a Freshsales API key and identify your account subdomain
  • How to store Freshsales credentials securely in Replit Secrets
  • How to create and update leads and contacts using Python and Node.js
  • How to manage deals and link them to contacts in the Freshsales pipeline
  • How to search and filter CRM records using the Freshsales filter API
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read20 minutesMarketingMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with Freshsales, generate an API key from your Freshsales profile settings, store it in Replit Secrets (lock icon πŸ”’), and call the Freshsales REST API from your Python or Node.js server to manage leads, contacts, deals, and sales activities. Your Freshsales subdomain is part of every API URL, so store it as a Secret too.

Why Connect Replit to Freshsales?

Freshsales is part of the Freshworks ecosystem, which means it connects natively with Freshdesk (support), Freshchat (messaging), and Freshmarketer (marketing automation). Integrating your Replit app with Freshsales lets you feed sales pipeline data directly from product events β€” creating a lead when a user signs up for a trial, moving a deal stage when a user upgrades, or logging an activity when a key in-app action occurs. This eliminates the manual data entry that slows down sales teams.

The Freshsales API uses straightforward API key authentication, making it easier to integrate than OAuth-based CRMs like Salesforce or HubSpot. Every resource β€” leads, contacts, accounts, deals, notes, tasks β€” is accessible through a RESTful JSON API. The same API key used for manual testing works in production, so there is no separate OAuth flow to implement for server-to-server integrations.

Replit's Secrets system (lock icon πŸ”’ in the sidebar) keeps your Freshsales API key encrypted and separate from your codebase. Because the API key provides full access to your CRM data including private deal notes and contact information, it must be treated as a sensitive credential. Always call the Freshsales API from server-side code in your Replit backend β€” never from client-side JavaScript that runs in the user's browser.

Integration method

Standard API Integration

You connect Replit to Freshsales by generating an API key from your Freshsales account profile, storing it along with your Freshsales subdomain in Replit Secrets, and calling the Freshsales REST API from your server-side Python or Node.js code. All requests authenticate with a Token header containing your API key. The API base URL includes your account's unique subdomain (e.g., yourcompany.freshsales.io), which must be stored separately from the key.

Prerequisites

  • A Replit account with a Python or Node.js project created
  • A Freshsales account (free tier supports API access)
  • Your Freshsales subdomain (the part before .freshsales.io in your account URL)
  • Basic familiarity with REST APIs and HTTP headers
  • Python 3.10+ or Node.js 18+ (both available on Replit by default)

Step-by-step guide

1

Generate a Freshsales API Key and Store Credentials in Replit Secrets

Log in to your Freshsales account and click your profile avatar in the top-right corner. Select 'Profile Settings' from the dropdown. On the profile page, scroll down to the 'API Settings' section. You will see your API key displayed β€” click 'Generate' if one does not exist yet. Copy the full API key. You also need your Freshsales subdomain. Look at your browser's address bar β€” your Freshsales URL is in the format https://yourcompany.freshsales.io. The subdomain is the part before .freshsales.io (e.g., 'yourcompany'). This subdomain is part of every API URL, so you need it alongside the key. Open your Replit project and click the lock icon πŸ”’ in the left sidebar to open the Secrets pane. Add two secrets: - FRESHSALES_API_KEY: your API key - FRESHSALES_SUBDOMAIN: your subdomain (just 'yourcompany', without .freshsales.io) In Python, access these with os.environ['FRESHSALES_API_KEY'] and os.environ['FRESHSALES_SUBDOMAIN']. In Node.js, use process.env.FRESHSALES_API_KEY and process.env.FRESHSALES_SUBDOMAIN. The full API base URL for your account is https://{subdomain}.freshsales.io/api.

Pro tip: Freshsales API keys are tied to the user who generated them. If that user's account is deactivated, the API key stops working. Consider creating a dedicated 'API Integration' user in Freshsales admin settings and generating the key from that account for resilience.

Expected result: FRESHSALES_API_KEY and FRESHSALES_SUBDOMAIN secrets appear in the Replit Secrets pane, accessible as environment variables in your code.

2

Create and Manage Leads and Contacts in Python

The Freshsales API authenticates with a Token scheme in the Authorization header. The exact format is: Authorization: Token token={your_api_key}. Note this differs from Bearer token authentication β€” the format is 'Token token=' not 'Bearer '. Freshsales has separate endpoints for Leads (/api/leads) and Contacts (/api/contacts). Leads represent potential customers not yet in your CRM; Contacts are qualified prospects or customers. Leads can be converted to Contacts, Accounts, and Deals in a single API call. The Python code below shows how to create leads, search for existing contacts to prevent duplicates, and update deal stages. The search endpoint supports filtering by email, which is useful for checking if a lead already exists before creating a duplicate.

freshsales_client.py
1import os
2import requests
3from typing import Optional
4
5API_KEY = os.environ["FRESHSALES_API_KEY"]
6SUBDOMAIN = os.environ["FRESHSALES_SUBDOMAIN"]
7BASE_URL = f"https://{SUBDOMAIN}.freshsales.io/api"
8
9# Freshsales uses 'Token token=' format β€” not 'Bearer'
10HEADERS = {
11 "Authorization": f"Token token={API_KEY}",
12 "Content-Type": "application/json"
13}
14
15def create_lead(email: str, first_name: str, last_name: str,
16 company: str = "", mobile: str = "") -> dict:
17 """Create a new lead in Freshsales."""
18 payload = {
19 "lead": {
20 "email": email,
21 "first_name": first_name,
22 "last_name": last_name,
23 "company": {"name": company} if company else None,
24 "mobile_number": mobile
25 }
26 }
27 # Remove None values
28 payload["lead"] = {k: v for k, v in payload["lead"].items() if v is not None}
29
30 response = requests.post(f"{BASE_URL}/leads", json=payload, headers=HEADERS)
31 response.raise_for_status()
32 return response.json().get("lead", {})
33
34def search_contacts_by_email(email: str) -> Optional[dict]:
35 """Search for an existing contact by email address."""
36 params = {"q": email, "include": "owner"}
37 response = requests.get(f"{BASE_URL}/search", params=params, headers=HEADERS)
38 response.raise_for_status()
39 results = response.json()
40 contacts = results.get("contacts", [])
41 return contacts[0] if contacts else None
42
43def create_deal(name: str, contact_id: int, amount: float,
44 stage_id: int = None) -> dict:
45 """Create a deal linked to a contact."""
46 payload = {
47 "deal": {
48 "name": name,
49 "amount": amount,
50 "contacts_added_list": [contact_id]
51 }
52 }
53 if stage_id:
54 payload["deal"]["deal_stage_id"] = stage_id
55
56 response = requests.post(f"{BASE_URL}/deals", json=payload, headers=HEADERS)
57 response.raise_for_status()
58 return response.json().get("deal", {})
59
60def update_deal_stage(deal_id: int, stage_id: int) -> dict:
61 """Move a deal to a different pipeline stage."""
62 payload = {"deal": {"deal_stage_id": stage_id}}
63 response = requests.put(
64 f"{BASE_URL}/deals/{deal_id}", json=payload, headers=HEADERS
65 )
66 response.raise_for_status()
67 return response.json().get("deal", {})
68
69def get_deal_stages() -> list:
70 """Get all deal stages in the pipeline."""
71 response = requests.get(f"{BASE_URL}/settings/deal_stages", headers=HEADERS)
72 response.raise_for_status()
73 return response.json().get("deal_stages", [])
74
75# Example usage
76if __name__ == "__main__":
77 stages = get_deal_stages()
78 print("Pipeline stages:")
79 for stage in stages:
80 print(f" {stage['id']}: {stage['name']}")
81
82 lead = create_lead(
83 email="prospect@example.com",
84 first_name="Alex",
85 last_name="Smith",
86 company="Acme Corp"
87 )
88 print(f"Lead created: ID {lead.get('id')}, {lead.get('email')}")

Pro tip: Call GET /api/settings/deal_stages first to get the correct deal_stage_id values for your pipeline. Stage IDs are account-specific numbers β€” you cannot hardcode them across different Freshsales accounts.

Expected result: The script prints your pipeline stages and creates a test lead in Freshsales. The lead appears in the Leads view of your Freshsales account.

3

Build a Node.js Express Integration Server

The Node.js integration follows the same API key authentication pattern using the 'Token token=' header format. The Express server below exposes endpoints for creating leads from web form submissions and updating deal stages based on product events. A key feature of this server is idempotent lead creation: before creating a new lead, it searches for an existing contact or lead with the same email. This prevents duplicate records in your CRM when users fill out a form multiple times or when webhooks are delivered more than once. The server also includes a notes endpoint for logging activities. Adding notes to contacts and deals is important for keeping the sales team informed about what triggered the API call β€” for example, noting that a lead was created by a web form submission from the 'Google Ads' campaign. Install dependencies with 'npm install express axios' in the Replit shell.

server.js
1const express = require('express');
2const axios = require('axios');
3
4const app = express();
5app.use(express.json());
6
7const API_KEY = process.env.FRESHSALES_API_KEY;
8const SUBDOMAIN = process.env.FRESHSALES_SUBDOMAIN;
9const BASE_URL = `https://${SUBDOMAIN}.freshsales.io/api`;
10
11const freshsales = axios.create({
12 baseURL: BASE_URL,
13 headers: {
14 'Authorization': `Token token=${API_KEY}`,
15 'Content-Type': 'application/json'
16 }
17});
18
19// Create a lead from a web form submission
20app.post('/leads', async (req, res) => {
21 const { email, firstName, lastName, company, source } = req.body;
22 if (!email || !firstName) {
23 return res.status(400).json({ error: 'email and firstName are required' });
24 }
25
26 try {
27 // Check for existing contact first
28 const searchResp = await freshsales.get('/search', {
29 params: { q: email }
30 });
31 const existingContacts = searchResp.data.contacts || [];
32 if (existingContacts.length > 0) {
33 return res.json({
34 success: true,
35 existing: true,
36 contactId: existingContacts[0].id
37 });
38 }
39
40 // Create new lead
41 const payload = {
42 lead: {
43 email,
44 first_name: firstName,
45 last_name: lastName || '',
46 lead_source_name: source || 'Web'
47 }
48 };
49 if (company) payload.lead.company = { name: company };
50
51 const { data } = await freshsales.post('/leads', payload);
52 res.json({ success: true, existing: false, leadId: data.lead.id });
53 } catch (err) {
54 console.error('Lead creation error:', err.response?.data || err.message);
55 res.status(500).json({ error: err.message });
56 }
57});
58
59// Add a note to a contact or deal
60app.post('/notes', async (req, res) => {
61 const { notable_type, notable_id, description } = req.body;
62 if (!notable_type || !notable_id || !description) {
63 return res.status(400).json({ error: 'notable_type, notable_id, and description are required' });
64 }
65
66 try {
67 const { data } = await freshsales.post('/notes', {
68 note: { notable_type, notable_id, description }
69 });
70 res.json({ success: true, noteId: data.note.id });
71 } catch (err) {
72 console.error('Note error:', err.response?.data || err.message);
73 res.status(500).json({ error: err.message });
74 }
75});
76
77// Update deal stage
78app.put('/deals/:dealId/stage', async (req, res) => {
79 const { stageId } = req.body;
80 if (!stageId) return res.status(400).json({ error: 'stageId is required' });
81
82 try {
83 const { data } = await freshsales.put(`/deals/${req.params.dealId}`, {
84 deal: { deal_stage_id: stageId }
85 });
86 res.json({ success: true, deal: data.deal });
87 } catch (err) {
88 res.status(500).json({ error: err.message });
89 }
90});
91
92app.get('/health', (req, res) => res.json({ status: 'ok' }));
93
94app.listen(3000, '0.0.0.0', () => {
95 console.log('Freshsales integration server running on port 3000');
96});

Pro tip: Freshsales rate limits API calls to approximately 1000 requests per hour per API key. For bulk operations like syncing large contact lists, add delays between requests or use the batch import feature in the Freshsales UI for the initial data load.

Expected result: The server starts and POST /leads creates a test lead in your Freshsales account. The GET /health endpoint returns 200 confirming the server is running.

4

Deploy on Replit and Set Up Webhooks

Freshsales supports outbound webhooks that notify your Replit server when records change β€” a contact is updated, a deal stage changes, or a note is added. This allows your app to react to CRM changes without polling the API. To configure webhooks in Freshsales, go to Settings > Integrations > Webhooks > New Webhook. Enter your deployed Replit URL as the webhook URL and select the events you want to receive. Freshsales webhooks send JSON POST requests with the event type and the full record data. Deploy your Replit app by clicking 'Deploy' in the editor. Autoscale is recommended for this integration β€” it handles the occasional webhook delivery from Freshsales without requiring always-on resources. After deployment, copy the stable URL (e.g., https://your-app.replit.app) and use it as the webhook target in Freshsales. Test the webhook by manually updating a deal stage in Freshsales and checking your Replit deployment logs to confirm the event arrived.

webhook_server.py
1# Flask webhook receiver for Freshsales events
2from flask import Flask, request, jsonify
3import os
4
5app = Flask(__name__)
6
7@app.route('/freshsales/webhook', methods=['POST'])
8def freshsales_webhook():
9 data = request.get_json(force=True)
10 if not data:
11 return jsonify({'error': 'No data'}), 400
12
13 event = data.get('event', 'unknown')
14 object_type = data.get('object_type', 'unknown') # lead, contact, deal
15 record = data.get('object', {})
16
17 print(f"Freshsales event: {event} on {object_type} {record.get('id')}")
18
19 if event == 'deal_updated' and object_type == 'deal':
20 stage = record.get('deal_stage', {}).get('name', 'unknown')
21 amount = record.get('amount', 0)
22 print(f"Deal moved to: {stage}, amount: ${amount}")
23 # Trigger downstream actions, e.g., notify Slack channel
24
25 elif event == 'lead_created' and object_type == 'lead':
26 email = record.get('email', '')
27 print(f"New lead: {email}")
28 # Add to email nurture sequence
29
30 return jsonify({'status': 'received'}), 200
31
32@app.route('/health', methods=['GET'])
33def health():
34 return jsonify({'status': 'ok'}), 200
35
36if __name__ == '__main__':
37 app.run(host='0.0.0.0', port=3000)

Pro tip: Freshsales webhook payloads do not include a signature header, so validate incoming webhooks by checking a secret token in the URL query string that you set when configuring the webhook URL.

Expected result: Your deployed Replit app receives Freshsales webhook events and logs them to the console. Changes in Freshsales appear in your deployment logs within seconds.

Common use cases

Automatic Lead Creation from Web Forms

When a potential customer fills out a contact or demo request form on your Replit-hosted website, automatically create a Freshsales lead with their name, email, company, and form submission data. Assign the lead to the right sales rep based on company size or geography using Freshsales assignment rules, so no lead is ever manually created.

Replit Prompt

Build a Flask endpoint that receives web form submissions and creates a Freshsales lead using the API key from Replit Secrets, assigning the lead to a specific owner ID and tagging it with the campaign source.

Copy this prompt to try it in Replit

Deal Stage Updates from Product Events

When users in your Replit app hit significant milestones β€” starting a free trial, requesting a quote, or completing a key feature β€” your server automatically advances the associated Freshsales deal to the correct pipeline stage. Sales managers see real-time deal progression without waiting for reps to manually update records.

Replit Prompt

Write a Node.js function that looks up a Freshsales deal by contact email, updates the deal stage to 'Trial Active' and adds a note with the trial start date when a user activates a trial in the application.

Copy this prompt to try it in Replit

CRM Data Sync for Customer Analytics

A nightly Replit script pulls closed-won deals from the last 30 days via the Freshsales API, transforms the data, and loads it into a data warehouse or analytics database. This gives your team a queryable history of sales performance without needing direct database access to Freshsales or expensive BI tool integrations.

Replit Prompt

Create a Python script that queries Freshsales for all deals closed in the past 30 days using the filter API, extracts deal value, owner, and close date, and writes the results to a CSV file in Replit's file system.

Copy this prompt to try it in Replit

Troubleshooting

API returns 401 Unauthorized on all requests

Cause: The Authorization header format is wrong. Freshsales uses 'Token token={key}' β€” not 'Bearer {key}'. Using the wrong scheme causes all requests to fail with 401.

Solution: Verify the Authorization header is exactly: 'Token token=' followed by your API key with no extra spaces. In the Replit Secrets pane, confirm the FRESHSALES_API_KEY value has no leading or trailing whitespace.

typescript
1# Python: correct Freshsales auth header
2headers = {
3 "Authorization": f"Token token={os.environ['FRESHSALES_API_KEY']}",
4 "Content-Type": "application/json"
5}
6
7// Node.js: correct format
8const headers = {
9 'Authorization': `Token token=${process.env.FRESHSALES_API_KEY}`,
10 'Content-Type': 'application/json'
11};

API returns 404 Not Found for all endpoints

Cause: The FRESHSALES_SUBDOMAIN Secret is incorrect or includes extra text. The base URL must be https://{subdomain}.freshsales.io/api β€” if the subdomain is wrong, every endpoint returns 404.

Solution: Open your Freshsales account in a browser and check the URL β€” your subdomain is the first segment before .freshsales.io. Update the FRESHSALES_SUBDOMAIN Secret with only the subdomain portion, without 'https://', '.freshsales.io', or any trailing slashes.

typescript
1# Python: verify URL construction
2subdomain = os.environ['FRESHSALES_SUBDOMAIN'] # e.g., 'mycompany'
3base_url = f"https://{subdomain}.freshsales.io/api"
4print(f"API URL: {base_url}")

Deal creation returns 422 Unprocessable Entity with 'Invalid deal stage'

Cause: The deal_stage_id is hardcoded to an ID that does not exist in your Freshsales pipeline. Stage IDs are account-specific integers and differ between Freshsales accounts.

Solution: Call GET /api/settings/deal_stages to get the correct stage IDs for your account. Store the required stage IDs in Replit Secrets (e.g., FRESHSALES_NEW_LEAD_STAGE_ID) rather than hardcoding numeric values.

typescript
1# Get your pipeline stage IDs
2response = requests.get(f"{BASE_URL}/settings/deal_stages", headers=HEADERS)
3for stage in response.json().get('deal_stages', []):
4 print(f"Stage: {stage['id']} -> {stage['name']}")

Search API returns empty results even though the contact exists

Cause: The search query must be a minimum of 3 characters and Freshsales search indexes may have a short delay after a record is created before it appears in search results.

Solution: For immediate lookups after creation, use the GET /contacts endpoint with an email filter parameter instead of the search endpoint: GET /api/contacts?q[email]={email}. This queries the database directly rather than the search index and returns results immediately.

typescript
1# Direct contact lookup by email (bypasses search index)
2params = {"q[email]": email}
3response = requests.get(f"{BASE_URL}/contacts", params=params, headers=HEADERS)
4contacts = response.json().get("contacts", [])

Best practices

  • Store FRESHSALES_API_KEY and FRESHSALES_SUBDOMAIN in Replit Secrets (lock icon πŸ”’) β€” the subdomain is as important as the key since it forms the base URL for all requests.
  • Use the 'Token token=' Authorization header format exactly β€” Freshsales does not use Bearer token authentication and the wrong format causes silent 401 failures.
  • Check for existing contacts before creating new ones to avoid duplicate records β€” use the search API with the email address as the query.
  • Retrieve deal stage IDs dynamically via GET /api/settings/deal_stages rather than hardcoding them, since stage IDs are account-specific integers.
  • Generate the API key from a dedicated service account user rather than a personal account to prevent the integration from breaking if a team member leaves.
  • Deploy your Replit app before setting up Freshsales webhooks β€” use your stable replit.app URL, not the development session URL.
  • Use Autoscale deployment for lead capture and CRM update integrations, and Reserved VM only if you require guaranteed sub-second webhook response times.
  • Add informative notes to contacts and deals via the API to document what triggered each CRM update β€” this helps the sales team understand the context of automated changes.

Alternatives

Frequently asked questions

How do I store my Freshsales API key in Replit?

Click the lock icon πŸ”’ in the left sidebar of your Replit project to open the Secrets pane. Add FRESHSALES_API_KEY with your Freshsales API key, and FRESHSALES_SUBDOMAIN with your account subdomain (the part before .freshsales.io). Access these in Python with os.environ['FRESHSALES_API_KEY'] and in Node.js with process.env.FRESHSALES_API_KEY.

Where do I find my Freshsales subdomain?

Your Freshsales subdomain is in the URL when you are logged in to your account. The format is https://yourcompany.freshsales.io β€” the subdomain is the 'yourcompany' part. Store only this portion (without https:// or .freshsales.io) in the FRESHSALES_SUBDOMAIN Secret.

Does the Freshsales API work with Replit on the free plan?

Yes. The Freshsales API is available on all Freshsales plans including the free tier. Replit's free tier supports outbound API calls without restriction. You will need Replit's paid plan for always-on deployments needed for webhook reception, since free Replit projects sleep when inactive.

What is the difference between a Freshsales Lead and a Contact?

In Freshsales, Leads are unqualified prospects not yet confirmed as potential customers. Contacts are qualified prospects or existing customers. You typically create Leads from web forms or cold outreach, and convert them to Contacts (plus an Account and a Deal) once qualified. The API has separate /leads and /contacts endpoints for each resource type.

How do I prevent duplicate contacts when syncing from Replit?

Before creating a contact or lead, search for an existing record with the same email using GET /api/search?q={email} or GET /api/contacts?q[email]={email}. If a record is found, use PUT /contacts/{id} to update it rather than creating a new one. This prevents duplicate entries and keeps your CRM data clean.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation β€” no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.