> For the complete documentation index, see [llms.txt](https://docs.aisera.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aisera.com/aisera-platform/adding-data-to-your-tenant/integrations-and-data-sources/connectors/freshdesk-connector.md).

# FreshDesk Connector

The **FreshDesk Connector is a Generic Connector** that integrates with the **FreshDesk v2 REST API** (`https://<subdomain>.freshdesk.com/api/v2/`) to ingest Tickets, Contacts/Agents (Users & User Profiles), Groups, and Knowledge Base articles (Solution Articles) into Aisera.

* **Connector Type ID:** `232` (`ExternalSystemTypeEnum.FreshDesk`)
* `externalSystemTypeEnum` **value (used in Override Configurations):** `"FreshDesk"`
* **Base URL:** `https://<subdomain>.freshdesk.com`
* **API Version:** v2 (`/api/v2/`)

FreshDesk is a **different product from Fresh Connector** (also called "Freshervice"). While both are part of the Freshworks ecosystem and share some API conventions, they have distinct API paths, data structures, and content types.

### Authentication Types Supported <a href="#authentication-types-supported" id="authentication-types-supported"></a>

FreshDesk currently supports only one authentication method: Basic Authentication with API Key.

Note: In the Integration configuration for FreshDesk there is also an option for OAuth. This is simply a provision for our Admin UI to be proactive if FreshDesk’s API starts offering this option.

#### Basic Authentication (API Key) <a href="#basic-authentication-api-key" id="basic-authentication-api-key"></a>

The FreshDesk API uses HTTP Basic Auth with the **API key as the username** and **any non-empty string** (e.g. `X`) as the password. The API key is Base64-encoded together with the dummy password.

**To find your API key:**

1. Log into your FreshDesk account.
2. Click your avatar (top-right) → **Profile Settings**.
3. Your API key is shown on the right side under **Your API Key**.

**Setting up in Aisera:**

* In the Authentication tab of the Data Source, choose **Basic**.
* Enter the **API key** in the `Username` field.
* Enter any string (e.g. `X`) in the `Password` field.

### Global Configuration <a href="#global-configuration" id="global-configuration"></a>

FreshDesk Connector is a Generic Connector and therefore most of its functions depend on a JSON Configuration Script that is stored in the database:

`{"xml2json":false,"externalSystemTypeEnum":"FreshDesk","supportectContentTypes":["Ticket","Incident","User","UserProfile","UserGroup","KnowledgeArticle"],"contentTypeConfiguration":{"Ticket":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"nested-call","responseParamMappings":{},"pathParamMappings":{"id":"id"},"nestedConfigParamMappings":{},"nestedCallHasEntries":false,"nestedCallExpandsEntry":true,"nestedCallExpandsEntryWithOnlyFirstResult":false,"nestedCallAppendEntry":true,"nestedCallAppendPath":"comments","nestedCallConfiguration":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/tickets/{_id_}/conversations"}],"supportedOperations":["LIST"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"paginationConfig":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","parameters":[{"field":"updated_since","operator":"eq","value":"{_startDate_}"}],"tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"useParallelCalls":false,"sleepOnZeroResults":false,"zeroResultsRetries":0,"zeroResultsRetryInterval":0,"payloadFilterMappings":{},"enrichParamMappings":{},"enrichAppend":false,"nestedResponseFilterConfigurations":[],"copyDateFiltersFromExternalRequest":false,"defaultLimit":0,"defaultOffset":0,"isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/tickets?include=description&order_by=updated_at&order_type=desc","pathByKey":"/api/v2/tickets/{_key_}"}],"supportedOperations":["LIST","GET","CREATE","UPDATE","DELETE"],"operations":[{"operation":"UPDATE","headers":{},"comments":{"idPath":"id","commentsPath":"comments","removeCommentsFromPayload":true,"contentTypeConfiguration":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pathMappings":[{"path":"/api/v2/tickets/{_id_}/notes"}],"supportedOperations":["CREATE"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]}},"useFormPayload":false,"useBinaryPayload":false,"customClose":false,"returnGetTableEntryOnClose":false,"customAddComment":false,"useCreatePathInUpdate":false,"listEntryJoltTransforms":[]},{"operation":"GET","httpMethod":"get","headers":{},"enrich":{"listConfiguration":{"responseParamMappings":{},"pathParamMappings":{"id":"id"},"nestedConfigParamMappings":{},"nestedCallHasEntries":false,"nestedCallExpandsEntry":true,"nestedCallExpandsEntryWithOnlyFirstResult":false,"nestedCallAppendEntry":true,"nestedCallAppendPath":"comments","nestedCallConfiguration":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/tickets/{_id_}/conversations"}],"supportedOperations":["LIST"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"paginationConfig":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","parameters":[{"field":"updated_since","operator":"eq","value":"{_startDate_}"}],"tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"useParallelCalls":false,"sleepOnZeroResults":false,"zeroResultsRetries":0,"zeroResultsRetryInterval":0,"payloadFilterMappings":{},"enrichParamMappings":{},"enrichAppend":false,"nestedResponseFilterConfigurations":[],"copyDateFiltersFromExternalRequest":false,"defaultLimit":0,"defaultOffset":0,"isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"idPath":"id","returnRequestPayload":true},"useFormPayload":false,"useBinaryPayload":false,"customClose":false,"returnGetTableEntryOnClose":false,"customAddComment":false,"useCreatePathInUpdate":false,"listEntryJoltTransforms":[]}],"respectRequestPaginationInCount":true,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"itemDatePath":"updated_at","itemDateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","handleNullDatesConfiguration":true,"handleDefaultDatesConfiguration":true,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"User":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","parameters":[{"field":"updated_since","operator":"eq","value":"{_startDate_}"}],"tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/contacts","pathByKey":"/api/v2/contacts/{_key_}"}],"supportedOperations":["LIST","GET"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"itemDatePath":"updated_at","itemDateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"UserProfile":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","parameters":[{"field":"updated_since","operator":"eq","value":"{_startDate_}"}],"tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/contacts","pathByKey":"/api/v2/contacts/{_key_}"}],"supportedOperations":["LIST","GET"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"itemDatePath":"updated_at","itemDateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"Incident":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"nested-call","responseParamMappings":{},"pathParamMappings":{"id":"id"},"nestedConfigParamMappings":{},"nestedCallHasEntries":false,"nestedCallExpandsEntry":true,"nestedCallExpandsEntryWithOnlyFirstResult":false,"nestedCallAppendEntry":true,"nestedCallAppendPath":"comments","nestedCallConfiguration":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/tickets/{_id_}/conversations"}],"supportedOperations":["LIST"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"paginationConfig":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","parameters":[{"field":"updated_since","operator":"eq","value":"{_startDate_}"}],"tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"useParallelCalls":false,"sleepOnZeroResults":false,"zeroResultsRetries":0,"zeroResultsRetryInterval":0,"payloadFilterMappings":{},"enrichParamMappings":{},"enrichAppend":false,"nestedResponseFilterConfigurations":[],"copyDateFiltersFromExternalRequest":false,"defaultLimit":0,"defaultOffset":0,"isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/tickets?include=description&order_by=updated_at&order_type=desc","pathByKey":"/api/v2/tickets/{_key_}"}],"supportedOperations":["LIST","GET","CREATE","UPDATE","DELETE"],"operations":[{"operation":"UPDATE","headers":{},"comments":{"idPath":"id","commentsPath":"comments","removeCommentsFromPayload":true,"contentTypeConfiguration":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pathMappings":[{"path":"/api/v2/tickets/{_id_}/notes"}],"supportedOperations":["CREATE"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]}},"useFormPayload":false,"useBinaryPayload":false,"customClose":false,"returnGetTableEntryOnClose":false,"customAddComment":false,"useCreatePathInUpdate":false,"listEntryJoltTransforms":[]},{"operation":"GET","httpMethod":"get","headers":{},"enrich":{"listConfiguration":{"responseParamMappings":{},"pathParamMappings":{"id":"id"},"nestedConfigParamMappings":{},"nestedCallHasEntries":false,"nestedCallExpandsEntry":true,"nestedCallExpandsEntryWithOnlyFirstResult":false,"nestedCallAppendEntry":true,"nestedCallAppendPath":"comments","nestedCallConfiguration":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/tickets/{_id_}/conversations"}],"supportedOperations":["LIST"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"paginationConfig":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","parameters":[{"field":"updated_since","operator":"eq","value":"{_startDate_}"}],"tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"useParallelCalls":false,"sleepOnZeroResults":false,"zeroResultsRetries":0,"zeroResultsRetryInterval":0,"payloadFilterMappings":{},"enrichParamMappings":{},"enrichAppend":false,"nestedResponseFilterConfigurations":[],"copyDateFiltersFromExternalRequest":false,"defaultLimit":0,"defaultOffset":0,"isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"idPath":"id","returnRequestPayload":true},"useFormPayload":false,"useBinaryPayload":false,"customClose":false,"returnGetTableEntryOnClose":false,"customAddComment":false,"useCreatePathInUpdate":false,"listEntryJoltTransforms":[]}],"respectRequestPaginationInCount":true,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"itemDatePath":"updated_at","itemDateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","handleNullDatesConfiguration":true,"handleDefaultDatesConfiguration":true,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"KnowledgeArticle":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"plain","defaultLimit":0,"defaultOffset":0,"isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":""}],"supportedOperations":["LIST"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"responseFilterConfigurations":[],"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"innerAdapterClass":"com.aisera.externalsystems.freshdesk.FreshDeskAdapter","downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"UserGroup":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd''T''HH:mm:ss''Z''","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/groups","pathByKey":"/api/v2/groups/{_key_}"}],"supportedOperations":["LIST","GET"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]}},"contextParameters":{},"basicTokenAuth":false,"startDate":true,"endDate":true,"sleepOnErrors":300000,"retries":10,"rateLimitHeader":"x-ratelimit-remaining","rateLimitRespectNo":5,"rateLimitEntriesSize":5000,"sleepOnRateLimit":60000,"timezone":"UTC","record":false,"useInterval":false,"interval":0,"disableFetchingServiceCatalogCategories":false,"githubRepos":[],"ingestGithubPullRequestCommits":false,"ingestGithubPullRequestReviewComments":false,"ingestGithubPullRequestComments":false,"externalSystemClientV2":{"configuration":{"host":"tenant-server","port":8088,"apiBasePath":"/tenant-server/v2","trustAnySSLCertificate":true}},"secondReading":false,"hasImageProfile":true,"bypassNestedCallsForTestConnection":false,"useRawToken":false,"useRemoteExecutor":false,"useUserCredentials":false,"trustAnySSLCertificate":true,"useImageHandlerWithMetadata":false,"disableArchiving":false,"refreshTokenOn401":false}`

In the Data Source’s Configuration wizard there is a tab named “Overrides”. We can place JSON Configurations in that tab to change the Connector’s behavior.

These parameters appear at the top level of the Override Configuration and control the connector's global behavior.

| Parameter                | Default Value           | Description                                                                     |
| ------------------------ | ----------------------- | ------------------------------------------------------------------------------- |
| `externalSystemTypeEnum` | `"FreshDesk"`           | Must be `"FreshDesk"` for this connector                                        |
| `startDate`              | `true`                  | Enables incremental ingestion (only fetches entries updated since the last run) |
| `endDate`                | `true`                  | Enables end-date tracking                                                       |
| `sleepOnErrors`          | `300000` (5 min)        | Sleep time in milliseconds on non-rate-limit errors before retrying             |
| `sleepOnRateLimit`       | `60000` (60 sec)        | Sleep time in milliseconds when a rate limit response is received               |
| `rateLimitHeader`        | `x-ratelimit-remaining` | FreshDesk response header used to detect remaining rate limit budget            |
| `rateLimitRespectNo`     | `5`                     | Slow down ingestion when remaining API calls drops below this value             |
| `rateLimitEntriesSize`   | `5000`                  | Number of entries processed before a rate limit check                           |
| `retries`                | `10`                    | Max retries on transient (non-rate-limit) errors                                |
| `timezone`               | `"UTC"`                 | Timezone used for date calculations                                             |
| `hasImageProfile`        | `true`                  | Enables image/avatar support                                                    |
| `trustAnySSLCertificate` | `true`                  | Accepts any SSL certificate (useful for some environments)                      |

#### Context Parameters <a href="#context-parameters" id="context-parameters"></a>

These parameters can be placed inside the `contextParameters` object within the Override Configuration and allow fine-grained control of the FreshDesk KB adapter:

| Key                   | Type                    | Default   | Description                                                                                                                                                                  |
| --------------------- | ----------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerPortalKbUri` | `"true"` / `"false"`    | `"false"` | When `"true"`, KB article URIs use the customer portal format (`/support/solutions/articles/{id}`) instead of the agent portal format (`/a/support/solutions/articles/{id}`) |
| `kbPageSize`          | positive integer string | `"100"`   | Number of items per page for all KB pagination calls (categories, folders, sub-folders, articles). Falls back to `100` if invalid or missing                                 |

**Example:**

`{ "externalSystemTypeEnum": "FreshDesk", "contextParameters": { "customerPortalKbUri": "false", "kbPageSize": "100" } }`

***

### Learning Functions <a href="#learning-functions" id="learning-functions"></a>

| Learning Function   | Content Type       | Adapter Used                 | Default Endpoint        |
| ------------------- | ------------------ | ---------------------------- | ----------------------- |
| Learn Tickets       | `Ticket`           | Generic (GenericAdapterJSON) | `/api/v2/tickets`       |
| Learn Users         | `User`             | Generic (GenericAdapterJSON) | `/api/v2/contacts`      |
| Learn User Profiles | `UserProfile`      | Generic (GenericAdapterJSON) | `/api/v2/contacts`      |
| Learn User Groups   | `UserGroup`        | Generic (GenericAdapterJSON) | `/api/v2/groups`        |
| Learn KBs           | `KnowledgeArticle` | Custom `FreshDeskAdapter`    | `/api/v2/solutions/...` |

***

#### Learn Tickets / Incidents <a href="#learn-tickets-incidents" id="learn-tickets-incidents"></a>

Ticket learning fetches FreshDesk tickets and their full conversation history (replies and notes).

**How It Works**

1. The connector lists tickets from `/api/v2/tickets` with incremental filtering via `updated_since={startDate}`.
2. For each ticket, a **nested call** fetches conversations from `/api/v2/tickets/{id}/conversations`.
3. Conversations are appended to the ticket JSON under the `comments` array.
4. The JOLT transformation **converts the integer** `priority` **field to a human-readable string**.

**Important:** FreshDesk omits the ticket `description` from list responses by default. The query parameter `include=description` must be included in the list call to receive the full description.

**API Endpoints**

| Operation          | Method | Endpoint                                                                                            |
| ------------------ | ------ | --------------------------------------------------------------------------------------------------- |
| List tickets       | GET    | `/api/v2/tickets?include=description&order_by=updated_at&order_type=desc&updated_since={startDate}` |
| Get single ticket  | GET    | `/api/v2/tickets/{id}`                                                                              |
| List conversations | GET    | `/api/v2/tickets/{id}/conversations`                                                                |
| Create ticket      | POST   | `/api/v2/tickets`                                                                                   |
| Update ticket      | PUT    | `/api/v2/tickets/{id}`                                                                              |
| Delete ticket      | DELETE | `/api/v2/tickets/{id}`                                                                              |
| Add note/comment   | POST   | `/api/v2/tickets/{id}/notes`                                                                        |

**Pagination**

* **Type:** Page-based
* **Page parameter:** `page`
* **Limit parameter:** `per_page` (default: 100)
* **Date format:** `yyyy-MM-dd'T'HH:mm:ss'Z'`
* **Incremental filter:** `updated_since={startDate}`
* **Sort order:** `order_by=updated_at&order_type=desc`
* **Date tracking field:** `updated_at`

**Priority Mapping**

FreshDesk encodes ticket priority as an integer. The JOLT transformation converts it to a string label:

| Integer | String Label |
| ------- | ------------ |
| `1`     | `Low`        |
| `2`     | `Medium`     |
| `3`     | `High`       |
| `4`     | `Urgent`     |

> **Note:** FreshService uses different labels: `1=Low, 2=Moderate, 3=High, 4=Critical`.

**Ticket Status Values**

| Integer | Status   |
| ------- | -------- |
| `2`     | Open     |
| `3`     | Pending  |
| `4`     | Resolved |
| `5`     | Closed   |

**Default Field Mappings**

Field Mappings are loaded by default when we create a new Data Source. Admin UI users can edit the field mappings according to each FreshDesk instance’s configuration to better adapt to and process the API’s responses.

| Internal Field              | FreshDesk Field             | Notes                                                                                        |
| --------------------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
| `ticketId`                  | `id`                        |                                                                                              |
| `ticketDisplayId`           | `id`                        | Same as `id`                                                                                 |
| `ticketTitle`               | `subject`                   |                                                                                              |
| `ticketDescription`         | `description`               | Must use `include=description` in list call                                                  |
| `ticketStatus`              | `status`                    | Integer (see Status Values above)                                                            |
| `ticketPriority`            | `priority`                  | Converted to string via JOLT                                                                 |
| `ticketCategory`            | `category`                  |                                                                                              |
| `ticketSubCategory`         | `sub_category`              |                                                                                              |
| `ticketCreationDate`        | `created_at`                | Format: `yyyy-MM-dd'T'HH:mm:ss'Z'`                                                           |
| `ticketUpdatedAt`           | `updated_at`                | Format: `yyyy-MM-dd'T'HH:mm:ss'Z'`                                                           |
| `ticketReporterId`          | `requester_id`              | Ticket submitter                                                                             |
| `ticketReporterEmail`       | `email`                     |                                                                                              |
| `ticketAssignmentGroupId`   | `group_id`                  | Agent group assigned to ticket                                                               |
| `ticketAssignedToId`        | `responder_id`              | Individual assigned agent                                                                    |
| `ticketUri`                 | *(fixed value + JS script)* | Set to `https://<subdomain>.freshdesk.com/a/tickets/{_ID_}`; `{_ID_}` is replaced at runtime |
| `ticketCommentId`           | `$.comments[*].id`          | From nested conversations call                                                               |
| `ticketCommentText`         | `$.comments[*].body_text`   | From nested conversations call                                                               |
| `ticketCommentCreationDate` | `$.comments[*].created_at`  | Format: `yyyy-MM-dd'T'HH:mm:ss'Z'`                                                           |

**Ticket URI Configuration**

The `ticketUri` field is built in two steps:

1. A **fixed value** acts as the URL template (e.g. `https://mycompany.freshdesk.com/a/tickets/{_ID_}`).
2. The JS script `freshdesk.ticket.adduri.js` replaces `{_ID_}` with the actual ticket ID at runtime.

To configure: find the `ticketUri` field mapping and update the **Fixed Value** to match your subdomain.

| Use case               | Fixed value                                              |
| ---------------------- | -------------------------------------------------------- |
| Agent portal (default) | `https://mycompany.freshdesk.com/a/tickets/{_ID_}`       |
| Customer portal        | `https://mycompany.freshdesk.com/support/tickets/{_ID_}` |

**Filtering Private Comments**

By default, FreshDesk tickets include **all conversations** — both public replies and private notes (marked `"private": true`). To exclude private notes from ingestion, apply the following **Override Document Transformation**:

**Override Document Transformation JSON** (paste into the Data Source's Override Document Transformation field):

`{"Ticket":[{"externalSystemTypeId":232,"contentTypeName":"Ticket","name":"Filter Private Comments","transformationType":"SCRIPT","direction":"JSON2Proto","step":"PRE","content":"var comments = jsobject.get(\"comments\");\nvar JSONArray = Java.type('org.json.simple.JSONArray');\nvar filteredComments = new JSONArray();\nvar forEach = Array.prototype.forEach;\n\nif (comments) {\n forEach.call(comments, function(comment) {\n var isPrivate = comment.get(\"private\");\n if (isPrivate === false) {\n filteredComments.add(comment)\n }\n });\n}\njsobject.put(\"comments\", filteredComments);\njsobject;","transformationOrder":1}]}`

The underlying JavaScript logic:

`var comments = jsobject.get("comments"); var JSONArray = Java.type('org.json.simple.JSONArray'); var filteredComments = new JSONArray(); var forEach = Array.prototype.forEach; if (comments) { forEach.call(comments, function(comment) { var isPrivate = comment.get("private"); if (isPrivate === false) { filteredComments.add(comment) } }); } jsobject.put("comments", filteredComments); jsobject;`

This script checks the `private` field on each conversation entry. Only conversations where `private === false` (public replies) are retained. All agent-internal notes (`private === true`) are discarded before processing.

> **Note:** In FreshDesk terminology, ticket "conversations" include both public replies (shown to the requester) and private notes (visible only to agents). The `private` boolean field distinguishes between them.

***

#### Learn Users & User Profiles <a href="#learn-users-and-user-profiles" id="learn-users-and-user-profiles"></a>

FreshDesk supports two distinct user types: **Contacts** (end-users/customers) and **Agents** (support staff).

* **Default behavior:** The connector ingests **Contacts** from `/api/v2/contacts`.
* **With Override Configuration:** Switch to ingesting **Agents** from `/api/v2/agents`.

To ingest User Profiles, you **must** also enable User Learning in the same Data Source. Selecting only "Learn User Profiles" without "Learn Users" will prevent entries from being properly stored.

**Default FreshDesk Type: Contacts**

Contacts are end-users who raise support tickets. In FreshDesk, their `email` and `name` are **top-level fields**.

**API Endpoints:**

| Operation          | Method | Endpoint                                     |
| ------------------ | ------ | -------------------------------------------- |
| List contacts      | GET    | `/api/v2/contacts?updated_since={startDate}` |
| Get single contact | GET    | `/api/v2/contacts/{id}`                      |

**Pagination:**

* Page-based (`page`, `per_page=100`)
* Incremental filter: `updated_since={startDate}`
* Date tracking field: `updated_at`

**Sample Contact JSON:**

`{ "id": 456, "email": "customer@example.com", "name": "Customer Name", "company_id": 789, "created_at": "2024-01-15T10:00:00Z", "updated_at": "2024-03-20T14:30:00Z" }`

**Default User Field Mappings (Contacts):**

| Internal Field             | FreshDesk Field | Notes                |
| -------------------------- | --------------- | -------------------- |
| `userExternalId`           | `id`            |                      |
| `userIdentitiesExternalId` | `id`            |                      |
| `userEmail`                | `email`         | Top-level field      |
| `userUsername`             | `email`         |                      |
| `userFirstName`            | `name`          | Full name, top-level |

**Default User Profile Field Mappings (Contacts):**

| Internal Field                    | FreshDesk Field | Notes                      |
| --------------------------------- | --------------- | -------------------------- |
| `userProfileIdentityExternalId`   | `id`            |                            |
| `userProfileIdentityEmail`        | `email`         | Top-level field            |
| `userProfileIdentityFirstName`    | `name`          | Full name, top-level       |
| `userProfileIdentityUserName`     | `email`         |                            |
| `userProfileRoleName`             | *(fixed)*       | Always `"user"`            |
| `userProfileWorkInfoDepartmentId` | `company_id`    | Single company association |

**Switching to Agents**

To ingest Agents instead of Contacts, apply an **Override Configuration** that switches the `User` and `UserProfile` content type endpoints from `/api/v2/contacts` to `/api/v2/agents`.

In the Override Configuration, the `User` and `UserProfile` sections should point to `/api/v2/agents` with `tableEntriesArrayPath: "top"` (agents are returned as a plain JSON array).

**Override Configuration snippet** (replace the `User` and `UserProfile` blocks in your full Override Configuration):

`{"contentTypeConfiguration":{"User":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd'T'HH:mm:ss'Z'","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/agents","pathByKey":"/api/v2/agents/{_key_}"}],"supportedOperations":["LIST","GET"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]},"UserProfile":{"appliesToChildren":true,"httpContentType":"application/json","acceptContentType":"application/json","pagination":{"type":"page","useIncrementalPaging":false,"hasMoreField":false,"hasPageTotalCountField":false,"pageParam":"page","limitParam":"per_page","defaultLimit":100,"defaultOffset":0,"dateFormat":"yyyy-MM-dd'T'HH:mm:ss'Z'","tableEntriesArrayPath":"top","isTableEntryJSONArray":false,"retriesNo":0,"appendTopElement":false,"overrideWithDefaultLimit":false},"pathMappings":[{"path":"/api/v2/agents","pathByKey":"/api/v2/agents/{_key_}"}],"supportedOperations":["LIST","GET"],"respectRequestPaginationInCount":false,"respectEndDate":false,"respectStartDate":false,"discardEntriesOutOfStartOrEndDate":false,"handleNullDatesConfiguration":false,"handleDefaultDatesConfiguration":false,"nullDateStartHoursBefore":0,"nullDateEndHoursAfter":0,"addElementAsArrayUnderTop":false,"downloadFiles":false,"timezone":"UTC","doNotFailOnNestedCall":false,"isAttachment":false,"isFileDownloadRequest":false,"prefetchConfigurations":[]}}}`

**Agents:** In FreshDesk, an agent's `email` and `name` are **nested inside a** `contact` **sub-object**, unlike contacts where these are top-level fields. This means **field mappings must be updated** when switching to Agent ingestion.

**Updated Field Mappings for Agents:**

To switch to Agent Learning, we need to manually update the field mappings in the Admin UI.

Depending on the API responses of each FreshDesk instance, you might need to make the appropriate adjustments. Not all fields need to be mapped, the last fields in the following table can be ignored if not needed.

| Internal Field                    | Contact Path (default) | Agent Path           | Change Reason                         |
| --------------------------------- | ---------------------- | -------------------- | ------------------------------------- |
| `userEmail`                       | `email`                | `contact.email`      | Agent email is nested under `contact` |
| `userUsername`                    | `email`                | `contact.email`      | Same                                  |
| `userFirstName`                   | `name`                 | `contact.name`       | Agent name is nested under `contact`  |
| `userProfileIdentityEmail`        | `email`                | `contact.email`      | Same                                  |
| `userProfileIdentityFirstName`    | `name`                 | `contact.name`       | Same                                  |
| `userProfileIdentityUserName`     | `email`                | `contact.email`      | Same                                  |
| `userProfileRoleName`             | `"user"` *(fixed)*     | `"agent"` *(fixed)*  | Different role label                  |
| `userProfileWorkInfoDepartmentId` | `company_id`           | `department_ids.[0]` | Agents have a department ID array     |
| `userProfileWorkInfoGroupNames`   | *(not mapped)*         | `group_ids.[*]`      | Group membership (agents only)        |
| `userProfileWorkDepartmentIds`    | *(not mapped)*         | `department_ids.[*]` | Full department array (agents only)   |
| `userGroupIds`                    | *(not mapped)*         | `group_ids.[*]`      | Group IDs (agents only)               |

> **Important:** When switching to Agent ingestion you must update all email and name field mappings in both the User and User Profile sections to use the `contact.email` and `contact.name` paths. The fixed value for `userProfileRoleName` should also be changed from `"user"` to `"agent"`.

***

#### Learn User Groups <a href="#learn-user-groups" id="learn-user-groups"></a>

FreshDesk Groups map to Aisera User Groups. Groups in FreshDesk represent teams of agents who handle certain types of tickets.

**API Endpoints**

| Operation        | Method | Endpoint              |
| ---------------- | ------ | --------------------- |
| List groups      | GET    | `/api/v2/groups`      |
| Get single group | GET    | `/api/v2/groups/{id}` |

**Pagination**

* **Type:** Page-based
* **Page parameter:** `page`
* **Limit parameter:** `per_page` (default: 100)
* **Incremental filter:** None — the FreshDesk Groups API does not support `updated_since`
* Groups are returned as a plain JSON array, auto-wrapped to `{"top": [...]}` by the framework

**Field Mappings**

| Internal Field              | FreshDesk Field | Notes        |
| --------------------------- | --------------- | ------------ |
| `userGroupExternalId`       | `id`            |              |
| `userGroupEntityExternalId` | `id`            | Same as `id` |
| `userGroupName`             | `name`          |              |

> **FreshDesk vs. FreshService:** FreshDesk uses a single `/api/v2/groups` endpoint for all groups. FreshService has separate endpoints: `/api/v2/groups` for agent groups and `/api/v2/requester_groups` for requester groups. FreshDesk does not have a dedicated requester groups endpoint.

***

#### Learn Knowledge Base Articles <a href="#learn-knowledge-base-articles" id="learn-knowledge-base-articles"></a>

KB learning uses the **custom** `FreshDeskAdapter` class because of the multi-level hierarchical discovery required: Categories → Folders → Sub-folders (recursive) → Articles.

**Discovery Flow**

`GET /api/v2/solutions/categories └─ for each category: GET /api/v2/solutions/categories/{categoryId}/folders └─ for each folder: GET /api/v2/solutions/folders/{folderId}/subfolders ← always called └─ for each subfolder (recursive): GET /api/v2/solutions/folders/{subfolderId}/subfolders GET /api/v2/solutions/folders/{folderId}/articles`

The adapter:

1. Fetches all **categories** from the solutions API.
2. For each category, fetches all **folders**.
3. For each folder, recursively explores **sub-folders** (always calling the subfolder endpoint; if empty, no recursion occurs).
4. For each folder/subfolder, **paginates through articles** (page by page, default 100 per page).
5. Before emitting each article, **injects enrichment fields** (folder, category, visibility, KB URI).
6. Applies any configured **response filter rules**.

**API Endpoints**

| Call                     | Method | Endpoint                                    |
| ------------------------ | ------ | ------------------------------------------- |
| List categories          | GET    | `/api/v2/solutions/categories`              |
| List folders by category | GET    | `/api/v2/solutions/categories/{id}/folders` |
| List sub-folders         | GET    | `/api/v2/solutions/folders/{id}/subfolders` |
| List articles by folder  | GET    | `/api/v2/solutions/folders/{id}/articles`   |

**Pagination**

Each call uses page-based pagination:

* **Page parameter:** `page`
* **Per-page:** `per_page=100` (configurable via `kbPageSize` context parameter)
* **Stop condition:** A page returning fewer items than `per_page` indicates the last page
* **Duplicate-page guard:** If two consecutive pages return identical JSON, pagination stops to prevent infinite loops

In **test connection mode**, traversal is limited to 5 categories/folders and returns after the first article.

**Injected Fields**

Before each article is passed to the mapping pipeline, the adapter enriches it with the following fields:

| Injected Field                  | Source                       | Description                                                        |
| ------------------------------- | ---------------------------- | ------------------------------------------------------------------ |
| `computed_category_folder_path` | Built during traversal       | Full hierarchical path, e.g. `"IT Help > Hardware > Laptops"`      |
| `folder`                        | Parent folder JSON object    | Full folder object embedded in the article                         |
| `category`                      | Parent category JSON object  | Full category object embedded in the folder                        |
| `folder_visibility`             | `folder.visibility`          | Integer — see Folder Visibility Values                             |
| `folder_company_ids`            | `folder.company_ids`         | List of company IDs; present when `folder_visibility == 4`         |
| `folder_contact_segment_ids`    | `folder.contact_segment_ids` | List of contact segment IDs; present when `folder_visibility == 6` |
| `folder_company_segment_ids`    | `folder.company_segment_ids` | List of company segment IDs; present when `folder_visibility == 7` |
| `kbUri`                         | Constructed by adapter       | KB article URL — see KB URI Formats                                |

**Article Status Values**

| Value | Meaning   |
| ----- | --------- |
| `1`   | Draft     |
| `2`   | Published |

**Folder Visibility Values**

| Value | Meaning                   | Extra Fields Populated       |
| ----- | ------------------------- | ---------------------------- |
| `1`   | All Users                 | —                            |
| `2`   | Logged In Users           | —                            |
| `3`   | Agents Only               | —                            |
| `4`   | Selected Companies        | `folder_company_ids`         |
| `5`   | Bots                      | —                            |
| `6`   | Selected Contact Segments | `folder_contact_segment_ids` |
| `7`   | Selected Company Segments | `folder_company_segment_ids` |

**KB URI Formats**

The adapter constructs and injects a `kbUri` field for every article:

| Audience            | URI Format                                                           | Default?                                              |
| ------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- |
| **Agent portal**    | `https://{instance}.freshdesk.com/a/support/solutions/articles/{id}` | Yes                                                   |
| **Customer portal** | `https://{instance}.freshdesk.com/support/solutions/articles/{id}`   | Set `customerPortalKbUri=true` in `contextParameters` |

**KB Field Mappings**

| Internal Field | FreshDesk Field      | Notes                                        |
| -------------- | -------------------- | -------------------------------------------- |
| `kbSubject`    | `title`              | Type: html                                   |
| `kbTitle`      | `title`              | Type: string                                 |
| `kbBody`       | `description`        | HTML content of the article                  |
| `kbURL`        | `kbUri`              | Injected by adapter                          |
| `kbDisplayId`  | `id`                 |                                              |
| `kbExternalId` | `id`                 |                                              |
| `kbCategory`   | `category_id`        | Article's top-level category reference       |
| `kbTagScope`   | *(fixed)*            | Always `"global"`                            |
| `kbTagKey`     | `tag_keys.[*]`       | Injected by visibility transformation script |
| `kbTagValue`   | `tag_value_list.[*]` | Injected by visibility transformation script |

**Editing KB Article URLs**

Before going live, ensure the `kbUri` field mapping points to the correct FreshDesk instance. The adapter constructs URLs using `config.getUri()` (the Data Source base URL), so simply setting the correct base URL (e.g. `https://mycompany.freshdesk.com`) in the Data Source configuration is sufficient — no additional field mapping edits are needed for the URL format.

**Filtering KB Articles by Status or Folder Visibility**

By default, the FreshDesk Connector **does not apply any filtering**. All articles across all statuses and visibility levels are ingested.

You can enable server-side response filtering using the `responseFilterConfigurations` block inside the `KnowledgeArticle` section of the Override Configuration. This approach fetches all articles from the API but discards those that do not pass the configured filter rules before passing them to the mapping pipeline.

**Example 1 — Ingest only Published articles:**

Add the following `responseFilterConfigurations` in the `KnowledgeArticle` content type configuration of the original Override Configuration ([placed in this section](https://automationanywhere.atlassian.net/wiki/spaces/ENGG/pages/edit-v2/7455178938#Global-Configuration)):

`"responseFilterConfigurations": [ { "filterKey": "status", "filterValue": "2", "filterOperator": "NOTEQUAL", "filterOperation": "REMOVE" } ]`

This filter removes every article whose `status` is not equal to `2` (Published). Draft articles (status `1`) are discarded.

**Example 2 — Ingest only Public (All Users) articles:**

`"responseFilterConfigurations": [ { "filterKey": "folder_visibility", "filterValue": "1", "filterOperator": "NOTEQUAL", "filterOperation": "REMOVE" } ]`

This filter removes articles that do not belong to folders with `visibility == 1` (All Users).

**Example 3 — Ingest only Published articles visible to All Users:**

`"responseFilterConfigurations": [ { "filterKey": "status", "filterValue": "2", "filterOperator": "NOTEQUAL", "filterOperation": "REMOVE" }, { "filterKey": "folder_visibility", "filterValue": "1", "filterOperator": "NOTEQUAL", "filterOperation": "REMOVE" } ]`

Both filters are applied; only articles that are *published* **and** in *All Users* folders are kept.

> **How it works:** The `filterKey` is matched against the article JSON object (including injected fields like `folder_visibility`). The `filterOperator: "NOTEQUAL"` means "remove if key's value does not equal `filterValue`". Multiple filters are ANDed — an article must pass all filter rules to be retained.

**Filtering KB Articles by Folder ID (Override Document Transformation)**

For advanced filtering — such as only ingesting articles from specific folders by their numeric ID — use an **Override Document Transformation** with a JavaScript script.

The script receives each article JSON object as `jsobject`, examines the `folder_id` field, and if the folder is not in the approved list, nulls out the `id`, `title`, and `description` fields. This causes the article to be effectively skipped by the mapping pipeline.

**Override Document Transformation — Allow-list of Folder IDs:**

(Needs editing)

`{ "KnowledgeArticle": [ { "externalSystemTypeId": 232, "contentTypeName": "KnowledgeArticle", "name": "Filter by Folder ID", "transformationType": "SCRIPT", "direction": "JSON2Proto", "step": "PRE", "content": "var folderId = jsobject.get(\"folder_id\");\nvar allowedFolderIds = [12001234567, 12001234568, 12001234569];\nvar isAllowed = false;\nfor (var i = 0; i < allowedFolderIds.length; i++) {\n if (allowedFolderIds[i] == folderId) {\n isAllowed = true;\n break;\n }\n}\nif (!isAllowed) {\n print(\"Rejecting folder_id: \" + folderId);\n jsobject.put(\"id\", null);\n jsobject.put(\"title\", null);\n jsobject.put(\"description\", null);\n} else {\n print(\"Accepted folder_id: \" + folderId);\n}\njsobject;", "transformationOrder": 1 } ] }`

The underlying JavaScript:

`var folderId = jsobject.get("folder_id"); var allowedFolderIds = [12001234567, 12001234568, 12001234569]; var isAllowed = false; for (var i = 0; i < allowedFolderIds.length; i++) { if (allowedFolderIds[i] == folderId) { isAllowed = true; break; } } if (!isAllowed) { print("Rejecting folder_id: " + folderId); jsobject.put("id", null); jsobject.put("title", null); jsobject.put("description", null); } else { print("Accepted folder_id: " + folderId); } jsobject;`

**Important**: Replace the `allowedFolderIds` array with the actual numeric IDs of the folders you want to Accept. You can find a folder's ID from the FreshDesk admin portal or from the API response.

> **How it works:** Setting `id`, `title`, and `description` to `null` signals to the mapping pipeline that the article should be treated as empty/invalid and skipped. The `print()` calls write debug messages to the connector logs.

**Fault Tolerance**

The adapter continues exploration even when non-fatal errors occur at any level of the hierarchy:

| Failure scenario                         | Recovery                                                                    |
| ---------------------------------------- | --------------------------------------------------------------------------- |
| Initial categories fetch fails           | Fatal — the run fails completely                                            |
| Folder-list fetch fails for category X   | Category X is skipped; remaining categories continue                        |
| Subfolder exploration fails for folder A | Subfolder traversal for A is skipped; folder A's articles are still fetched |
| Article-list fetch fails for folder C    | Folder C is skipped; remaining folders continue                             |

**Rate Limiting (Custom Adapter)**

The `FreshDeskAdapter` handles HTTP 429 responses directly:

* Reads the `retry-after` header to determine sleep duration
* Default sleep: 60 seconds if `retry-after` is absent
* Maximum retries: 3 before throwing `AdapterOperationException`

***

### API Usage <a href="#api-usage" id="api-usage"></a>

Summary of all FreshDesk API endpoints used by the connector:

| Feature                     | Endpoint                                    | Method | Notes                                              |
| --------------------------- | ------------------------------------------- | ------ | -------------------------------------------------- |
| List tickets                | `/api/v2/tickets`                           | GET    | `include=description`, `updated_since`, pagination |
| Get ticket                  | `/api/v2/tickets/{id}`                      | GET    |                                                    |
| Create ticket               | `/api/v2/tickets`                           | POST   |                                                    |
| Update ticket               | `/api/v2/tickets/{id}`                      | PUT    |                                                    |
| Delete ticket               | `/api/v2/tickets/{id}`                      | DELETE |                                                    |
| List conversations          | `/api/v2/tickets/{id}/conversations`        | GET    | Nested call per ticket                             |
| Add note/comment            | `/api/v2/tickets/{id}/notes`                | POST   |                                                    |
| List contacts               | `/api/v2/contacts`                          | GET    | Default user source                                |
| Get contact                 | `/api/v2/contacts/{id}`                     | GET    |                                                    |
| List agents                 | `/api/v2/agents`                            | GET    | Alternative user source                            |
| Get agent                   | `/api/v2/agents/{id}`                       | GET    |                                                    |
| List groups                 | `/api/v2/groups`                            | GET    | User groups                                        |
| Get group                   | `/api/v2/groups/{id}`                       | GET    |                                                    |
| List KB categories          | `/api/v2/solutions/categories`              | GET    | KB top level                                       |
| List KB folders by category | `/api/v2/solutions/categories/{id}/folders` | GET    |                                                    |
| List KB sub-folders         | `/api/v2/solutions/folders/{id}/subfolders` | GET    | Recursive                                          |
| List KB articles by folder  | `/api/v2/solutions/folders/{id}/articles`   | GET    | Paginated                                          |

***

### Knowledge Base with ACL Tags <a href="#knowledge-base-with-acl-tags" id="knowledge-base-with-acl-tags"></a>

FreshDesk KB folders have a `visibility` attribute that controls who can read the articles within them. The connector can translate this visibility information into **ACL tags** on the ingested documents, enabling Aisera to enforce access control at the KB article level.

#### How It Works <a href="#how-it-works.1" id="how-it-works.1"></a>

1. When the `FreshDeskAdapter` fetches articles, it injects the following fields from the parent folder:
   * `folder_visibility` — the integer visibility code
   * `folder_company_ids` — company IDs (when `visibility == 4`)
   * `folder_contact_segment_ids` — contact segment IDs (when `visibility == 6`)
   * `folder_company_segment_ids` — company segment IDs (when `visibility == 7`)
2. An **Override Document Transformation** (JavaScript script) reads these injected fields and populates `tag_keys` and `tag_value_list` arrays on the article JSON.
3. **Field mappings** then map `tag_keys.[*]` → `kbTagKey` and `tag_value_list.[*]` → `kbTagValue`, which the Aisera system uses for ACL enforcement.

#### Visibility Tag Mapping <a href="#visibility-tag-mapping" id="visibility-tag-mapping"></a>

| Visibility Value | Meaning                   | Tags Produced                                       |
| ---------------- | ------------------------- | --------------------------------------------------- |
| `1`              | All Users                 | `roles=agent`, `roles=user`                         |
| `2`              | Logged In Users           | `roles=agent`, `roles=user`                         |
| `3`              | Agents Only               | `roles=agent`                                       |
| `4`              | Selected Companies        | `companyId={id}` per company + `roles=agent`        |
| `5`              | Bots                      | `roles=bot`                                         |
| `6`              | Selected Contact Segments | `contactSegmentId={id}` per segment + `roles=agent` |
| `7`              | Selected Company Segments | `companySegmentId={id}` per segment + `roles=agent` |

Articles in folders with no `folder_visibility` value produce no tags (unrestricted).

#### Override Document Transformation for ACL Tags <a href="#override-document-transformation-for-acl-tags" id="override-document-transformation-for-acl-tags"></a>

`{ "KnowledgeArticle": [ { "externalSystemTypeId": 232, "contentTypeName": "KnowledgeArticle", "name": "Parse Visibility ACL Tags", "transformationType": "SCRIPT", "direction": "JSON2Proto", "step": "PRE", "content": "var visibility = jsobject.get(\"folder_visibility\");\nvar tag_keys = new java.util.ArrayList();\ntag_value_list = new java.util.ArrayList();\nif (visibility) {\n var agentOnly = false;\n if (visibility == 3) {\n print(\"Found visibility 3 (Agents)\");\n tag_keys.add(\"roles\");\n tag_value_list.add(\"agent\");\n agentOnly = true;\n } else if (visibility == 4) {\n print(\"Found visibility 4 (Selected Companies)\");\n jsobject.get(\"folder_company_ids\").forEach(function (item, index) {\n tag_keys.add(\"companyId\");\n tag_value_list.add(item);\n });\n } else if (visibility == 5) {\n print(\"Found visibility 5 (Bots)\");\n tag_keys.add(\"roles\");\n tag_value_list.add(\"bot\");\n agentOnly = true;\n } else if (visibility == 6) {\n print(\"Found visibility 6 (Contact Segments)\");\n jsobject.get(\"folder_contact_segment_ids\").forEach(function (item, index) {\n tag_keys.add(\"contactSegmentId\");\n tag_value_list.add(item);\n });\n } else if (visibility == 7) {\n print(\"Found visibility 7 (Company Segments)\");\n jsobject.get(\"folder_company_segment_ids\").forEach(function (item, index) {\n tag_keys.add(\"companySegmentId\");\n tag_value_list.add(item);\n });\n }\n if (tag_keys.size() > 0 && !agentOnly) {\n tag_keys.add(\"roles\");\n tag_value_list.add(\"agent\");\n }\n jsobject.put(\"tag_keys\", tag_keys);\n jsobject.put(\"tag_value_list\", tag_value_list);\n}\njsobject;", "transformationOrder": 1 } ] }`

The underlying JavaScript:

`var visibility = jsobject.get("folder_visibility"); var tag_keys = new java.util.ArrayList(); tag_value_list = new java.util.ArrayList(); if (visibility) { var agentOnly = false; if (visibility == 3) { print("Found visibility 3 (Agents)"); tag_keys.add("roles"); tag_value_list.add("agent"); agentOnly = true; } else if (visibility == 4) { print("Found visibility 4 (Selected Companies)"); jsobject.get("folder_company_ids").forEach(function (item, index) { tag_keys.add("companyId"); tag_value_list.add(item); }); } else if (visibility == 5) { print("Found visibility 5 (Bots)"); tag_keys.add("roles"); tag_value_list.add("bot"); agentOnly = true; } else if (visibility == 6) { print("Found visibility 6 (Contact Segments)"); jsobject.get("folder_contact_segment_ids").forEach(function (item, index) { tag_keys.add("contactSegmentId"); tag_value_list.add(item); }); } else if (visibility == 7) { print("Found visibility 7 (Company Segments)"); jsobject.get("folder_company_segment_ids").forEach(function (item, index) { tag_keys.add("companySegmentId"); tag_value_list.add(item); }); } if (tag_keys.size() > 0 && !agentOnly) { tag_keys.add("roles"); tag_value_list.add("agent"); } jsobject.put("tag_keys", tag_keys); jsobject.put("tag_value_list", tag_value_list); } jsobject;`

#### Required Field Mappings for ACL Tags <a href="#required-field-mappings-for-acl-tags" id="required-field-mappings-for-acl-tags"></a>

After applying the transformation, add the following field mappings in the Data Source configuration:

| Internal Field | FreshDesk Field            | Type          |
| -------------- | -------------------------- | ------------- |
| `kbTagKey`     | `tag_keys.[*]`             | array element |
| `kbTagValue`   | `tag_value_list.[*]`       | array element |
| `kbTagScope`   | *(fixed value)* `"global"` | fixed         |

> **Note on FreshDesk vs FreshService visibility codes:** FreshDesk visibility `4` means "Selected Companies" (uses `folder_company_ids`). FreshService visibility `4` means "Departments" (uses `folder_department_ids`). These are different ACL concepts — do not copy FreshService ACL scripts directly for FreshDesk.

***

### KB Attachments <a href="#kb-attachments" id="kb-attachments"></a>

FreshDesk Solution Articles support file attachments. Each article in the Freshdesk v2 API response includes an `attachments` array; each element is an **attachment object** containing the file metadata and a download URL (which is usually a pre-signed CDN download URL).

FreshDesk Connector is able to ingest Attachments from Solution Articles, but this functionality **needs to be enabled**, as it is **inactive by default**.

#### Sample Article JSON with Attachments <a href="#sample-article-json-with-attachments" id="sample-article-json-with-attachments"></a>

`{ "id": 30000302162, "title": "My Article", "description": "<p>Article content</p>", "attachments": [ { "id": 19, "name": "Precautions.pdf", "content_type": "application/pdf", "size": 204800, "attachment_url": "https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/19/original/Precautions.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=..." } ] }`

> **Note on** `attachment_url`**:** Freshdesk CDN attachment URLs are pre-signed AWS S3 URLs. They contain embedded AWS credentials (`X-Amz-Algorithm`, `X-Amz-Signature`, etc.) and expire after a short window (typically 5 minutes). The adapter detects pre-signed URLs and downloads them **without adding an** `Authorization` **header** to avoid the AWS error `"Only one auth mechanism allowed"`.

&#x20;

#### Enabling Attachment Ingestion <a href="#enabling-attachment-ingestion" id="enabling-attachment-ingestion"></a>

By default, the connector **does not** handle article attachments. To enable attachment ingestion, add the `fileUploadConfig` block to your Override Configuration (either copy paste this, or if there is already an Override Configuration simply copy the `fileUploadConfig` part in the top level) :

`{ "fileUploadConfig": { "inheritFromFieldMappings": true, "fileNameHeader": "Content-Disposition", "fileUploadMetaDataKey": "fileUploadMetadata", "fileOriginAddressPath": "fileUploadMetadata.originAddress", "acceptBothFilesAndJsonEntries": true, "uploadToS3": true, "acceptedContentTypes": [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/plain" ] } }`

#### Content-Type Pre-filter (`acceptedContentTypes`) <a href="#content-type-pre-filter-acceptedcontenttypes" id="content-type-pre-filter-acceptedcontenttypes"></a>

Before downloading any attachment, the adapter checks the attachment object's `content_type` field against `acceptedContentTypes`.

The default value of `acceptedContentTypes` (if omitted from the Override Configuration) is the platform-wide `SUPPORTED_MIME_TYPES` list, which includes PDF, Word, Excel, PowerPoint, plain text, CSV, and other common document formats (like the sample json configuration above).

**Pre-signed URL Handling**

The adapter inspects `attachment_url` for embedded credential query parameters before deciding how to authenticate the download:

| Detected parameter                                          | Scheme         | Action                                            |
| ----------------------------------------------------------- | -------------- | ------------------------------------------------- |
| `X-Amz-Algorithm`, `X-Amz-Signature`, or `X-Amz-Credential` | AWS SigV4      | Download **without** `Authorization` header       |
| `AWSAccessKeyId`                                            | AWS SigV2      | Download **without** `Authorization` header       |
| `sig` **and** `se` (both present)                           | Azure SAS      | Download **without** `Authorization` header       |
| `X-Goog-Signature`                                          | GCP Signed URL | Download **without** `Authorization` header       |
| None of the above                                           | Plain URL      | Download **with** standard `Authorization` header |

**Failsafe:** If the no-auth download attempt fails for any reason, the adapter automatically retries once with the standard `Authorization` header added.

#### ACL for Attachments <a href="#acl-for-attachments" id="acl-for-attachments"></a>

Attachments automatically **inherit the ACL tags from their parent article**. The same visibility transformation and field mappings described in [Knowledge Base with ACL Tags](http://localhost:63342/markdownPreview/1669066625/markdown-preview-index-v4l5jsn2h95is8d05rfb0l3ta9.html#knowledge-base-with-acl-tags) apply to both the article and its attachment entries. No additional configuration is needed.

#### URL of Attachments <a href="#url-of-attachments" id="url-of-attachments"></a>

By default, the URI field for the KBs that have been ingested from Attachment files will be the aisera internal S3 URI. Alternatively, you can activate the `Use origin url for files download` Check Box under the “Ingestion Configuration” Tab of the Data Source Configuration. This will use the Rest API url that the connector used to Download the Attachment, which is not a browser friendly URI.

If you have a patter to produce browser friendly URIs, you can use a JS Custom Script (must be provided through the Override Document Transformations field under the Overrides Tab of the Data Source configuration) to edit the connector’s JSON Entry that is produced for each ingested attachment. Sample json entry that will need to be edited:

`{"id":48012345678,"type":1,"category_id":12000001234,"folder_id":12000056789,"title":"How to Reset Your Password","description":"<div>Step-by-step guide...</div>","description_text":"Step-by-step guide...","status":2,"tags":["password","account"],"thumbs_up":10,"thumbs_down":1,"hits":250,"created_at":"2024-01-15T10:00:00Z","updated_at":"2025-03-20T08:30:00Z","attachments":[{"id":81006248278,"name":"guide.pdf","content_type":"application/pdf","size":204800,"attachment_url":"https://cdn.freshdesk.com/data/helpdesk/attachments/production/81006248278/original/guide.pdf"}],"folder":{"id":12000056789,"name":"Account Management","visibility":1,"company_ids":[],"contact_segment_ids":[],"company_segment_ids":[],"category":{"id":12000001234,"name":"General Help"}},"folder_visibility":1,"computed_category_folder_path":"General Help > Account Management","kbUri":"https://yourcompany.freshdesk.com/a/support/solutions/articles/48012345678","file_upload_metadata":{"s3_bucket":"aisera-ingestion-bucket","s3_key":"tenant-uuid/datasource-id/guide.pdf","attachment_id":"81006248278","filename":"guide.pdf","download_url":"https://yourcompany.freshdesk.com/api/v2/attachments/81006248278","attachment_url":"https://cdn.freshdesk.com/data/helpdesk/attachments/production/81006248278/original/guide.pdf","content_type":"application/pdf","size":204800}}`

In the above JSON Structure, the JS Script needs to replace the value of the `download_url`value under the `file_upload_metadata` object. Editing this value, when the Check Box is enabled, will alter the URI of the ingested KB.

***

### Resource Files <a href="#resource-files" id="resource-files"></a>

In the Actions Repo, we keep the following resource files:

| File | Purpose |
| ---- | ------- |

| File                                                                            | Purpose                                                                                                                  |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `freshdesk/transformations/freshdesk.ticket.pipeline.transform.json2proto.json` | JOLT transform: maps FreshDesk integer priority to string label (Low/Medium/High/Urgent)                                 |
| `freshdesk/transformations/freshdesk.ticket.adduri.js`                          | JS script (POST transform): resolves `{_ID_}` placeholder in `ticketUri` with the actual ticket ID                       |
| `freshdesk/transformations/freshdesk.ticket.pipeline.transform.proto2json.json` | JOLT transform: maps string priority back to integer and applies field defaults for create/update operations             |
| `fresh/transformations/fresh.ticket.pipeline.transform.json2proto.js`           | JS script (POST transform): extracts resolution text from last comment for closed tickets — **shared with FreshService** |
| `fresh/transformations/fresh.kb.adduri.js`                                      | JS script (POST transform): replaces `{ID}` placeholder in source URL for KB articles — **shared with FreshService**     |

Related content


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aisera.com/aisera-platform/adding-data-to-your-tenant/integrations-and-data-sources/connectors/freshdesk-connector.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
