Connect Retool to Freshsales by creating a REST API Resource using your Freshworks domain-specific base URL and API key authentication. Build CRM dashboards that display lead pipeline, contact activity, deal stages, and account health — giving sales teams a faster view than Freshsales' native interface.
| Fact | Value |
|---|---|
| Tool | Freshsales |
| Category | Marketing |
| Method | REST API Resource |
| Difficulty | Beginner |
| Time required | 15 minutes |
| Last updated | April 2026 |
Build a Freshsales CRM Dashboard in Retool
Freshsales is designed for individual sales reps navigating their own pipelines, but sales managers and revenue operations teams need cross-team views: all deals in a stage across all reps, lead response time by source, account health combining CRM activity with support ticket history, or a bulk re-assignment tool when a rep leaves the team. Retool lets you build these views directly against the Freshsales API.
Freshsales API v2 provides access to contacts, leads, deals, accounts, sales activities, and sequences. Authentication uses an API key passed as a Bearer token — no OAuth flow required, making setup straightforward. The base URL is specific to your Freshworks account domain: https://your-domain.myfreshworks.com/crm/sales/api/v2.
The most impactful Retool use case for Freshsales is a sales manager dashboard that shows all deals across all reps in a single Table, filterable by stage, deal value, and expected close date. Combined with an account view that surfaces Freshdesk ticket history alongside CRM activity, sales managers can see the complete customer picture — active opportunities, open support issues, recent interactions — without switching between tools.
Integration method
Freshsales connects to Retool via a REST API Resource using API key authentication passed as a Bearer token in the Authorization header. The Freshsales API uses a Freshworks domain-specific base URL in the format https://your-domain.myfreshworks.com/crm/sales/api/. Retool proxies all API calls server-side, keeping credentials secure and eliminating CORS issues.
Prerequisites
- A Freshsales (Freshworks CRM) account with contacts, deals, or leads data
- Your Freshworks account domain name (the subdomain before .myfreshworks.com when logged into Freshsales)
- Your Freshsales API key (found in Freshsales → Settings → API Settings)
- A Retool account with permission to create Resources
- Basic familiarity with Retool's query editor and Table component
Step-by-step guide
Find your Freshsales API key and domain URL
In Freshsales, click your profile avatar in the top-right corner and select Settings. In the Settings sidebar, scroll down to the API Settings section. You'll see a field labeled Your API Key with a visible or masked value — click the copy icon to copy it. Your Freshworks domain is the subdomain visible in your browser URL when logged into Freshsales. If you access Freshsales at https://acme.myfreshworks.com, your domain is 'acme'. The full API base URL will be: https://acme.myfreshworks.com/crm/sales/api/v2. Freshsales API keys are account-level and have the same permissions as the user account they belong to. For a shared Retool tool that needs access to all contacts, deals, and accounts across the entire organization, use the API key from an admin account. Individual user API keys may be restricted by Freshsales territory rules or deal visibility settings. Note: Freshworks recently rebranded from Freshsales Suite to Freshworks CRM. The API endpoints and authentication mechanism remain the same — the API key approach works for both older Freshsales and newer Freshworks CRM accounts.
Pro tip: Use an admin account's API key for shared Retool tools. Freshsales has territory-based visibility rules that may prevent non-admin API keys from accessing records assigned to other reps or territories.
Expected result: You have your Freshsales API key and know your Freshworks domain (e.g., 'yourcompany' from yourcompany.myfreshworks.com).
Create the Freshsales REST API Resource in Retool
In Retool, navigate to the Resources tab and click Add Resource. Select REST API from the connector list. Name: Freshsales API Base URL: https://YOUR_DOMAIN.myfreshworks.com/crm/sales/api/v2 Replace YOUR_DOMAIN with your actual Freshworks subdomain. Include /crm/sales/api/v2 in the base URL. Authentication: Select Bearer Token. In the Token field, enter your Freshsales API key. For better security, store the API key in a Retool Configuration Variable first: Settings → Configuration Variables → create FRESHSALES_API_KEY → mark as secret. Then reference it in the Token field as {{ retoolContext.configVars.FRESHSALES_API_KEY }}. Add a default header: Content-Type: application/json. Click Create Resource. Test the connection immediately with a simple query: GET /contacts/filters with no additional parameters. This endpoint returns the list of available contact filter views and should respond with a 200 OK and a JSON body containing filter objects. If you get 401, verify the API key is correct and the Authorization header is being sent as 'Token token=YOUR_KEY' (some Freshsales API versions require this format rather than standard Bearer).
Pro tip: Some Freshsales API versions require the Authorization header format 'Token token=YOUR_API_KEY' rather than 'Bearer YOUR_API_KEY'. If Bearer token auth returns 401, try switching to a custom header: Authorization: Token token={{ retoolContext.configVars.FRESHSALES_API_KEY }}.
Expected result: The Freshsales REST API Resource is created. GET /contacts/filters returns a 200 OK with a list of available filter views.
Query contacts and leads with filtering and search
Freshsales uses a filter-based system for listing records. To fetch contacts, use POST /contacts/filter with a JSON body specifying the filter criteria. This is Freshsales' preferred approach for programmatic queries — the GET /contacts endpoint returns all contacts without filtering, which is impractical for large databases. Create a getContacts query: POST /contacts/filter with body: { "filter_rule": [], "sort": "last_contacted", "sort_type": "desc", "page": 1, "per_page": 25 }. An empty filter_rule array returns all contacts. Add filter rules to narrow results — for example, to filter contacts assigned to a specific rep: { "attribute": "owner_id", "operator": "is_in", "value": [{{ select_owner.value }}] }. For leads, use POST /leads/filter with the same structure. Freshsales treats leads (prospects who haven't been qualified) separately from contacts (qualified prospects connected to accounts). Create a searchCRM query for global search: GET /search?q={{ textInput_search.value }}&include=contact,lead,deal,account. This searches across all CRM entity types simultaneously. Bind getContacts to a Table component. Configure columns: display_name, email, phone, mobile_number, owner_name, created_at, and last_contacted. Freshsales returns nested objects — the owner is in a contact.owner object, so you'll need a transformer to flatten these.
1// Transformer for Freshsales contacts2const contacts = data?.contacts || [];3return contacts.map(contact => ({4 id: contact.id,5 name: contact.display_name || `${contact.first_name || ''} ${contact.last_name || ''}`.trim(),6 email: contact.email || '',7 phone: contact.work_number || contact.mobile_number || '',8 company: contact.company_name || '',9 owner: contact.owner?.name || 'Unassigned',10 owner_id: contact.owner?.id || null,11 lead_score: contact.lead_score || 0,12 created_at: contact.created_at ? new Date(contact.created_at).toLocaleDateString() : '',13 last_contacted: contact.last_contacted ? new Date(contact.last_contacted).toLocaleDateString() : 'Never',14 stage: contact.lifecycle_stage?.name || '',15 tags: (contact.tags || []).join(', ')16}));Pro tip: Freshsales' POST /filter endpoint is more powerful than GET for listing records. Use it with sort and sort_type parameters to control ordering, and filter_rule arrays to narrow results without building complex query strings.
Expected result: The getContacts query returns a flattened array of contacts displayed in a Table with owner name, company, and last contacted date.
Fetch deals and build the pipeline view
Create a getDeals query: POST /deals/filter with body: { "filter_rule": [], "sort": "updated_at", "sort_type": "desc", "page": 1, "per_page": 25, "include": "deal_stage,owner,account" }. The include parameter embeds related objects so you get deal stage name and owner name without separate lookups. Freshsales deals contain: id, name, amount, deal_stage (object with id and name), expected_close, owner (object with id and name), account (object with id and name), probability, and updated_at. Create a getDealStages query: GET /deals/stages to fetch all pipeline stages. This returns an array of stage objects with id and name that you can use to populate a Stage dropdown filter and for the pipeline column layout. For the pipeline view, build a transformer that groups deals by stage name: const byStage = {}; deals.forEach(deal => { const stage = deal.deal_stage || 'Unassigned'; byStage[stage] = [...(byStage[stage] || []), deal]; }); Create an updateDeal query: PUT /deals/{{ table_deals.selectedRow.data.id }} with body including the fields to update. Use this to move deals between stages and update expected close dates from Retool without switching to Freshsales.
1// Transformer for deals with pipeline grouping2const deals = data?.deals || [];3return deals.map(deal => ({4 id: deal.id,5 name: deal.name,6 amount: deal.amount ? `$${parseFloat(deal.amount).toLocaleString()}` : '$0',7 amount_raw: parseFloat(deal.amount || 0),8 stage: deal.deal_stage?.name || 'Unknown',9 stage_id: deal.deal_stage?.id || null,10 probability: `${deal.probability || 0}%`,11 expected_close: deal.expected_close ? new Date(deal.expected_close).toLocaleDateString() : 'None',12 owner: deal.owner?.name || 'Unassigned',13 account: deal.account?.name || 'No Account',14 updated_at: new Date(deal.updated_at).toLocaleDateString(),15 weighted_value: ((parseFloat(deal.amount || 0) * (deal.probability || 0)) / 100).toFixed(0)16}));Pro tip: Always use include=deal_stage,owner,account in the deals filter body. Without it, Freshsales returns only IDs for related objects, requiring separate API calls to resolve each stage name and owner.
Expected result: The deals Table shows all pipeline deals with stage names, owners, and weighted values. The getDealStages query populates a stage filter dropdown.
Add CRM record creation and activity logging
Build contact creation with a Modal containing form fields: First Name, Last Name, Email (required), Phone, Company Name, and Owner Select (populated from GET /selector/owners which returns all team members available for assignment). Create a createContact query: POST /contacts with body: { "contact": { "first_name": "{{ input_firstName.value }}", "last_name": "{{ input_lastName.value }}", "email": "{{ input_email.value }}", "work_number": "{{ input_phone.value }}", "company_name": "{{ input_company.value }}", "owner_id": {{ select_owner.value }} } }. For activity logging (calling, emailing, meeting), use POST /sales_activities with body: { "sales_activity": { "title": "{{ input_title.value }}", "note": "{{ textArea_note.value }}", "start_date": "{{ datepicker_start.value }}", "end_date": "{{ datepicker_end.value }}", "sales_activity_type_id": {{ select_activityType.value }}, "targetable_id": {{ table_contacts.selectedRow.data.id }}, "targetable_type": "Contact" } }. Fetch activity types for the dropdown: GET /selector/sales_activity_types. This returns types like Call, Email, Meeting, and Demo that map to activity_type_id values. For complex Freshsales setups combining lead scoring, automated sequences, and multi-territory visibility rules with your internal database, RapidDev's team can help architect the Retool solution.
1// createContact request body2{3 "contact": {4 "first_name": "{{ input_firstName.value }}",5 "last_name": "{{ input_lastName.value }}",6 "email": "{{ input_email.value }}",7 "work_number": "{{ input_phone.value }}",8 "company_name": "{{ input_company.value }}",9 "owner_id": {{ select_owner.value }},10 "lead_source_id": {{ select_leadSource.value || 'null' }},11 "tags": {{ JSON.stringify((input_tags.value || '').split(',').map(t => t.trim()).filter(Boolean)) }}12 }13}Pro tip: Freshsales validates email format strictly on contact creation and returns a 422 error with field-level validation messages in the errors object. Add a validation rule to the email input field in your Retool form before allowing submission.
Expected result: The Create Contact Modal allows filling in contact details and assigning an owner. Submitting creates the contact in Freshsales and refreshes the contacts table.
Common use cases
Build a deal pipeline management dashboard
Create a Retool dashboard showing all Freshsales deals across all sales reps, grouped by deal stage and filterable by rep, account, and expected close date. Managers can move deals between stages, update expected close dates, and reassign deals to other reps without logging into Freshsales.
Build a Retool deal pipeline panel showing all Freshsales deals with stage, deal value, expected close date, assigned rep, and account name. Add a Kanban-style column view by deal stage (Qualified, Demo, Proposal, Negotiation, Closed Won/Lost). Include filters by rep and date range. Add an Update Stage dropdown for the selected deal.
Copy this prompt to try it in Retool
Build a lead scoring and assignment panel
Build a Retool tool showing all new Freshsales leads with lead score, source, and time since creation. Ops teams can bulk-assign leads to reps based on territory or product interest, and managers can see which lead sources produce the highest-scoring leads to optimize marketing spend.
Build a Retool lead management panel. Show all Freshsales leads with lead score, source, created date, assigned rep, and email. Sort by lead score descending. Add a bulk assignment feature: multiselect rows, choose a rep from dropdown, click Assign. Include a Chart showing lead count by source for the last 30 days.
Copy this prompt to try it in Retool
Build a unified customer view combining CRM and support data
Create a Retool panel that shows a Freshsales account with its deals and contacts alongside Freshdesk support ticket history. Account managers can review the customer's open deals, recent wins, and any outstanding support issues in a single view before a renewal conversation.
Build a Retool account 360 view. Search for a Freshsales account by name. Show: account details, all contacts with role/email/phone, all deals with stage and value, and recent Freshdesk tickets (fetch from a separate Freshdesk REST API Resource). Display everything in a clean multi-section Container layout.
Copy this prompt to try it in Retool
Troubleshooting
401 Unauthorized despite entering the correct API key
Cause: Freshsales requires the Authorization header in a specific format — some API versions use 'Token token=YOUR_KEY' rather than the standard Bearer token format. If the Bearer token format is rejected, the request fails authentication.
Solution: In your Retool REST API Resource, switch from the Bearer Token auth type to Custom Headers. Add a header: Key: Authorization, Value: Token token={{ retoolContext.configVars.FRESHSALES_API_KEY }}. This matches Freshsales' required format. Also verify the API key is current — Freshsales keys can be regenerated, invalidating older keys.
1// Custom header format for Freshsales auth:2// Header key: Authorization3// Header value: Token token=YOUR_FRESHSALES_API_KEYPOST /contacts/filter returns an empty contacts array despite records existing in Freshsales
Cause: The filter_rule array may be using incorrect attribute names or operator values. Freshsales filter attributes are case-sensitive and use internal field names that differ from the UI labels.
Solution: Start with an empty filter_rule array to return all contacts and confirm the endpoint works. Then add one filter rule at a time. Use GET /contacts/filters to see available filter definitions and their exact attribute names. Common attribute names: owner_id (not assigned_to), lifecycle_stage_id (not stage), created_at (date filters require date format YYYY-MM-DD).
1// Correct filter_rule format for Freshsales:2// { "filter_rule": [3// { "attribute": "owner_id", "operator": "is_in", "value": [123] }4// ], "sort": "created_at", "sort_type": "desc", "page": 1 }Deal stage names show as null or undefined in the Table
Cause: The deals filter request did not include the include parameter, so Freshsales returned only deal stage IDs rather than stage objects with names.
Solution: Add include: 'deal_stage,owner,account' to the POST /deals/filter request body (not as a URL parameter). Freshsales uses the include field in the JSON body, not in query parameters, for the filter endpoint. After adding it, deal_stage will be an object with id and name fields.
1// POST /deals/filter body with include:2{3 "filter_rule": [],4 "sort": "updated_at",5 "sort_type": "desc",6 "page": 1,7 "per_page": 25,8 "include": "deal_stage,owner,account"9}Best practices
- Store your Freshsales API key in Retool Configuration Variables (Settings → Configuration Variables) marked as secret — never put it directly in the resource auth fields without using a config variable reference.
- Use POST /filter endpoints instead of GET list endpoints for all Freshsales entity queries — filter endpoints support sorting, pagination, and field-level filtering that the basic GET endpoints lack.
- Always include the include parameter in deal and contact filter requests to embed related objects (owner, stage, account) — this eliminates N+1 API calls for resolving IDs to names.
- Cache reference data queries (GET /selector/owners, GET /deals/stages, GET /selector/sales_activity_types) for 10 minutes — these lists change rarely and caching prevents redundant calls.
- Use Freshsales' POST /search endpoint for global CRM search across contacts, leads, deals, and accounts simultaneously — this is more efficient than running separate filter queries on each entity type.
- Implement server-side pagination with per_page: 25 and bind the page parameter to your Table's pagination offset — Freshsales databases can contain thousands of contacts and client-side pagination on large datasets degrades performance.
- For bulk operations like reassigning multiple contacts to a new rep, use Retool's loop query pattern or a JavaScript query with Promise.all() to run updates in parallel — but limit concurrency to avoid Freshsales rate limits.
- When combining Freshsales data with Freshdesk support data in a unified customer view, use the contact's email address as the join key since it's the common identifier between both Freshworks products.
Alternatives
HubSpot has a native Retool connector with pre-built actions, a broader ecosystem including marketing and service hubs, and more extensive reporting — choose it over Freshsales if your team needs deeper marketing automation or already uses HubSpot.
Pipedrive is pipeline-focused with a simpler data model than Freshsales — choose it if your sales process is straightforward and you want a CRM that maps directly to deal stage progression without territory or lead scoring complexity.
Zoho CRM offers more extensive customization and a broader Zoho ecosystem integration — choose it over Freshsales if your team already uses other Zoho products like Zoho Books or Zoho Desk.
Frequently asked questions
Does Retool have a native Freshsales connector?
No, Retool does not have a dedicated native connector for Freshsales. You connect via a REST API Resource using Freshsales API key authentication. Setup takes about 15 minutes and provides access to all Freshsales API v2 endpoints including contacts, leads, deals, accounts, sales activities, and sequences.
Can I access all contacts across all reps in Freshsales from Retool?
Yes, if you use an admin account's API key. Freshsales has territory-based visibility rules that restrict non-admin users to contacts and deals in their assigned territory. For a shared Retool dashboard that shows all CRM data across the whole sales team, configure the REST API Resource with an admin account's API key stored in Retool Configuration Variables.
How do I filter Freshsales contacts by a specific owner in Retool?
Use the POST /contacts/filter endpoint with a filter_rule in the request body: { "filter_rule": [{ "attribute": "owner_id", "operator": "is_in", "value": [OWNER_ID] }] }. First fetch available owners using GET /selector/owners to get their IDs, then bind the selected owner ID from a Retool Select component to the filter_rule value.
Can Retool write back to Freshsales — create contacts or update deals?
Yes. Use POST /contacts or POST /deals to create records, and PUT /contacts/{id} or PUT /deals/{id} to update them. Freshsales enforces required fields (email for contacts, name and deal_stage_id for deals) and returns 422 Unprocessable Entity with field-level error details if validation fails. Configure Retool form validation before submitting to catch common errors early.
How do I combine Freshsales CRM data with Freshdesk support ticket data in Retool?
Create two separate Retool Resources: one for Freshsales API and one for Freshdesk API. Both use API key authentication but have different base URLs. In your Retool app, search for a customer by email in Freshsales (GET /search?q=email), then use the same email to query Freshdesk for matching tickets (GET /search/tickets?query='requester_email:email'). Display both datasets in a side-by-side Container for a unified customer view.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation