Skip to main content
RapidDev - Software Development Agency
retool-integrationsREST API Resource

How to Integrate Retool with Freshsales

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.

What you'll learn

  • How to locate your Freshsales API key and configure it in a Retool REST API Resource
  • How to query Freshsales contacts, leads, deals, and accounts with filtering
  • How to build a sales pipeline dashboard showing deal stages and values
  • How to create and update CRM records from Retool forms
  • How to combine Freshsales data with Freshdesk support tickets for a full customer view
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner13 min read15 minutesMarketingLast updated April 2026RapidDev Engineering Team
TL;DR

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.

Quick facts about this guide
FactValue
ToolFreshsales
CategoryMarketing
MethodREST API Resource
DifficultyBeginner
Time required15 minutes
Last updatedApril 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

REST API Resource

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

1

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

2

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.

3

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.

transformer_contacts.js
1// Transformer for Freshsales contacts
2const 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.

4

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.

transformer_deals.js
1// Transformer for deals with pipeline grouping
2const 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.

5

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.

createContact_body.json
1// createContact request body
2{
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.

Retool Prompt

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.

Retool Prompt

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.

Retool Prompt

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.

typescript
1// Custom header format for Freshsales auth:
2// Header key: Authorization
3// Header value: Token token=YOUR_FRESHSALES_API_KEY

POST /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).

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

typescript
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

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.

RapidDev

Talk to an Expert

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

Book a free consultation

Integrations are where projects stall

We wire Retool integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

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.