REST API: expose tickets for a specific overview via agent token

Problem

GET /api/v1/ticket_overviews returns the overview list with counts, but there is no way for an external client to fetch the actual tickets belonging to a specific overview using an agent-level API token.

All three possible workarounds fail:

Approach Result
GET /api/v1/tickets?view={link} view= parameter is silently ignored — returns all tickets
GET /api/v1/overviews/{id} HTTP 403 for agent tokens (admin-only)
Port 6042 WebSocket Uses session cookie auth, not API token; not publicly routed through nginx

This means any third-party native client (macOS, iOS, Android) authenticated with a user API token is forced to reconstruct overview conditions from scratch using heuristic Elasticsearch queries. The results are inaccurate for any custom or condition-based overview.

Proposed solution

Option A — make view= work on the existing endpoint:

GET /api/v1/tickets?view={link}&per_page=50&expand=true

Option B — new dedicated endpoint:

GET /api/v1/ticket_overviews/{id}/tickets?per_page=50&page=1&expand=true

Requirements:

  • Accept standard API token auth (agent scope — no admin token needed)
  • Apply the overview’s server-side conditions exactly as the web UI does
  • Support pagination (page, per_page)
  • Support expand=true for inline field resolution

Why this matters

Zammad’s web frontend works because it connects to the internal WebSocket server (port 6042, session cookie auth). External API clients have no equivalent path.

Any developer building a native Zammad client hits this wall immediately. Currently required workaround:

  1. Parse overview name for bracket patterns (e.g. [base72]) to guess group IDs
  2. Map known link values (my_assigned, all_open) to hand-crafted Elasticsearch queries
  3. Apply client-side state filters — state_id:X in /api/v1/tickets/search behaves as full-text, not a field filter
  4. Accept that condition-based overviews (tag filters, escalation rules, custom conditions) cannot be replicated at all

This is fragile, incomplete, and wrong for any non-trivial overview.

Version

Tested on self-hosted Zammad (recent release, 6.x branch).

Two things:

  • this is technically not a feature request and if it would be, the template shouldn’t be ignored. I moved it to technical question.
  • Because you’re throwing a microwave text at us, I’m throwing one back
⏺ Here's how to do it with two independent API calls:

  1. List all overviews

  GET /api/v1/ticket_overviews

  Returns a navbar-style list with id, name, priority, link, and ticket count per overview. Use this to enumerate overviews and grab their link
  identifiers.

  2. Get tickets for a specific overview

  GET /api/v1/ticket_overviews?view=<link>

  Pass the link value from step 1 as the view query parameter. The response includes an assets object with the matching tickets.

  ---
  Example flow:

  # Step 1: List overviews
  curl -u user@example.com:password \
    https://your-zammad.example/api/v1/ticket_overviews

  # Step 2: Get tickets for a specific overview (e.g. link = "my_assigned_tickets")
  curl -u user@example.com:password \
    "https://your-zammad.example/api/v1/ticket_overviews?view=my_assigned"

  Authentication options: HTTP Basic (user:password), or token-based via Authorization: Token token=<your-token>.

  Key notes:
  - Results are permission-scoped to the authenticated user automatically.
  - The overviews endpoint (GET /api/v1/overviews) also exists but returns overview definitions (conditions, roles, etc.) — not the tickets themselves.
  - Sorting can be controlled with order_by and order_direction parameters on the ticket_overviews call.

I verified it, it works. Have fun.

1 Like

thank you for the quick response!!

Ran into this exact wall building a native Zammad client too — a practical follow-up to @MrGeneration’s answer (which is correct: GET /api/v1/ticket_overviews?view=<link>).

The response isn’t a flat ticket array; it’s split into an index and an assets bag:

{
  "index": {
    "overview": { "...": "order, group_by, visible columns" },
    "tickets":  [ { "id": 123, "updated_at": "..." } ],
    "count":    42
  },
  "assets": {
    "Ticket":         { "123": { "...": "full ticket" } },
    "User":           { },
    "TicketState":    { },
    "TicketPriority": { },
    "Group":          { },
    "Organization":   { }
  }
}

Client flow:

  1. Take the ordered ID list from index.tickets — it preserves the overview’s server-side sort, so keep that order, don’t re-sort.
  2. Resolve each id against assets.Ticket["<id>"] for the full object.
  3. Resolve foreign keys from the other bags:
Field on ticket Asset bag
group_id assets.Group
state_id assets.TicketState
priority_id assets.TicketPriority
owner_id / customer_id assets.User
organization_id assets.Organization

index.count is the total.

Auth: a plain agent token is enough — no admin scope — and results come back already permission-scoped to that user:

Authorization: Token token=<your-token>

The overview’s own default sort is in index.overview.order; order_by / order_direction override it as you mentioned.

One gotcha that cost me time: the assets bag isn’t always complete. In my case assets.Group and assets.TicketPriority were sometimes absent, so resolving group_id / priority_id from assets alone came back blank. I backfill them with separate GET /api/v1/groups and GET /api/v1/ticket_priorities calls and merge. Worth knowing before you trust the bag to be exhaustive.

Net result: exactly what the web UI shows, including condition-based overviews, with zero Elasticsearch reconstruction.

1 Like