Zendesk v2

IT service management.

Case Management · Zendesk

Details

IDZendesk v2
ProviderZendesk
CategoryCase Management
From Version6.6.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix Cloud Runtime Security XSIAM EDR Cortex Cloud

README

Some changes have been made that might affect your existing content.
If you are upgrading from a previous of this integration, see Breaking Changes.

Configure Zendesk v2 in Cortex

Parameter Description Required
Server URL (e.g., https://demisto.zendesk.com)   True
Username (or ‘<username>/token’ when using API key). example ‘admin@org.com’ when using the password, or ‘admin@org.com/token’ when using an API key. True
Password/API key   True
Fetch incidents   False
Incident Mirroring Direction Selects which direction you want the incidents mirrored. You can mirror **Incoming** only (from Zendesk to Cortex XSOAR), **Outgoing** only (from Cortex XSOAR to Zendesk), or both **Incoming And Outgoing**. False
Close mirrored incidents If true, XSOAR will mirror also the ticket closeing. False
Mirror tags Comment and files that will be marked with this tag will be pushed into Zendesk. False
Ticket Field to Fetch by Duplications might accrue when choosing ‘updated-at’ True
Ticket types to fetch   False
Fetch tickets status filter   False
Fetch tickets priority filter   False
Fetch tickets query filter   False
Maximum number of incidents per fetch   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)   False
Incidents Fetch Interval   False
Incident type   False
Use system proxy settings   False
Trust any certificate (not secure)   False
(The test does not ensure sufficient permissions for all integration commands.)

Commands

You can execute these commands from the 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.

Required permissions

This integration enables executing commands with different permission levels. See the commands’ descriptions for more information on the required permission.
To learn more on Zendesk roles refer to:
Understanding Zendesk Support user roles
About team member product roles and access

zendesk-user-list


Gets the specified user’s data.
Required permissions: Admins, Agents and Light Agents.

Base Command

zendesk-user-list

Input

Argument Name Description Required
user_id The user’s ID to retrieve. Optional
user_name The user’s name.
Required permissions to use this argument: Agents .
Optional
external_id The user’s unique identifier from another system. Optional
role The user’s role. Possible values are: end_user, agent, admin. Optional
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

Path Type Description
Zendesk.User.active boolean False if the user has been deleted.
Zendesk.User.alias string An alias displayed to end users.
Zendesk.User.chat_only boolean Whether the user is a chat-only agent.
Zendesk.User.created_at date The time the user was created.
Zendesk.User.custom_role_id number A custom role if the user is an agent on the Enterprise plan or above.
Zendesk.User.default_group_id number The ID of the user’s default group.
Zendesk.User.details string Any details you want to store about the user, such as an address.
Zendesk.User.external_id string A unique identifier from another system.
Zendesk.User.email string The user’s primary email address.
Zendesk.User.iana_time_zone string The time zone for the user.
Zendesk.User.id number The user’s ID.
Zendesk.User.last_login_at string The last time the user signed in to Zendesk Support.
Zendesk.User.locale string The user’s locale.
Zendesk.User.moderator boolean Whether the user has forum moderation capabilities.
Zendesk.User.name string The user’s name.
Zendesk.User.notes string Any notes you want to store about the user.
Zendesk.User.only_private_comments boolean True if the user can only create private comments.
Zendesk.User.organization_id number The ID of the user’s organization.
Zendesk.User.organization string The name of the user’s organization.
Zendesk.User.phone string The user’s primary phone number.
Zendesk.User.remote_photo_url string A URL pointing to the user’s profile picture.
Zendesk.User.report_csv boolean Whether the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.
Zendesk.User.restricted_agent boolean Whether the agent has any restrictions; false for admins and unrestricted agents, true for other agents.
Zendesk.User.role string The user’s role. Possible values are “end-user”, “agent”, or “admin”.
Zendesk.User.role_type string The user’s role type.
Zendesk.User.shared boolean Whether the user is shared from a different Zendesk Support instance.
Zendesk.User.shared_agent boolean Whether the user is a shared agent from a different Zendesk Support instance.
Zendesk.User.shared_phone_number boolean Whether the phone number is shared.
Zendesk.User.signature string The user’s signature.
Zendesk.User.suspended boolean Whether the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal.
Zendesk.User.tags string The user’s tags. Only present if your account has user tagging enabled.
Zendesk.User.ticket_restriction string Specifies which tickets the user has access to. Possible values are: “organization”, “groups”, “assigned”, “requested”, null.
Zendesk.User.time_zone string The user’s time zone.
Zendesk.User.two_factor_auth_enabled boolean Whether two factor authentication is enabled.
Zendesk.User.updated_at string The time the user was last updated.
Zendesk.User.verified boolean Whether any of the user’s identities is verified.

zendesk-user-create


Creates a new Zendesk user.
Required permissions: Agents, with restrictions on certain actions.

Base Command

zendesk-user-create

Input

Argument Name Description Required
name The user’s name. Required
email The user’s primary email address. Required
role The user’s role. Possible values are “end-user”, “agent”, or “admin”. Possible values are: end_user, agent, admin. Required
role_type The user’s role type. Possible values are: custom_agent, light_agent, chat_agent, chat_agent_contributor, admin, billing_admin. Optional
verified Whether to mark the user’s email as verified. Possible values are: true, false. Default is false. Optional
tags A comma-separated list of users’ tags. Optional
phone The user’s primary phone number. Optional
organization_name The name of the user’s organization. Optional
organization_id The ID of the user’s organization. Optional
notes Any notes you want to store about the user. Optional
details Any details you want to store about the user, such as an address. Optional
external_id A unique identifier from another system. Optional
locale The user’s locale. Optional
alias An alias displayed to end users. Optional
default_group_id The ID of the user’s default group. Optional
custom_role_id A custom role if the user is an agent on the Enterprise plan or above. Optional
identities Users external identities (JSON-formatted argument) e.g., [{“type”: “email”, “value”: “test@user.com”}, {“type”: “twitter”, “value”: “tester84”}]. Optional
user_fields Values of custom fields in the user’s profile (json formated field). Optional
check_if_user_exists Check if the user already exists (Will fail if the user exists already or will update a user’s existing data). Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
Zendesk.User.active boolean false if the user has been deleted.
Zendesk.User.alias string An alias displayed to end users.
Zendesk.User.chat_only boolean Whether or not the user is a chat-only agent.
Zendesk.User.created_at date The time the user was created.
Zendesk.User.custom_role_id number A custom role if the user is an agent on the Enterprise plan or above.
Zendesk.User.default_group_id number The ID of the user’s default group.
Zendesk.User.details string Any details you want to store about the user, such as an address.
Zendesk.User.external_id string A unique identifier from another system.
Zendesk.User.email string The user’s primary email address.
Zendesk.User.iana_time_zone string The time zone for the user.
Zendesk.User.id number The user’s ID.
Zendesk.User.last_login_at string The last time the user signed in to Zendesk Support.
Zendesk.User.locale string The user’s locale.
Zendesk.User.moderator boolean Whether the user has forum moderation capabilities.
Zendesk.User.name string The user’s name.
Zendesk.User.notes string Any notes you want to store about the user.
Zendesk.User.only_private_comments boolean True if the user can only create private comments.
Zendesk.User.organization_id number The ID of the user’s organization.
Zendesk.User.organization string The name of the user’s organization.
Zendesk.User.phone string The user’s primary phone number.
Zendesk.User.remote_photo_url string A URL pointing to the user’s profile picture.
Zendesk.User.report_csv boolean Whether the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.
Zendesk.User.restricted_agent boolean Whether the agent has any restrictions; false for admins and unrestricted agents, true for other agents.
Zendesk.User.role string The user’s role. Possible values are “end-user”, “agent”, or “admin”.
Zendesk.User.role_type string The user’s role type.
Zendesk.User.shared boolean Whether the user is shared from a different Zendesk Support instance.
Zendesk.User.shared_agent boolean Whether the user is a shared agent from a different Zendesk Support instance.
Zendesk.User.shared_phone_number boolean Whether the phone number is shared.
Zendesk.User.signature string The user’s signature.
Zendesk.User.suspended boolean Whether the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal.
Zendesk.User.tags string The user’s tags. Only present if your account has user tagging enabled.
Zendesk.User.ticket_restriction string Specifies which tickets the user has access to. Possible values are: “organization”, “groups”, “assigned”, “requested”, null.
Zendesk.User.time_zone string The user’s time zone.
Zendesk.User.two_factor_auth_enabled boolean Whether two factor authentication is enabled.
Zendesk.User.updated_at string The time the user was last updated.
Zendesk.User.verified boolean Whether any of the user’s identities is verified.

zendesk-user-update


Update user data.
Required permissions: Agents, with restrictions on certain actions.

Base Command

zendesk-user-update

Input

Argument Name Description Required
user_id The user ID to update. Required
name The user’s name. Optional
role The user’s role. Possible values are “end-user”, “agent”, or “admin”. Possible values are: end_user, agent, admin. Optional
role_type The user’s role type. Possible values are: custom_agent, light_agent, chat_agent, chat_agent_contributor, admin, billing_admin. Optional
verified Whether to mark the user email as verified (the added email). Possible values are: true, false. Optional
suspended Whether the user is suspended. Possible values are: true, false. Optional
tags The user’s tags. Optional
email Secondary email to add. Optional
phone The user’s primary phone number. Optional
organization_name The name of the user’s organization. Optional
organization_id The ID of the user’s organization. Optional
notes Any notes you want to store about the user. Optional
details Any details you want to store about the user, such as an address. Optional
external_id A unique identifier from another system. Optional
locale The user’s locale. Optional
alias An alias displayed to end users. Optional
default_group_id The ID of the user’s default group. Optional
custom_role_id A custom role if the user is an agent on the Enterprise plan or above. Optional
identities Users external identities (JSON-formatted argument) e.g., [{“type”: “email”, “value”: “test@user.com”}, {“type”: “twitter”, “value”: “tester84”}]. Optional
user_fields Values of custom fields in the user’s profile (JSON-formatted field). Optional

Context Output

Path Type Description
Zendesk.User.active boolean False if the user has been deleted.
Zendesk.User.alias string An alias displayed to end users.
Zendesk.User.chat_only boolean Whether the user is a chat-only agent.
Zendesk.User.created_at date The time the user was created.
Zendesk.User.custom_role_id number A custom role if the user is an agent on the Enterprise plan or above.
Zendesk.User.default_group_id number The ID of the user’s default group.
Zendesk.User.details string Any details you want to store about the user, such as an address.
Zendesk.User.external_id string A unique identifier from another system.
Zendesk.User.email string The user’s primary email address.
Zendesk.User.iana_time_zone string The time zone for the user.
Zendesk.User.id number The user’s ID.
Zendesk.User.last_login_at string The last time the user signed in to Zendesk Support.
Zendesk.User.locale string The user’s locale.
Zendesk.User.moderator boolean Whether the user has forum moderation capabilities.
Zendesk.User.name string The user’s name.
Zendesk.User.notes string Any notes you want to store about the user.
Zendesk.User.only_private_comments boolean True if the user can only create private comments.
Zendesk.User.organization_id number The ID of the user’s organization.
Zendesk.User.organization string The name of the user’s organization.
Zendesk.User.phone string The user’s primary phone number.
Zendesk.User.remote_photo_url string A URL pointing to the user’s profile picture.
Zendesk.User.report_csv boolean Whether the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.
Zendesk.User.restricted_agent boolean If the agent has any restrictions; false for admins and unrestricted agents, true for other agents.
Zendesk.User.role string The user’s role. Possible values are “end-user”, “agent”, or “admin”.
Zendesk.User.role_type string The user’s role type.
Zendesk.User.shared boolean Whether the user is shared from a different Zendesk Support instance.
Zendesk.User.shared_agent boolean Whether the user is a shared agent from a different Zendesk Support instance.
Zendesk.User.shared_phone_number boolean Whether the phone number is shared.
Zendesk.User.signature string The user’s signature.
Zendesk.User.suspended boolean Whether the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal.
Zendesk.User.tags string The user’s tags. Only present if your account has user tagging enabled.
Zendesk.User.ticket_restriction string Specifies which tickets the user has access to. Possible values are: “organization”, “groups”, “assigned”, “requested”, null
Zendesk.User.time_zone string The user’s time zone.
Zendesk.User.two_factor_auth_enabled boolean Whether two factor authentication is enabled.
Zendesk.User.updated_at string The time the user was last updated.
Zendesk.User.verified boolean Whether any of the user’s identities is verified.

