Intro
We have been using Zendesk for around a year now for our internal IT Helpdesk quite successfully. Our configuration gives all groups access to all other groups' tickets so they can follow them and check as they are escalated hierarchically and functionally. In particular the phone support team need to be able to find a ticket if and end user calls to check or escalate a ticket with another group.
Recently, HR and Payroll have noticed the success we have had with Zendesk and would like to move away from their email inbox and de-centralised support model where each division or team would typically go to their HR rep directly, to using Zendesk. This will allow them to better manage workloads, share work within their team and provides far better reporting. The IT Helpdesk also often have to escalate queries to the HR team as they perform an operations role around account creation and modification for onboarding and transferring staff between roles.
The Problem
This introduced the problem that we could no longer have agents able to see tickets from all groups, as HR and Payroll would be dealing with sensitive and confidential information such as salaries and personal information that we did not want to expose to all agents within the IT teams. We also wanted to be able to escalate tickets to the HR team and retain some record of this in the IT team to report on what types of tickets are escalated, and so the IT team can provide internal notes if needed on these tickets.
Our Solution
To overcome this security restriction, we created a second Zendesk instance for the Payroll and HR teams to work out of, and set up a custom integration to allow seamless ticket forwarding between the instances.
The ticket flow goes like this:
- Ticket received to standard instance.
- Agent assigns ticket to pseudo group corresponding to secure instance.
- API call from the standard instance creates the same ticket in the secure instance and deletes the ticket in the standard instance to prevent confidential info from being present.
- API call from the secure instance creates a 'stub' ticket in the standard instance containing the bare minimum of info from the escalated ticket, includes the secure ticket's ID as external ID.
- Stub creation in the standard instance causes an API call to the secure instance to update the external ID of the secure ticket to the stub ID.
- Assignment or closing of the secure ticket causes API calls to update the assignee, status or category of the standard instance stub.
- private comment on the stub causes an api call to put the same comment on the secure ticket.
Technical implementation
Ticket fields and forms
Standard Instance
- Ticket stub form: A ticket form containing the following fields:
- Secure categories: A dropdown containing the exact same categories available in the secure instance
- Secure Assignee: A text field that will show which agent is assigned in the secure instance
- Secure Group: A text field that will show which group is assigned in the secure instance
Secure Instance
- Escalated By: A text field that will show who escalated the ticket to the secure instance. Add this to the default ticket form.
Extensions
All are HTTP target, using JSON datatype
Standard Instance
- Create secure ticket: Makes a POST request to [securedomain].zendesk.com/api/v2/tickets.json
- Update secure ticket: Makes a PUT request to [securedomain].zendesk.com/api/v2/update_many.json?id={{ticket.external_id}}
the update_many endpoint is used to take advantage of additional_tags - Delete standard ticket: Makes a DELETE request to [standarddomain].zendesk.com/api/v2/tickets/{{ticket.id}}.json
Secure instance
- Create standard ticket: Makes a POST request to [standarddomain].zendesk.com/api/v2/tickets.json
- Update standard ticket: Makes a PUT request to [standarddomain].zendesk.com/api/v2/update_many.json?id={{ticket.external_id}}
Triggers
Standard Instance
-
Forward and delete: uses the Create secure ticket and Delete standard ticket extensions, runs when a ticket is in a pseudo group set up for escalations and doesn't have the tag escalated_to_secure. The body of the create request is:
{% case ticket.group.id %}
{% when standard_group_id %}
{% assign group = 'secure_group_id' %}
...
{% else %}
{% assign group = '' %}
{% endcase %}
{
"ticket":
{
"subject": "{{ticket.title}}",
"group_id": {{group}},
"comment": {
"body": "{{ticket.comments_formatted}}"
},
"requester": "{{ticket.requester.email}}",
"tags": [
"escalated_from_standard",
"ticket_stub_required"
],
"custom_fields": [{"id": escalated_by_id, "value": "{{current_user.email}}"}]
}
}The case block at the top uses Liquid Markup to match the groups between the 2 instances, add as many when/assign blocks as required with the group ID numbers of the 2 instances, the else allows assignment to the unassigned queue of the secure instance.
The custom fields assignment is a text field on the ticket form within the secure instance that lets agents within that instance know who escalated the ticket in case it was in error or they need further info. replace escalated_by_id with the ID of this field or remove this section. -
Send Stub ID: Uses the Update secure ticket extension to set the external_id of the secure ticket to the id of the created stub. Runs when a ticket is created with the tags has_secure_id and escalated_to_secure. Body of the request is:
{"ticket": {"external_id":"{{ticket.id}}","additional_tags": ["has_standard_id"]}} -
Send comment: Uses the Update secure ticket extension to put the new comment on the secure ticket. Runs when a ticket is updated and comment is present, and has the tags has_secure_id and escalated_to_secure. Body of the request is:
{"ticket": {"comment":{}
"body": "Comment via standard instance:\n{{ticket.latest_comment_formatted}}",
"public": false}}
Secure Instance
-
Create stub ticket: Uses the Create standard ticket extension to create the stub when a ticket is escalated. Runs when a ticket is created and has the tags escalated_from_standard and ticket_stub_required but not has_standard_id. The request body is:
Liquid markup is used again to convert secure group IDs to standard group IDs, invert your case and assign IDs from the create secure ticket trigger above.{%caseticket.group.id %}{% when secure_group_id %}{% assign group ='standard_group_id'%}{%else%}{% assign group ='pseudo_secure_unassigned_group_id'%}{% endcase %}{"ticket":{"subject":"{{ticket.title}}","submitter":"{{ticket.requester.email}}","group_id":"{{group}}","ticket_form_id": stub_form_id,"status": "hold","comment":{"body":"This is a ticket stub for a ticket escalated to secure, the secure ticket ID is {{ticket.id}}\nDo not edit this ticket.","public":false},"requester":"{{ticket.requester.email}}","external_id":"{{ticket.id}}","tags": ["stub_requires_sync","has_secure_id","escalated_to_secure"]}}
stub_form_id is the ID of a form created for ticket stubs, this has custom text fields to display the current assignee and group from the secure instance, and a categories dropdown that matches the categories from the secure instance. -
Update standard stub - reassigned: Uses the Update standard ticket endpoint with the current assigned agent and group. Runs when a ticket is updated, the assignee or group is changed and has the tag has_standard_id. Body of the request:
assignee_id and group_id are the IDs of 2 text fields on the standard instance's stub ticket form that will show the assignee and group.{"ticket": {"custom_fields": [{"id": assignee_id,"value":"{{ticket.assignee.email}}"},{"id": group_id,"value":"{{ticket.group.name}}"}]}} -
Update standard stub - closed: Uses the Update standard ticket extension, to close the stub and assign a category. Runs when a ticket is closed that has the has_standard_id tag. Body of the request is:
{{"ticket": {"status":"closed","custom_fields": [{"id": standard_instance_categories_id,"value":"{{ticket.ticket_field_[secure_categories_id]}}"}]}}This runs only when the ticket is closed, as the secure ticket could be re-opened or re-categorised which would not be reflected in the stub.
standard_instance_categories_id is the id of a categories dropdown on the standard instance's stub ticket form.
[secure_instance_categories_id] is the numeric ID of the same dropdown in the secure instance. Note the tags of the options in these 2 fields must match. The actual liquid markup will be like:{{ticket.ticket_field_12345}}
Conclusion
What we end up with is a fluid yet controlled replication between the 2 instances that allows comments to flow into the secure instance but not out, and allows some visibility to the helpdesk so that they can report on what is being escalated (and create guide articles to reduce this) and provide updates to end users should they enquire about a secure ticket that was escalated.