Route refund requests from Gorgias using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code, Cursor, or another AI coding agent that supports skills
  • GORGIAS_EMAIL — your Gorgias account email
  • GORGIAS_API_KEY — your Gorgias REST API key (Settings → API → REST API)
  • GORGIAS_DOMAIN — your store subdomain (e.g., your-store)
  • ANTHROPIC_API_KEY — your Anthropic API key

Overview

This approach uses an agent skill that fetches recent unrouted tickets from Gorgias, passes each one to Claude (claude-haiku-4-5) to determine whether it's a refund request, and if so assigns it to your refund team and applies a tag via the Gorgias API. Because Claude reads the full message body, it catches refund requests phrased conversationally — "this isn't what I ordered and I want my money" — that keyword Rules would miss.

Step 1: Create the skill directory

mkdir -p .claude/skills/refund-routing/scripts

Step 2: Write the SKILL.md

Create .claude/skills/refund-routing/SKILL.md:

---
name: refund-routing
description: Identifies refund and return requests in unrouted Gorgias tickets using Claude, then assigns them to the refund team and applies a tag via the Gorgias API.
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Route refund requests to authorized agents:
 
1. Run: `python $SKILL_DIR/scripts/route_refunds.py`
2. Review the output — it lists each ticket classified and whether it was routed
3. Spot-check a few in Gorgias to verify accuracy

Step 3: Write the routing script

Create .claude/skills/refund-routing/scripts/route_refunds.py:

#!/usr/bin/env python3
"""
Gorgias Refund Request Routing
Fetches unrouted open tickets → classifies refund intent with Claude → routes to refund team.
"""
import os
 
try:
    import requests
    from anthropic import Anthropic
except ImportError:
    os.system("pip install requests anthropic -q")
    import requests
    from anthropic import Anthropic
 
GORGIAS_EMAIL  = os.environ["GORGIAS_EMAIL"]
GORGIAS_KEY    = os.environ["GORGIAS_API_KEY"]
GORGIAS_DOMAIN = os.environ["GORGIAS_DOMAIN"]
BASE_URL       = f"https://{GORGIAS_DOMAIN}.gorgias.com/api"
AUTH           = (GORGIAS_EMAIL, GORGIAS_KEY)
 
REFUND_TEAM    = "Refund Team"  # Change to match your team name
REFUND_TAG     = "refund-request"
 
client = Anthropic()
 
 
def get_unrouted_tickets(limit: int = 50) -> list:
    """Fetch open tickets that haven't been assigned to a team yet."""
    resp = requests.get(
        f"{BASE_URL}/tickets",
        auth=AUTH,
        params={"status": "open", "limit": limit},
    )
    resp.raise_for_status()
    tickets = resp.json().get("data", [])
    return [
        t for t in tickets
        if not t.get("assignee_team")
        and not any(tag["name"] == REFUND_TAG for tag in (t.get("tags") or []))
    ]
 
 
def classify_refund_intent(subject: str, body: str) -> dict:
    """Use Claude to determine if a ticket is a refund/return request."""
    prompt = (
        "You are a support ticket classifier. Determine if this ticket is a "
        "refund or return request.\n\n"
        f"Subject: {subject}\n"
        f"Body (first 500 chars): {body[:500]}\n\n"
        "Reply with exactly one of:\n"
        "- REFUND — customer wants money back, a refund, store credit, or reimbursement\n"
        "- RETURN — customer wants to return, exchange, or replace an item\n"
        "- NOT_REFUND — ticket is not about refunds or returns\n\n"
        "Reply with only the label, nothing else."
    )
    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=20,
        messages=[{"role": "user", "content": prompt}],
    )
    label = message.content[0].text.strip().upper()
    is_refund = label in ("REFUND", "RETURN")
    return {"is_refund": is_refund, "label": label}
 
 
def route_ticket(ticket_id: int, existing_tags: list) -> None:
    """Assign the ticket to the refund team and apply the refund tag."""
    merged_tags = list({t["name"] for t in existing_tags} | {REFUND_TAG})
    resp = requests.put(
        f"{BASE_URL}/tickets/{ticket_id}",
        auth=AUTH,
        json={
            "tags": [{"name": t} for t in merged_tags],
            "assignee_team": {"name": REFUND_TEAM},
        },
    )
    resp.raise_for_status()
 
 
def main() -> None:
    print("Fetching unrouted open tickets...")
    tickets = get_unrouted_tickets()
    print(f"Found {len(tickets)} unrouted tickets to check\n")
 
    if not tickets:
        print("No unrouted tickets to process.")
        return
 
    routed = 0
    for ticket in tickets:
        subject  = ticket.get("subject", "")
        messages = ticket.get("messages", [])
        body     = messages[0].get("body_text", "") if messages else ""
 
        result = classify_refund_intent(subject, body)
 
        if result["is_refund"]:
            route_ticket(ticket["id"], ticket.get("tags", []))
            routed += 1
            print(f"  #{ticket['id']}  {subject[:60]!r}  →  routed ({result['label']})")
        else:
            print(f"  #{ticket['id']}  {subject[:60]!r}  →  skipped (not a refund)")
 
    print(f"\nRouted {routed} of {len(tickets)} tickets to {REFUND_TEAM}.")
 
 
if __name__ == "__main__":
    main()
Customize the refund team name

Update the REFUND_TEAM variable at the top of the script to match the exact team name in your Gorgias account. You can find your team names under Settings → Teams.

Step 4: Run the skill

# Via Claude Code
/refund-routing
 
# Or run the script directly
python .claude/skills/refund-routing/scripts/route_refunds.py

The script processes up to 50 unrouted open tickets per run. A typical run looks like:

Fetching unrouted open tickets...
Found 12 unrouted tickets to check
 
  #14301  'I want a refund for order #7821'       →  routed (REFUND)
  #14305  'Where is my package?'                   →  skipped (not a refund)
  #14308  'This arrived broken, I want my money'   →  routed (REFUND)
  #14312  'Can I exchange this for a different size' →  routed (RETURN)
  #14315  'How do I update my address?'            →  skipped (not a refund)
 
Routed 3 of 5 tickets to Refund Team.

Step 5: Schedule it

Option A: Cron

# crontab -e — run every 30 minutes during business hours
*/30 8-18 * * 1-5 cd /path/to/project && python .claude/skills/refund-routing/scripts/route_refunds.py

Option B: GitHub Actions

name: Refund Request Routing
on:
  schedule:
    - cron: '*/30 13-22 * * 1-5'  # 8 AM–5 PM ET, weekdays
  workflow_dispatch: {}
jobs:
  route:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install requests anthropic
      - run: python .claude/skills/refund-routing/scripts/route_refunds.py
        env:
          GORGIAS_EMAIL:    ${{ secrets.GORGIAS_EMAIL }}
          GORGIAS_API_KEY:  ${{ secrets.GORGIAS_API_KEY }}
          GORGIAS_DOMAIN:   ${{ secrets.GORGIAS_DOMAIN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Refund requests are time-sensitive — schedule frequently

Refund tickets have higher urgency than general triage. If you're using a scheduled approach, run every 15–30 minutes during business hours rather than hourly. For real-time routing, combine this with Gorgias native Rules for common keywords and use the skill to catch the edge cases.

Cost

  • Claude Haiku: ~$0.001 per ticket classified
  • 500 tickets checked/month (not all will be refunds) ≈ $0.50 in API costs
  • Only unrouted tickets are checked, so costs stay low even at scale

Need help implementing this?

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