Synchronising tickets between Zendesk instances | Community
Skip to main content

Synchronising tickets between Zendesk instances

  • May 26, 2018
  • 8 replies
  • 0 views

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:

  1. Ticket received to standard instance.
  2. Agent assigns ticket to pseudo group corresponding to secure instance.
  3. 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.
  4. 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.
  5. 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.
  6. Assignment or closing of the secure ticket causes API calls to update the assignee, status or category of the standard instance stub.
  7. 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:
    {% case ticket.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"
        ]
      }
    }
    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.
    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:
    {
      "ticket": {
        "custom_fields": [
          {"id": assignee_id, "value""{{ticket.assignee.email}}"},
          {"id": group_id, "value""{{ticket.group.name}}"}
        ]
      }
    }
    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.
  • 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.

8 replies

  • June 7, 2018

Hey Dominic, this is an interesting implementation.  We've got multiple instances running, too, and we're implementing some limited instances of ticket sharing.  Couple questions about your config:

  1. What was the trade-off between developing this vs. trying to use native ticket sharing between instances? Did you have any ideas that would maintain the secure separation while requiring less dev work?
  2. What kind of load does this put on your API usage?  Looks like each time this process runs it takes 5+ API calls, which can get pretty expensive a lot of tickets are shared.
  3. Did you think at all about using the new Side Conversations feature for sharing input with minimal data sharing between those teams?

  • Author
  • June 8, 2018

Hi Matt, The built in ticket sharing didn't work for us at all since all our user accounts are synced from Active Directory to both our instances, and when a ticket is shared it would create a duplicate dummy user in the receiving instance. We could have worked around this but it probably would have been harder. This also gave us more control and a better end user experience.

The load is not so bad since each instance has its own API limit (both are enterprise plans) and there are only 2 api calls to each instance for a ticket escalation. The volume of tickets escalated (currently) is also fairly low.

I did not know about the side conversation feature until now, but it looks like this would not allow the HR/Payroll team to own their tickets and communicate securely without other agents being able to see their conversations. We may look at adding this into the secure instance so the HR team can still communicate with the other support teams in the standard instance.


Heather13
  • June 17, 2018

Dominic,
Fantastic post! Bravo! I really appreciate the details and technical implementation notes. I'll be referencing this, I'm sure.

How do you handle the user base? What I mean is, an update in one instance does not auto update the user in the other account. Small issue to tackle.

Thank you for your post!!


  • Author
  • June 17, 2018

Both userbases are kept the same by a script that creates and suspends users based on information in Active Directory.


Heather13
  • June 18, 2018

Brilliant!


Kuldeep11
  • August 30, 2021

@here Hi Everyone, I hope you all are doing well!

First of all, I would like to say that this is a very useful article. I really appreciate the details and how you classified them.

I have exactly the same scenarios and I wanted to configure a integration between two different Zendesk instances.

Based on your instructions, I have tried to run this setup and successfully completed a few steps.
That is:

  • Ticket received to the standard instance.
  • Agent assigns the ticket to the pseudo group corresponding to the secure instance.
  • API call from the standard instance creates the same ticket (excluding the ticket form and fields) in the secure instance and deletes the ticket in the standard instance to prevent confidential info from being present.

But there are some areas where I've been caught, and I would like to ask for your kind assistance on that.

  1. I want to synchronize the ticket fields along with the form - Is there any way to pass ticket forms & fields? FYI: I have already created the same ticket fields with their corresponding values and types in both the instances.
  2. What should I put in the 'ticket.group.id' and 'pseudo_secure_unassigned_group_id'?
{% case ticket.group.id %}
{% when 360020957191 %}
{% assign group = 360020723832 %}
{% 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": 360047087692, "value": "{{current_user.email}}"}]
  }
}

Also, for the 4th point i.e. "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.". I did some testing in the Create stub ticket: Trigger using the API call in secure instancebut each time the HTTP extension did not succeed in creating a stub ticket in the main instance. The Trigger event appears in the ticket summary, but it looks like that the API did not work as intended.

Below is what I sent in Target request: Create stub ticket: Trigger

{% case ticket.group.id %}
{% when 360020723832 %}
{% assign group = 360020957191 %}
{% else %}
{% assign group = ' ' %}
{% endcase %}
{
  "ticket":
  {
    "subject": "{{ticket.title}}",
    "submitter": "{{ticket.requester.email}}",
    "group_id": "{{group}}",
    "ticket_form_id": 360000851372,
    "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"
    ]
  }
}

Please enlighten me if I missed any point or went inaccurate, also if you'd like to mention any important point to make this work, I'm really looking forward to hearing from you on this.

Regards,

Kuldeep


  • September 1, 2021

Debugging the calls is one of the issues we faced with the above approach. Eventually we switched to using AWS Lambda to perform the integration. For your issue with creating stubs, I'm not sure if you'd get enough info from the extension log to debug it further.

To link your ticket fields between instances you'd need to use the API to list all custom fields in both instances and create the mapping between instances. Even when they have the same name in each instance they will have unique IDs. Once you have the IDs see here for info about adding it to the API request.


Kuldeep11
  • September 3, 2021

Hello @frogamic,

Thank you for having responded, I wonder if AWS will also be useful for me too, for creating stubs. Would you please share with me the context of AWS?

Also, thank you for sharing these additional resources.