Send Slack alerts for low Zendesk CSAT scores using n8n

low complexityCost: $0-24/moRecommended

Prerequisites

Prerequisites
  • n8n instance (cloud or self-hosted)
  • Zendesk API credentials: subdomain, agent email, and API token (Admin Center → Apps and integrations → APIs → Zendesk API → enable Token Access)
  • Slack Bot Token with chat:write scope and the bot invited to your alert channel
  • Slack channel ID for your CSAT alerts channel (right-click channel → View channel details → copy ID at bottom)

Overview

Zendesk has no webhook or trigger for CSAT responses. When a customer rates a ticket "Bad," there's no event fired that you can listen for. The only way to detect new bad ratings is to poll the satisfaction ratings API on a schedule.

This workflow runs every 5 minutes, queries the Zendesk satisfaction ratings endpoint filtered to bad scores, checks for new ratings since the last poll, fetches ticket details for context, and sends a Slack Block Kit alert with a direct link to the ticket. This is the recommended approach because it provides near-real-time alerts with rich formatting and no code to maintain.

Step 1: Set up Zendesk credentials in n8n

Go to Credentials → Add credential → Zendesk API:

  • Subdomain: your Zendesk subdomain (the xxx part of xxx.zendesk.com)
  • Email: your agent email address
  • API Token: the token from Admin Center → APIs → Zendesk API

Test the connection to verify access.

Step 2: Add a Schedule Trigger node

Add a Schedule Trigger node as the workflow entry point:

  • Interval: Every 5 minutes

This is the polling frequency. Five minutes balances responsiveness with API rate limits.

Why 5 minutes?

Zendesk allows roughly 700 API requests per minute. This workflow uses 2-3 requests per poll cycle (1 for ratings, 1 per bad rating for ticket details). At 5-minute intervals, that's well under 1% of your rate limit — leaving plenty of headroom for other integrations.

Step 3: Poll the Zendesk satisfaction ratings API

Add an HTTP Request node:

  • Method: GET
  • URL: https://YOUR_SUBDOMAIN.zendesk.com/api/v2/satisfaction_ratings?score=bad&sort_order=desc&per_page=10
  • Authentication: Predefined Credential Type → Zendesk API (or use Basic Auth with email/token:api_token)

The score=bad parameter filters to only unsatisfied ratings. The sort_order=desc returns newest first so you only need the first page.

Step 4: Filter to new ratings only

Add a Code node to deduplicate. Since we poll every 5 minutes, we only want ratings that appeared since the last poll:

const ratings = $input.first().json.satisfaction_ratings || [];
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
 
const newBadRatings = ratings.filter(r => r.updated_at > fiveMinAgo);
 
if (newBadRatings.length === 0) {
  return []; // No new bad ratings — stop workflow
}
 
return newBadRatings.map(r => ({
  json: {
    ratingId: r.id,
    ticketId: r.ticket_id,
    comment: r.comment || '(no comment)',
    createdAt: r.updated_at,
  }
}));

If no new bad ratings exist, the workflow stops here — no unnecessary Slack or Zendesk API calls.

Time window overlap

If a poll cycle takes longer than expected or n8n skips an interval, you could miss ratings. A safer approach is to store the last-seen rating ID in n8n's static data and filter by ID instead of time. For most teams, the 5-minute time window is reliable enough.

Step 5: Fetch ticket details

Add an HTTP Request node to get context for each bad rating:

  • Method: GET
  • URL: https://YOUR_SUBDOMAIN.zendesk.com/api/v2/tickets/{'{'}$json.ticketId{'}'}.json
  • Authentication: Same Zendesk credentials
  • Batching: Process items individually (one request per bad rating)

This returns the ticket subject, requester name, assignee, tags, and other details you'll include in the Slack alert.

Step 6: Post a Slack Block Kit alert

Add an HTTP Request node (or Slack node) to post the alert:

  • Method: POST
  • URL: https://slack.com/api/chat.postMessage
  • Headers: Authorization: Bearer YOUR_SLACK_BOT_TOKEN
  • Body (JSON):
{
  "channel": "CSAT_CHANNEL_ID",
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "Bad CSAT Rating Received",
        "emoji": true
      }
    },
    {
      "type": "section",
      "fields": [
        {"type": "mrkdwn", "text": "*Ticket:* #{{$json.ticketId}}"},
        {"type": "mrkdwn", "text": "*Subject:* {{$json.ticket.subject}}"},
        {"type": "mrkdwn", "text": "*Customer:* {{$json.ticket.requester.name}}"},
        {"type": "mrkdwn", "text": "*Assignee:* {{$json.ticket.assignee_id}}"}
      ]
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Customer comment:* {{$json.comment}}"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": {"type": "plain_text", "text": "View in Zendesk"},
          "url": "https://YOUR_SUBDOMAIN.zendesk.com/agent/tickets/{{$json.ticketId}}"
        }
      ]
    }
  ]
}

Replace YOUR_SUBDOMAIN with your Zendesk subdomain and CSAT_CHANNEL_ID with your Slack channel ID.

Include the customer comment

When a customer rates "Bad," Zendesk optionally lets them leave a comment explaining why. Including this in the Slack alert gives the follow-up agent immediate context about what went wrong, without needing to open the ticket first.

Step 7: Reopen the ticket (optional)

If your team wants bad-rated tickets automatically reopened for follow-up, add another HTTP Request node:

  • Method: PUT
  • URL: https://YOUR_SUBDOMAIN.zendesk.com/api/v2/tickets/{'{'}$json.ticketId{'}'}.json
  • Body:
{
  "ticket": {
    "status": "open",
    "tags": ["csat-bad", "needs-followup"]
  }
}

This reopens the ticket and adds tags so agents can filter to all bad-CSAT tickets in a Zendesk View.

Step 8: Activate and test

  1. Set the workflow to Active
  2. To test immediately, submit a bad CSAT rating on a solved ticket in your Zendesk sandbox
  3. Wait for the next 5-minute poll cycle (or trigger the workflow manually)
  4. Verify the Slack alert appears with the correct ticket details and link
  5. Enable Retry On Fail on all HTTP Request nodes for resilience against temporary API errors
No CSAT webhook exists in Zendesk

You must poll the API on a schedule. Polling every 5 minutes balances responsiveness with API rate limits. Zendesk allows roughly 700 requests per minute on Professional plans. Do not poll more frequently than every 2 minutes — it wastes executions and provides minimal benefit since customers rarely submit ratings in real-time.

Cost

  • n8n Cloud Starter: $0-24/mo depending on execution volume. This workflow uses approximately 4 node executions per poll cycle. At 5-minute intervals, that's ~1,152 executions per day — well within the Starter plan limits.
  • n8n self-hosted: Free. Only costs are your hosting infrastructure.

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.