Freshdesk

The Freshdesk integration allows you to create, update, and delete tickets; reply to and create notes for tickets as well as view Groups, Agents and Contacts.

Case Management · Freshdesk

Details

IDFreshdesk
ProviderFreshworks Inc.
CategoryCase Management
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Use the Freshdesk integration to manage and create Freshdesk tickets from Cortex XSOAR.

Use Cases

  1. Create a Freshdesk ticket from Cortex XSOAR
  2. Update a Freshdesk ticket from Cortex XSOAR
  3. Get information from a Freshdesk ticket

Configure Freshdesk on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Freshdesk.
  3. Click Add instance to create and configure a new integration instance.
    • Name: a textual name for the integration instance.
    • Server URL https://demistohelp.freshdesk.com )
    • API Token. (You must enter either the API token or your Freshdesk credentials)
    • Trust any certificate (not secure)
    • Use system proxy
    • First fetch timestamp ( , e.g., 12 hours, 7 days)
    • Username
  4. Click Test to validate the URLs, token, and connection.

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

1. Create a ticket


Creates a new Freshdesk ticket.

Base Command

fd-create-ticket

Input
Argument Name Description Required
subject Subject of the ticket Required
description Details of the issue that you are creating a ticket for Required
priority Priority of the ticket. Each number has a corresponding value. 1 - Low, 2 - Medium, 3 - High, 4 - Urgent.
This argument accepts the priority number or string.
Optional
status Status of the ticket. Each number has a corresponding value. 2 - Open, 3 - Pending, 4 - Resolved, 5 - Closed, 6 - Waiting on Customer, 7 - Waiting on Third Party.
This argument accepts the ticket status number or string.
Optional
identifier Email address or a Twitter handle of the requesting user.
If an email address is entered and no contact exists with this email address in Freshdesk, it will be added as a new contact. If a Twitter handle is entered and no contact exists with this handle in Freshdesk, it will be added as a new contact.
Required
responder ID or name of the group or agent to assign the ticket to.
Use the fd-list-groups command to find potential assignees.
Optional
attachments CSV list of entry IDs of files to attach to the ticket.
For example: “468@73f988d1-bda2-4adc-8e02-926f02190070,560@73f988d1-bda2-4adc-8e02-926f02190070”.
The total size of these attachments cannot exceed 15MB.
Optional
additional_fields Additional, optional ticket fields.
Format - “field=value” where field value pairs are delimited from subsequent pairs by a semicolon symbol ‘;’ and where values that are lists are delimited by commas ‘,’.
For example: “name=Jeffrey Collins;email=jeffrey.collins@gmail.com;tags=new,attention needed,billing related”
Optional

 

Context Output
Path Type Description
Freshdesk.Ticket.ID Number ID number of the ticket
Freshdesk.Ticket.Priority Number Ticket priority
Freshdesk.Ticket.DueBy Date Timestamp that denotes when the ticket is due to be resolved
Freshdesk.Ticket.Subject String Ticket subject
Freshdesk.Ticket.Status Number Status of the ticket
Freshdesk.Ticket.RequesterID Number User ID of the requester
Freshdesk.Ticket.Tag Unknown Tags associated with the ticket
Freshdesk.Ticket.GroupID Number ID of the group the ticket was assigned to
Freshdesk.Ticket.Source Number The channel through which the ticket was created
Freshdesk.Ticket.CreatedAt Date Ticket creation timestamp
Freshdesk.Ticket.ResponderID Number ID of the agent the ticket was assigned to
Freshdesk.Ticket.FrDueBy Date Timestamp that denotes when the first response is due
Freshdesk.Ticket.AdditionalFields Unknown Additional fields and values that were entered using the ‘additional_fields’ arguments
Freshdesk.Ticket.Attachment.AttachmentURL String URL to download the file attached to the ticket to your local machine
Freshdesk.Ticket.Attachment.Name Unknown The name of the file attached to the ticket
Freshdesk.Ticket.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.Ticket.Attachment.ID Unknown ID number for the file attached to the ticket
Freshdesk.Ticket.Attachment.Size String Size of the file attached to the ticket

 

Command Example
!fd-create-ticket subject="Demonstrate Ticket Creation" description="<em>Here</em> we are demonstrating the freshdesk integration ticket creation command" identifier="jeffrey.collins@gmail.com" priority="High" additional_fields="name=Jeff Testman;email=jeffrey.collins@gmail.com;tags=new,attention needed,billing related"
Context Example
{
    "Freshdesk.Ticket": {
        "Status": 2,
        "DueBy": "2019-02-07T09:00:00Z",
        "FrDueBy": "2019-02-06T10:00:00Z",
        "Priority": 3,
        "Source": 2,
        "Tag": [
            "attention needed",
            "billing related",
            "new"
        ],
        "RequesterID": 2043024010476,
        "UpdatedAt": "2019-02-05T15:55:35Z",
        "AdditionalFields": {
            "DescriptionHTML": "<em>Here</em> we are demonstrating the freshdesk integration ticket creation command",
            "DescriptionText": "Here we are demonstrating the freshdesk integration ticket creation command",
            "Email": "jeffrey.collins@gmail.com",
            "Name": "Jeff Testman"
        },
        "ID": 108,
        "CreatedAt": "2019-02-05T15:55:35Z",
        "Subject": "Demonstrate Ticket Creation"
    }
}
Human Readable Output

Newly Created Ticket #108

Status DueBy FrDueBy Priority Source Tag RequesterID UpdatedAt AdditionalFields ID CreatedAt Subject
2 2019-02-07T09:00:00Z 2019-02-06T10:00:00Z 3 2 attention needed,
billing related,
new
2043024010476 2019-02-05T15:55:35Z DescriptionHTML: Herewe are demonstrating the freshdesk integration ticket creation command
DescriptionText: Here we are demonstrating the freshdesk integration ticket creation command
Name: Jeff Testman
Email: jeffrey.collins@gmail.com
108 2019-02-05T15:55:35Z Demonstrate Ticket Creation

 

2. Get ticket details


Gets details of a ticket, specified by the ticket ID number.

Base Command

fd-get-ticket

Input
Argument Name Description Required
id ID number of the ticket to fetch Required
include_requester If set to true, the ticket requester’s ID, email address, mobile number, name, and phone number will be included in the ticket’s output. Note that this is not set by default because setting this to ‘true’ will consume an additional API credit per API call.
For more information, see the Freshdesk API documentation.
Optional
include_stats If set to true then the ticket’s closed_at, resolved_at and first_responded_at time will be included in the response. Note that this is not set by default because setting this to ‘true’ will consume an additional API credit per API call.
For more information, see the Freshdesk API documentation.
Optional

 

Context Output
Path Type Description
Freshdesk.Ticket.ID Number ID number of the fetched ticket
Freshdesk.Ticket.Priority Number Ticket priority
Freshdesk.Ticket.DueBy Date Timestamp that denotes when the ticket is due to be resolved
Freshdesk.Ticket.Subject String Ticket subject
Freshdesk.Ticket.Status Number Ticket status
Freshdesk.Ticket.RequesterID Number User ID of the requester
Freshdesk.Ticket.Tag Unknown Tags associated with the ticket
Freshdesk.Ticket.GroupID Number ID of the group the ticket was assigned to
Freshdesk.Ticket.Source Number The channel through which the ticket was created
Freshdesk.Ticket.CreatedAt Date Ticket creation timestamp
Freshdesk.Ticket.ResponderID Number ID of the agent the ticket was assigned to
Freshdesk.Ticket.FrDueBy Date Timestamp that denotes when the first response is due
Freshdesk.Ticket.Conversation Unknown Conversations associated with this ticket
Freshdesk.Ticket.Attachment.AttachmentURL Unknown URL to download the file attached to the ticket to your local machine
Freshdesk.Ticket.Attachment.Name Unknown The name of the file attached to the ticket
Freshdesk.Ticket.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.Ticket.Attachment.ID Unknown ID number of the file attached to the ticket
Freshdesk.Ticket.Attachment.Size String Size of the file attached to the ticket
Freshdesk.Ticket.UpdatedAt Date Ticket update timestamp

 

Command Example
!fd-get-ticket id=108 include_requester=true include_stats=true
Context Example
{
    "Freshdesk.Ticket": {
        "Status": 2,
        "DueBy": "2019-02-07T09:00:00Z",
        "FrDueBy": "2019-02-06T10:00:00Z",
        "Priority": 3,
        "Source": 2,
        "Tag": [
            "attention needed",
            "billing related",
            "new"
        ],
        "RequesterID": 2043024010476,
        "UpdatedAt": "2019-02-05T15:55:35Z",
        "AdditionalFields": {
            "Stats": {
                "StatusUpdatedAt": "2019-02-05T15:55:35Z"
            },
            "IsEscalated": false,
            "Deleted": false,
            "Spam": false,
            "FrEscalated": false,
            "Requestor": {
                "Name": "Jeff Testman",
                "Email": "jeffrey.collins@gmail.com",
                "Id": 2043024010476
            },
            "DescriptionHTML": "<em>Here</em> we are demonstrating the freshdesk integration ticket creation command",
            "DescriptionText": "Here we are demonstrating the freshdesk integration ticket creation command"
        },
        "ID": 108,
        "CreatedAt": "2019-02-05T15:55:35Z",
        "Subject": "Demonstrate Ticket Creation"
    }
}
Human Readable Output

Viewing Ticket #108

Status DueBy FrDueBy Priority Source Tag RequesterID UpdatedAt AdditionalFields ID CreatedAt Subject
2 2019-02-07T09:00:00Z 2019-02-06T10:00:00Z 3 2 attention needed,
billing related,
new
2043024010476 2019-02-05T15:55:35Z DescriptionHTML: Here we are demonstrating the freshdesk integration ticket creation command
IsEscalated: false
DescriptionText: Here we are demonstrating the freshdesk integration ticket creation command
Spam: false
FrEscalated: false
108 2019-02-05T15:55:35Z Demonstrate Ticket Creation

 

3. Update a ticket


Updates a ticket specified by the ID.

Base Command

fd-update-ticket

Input
Argument Name Description Required
subject Subject of the ticket Optional
status Status of the ticket. Each number has a corresponding value.
2 is Open, 3 is Pending, 4 is Resolved, 5 is Closed, 6 is Waiting on Customer, 7 is Waiting on Third Party.
Acceptable values for this command argument are the digits 2,3,4,5,6,7, or their corresponding strings ‘Open’,‘Pending’,‘Resolved’,‘Closed’,‘Waiting on Customer’,‘Waiting on Third Party’.
Optional
priority Priority of the ticket. Each number has a corresponding value.
1 is Low, 2 is Medium, 3 is High, 4 is Urgent.
Acceptable values for this command argument are the digits 1,2,3,4, or their corresponding strings ‘Low’,‘Medium’,‘High’,‘Urgent’.
Optional
description HTML content of the ticket. Optional
id ID number of the ticket to update Required
assigned_agent Update which agent is assigned to respond to this ticket by entering either their unique agent ID, name, or email. Optional
assigned_group Update the group assigned to respond to this ticket by entering the group’s unique ID or the name of the group. Optional
additional_fields Fields not included in the default command arguments that you wish to enter the value for.
Format - “field=value” where field value pairs are delimited from subsequent pairs by a semicolon symbol ‘;’ and where values that are lists are delimited by commas ‘,’.
For example: “name=Jeffrey Collins;email=jeffrey.collins@gmail.com;tags=new,attention needed,billing related”
Optional

 

Context Output
Path Type Description
Freshdesk.Ticket.ID Number ID of the updated ticket
Freshdesk.Ticket.Priority Number Ticket priority
Freshdesk.Ticket.DueBy Date Timestamp that denotes when the ticket is due to be resolved
Freshdesk.Ticket.Subject String Ticket subject
Freshdesk.Ticket.Status Number Ticket status
Freshdesk.Ticket.RequesterID Number User ID of the requester
Freshdesk.Ticket.Tag Unknown Tags associated with the ticket
Freshdesk.Ticket.GroupID Number ID of the group assigned to the ticket
Freshdesk.Ticket.Source Number The channel through which the ticket was created
Freshdesk.Ticket.CreatedAt Date Ticket creation timestamp
Freshdesk.Ticket.ResponderId Number ID of the agent the ticket was assigned to
Freshdesk.Ticket.FrDueBy Date Timestamp that denotes when the first response is due
Freshdesk.Ticket.UpdatedAt Date Ticket update timestamp
Freshdesk.Ticket.AdditionalFields Unknown Additional fields and values that were updated using the ‘additional_fields’ argument
Freshdesk.Ticket.Attachment.AttachmentURL Unknown URL to download the file attached to the ticket to your local machine
Freshdesk.Ticket.Attachment.Name Unknown The name of the file attached to the ticket
Freshdesk.Ticket.Attachment.ContentType String Content type of the attached file
Freshdesk.Ticket.Attachment.ID Unknown ID number for the attached file
Freshdesk.Ticket.Attachment.Size String Size of the attached file in bytes

 

Command Example
!fd-update-ticket id=108 priority=Medium subject="Demonstrating Ticket Updating" assigned_agent=jeffrey.collins@gmail.com additional_fields="tags=almost completed,yep;attachments=2@5a0be47f-748f-4d60-8f46-81feb8f0c438"
Context Example
{
    "Freshdesk.Ticket": {
        "Status": 2,
        "ResponderID": 2043022085976,
        "DueBy": "2019-02-06T15:00:00Z",
        "FrDueBy": "2019-02-06T14:00:00Z",
        "Priority": 2,
        "Source": 2,
        "Tag": [
            "almost completed",
            "yep"
        ],
        "Attachment": [
            {
                "Name": "sample_attachment.md",
                "Size": 76,
                "ContentType": "application/octet-stream",
                "ID": 2043010708407,
                "AttachmentURL": "https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708407/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155538Z&X-Amz-Expires=86400&X-Amz-Signature=05dfe20c475c01763766b71ee0d4f0e808cef5fa116cede418eabafce4802947&X-Amz-SignedHeaders=Host"
            }
        ],
        "RequesterID": 2043024010476,
        "UpdatedAt": "2019-02-05T15:55:37Z",
        "AdditionalFields": {
            "DescriptionHTML": "<em>Here</em> we are demonstrating the freshdesk integration ticket creation command",
            "DescriptionText": "Here we are demonstrating the freshdesk integration ticket creation command"
        },
        "ID": 108,
        "CreatedAt": "2019-02-05T15:55:35Z",
        "Subject": "Demonstrating Ticket Updating"
    }
}
Human Readable Output

Ticket #108 Updated

