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
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
Generate a Freshsales API Key and Store Credentials in Replit Secrets
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.
Create and Manage Leads and Contacts in Python
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.
1import os2import requests3from typing import Optional45API_KEY = os.environ["FRESHSALES_API_KEY"]6SUBDOMAIN = os.environ["FRESHSALES_SUBDOMAIN"]7BASE_URL = f"https://{SUBDOMAIN}.freshsales.io/api"89# Freshsales uses 'Token token=' format β not 'Bearer'10HEADERS = {11 "Authorization": f"Token token={API_KEY}",12 "Content-Type": "application/json"13}1415def 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": mobile25 }26 }27 # Remove None values28 payload["lead"] = {k: v for k, v in payload["lead"].items() if v is not None}2930 response = requests.post(f"{BASE_URL}/leads", json=payload, headers=HEADERS)31 response.raise_for_status()32 return response.json().get("lead", {})3334def 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 None4243def 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_id5556 response = requests.post(f"{BASE_URL}/deals", json=payload, headers=HEADERS)57 response.raise_for_status()58 return response.json().get("deal", {})5960def 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=HEADERS65 )66 response.raise_for_status()67 return response.json().get("deal", {})6869def 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", [])7475# Example usage76if __name__ == "__main__":77 stages = get_deal_stages()78 print("Pipeline stages:")79 for stage in stages:80 print(f" {stage['id']}: {stage['name']}")8182 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.
Build a Node.js Express Integration Server
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.
1const express = require('express');2const axios = require('axios');34const app = express();5app.use(express.json());67const API_KEY = process.env.FRESHSALES_API_KEY;8const SUBDOMAIN = process.env.FRESHSALES_SUBDOMAIN;9const BASE_URL = `https://${SUBDOMAIN}.freshsales.io/api`;1011const freshsales = axios.create({12 baseURL: BASE_URL,13 headers: {14 'Authorization': `Token token=${API_KEY}`,15 'Content-Type': 'application/json'16 }17});1819// Create a lead from a web form submission20app.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 }2526 try {27 // Check for existing contact first28 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].id37 });38 }3940 // Create new lead41 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 };5051 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});5859// Add a note to a contact or deal60app.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 }6566 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});7677// Update deal stage78app.put('/deals/:dealId/stage', async (req, res) => {79 const { stageId } = req.body;80 if (!stageId) return res.status(400).json({ error: 'stageId is required' });8182 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});9192app.get('/health', (req, res) => res.json({ status: 'ok' }));9394app.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.
Deploy on Replit and Set Up Webhooks
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.
1# Flask webhook receiver for Freshsales events2from flask import Flask, request, jsonify3import os45app = Flask(__name__)67@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'}), 4001213 event = data.get('event', 'unknown')14 object_type = data.get('object_type', 'unknown') # lead, contact, deal15 record = data.get('object', {})1617 print(f"Freshsales event: {event} on {object_type} {record.get('id')}")1819 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 channel2425 elif event == 'lead_created' and object_type == 'lead':26 email = record.get('email', '')27 print(f"New lead: {email}")28 # Add to email nurture sequence2930 return jsonify({'status': 'received'}), 2003132@app.route('/health', methods=['GET'])33def health():34 return jsonify({'status': 'ok'}), 2003536if __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.
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.
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.
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.
1# Python: correct Freshsales auth header2headers = {3 "Authorization": f"Token token={os.environ['FRESHSALES_API_KEY']}",4 "Content-Type": "application/json"5}67// Node.js: correct format8const 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.
1# Python: verify URL construction2subdomain = 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.
1# Get your pipeline stage IDs2response = 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.
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
Pipedrive offers a cleaner pipeline-focused API with better deal management features and is a good alternative if your primary need is visualizing and moving deals through a sales funnel.
HubSpot provides a more comprehensive CRM plus marketing automation with a larger free tier and more extensive documentation, making it the better choice for teams that also need inbound marketing tools.
Zoho CRM is part of the same affordable software ecosystem and includes a generous free tier with more customization options, making it a good fit for cost-conscious teams already using Zoho apps.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation