Gareth Wilson Gareth Wilson

Building AI ticket triage with Zendesk and Claude

Published


A ticket lands in Zendesk describing the customer's attempts to complete an order. Within a second of the ticket being created, Claude has classified it as a checkout-funnel bug (not a billing complaint, not a feature request), cross-referenced the customer's order history to confirm they're a returning buyer with three prior orders in 60 days, scored the priority as "high — paying customer, revenue at risk, mobile-specific," tagged the ticket with checkout, safari-ios, revenue-loss, drafted a reply that acknowledges the specific error pattern and offers a workaround, and routed it to the iOS-experienced agent group instead of the general queue. By the time the overnight agent logs in, the ticket is one click from a sent reply, and the bug pattern is already aggregated into a Slack channel that the product team watches.

That's the workflow most teams want from Zendesk and an LLM. The Zendesk side is easy — triggers, automations, and webhooks are all well-documented. The Claude side is easy, too — POST /v1/messages is one call. The hard part is the middle: making sure every ticket gets triaged, that the AI step doesn't blow your rate limits during a Monday morning spike, and that you can answer "why didn't this ticket get prioritised correctly?" three weeks after the fact.

This guide walks through the glue layer end to end: the architecture, seven concrete steps to wire it up, and the production concerns that you need to get ahead of.

The flow

flowchart TB
    A[New ticket<br/>in Zendesk] --> B[Zendesk webhook<br/>via trigger]
    B -->|POST JSON| C[Hookdeck<br/>inbound source]
    C -->|filter + transform<br/>+ queue + rate limit| D[Claude<br/>triage handler]
    D -.->|messages API| E[claude-sonnet-4]
    D -->|POST triage result| F[Hookdeck<br/>callback source]
    F -->|priority routing| G1[Zendesk API<br/>update ticket + tags]
    F -->|escalation routing| G2[Slack<br/>on-call channel]
    F -->|pattern routing| G3[Bug pattern<br/>aggregation]

There are two webhook flows that need to be reliable:

  1. Zendesk's trigger webhook into the AI step — must survive Monday-morning spikes, holiday weekend backlogs, and incident-driven volume surges.
  2. The AI's outputs back into Zendesk and downstream systems — must reach Zendesk reliably even when Zendesk's own API is rate-limiting you, and must fan out cleanly to Slack and pattern-aggregation systems with their own availability profiles.

Most teams build this with a POST /webhooks/zendesk endpoint that calls Claude inline, then calls Zendesk's API to update the ticket. That's enough to demo. It's not enough to run, and the reasons it isn't enough are where Hookdeck Event Gateway helps.

What you'll need

  • A Zendesk account with admin access to create triggers and webhooks (see the Zendesk webhooks guide)
  • An Anthropic API key with access to a Claude model
  • A Hookdeck Event Gateway account — the free tier covers this workflow at low volume
  • Hookdeck CLI installed: npm install hookdeck-cli -g or brew install hookdeck/hookdeck/hookdeck
  • A handler endpoint that receives the ticket payload, calls Claude, and POSTs the result back

Step 1: Create the Hookdeck Event Gateway source for Zendesk

In the Hookdeck Event Gateway dashboard:

  • Create Connection → New Source
  • Type: Zendesk (Hookdeck has a pre-configured source — see creating Zendesk webhooks)
  • Name: zendesk-new-tickets
  • Provide your Zendesk webhook signing secret so Hookdeck Event Gateway can verify the x-zendesk-webhook-signature header

Copy the generated source URL.

Step 2: Create the Zendesk webhook and trigger

In Zendesk Admin Center:

  • Apps and integrations → Webhooks → Create webhook
  • Name: AI triage
  • Endpoint URL: paste the Hookdeck source URL
  • Request method: POST
  • Request format: JSON
  • Authentication: signed (paste the same secret you gave Hookdeck)
  • Save

Now create the trigger that fires the webhook:

  • Objects and rules → Triggers → Create trigger
  • Conditions: Ticket is Created
  • Actions: Notify active webhook → choose AI triage
  • JSON body:
{
  "ticket_id": "{{ticket.id}}",
  "subject": "{{ticket.title}}",
  "description": "{{ticket.description}}",
  "requester_email": "{{ticket.requester.email}}",
  "requester_id": "{{ticket.requester.id}}",
  "channel": "{{ticket.via}}",
  "created_at": "{{ticket.created_at_with_timestamp}}"
}

Save the trigger. Create a test ticket. The payload should appear in the Hookdeck Event Gateway dashboard within a second.

Step 3: Add the destination — your Claude triage handler

The handler:

  1. Receives the parsed ticket payload
  2. Optionally enriches with customer data from your own systems
  3. Calls Claude with a triage prompt returning structured JSON
  4. POSTs the triage result to a second Hookdeck source for fan-out

A minimal Cloudflare Worker:

export default {
  async fetch(request, env) {
    const ticket = await request.json();

    // Optional: enrich with internal customer data
    const customer = await fetch(
      `${env.INTERNAL_API}/customers/${ticket.requester_email}`,
      { headers: { authorization: `Bearer ${env.INTERNAL_TOKEN}` } }
    ).then(r => r.json());

    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'x-api-key': env.ANTHROPIC_API_KEY,
        'anthropic-version': '2023-06-01',
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        model: 'claude-sonnet-4-5',
        max_tokens: 1024,
        system: TRIAGE_PROMPT,
        messages: [{
          role: 'user',
          content: JSON.stringify({
            subject: ticket.subject,
            description: ticket.description,
            channel: ticket.channel,
            customer_plan: customer.plan,
            order_count_60d: customer.order_count_60d,
            lifetime_value: customer.lifetime_value,
          }),
        }],
      }),
    });

    const result = await response.json();
    const triage = JSON.parse(result.content[0].text);

    await fetch(env.HOOKDECK_CALLBACK_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ ticket_id: ticket.ticket_id, triage }),
    });

    return new Response('ok', { status: 200 });
  },
};

Configure the Hookdeck Event Gateway destination:

  • Type: HTTP
  • URL: your handler URL
  • Authentication: an HTTP header carrying a shared secret

Step 4: Add filter, transformation, and rate-limit rules

Transformation — the Zendesk trigger payload is mostly clean already; the main work is normalizing channel names and stripping HTML from descriptions:

addHandler('transform', (request, context) => {
  const body = request.body;

  request.body = {
    ticket_id: body.ticket_id,
    subject: body.subject || '(no subject)',
    description: stripHtml(body.description || ''),
    requester_email: body.requester_email,
    requester_id: body.requester_id,
    channel: body.channel || 'unknown',
    created_at: body.created_at,
  };

  return request;
});

function stripHtml(html) {
  return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
}

Filter — skip tickets that don't need AI triage. Tickets created via internal API calls, or from a known automated source, can be excluded:

{
  "body": {
    "channel": {
      "$nin": ["api", "system", "automated"]
    },
    "description": {
      "$exists": true,
      "$ne": ""
    }
  }
}

Rate limit — Claude has per-account limits; protect them during spikes:

  • Rate: 10 per second
  • Burst: 20

Retry policy — Claude occasionally returns 529 overloaded_error and 429; handle both:

  • Initial delay: 30 seconds
  • Max attempts: 15
  • Max age: 24 hours
  • Apply on status codes: 408, 429, 500, 502, 503, 504, 529

Claude's 429 responses respect a retry-after header that Hookdeck honours.

Step 5: Test the inbound leg locally with the CLI

Route the inbound connection to the CLI:

hookdeck login
hookdeck listen 3000 zendesk-new-tickets

A local inspector:

// inspect.js
const http = require('http');
http.createServer((req, res) => {
  let body = '';
  req.on('data', chunk => body += chunk);
  req.on('end', () => {
    console.log('Canonical ticket:', JSON.parse(body));
    res.writeHead(200);
    res.end('ok');
  });
}).listen(3000);

Create a real test ticket in Zendesk. Verify the canonical payload looks right and that the description is clean text without HTML noise. Iterate on the transformation; press r in the CLI to replay events without creating new tickets.

Step 6: Wire triage results back through Hookdeck

The triage result fans out:

  • Zendesk — update ticket priority, group, tags; add a draft reply as a private comment
  • Slack — escalate high-priority tickets to an on-call channel
  • Bug pattern aggregation — write specific tag combinations (e.g. checkout, safari-ios) to a product-team dashboard

Create a second connection with the source ticket-triage-results and one destination per downstream.

For the Zendesk write-back, a transformation builds the API call:

addHandler('transform', (request, context) => {
  const { ticket_id, triage } = request.body;

  request.url = `https://acme.zendesk.com/api/v2/tickets/${ticket_id}.json`;
  request.method = 'PUT';
  request.headers = {
    ...request.headers,
    authorization: `Basic ${context.secrets.ZENDESK_AUTH}`,
    'content-type': 'application/json',
  };
  request.body = {
    ticket: {
      priority: triage.priority,
      group_id: triage.group_id,
      tags: ['ai-triaged', ...triage.tags],
      comment: {
        body: triage.suggested_reply,
        public: false,
        author_id: -1, // suggestions appear as system user
      },
      custom_fields: [
        { id: 360000000001, value: triage.category },
        { id: 360000000002, value: triage.summary },
      ],
    },
  };

  return request;
});

A second connection from the same source routes urgent tickets to Slack with a filter:

{
  "body": {
    "triage": {
      "priority": "urgent"
    }
  }
}

A third connection writes specific tag patterns to a bug-aggregation endpoint for product-team visibility.

Retry policy on these callbacks:

  • Initial delay: 15 seconds
  • Max attempts: 15
  • Max age: 72 hours

Step 7: Run the full chain end to end

Create a test ticket that mimics a real support case. You should see, in order:

  1. The Zendesk trigger fires and POSTs to zendesk-new-tickets
  2. Hookdeck Event Gateway verifies, filters, transforms, and delivers to your handler
  3. Your handler enriches, calls Claude, and POSTs to ticket-triage-results
  4. Zendesk receives the priority/group/tag/draft update
  5. Slack gets a notification if priority is urgent
  6. The bug pattern dashboard updates if relevant tags are present

If anything fails, the Event Gateway dashboard tells you exactly where, with payload and response visibility at every hop. The ticket_id is preserved end to end so you can trace any specific case.

Why Hookdeck and not just a try/except in your app server?

Three properties of support workflows make a direct integration the wrong choice once you're past the demo:

Support events spike during incidents. This is the defining property of support volume — it correlates exactly with the moments your own infrastructure is under stress. A direct Zendesk-to-Claude handler that runs on the same app server as your product becomes the second thing that falls over during an outage. Hookdeck Event Gateway queues tickets during the spike on infrastructure independent of yours, so triage keeps running even when your app is partially down.

AI APIs throttle when you most need them. Claude rate-limits per workspace, and a Monday-morning ticket spike or an incident-driven flood will hit those limits faster than usual. Hookdeck Event Gateway respects retry-after, retries with exponential backoff, and keeps queueing until the rate limit recovers — without losing tickets.

Audit trail matters in support. When a customer asks "why was my urgent ticket triaged as low?", or when leadership asks "how many tickets got AI-triaged last quarter and what did the agents change?", you need an answer. Hookdeck Event Gateway stores every event, transformation, retry, and response. The support manager (not just engineering) gets a single dashboard that shows the path of any ticket through the AI step.

You can build all of this on your own: a queue, a retry worker, a transformation step, an audit log, an observability layer, a replay tool. That's the work Hookdeck Event Gateway collapses into a connection in a dashboard. The hours you don't spend on that are hours you spend on the triage prompt and the customer experience.

Going to production

Observability for support leadership. Hookdeck's Issues feature surfaces failure patterns automatically — repeated retries, signature failures, payload spikes. Wire alerts so a stuck triage step is visible before the support manager hears about it from a customer.

Tune retries against SLAs. If your first-response SLA is 30 minutes, configure aggressive early retries and a fallback to a human queue after a few failed attempts. Don't silently retry for 24 hours.

Replay deliberately when the prompt changes. When you update categories, priorities, or routing rules, Hookdeck Event Gateway's replay lets you re-run a historical window through the new prompt — useful for regression-testing before flipping it live.

Handle PII responsibly. Ticket bodies often contain account numbers, addresses, and sometimes payment details. Configure Hookdeck Event Gateway's payload redaction on sensitive fields before going live.

Plan for Zendesk's own rate limits on the write-back leg. Zendesk's API has tight per-endpoint limits. The rate-limit configuration on the callback connection should match — typically 200/minute for the tickets endpoint on lower plans.

What to build next

This pattern generalizes: Swap Zendesk for Freshdesk, Help Scout, or Salesforce Service Cloud. Swap Claude for OpenAI or any other model. Extend the AI step from classification to full automation for low-risk ticket types (password resets, FAQ matches).

If you're building any of this, the fastest way to get past the demo phase is to stop maintaining your own webhook infrastructure. Start with the Hookdeck free tier (you can run this entire workflow without paying anything until you hit real volume) and use the CLI to keep your development loop fast.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.