Status ResponderID DueBy FrDueBy Priority Source Tag Attachment RequesterID UpdatedAt AdditionalFields ID CreatedAt Subject
2 2043022085976 2019-02-06T15:00:00Z 2019-02-06T14:00:00Z 2 2 almost completed,
yep
ID: 2043010708407, Size: 76, ContentType: application/octet-stream, Name: sample_attachment.md, AttachmentURL: https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708407/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155538Z&X-Amz-Expires=86400&X-Amz-Signature=05dfe20c475c01763766b71ee0d4f0e808cef5fa116cede418eabafce4802947&X-Amz-SignedHeaders=Host 2043024010476 2019-02-05T15:55:37Z DescriptionHTML: Here we are demonstrating the freshdesk integration ticket creation command
DescriptionText: Here we are demonstrating the freshdesk integration ticket creation command
108 2019-02-05T15:55:35Z Demonstrating Ticket Updating

 

4. Get a list of all contacts


Returns a list of all contacts matching the specified filters. If no filters are provided then all unblocked and undeleted contacts will be returned.

Base Command

fd-list-contacts

Input
Argument Name Description Required
mobile mobile number to filter the contacts by Optional
phone phone number to filter contacts by Optional
state The state of contacts by which you want to filter the contacts Optional
updated_since Return contacts that have been updated after the timestamp given as this argument value. Acceptable format is ‘YYYY-MM-DDTHH:MM:SSZ’
For example: ‘2018-01-19T02:00:00Z’.
Optional

 

Context Output
Path Type Description
Freshdesk.Contact.Active Boolean Set to true if the contact has been verified
Freshdesk.Contact.Address String Address of the contact
Freshdesk.Contact.CompanyID Number ID of the primary company to which this contact belongs
Freshdesk.Contact.ViewAllTickets Boolean Set to true if the contact can see all tickets that are associated with the company to which s/he belong
Freshdesk.Contact.Deleted Boolean Set to true if the contact has been deleted
Freshdesk.Contact.Description String A short description of the contact
Freshdesk.Contact.Email String Primary email address of the contact
Freshdesk.Contact.ID Number ID of the contact
Freshdesk.Contact.JobTitle String Job Title of the contact
Freshdesk.Contact.Language String Language of the contact
Freshdesk.Contact.Mobile Number Mobile number of the contact
Freshdesk.Contact.Name String Name of the contact
Freshdesk.Contact.Phone Number Telephone number of the contact
Freshdesk.Contact.Tag Unknown Tags associated with this contact
Freshdesk.Contact.TimeZone String Time zone in which the contact resides
Freshdesk.Contact.TwitterID String Twitter handle of the contact
Freshdesk.Contact.UniqueExternalID String External ID of the contact
Freshdesk.Contact.CreatedAt Date Contact creation stamp
Freshdesk.Contact.UpdatedAt Date Contact updated timestamp

 

Command Example
!fd-list-contacts updated_since=2018-01-19T02:00:00Z
Context Example
{
    "Freshdesk.Contact": [
        {
            "Name": "Bob Tree",
            "Language": "en",
            "CompanyID": 44000302032,
            "Mobile": "+972501231231",
            "JobTitle": "Security Researcher",
            "UpdatedAt": "2019-01-20T09:51:11Z",
            "UniqueExternalID": "12345",
            "Email": "bob.tree@freshdesk.com",
            "Phone": "+972501231231",
            "Address": "Rothchild 45",
            "TimeZone": "Athens",
            "ID": 2043022085984,
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Clarice Boone",
            "Language": "en",
            "ID": 2043022085990,
            "UpdatedAt": "2019-01-20T09:06:34Z",
            "TimeZone": "Athens",
            "Email": "clboone@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:34Z"
        },
        {
            "Name": "Emily Testman",
            "Language": "en",
            "ID": 2043022085989,
            "UpdatedAt": "2019-01-20T09:06:34Z",
            "TimeZone": "Athens",
            "Email": "emily.dean@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:34Z"
        },
        {
            "Name": "Finch Hoot",
            "Language": "en",
            "ID": 2043022085991,
            "UpdatedAt": "2019-01-20T09:06:34Z",
            "TimeZone": "Athens",
            "Email": "finchhoot1@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:34Z"
        },
        {
            "Name": "James Dean",
            "Language": "en",
            "ID": 2043022085977,
            "UpdatedAt": "2019-01-20T09:06:32Z",
            "TimeZone": "Athens",
            "Email": "james@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:32Z"
        },
        {
            "Name": "Jeff Testman",
            "Language": "en",
            "ID": 2043024010476,
            "UpdatedAt": "2019-02-05T10:30:16Z",
            "TimeZone": "Athens",
            "Email": "jeffrey.collins@gmail.com",
            "CreatedAt": "2019-02-05T10:30:16Z"
        },
        {
            "Name": "Joe Mathew",
            "Language": "en",
            "ID": 2043022085982,
            "UpdatedAt": "2019-01-20T09:06:33Z",
            "TimeZone": "Athens",
            "Email": "joe.mathew@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Johnny Appleseed",
            "Language": "en",
            "ID": 2043022085986,
            "UpdatedAt": "2019-01-20T09:06:33Z",
            "TimeZone": "Athens",
            "Email": "johnny.appleseed@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Lewis Testman",
            "Language": "en",
            "ID": 2043022085987,
            "UpdatedAt": "2019-01-20T09:06:33Z",
            "TimeZone": "Athens",
            "Email": "lewis.clarke@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Maria Von Trapp",
            "Language": "en",
            "ID": 2043022085988,
            "UpdatedAt": "2019-01-20T09:06:33Z",
            "TimeZone": "Athens",
            "Email": "soundofmusic@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Mark Colbert",
            "Language": "en",
            "ID": 2043022085992,
            "UpdatedAt": "2019-01-20T09:06:34Z",
            "TimeZone": "Athens",
            "Email": "mark.colbert@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:34Z"
        },
        {
            "Name": "Matt Rogers",
            "Language": "en",
            "ID": 2043022085980,
            "UpdatedAt": "2019-01-20T09:06:32Z",
            "TimeZone": "Athens",
            "Email": "matt.rogers@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:32Z"
        },
        {
            "Name": "Pedro Testez",
            "ID": 2043023084321,
            "UpdatedAt": "2019-01-28T14:16:49Z",
            "TimeZone": "Athens",
            "Email": "pedmart@gmail.com",
            "CreatedAt": "2019-01-28T14:16:49Z"
        },
        {
            "Name": "Phileas Fogg",
            "Language": "en",
            "ID": 2043022085985,
            "UpdatedAt": "2019-01-20T09:06:33Z",
            "TimeZone": "Athens",
            "Email": "aroundtheworld80@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Rachel Doe",
            "Language": "en",
            "ID": 2043022085978,
            "Phone": "1 866 832 3090",
            "UpdatedAt": "2019-01-20T09:06:32Z",
            "TimeZone": "Athens",
            "Email": "rachel@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:32Z"
        },
        {
            "Name": "Sam Kart",
            "Language": "en",
            "ID": 2043022085983,
            "UpdatedAt": "2019-01-20T09:06:33Z",
            "TimeZone": "Athens",
            "Email": "sam.kart@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:33Z"
        },
        {
            "Name": "Sam Osborne",
            "Language": "en",
            "ID": 2043022085995,
            "UpdatedAt": "2019-01-20T09:06:47Z",
            "TimeZone": "Athens",
            "Email": "sam.ozzy@freshdesk.com",
            "CreatedAt": "2019-01-20T09:06:47Z"
        }
    ]
}
Human Readable Output

Contacts Filtered by _updated_since: 2018-01-19T02:00:00Z

Name Language TwitterID UpdatedAt TimeZone ID CreatedAt
Bob Tree en   2019-01-20T09:51:11Z Athens 2043022085984 2019-01-20T09:06:33Z
Clarice Boone en   2019-01-20T09:06:34Z Athens 2043022085990 2019-01-20T09:06:34Z
Emily Testman en   2019-01-20T09:06:34Z Athens 2043022085989 2019-01-20T09:06:34Z
Finch Hoot en   2019-01-20T09:06:34Z Athens 2043022085991 2019-01-20T09:06:34Z
James Dean en   2019-01-20T09:06:32Z Athens 2043022085977 2019-01-20T09:06:32Z
Jeff Testman en   2019-02-05T10:30:16Z Athens 2043024010476 2019-02-05T10:30:16Z
Joe Mathew en   2019-01-20T09:06:33Z Athens 2043022085982 2019-01-20T09:06:33Z
Johnny Appleseed en   2019-01-20T09:06:33Z Athens 2043022085986 2019-01-20T09:06:33Z
Lewis Testman en   2019-01-20T09:06:33Z Athens 2043022085987 2019-01-20T09:06:33Z
Maria Von Trapp en   2019-01-20T09:06:33Z Athens 2043022085988 2019-01-20T09:06:33Z
Mark Colbert en   2019-01-20T09:06:34Z Athens 2043022085992 2019-01-20T09:06:34Z
Matt Rogers en   2019-01-20T09:06:32Z Athens 2043022085980 2019-01-20T09:06:32Z
Pedro Testez     2019-01-28T14:16:49Z Athens 2043023084321 2019-01-28T14:16:49Z
Phileas Fogg en   2019-01-20T09:06:33Z Athens 2043022085985 2019-01-20T09:06:33Z
Rachel Doe en   2019-01-20T09:06:32Z Athens 2043022085978 2019-01-20T09:06:32Z
Sam Kart en   2019-01-20T09:06:33Z Athens 2043022085983 2019-01-20T09:06:33Z
Sam Osborne en   2019-01-20T09:06:47Z Athens 2043022085995 2019-01-20T09:06:47Z

 

5. Get contact details


View a contact’s details specified by the ID number.

Base Command

fd-get-contact

Input
Argument Name Description Required
id ID of the contact you wish to view the details of. To find ID numbers for your contacts try executing the fd-list-contacts command. Optional
mobile Mobile number of the contact you wish to view the details of Optional
email Email address of the contact you wish to view the details of Optional

 

Context Output
Path Type Description
Freshdesk.Contact.Active Boolean Set to true if the contact has been verified
Freshdesk.Contact.Address String Address of the contact
Freshdesk.Contact.CompanyID Number ID of the primary company to which this contact belongs
Freshdesk.Contact.ViewAllTickets Boolean Set to true if the contact can see all tickets that are associated with the company to which s/he belong
Freshdesk.Contact.Deleted Boolean Set to true if the contact has been deleted
Freshdesk.Contact.Description String A short description of the contact
Freshdesk.Contact.Email String Primary email address of the contact
Freshdesk.Contact.Id Number ID of the contact
Freshdesk.Contact.JobTitle String Job Title of the contact
Freshdesk.Contact.Language String Language of the contact
Freshdesk.Contact.Mobile Number Mobile number of the contact
Freshdesk.Contact.Name String Name of the contact
Freshdesk.Contact.Phone Number Telephone number of the contact
Freshdesk.Contact.Tag Unknown Tags associated with this contact
Freshdesk.Contact.TimeZone String Time zone in which the contact resides
Freshdesk.Contact.TwitterID String Twitter handle of the contact
Freshdesk.Contact.UniqueExternalID String External ID of the contact
Freshdesk.Contact.CreatedAt Date Contact creation timestamp
Freshdesk.Contact.UpdatedAt Date Contact updated timestamp

 

Command Example
!fd-get-contact id=2043022085984
Context Example
{
    "Freshdesk.Contact": {
        "Name": "Bob Tree",
        "Language": "en",
        "CompanyID": 44000302032,
        "Mobile": "+972501231231",
        "JobTitle": "Security Researcher",
        "UpdatedAt": "2019-01-20T09:51:11Z",
        "UniqueExternalID": "12345",
        "Email": "bob.tree@freshdesk.com",
        "Phone": "+972501231231",
        "Tag": [
            "security"
        ],
        "Address": "Rothchild 45",
        "TimeZone": "Athens",
        "ID": 2043022085984,
        "CreatedAt": "2019-01-20T09:06:33Z"
    }
}
Human Readable Output

Viewing Contact #2043022085984

Address Language CompanyID Mobile JobTitle UniqueExternalID ID Phone Tag UpdatedAt TimeZone Email CreatedAt Name
Rothchild 45 en 44000302032 +972501231231 Security Researcher 12345 2043022085984 +972501231231 security 2019-01-20T09:51:11Z Athens bob.tree@freshdesk.com 2019-01-20T09:06:33Z Bob Tree

 

6. Get a list of all canned response folders


Returns a list of all canned response folders (only users with Admin privileges).

Base Command

fd-list-canned-response-folders

Input

There is no input for this command.

Context Output
Path Type Description
Freshdesk.CRFolder.ID Number Unique ID of the canned response folder
Freshdesk.CRFolder.Name String Name of the canned response folder
Freshdesk.CRFolder.Personal Boolean Set true if the folder can only be accessed by you
Freshdesk.CRFolder.ResponsesCount Number Number of canned responses in the folder
Freshdesk.CRFolder.CreatedAt Date Canned Response Folder’s creation timestamp
Freshdesk.CRFolder.UpdatedAt Date Canned Response Folder’s updated timestamp

 

Command Example
!fd-list-canned-response-folders
Context Example
{
    "Freshdesk.CRFolder": [
        {
            "Personal": true,
            "ResponsesCount": 1,
            "ID": 2043000174274,
            "Name": "Personal"
        }
    ]
}
Human Readable Output

All Canned Response Folders

Personal Name ID ResponsesCount
true Personal 2043000174274 1

 

7. Get a list of details for all canned responses in a folder


Returns a list of details for all canned responses in a folder.

Base Command

fd-get-canned-response-folder

Input
Argument Name Description Required
id ID of the Folder containing the Canned Responses you wish to view the details of. To find ID numbers for your Canned Response folders try executing the fd-list-canned-response-folders command. Required

 

Context Output
Path Type Description
Freshdesk.CRFolder.CR.ID Number Unique ID of the canned response
Freshdesk.CRFolder.CR.Title String Title of the canned response
Freshdesk.CRFolder.CR.FolderID Number ID of the containing folder
Freshdesk.CRFolder.CR.Content String Plaintext version of the canned response content
Freshdesk.CRFolder.CR.ContentHTML String HTML version of the canned response content
Freshdesk.CRFolder.CR.Attachment.AttachmentURL String URL to download the file attached to the ticket to your local machine
Freshdesk.CRFolder.CR.Attachment.Name String The name of the file attached to the ticket
Freshdesk.CRFolder.CR.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.CRFolder.CR.Attachment.ID Number ID number for the file attached to the ticket
Freshdesk.CRFolder.CR.Attachment.Size Number Size of the file attached to the file

 

Command Example
!fd-get-canned-response-folder id=2043000174274
Context Example
{
    "Freshdesk.CRFolder": [
        {
            "Content": "Thank you for reaching out to us. Our team will look into your request and get back to you shortly. \n       \n       \n      You can check the status of your request and add comments here:\n       \n      {{ticket.url}}\n       \n       \n      Regards, \n      {{ticket.agent.name}}",
            "FolderID": 2043000174274,
            "ContentHTML": "<div dir=\"ltr\">\n      Thank you for reaching out to us. Our team will look into your request and get back to you shortly. \n      <br>\n      <br>\n      You can check the status of your request and add comments here:\n      <br>\n      {{ticket.url}}\n      <br>\n      <br>\n      Regards,<br>\n      {{ticket.agent.name}}\n    </div>\n  ",
            "ID": 2043000056698,
            "Title": "We\u2019ve received your request"
        }
    ]
}
Human Readable Output

Details of Canned Responses in CR Folder #2043000174274

Content FolderID ContentHTML ID Title
Thank you for reaching out to us. Our team will look into your request and get back to you shortly.


You can check the status of your request and add comments here:

{{ticket.url}}


Regards,
{{ticket.agent.name}}
2043000174274

Thank you for reaching out to us. Our team will look into your request and get back to you shortly.




You can check the status of your request and add comments here:


{{ticket.url}}




Regards,

{{ticket.agent.name}}
2043000056698 We’ve received your request

 

8. Get a list of all groups


Returns a list of all groups.

Base Command

fd-list-groups

Input

There are no input arguments for this command.

Context Output
Path Type Description
Freshdesk.Group.AgentID Unknown Array of agent user IDs separated by commas
Freshdesk.Group.AutoTicketAssign Boolean Set to true when automatic ticket assignment was enabled. Automatic ticket assignment is only available on certain plans
Freshdesk.Group.BusinessHourID Number Unique ID of the business hour associated with the group
Freshdesk.Group.Description String Description of the group
Freshdesk.Group.EscalateTo Number The ID of the user that an escalation email is sent to if a ticket is unassigned
Freshdesk.Group.ID Number Unique ID of the group
Freshdesk.Group.Name String Name of the group
Freshdesk.Group.UnassignedFor String The time after which an escalation email is sent if a ticket remains unassigned
Freshdesk.Group.CreatedAt Date Group creation timestamp
Freshdesk.Group.UpdatedAt Date Grup updated timestamp
Freshdesk.Group.GroupType String Group Type of the group

 

Command Example
!fd-list-groups
Context Example
{
    "Freshdesk.Group": [
        {
            "GroupType": "support_agent_group",
            "Name": "Account managers",
            "UpdatedAt": "2019-01-20T09:06:49Z",
            "ID": 2043000867330,
            "CreatedAt": "2019-01-20T09:06:49Z",
            "Description": "Account managers"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Billing",
            "UpdatedAt": "2019-01-20T09:06:31Z",
            "ID": 2043000867325,
            "CreatedAt": "2019-01-20T09:06:31Z",
            "Description": "Members of the Billing team belong to this group"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Customer Support",
            "UpdatedAt": "2019-01-20T09:06:47Z",
            "ID": 2043000867327,
            "CreatedAt": "2019-01-20T09:06:47Z",
            "Description": "Customer Support"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Development",
            "UpdatedAt": "2019-01-20T09:06:49Z",
            "ID": 2043000867329,
            "CreatedAt": "2019-01-20T09:06:49Z",
            "Description": "Development"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Escalations",
            "UpdatedAt": "2019-01-20T09:06:31Z",
            "ID": 2043000867326,
            "CreatedAt": "2019-01-20T09:06:31Z",
            "Description": "Team to handle Customer escalations"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Login and security",
            "UpdatedAt": "2019-01-20T09:06:47Z",
            "ID": 2043000867328,
            "CreatedAt": "2019-01-20T09:06:47Z",
            "Description": "Login and security"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Product Management",
            "UpdatedAt": "2019-01-20T09:06:31Z",
            "ID": 2043000867322,
            "CreatedAt": "2019-01-20T09:06:31Z",
            "Description": "Product Management group"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "QA",
            "UpdatedAt": "2019-01-20T09:06:31Z",
            "ID": 2043000867323,
            "CreatedAt": "2019-01-20T09:06:31Z",
            "Description": "Members of the QA team belong to this group"
        },
        {
            "GroupType": "support_agent_group",
            "Name": "Sales",
            "UpdatedAt": "2019-01-20T09:06:31Z",
            "ID": 2043000867324,
            "CreatedAt": "2019-01-20T09:06:31Z",
            "Description": "People in the Sales team are members of this group"
        }
    ]
}
Human Readable Output

All Groups

GroupType Description UpdatedAt ID CreatedAt Name
support_agent_group Account managers 2019-01-20T09:06:49Z 2043000867330 2019-01-20T09:06:49Z Account managers
support_agent_group Members of the Billing team belong to this group 2019-01-20T09:06:31Z 2043000867325 2019-01-20T09:06:31Z Billing
support_agent_group Customer Support 2019-01-20T09:06:47Z 2043000867327 2019-01-20T09:06:47Z Customer Support
support_agent_group Development 2019-01-20T09:06:49Z 2043000867329 2019-01-20T09:06:49Z Development
support_agent_group Team to handle Customer escalations 2019-01-20T09:06:31Z 2043000867326 2019-01-20T09:06:31Z Escalations
support_agent_group Login and security 2019-01-20T09:06:47Z 2043000867328 2019-01-20T09:06:47Z Login and security
support_agent_group Product Management group 2019-01-20T09:06:31Z 2043000867322 2019-01-20T09:06:31Z Product Management
support_agent_group Members of the QA team belong to this group 2019-01-20T09:06:31Z 2043000867323 2019-01-20T09:06:31Z QA
support_agent_group People in the Sales team are members of this group 2019-01-20T09:06:31Z 2043000867324 2019-01-20T09:06:31Z Sales

 

9. Add a reply to a ticket


Adds a reply to a specified ticket.

Base Command

fd-ticket-reply

Input
Argument Name Description Required
ticket_id ID of the ticket to add a reply to Required
body Content of the reply (in HTML format) Required
from_email The email address from which the reply is sent. By default, the global support email is used. Optional
user_id ID of the agent who is adding the reply to the ticket Optional
cc_emails CSV list of email addresses to add to the ‘cc’ field of the outgoing ticket email, e.g., "example1@gmail.com,example2@gmail.com" Optional
bcc_emails CSV list of email addresses to add to the ‘bcc’ field of the outgoing ticket email, e.g., "example1@gmail.com,example2@gmail.com" Optional
attachments CSV list of Entry IDs of files to attach to the reply, e.g., “468@73f988d1-bda2-4adc-8e02-926f02190070,560@73f988d1-bda2-4adc-8e02-926f02190070”. The total size of these attachments cannot exceed 15MB. Optional

 

Context Output
Path Type Description
Freshdesk.Ticket.Conversation.BodyHTML String Content of the conversation (in HTML format)
Freshdesk.Ticket.Conversation.BodyText String Content of the conversation (in plain text format)
Freshdesk.Ticket.Conversation.ID Number ID of the conversation
Freshdesk.Ticket.Conversation.Incoming Boolean Set to true when a particular conversation should appear as being created outside of the web portal
Freshdesk.Ticket.Conversation.ToEmail Unknown Array of email addresses of agents/users who need to be notified about this conversation
Freshdesk.Ticket.Conversation.Private Boolean Set to true if the note is private
Freshdesk.Ticket.Conversation.Source Number Denotes the type of conversation
Freshdesk.Ticket.Conversation.SupportEmail String Email address from which the reply is sent. For notes
Freshdesk.Ticket.Conversation.TicketID Number ID of the ticket that the conversation was added to
Freshdesk.Ticket.Conversation.UserID Number ID of the agent/user who added the conversation
Freshdesk.Ticket.Conversation.CreatedAt Date Conversation creation timestamp
Freshdesk.Ticket.Conversation.UpdatedAt Date Conversation updated timestamp
Freshdesk.Ticket.Conversation.FromEmail String The email address that the reply was sent from. By default
Freshdesk.Ticket.Conversation.Attachment.AttachmentURL String URL of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.Name String The name of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.ID Number ID number of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.Size Number Size of the file attached to the ticket

 

Command Example
!fd-ticket-reply ticket_id=108 body="Demonstrating the Ticket Reply Command" attachments=2@5a0be47f-748f-4d60-8f46-81feb8f0c438 cc_emails=example1@gmail.com,example2@gmail.com,example3@gmail.com
Context Example
{
    "Freshdesk.Ticket": {
        "Conversation": {
            "BodyText": "Demonstrating the Ticket Reply Command",
            "UserID": 2043022085976,
            "Attachment": [
                {
                    "Name": "sample_attachment.md",
                    "Size": 76,
                    "ContentType": "application/octet-stream",
                    "ID": 2043010708409,
                    "AttachmentURL": "https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708409/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155542Z&X-Amz-Expires=86400&X-Amz-Signature=9b19a5795e98af94f5b878a62db473b943986d59496e91f046ab4368b0b4f461&X-Amz-SignedHeaders=Host"
                }
            ],
            "UpdatedAt": "2019-02-05T15:55:42Z",
            "AdditionalFields": {
                "CCEmail": [
                    "example1@gmail.com",
                    "example2@gmail.com",
                    "example3@gmail.com"
                ],
                "BodyHTML": "<div>Demonstrating the Ticket Reply Command</div>",
                "ToEmail": [
                    "example@gmail.com"
                ],
                "TicketID": 108,
                "FromEmail": "Demisto <support@demistohelp.freshdesk.com>"
            },
            "ID": 44007154227,
            "CreatedAt": "2019-02-05T15:55:42Z"
        },
        "ID": 108
    }
}
Human Readable Output

Reply to Ticket #108

BodyText UserID Attachment UpdatedAt AdditionalFields ID CreatedAt
Demonstrating the Ticket Reply Command 2043022085976 ID: 2043010708409, Size: 76, ContentType: application/octet-stream, Name: sample_attachment.md, AttachmentURL: https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708409/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155542Z&X-Amz-Expires=86400&X-Amz-Signature=9b19a5795e98af94f5b878a62db473b943986d59496e91f046ab4368b0b4f461&X-Amz-SignedHeaders=Host 2019-02-05T15:55:42Z CCEmail: example1@gmail.com,
example2@gmail.com,
example3@gmail.com
TicketID: 108
BodyHTML:
Demonstrating the Ticket Reply Command

ToEmail: example@gmail.com
FromEmail: Demisto support@demistohelp.freshdesk.com
44007154227 2019-02-05T15:55:42Z

 

10. Get a list of all replies and notes for a ticket


Returns a list of all replies and notes for a specified ticket.

Base Command

fd-get-ticket-conversations

Input
Argument Name Description Required
ticket_id ID of the ticket that you want to list all conversations for Required

 

Context Output
Path Type Description
Freshdesk.Ticket.Conversation.BodyHTML String Content of the conversation (in HTML format)
Freshdesk.Ticket.Conversation.BodyText String Content of the conversation (in plain text format)
Freshdesk.Ticket.Conversation.ID Number ID of the conversation
Freshdesk.Ticket.Conversation.Incoming Boolean Set to true when a particular conversation should appear as being created outside of the web portal
Freshdesk.Ticket.Conversation.ToEmail Unknown Array of email addresses of agents/users who need to be notified about this conversation
Freshdesk.Ticket.Conversation.Private Boolean Set to true if the note is private
Freshdesk.Ticket.Conversation.Source Number Denotes the type of conversation
Freshdesk.Ticket.Conversation.SupportEmail String Email address from which the reply is sent. For notes
Freshdesk.Ticket.Conversation.TicketID Number ID of the ticket that the conversation was added to
Freshdesk.Ticket.Conversation.UserID Number ID of the agent/user who added the conversation
Freshdesk.Ticket.Conversation.CreatedAt Date Conversation creation timestamp
Freshdesk.Ticket.Conversation.UpdatedAt Date Conversation updated timestamp
Freshdesk.Ticket.Conversation.FromEmail String The email address that the reply was sent from. By default
Freshdesk.Ticket.Conversation.Attachment.AttachmentURL String URL of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.Name String The name of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.ID Number ID number of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.Size Number Size of the file attached to the ticket

 

Command Example
!fd-get-ticket-conversations ticket_id=108
Context Example
{
    "Freshdesk.Ticket": {
        "Conversation": [
            {
                "Category": 3,
                "BodyText": "Demonstrating the Ticket Reply Command",
                "UserID": 2043022085976,
                "Attachment": [
                    {
                        "Name": "sample_attachment.md",
                        "Size": 76,
                        "ContentType": "application/octet-stream",
                        "ID": 2043010708409,
                        "AttachmentURL": "https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708409/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155543Z&X-Amz-Expires=86400&X-Amz-Signature=f26a2ab003aa5a9a0862389ae1c4a44470bf06ba0acd0e60c742ca164c16dff2&X-Amz-SignedHeaders=Host"
                    }
                ],
                "UpdatedAt": "2019-02-05T15:55:42Z",
                "AdditionalFields": {
                    "TicketID": 108,
                    "CCEmail": [
                        "example1@gmail.com",
                        "example2@gmail.com",
                        "example3@gmail.com"
                    ],
                    "BodyHTML": "<div>Demonstrating the Ticket Reply Command</div>",
                    "ToEmail": [
                        "example@gmail.com"
                    ],
                    "FromEmail": "Demisto <support@demistohelp.freshdesk.com>",
                    "SupportEmail": "support@demistohelp.freshdesk.com"
                },
                "ID": 44007154227,
                "CreatedAt": "2019-02-05T15:55:42Z"
            }
        ],
        "ID": 108
    }
}
Human Readable Output

Conversations of Ticket #108

Category BodyText UserID Attachment UpdatedAt AdditionalFields ID CreatedAt
3 Demonstrating the Ticket Reply Command 2043022085976 ID: 2043010708409, Size: 76, ContentType: application/octet-stream, Name: sample_attachment.md, AttachmentURL: https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708409/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155543Z&X-Amz-Expires=86400&X-Amz-Signature=f26a2ab003aa5a9a0862389ae1c4a44470bf06ba0acd0e60c742ca164c16dff2&X-Amz-SignedHeaders=Host 2019-02-05T15:55:42Z FromEmail: Demisto support@demistohelp.freshdesk.com
BodyHTML:
Demonstrating the Ticket Reply Command

SupportEmail: support@demistohelp.freshdesk.com
CCEmail: example1@gmail.com,
example2@gmail.com,
example3@gmail.com
TicketID: 108
ToEmail: example@gmail.com
44007154227 2019-02-05T15:55:42Z

 

11. Get a list of all agents


Returns a list of agents that match the filter criteria.

Base Command

fd-list-agents

Input
Argument Name Description Required
mobile Mobile phone number to filter agents by. Enter the number without dashes or spaces between the numbers.
Numbers should be entered as they appear in your Freshdesk web portal. If the number appears in your web portal with a plus sign and country code, then that is how you should enter here, e.g., ‘+972501231231’.
Optional
phone Telephone number to filter agents by. Enter the number without dashes or spaces between the numbers.
Numbers should be entered as they appear in your Freshdesk web portal. If the number appears in your web portal with a plus sign and country code, then that is how you should enter it here, e.g., ‘+972501231231’.
Optional
state List all agents who are either ‘fulltime’ or ‘occasional’ Optional

 

Context Output
Path Type Description
Freshdesk.Agent.Available Boolean Set to true when the agent is in a group that has enabled “Automatic Ticket Assignment” and is accepting new tickets
Freshdesk.Agent.AvailableSince Date Timestamp that denotes when the agent became available/unavailable (depending on the value of the ‘available’ attribute)
Freshdesk.Agent.ID Number User ID of the agent
Freshdesk.Agent.Occasional Boolean Set to true when the agent is an occasional agent (true => occasional
Freshdesk.Agent.Signature String Signature of the agent (in HTML format)
Freshdesk.Agent.TicketScope Number Ticket permission of the agent
(1 - Global Access, 2 - Group Access, 3 - Restricted Access)    
Freshdesk.Agent.GroupID Unknown Group IDs associated with the agent
Freshdesk.Agent.RoleID Unknown Role IDs associated with the agent
Freshdesk.Agent.CreatedAt Date Agent creation timestamp
Freshdesk.Agent.UpdatedAt Date Agent updated timestamp
Freshdesk.Agent.Contact.Active Boolean Set to true when the agent is verified
Freshdesk.Agent.Contact.Email String Email Address of the agent
Freshdesk.Agent.Contact.JobTitle String Job title of the agent
Freshdesk.Agent.Contact.Language String Language of the agent. Default language is “en”
Freshdesk.Agent.Contact.LastLoginAt Date Timestamp of the agent’s last successful login
Freshdesk.Agent.Contact.Mobile Number Mobile number of the agent
Freshdesk.Agent.Contact.Name String Name of the agent
Freshdesk.Agent.Contact.Phone Number Telephone number of the agent
Freshdesk.Agent.Contact.TimeZone String Time zone of the agent
Freshdesk.Agent.Contact.CreatedAt Date Contact creation timestamp
Freshdesk.Agent.Contact.UpdatedAt Date Contact updated timestamp
Freshdesk.Agent.Type String Type of agent

 

Command Example
!fd-list-agents state=fulltime
Context Example
{
    "Freshdesk.Agent": [
        {
            "TicketScope": 1,
            "Contact": {
                "Name": "Jeff Testman",
                "Language": "en",
                "LastLoginAt": "2019-01-23T09:05:34Z",
                "Phone": "506912312",
                "UpdatedAt": "2019-01-20T09:14:26Z",
                "Active": true,
                "TimeZone": "Athens",
                "Email": "jeffrey.collins@gmail.com",
                "CreatedAt": "2019-01-20T09:06:31Z"
            },
            "UpdatedAt": "2019-02-04T17:12:33Z",
            "Type": "support_agent",
            "ID": 2043022085976,
            "CreatedAt": "2019-01-20T09:06:31Z"
        }
    ]
}
Human Readable Output

All Agents

TicketScope Contact UpdatedAt Type ID CreatedAt
1 Name: Jeff Testman
Language: en
LastLoginAt: 2019-01-23T09:05:34Z
Phone: 506912312
UpdatedAt: 2019-01-20T09:14:26Z
Active: true
TimeZone: Athens
Email: jeffrey.collins@gmail.com
CreatedAt: 2019-01-20T09:06:31Z
2019-02-04T17:12:33Z support_agent 2043022085976 2019-01-20T09:06:31Z

 

12. Create a note for a ticket


Creates a note for a specified ticket. By default, notes are private. To make a note public, set the ‘private’ argument to false.

Base Command

fd-create-ticket-note

Input
Argument Name Description Required
ticket_id ID of the ticket to make a note for Required
body Content of the note (in HTML format) Required
private Set to false if the note is public Optional
user_id ID of the agent who is adding the note.
To find agent ID numbers, run the ‘fd-list-agents’ command.
Optional
notify_emails CSV list of agent email addresses to be notify about this note, e.g., "example1@gmail.com,example2@gmail.com,example3@gmail.com" Optional
incoming Set to true if a particular note should appear as being created outside of the web portal Optional
attachments CSV list of entry IDs of files to attach to the note, e.g., “468@73f988d1-bda2-4adc-8e02-926f02190070,560@73f988d1-bda2-4adc-8e02-926f02190070”. The total size of these attachments cannot exceed 15MB. Optional

 

Context Output
Path Type Description
Freshdesk.Ticket.Conversation.BodyHTML String Content of the conversation (in HTML)
Freshdesk.Ticket.Conversation.BodyText String Content of the conversation (in plain text)
Freshdesk.Ticket.Conversation.ID Number ID of the conversation
Freshdesk.Ticket.Conversation.Incoming Boolean Set to true when a particular conversation should appear as being created outside of the web portal
Freshdesk.Ticket.Conversation.ToEmail Unknown List of agent/user email addresses of agents/users who need to be notified about this conversation
Freshdesk.Ticket.Conversation.Private Boolean Is the conversation private
Freshdesk.Ticket.Conversation.Source Number Conversation type
Freshdesk.Ticket.Conversation.SupportEmail String Email address the reply is sent from. For notes
Freshdesk.Ticket.Conversation.TicketID Number ID of the ticket the conversation was added to
Freshdesk.Ticket.Conversation.UserID Number ID of the agent/user who added the conversation
Freshdesk.Ticket.Conversation.CreatedAt Date Conversation creation timestamp
Freshdesk.Ticket.Conversation.UpdatedAt Date Conversation updated timestamp
Freshdesk.Ticket.Conversation.FromEmail String The email address that the reply/note was sent from. By default
Freshdesk.Ticket.Conversation.Attachment.AttachmentURL String URL of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.Name String The name of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.ID Number ID number of the file attached to the ticket
Freshdesk.Ticket.Conversation.Attachment.Size Number ize of the file attached to the ticket

 

Command Example
!fd-create-ticket-note ticket_id=108 body="Demonstrating the Ticket Reply Command" attachments=2@5a0be47f-748f-4d60-8f46-81feb8f0c438 incoming=true private=false
Context Example
{
    "Freshdesk.Ticket": {
        "Conversation": {
            "BodyText": "Demonstrating the Ticket Reply Command",
            "Incoming": true,
            "UserID": 2043022085976,
            "Attachment": [
                {
                    "Name": "sample_attachment.md",
                    "Size": 76,
                    "ContentType": "application/octet-stream",
                    "ID": 2043010708410,
                    "AttachmentURL": "https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708410/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155545Z&X-Amz-Expires=86400&X-Amz-Signature=db5ba2faa0753ffbe06b5b575500eb3bf55eb6460feab531cc4a723ce9e15512&X-Amz-SignedHeaders=Host"
                }
            ],
            "UpdatedAt": "2019-02-05T15:55:45Z",
            "AdditionalFields": {
                "BodyHTML": "<div>Demonstrating the Ticket Reply Command</div>",
                "TicketID": 108
            },
            "ID": 44007154233,
            "CreatedAt": "2019-02-05T15:55:45Z"
        },
        "ID": 108
    }
}
Human Readable Output

Note for Ticket #108

BodyText Incoming UserID Attachment UpdatedAt AdditionalFields ID CreatedAt
Demonstrating the Ticket Reply Command true 2043022085976 ID: 2043010708410, Size: 76, ContentType: application/octet-stream, Name: sample_attachment.md, AttachmentURL: https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/2043010708410/original/sample_attachment.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJ2JSYZ7O3I4JO6DA%2F20190205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20190205T155545Z&X-Amz-Expires=86400&X-Amz-Signature=db5ba2faa0753ffbe06b5b575500eb3bf55eb6460feab531cc4a723ce9e15512&X-Amz-SignedHeaders=Host 2019-02-05T15:55:45Z TicketID: 108
BodyHTML:
Demonstrating the Ticket Reply Command
44007154233 2019-02-05T15:55:45Z

 

13. Delete a ticket


Deletes a ticket, specified by ID.

Base Command

fd-delete-ticket

Input
Argument Name Description Required
id ID of the ticket to delete Required

 

Context Output

There is no context output for this command.

 
Command Example
!fd-delete-ticket id=108
Context Example
{
    "Freshdesk.Ticket": {
        "AdditionalFields": {
            "Deleted": true
        },
        "ID": 108
    }
}
Human Readable Output

Soft-Deleted Ticket #108

14. Search tickets


Returns a list of all tickets that match the filter criteria. If no filters are specified, all tickets from the last 30 days are returned.
Note that this command can consume multiple API credits. This can occur if the count of tickets resulting from your query exceeds 30. In that instance this command makes additional calls to the API to retrieve additional tickets matching your query.

Base Command

fd-search-tickets

Input
Argument Name Description Required
filter Predefined filters for filtering tickets. The options are:
‘new_and_my_open’ - New and my open tickets.
‘watching’ - Tickets I’m watching.
‘spam’ - Tickets that have been marked as spam.
‘deleted’ - Tickets that have been soft-deleted, aka moved to Trash.
Optional
requester Filter results by the ticket requester’s email address or ID. To find your contacts’ ID numbers or email addresses, run the fd-list-contacts command. Optional
updated_since By default, tickets created within the previous 30 days are returned. For older tickets, use this filter (“2015-01-19T02:00:00Z”) Optional
order_by Field for ordering the list of tickets. The default sort order uses the ‘created_at’ field. Optional
order_type Return list results in ascending or descending order according to the order_by value, default is descending Optional
include_stats If set to ‘true’ then ticket’s closed_at, resolved_at and first_responded_at time will be included. Note that this is not set by default because setting this to ‘true’ will consume an additional 2 API credits per API call. To see more details, see the Freshdesk API documentation. Optional
include_requester If set to ‘true’ then the ticket requester’s ID, email address, mobile number, name, and phone number are included in the ticket’s output for each ticket. Note that this is not set by default because setting this to ‘true’ will consume an additional 2 API credits per API call. To see more details, see the Freshdesk API documentation. Optional
include_description If set to ‘true’ then the ticket’s description and description_text are included the tickets’ outputs. Note that this is not set by default because setting this to ‘true’ will consume an additional 2 API credits per API call. To see more details, see the Freshdesk API documentation. Optional
custom_query Filter tickets using a custom query.

Format - "(ticket_field:integer OR ticket_field:‘string’) AND ticket_field:boolean"
Example - "(type:‘Question’ OR type:‘Problem’) AND (due_by:>‘2017-10-01’ AND due_by:<‘2017-10-07’)"
For more examples, see the Freshdesk API documentation.

Note that the custom_query argument cannot be used in conjunction with this command’s other arguments.
Optional

 

Context Output
Path Type Description
Freshdesk.Ticket.ID Number ID number of the fetched ticket
Freshdesk.Ticket.Priority Number Ticket priority
Freshdesk.Ticket.DueBy Date Timestamp that denotes when the ticket is due to be resolved
Freshdesk.Ticket.Subject String Ticket subject
Freshdesk.Ticket.Status Number Ticket status
Freshdesk.Ticket.RequesterID Number User ID of the requester
Freshdesk.Ticket.Tag Unknown Tags associated with the ticket
Freshdesk.Ticket.GroupID Number ID of the group the ticket was assigned to
Freshdesk.Ticket.Source Number The channel through which the ticket was created
Freshdesk.Ticket.CreatedAt Date Ticket creation timestamp
Freshdesk.Ticket.ResponderID Number ID of the agent the ticket was assigned to
Freshdesk.Ticket.FrDueBy Date Timestamp that denotes when the first response is due
Freshdesk.Ticket.Conversation Unknown Conversations associated with this ticket
Freshdesk.Ticket.Attachment.AttachmentURL Unknown URL to download the file attached to the ticket to your local machine
Freshdesk.Ticket.Attachment.Name Unknown The name of the file attached to the ticket
Freshdesk.Ticket.Attachment.ContentType String Content type of the file attached to the ticket
Freshdesk.Ticket.Attachment.ID Unknown ID number of the file attached to the ticket
Freshdesk.Ticket.Attachment.Size String Size of the file attached to the ticket

 

Command Example
!fd-search-tickets updated_since=2019-02-05T08:30:00Z
Context Example
{
    "Freshdesk.Ticket": [
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-06T13:30:16Z",
            "FrDueBy": "2019-02-05T14:30:16Z",
            "GroupID": null,
            "Priority": 3,
            "Source": 2,
            "Tag": [
                "new",
                "attention needed",
                "billing related"
            ],
            "RequesterID": 2043024010476,
            "UpdatedAt": "2019-02-05T14:32:44Z",
            "AdditionalFields": {},
            "ID": 105,
            "CreatedAt": "2019-02-05T10:30:16Z",
            "Subject": "Demonstrate Ticket Creation"
        },
        {
            "Status": 2,
            "ResponderID": 2043022085976,
            "DueBy": "2019-02-05T11:05:48Z",
            "FrDueBy": "2019-02-05T08:05:50Z",
            "GroupID": 2043000867326,
            "Priority": 4,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T13:07:54Z",
            "AdditionalFields": {},
            "ID": 97,
            "CreatedAt": "2019-02-05T07:05:48Z",
            "Subject": "Testing Ticket Update"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-05T10:00:00Z",
            "FrDueBy": "2019-02-05T07:00:00Z",
            "GroupID": 2043000867326,
            "Priority": 4,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T12:08:05Z",
            "AdditionalFields": {},
            "ID": 96,
            "CreatedAt": "2019-02-04T21:09:31Z",
            "Subject": "Testing Ticket Update"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-05T10:00:00Z",
            "FrDueBy": "2019-02-05T07:00:00Z",
            "GroupID": 2043000867326,
            "Priority": 4,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T12:08:03Z",
            "AdditionalFields": {},
            "ID": 95,
            "CreatedAt": "2019-02-04T21:03:46Z",
            "Subject": "Testing Ticket Update"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-07T15:00:00Z",
            "FrDueBy": "2019-02-05T15:00:00Z",
            "GroupID": null,
            "Priority": 1,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T15:00:49Z",
            "AdditionalFields": {},
            "ID": 94,
            "CreatedAt": "2019-02-04T21:00:41Z",
            "Subject": "Testing Ticket Creation"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-07T15:00:00Z",
            "FrDueBy": "2019-02-05T15:00:00Z",
            "GroupID": null,
            "Priority": 1,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T15:00:48Z",
            "AdditionalFields": {},
            "ID": 93,
            "CreatedAt": "2019-02-04T20:19:19Z",
            "Subject": "Testing Ticket Creation"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-05T13:20:08Z",
            "FrDueBy": "2019-02-01T13:20:08Z",
            "GroupID": null,
            "Priority": 1,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T13:33:04Z",
            "AdditionalFields": {},
            "ID": 61,
            "CreatedAt": "2019-01-31T13:20:08Z",
            "Subject": "Testing Ticket Creation"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-05T12:56:02Z",
            "FrDueBy": "2019-02-01T12:56:02Z",
            "GroupID": 2043000867326,
            "Priority": 1,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T15:08:34Z",
            "AdditionalFields": {},
            "ID": 60,
            "CreatedAt": "2019-01-31T12:56:02Z",
            "Subject": "Testing Ticket Creation"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-05T12:53:29Z",
            "FrDueBy": "2019-02-01T12:53:29Z",
            "GroupID": 2043000867326,
            "Priority": 1,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023528657,
            "UpdatedAt": "2019-02-05T15:08:33Z",
            "AdditionalFields": {},
            "ID": 59,
            "CreatedAt": "2019-01-31T12:53:29Z",
            "Subject": "Lets see RawJson"
        },
        {
            "Status": 2,
            "ResponderID": null,
            "DueBy": "2019-02-05T12:51:35Z",
            "FrDueBy": "2019-02-01T12:51:35Z",
            "GroupID": 2043000867326,
            "Priority": 1,
            "Source": 2,
            "Tag": [],
            "RequesterID": 2043023521956,
            "UpdatedAt": "2019-02-05T15:08:33Z",
            "AdditionalFields": {},
            "ID": 58,
            "CreatedAt": "2019-01-31T12:51:35Z",
            "Subject": "Testing Ticket Creation"
        }
    ]
}
Human Readable Output