zendesk-user-delete


Delete a user.
Required permissions: Admins.

Base Command

zendesk-user-delete

Input

Argument Name Description Required
user_id The ID of the user to delete. Required

Context Output

There is no context output for this command.

zendesk-organization-list


Get organization’s data.
Required permissions: Agents, with certain restrictions.
If the agent has a custom agent role that restricts the agent’s access to only users in their own organization,
a 403 Forbidden error is returned. See Creating custom agent roles in Zendesk help.

Base Command

zendesk-organization-list

Input

Argument Name Description Required
organization_id The ID of a specific organization.
Required permissions: Admins, Agents.
Optional
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

Path Type Description
Zendesk.Organization.created_at date The time the organization was created.
Zendesk.Organization.details string Any details about the organization, such as the address.
Zendesk.Organization.domain_names string Domain names associated with this organization.
Zendesk.Organization.external_id string A unique external ID to associate organizations to an external record.
Zendesk.Organization.group_id number New tickets from users in this organization are automatically put in this group.
Zendesk.Organization.id number The organization ID.
Zendesk.Organization.name string The name of the organization.
Zendesk.Organization.notes string Any notes you have about the organization.
Zendesk.Organization.shared_comments boolean End users in this organization are able to see each other’s comments on tickets.
Zendesk.Organization.shared_tickets boolean End users in this organization are able to see each other’s tickets.
Zendesk.Organization.tags string The tags of the organization.
Zendesk.Organization.updated_at date The time of the last update of the organization.
Zendesk.Organization.url string The API URL of this organization.

zendesk-ticket-list


List Zendesk tickets.
Required permissions: Agents.

Base Command

zendesk-ticket-list

Input

Argument Name Description Required
query Query to search for tickets. For more information about the search syntax, see https://developer.zendesk.com/rest_api/docs/core/search. . Optional
ticket_id The ID of a specific ticket. Optional
filter Filter tickets for which the specified user is relevant. Possible values are: assigned, requested, ccd, followed, recent. Optional
user_id The agent ID associated with tickets filtered. Optional
sort The order of the retrieved tickets. Possible values are: id_asc, status_asc, updated_at_asc, id_desc, status_desc, updated_at_desc. Optional
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

Path Type Description
Zendesk.Ticket.allow_attachments boolean Whether the agents have permission to attachments to a comment.
Zendesk.Ticket.allow_channelback boolean False if channelback is disabled, true otherwise. Only applicable for channels framework ticket.
Zendesk.Ticket.assignee string The agent currently assigned to the ticket.
Zendesk.Ticket.assignee_id number The ID of the agent currently assigned to the ticket.
Zendesk.Ticket.collaborators string The users currently CC’ed on the ticket.
Zendesk.Ticket.collaborator_ids number The ID of the users currently CC’ed on the ticket.
Zendesk.Ticket.created_at date The ticket creation time.
Zendesk.Ticket.description string The ticket description.
Zendesk.Ticket.due_at date If this is a ticket of type “task” it has a due date. Due date format uses ISO 8601 format.
Zendesk.Ticket.email_ccs string The agents or end users currently CC’ed on the ticket.
Zendesk.Ticket.email_cc_ids number The ID of the agents or end users currently CC’ed on the ticket.
Zendesk.Ticket.external_id string An ID you can use to link Zendesk Support tickets to local records.
Zendesk.Ticket.followers string The agents currently following the ticket.
Zendesk.Ticket.follower_ids number The ID of the user who is currently following the ticket.
Zendesk.Ticket.followup_ids number The IDs of the followups created from this ticket. IDs are only visible once the ticket is closed.
Zendesk.Ticket.forum_topic_id number The topic in the Zendesk Web portal this ticket originated from, if any.
Zendesk.Ticket.group_id number The ID of the group this ticket is assigned to.
Zendesk.Ticket.has_incidents boolean True if a ticket is a problem type and has one or more incidents linked to it. Otherwise, the value is false.
Zendesk.Ticket.id number The ticket ID.
Zendesk.Ticket.is_public boolean True if any comments are public, false otherwise.
Zendesk.Ticket.organization string The organization of the requester.
Zendesk.Ticket.organization_id number The ID of the organization of the requester.
Zendesk.Ticket.priority string The urgency with which the ticket should be addressed.
Zendesk.Ticket.problem_id number For tickets of type “incident”, the ID of the problem the incident is linked to.
Zendesk.Ticket.raw_subject string The dynamic content placeholder, if present, or the “subject” value, if not.
Zendesk.Ticket.recipient string The original recipient email address of the ticket.
Zendesk.Ticket.requester string The user who requested this ticket.
Zendesk.Ticket.requester_id number The ID of the user who requested this ticket.
Zendesk.Ticket.satisfaction_rating unknown The satisfaction rating of the ticket, if it exists, or the state of satisfaction, “offered” or “unoffered”.
Zendesk.Ticket.sharing_agreement_ids number The IDs of the sharing agreements used for this ticket.
Zendesk.Ticket.status string The state of the ticket.
Zendesk.Ticket.subject string The value of the subject field for this ticket.
Zendesk.Ticket.submitter string The user who submitted the ticket.
Zendesk.Ticket.submitter_id number The ID of the user who submitted the ticket.
Zendesk.Ticket.tags string The array of tags applied to this ticket.
Zendesk.Ticket.type string The type of this ticket.
Zendesk.Ticket.updated_at date When this ticket was last updated.
Zendesk.Ticket.url string The API URL of this ticket.

zendesk-ticket-create


Create a new zendesk ticket.
Required permissions: Agents.

Base Command

zendesk-ticket-create

Input

