FreshService as a Migration Target
FreshService is a common destination for archived Cherwell data. Cortex writes to it over the FreshService REST API, so the target must be configured as a FreshService data source in Cortex before a migration can use it (see Data Sources).
Configuring the target
The FreshService data source editor has three tabs.
Connection Details
| Setting | Notes |
|---|---|
| Server URL | Your FreshService URL, for example https://mycompany.freshservice.com. |
| API Key | The API key Cortex authenticates with. It is stored encrypted and is not displayed again after saving. |
| Workspace ID | Optional - the FreshService workspace the tickets belong to. Leave blank for a single-workspace account. See Workspaces. |
| Test Connection | Confirms Cortex can reach the instance with the key you entered. |
| Normalize rich-text HTML on write | On by default - see Rich text below. |
| Continue when embedded body images fail to migrate | Off by default - see Rich text below. |
| Treat 401 Unauthorized as a rate limit (auto-retry) | Off by default - see Intermittent 401 responses below. |
Workspaces
FreshService only exposes the global fields plus the fields of the primary workspace on an unscoped request, and it creates tickets in that primary workspace. An account that keeps a business area in its own workspace is therefore invisible to Cortex by default: the tickets table offers only the global category fields, and the workspace's own fields cannot be mapped.
Set Workspace ID on the Connection Details tab when the ticket fields you migrate into live in a workspace other than the primary one. Leaving it blank keeps the unscoped behaviour, which is what a single-workspace account needs.
With a workspace set:
- Ticket list, paged and count requests are scoped to that workspace, and new tickets are created in it. The workspace is only sent when a ticket is created, because FreshService cannot change it afterwards.
- Import Ticket Fields reads the workspace's own ticket form fields, so its custom and dependent fields become available for mapping.
- Test Connection probes the scoped endpoint and reports the workspace name, so a wrong id fails outright instead of silently passing.
- Requesters and agents are account-level in FreshService and are deliberately left unscoped.
The Field Definitions tab warns when the cached field definitions were imported for a different workspace than the Workspace ID currently set - run Import Ticket Fields again to load the new workspace's fields.
Field Definitions
Cortex reads the ticket fields it can write to from this tab:
- Import Ticket Fields loads the ticket field definitions from the instance. On plans where the field API is unavailable, define the fields manually instead.
- Add Manual Field (and Edit Field) let you record a field's Name, Label, Field Type, whether it is Required, and its Lookup Values (choices).
- Dependent (nested) fields are imported one level at a time. FreshService stores each level of a dependent field as its own ticket key (for example
category, thensub_category, thenitem_category), so Cortex turns each level into its own settable target field with that level's choices as its lookup values, and you map the levels you need individually. - Required Field Overrides marks a ticket field as required for migration purposes, so Cortex knows to expect it even when the instance reports it differently.
Populating this tab matters: it is what makes target fields and their choice values available when you build the mapping, and what turns a choice field into a dropdown of accepted values in the Migrate Data screen.
Debug / API Test
Raw API calls against the instance, for troubleshooting an endpoint or a key.
Intermittent 401 responses
FreshService can answer 401 Unauthorized for an API key that is working - most often when a ticket is being created - and the very same record migrates successfully when it is retried. By default a 401 is treated as a straight failure, so the record is reported under Records needing attention and nothing retries it for you.
Treat 401 Unauthorized as a rate limit (auto-retry), on the data source's Connection Details tab, makes a 401 take the same path as a rate-limit (429) response:
- The request is retried automatically up to Max retry attempts, in the data source's Rate Limiting tab, and only while Auto-retry on rate limit is also enabled - a 401 is not retried independently of those settings.
- Every attempt, retries included, passes through the configured request windows, so retrying a record cannot push the instance past the limits Cortex has been told to respect.
- No delay is invented for the 401. A retry waits only if FreshService asks for one with
Retry-After. - If the attempts are used up, the real 401 is reported and the record still appears as a failure, so a genuinely bad key ends up in the same place as before.
Retrying a 401 is safe even on the ticket-creating POST: the request was never authenticated, so it
was never processed and cannot produce a duplicate ticket.
Leave the option off unless you are actually seeing intermittent 401s. An invalid or revoked API key also answers 401, and retrying it only delays the error.
Mapping into FreshService
- Requesters and agents by email. FreshService does not accept a free-text requester name - it expects an existing requester or agent. Use the Lookup Requester Via Email Address transformation to resolve the ticket's requester from an email address field on the source record, and Lookup User Via Email Address for agent fields. If the address matches nothing on the instance, the mapping's Default value is written instead, so a sensible fallback requester can keep records from being rejected.
- Opened/closed metadata. Fields such as Opened by, Closed by and the opened/closed date-times are set by FreshService itself, not by the API. If those values matter for the archive, map them into custom ticket fields on the instance and map your source values into those fields instead.
- Descriptions and resolution notes. These are submitted as HTML, so formatting coming from the source system is preserved rather than flattened to plain text.
- Dates and date-times. FreshService accepts ISO 8601 only, so Cortex converts date values for you - see below.
Date and date/time values
The FreshService API accepts ISO 8601 date and date-time values only. A source system that holds a date in another shape - 23/01/2026, an epoch number, a value with padding or quotes around it - would be rejected with an invalid_value error, so Cortex converts date-typed values while it builds the request:
- A value already in the shape its target field accepts is sent unchanged. Nothing is rewritten and nothing is logged, so a mapping that already works cannot change behaviour.
- Anything else that can be read as a date is rewritten: a date field receives
yyyy-MM-dd(2026-01-23), a date and time field receives the UTC instantyyyy-MM-ddTHH:mm:ssZ. - A value that cannot be read as a date is sent as it is, so the API's own validation error still names the field and the value at fault instead of the value being blanked.
- Padding and the single quotes FreshService's own query syntax uses are never valid on write, so they are stripped and the change is reported.
Conversions are reported in the migration message log when a record is created, and in the Cortex log when a record is updated.
What counts as "already valid" is judged per target field, not as a general "looks like ISO 8601" test, because the API's accepted shape differs by field:
| Target field | Sent unchanged | Converted |
|---|---|---|
| A date-type custom field | 2026-01-23 only | an instant bound for it, so 2026-01-23T15:04:05Z becomes 2026-01-23 |
due_by, fr_due_by | a full instant that carries a time zone | 2026-01-23 becomes 2026-01-23T00:00:00Z; an instant with no zone gets one |
planned_start_date, planned_end_date | either clean shape, because FreshService documents no precision for them | an instant with no zone, or a value carrying padding or quotes |
The date fields Cortex recognises are the built-in ones above plus every imported custom field whose FreshService field type is a date type. When you import ticket fields, a date custom field is typed as a date rather than as short text, which is also what lets the mapping screen treat its values as dates.
The planned dates keep the source value's own precision - a date stays a date, a date and time stays a date and time - so a time component the API may accept is never silently dropped. If a target field should receive a specific shape, convert it on the mapping with one of the date modifiers in Field Mappings rather than relying on the write path.
A bare number is never read as an epoch on the write path: reading 1769174045 as a date is a mapping author's explicit choice, and doing it implicitly could reinterpret an unrelated number as a date. Use the Format Date modifier with epoch input handling for source fields that hold an epoch.
Rich text and embedded images
Source systems often store rich text as Word/Office-style HTML: a <style> block with generated class names (cs1A2B…) and <span class="cs1A2B"> wrappers. FreshService's write path strips classes and style blocks, which loses bold text and other formatting, and it drops data: image sources, which leaves broken image boxes.
With Normalize rich-text HTML on write enabled (the default), Cortex rewrites the HTML before it is sent:
- Simple class rules from the
<style>block are inlined into each element'sstyleattribute, so formatting such as bold survives the sanitizer. Existing inline styles win, and complex or descendant selectors are left alone. - Embedded
data:images are extracted, uploaded to the created ticket as attachments, and the field is saved again with the hosted image URLs. Theirdata:payloads are removed from the request instead of being sent and discarded.
Normalisation costs no extra API calls unless a field actually contains an embedded image - in that case you will see the extra attachment upload and update entries in the migration log. Turning the option off sends the source HTML exactly as it is.
When an embedded image cannot be migrated
A source record can hold an image - typically a signature or logo - whose markup FreshService will not accept, so that one image cannot be uploaded even though the ticket itself is accepted. What happens to the record is your choice:
- With Continue when embedded body images fail to migrate off (the default), the image failure fails the record, and Cortex deletes the ticket it just created so that a retry recreates it cleanly rather than leaving a ticket with a broken image. Because the create reported a failure, the record's related items and attachments are not sent.
- With it on, the ticket is kept, each image that could not be migrated is written into the record's details as a
WARNING:line naming it, and the record is reported as created - so its related items and attachments are still sent. Use this when one broken image should not cost you an otherwise good ticket.
Either way the image failure is visible: the option decides whether it fails the record or is recorded on it.
If the ticket could not be deleted after an image failure, its id is kept so a retry does not create a duplicate, and the record is left for manual verification. Records in that state can be retried with Force retry related items - see Running and Monitoring a Migration.
Attachments
Because FreshService can accept files, the Migrate Data screen offers Transfer File Attachments As Well? for a FreshService target, which copies each record's attachments to the new ticket. See Running and Monitoring a Migration for where attachment problems are reported.
When FreshService refuses a write to a ticket
FreshService refuses updates and deletes on tickets it has marked as spam, and on tickets that have been deleted or trashed. It answers such a write with a bare 405 Method Not Allowed (naming only the methods it will still accept) or a 403, while the ticket itself still reads normally.
Because the follow-up work of a migrated record is all a write - the record's file attachments, a description's embedded images, the rich-text rewrite that points the body at the hosted image URLs, and the compensating delete when images could not be uploaded - every one of those steps fails on such a ticket, and a retry fails in exactly the same way.
When a write to an existing ticket is refused with a 405 or a 403, Cortex reads the ticket back and reports the ticket's real state rather than passing on only the API wording:
| What the read-back finds | What the migration reports |
|---|---|
| The ticket is marked spam | FreshService refuses both updates and deletes on a spam ticket, so no file attachment or embedded image can be added to it. Unspam the ticket (Tickets view > Spam, open the ticket, then Not spam) and retry the record's attachments. If many migrated records are marked spam, check the account's ticket-creation automations and any blocked or deleted requesters for the cause. |
| The ticket has been deleted or trashed | A deleted ticket can still be read but never written to. Restore the ticket, or re-run the record so a new ticket is created, and then retry. |
The ticket cannot be found (404) | The ticket no longer exists, or the id is not a ticket on that server, so nothing can be written to it. Restore it, or re-run the record so a new ticket is created, and then retry. |
| The ticket is readable and is neither spam nor deleted | The refusal is about the credential rather than the ticket: check that the API key this data source uses is allowed to update (and delete) tickets. FreshService applies the permissions of the key's agent role and of any OAuth scope, and refuses a write the credential may not make in this same way. |
| The read-back cannot confirm anything | Nothing is claimed about the ticket, and the original API response is reported unchanged. |
The read-back is a diagnostic only:
- It never writes, and it never throws - a failed read-back cannot turn a reported failure into an error.
- It is issued once per ticket per operation, so a record with several failing files does not spend one call per file.
- It is skipped for failures that already explain themselves (for example a
400validation rejection), and for rate limits or transport errors. - The API's own response body is still reported after the explanation, because that is what FreshService support will ask for.
The read-back needs to be able to reach FreshService with a GET on the ticket. If the instance cannot be reached, the refused write keeps the detail it already had.
See also
- Field Mappings - the transformations referenced above
- Value Translations - mapping source values onto FreshService choice values
- Running and Monitoring a Migration - progress, retries and failures