Viewing All Requested Tickets

ID Priority Status Subject DueBy FrDueBy RequesterID GroupID Source CreatedAt UpdatedAt Tag
105 3 2 Demonstrate Ticket Creation 2019-02-06T13:30:16Z 2019-02-05T14:30:16Z 2043024010476   2 2019-02-05T10:30:16Z 2019-02-05T14:32:44Z new,
attention needed,
billing related
97 4 2 Testing Ticket Update 2019-02-05T11:05:48Z 2019-02-05T08:05:50Z 2043023521956 2043000867326 2 2019-02-05T07:05:48Z 2019-02-05T13:07:54Z  
96 4 2 Testing Ticket Update 2019-02-05T10:00:00Z 2019-02-05T07:00:00Z 2043023521956 2043000867326 2 2019-02-04T21:09:31Z 2019-02-05T12:08:05Z  
95 4 2 Testing Ticket Update 2019-02-05T10:00:00Z 2019-02-05T07:00:00Z 2043023521956 2043000867326 2 2019-02-04T21:03:46Z 2019-02-05T12:08:03Z  
94 1 2 Testing Ticket Creation 2019-02-07T15:00:00Z 2019-02-05T15:00:00Z 2043023521956   2 2019-02-04T21:00:41Z 2019-02-05T15:00:49Z  
93 1 2 Testing Ticket Creation 2019-02-07T15:00:00Z 2019-02-05T15:00:00Z 2043023521956   2 2019-02-04T20:19:19Z 2019-02-05T15:00:48Z  
61 1 2 Testing Ticket Creation 2019-02-05T13:20:08Z 2019-02-01T13:20:08Z 2043023521956   2 2019-01-31T13:20:08Z 2019-02-05T13:33:04Z  
60 1 2 Testing Ticket Creation 2019-02-05T12:56:02Z 2019-02-01T12:56:02Z 2043023521956 2043000867326 2 2019-01-31T12:56:02Z 2019-02-05T15:08:34Z  
59 1 2 Lets see RawJson 2019-02-05T12:53:29Z 2019-02-01T12:53:29Z 2043023528657 2043000867326 2 2019-01-31T12:53:29Z 2019-02-05T15:08:33Z  
58 1 2 Testing Ticket Creation 2019-02-05T12:51:35Z 2019-02-01T12:51:35Z 2043023521956 2043000867326 2 2019-01-31T12:51:35Z 2019-02-05T15:08:33Z  