Argument Name Description Required
subject The subject of this ticket. Required
type The type of this ticket. Possible values are: problem, incident, question, task. Required
requester The user who requested this ticket. Required
assignee_email The email address of the agent to assign the ticket to. Optional
collaborators Users to add as CC’s when creating a ticket. Optional
description The ticket description. Required
recipient The original recipient email address of the ticket. Optional
status The state of the ticket. Possible values are: new, open, pending, hold, solved, closed. Optional
priority The urgency with which the ticket should be addressed. Possible values are: urgent, high, normal, low. Default is normal. Optional
due_at If this is a ticket of type “task” it has a due date. Due date format uses ISO 8601 format. Optional
email_ccs An array of agents or end users email CCs to add or delete from the ticket. Default is add.\nThe format is ‘562624,562625:put,example@example.com:delete’. Optional
external_id An ID you can use to link Zendesk Support tickets to local records. Optional
forum_topic_id The topic in the Zendesk Web portal this ticket originated from, if any. Optional
followers An array of agent followers to add or delete from the ticket. Default is add.\nThe format is ‘562624,562625:put,example@example.com:delete’. Optional
group_id The group this ticket is assigned to. Optional
organization_id The organization of the requester. Optional
problem_id For tickets of type “incident”, the ID of the problem the incident is linked to. Optional
tags The tags applied to this ticket. Optional
via_followup_source_id The ID of a closed ticket when creating a follow-up ticket. Optional
custom_fields Custom fields for the ticket (this is a JSON-formatted argument, see: https://developer.zendesk.com/documentation/ticketing/managing-tickets/creating-and-updating-tickets#setting-custom-field-values). Optional
brand_id Enterprise only. The ID of the brand this ticket is associated with. Optional
comment A comment to add to the ticket. Optional
html_comment An HTML comment to add to the ticket. Optional
public true if a public comment; false if an internal note. The initial value set on ticket creation persists for any additional comment unless you change it. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
Zendesk.Ticket.allow_attachments boolean Whether the agents have permission to add attachments to a comment.
Zendesk.Ticket.allow_channelback boolean False if channelback is disabled, true otherwise. Only applicable for channels framework ticket.
Zendesk.Ticket.assignee string The agent currently assigned to the ticket.
Zendesk.Ticket.assignee_id number The ID of the agent currently assigned to the ticket.
Zendesk.Ticket.collaborators string The users currently CC’ed on the ticket.
Zendesk.Ticket.collaborator_ids number The ID of the users currently CC’ed on the ticket.
Zendesk.Ticket.created_at date The ticket creation time.
Zendesk.Ticket.description string The ticket description.
Zendesk.Ticket.due_at date If this is a ticket of type “task” it has a due date. Due date format uses ISO 8601 format.
Zendesk.Ticket.email_ccs string The agents or end users currently CC’ed on the ticket.
Zendesk.Ticket.email_cc_ids number The ID of the agents or end users currently CC’ed on the ticket.
Zendesk.Ticket.external_id string An ID you can use to link Zendesk Support tickets to local records.
Zendesk.Ticket.followers string The agents currently following the ticket.
Zendesk.Ticket.follower_ids number The ID of the user who is currently following the ticket.
Zendesk.Ticket.followup_ids number The IDs of the followups created from this ticket. IDs are only visible once the ticket is closed.
Zendesk.Ticket.forum_topic_id number The topic in the Zendesk Web portal this ticket originated from, if any.
Zendesk.Ticket.group_id number The ID of the group this ticket is assigned to.
Zendesk.Ticket.has_incidents boolean True if a ticket is a problem type and has one or more incidents linked to it. Otherwise, the value is false.
Zendesk.Ticket.id number The ticket ID.
Zendesk.Ticket.is_public boolean True if any comments are public, false otherwise.
Zendesk.Ticket.organization string The organization of the requester.
Zendesk.Ticket.organization_id number The ID of the organization of the requester.
Zendesk.Ticket.priority string The urgency with which the ticket should be addressed.
Zendesk.Ticket.problem_id number For tickets of type “incident”, the ID of the problem the incident is linked to.
Zendesk.Ticket.raw_subject string The dynamic content placeholder, if present, or the “subject” value, if not.
Zendesk.Ticket.recipient string The original recipient email address of the ticket.
Zendesk.Ticket.requester string The user who requested this ticket.
Zendesk.Ticket.requester_id number The ID of the user who requested this ticket.
Zendesk.Ticket.satisfaction_rating unknown The satisfaction rating of the ticket, if it exists, or the state of satisfaction, “offered” or “unoffered”.
Zendesk.Ticket.sharing_agreement_ids number The IDs of the sharing agreements used for this ticket.
Zendesk.Ticket.status string The state of the ticket.
Zendesk.Ticket.subject string The value of the subject field for this ticket.
Zendesk.Ticket.submitter string The user who submitted the ticket.
Zendesk.Ticket.submitter_id number The ID of the user who submitted the ticket.
Zendesk.Ticket.tags string The array of tags applied to this ticket.
Zendesk.Ticket.type string The type of this ticket.
Zendesk.Ticket.updated_at date When this ticket was last updated.
Zendesk.Ticket.url string The API URL of this ticket.

zendesk-ticket-update


Updates a Zendesk ticket.
Required permissions: Agents.

Base Command

zendesk-ticket-update

Input

Argument Name Description Required
ticket_id The ID of the ticket to update. Required
subject The subject of this ticket. Optional
type The type of this ticket. Possible values are: problem, incident, question, task. Optional
requester The user who requested this ticket. Optional
assignee_email The email address of the agent to assign the ticket to. Optional
collaborators Users to add as CCs when creating a ticket. Optional
comment A comment to add to the ticket. Optional
html_comment An HTML comment to add to the ticket. Optional
public true if a public comment; false if an internal note. The initial value set on ticket creation persists for any additional comment unless you change it. Possible values are: true, false. Default is true. Optional
recipient The original recipient email address of the ticket. Optional
status The state of the ticket. Possible values are: open, pending, hold, solved, closed. Optional
priority The urgency with which the ticket should be addressed. Possible values are: urgent, high, normal, low. Optional
due_at If this is a ticket of type “task” it has a due date. Due date format uses ISO 8601 format. Optional
email_ccs An array of agents or end users email CC’s to add or delete from the ticket. Default is add.\nThe format is ‘562624,562625:put,example@example.com:delete’. Optional
external_id An ID you can use to link Zendesk Support tickets to local records. Optional
followers An array of agent followers to add or delete from the ticket. Default is add.\nThe format is ‘562624,562625:put,example@example.com:delete’. Optional
group_id The ID of the group this ticket is assigned to. Optional
organization The ID of the organization of the requester. Optional
problem_id For tickets of type “incident”, the ID of the problem the incident is linked to. Optional
tags The tags applied to this ticket. Optional
custom_fields Custom fields for the ticket (this is a JSON-formatted argument see: https://developer.zendesk.com/documentation/ticketing/managing-tickets/creating-and-updating-tickets#setting-custom-field-values). Optional
brand_id Enterprise only. The ID of the brand this ticket is associated with. Optional

Context Output

Path Type Description
Zendesk.Ticket.allow_attachments boolean Whether the agents have permission to attachments to a comment.
Zendesk.Ticket.allow_channelback boolean False if channelback is disabled, true otherwise. Only applicable for channels framework ticket.
Zendesk.Ticket.assignee string The agent currently assigned to the ticket.
Zendesk.Ticket.collaborators string The users currently CC’ed on the ticket.
Zendesk.Ticket.collaborator_ids number The ID of the users currently CC’ed on the ticket.
Zendesk.Ticket.created_at date The ticket creation time.
Zendesk.Ticket.description string The ticket description.
Zendesk.Ticket.due_at date If this is a ticket of type “task” it has a due date. Due date format uses ISO 8601 format.
Zendesk.Ticket.email_ccs string The agents or end users currently CC’ed on the ticket.
Zendesk.Ticket.email_cc_ids number The ID of the agents or end users currently CC’ed on the ticket.
Zendesk.Ticket.external_id string An ID you can use to link Zendesk Support tickets to local records.
Zendesk.Ticket.followers string The agents currently following the ticket.
Zendesk.Ticket.follower_ids number The ID of the user who is currently following the ticket.
Zendesk.Ticket.followup_ids number The IDs of the followups created from this ticket. Ids are only visible once the ticket is closed.
Zendesk.Ticket.forum_topic_id number The topic in the Zendesk Web portal this ticket originated from, if any.
Zendesk.Ticket.group_id number The group this ticket is assigned to.
Zendesk.Ticket.has_incidents boolean True if a ticket is a problem type and has one or more incidents linked to it. Otherwise, the value is false.
Zendesk.Ticket.id number The ticket ID.
Zendesk.Ticket.is_public boolean True if any comments are public, false otherwise.
Zendesk.Ticket.organization string The organization of the requester.
Zendesk.Ticket.priority string The urgency with which the ticket should be addressed.
Zendesk.Ticket.problem_id number For tickets of type “incident”, the ID of the problem the incident is linked to.
Zendesk.Ticket.raw_subject string The dynamic content placeholder, if present, or the “subject” value, if not.
Zendesk.Ticket.recipient string The original recipient email address of the ticket.
Zendesk.Ticket.requester string The user who requested this ticket.
Zendesk.Ticket.satisfaction_rating unknown The satisfaction rating of the ticket, if it exists, or the state of satisfaction, “offered” or “unoffered”.
Zendesk.Ticket.sharing_agreement_ids number The IDs of the sharing agreements used for this ticket.
Zendesk.Ticket.status string The state of the ticket.
Zendesk.Ticket.subject string The value of the subject field for this ticket.
Zendesk.Ticket.submitter string The user who submitted the ticket.
Zendesk.Ticket.tags string The array of tags applied to this ticket.
Zendesk.Ticket.type string The type of this ticket.
Zendesk.Ticket.updated_at date When this ticket was last updated.
Zendesk.Ticket.url string The API URL of this ticket.

zendesk-ticket-delete


Delete ticket.
Required permissions: Admins, Agents with permission to delete tickets.
Agent delete permissions are set in Support.
See Deleting tickets in the Zendesk Support Help Center.

Base Command

zendesk-ticket-delete

Input

Argument Name Description Required
ticket_id The ID of the ticket to delete. Required

Context Output

There is no context output for this command.

zendesk-ticket-comment-list


List comments for a given ticket.
Required permissions: Agents.

Base Command

zendesk-ticket-comment-list

Input

Argument Name Description Required
ticket_id The ID of the ticket to list comments. Required
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

Path Type Description
Zendesk.Ticket.Comment.attachments.content_type string The content type of the image. Example value: “image/png”.
Zendesk.Ticket.Comment.attachments.content_url string A full URL where the attached image file can be downloaded.
Zendesk.Ticket.Comment.attachments.deleted boolean If true, the attachment has been deleted.
Zendesk.Ticket.Comment.attachments.file_name string The name of the file.
Zendesk.Ticket.Comment.attachments.height string The height of the image file in pixels.
Zendesk.Ticket.Comment.attachments.id number The ID of the attachment.
Zendesk.Ticket.Comment.attachments.inline boolean If true, the attachment is excluded from the attachment list and the attachment’s URL can be referenced within the comment of a ticket.
Zendesk.Ticket.Comment.attachments.mapped_content_url string The URL the attachment image file has been mapped to.
Zendesk.Ticket.Comment.attachments.size number The size of the image file in bytes.
Zendesk.Ticket.Comment.attachments.url string A URL to access the attachment details.
Zendesk.Ticket.Comment.attachments.width string The width of the image file in pixels.
Zendesk.Ticket.Comment.audit_id number The ID of the ticket audit record.
Zendesk.Ticket.Comment.author string The comment author.
Zendesk.Ticket.Comment.body string The comment string.
Zendesk.Ticket.Comment.created_at date The time the comment was created.
Zendesk.Ticket.Comment.html_body string The comment formatted as HTML.
Zendesk.Ticket.Comment.id number The comment ID.
Zendesk.Ticket.Comment.metadata unknown System information (web client, IP address, etc.) and comment flags, if any.
Zendesk.Ticket.Comment.plain_body string The comment as plain text.
Zendesk.Ticket.Comment.public boolean True if a public comment, false if an internal note.
Zendesk.Ticket.Comment.type string Comment or VoiceComment.

zendesk-ticket-attachment-add


Attach file to ticket.
Required permissions: End users.

Base Command

zendesk-ticket-attachment-add

Input

Argument Name Description Required
ticket_id The ID of the ticket where the file should be uploaded. Required
file_id The file entry ID. Required
comment The comment body for the attached file. Required
filename The filename to upload. Default is the original filename. Optional

Context Output

There is no context output for this command.

zendesk-attachment-get


Get attachment.
Required permissions: Admins.

Base Command

zendesk-attachment-get

Input

Argument Name Description Required
attachment_id The ID of attachment to retrieve. Required

Context Output

There is no context output for this command.

zendesk-search


Search in Zendesk.

Base Command

zendesk-search

Input

Argument Name Description Required
query The query for the search. Required
page_number The page number (used for pagination). Optional
page_size The page size (used for pagination). Optional
limit Maximum number of results to return. Default is 50. Optional

Context Output

Path Type Description
Zendesk.Search unknown The search results.

zendesk-article-list


List all available articles.
Required permissions: Agents, End users, Anonymous users.

Base Command

zendesk-article-list

Input

Argument Name Description Required
article_id The ID of a specific article to retrieve. Optional
locale The locale that the article is displayed in. Optional
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

There is no context output for this command.

zendesk-clear-cache


Clears the Zendesk integration cache.

Base Command

zendesk-clear-cache

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

get-remote-data


Get remote data from a remote incident. Note that this method will not update the current incident. It’s used for debugging purposes.

Base Command

get-remote-data

Input

Argument Name Description Required
id The remote incident ID. Required
lastUpdate UTC timestamp in seconds. The incident is only updated if it was modified after the last update time. Default is 0. Optional

Context Output

There is no context output for this command.

get-mapping-fields


Returns the list of fields for an incident type.

Base Command

get-mapping-fields

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

get-modified-remote-data


Get the list of incidents that were modified since the last update. Note that this method is used for debugging purposes. get-modified-remote-data is used as part of a Mirroring feature, which is available since version 6.1.

Base Command

get-modified-remote-data

Input

Argument Name Description Required
lastUpdate Date string representing the local time. The incident is only returned if it was modified after the last update time. Optional

Context Output

There is no context output for this command.

update-remote-system


Updates local incident changes in the remote incident. This method is only used for debugging purposes and will not update the current incident.

Base Command

update-remote-system

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

zendesk-group-user-list


Get group’s users.
Allowed for: Admins, Agents and Light Agents.

Note: In case the group_id does not exist, the command will return all users.

Base Command

zendesk-group-user-list

Input

Argument Name Description Required
group_id The ID of a specific group. Required
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

Path Type Description
Zendesk.UserGroup.active Boolean False if the user has been deleted.
Zendesk.UserGroup.alias String An alias displayed to end users.
Zendesk.UserGroup.created_at Date The time the user was created.
Zendesk.UserGroup.custom_role_id Number A custom role if the user is an agent on the Enterprise plan or above.
Zendesk.UserGroup.default_group_id Number The ID of the user’s default group.
Zendesk.UserGroup.details String Any details you want to store about the user, such as an address.
Zendesk.UserGroup.email String The user’s primary email address.
Zendesk.UserGroup.external_id String A unique identifier from another system.
Zendesk.UserGroup.iana_time_zone String The time zone for the user.
Zendesk.UserGroup.id Number The user’s ID.
Zendesk.UserGroup.last_login_at Date The last time the user signed in to Zendesk Support.
Zendesk.UserGroup.locale String The user’s locale.
Zendesk.UserGroup.locale_id Number The user’s locale ID.
Zendesk.UserGroup.moderator Boolean Whether the user has forum moderation capabilities.
Zendesk.UserGroup.name String The user’s name.
Zendesk.UserGroup.notes String Any notes you want to store about the user.
Zendesk.UserGroup.only_private_comments Boolean True if the user can only create private comments.
Zendesk.UserGroup.organization_id Number The ID of the user’s organization.
Zendesk.UserGroup.phone String The user’s primary phone number.
Zendesk.UserGroup.photo String A URL pointing to the user’s profile picture.
Zendesk.UserGroup.report_csv Boolean Whether the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.
Zendesk.UserGroup.restricted_agent Boolean Whether the agent has any restrictions; false for admins and unrestricted agents, true for other agents.
Zendesk.UserGroup.role String The user’s role. Possible values are “end-user”, “agent”, or “admin”.
Zendesk.UserGroup.role_type Number The user’s role type.
Zendesk.UserGroup.shared Boolean Whether the user is shared from a different Zendesk Support instance.
Zendesk.UserGroup.shared_agent Boolean Whether the user is a shared agent from a different Zendesk Support instance.
Zendesk.UserGroup.shared_phone_number Boolean Whether the phone number is shared.
Zendesk.UserGroup.signature String The user’s signature.
Zendesk.UserGroup.suspended Boolean Whether the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal.
Zendesk.UserGroup.tags Unknown The user’s tags. Only present if your account has user tagging enabled.
Zendesk.UserGroup.ticket_restriction Unknown Specifies which tickets the user has access to. Possible values are: “organization”, “groups”, “assigned”, “requested”, null.
Zendesk.UserGroup.time_zone String The user’s time zone.
Zendesk.UserGroup.two_factor_auth_enabled Unknown Whether two factor authentication is enabled.
Zendesk.UserGroup.updated_at Date The time the user was last updated.
Zendesk.UserGroup.url String The URL that points to the user’s API record.
Zendesk.UserGroup.user_fields Unknown The user fields as shown in the Zendesk user interface.
Zendesk.UserGroup.verified Boolean Whether any of the user’s identities is verified.

Command example

!zendesk-group-user-list group_id=12345 limit=1

Context Example

{
    "Zendesk": {
        "UserGroup": [
            {
                "active": true,
                "alias": "",
                "created_at": "2022-03-27T08:42:06Z",
                "custom_role_id": 4678497517981,
                "default_group_id": 4678483739805,
                "details": "",
                "email": "test@user.com",
                "external_id": null,
                "iana_time_zone": "Asia/Jerusalem",
                "id": 1908275070333,
                "last_login_at": "2023-05-31T11:13:41Z",
                "locale": "en-US",
                "locale_id": 1,
                "moderator": true,
                "name": "Admin",
                "notes": "",
                "only_private_comments": false,
                "organization_id": 4678483740317,
                "phone": null,
                "photo": null,
                "report_csv": true,
                "restricted_agent": false,
                "role": "admin",
                "role_type": 4,
                "shared": false,
                "shared_agent": false,
                "shared_phone_number": null,
                "signature": "",
                "suspended": false,
                "tags": [],
                "ticket_restriction": null,
                "time_zone": "Asia/Jerusalem",
                "two_factor_auth_enabled": null,
                "updated_at": "2023-05-31T11:13:41Z",
                "url": "https://some-url/api/v2/users/1908275070333.json",
                "user_fields": {},
                "verified": true
            }
        ]
    }
}

Human Readable Output

Zendesk Group Users

Id Name Email Role CreatedAt
1908275070333 Admin test@user.com admin 2022-03-27T08:42:06Z

zendesk-group-list


Get Zendesk groups.
Allowed for: Admins, Agents.

Base Command

zendesk-group-list

Input

Argument Name Description Required
limit Maximum number of results to return. Default is 50. Optional
page_size The page size (used for pagination). Optional
page_number The page number (used for pagination). Optional

Context Output

Path Type Description
Zendesk.Group.created_at Date The time the group was created.
Zendesk.Group.default Boolean If the group is the default one for the account.
Zendesk.Group.deleted Boolean true if the group is deleted false otherwise.
Zendesk.Group.description String The description of the group.
Zendesk.Group.id Number The group ID.
Zendesk.Group.is_public Boolean If true, the group is public. If false, the group is private. You can’t change a private group to a public group.
Zendesk.Group.name String The name of the group.
Zendesk.Group.updated_at Date The time of the last update of the group.
Zendesk.Group.url String The API URL of the group.

Command example

!zendesk-group-list limit=1

Context Example

{
    "Zendesk": {
        "Group": [
            {
                "created_at": "2023-06-06T07:44:20Z",
                "default": false,
                "deleted": false,
                "description": "This is a group for testing",
                "id": 11395818128925,
                "is_public": true,
                "name": "Test Group",
                "updated_at": "2023-06-06T07:44:20Z",
                "url": "https://some-url/api/v2/groups/11395818128925.json"
            }
        ]
    }
}

Human Readable Output

Zendesk groups

Id Name IsPublic CreatedAt UpdatedAt
11395818128925 Test Group true 2023-06-06T07:44:20Z 2023-06-06T07:44:20Z

Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and Zendesk v2 corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:

  1. Enable Fetching incidents in your instance configuration.
  2. In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:

    Option Description
    None Turns off incident mirroring.
    Incoming Any changes in Zendesk v2 events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents.
    Outgoing Any changes in Cortex XSOAR incidents will be reflected in Zendesk v2 events (outgoing mirrored fields).
    Incoming And Outgoing Changes in Cortex XSOAR incidents and Zendesk v2 events will be reflected in both directions.
  3. Optional: Check the Close Mirrored XSOAR Incident integration parameter to close the Cortex XSOAR incident when the corresponding event is closed in Zendesk v2.

Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.

Important Notes:

  • To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Zendesk v2.
  • Required permissions: Admins

Configuration parameters

  • base_url — Server URL (e.g., https://demisto.zendesk.com) (required)
  • credentials — Username (or '<username>/token' when using API key). (required)
  • isFetch — Fetch incidents
  • mirror_direction — Incident Mirroring Direction
  • close_incident — Close mirrored incidents
  • get_attachments — Get incident attachments
  • mirror_tags — Mirror tags
  • time_filter — Ticket Field to Fetch by
  • ticket_types — Ticket types to fetch
  • ticket_status — Fetch tickets status filter
  • ticket_priority — Fetch tickets priority filter
  • fetch_query — Fetch tickets query filter
  • max_fetch — Maximum number of incidents per fetch
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentType — Incident type
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)

Commands (21)

  • get-mapping-fields

    Returns the list of fields for an incident type.

  • get-modified-remote-data

    Get the list of incidents that were modified since the last update. Note that this method is used for debugging purposes. get-modified-remote-data is used as part of a Mirroring feature, which is available since version 6.1.

  • get-remote-data

    Get remote data from a remote incident. Note that this method will not update the current incident. It's used for debugging purposes.

  • update-remote-system

    Updates local incident changes in the remote incident. This method is only used for debugging purposes and will not update the current incident.

  • zendesk-article-list

    List all available articles. Required permissions: Agents, End users, Anonymous users.

  • zendesk-attachment-get

    Get attachment. Required permissions: Admins.

  • zendesk-clear-cache

    Clears the Zendesk integration cache.

  • zendesk-group-list

    Get Zendesk groups. Allowed for: Admins, Agents.

  • zendesk-group-user-list

    Get group's users. Allowed for: Admins, Agents and Light Agents.

  • zendesk-organization-list

    Get organization's data. Required permissions: Agents, with certain restrictions. If the agent has a custom agent role that restricts the agent's access to only users in their own organization, a 403 Forbidden error is returned. See Creating custom agent roles in the Zendesk help.

  • zendesk-search

    Search in Zendesk.

  • zendesk-ticket-attachment-add

    Attach a file to a ticket. Required permissions: End users.

  • zendesk-ticket-comment-list

    List comments for a given ticket. Required permissions: Agents.

  • zendesk-ticket-create

    Create a new Zendesk ticket. Required permissions: Agents.

  • zendesk-ticket-delete

    Delete ticket. Required permissions: Admins, Agents with permission to delete tickets. Agent delete permissions are set in Support. See Deleting tickets in the Zendesk Support Help Center.

  • zendesk-ticket-list

    List Zendesk tickets. Required permissions: Agents.

  • zendesk-ticket-update

    Updates a Zendesk ticket. Required permissions: Agents.

  • zendesk-user-create

    Creates a new Zendesk user. Required permissions: Agents, with restrictions on certain actions.

  • zendesk-user-delete

    Delete a user. Required permissions: Admins.

  • zendesk-user-list

    Gets the specified user's data. Required permissions: Admins, Agents and Light Agents.

  • zendesk-user-update

    Update user data. Required permissions: Agents, with restrictions on certain actions.

from collections import deque
from collections.abc import Callable, Iterable, Iterator
from copy import copy
from functools import lru_cache

import demistomock as demisto
from CommonServerPython import *
from requests import Response
from requests.exceptions import HTTPError
from urllib3 import disable_warnings

STR_OR_STR_LIST = str | list[str]
MAX_PAGE_SIZE = 100
USER_CONTEXT_PATH = "Zendesk.User"
USERS_HEADERS = ["id", "name", "email", "role", "active", "external_id", "created_at", "updated_at"]
ORGANIZATIONS_HEADERS = ["id", "name", "domain_names", "tags", "external_id", "created_at", "updated_at"]
TICKETS_HEADERS = ["id", "subject", "description", "priority", "status", "assignee_id", "created_at", "updated_at", "external_id"]
COMMENTS_HEADERS = ["id", "body", "created_at", "public", "attachments"]
ATTACHMENTS_HEADERS = ["id", "file_name", "content_url", "size", "content_type"]
GROUP_USER_HEADERS = ["id", "name", "email", "role", "created_at"]
GROUP_HEADERS = ["id", "name", "is_public", "created_at", "updated_at"]
ARTICLES_HEADERS = ["body"]
ROLES = ["end-user", "admin", "agent"]
ROLE_TYPES = {
    "custom_agent": 0,
    "light_agent": 1,
    "chat_agent": 2,
    "chat_agent_contributor": 3,
    "admin": 4,
    "billing_admin": 5,
}
TICKET_FILTERS = ["assigned", "requested", "ccd", "followed", "recent"]
CURSOR_SORTS = {
    "id_asc": "id",
    "status_asc": "status",
    "updated_at_asc": "updated_at",
    "id_desc": "-id",
    "status_desc": "-status",
    "updated_at_desc": "-updated_at",
}
TICKET_TYPE = ["problem", "incident", "question", "task"]
TICKET_STATUS = ["open", "pending", "hold", "solved", "closed"]
TICKET_PRIORITY = ["urgent", "high", "normal", "low"]
PRIORITY_MAP = {
    "urgent": IncidentSeverity.CRITICAL,
    "high": IncidentSeverity.HIGH,
    "normal": IncidentSeverity.MEDIUM,
    "low": IncidentSeverity.LOW,
}
params = demisto.params()  # pylint: disable=W9016
MIRROR_USER_AGENT = "XSOAR mirror"
MIRROR_DIRECTION = {"Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}.get(params.get("mirror_direction"))
FIELDS_TO_REMOVE_FROM_MIROR_IN = ["url", "id", "created_at"]
DEFAULT_UPLOAD_FILES_COMMENT = "Uploaded from XSOAR."
MIRROR_TAGS = params.get("mirror_tags") or []
CLOSE_INCIDENT = argToBoolean(params.get("close_incident", False))
INTEGRATION_INSTANCE = demisto.integrationInstance()
CACHE = None
ZENDESK_FETCH_TIME_FORMAT = "%Y-%m-%dT%H:%M:00Z"


class CacheManager:
    def __init__(self, zendesk_client):
        self._data = None
        self._zendesk_client: ZendeskClient = zendesk_client

    @staticmethod
    def zendesk_clear_cache(**kwargs):
        demisto.setIntegrationContext({})
        return "Cache clear done."

    def save(self):
        if self._data:
            demisto.setIntegrationContext(self._data)

    def replace_ids_change(self, data: dict, organization_fields: list[str] = [], user_fields: list[str] = []):
        for fields, get_func in [(organization_fields, self.organization), (user_fields, self.user)]:
            for field in fields:
                obj_id = data.get(field)
                if obj_id:
                    field = field.replace("_id", "")
                    if isinstance(obj_id, list):
                        data[field] = list(map(get_func, obj_id))
                    else:
                        data[field] = get_func(obj_id)
        return data

    @property
    def data(self):
        if self._data is None:
            self._data = demisto.getIntegrationContext()
        return self._data

    @lru_cache
    def user(self, user_id: int) -> str:
        return self._generic_get_by_id("users", user_id, self._zendesk_client._get_user_by_id, "email")

    @lru_cache
    def organization(self, organization_id: int) -> str:
        return self._generic_get_by_id("organizations", organization_id, self._zendesk_client._get_organization_by_id, "name")

    @lru_cache
    def organization_name(self, organization_name: str) -> int | dict:
        organizations = self._zendesk_client._get_organizations_by_name(organization_name)
        ids = ",".join(str(x["id"]) for x in organizations)
        assert len(organizations) == 1, f"found {len(organizations)} organizations with name {organization_name} and ids {ids}"

        return organizations[0]["id"]

    def _generic_get_by_id(self, data_type: str, obj_id: int, data_get: Callable, val_field: str):
        self.data[data_type] = self.data.get(data_type, {})
        try:
            return self.data[data_type][obj_id]
        except KeyError:
            pass

        try:
            user_email = data_get(obj_id)[val_field] or obj_id
            self.data[data_type][obj_id] = user_email
            return user_email
        except:  # noqa # lgtm[py/]
            return obj_id


def datetime_to_iso(date: datetime) -> str:
    return date.strftime("%Y-%m-%dT%H:%M:%SZ")


def prepare_kwargs(
    kwargs: dict[str, Any],
    ignore_args: STR_OR_STR_LIST = [],
    str_args: STR_OR_STR_LIST = [],
    list_args: STR_OR_STR_LIST = [],
    bool_args: STR_OR_STR_LIST = [],
    int_args: STR_OR_STR_LIST = [],
    json_args: STR_OR_STR_LIST = [],
) -> dict[str, Any]:
    return_kwargs = {}

    for arg in ignore_args if isinstance(ignore_args, list) else [ignore_args]:
        if arg in kwargs:
            return_kwargs[arg] = kwargs[arg]

    for arg in str_args if isinstance(str_args, list) else [str_args]:
        if arg in kwargs:
            return_kwargs[arg] = str(kwargs[arg])

    for arg in list_args if isinstance(list_args, list) else [list_args]:
        if arg in kwargs:
            return_kwargs[arg] = argToList(kwargs[arg])

    for arg in bool_args if isinstance(bool_args, list) else [bool_args]:
        if arg in kwargs:
            return_kwargs[arg] = argToBoolean(kwargs[arg])

    for arg in int_args if isinstance(int_args, list) else [int_args]:
        if arg in kwargs:
            return_kwargs[arg] = int(kwargs[arg])

    for arg in json_args if isinstance(json_args, list) else [json_args]:
        if arg in kwargs:
            return_kwargs[arg] = kwargs[arg] if isinstance(kwargs[arg], dict) else json.loads(kwargs[arg])

    return return_kwargs


def error_entry(error_msg: str) -> dict[str, Any]:
    return {
        "Type": EntryType.ERROR,
        "ContentsFormat": EntryFormat.TEXT,
        "Contents": error_msg,
    }


def close_entry(reason: str) -> dict[str, Any]:
    return {
        "Type": EntryType.NOTE,
        "Contents": {"dbotIncidentClose": True, "closeReason": reason},
        "ContentsFormat": EntryFormat.JSON,
    }


class Validators:
    @staticmethod
    def _validate(val: Any, arg_name: str, allowed: Iterable[Any]):
        copy_value = argToList(val)
        for value in copy_value:
            assert value in allowed, f"'{val}' is not a valid {arg_name}.\nallowed {arg_name}s are '{','.join(allowed)}'"

    @staticmethod
    def validate_role(role: STR_OR_STR_LIST):
        Validators._validate(role, "role", ROLES)

    @staticmethod
    def validate_role_type(role_type: str):
        Validators._validate(role_type, "role type", ROLE_TYPES.keys())

    @staticmethod
    def validate_ticket_filter(ticket_filter: str):
        Validators._validate(ticket_filter, "filter", TICKET_FILTERS)

    @staticmethod
    def validate_ticket_sort(ticket_sort: str):
        Validators._validate(ticket_sort, "sort", CURSOR_SORTS.keys())

    @staticmethod
    def validate_ticket_type(ticket_type: str):
        Validators._validate(ticket_type, "type", TICKET_TYPE)

    @staticmethod
    def validate_ticket_status(ticket_status: str):
        Validators._validate(ticket_status, "status", TICKET_STATUS)

    @staticmethod
    def validate_ticket_priority(ticket_priority: str):
        Validators._validate(ticket_priority, "priority", TICKET_PRIORITY)


class TicketEvents:
    def __init__(self, zendesk_client, after_cursor: str = None, tickets_list: list = []):
        self._client = zendesk_client
        self._demisto_params = demisto.params()  # pylint: disable=W9016
        self._tickets_list = tickets_list
        self._after_cursor = after_cursor
        self._last_fetch: dict = {}

    def _get_all(self, **kwargs):
        return self._client._http_request("GET", url_suffix="incremental/tickets/cursor", params=kwargs)

    def next_run(self):
        next_run = {
            "after_cursor": self._last_fetch.get("after_cursor", self._after_cursor),
        }
        if self._tickets_list:
            next_run["tickets"] = self._tickets_list
        return next_run

    @abstractmethod
    def query_params(self):
        if after_cursor := self._last_fetch.get("after_cursor", self._after_cursor):
            return {"cursor": after_cursor}
        return {}

    def tickets(self, limit: int = 1000, params: dict | None = {}):
        yielded = 0
        if self._tickets_list:
            for _ in range(min(limit, len(self._tickets_list))):
                yield self._tickets_list.pop(0)
                yielded += 1
            if yielded >= limit:
                return

        res = self._get_all(**(self.query_params() | params))
        self._tickets_list = res.get("tickets", [])
        while True:
            self._after_cursor = res.get("after_cursor") or self._after_cursor
            for _ in range(min(limit - yielded, len(self._tickets_list))):
                yield self._tickets_list.pop(0)
                yielded += 1

            if res["end_of_stream"]:
                return
            res = self._get_all(**(self.query_params() | params))


class UpdatedTickets(TicketEvents):
    def __init__(self, zendesk_client, last_update: int, last_run_data: dict = {}):
        self._last_update = last_update
        super().__init__(zendesk_client, last_run_data.get("after_cursor"))

    def query_params(self):
        params = super().query_params()
        if not params:
            params["start_time"] = self._last_update
        return params

    def tickets(self):  # type: ignore[override]
        def filter_created_ticket(ticket: dict):
            return ticket["created_at"] != ticket["updated_at"]

        yield from filter(filter_created_ticket, super().tickets())


class ZendeskClient(BaseClient):
    def __init__(
        self, base_url: str, username: str | None = None, password: str | None = None, proxy: bool = False, verify: bool = True
    ):
        base_url = urljoin(base_url, "/api/v2/")
        auth = headers = None
        if username and password:
            # auth = (f'{username}/token', password)
            auth = (username, password)
        elif password:
            headers = {"Authorization": f"Bearer {password}"}

        super().__init__(base_url, auth=auth, proxy=proxy, verify=verify, headers=headers)

    @staticmethod
    def error_handler(res: Response) -> None:
        if res.text:
            raise DemistoException(f"Error occurred in Zendesk API: {res.text}")
        res.raise_for_status()

    def _http_request(  # type: ignore[override]
        self,
        method: str,
        url_suffix: str = "",
        full_url: str | None = None,  # type: ignore[override]
        json_data: dict | None = None,
        params: dict = None,
        data: dict = None,
        content: bytes = None,
        resp_type: str = "json",
        return_empty_response: bool = False,
        **kwargs,
    ):
        if params:
            final_params_list = []
            for k, v in params.items():
                if isinstance(v, list):
                    for singel_v in v:
                        final_params_list.append(f"{k}[]={singel_v}")
                else:
                    final_params_list.append(f"{k}={v}")
            params_str = f'?{"&".join(final_params_list)}'
            if url_suffix:
                url_suffix = f"{url_suffix}{params_str}"
            if full_url:
                full_url = f"{full_url}{params_str}"

        return super()._http_request(
            method,
            url_suffix=url_suffix,
            full_url=full_url,
            json_data=json_data,
            data=data or content,
            return_empty_response=return_empty_response,
            resp_type=resp_type,
            error_handler=self.error_handler,
            **kwargs,
        )

    def __cursor_pagination(
        self, url_suffix: str, data_field_name: str, params: dict | None = None, limit: int = 50
    ) -> Iterator[dict]:
        # API docs here https://developer.zendesk.com/api-reference/ticketing/introduction/#using-cursor-pagination
        page_size = min(limit, MAX_PAGE_SIZE)
        next_link_section = "next"
        count_data = 1
        paged_params = copy(params) if params is not None else {}
        paged_params["page[size]"] = page_size
        res = self._http_request("GET", url_suffix=url_suffix, params=paged_params)
        while True:
            for i in res[data_field_name]:
                yield i
                count_data += 1
                if count_data > limit:
                    return

            if not dict_safe_get(res, ["meta", "has_more"]):
                break

            res = self._http_request("GET", full_url=res["links"][next_link_section])

    def __get_spesific_page(
        self, url_suffix: str, data_field_name: str, page_size: int, page_number: int, params: dict | None = None
    ) -> Iterator[dict]:
        # API docs here https://developer.zendesk.com/api-reference/ticketing/introduction/#using-offset-pagination
        page_size = min(page_size, MAX_PAGE_SIZE)
        paged_params = copy(params) if params is not None else {}
        paged_params["per_page"] = page_size
        paged_params["page"] = page_number
        yield from self._http_request("GET", url_suffix=url_suffix, params=paged_params)[data_field_name]

    def _paged_request(
        self,
        url_suffix: str,
        data_field_name: str,
        params: dict | None = None,
        limit: int = 50,
        page_size: int | None = None,
        page_number: int | None = None,
    ) -> Iterator[dict]:
        # validate parameters
        if page_size is not None and page_number is not None:
            return self.__get_spesific_page(
                url_suffix=url_suffix,
                data_field_name=data_field_name,
                params=params,
                page_size=int(page_size),
                page_number=int(page_number),
            )
        elif page_size is not None or page_number is not None:
            raise AssertionError("you need to specify both 'page_size' and 'page_number'.")
        else:
            return self.__cursor_pagination(
                url_suffix=url_suffix, data_field_name=data_field_name, params=params, limit=int(limit)
            )

    # ---- user related functions ---- #

    @staticmethod
    def __command_results_zendesk_users(users: list[dict]):
        role_types_reverse = {int_k: str_k for str_k, int_k in ROLE_TYPES.items()}

        def _iter_context(user: dict):
            user = CACHE.replace_ids_change(user, ["organization_id"])  # type: ignore
            role_type = role_types_reverse.get(user.get("role_type"))  # type: ignore
            if role_type:
                user["role_type"] = role_type
            return user

        raw_results = copy(users)
        context = list(map(_iter_context, users))
        readable_outputs = tableToMarkdown(
            name="Zendek users:", t=context, headers=USERS_HEADERS, headerTransform=camelize_string
        )
        return CommandResults(
            outputs_prefix=USER_CONTEXT_PATH, outputs=context, readable_output=readable_outputs, raw_response=raw_results
        )

    def _get_user_by_id(self, user_id: str):
        return self._http_request("GET", f"users/{user_id}")["user"]

    def zendesk_user_list(
        self, user_id: STR_OR_STR_LIST | None = None, user_name: str | None = None, role: list[str] | str | None = None, **kwargs
    ):
        users_field_name = "users"
        results = []
        error_msgs = []

        if user_id is not None:
            users_list = []
            for single_user in argToList(user_id):
                try:
                    users_list.append(self._get_user_by_id(single_user))
                except Exception as e:
                    demisto.error(f"could not retrieve user: {single_user}\n{traceback.format_exc()}")
                    error_msgs.append(f"could not retrieve user: {single_user}\n{e}")
        elif user_name is not None:
            users_list = self._http_request("GET", "users/autocomplete", params={"name": user_name})[users_field_name]
        else:
            params = prepare_kwargs(kwargs=kwargs, str_args="external_id")
            if role:
                role_list = argToList(role)
                Validators.validate_role(role_list)
                params["role"] = role_list[0] if len(role_list) == 1 else role_list
            users_list = list(self._paged_request("users", "users", params=params, **kwargs))

        if users_list:
            results.append(self.__command_results_zendesk_users(users_list))

        if error_msgs:
            results.append(error_entry("\n".join(error_msgs)))

        return results if results else "No outputs."

    @staticmethod
    def _handle_role_argument(role: str | None = None, role_type: str | None = None) -> dict[str, Any]:
        role_params: dict[str, str | int] = {}
        if role:
            Validators.validate_role(role)
            role_params["role"] = role
            if role_type is not None:
                assert role == "agent", "You cannot use the 'role_type' argument if the selected role is not 'agent'"
                Validators.validate_role_type(role_type)
                role_params["role_type"] = ROLE_TYPES[role_type]
        return role_params

    def zendesk_user_create(
        self,
        name: str,
        email: str,
        role: str | None = None,
        role_type: str | None = None,
        check_if_user_exists: bool = False,
        **kwargs,
    ):
        url_suffix = "users/create" if argToBoolean(check_if_user_exists) else "users/create_or_update"

        user_body = {"name": name, "email": email}

        if "organization_name" in kwargs:
            assert "organization_id" not in kwargs, "you can specify 'organization_id' or 'organization_name' not both."
            kwargs["organization_id"] = CACHE.organization_name(kwargs.pop("organization_name"))  # type: ignore

        user_body.update(
            prepare_kwargs(
                kwargs=kwargs,
                str_args=["phone", "notes", "details", "external_id", "locale", "alias"],
                list_args="tags",
                int_args=["organization_id", "default_group_id", "custom_role_id"],
                bool_args="verified",
                json_args=["identities", "user_fields"],
            )
        )
        user_body.update(self._handle_role_argument(role=role, role_type=role_type))

        return self.__command_results_zendesk_users(
            [self._http_request("POST", url_suffix=url_suffix, json_data={"user": user_body})["user"]]
        )

    def zendesk_user_update(self, user_id: str, role: str | None = None, role_type: str | None = None, **kwargs):
        if "organization_name" in kwargs:
            assert "organization_id" not in kwargs, "you can specify 'organization_id' or 'organization_name' not both."
            kwargs["organization_id"] = CACHE.organization_name(kwargs.pop("organization_name"))  # type: ignore

        user_body = prepare_kwargs(
            kwargs=kwargs,
            str_args=["name", "email", "phone", "notes", "details", "external_id", "locale", "alias"],
            list_args="tags",
            int_args=["organization_id", "default_group_id", "custom_role_id"],
            bool_args=["verified", "suspended"],
            json_args=["identities", "user_fields"],
        )
        user_body.update(self._handle_role_argument(role=role or "agent", role_type=role_type))

        return self.__command_results_zendesk_users(
            [self._http_request("PUT", url_suffix=f"users/{user_id}", json_data={"user": user_body})["user"]]
        )

    def zendesk_user_delete(self, user_id: str, **kwargs):  # pragma: no cover
        self._http_request("DELETE", url_suffix=f"users/{user_id}")
        return f"User deleted. (id: {user_id})"

    # ---- organization related functions ---- #

    @staticmethod
    def __command_results_zendesk_organizations(organizations: list[dict]):  # pragma: no cover
        readable_outputs = tableToMarkdown(
            name="Zendek organizations:", t=organizations, headers=ORGANIZATIONS_HEADERS, headerTransform=camelize_string
        )
        return CommandResults(outputs_prefix="Zendesk.Organization", outputs=organizations, readable_output=readable_outputs)

    def _get_organization_by_id(self, organization_id: str) -> dict[str, Any]:
        return self._http_request("GET", f"organizations/{organization_id}")["organization"]

    def _get_organizations_by_name(self, organization_name: str) -> list[dict[str, Any]]:
        return self._http_request("GET", "organizations/autocomplete", params={"name": organization_name})["organizations"]

    def zendesk_organization_list(self, organization_id: str | None = None, **kwargs):
        if organization_id:
            organizations = [self._get_organization_by_id(organization_id)]
        else:
            organizations = list(self._paged_request(url_suffix="organizations", data_field_name="organizations", **kwargs))

        return self.__command_results_zendesk_organizations(organizations)

    # ---- group related functions ---- #
    @staticmethod
    def __command_results_zendesk_group_users(users: list[dict]):  # pragma: no cover
        readable_outputs = tableToMarkdown(
            name="Zendesk Group Users:", t=users, headers=GROUP_USER_HEADERS, headerTransform=camelize_string
        )
        return CommandResults(outputs_prefix="Zendesk.UserGroup", outputs=users, readable_output=readable_outputs)

    def list_group_users(self, group_id: int, **kwargs):
        users = list(self._paged_request(url_suffix=f"groups/{group_id}/users", data_field_name="users", **kwargs))
        return self.__command_results_zendesk_group_users(users)

    @staticmethod
    def __command_results_zendesk_groups(groups):  # pragma: no cover
        readable_outputs = tableToMarkdown(
            name="Zendesk groups:", t=groups, headers=GROUP_HEADERS, headerTransform=camelize_string
        )
        return CommandResults(outputs_prefix="Zendesk.Group", outputs=groups, readable_output=readable_outputs)

    def list_groups(self, **kwargs):
        groups = list(self._paged_request(url_suffix="groups", data_field_name="groups", **kwargs))
        return self.__command_results_zendesk_groups(groups)

    # ---- ticket related functions ---- #

    @staticmethod
    def __ticket_context(ticket: dict[str, Any]):
        return CACHE.replace_ids_change(  # type: ignore
            ticket,
            organization_fields=["organization_id"],  # type: ignore
            user_fields=["assignee_id", "collaborator_ids", "email_cc_ids", "follower_ids", "requester_id", "submitter_id"],
        )

    @staticmethod
    def __command_results_zendesk_tickets(tickets: list[dict]):
        raw = tickets
        context = list(map(ZendeskClient.__ticket_context, tickets))
        readable_outputs = tableToMarkdown(
            name="Zendek tickets:", t=context, headers=TICKETS_HEADERS, headerTransform=camelize_string
        )
        return CommandResults(
            outputs_prefix="Zendesk.Ticket", outputs=tickets, readable_output=readable_outputs, raw_response=raw
        )

    def _get_ticket_by_id(self, ticket_id: str, **kwargs):
        return self._http_request("GET", f"tickets/{ticket_id}", **kwargs)["ticket"]

    @staticmethod
    def __get_sort_params(sort: str, cursor_paging: bool = False):
        Validators.validate_ticket_sort(sort)
        if not cursor_paging:
            # using the offset paged request
            sort_list = sort.split("_")
            sort, order = "_".join(sort_list[:-1]), sort_list[-1]
            return {"sort_by": sort, "sort_order": order}
            # using the cursor paged request
        return {"sort": CURSOR_SORTS[sort]}

    @staticmethod
    def __get_tickets_url_suffix(filter: str, user_id: str | int | None = None) -> str:
        match filter:
            case None:
                return "tickets"
            case "recent":  # lgtm[py/unreachable-statement]
                return "tickets/recent"
            case _:  # lgtm[py/unreachable-statement]
                assert user_id, f"user_id is required when using '{filter}' as filter."
                Validators.validate_ticket_filter(filter)
                return f"/users/{user_id}/tickets/{filter}"

    def zendesk_ticket_list(
        self,
        ticket_id: STR_OR_STR_LIST | None = None,
        query: str | None = None,
        user_id: str | None = None,
        sort: str | None = None,
        page_number: int | None = None,
        **kwargs,
    ):
        filter_ = kwargs.pop("filter", None)
        error_msgs = []
        command_results = []
        if query is not None:
            assert ticket_id is None, "please provide either 'query' or 'ticket_id' not both."
            ticket_filter = "type:ticket"
            query = query if query.startswith(ticket_filter) else f"{ticket_filter} {query}"
            ticket_id = [
                x["id"]
                for x in filter(
                    lambda x: x["result_type"] == "ticket",
                    self.__zendesk_search_results(query=query, page_number=page_number, **kwargs),
                )
            ]
        if ticket_id is not None:
            tickets = []
            for single_ticket in argToList(ticket_id):
                try:
                    tickets.append(self._get_ticket_by_id(single_ticket))
                except Exception as e:
                    demisto.error(f"could not retrieve ticket: {single_ticket}\n{traceback.format_exc()}")
                    error_msgs.append(f"could not retrieve ticket: {single_ticket}\n{e}")
        else:
            can_use_cursor_paging = page_number is None
            sort_params = self.__get_sort_params(sort, can_use_cursor_paging) if sort else None
            url_suffix = self.__get_tickets_url_suffix(filter_, user_id)
            tickets = list(
                self._paged_request(
                    url_suffix=url_suffix, data_field_name="tickets", params=sort_params, page_number=page_number, **kwargs
                )
            )

        if tickets:
            command_results.append(self.__command_results_zendesk_tickets(tickets))
        if error_msgs:
            command_results.append(error_entry("\n".join(error_msgs)))
        return command_results if command_results else "No outputs."

    class Ticket:
        def __init__(
            self,
            type: str | None = None,
            collaborators: str | None = None,
            comment: str | None = None,
            html_comment: str | None = None,
            public: str | bool | None = None,
            email_ccs: str | None = None,
            priority: str | None = None,
            followers: list[str] | str | None = None,
            status: str | None = None,
            **kwargs,
        ):
            self._data: dict[str, Any] = {}

            if type:
                Validators.validate_ticket_type(type)
                self._data["type"] = type
                if type != "incident":
                    assert "problem_id" not in kwargs, "you can't use 'problem_id' if the ticket type is not 'incident'"
            if priority:
                Validators.validate_ticket_priority(priority)
                self._data["priority"] = priority
            if comment:
                self._data["comment"] = {"body": comment}
                if public:
                    self._data["comment"]["public"] = argToBoolean(public)
            if html_comment:
                self._data["comment"] = {"html_body": html_comment}
                if public:
                    self._data["comment"]["public"] = argToBoolean(public)
            if status:
                Validators.validate_ticket_status(status)
                self._data["status"] = status
            if collaborators:
                self._data["collaborators"] = list(map(self.try_int, argToList(collaborators)))
            if followers:
                self._data["followers"] = list(map(self.follower_and_email_cc_parse, argToList(followers)))
            if email_ccs:
                self._data["email_ccs"] = list(map(self.follower_and_email_cc_parse, argToList(email_ccs)))

            self._data.update(
                prepare_kwargs(
                    kwargs=kwargs,
                    str_args=[
                        "subject",
                        "requester",
                        "assignee_email",
                        "recipient",
                        "priority",
                        "external_id",
                        "due_at",
                        "comment",
                    ],
                    list_args="tags",
                    int_args=[
                        "forum_topic_id",
                        "group_id",
                        "organization_id",
                        "via_followup_source_id",
                        "brand_id",
                        "problem_id",
                    ],
                    json_args="custom_fields",
                )
            )

        def __iter__(self):
            yield from self._data.items()

        @staticmethod
        def try_int(value: str) -> int | str:
            try:
                return int(value)
            except (ValueError, TypeError):
                return value

        @staticmethod
        def follower_and_email_cc_parse(user_and_action: str):
            follower_or_email_cc = {}
            splited_user_and_action = user_and_action.split(":")
            try:
                follower_or_email_cc["user_id"] = str(int(splited_user_and_action[0]))
            except ValueError:
                follower_or_email_cc["user_email"] = splited_user_and_action[0]

            if len(splited_user_and_action) == 2:  # action included
                follower_or_email_cc["action"] = splited_user_and_action[1]

            return follower_or_email_cc

    def zendesk_ticket_create(self, **kwargs):  # pragma: no cover
        for argument in ["subject", "type", "requester", "description"]:
            assert argument in kwargs, f"'{argument}' is a required argument."
        kwargs["comment"] = kwargs.pop("description")
        return self.__command_results_zendesk_tickets(
            [self._http_request("POST", url_suffix="tickets", json_data={"ticket": dict(self.Ticket(**kwargs))})["ticket"]]
        )

    def zendesk_ticket_update(
        self,
        ticket_id: str,
        results: bool | None = True,  # pragma: no cover
        is_mirror: bool = False,
        **kwargs,
    ):
        headers = {"user-agent": MIRROR_USER_AGENT} if is_mirror else {}
        res = self._http_request(
            "PUT", url_suffix=f"tickets/{ticket_id}", json_data={"ticket": dict(self.Ticket(**kwargs))}, headers=headers
        )
        if results:
            return self.__command_results_zendesk_tickets([res["ticket"]])
        return None

    def zendesk_ticket_delete(self, ticket_id: str, **kwargs):  # pragma: no cover
        self._http_request("DELETE", url_suffix=f"tickets/{ticket_id}", return_empty_response=True)
        return f"ticket: {ticket_id} deleted."

    @staticmethod
    def _map_comment_attachments(comment: dict):
        if not comment.get("attachments"):
            return comment

        copy_comment = copy(comment)
        copy_comment["attachments"] = []
        for attachment in comment["attachments"]:
            copy_comment["attachments"].append(
                {
                    "file name": attachment["file_name"],
                    "content url": attachment["content_url"],
                    "id": attachment["id"],
                }
            )
        return copy_comment

    @staticmethod
    def __command_results_zendesk_ticket_comments(comments: list[dict]):
        readable_pre_proces = list(map(ZendeskClient._map_comment_attachments, comments))
        readable_outputs = tableToMarkdown(
            name="Zendek comments:",
            t=readable_pre_proces,
            headers=COMMENTS_HEADERS,
            headerTransform=camelize_string,
            is_auto_json_transform=True,
        )
        return CommandResults(outputs_prefix="Zendesk.Ticket.Comment", outputs=comments, readable_output=readable_outputs)

    def _get_comments(self, ticket_id: str, **kwargs) -> list:  # type:ignore
        for comment in self._paged_request(url_suffix=f"tickets/{ticket_id}/comments", data_field_name="comments", **kwargs):
            for attachment in comment.get("attachments", []):
                attachment.pop("thumbnails", None)
            yield CACHE.replace_ids_change(comment, user_fields=["author_id"])  # type: ignore

    def zendesk_ticket_comment_list(self, ticket_id: str, **kwargs):
        return self.__command_results_zendesk_ticket_comments(list(self._get_comments(ticket_id, **kwargs)))

    # ---- attachment related functions ---- #

    def zendesk_ticket_attachment_add(
        self,
        file_id: STR_OR_STR_LIST,
        ticket_id: int,
        comment: str,
        filename: STR_OR_STR_LIST | None = None,
        is_mirror: bool = False,
        **kwargs,
    ):
        headers = {"Content-Type": "application/binary"}
        if is_mirror:
            headers["user-agent"] = MIRROR_USER_AGENT
        file_id = argToList(file_id)
        file_name_list = argToList(filename) if filename else [None] * len(file_id)
        file_tokens = []
        uploaded_files = []
        for single_file_id, single_file_name in zip(file_id, file_name_list):
            single_file = demisto.getFilePath(single_file_id)
            display_name = single_file_name or single_file["name"]
            with open(single_file["path"], "rb") as file_to_upload:
                file_tokens.append(
                    self._http_request(
                        "POST",
                        url_suffix="uploads",
                        params={"filename": display_name},
                        headers=headers,
                        content=file_to_upload.read(),
                    )["upload"]["token"]
                )
            uploaded_files.append(display_name)

        self._http_request(
            "PUT", url_suffix=f"tickets/{ticket_id}", json_data={"ticket": {"comment": {"uploads": file_tokens, "body": comment}}}
        )

        return f'file: {", ".join(uploaded_files)} attached to ticket: {ticket_id}'

    def zendesk_attachment_get(self, attachment_id):
        attachments = [
            self._http_request("GET", url_suffix=f"attachments/{single_attachent_id}")["attachment"]
            for single_attachent_id in argToList(attachment_id)
        ]

        def filter_thumbnails(attachment: dict):
            attachment.pop("thumbnails")
            return attachment

        attachments = list(map(filter_thumbnails, attachments))

        return attachments

    def get_file_entries(self, attachments):
        results = []
        for attachment_link, attachment_name in ((x["content_url"], x["file_name"]) for x in attachments):
            res = self._http_request("GET", full_url=attachment_link, resp_type="response")
            res.raise_for_status()
            results.append(fileResult(filename=attachment_name, data=res.content, file_type=EntryType.ENTRY_INFO_FILE))
        return results

    def zendesk_attachment_get_command(self, attachment_id: int, **kwargs):
        attachments = self.zendesk_attachment_get(attachment_id)
        readable_output = tableToMarkdown(
            name="Zendesk attachments", t=attachments, headers=ATTACHMENTS_HEADERS, headerTransform=camelize_string
        )
        results = [CommandResults(outputs_prefix="Zendesk.Attachment", outputs=attachments, readable_output=readable_output)]

        file_entries = self.get_file_entries(attachments)
        results.append(file_entries)

        return results

    # ---- search related functions ---- #

    def __zendesk_search_results(
        self, query: str, limit: int = 50, page_number: int | None = None, page_size: int = 50, additional_params: dict = {}
    ):
        params = {"query": query} | additional_params
        results = []
        if page_number:
            results = list(
                self.__get_spesific_page(
                    url_suffix="search.json",
                    params=params,
                    data_field_name="results",
                    page_number=int(page_number),
                    page_size=int(page_size),
                )
            )
        else:
            count = self._http_request("GET", url_suffix="search/count.json", params=params)["count"]
            limit = min(int(limit), count)
            size = min(limit, MAX_PAGE_SIZE)
            current_page = 1
            while len(results) < limit:
                results.extend(
                    self.__get_spesific_page(
                        url_suffix="search.json",
                        params=params,
                        data_field_name="results",
                        page_number=current_page,
                        page_size=size,
                    )
                )
                current_page += 1
            results = results[:limit]

        return results

    def zendesk_search(self, query: str, limit: int = 50, page_number: int | None = None, page_size: int = 50, **kwargs):
        return CommandResults(
            outputs_prefix="Zendesk.Search",
            outputs=self.__zendesk_search_results(query=query, limit=limit, page_number=page_number, page_size=page_size),
        )

    # ---- articles related functions ---- #

    def zendesk_article_list(self, locale: str | None = "", article_id: int | None = None, **kwargs):
        if locale:
            locale = f"{locale}/"
        if article_id:
            articles = [self._http_request("GET", url_suffix=f"help_center/{locale}articles/{article_id}")["article"]]
        else:
            articles = list(self._paged_request(url_suffix=f"help_center/{locale}articles", data_field_name="articles", **kwargs))

        readable_output = ["</h1>Zendesk articles</h1>"]
        for title, body in ((x["title"], x["body"]) for x in articles):
            readable_output.append(f"<h1>{title}</h1>\n{body}")

        return CommandResults(outputs_prefix="Zendesk.Article", outputs=articles, readable_output="\n\n\n".join(readable_output))

    # ---- demisto related functions ---- #

    def test_module(self):  # pragma: no cover
        exception: Exception
        # If one of the endpoints work we will pass the test_module check.
        for data_type in ["tickets", "users", "organizations"]:
            try:
                self._paged_request(url_suffix=data_type, data_field_name=data_type, limit=1)
                return "ok"
            except Exception as e:
                exception = e

        raise exception from None

    def _ticket_to_incident(self, ticket: dict):
        ticket |= {
            "severity": PRIORITY_MAP.get(ticket["priority"]),
            "mirror_instance": INTEGRATION_INSTANCE,
            "mirror_id": str(ticket["id"]),
            "mirror_direction": MIRROR_DIRECTION,
            "mirror_last_sync": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
            "mirror_tags": MIRROR_TAGS,
        }
        return {
            "rawJSON": json.dumps(ticket),
            "name": ticket["subject"],
            "occurred": ticket["created_at"],
            "attachment": ticket.get("attachments", []),
        }

    @staticmethod
    def _fetch_query_builder(ticket_priority: str = None, ticket_status: str = None, ticket_types: str = None, **_):
        query_parts = []
        if ticket_priority and "all" not in ticket_priority:
            for priority in argToList(ticket_priority):
                query_parts.append(f"priority:{priority}")
        if ticket_status and "all" not in ticket_status:
            for status in argToList(ticket_status):
                query_parts.append(f"status:{status}")
        if ticket_types and "all" not in ticket_types:
            for ticket_type in argToList(ticket_types):
                query_parts.append(f"ticket_type:{ticket_type}")
        return " ".join(query_parts)

    @staticmethod
    def _fetch_args(params, last_run):
        # from params
        time_filter = "updated" if params.get("time_filter") == "updated-at" else "created"
        query = params.get("fetch_query") or ZendeskClient._fetch_query_builder(**params)
        max_fetch = min(100, int(params.get("max_fetch") or 50))
        get_attachments = params.get("get_attachments")

        # from last_run
        fetched_tickets = deque(last_run.get("fetched_tickets") or [])
        if query != last_run.get("query") or time_filter != last_run.get("time_filter"):
            last_run = {"fetched_tickets": fetched_tickets}
        page_number = last_run.get("page_number") or 1
        last_fetch = last_run.get("fetch_time")
        max_fetch = last_run.get("max_fetch") or max_fetch
        if not last_fetch:
            first_fetch = params.get("first_fetch") or "3 days"
            first_fetch_datetime = dateparser.parse(first_fetch, settings={"TIMEZONE": "UTC"})
            if not first_fetch_datetime:
                raise DemistoException(f"invalid first fetch time specified ({first_fetch})")
            last_fetch = first_fetch_datetime.strftime(ZENDESK_FETCH_TIME_FORMAT)

        return fetched_tickets, last_fetch, time_filter, query, max_fetch, page_number, get_attachments

    @staticmethod
    def _next_fetch_args(
        fetched_tickets, search_results_ids, next_run_start_time, query, time_filter, max_fetch, page_number, current_fetch
    ):
        next_run = {
            "fetched_tickets": list(fetched_tickets)[-1000:],
            "fetch_time": next_run_start_time.strftime(ZENDESK_FETCH_TIME_FORMAT),
        }
        if len(search_results_ids) >= max_fetch:
            next_run |= {
                "max_fetch": max_fetch,
                "page_number": page_number + 1,
                "fetch_time": current_fetch,
                "query": query,
                "time_filter": time_filter,
            }

        return next_run

    def get_attachments_ids(self, ticket: dict) -> list[int]:
        """

        Args:
            ticket (dict): The fetched ticket.

        Returns (list): all the attachment ids for a ticket

        """
        attachments_ids = []
        ticket_id = ticket["id"]
        comments_list = self._get_comments(ticket_id=ticket_id)
        for comment in comments_list:
            attachments = comment.get("attachments", [])
            for attachment in attachments:
                attachment_id = attachment.get("id")
                attachments_ids.append(attachment_id)

        return attachments_ids

    def get_attachment_entries(self, ticket: dict) -> list:
        """

        Args:
            ticket (dict): The ticket to get the file entries

        Returns: The attachments entries.

        """
        attachments_ids = self.get_attachments_ids(ticket)
        file_names = []
        attachments = self.zendesk_attachment_get(attachments_ids)
        demisto.debug(f"The fetched attachments - {attachments}")
        attachments_entries = self.get_file_entries(attachments)
        if isinstance(attachments_entries, list):
            for file_result in attachments_entries:
                if file_result["Type"] == entryTypes["error"]:
                    raise Exception(f"Error getting attachment: {file_result.get('Contents', '')!s}")
                file_names.append({"path": file_result.get("FileID", ""), "name": file_result.get("File", "")})
        return file_names

    def fetch_incidents(self, params: dict, lastRun: str | None = None):
        last_run = json.loads(lastRun or "null") or demisto.getLastRun() or {}
        fetched_tickets, last_fetch, time_filter, query, max_fetch, page_number, get_attachments = self._fetch_args(
            params, last_run
        )

        # look back window for tickets
        next_run_start_time = datetime.utcnow() - timedelta(minutes=1)

        query_parts = ["type:ticket", f"{time_filter}>{last_fetch}"]
        query_parts = query_parts + [query] if query else query_parts

        search_results = self.__zendesk_search_results(
            query=" ".join(query_parts),
            limit=max_fetch,
            page_size=max_fetch,
            page_number=page_number,
            additional_params={"sort_by": f"{time_filter}_at", "sort_order": "asc"},
        )
        search_results_ids = [x["id"] for x in search_results]
        filtered_search_results_ids = list(filter(lambda x: x not in fetched_tickets, search_results_ids))
        tickets = (self._get_ticket_by_id(x) for x in filtered_search_results_ids)
        ticket_modified = []
        if get_attachments:
            for ticket in tickets:
                attachments = ZendeskClient.get_attachment_entries(self, ticket)
                ticket.update({"attachments": attachments})
                ticket_modified.append(ticket)

        tickets = ticket_modified if ticket_modified else tickets
        incidents = list(map(self._ticket_to_incident, tickets))

        demisto.incidents(incidents)
        fetched_tickets.extend(filtered_search_results_ids)
        demisto.setLastRun(
            self._next_fetch_args(
                fetched_tickets, search_results_ids, next_run_start_time, query, time_filter, max_fetch, page_number, last_fetch
            )
        )

    def get_modified_remote_data(self, lastUpdate: str | None = None, **kwargs):
        try:
            timestamp = int(dateparser.parse(lastUpdate).timestamp())  # type: ignore
        except (TypeError, AttributeError):
            timestamp = 0
        last_run = get_last_mirror_run() or {}
        updated_tickets = UpdatedTickets(self, timestamp, last_run)
        tickets_ids = [str(x["id"]) for x in updated_tickets.tickets()]
        if tickets_ids:
            return_results(GetModifiedRemoteDataResponse(tickets_ids))
        try:
            set_last_mirror_run(updated_tickets.next_run())
        except json.decoder.JSONDecodeError as e:
            demisto.debug(f"{e}")

    @staticmethod
    def _create_entry_from_comment(comment: dict):
        comment_body = comment.get("body")
        attachments = comment.get("attachments")
        if attachments:
            attachments_table = tableToMarkdown("attachments", attachments, ["file_name", "id"], headerTransform=camelize_string)
            comment_body = f"{comment_body}\n{attachments_table}"

        return {"Type": EntryType.NOTE, "Contents": comment_body, "ContentsFormat": EntryFormat.MARKDOWN, "Note": True}

    def get_remote_data(self, **kwargs):
        try:
            parsed_args = GetRemoteDataArgs(kwargs)
            last_update = datetime_to_iso(dateparser.parse(parsed_args.last_update, settings={"TIMEZONE": "UTC"}))  # type: ignore
            try:
                ticket_data = self._get_ticket_by_id(parsed_args.remote_incident_id)
            except HTTPError as e:
                if e.response.status_code == 404 and CLOSE_INCIDENT:
                    return GetRemoteDataResponse(
                        close_entry(f"ticket {parsed_args.remote_incident_id} deleted."),
                        [close_entry(f"ticket {parsed_args.remote_incident_id} deleted.")],
                    )
                raise e from None

            context = self.__ticket_context(ticket_data)
            context["severity"] = PRIORITY_MAP.get(ticket_data["priority"])
            context["incomming_mirror_error"] = ""
            for field_to_delete in FIELDS_TO_REMOVE_FROM_MIROR_IN:
                if field_to_delete in context:
                    del context[field_to_delete]

            def filter_comments(comment: dict):
                return (
                    comment["created_at"] > last_update
                    and dict_safe_get(comment, ["metadata", "system", "client"]) != MIRROR_USER_AGENT
                )

            ticket_entries = list(
                map(
                    self._create_entry_from_comment,
                    filter(filter_comments, self._get_comments(parsed_args.remote_incident_id, limit=200)),
                )
            )
            if ticket_data.get("status") == "closed" and CLOSE_INCIDENT:
                ticket_entries.append(close_entry(f"ticket {parsed_args.remote_incident_id} closed."))

            return GetRemoteDataResponse(context, ticket_entries)
        except Exception as e:
            return GetRemoteDataResponse(
                {"incomming_mirror_error": f"mirroring failed with error: {e}\n{traceback.format_exc()}"}, []
            )

    def update_remote_system(self, **kwargs):
        args = UpdateRemoteSystemArgs(kwargs)
        files = []
        args.delta = {key: val for key, val in args.delta.items() if val}

        if "severity" in args.delta:
            severity = args.delta.pop("severity")
            severity = max(severity, IncidentSeverity.LOW)
            for priority, severity_val in PRIORITY_MAP.items():
                if severity == severity_val:
                    args.delta["priority"] = priority
                    break

        if (
            args.incident_changed
            and CLOSE_INCIDENT
            and (args.inc_status == IncidentStatus.DONE or (args.data.get("state") == "closed"))
        ):
            args.delta["status"] = "closed"

        def upload_files_and_reset_files_list(files: list):
            while files:
                comment = files[0].get("contents", DEFAULT_UPLOAD_FILES_COMMENT)
                files_to_upload = []
                while files and files[0].get("contents", DEFAULT_UPLOAD_FILES_COMMENT) == comment:
                    files_to_upload.append(files.pop(0)["id"])

                self.zendesk_ticket_attachment_add(
                    file_id=files_to_upload, ticket_id=args.remote_incident_id, comment=comment, is_mirror=True
                )

        try:
            for entry in args.entries or []:
                # Mirroring files as entries
                if entry["type"] in [EntryType.ENTRY_INFO_FILE, EntryType.FILE, EntryType.IMAGE]:
                    files.append(entry)
                else:
                    upload_files_and_reset_files_list(files)
                    # Mirroring comment and work notes as entries
                    self.zendesk_ticket_update(
                        ticket_id=args.remote_incident_id, comment=entry["contents"], results=False, is_mirror=True
                    )

            upload_files_and_reset_files_list(files)
            if args.delta:
                self.zendesk_ticket_update(ticket_id=args.remote_incident_id, results=False, **args.delta)

        except HTTPError as e:
            if e.response.status_code != 404 and CLOSE_INCIDENT:
                raise e from None
            demisto.debug(f"ticket {args.remote_incident_id} deleted.")

        return args.remote_incident_id

    def get_mapping_fields(self, **kwargs):  # pragma: no cover
        zendesk_ticket_scheme = SchemeTypeMapping("Zendesk Ticket")
        zendesk_ticket_scheme.add_field(
            name="type", description='The type of this ticket. Allowed values are "problem", "incident", "question", or "task".'
        )
        zendesk_ticket_scheme.add_field(name="subject", description="The value of the subject field for this ticket.")
        zendesk_ticket_scheme.add_field(name="description", description="The ticket description.")
        zendesk_ticket_scheme.add_field(
            name="priority",
            description="The urgency with which the ticket should be addressed. "
            'Allowed values are "urgent", "high", "normal", or "low".',
        )
        zendesk_ticket_scheme.add_field(
            name="status",
            description='The state of the ticket. Allowed values are "new", "open", "pending", "hold", "solved", or "closed".',
        )
        zendesk_ticket_scheme.add_field(name="recipient", description="The original recipient e-mail address of the ticket.")
        zendesk_ticket_scheme.add_field(name="requester", description="The user who requested this ticket.")
        zendesk_ticket_scheme.add_field(name="assigne", description="The agent currently assigned to the ticket.")
        zendesk_ticket_scheme.add_field(name="organization", description="The organization of the requester.")
        zendesk_ticket_scheme.add_field(name="collaborators", description="The users currently CC'ed on the ticket.")
        zendesk_ticket_scheme.add_field(name="followers", description="The agents currently following the ticket.")
        zendesk_ticket_scheme.add_field(name="email_ccs", description="The agents or end users currently CC'ed on the ticket.")
        zendesk_ticket_scheme.add_field(name="tags", description="The array of tags applied to this ticket.")
        zendesk_ticket_scheme.add_field(
            name="custom_fields",
            description="Custom fields for the ticket (this is a json formatted argument see: https://developer.zendesk.com/documentation/ticketing/managing-tickets/creating-and-updating-tickets#setting-custom-field-values).",
        )  # noqa: E501

        return GetMappingFieldsResponse([zendesk_ticket_scheme])


def main():  # pragma: no cover
    params = demisto.params()
    verify = not params.get("insecure", False)
    if not verify:
        disable_warnings()
    client = ZendeskClient(
        base_url=params["base_url"],
        username=params["credentials"].get("identifier"),
        password=params["credentials"]["password"],
        proxy=params.get("proxy", False),
        verify=verify,
    )
    global CACHE
    CACHE = CacheManager(client)
    try:
        command = demisto.command()
        args = demisto.args()
        commands: dict[str, Callable] = {
            # demisto commands
            "test-module": client.test_module,
            "get-modified-remote-data": client.get_modified_remote_data,
            "get-remote-data": client.get_remote_data,
            "update-remote-system": client.update_remote_system,
            "get-mapping-fields": client.get_mapping_fields,
            "zendesk-clear-cache": CacheManager.zendesk_clear_cache,
            # user commands
            "zendesk-user-list": client.zendesk_user_list,
            "zendesk-user-create": client.zendesk_user_create,
            "zendesk-user-update": client.zendesk_user_update,
            "zendesk-user-delete": client.zendesk_user_delete,
            "zendesk-group-user-list": client.list_group_users,
            # Group commands
            "zendesk-group-list": client.list_groups,
            # organization commands
            "zendesk-organization-list": client.zendesk_organization_list,
            # ticket commands
            "zendesk-ticket-list": client.zendesk_ticket_list,
            "zendesk-ticket-create": client.zendesk_ticket_create,
            "zendesk-ticket-update": client.zendesk_ticket_update,
            "zendesk-ticket-delete": client.zendesk_ticket_delete,
            "zendesk-ticket-comment-list": client.zendesk_ticket_comment_list,
            # attachment commands
            "zendesk-ticket-attachment-add": client.zendesk_ticket_attachment_add,
            "zendesk-attachment-get": client.zendesk_attachment_get_command,
            # search command
            "zendesk-search": client.zendesk_search,
            # articles command
            "zendesk-article-list": client.zendesk_article_list,
        }
        demisto.debug(f"command {command} called")

        if command == "fetch-incidents":
            client.fetch_incidents(params, **args)
        elif command in commands:
            if command_res := commands[command](**args):
                return_results(command_res)
        else:
            raise NotImplementedError(command)
    except Exception as e:
        return_error(f"An error occurred: {e}", error=e)
    finally:
        CACHE.save()


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()