Configuration parameters

  • url — Server URL (e.g., https://demistohelp.freshdesk.com) (required)
  • token — API Token. (You must enter either the API token or your Freshdesk credentials)
  • token_creds
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • fetch_time — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • credentials — Username
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • maxFetch — Maximum number of incidents per fetch.

Commands (14)

  • fd-create-ticket

    Creates a new Freshdesk ticket.

  • fd-create-ticket-note

    Creates a note for a specified ticket. By default, notes are private. To make a note public, set the 'private' argument to false.

  • fd-delete-ticket

    Deletes a ticket.

  • fd-get-canned-response-folder

    View the details of all the Canned Responses in a Folder.

  • fd-get-contact

    View the details of the contact specified by the ID number.

  • fd-get-ticket

    Gets details of a ticket, specified by the ticket ID number.

  • fd-get-ticket-conversations

    Lists all replies and notes for a specified ticket.

  • fd-list-agents

    Returns a list of agents that match the filter criteria.

  • fd-list-canned-response-folders

    List all Canned Response Folders (Only users with Admin Privileges).

  • fd-list-contacts

    List all contacts matching the specified filters. If no filters are provided then all unblocked and undeleted contacts will be returned.

  • fd-list-groups

    Lists all groups.

  • fd-search-tickets

    Returns a list of all tickets that match the filter criteria. If no filters are specified, all tickets from the last 30 days are returned. Note that this command can consume multiple API credits - this can occur if the count of tickets resulting from your query exceeds 30 - in that instance this command makes additional calls to the API to retrieve additional tickets matching your query.

  • fd-ticket-reply

    Adds a reply to a specified ticket.

  • fd-update-ticket

    Update the ticket specified by the ID.

import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *

""" IMPORTS """


import requests
import urllib3

# Disable insecure warnings
urllib3.disable_warnings()
# Remove proxy if not set to true in params
handle_proxy(proxy_param_name="proxy")
""" GLOBALS/PARAMS """

PARAMS = demisto.params()
CREDS = PARAMS.get("credentials")
USERNAME = CREDS.get("identifier") if CREDS else None
PASSWORD = CREDS.get("password") if CREDS else None
TOKEN = PARAMS.get("token_creds", {}).get("password") or PARAMS.get("token")

if not (USERNAME and PASSWORD) and not TOKEN:
    err_msg = (
        "You must provide either your Freshdesk account API key or the "
        "username and password you use to sign into your Freshdesk account "
        "when instantiating an instance of the Freshdesk integration."
    )
    return_error(err_msg)

AUTH = (TOKEN, "X") if TOKEN else (USERNAME, PASSWORD)

# How much time before the first fetch to retrieve incidents
FETCH_TIME = PARAMS.get("fetch_time", "24 hours")
# Remove trailing slash to prevent wrong URL path to service
SERVER = PARAMS["url"].removesuffix("/")
# Should we use SSL
USE_SSL = not PARAMS.get("insecure", False)
# Service base URL
BASE_URL = SERVER + "/api/v2/"

# Headers to be sent in requests
HEADERS = {"Content-Type": "application/json", "Accept": "application/json"}
# Headers to be used when making a request to POST a multi-part encoded file
MULTIPART_HEADERS = {"Accept": "application/json"}

# Amount of results returned per fetch (default 50)
MAX_INCIDENTS = int(PARAMS.get("maxFetch", 50))

# Default amount of results returned per-page/per-api-call when the
# fd-search-tickets command's results that match the command's specified
# filter criteria exceeds 30
PER_PAGE = 30

# The API response ticket attributes that will be included
# in most command's context outputs
DEFAULT_TICKET_CONTEXT_FIELDS = [
    "priority",
    "due_by",
    "subject",
    "status",
    "requester_id",
    "tags",
    "group_id",
    "source",
    "created_at",
    "responder_id",
    "fr_due_by",
    "id",
]


""" HELPER FUNCTIONS """


def get_number_of_incidents_to_fetch():
    # FreshDesk API supports maximum of 100 tickets per page so if user asked for more, pagination is needed.
    return 100 if MAX_INCIDENTS >= 100 else MAX_INCIDENTS


def reformat_canned_response_context(context):
    """
    Reformat context for canned-response related commands (from having used string_to_context_key)
    to desired output format.

    parameter: (dict) context
        The context to reformat

    returns:
        The reformatted context
    """
    for key, val in list(context.items()):
        if "Id" in key:
            new_key = key.replace("Id", "ID")
            context[new_key] = val
            del context[key]
        elif "Html" in key:
            new_key = key.replace("Html", "HTML")
            context[new_key] = val
            del context[key]
    return context


def reformat_conversation_context(context):
    """
    Reformat context for conversation related commands (from having used string_to_context_key)
    to desired output format.

    parameter: (dict) context
        The context to reformat

    returns:
        The reformatted context
    """
    to_emails = context.get("ToEmails")
    body = context.get("Body")
    attachments = context.get("Attachments")
    if to_emails:
        context["ToEmail"] = to_emails
        del context["ToEmails"]
    if body:
        context["BodyHTML"] = body
        del context["Body"]
    if attachments:
        del context["Attachments"]
    return context


def format_contact_context(contact):
    """
    Format context for contact related commands.

    parameter: (dict) contact
        The API response from executing a contact related command whose attributes need
        to be parsed into context

    returns:
        The formatted context
    """
    dont_include = ["other_companies", "other_emails", "custom_fields", "avatar"]
    # Parse response into context
    context = {}
    for key, val in contact.items():
        if key not in dont_include and val:
            new_key = string_to_context_key(key)
            if "Id" in new_key:
                new_key = new_key.replace("Id", "ID")
            elif new_key == "Tags":
                new_key = "Tag"
            context[new_key] = val
    return context


def reformat_ticket_context(context):
    """
    Reformat context outputs (from having used string_to_context_key) to desired output format.

    parameter: (dict) context
        The context to reformat

    returns:
        The reformatted context
    """
    standard_context_outputs = [
        "Priority",
        "DueBy",
        "Subject",
        "Status",
        "RequesterID",
        "Tag",
        "GroupID",
        "Source",
        "CreatedAt",
        "ResponderID",
        "FrDueBy",
        "ID",
        "UpdatedAt",
        "Attachment",
        "AdditionalFields",
        "UserID",
        "BodyText",
        "Category",
        "Private",
        "Incoming",
    ]

    for key, val in list(context.items()):
        if key == "Tags":
            new_key = key[:-1]
            context[new_key] = val
            del context[key]
        elif key == "CustomFields":
            new_key = key[:-1]
            context[new_key] = val
            del context[key]
        elif key == "FwdEmails":
            new_key = key[:-1]
            context[new_key] = val
            del context[key]
        elif key == "Description":
            new_key = "DescriptionHTML"
            context[new_key] = val
            del context[key]
        elif "Id" in key:
            new_key = key.replace("Id", "ID")
            context[new_key] = val
            del context[key]
        elif "Cc" in key:
            new_key = key.removesuffix("s").replace("Cc", "CC")
            context[new_key] = val
            del context[key]

    # If 'Attachments' are still in context get rid of them - should be 'Attachment'
    attachments = context.get("Attachments")
    if attachments:
        del context["Attachments"]

    new_context = {}
    new_context["AdditionalFields"] = context.get("AdditionalFields") if context.get("AdditionalFields") else {}
    additional_fields = {}
    for key, val in list(context.items()):
        if key not in standard_context_outputs:
            if not ((isinstance(val, dict | list)) and len(val) == 0):
                additional_fields[key] = val
        else:
            new_context[key] = val
    new_context["AdditionalFields"] = dict(new_context.get("AdditionalFields", {}), **additional_fields)
    return new_context


def handle_search_tickets_pagination(args, response, limit=-1):
    """
    Retrieve all resulting tickets even over the default 30 returned by a single API call.

    When the search_tickets_command results in more tickets than the default per page count (30) returned from
    making an API call, then this function retrieves the remaining tickets by iterating and making API calls
    per additional page of results.

    parameter: (dict) args
        search_tickets_command arguments

    parameter: (dict) response
        The initial json response from making an API call in the search_tickets function

    parameter: (int) limit
        Stops the pagination as soon as the number of tickets exceeds the limit (default -1 for no limit)

    returns:
        All Ticket Objects
    """
    # If user entered custom_query arg, the resulting tickets are in the 'results' attribute of the response
    if args.get("custom_query"):
        # Max page count allowed by API when using custom query
        max_pages = 10
        # Deal with pagination if necessary
        tickets = response.get("results")
        total_tickets = response.get("total")
        total_tickets -= PER_PAGE
        page = 1
        while total_tickets > 0 and page <= max_pages:
            page += 1
            args["page"] = page
            tickets_page = search_tickets(args)
            tickets.extend(tickets_page.get("results"))
            total_tickets -= PER_PAGE
    else:
        # Max page count allowed by API when using normal filters
        max_pages = 300
        tickets = response
        page = 1
        next_page = tickets
        while next_page and page <= max_pages:
            # Stop pagination if limit is defined and we exceeded it
            if 0 < limit <= len(tickets):
                break
            page += 1
            args["page"] = page
            next_page = search_tickets(args)
            if next_page:
                tickets.extend(next_page)
    return tickets


def attachments_into_context(api_response, context):
    """
    Get the attachments field from the api_response argument if present and parse it into the context.

    parameter: (dict) api_response
        The json response returned by the calling function's associated 'requests' function in which calls
        to the API are made.

    parameter: (dict) context
        The context that will be modified and returned to the war room

    returns:
        The modified context, and the modified context with the attachments in readable format for the
        human readable output
    """
    attachment_keys_to_include = ["attachment_url", "content_type", "id", "name", "size"]

    context_readable = dict(**context)
    # Parse attachments into context
    attachments = api_response.get("attachments")
    if attachments:
        attachments_context = []
        attachments_context_readable = []
        for attachment in attachments:
            attachment_context = {}
            for key, val in attachment.items():
                if key in attachment_keys_to_include:
                    if key == "attachment_url":
                        key = "AttachmentURL"
                    elif key == "id":
                        key = "ID"
                    else:
                        key = string_to_context_key(key)
                    attachment_context[key] = val
            attachment_formatted = formatCell(attachment_context).split("\n")
            attachment_formatted = ", ".join(attachment_formatted)
            attachments_context_readable.append(attachment_formatted)

            attachments_context.append(attachment_context)
        context["Attachment"] = attachments_context
        context_readable["Attachment"] = attachments_context_readable
    return context, context_readable


def additional_fields_to_context(context, already_in_context, additional_fields, additional_values):
    """
    Parses fields not presented as part of the command's standard arguments into the context.

    For commands where the user can enter additional fields and their associated values beyond what
    is offered by the standard arguments (but are still supported by the API endpoint). If the additional
    fields are not part of the standard context output for that command, then those fields and values
    from the API call are parsed and subcategorized under the 'AdditionalFields' context output.

    parameter: (dict) context
        The context that will be modified and returned to the war room

    parameter: (list) already_in_context
        List of fields which are ordinarily/already parsed into the context

    parameter: (list) additional_fields
        List of the fields beyond the command's standard arguments that the user entered as part
        of the call to the API endpoint

    parameter: (list) additional_values
        List of values corresponding to the additional_fields argument

    returns:
        The modified context
    """
    # Parse additional fields into context
    if additional_fields and additional_values:
        added_context = {}
        for field, value in zip(additional_fields, additional_values):
            if field not in already_in_context and field != "attachments":
                key = string_to_context_key(field)
                added_context[key] = value
        context["AdditionalFields"] = added_context
    return context


def additional_fields_to_args(args, additional_fields_arg_name):
    """
    Parses the additional_fields command argument for the individual fields and values and
    reassigns them to the args dictionary.

    parameter: (dict) args
        The command's arguments

    parameter: (string) additional_fields_arg_name
        The name of the command argument that contains the additional fields and values

    returns:
        The args dictionary that has been updated with the additional fields and values,
        the list of additional fields, and the list of additional values.
    """
    additional_fields = args.get(additional_fields_arg_name)
    if additional_fields:
        fields, values = [], []
        fields_and_vals = additional_fields.split(";")
        # For the case there is only one additional field + value
        if len(fields_and_vals) == 1:
            fields_and_vals = list(fields_and_vals)
        for field_and_val in fields_and_vals:
            field_and_val = field_and_val.split("=")
            # If the length doesn't equal 2, means there were either no equal signs or more than one
            if len(field_and_val) != 2:
                err_msg = "It appears you entered either too many or too few equal signs in the 'additional_fields' argument."
                return_error(err_msg)
            field = field_and_val[0].strip()
            val = field_and_val[1]

            # If the value contains commas, then it is a list
            if "," in val:
                val = argToList(val)
            args[field] = val
            fields.append(field)
            values.append(val)
        del args[additional_fields_arg_name]
        return args, fields, values
    return args, None, None


def ticket_to_incident(ticket):
    """
    Create incident from ticket object.

    parameter: (object) ticket
        Ticket object

    returns:
        Incident Object
    """
    incident = {}
    # Incident Title
    subject = ticket.get("subject", "").encode("ascii", "replace").decode("utf-8")
    incident["name"] = f'Freshdesk Ticket: "{subject}"'
    # Incident update time - the ticket's update time - The API does not support filtering tickets by creation time
    # but only by update time. The update time will be the creation time of the incidents and the incident id check will
    # prevent duplications of incidents.
    incident["occurred"] = ticket.get("updated_at")
    # The raw response from the service, providing full info regarding the item
    incident["rawJSON"] = json.dumps(ticket)
    return incident


def get_additional_fields(args):
    """
    Determine which fields need to be added to context based off arguments given in the search_tickets_command.

    parameter: (dict) args
        The search_tickets_command arguments

    returns:
        List of fields to be added to the context outputs
    """
    additional_fields = []  # fields that should be added to output context
    filter = args.get("filter")
    if filter:
        if filter == "deleted":
            additional_fields.append("deleted")
        elif filter == "spam":
            additional_fields.append("spam")
    requester = args.get("requester")
    if requester and "@" in requester:
        additional_fields.append("email")
    company_id = args.get("company_id")
    if company_id:
        additional_fields.append("company_id")
    if args.get("include_description") and args.get("include_description").lower() == "yes":
        additional_fields.extend(["description", "description_text"])
    return additional_fields


def entries_to_files(entry_ids):
    """
    Format file details (retrieved using the files' entry IDs) to API expectations to include files in API call.

    parameter: (list) entry_ids
        List of entry ID strings for files uploaded to the warroom

    returns:
        List of attachment field, value tuples formatted according to API expectations
    """
    attachments = []
    for entry_id in entry_ids:
        execute_results = demisto.getFilePath(entry_id)
        file_path = execute_results["path"]
        file_name = execute_results["name"]
        attachments.append(("attachments[]", (file_name, open(file_path, "rb"))))

    return attachments


def handle_array_input(args):
    """
    Format any command argument that is supposed to be an array from a string to a list.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with field values transformed from strings to lists where necessary
    """
    array_inputs = [
        "tags",
        "attachments",
        "cc_emails",
        "bcc_emails",
        "to_emails",
        "update_fields",
        "update_values",
        "notify_emails",
    ]
    attchs_present = args.get("attachments")
    if attchs_present:
        for arr_input in array_inputs:
            if arr_input in args:
                if arr_input != "attachments":
                    args[arr_input + "[]"] = argToList(args.get(arr_input))
                    del args[arr_input]
                else:
                    args[arr_input] = argToList(args.get(arr_input))
    else:
        for arr_input in array_inputs:
            if arr_input in args:
                args[arr_input] = argToList(args.get(arr_input))
    return args


def validate_priority_input(args):
    """
    Check entered value for command argument 'priority' and format to API expectations.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with the value for 'priority' field reformatted if necessary
    """
    # Parse and ensure valid command argument
    priority = args.get("priority", None)

    # If priority wasn't given by the user as a cmd arg
    # then no need to alter it to API expectations
    if not priority:
        return args

    priorities = ["low", "medium", "high", "urgent"]

    # Check if the user entered status as words - aka the
    # options listed above in 'statuses'
    err_msg = "priority should be one of these values: 1, 2, 3, 4, {}".format(", ".join(priorities))
    if len(priority) > 1:
        if priority.lower() in priorities:
            # Add 1 since API status numbers for tickets start at 1
            # Cast to string so clean_arguments helper function doesn't throw any errors
            args["priority"] = str(priorities.index(priority.lower()) + 1)
        else:
            return_error(err_msg)
    # Otherwise make sure the user entered valid status number
    elif not (0 < int(priority) < 5):
        return_error(err_msg)
    return args


def validate_status_input(args):
    """
    Check entered value for command argument 'status' and format to API expectations.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with the value for 'status' field reformatted if necessary
    """
    # Parse and ensure valid command argument
    status = args.get("status", None)

    # If status wasn't given by the user as a cmd arg
    # then no need to alter it to API expectations
    if not status:
        return args

    statuses = ["open", "pending", "resolved", "closed", "waiting on customer", "waiting on third party"]

    # Check if the user entered status as words - aka the
    # options listed above in 'statuses'
    err_msg = "status should be one of these values: 2, 3, 4, 5, 6, 7, {}".format(", ".join(statuses))
    if len(status) > 1:
        if status.lower() in statuses:
            # Add 2 since API status numbers for tickets start at 2
            # Cast to string so clean_arguments helper function doesn't throw any errors
            args["status"] = str(statuses.index(status.lower()) + 2)
        else:
            return_error(err_msg)
    # Otherwise make sure the user entered valid status number
    elif not (1 < int(status) < 8):
        return_error(err_msg)
    return args


def handle_number_input(args):
    """
    Format any command argument that is supposed to be a number from a string to an int.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with field values transformed from strings to numbers where necessary
    """
    # Command args that should be numbers
    number_args = [
        "requester_id",
        "status",
        "priority",
        "responder_id",
        "email_config_id",
        "group_id",
        "product_id",
        "source",
        "company_id",
    ]
    # Convert cmd args that are expected to be numbers from strings to numbers
    for num_arg in number_args:
        if num_arg in args:
            args[num_arg] = int(args.get(num_arg))
    return args


def clean_arguments(args):
    """
    Perform all validation and reformatting of command arguments.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with all field values reformatted where necessary
    """
    args = validate_status_input(args)
    args = validate_priority_input(args)
    args = handle_array_input(args)
    args = handle_number_input(args)
    return args


def determine_identifier(args):
    """
    Determine whether the input for the 'identifier' argument is an
    email or twitter handle and adjust 'args' accordingly.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with the email or twitter_id field (depending on what the value was)
        assigned the value entered for the 'identifier' argument
    """
    identifier = args.get("identifier")
    if identifier.startswith("@"):
        # Then it's a twitter handle
        args["twitter_id"] = identifier
    elif "@" in identifier:
        # Otherwise assume it's an email address
        args["email"] = identifier
    else:
        err_msg = "The entered value for the 'identifier' argument must be either a Twitter handle or an Email Address."
        return_error(err_msg)
    # Delete identifier field from args since it doesn't match API expected inputs
    del args["identifier"]
    return args


def determine_responder(args):
    """
    Determine whether the input for the 'responder' argument is a group or an agent and adjust 'args' accordingly.

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with the group_id or responder_id field (depending on what the value was)
        assigned the value entered for the 'responder' argument
    """
    responder = args.get("responder", None)
    if responder:
        args = determine_group(args, "responder")
        args = determine_agent(args, "responder")
    return args


def determine_agent(args, key_name):
    """
    Determine if the value points to an agent by checking against all agent names, emails, and IDs, and adjust
    'args' accordingly.

    parameter: (string) key_name
        The name of the command argument

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with responder_id field assigned the appropriate value if the value passed
        for the command argument represented by 'key_name' is associated with an Agent's details
    """
    assigned_agent = args.get(key_name, None)
    if assigned_agent:
        agent_emails, agent_names, agent_ids = [], [], []
        # Get names, emails and ids of agents
        agents = list_agents()
        for agent in agents:
            agent_ids.append(agent.get("id"))
            contact_info = agent.get("contact")
            if contact_info:
                agent_names.append(contact_info.get("name", "").lower())
                agent_emails.append(contact_info.get("email", "").lower())
        # Check if responder value is a contact ID
        if assigned_agent in agent_ids:
            args["responder_id"] = assigned_agent
            # Delete assigned_agent field from args since it doesn't match API expected inputs
            del args[key_name]
        elif assigned_agent.lower() in agent_names:
            agent_name_idx = agent_names.index(assigned_agent.lower())
            args["responder_id"] = agent_ids[agent_name_idx]
            del args[key_name]
        elif assigned_agent.lower() in agent_emails:
            agent_email_idx = agent_emails.index(assigned_agent.lower())
            args["responder_id"] = agent_ids[agent_email_idx]
            del args[key_name]

    return args


def determine_group(args, key_name):
    """
    Determine if the value points to a group by checking against all group names and IDs, and adjust
    'args' accordingly.

    parameter: (string) key_name
        The name of the command argument

    parameter: (dict) args
        The command arguments dictionary

    returns:
        The arguments dict with group_id field assigned the appropriate value if the value passed
        for the command argument represented by 'key_name' is associated with a Group's details
    """
    assigned_group = args.get(key_name, None)
    if assigned_group:
        group_names, group_ids = [], []
        # Get names and ids of groups
        groups = list_groups()
        for group in groups:
            group_ids.append(group.get("id"))
            group_names.append(group.get("name", "").lower())
        # Check if responder value is a group ID
        if assigned_group in group_ids:
            args["group_id"] = assigned_group
            del args[key_name]
        # Or the name of a group
        elif assigned_group.lower() in group_names:
            group_name_idx = group_names.index(assigned_group.lower())
            args["group_id"] = group_ids[group_name_idx]
            del args[key_name]
    return args


def http_request(method, url_suffix, params=None, data=None, files=None, headers=HEADERS):
    """
    A wrapper for requests lib to send our requests and handle requests and responses better.

    parameter: (string) method
        A string denoting the http request method to use.
        Can be 'GET', 'POST, 'PUT', 'DELETE', etc.

    parameter: (string) url_suffix
        The API endpoint that determines which data we are trying to access/change in our
        call to the API

    parameter: (dict) params
        The key/value pairs to be encoded as part of the URL's query string

    parameter: (dict) data
        The key/value pairs to be form-encoded

    parameter: (list) files
        The multipart-encoded files to upload

    parameter: (dict) headers
        The headers to use with the request

    returns:
        JSON Response Object
    """
    res = requests.request(
        method,
        BASE_URL + url_suffix,
        verify=USE_SSL,
        params=params,
        data=data,
        headers=headers,
        auth=AUTH,  # type: ignore[arg-type]
        files=files,
    )
    # Handle error responses gracefully
    if res.status_code not in {200, 201, 202, 204}:
        LOG(res.json())
        LOG(res.text)
        LOG.print_log()
        err_msg = f"Error in API call to Freshdesk Integration [{res.status_code}] - {res.reason}"
        err = json.loads(res.content)
        if err.get("errors"):
            for error in err.get("errors"):
                err_msg += "\n" + json.dumps(error, indent=2)
        else:
            for key, value in res.json().items():
                err_msg += f"\n{key}: {value}"
        return_error(err_msg)
    # Handle response with no content
    elif res.status_code == 204:
        return res

    return res.json()


""" COMMANDS + REQUESTS FUNCTIONS """


def test_module():
    """
    Will try to make a request to the API endpoint for listing all tickets.
    """
    http_request("GET", "tickets")


def fetch_incidents():
    per_page = get_number_of_incidents_to_fetch()

    # demisto.getLastRun() will returns an obj with the previous run in it.
    last_run = demisto.getLastRun()
    # Get the last fetch time and last id fetched if exist
    last_fetch = last_run.get("last_created_incident_timestamp")
    last_incident_id = last_run.get("last_incident_id", -1)
    # Handle first time fetch, fetch incidents retroactively
    if not last_fetch:
        last_fetch, _ = parse_date_range(FETCH_TIME, to_timestamp=True)
    updated_since = timestamp_to_datestring(last_fetch, date_format="%Y-%m-%dT%H:%M:%SZ")
    args = {"updated_since": updated_since, "order_type": "asc", "per_page": per_page}

    response = search_tickets(args)  # page 1
    # handle pagination until user's limit
    tickets = handle_search_tickets_pagination(args, response, limit=MAX_INCIDENTS)
    # convert the ticket/events to demisto incidents
    incidents = []
    for ticket in tickets:
        incident = ticket_to_incident(ticket)
        incident_id = ticket.get("id")
        incident_date = date_to_timestamp(incident.get("occurred"), "%Y-%m-%dT%H:%M:%SZ")
        # Update last run and add incident if the incident is newer than last fetch and was not fetched before
        # The incident IDs are in incremental order.
        if incident_date >= last_fetch and incident_id > last_incident_id:
            last_fetch = incident_date
            incidents.append(incident)
            last_incident_id = incident_id
        if len(incidents) >= MAX_INCIDENTS:
            break

    demisto.setLastRun({"last_created_incident_timestamp": last_fetch, "last_incident_id": last_incident_id})
    demisto.incidents(incidents)


"""<------ TICKETS ------>"""


def create_ticket(args):
    args = determine_identifier(args)
    args = determine_responder(args)
    args = clean_arguments(args)
    endpoint_url = "tickets"

    response = None
    if not args.get("attachments"):
        # The service endpoint to request from
        # Send a request using our http_request wrapper
        response = http_request("POST", endpoint_url, data=json.dumps(args))
    else:
        # Get the files from their entry IDs
        attachments = entries_to_files(args.get("attachments"))
        # Format to API expectations
        del args["attachments"]
        # Send a request and get raw response
        response = http_request("POST", endpoint_url, data=args, files=attachments, headers=MULTIPART_HEADERS)
    return response


def create_ticket_command():
    """
    Create a new Freshdesk ticket.

    demisto parameter: (string) subject
        Subject of the ticket. The default Value is null.

    demisto parameter: (string) description
        Details of the issue for which you are creating a ticket.

    demisto parameter: (number) priority
        Priority of the ticket. Each number has a corresponding value.
        1 is Low, 2 is Medium, 3 is High, 4 is Urgent.

    demisto parameter: (number) status
        Status of the ticket. Each number has a corresponding value.
        2 is Open, 3 is Pending, 4 is Resolved, 5 is Closed, 6 is Waiting
        on Customer, 7 is Waiting on Third Party.

    demisto parameter: (string) identifier
        This can be an email address or a twitter handle

    demisto parameter: (list) responder
        ID or name of the group or agent to whom you wish to assign this ticket.
        To find potential assignees, try executing the fd-list-groups command.

    demisto parameter: (list) attachments
        Entry IDs of files to attach to the ticket.
        The total size of these attachments cannot exceed 15MB.

    demisto parameter: (list) additional_fields
        Additional ticket fields you wish to set the value of

    returns:
        Ticket Object
    """
    # Get command arguments from user
    args = demisto.args()

    # Handle additional_fields command arguments
    args, additional_fields, additional_values = additional_fields_to_args(args, "additional_fields")

    # Make request and get raw response
    ticket = create_ticket(args)
    # Parse response into context
    include_in_context = DEFAULT_TICKET_CONTEXT_FIELDS[:]

    context = {string_to_context_key(key): val for key, val in ticket.items() if val}
    context = additional_fields_to_context(context, include_in_context, additional_fields, additional_values)
    context, context_readable = attachments_into_context(ticket, context)
    context = reformat_ticket_context(context)
    context_readable = reformat_ticket_context(context_readable)
    title = "Newly Created Ticket #{}".format(context.get("ID"))
    human_readable = tableToMarkdown(title, context_readable, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": ticket,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": context},
        }
    )


def update_ticket(args):
    # Get ticket number
    ticket_number = args.get("id")
    # Remove ticket number from args
    del args["id"]

    args = determine_agent(args, "assigned_agent")
    args = determine_group(args, "assigned_group")

    args = clean_arguments(args)

    # The service endpoint to request from
    endpoint_url = f"tickets/{ticket_number}"

    response = None
    if not args.get("attachments"):
        # Send a request using our http_request wrapper
        response = http_request("PUT", endpoint_url, data=json.dumps(args))
    else:
        # Get the files from their entry IDs
        attachments = entries_to_files(args.get("attachments"))
        # Format to API expectations
        del args["attachments"]
        # Send a request and get raw response
        response = http_request("PUT", endpoint_url, data=args, files=attachments, headers=MULTIPART_HEADERS)
    return response


def update_ticket_command():
    """
    Update the ticket specified by a ticket ID number.

    demisto parameter: (string) subject
        Update the ticket's subject field

    demisto parameter: (number,string) status
        Update the ticket's status. Possible values are 2,3,4,5,6,7 or
        'Open' , 'Pending', 'Resolved', 'Closed', 'Waiting on Customer',
        'Waiting on Third Party'

    demisto parameter: (number,string) priority
        Update the ticket's priority. Possible values are 1,2,3,4 or
        'Low', 'Medium', 'High', 'Urgent'

    demisto parameter: (string) description
        The HTML content of the ticket

    demisto parameter: (number) id
        ID number of the ticket to update

    demisto parameter: assigned_agent
        Update which agent is assigned to respond to this ticket.
        Values can be either the agent's ID number, name, or email.

    demisto parameter: assigned_group
        Update which group is assigned to respond to this ticket.
        Values can be either the group's ID number or name.

    demisto parameter: (list) additional_fields
        Fields not included in the default command arguments that
        you wish to enter the value for

    returns:
        Ticket specified by the ticket ID number with its updated values
    """
    args = demisto.args()
    args, additional_fields, additional_fields_values = additional_fields_to_args(args, "additional_fields")

    # Make request and get raw response
    ticket = update_ticket(args)

    # Parse response into context
    include_in_context = DEFAULT_TICKET_CONTEXT_FIELDS[:]
    include_in_context.append("updated_at")
    # Parse default context fields
    context = {string_to_context_key(key): val for key, val in ticket.items() if val}
    # Parse additional fields into context
    context = additional_fields_to_context(context, include_in_context, additional_fields, additional_fields_values)
    # Parse attachments into context
    context, context_readable = attachments_into_context(ticket, context)
    context = reformat_ticket_context(context)
    context_readable = reformat_ticket_context(context_readable)
    title = "Ticket #{} Updated".format(context.get("ID"))
    human_readable = tableToMarkdown(title, context_readable, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": ticket,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": context},
        }
    )


def get_ticket(args):
    ticket_number = args.get("id")
    endpoint_url = f"tickets/{ticket_number}"
    url_params = {}

    # Check if embedding additional info in API response was specified in cmd args
    include = ""
    if args.get("include_stats") and args.get("include_stats").lower() == "true":
        include += "stats"
    if args.get("include_requester") and args.get("include_requester").lower() == "true":
        include += "requester" if include == "" else ", requester"
    if include != "":
        url_params["include"] = include

    response = http_request("GET", endpoint_url, params=url_params)
    return response


def get_ticket_command():
    """
    View a Ticket.

    demisto parameter: (number) id
        ID number of the ticket to fetch

    demisto parameter: (string) include_requester
        If set to 'yes' then the ticket requester's id, email, mobile, name, and phone
        will be included in the ticket's output.

    demisto parameter: (string) include_stats
        If set to 'yes' then the ticket's closed_at, resolved_at and first_responded_at times will be included.

    returns:
        Ticket Object
    """
    # Get command arguments from user
    args = demisto.args()
    # Make request and get raw response
    ticket = get_ticket(args)

    nonstd_context_fields = ["requester", "stats"]

    # Parse response into context
    context = {
        string_to_context_key(key): val for key, val in ticket.items() if key not in nonstd_context_fields and val is not None
    }

    # Parse attachments into context
    context, context_readable = attachments_into_context(ticket, context)

    context["AdditionalFields"] = {}
    requester = ticket.get("requester")
    if requester:
        requester_context = {string_to_context_key(key): val for key, val in requester.items() if val}
        context["AdditionalFields"]["Requestor"] = requester_context
    stats = ticket.get("stats")
    if stats:
        stats_context = {string_to_context_key(key): val for key, val in stats.items() if val}
        context["AdditionalFields"]["Stats"] = stats_context

    if not ticket.get("deleted"):
        context["AdditionalFields"]["Deleted"] = False

    context = reformat_ticket_context(context)
    context_readable = reformat_ticket_context(context_readable)
    title = "Viewing Ticket #{}".format(ticket.get("id"))
    human_readable = tableToMarkdown(title, context_readable, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": ticket,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": context},
        }
    )


def delete_ticket(ticket_id):
    endpoint_url = f"tickets/{ticket_id}"
    response = http_request("DELETE", endpoint_url)
    return response


def delete_ticket_command():
    """
    Soft-Delete the ticket specified by the 'id' command argument.

    demisto parameter: (number) id
        ID of the ticket to delete

    returns:
        Success Message
    """
    ticket_id = demisto.args().get("id")
    # Make request
    delete_ticket(ticket_id)
    ticket_context = {"ID": int(ticket_id), "AdditionalFields": {"Deleted": True}}
    message = f"Soft-Deleted Ticket #{ticket_id}"
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": ticket_context,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": message,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": ticket_context},
        }
    )


def search_tickets(args):
    endpoint_url = "tickets"
    url_params = {}

    # Filter By
    filter = args.get("filter")
    if filter:
        url_params["filter"] = filter
    requester = args.get("requester")
    if requester:
        if "@" in requester:
            url_params["email"] = requester
        else:
            url_params["requester_id"] = requester
    updated_since = args.get("updated_since")
    if updated_since:
        url_params["updated_since"] = updated_since
    per_page = args.get("per_page")
    if per_page:
        url_params["per_page"] = per_page

    # Sort By
    order_by = args.get("order_by")
    if order_by:
        url_params["order_by"] = order_by
    order_type = args.get("order_type")
    if order_type:
        url_params["order_type"] = order_type

    # Embeddings (include additional information)
    include = ""
    if args.get("include_stats") and args.get("include_stats").lower() == "true":
        include += "stats"
    if args.get("include_requester") and args.get("include_requester").lower() == "true":
        include += "requester" if include == "" else ", requester"
    if args.get("include_description") and args.get("include_description").lower() == "true":
        include += "description" if include == "" else ", description"
    if include != "":
        url_params["include"] = include

    # Custom Query
    custom_query = args.get("custom_query")
    if custom_query and url_params:
        err_msg = (
            "You cannot use the custom_query argument in conjunction with the other command arguments. You can "
            "either use the other arguments that allow you to choose options for filtering, sorting, "
            "and including information for tickets, or to use the custom_query alone to create a custom filter "
            "that determines which tickets are listed."
        )
        return_error(err_msg)
    elif custom_query:
        endpoint_url = "search/tickets"
        url_params["query"] = '"' + custom_query + '"'

    page = args.get("page")
    if page:
        url_params["page"] = page

    # Make request and get raw response
    response = http_request("GET", endpoint_url, params=url_params)
    return response


def search_tickets_command():
    """
    List all tickets that match the filter criteria you specify.

    demisto parameter: (string) filter
        Predefined filters

    demisto parameter: requester
        Filter by either the ticket requester's email or ID

    demisto parameter: (datetime) updated_since
        By default, only tickets that have been created within the past 30 days will be returned.
        For older tickets, use this filter. Example value for this field would be '2015-01-19T02:00:00Z'

    demisto parameter: (string) order_by
        Reference field for ordering the list of tickets. The default sort order is created_at.

    demisto parameter: (string) order_type
        Whether to order the resulting tickets in ascending or descending order.
        The default is descending. Value can be either 'asc' or 'desc'.

    demisto parameter: (string) include_stats
        If set to 'yes' then the ticket's closed_at, resolved_at and first_responded_at times will be included.

    demisto parameter: (string) include_requester
        If set to 'yes' then the ticket requester's id, email, mobile, name, and phone
        will be included in the ticket's output for each ticket.

    demisto parameter: (string) include_description
        If set to 'yes' then the ticket's description and description_text will be included the tickets' outputs.

    demisto parameter: (string) custom_query
        Filter tickets using a custom query.
        Format  -  "(ticket_field:integer OR ticket_field:'string') AND ticket_field:boolean"
        Example -  "(type:'Question' OR type:'Problem') AND (due_by:>'2017-10-01' AND due_by:<'2017-10-07')"
        Note that the custom_query argument cannot be used in conjunction with this command's other arguments.

    returns:
        Ticket Objects
    """
    args = demisto.args()
    additional_fields = get_additional_fields(args)
    response = search_tickets(args)  # page 1

    tickets = handle_search_tickets_pagination(args, response)

    context_outputs = DEFAULT_TICKET_CONTEXT_FIELDS[:]
    context_outputs.append("updated_at")

    # Parse response into context
    contexts = []
    readable_contexts = []
    for ticket in tickets:
        # Parse ticket into the standard outputs
        context = {string_to_context_key(key): val for key, val in ticket.items() if key in context_outputs}

        # Parse ticket attachments into context
        context, context_readable = attachments_into_context(ticket, context)

        # Parse ticket for the additionally requested fields
        context["AdditionalFields"] = {string_to_context_key(key): val for key, val in ticket.items() if key in additional_fields}
        requester = ticket.get("requester")
        if requester:
            requester_context = {string_to_context_key(key): val for key, val in requester.items() if val}
            context["AdditionalFields"]["Requestor"] = requester_context
        stats = ticket.get("stats")
        if stats:
            stats_context = {string_to_context_key(key): val for key, val in stats.items() if val}
            context["AdditionalFields"]["Stats"] = stats_context

        context_readable = reformat_ticket_context(context_readable)
        readable_contexts.append(context_readable)
        context = reformat_ticket_context(context)
        contexts.append(context)

    table_headers = [
        "ID",
        "Priority",
        "Status",
        "Subject",
        "DueBy",
        "FrDueBy",
        "RequesterID",
        "GroupID",
        "Source",
        "CreatedAt",
        "UpdatedAt",
        "Tag",
        "AdditionalFields",
        "Attachment",
    ]
    title = "Viewing All Requested Tickets"
    human_readable = tableToMarkdown(title, readable_contexts, headers=table_headers, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": tickets,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": contexts},
        }
    )


"""<------ CONVERSATIONS ------>"""


def ticket_reply(args):
    ticket_id = args.get("ticket_id")
    del args["ticket_id"]
    args = handle_array_input(args)
    endpoint_url = f"tickets/{ticket_id}/reply"

    response = None
    if not args.get("attachments"):
        # The service endpoint to request from
        # Send a request using our http_request wrapper
        response = http_request("POST", endpoint_url, data=json.dumps(args))
    else:
        # Get the files from their entry IDs
        attachments = entries_to_files(args.get("attachments"))
        # Format to API expectations
        del args["attachments"]
        # Send a request and get raw response
        response = http_request("POST", endpoint_url, data=args, files=attachments, headers=MULTIPART_HEADERS)
    return response


def ticket_reply_command():
    """
    Reply to a specified ticket.

    demisto parameter: (number) ticket_id
        ID of the ticket you wish to respond to

    demisto parameter: (string) body
        Content of the reply in HTML format

    demisto parameter: (string) from_email
        The email address from which the reply is sent. By default the global support email will be used.

    demisto parameter: (number) user_id
        ID of the agent who is adding the note

    demisto parameter: (list) cc_emails
        Array of email address strings added in the 'cc' field of the outgoing ticket email.

    demisto parameter: (list) bcc_emails
        Array of email address strings added in the 'bcc' field of the outgoing ticket email.

    demisto parameter: (list) attachments
        Entry IDs of files to attach to the reply. The total size of these attachments cannot exceed 15MB.

    returns:
        Ticket Reply Object
    """
    args = demisto.args()
    # Make request and get raw response
    reply = ticket_reply(args)
    # Parse response into context
    context = {string_to_context_key(key): val for key, val in reply.items() if val}
    context = reformat_conversation_context(context)
    # Parse attachments into context
    context, context_readable = attachments_into_context(reply, context)
    context = reformat_ticket_context(context)
    context_readable = reformat_ticket_context(context_readable)
    complete_context = {"ID": int(reply.get("ticket_id")), "Conversation": context}
    title = "Reply to Ticket #{}".format(reply.get("ticket_id"))
    human_readable = tableToMarkdown(title, context_readable, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": reply,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": complete_context},
        }
    )


def create_ticket_note(args):
    ticket_id = args.get("ticket_id")
    del args["ticket_id"]
    args = handle_array_input(args)
    # Set defaults for 'private' and 'incoming' fields if not set by user
    args["private"] = args.get("private", "true")
    args["incoming"] = args.get("incoming", "false")
    endpoint_url = f"tickets/{ticket_id}/notes"

    response = None
    if not args.get("attachments"):
        # Format boolean args to API expectations
        dumped_args = json.dumps(args).replace('"false"', "false").replace('"true"', "true")
        # The service endpoint to request from
        # Send a request using our http_request wrapper
        response = http_request("POST", endpoint_url, data=dumped_args)
    else:
        # Get the files from their entry IDs
        attachments = entries_to_files(args.get("attachments"))
        # Format to API expectations
        del args["attachments"]
        # Send a request and get raw response
        response = http_request("POST", endpoint_url, data=args, files=attachments, headers=MULTIPART_HEADERS)
    return response


def create_ticket_note_command():
    """
    Create a note for a specified ticket.

    Notes by default are private (AKA not visible to non-agents) unless you
    set the 'private' command argument to False.

    demisto parameter: (number) ticket_id
        ID of the ticket you wish to make a note for

    demisto parameter: (string) body
        Content of the note in HTML format

    demisto parameter: (boolean) private
        Set to false if the note is not private

    demisto parameter: (number) user_id
        ID of the agent who is adding the note

    demisto parameter: (list) notify_emails
        Array of email addresses of agents/users who need to be notified about this note

    demisto parameter: (boolean) incoming
        Set to true if a particular note should appear as being created from outside (i.e., not through web portal).

    demisto parameter: (list) attachments
        Entry IDs of files to attach to the note. The total size of these attachments cannot exceed 15MB.

    returns:
        Note Object
    """
    # Get command arguments
    args = demisto.args()
    # Make request and get raw response
    note = create_ticket_note(args)
    # Parse response into context
    context = {string_to_context_key(key): val for key, val in note.items() if val}
    context = reformat_conversation_context(context)
    # Parse attachments into context
    context, context_readable = attachments_into_context(note, context)
    context = reformat_ticket_context(context)
    context_readable = reformat_ticket_context(context_readable)
    complete_context = {"ID": int(note.get("ticket_id")), "Conversation": context}
    title = "Note for Ticket #{}".format(note.get("ticket_id"))
    human_readable = tableToMarkdown(title, context_readable, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": note,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": complete_context},
        }
    )


def get_ticket_conversations(ticket_id):
    endpoint_url = f"tickets/{ticket_id}/conversations"
    response = http_request("GET", endpoint_url)
    return response


def get_ticket_conversations_command():
    """
    Lists all replies and notes for a specified ticket.

    demisto parameter: (number) ticket_id
        ID of the ticket for which you would like to list all of its conversations

    returns:
        Conversation Objects
    """
    # Get id number of ticket as cmd arg for which you want to see all the conversations
    ticket_id = demisto.args().get("ticket_id")
    # Make request and get raw response
    conversations = get_ticket_conversations(ticket_id)
    # Parse response into context
    contexts = []
    readable_contexts = []
    for conversation in conversations:
        context = {string_to_context_key(key): val for key, val in conversation.items() if val}
        context = reformat_conversation_context(context)
        # Parse attachments into context
        context, context_readable = attachments_into_context(conversation, context)
        context = reformat_ticket_context(context)
        context_readable = reformat_ticket_context(context_readable)
        contexts.append(context)
        readable_contexts.append(context_readable)
    complete_context = {"ID": int(ticket_id), "Conversation": contexts}
    title = f"Conversations of Ticket #{ticket_id}"
    human_readable = tableToMarkdown(title, readable_contexts, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": conversations,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Ticket(val.ID && val.ID === obj.ID)": complete_context},
        }
    )


"""<------ CONTACTS ------>"""


def list_contacts(filters):
    # Alter to match API expected inputs
    updated_since = filters.get("updated_since", None)
    if updated_since:
        del filters["updated_since"]
        filters["_updated_since"] = updated_since

    endpoint_url = "contacts"
    response = http_request("GET", endpoint_url, params=filters)
    return response


def list_contacts_command():
    """
    List all contacts.

    Lists all contacts matching the specified filters. If no filters are provided
    then all unblocked and undeleted contacts will be returned.

    demisto parameter: (number) mobile
        mobile number to filter the contacts by

    demisto parameter: (number) phone
        phone number to filter contacts by

    demisto parameter: (string) state
        The state of contacts by which you want to filter the contacts. Values
        are 'verified', 'unverified', 'blocked', or 'deleted'.

    demisto parameter: (datetime) updated_since
        return contacts that have been updated after the timestamp given as this argument value

    returns:
        Contact Objects
    """
    # Get command arguments from user
    filters = demisto.args()
    # Make request and get raw response
    contacts = list_contacts(filters)
    # Parse response into context
    contexts = []
    for contact in contacts:
        # Parse individual contact response in context
        context = format_contact_context(contact)
        contexts.append(context)
    filters_as_strings = ", ".join([f"{key}: {val}" for key, val in filters.items()])
    title = f"Contacts Filtered by {filters_as_strings}" if filters else "All Contacts"
    human_readable = tableToMarkdown(title, contexts, removeNull=False)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": contacts,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Contact(val.ID && val.ID === obj.ID)": contexts},
        }
    )


def get_contact(args):
    contact_id = 0
    if not args:
        err_msg = "You must provide a value for either the mobile, email or id command argument fields."
        return_error(err_msg)
    elif args.get("id") is not None:
        contact_id = args.get("id")
    elif args.get("email") is not None:
        try:
            filters = {"email": args.get("email")}
            # Get id field of first result of contacts with that email (There should only be one)
            contact_id = list_contacts(filters)[0].get("id")
        # If there is an IndexError, it means no results were returned for the given filter
        except IndexError:
            err_msg = (
                "Couldn't find a contact with that email address."
                " Double check that you wrote the email address correctly"
                " and/or that you have a FreshDesk contact with that exact"
                " email address."
            )
            return_error(err_msg)
        except Exception as e:
            return_error(e)
    else:
        try:
            filters = {"mobile": args.get("mobile")}
            # Get id field of first result of contacts with that mobile number
            contact_id = list_contacts(filters)[0].get("id")
        # If there is an IndexError, it means no results were returned for the given filter
        except IndexError:
            err_msg = (
                "Couldn't find a contact with that mobile number."
                " Double check that you wrote it correctly and/or that "
                "you have a FreshDesk contact with that exact mobile number."
            )
            return_error(err_msg)
        except Exception as e:
            return_error(e)

    endpoint_url = f"contacts/{contact_id}"
    response = http_request("GET", endpoint_url)
    return response


def get_contact_command():
    """
    View the details of the contact specified by the ID number.

    demisto parameter: (number) id
        ID of the contact you wish to view the details of

    demisto parameter: (number) mobile
        Mobile number of the contact you wish to view the details of

    demisto parameter: (string) email
        Email address of the contact you wish to view the details of

    returns:
        Contact Object
    """
    # Get command arguments from user
    args = demisto.args()
    # Make request and get raw response
    contact = get_contact(args)

    context = format_contact_context(contact)
    title = "Viewing Contact #{}".format(contact.get("id"))
    human_readable = tableToMarkdown(title, context, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": contact,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Contact(val.ID && val.ID === obj.ID)": context},
        }
    )


"""<------ CANNED RESPONSES ------>"""


def list_canned_response_folders():
    endpoint_url = "canned_response_folders"
    response = http_request("GET", endpoint_url)
    return response


def list_canned_response_folders_command():
    """
    List all Canned Response Folders (Only users with Admin Privileges).

    returns:
        Folder Objects
    """
    # Make request and get raw response
    cr_folders = list_canned_response_folders()
    # Parse response into context
    contexts = []
    for folder in cr_folders:
        # Parse individual contact response in context
        context = {string_to_context_key(key): val for key, val in folder.items() if val}
        context = reformat_canned_response_context(context)
        contexts.append(context)
    title = "All Canned Response Folders"
    human_readable = tableToMarkdown(title, contexts, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": cr_folders,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.CRFolder(val.ID && val.ID === obj.ID)": contexts},
        }
    )


def get_canned_response_folder(id):
    endpoint_url = f"canned_response_folders/{id}/responses"
    response = http_request("GET", endpoint_url)
    return response


def get_canned_response_folder_command():
    """
    View the details of all the Canned Responses in a Folder.

    demisto parameter: (number) id
        ID of the Folder containing the Canned Responses you wish to view the details of

    returns:
        Canned Response Objects with more details, aka all of a Canned Response Object's attributes
    """
    # Get id of the containing canned response folder as cmd argument
    cr_folder_id = demisto.args().get("id")
    # Make request and get raw response
    canned_responses = get_canned_response_folder(cr_folder_id)
    # Parse the responses into context
    contexts = []
    readable_contexts = []
    for cr in canned_responses:
        context = {string_to_context_key(key): val for key, val in cr.items() if val}
        context = reformat_canned_response_context(context)
        context, context_readable = attachments_into_context(cr, context)
        contexts.append(context)
        readable_contexts.append(context_readable)
    title = f"Details of Canned Responses in CR Folder #{cr_folder_id}"
    human_readable = tableToMarkdown(title, readable_contexts, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": canned_responses,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.CRFolder(val.ID && val.ID === obj.ID).CR(val.ID && val.ID === obj.ID)": contexts},
        }
    )


"""<------ GROUPS ------>"""


def list_groups():
    endpoint_url = "groups"
    response = http_request("GET", endpoint_url)
    return response


def list_groups_command():
    """
    List all groups.

    returns:
        Group Objects
    """
    # Make request and get raw response
    groups = list_groups()
    # Parse response into context
    contexts = []
    for group in groups:
        # Parse individual group response in context
        context = {}
        for key, val in list(group.items()):
            if val:
                if key == "agent_ids":
                    key = "agent_id"
                new_key = string_to_context_key(key)
                if "Id" in new_key:
                    new_key = new_key.replace("Id", "ID")
                context[new_key] = val
        contexts.append(context)
    title = "All Groups"
    human_readable = tableToMarkdown(title, contexts, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": groups,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Group(val.ID && val.ID === obj.ID)": contexts},
        }
    )


"""<------ AGENTS ------>"""


def list_agents(filters={}):
    endpoint_url = "agents"
    response = http_request("GET", endpoint_url, params=filters)
    return response


def list_agents_command():
    """
    List agents that match the criteria of the filters entered as command arguments.

    demisto parameter: (number) mobile
        Mobile phone number to filter agents by

    demisto parameter: (number) phone
        Telephone number to filter agents by

    demisto parameter: (string) state
        Filter agents by whether they are 'fulltime' or 'occasional'

    returns:
        Agent Objects
    """
    # Get filter as cmd arg
    args = demisto.args()
    # Make request and get raw response
    agents = list_agents(args)
    # Parse response into context
    contexts = []
    for agent in agents:
        # Parse the individual agent into context
        context = {}
        for key, val in list(agent.items()):
            if val:
                if key == "group_ids":
                    key = "group_id"
                elif key == "role_ids":
                    key = "role_id"
                new_key = string_to_context_key(key)
                if "Id" in new_key:
                    new_key = new_key.replace("Id", "ID")
                context[new_key] = val
        context["Contact"] = {string_to_context_key(key): val for key, val in agent.get("contact").items() if val}
        contexts.append(context)
    title = "All Agents"
    human_readable = tableToMarkdown(title, contexts, removeNull=True)
    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": agents,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": human_readable,
            "EntryContext": {"Freshdesk.Agent(val.ID && val.ID === obj.ID)": contexts},
        }
    )


""" COMMANDS MANAGER / SWITCH PANEL """


# Commands Switch Panel
commands = {
    "fd-create-ticket": create_ticket_command,
    "fd-update-ticket": update_ticket_command,
    "fd-get-ticket": get_ticket_command,
    "fd-get-contact": get_contact_command,
    "fd-list-contacts": list_contacts_command,
    "fd-list-canned-response-folders": list_canned_response_folders_command,
    "fd-get-canned-response-folder": get_canned_response_folder_command,
    "fd-list-groups": list_groups_command,
    "fd-ticket-reply": ticket_reply_command,
    "fd-create-ticket-note": create_ticket_note_command,
    "fd-get-ticket-conversations": get_ticket_conversations_command,
    "fd-list-agents": list_agents_command,
    "fd-delete-ticket": delete_ticket_command,
    "fd-search-tickets": search_tickets_command,
}

LOG(f"Command being called is {demisto.command()}")

try:
    if demisto.command() == "test-module":
        # This is the call made when pressing the integration test button.
        test_module()
        demisto.results("ok")
    elif demisto.command() == "fetch-incidents":
        fetch_incidents()
    elif demisto.command() in commands:
        # Execute that command
        commands[demisto.command()]()

# Log exceptions
except Exception as e:
    return_error(f"Failed to execute {demisto.command()} command. Error: {str(e)}", e)