---
title: "Poll the change feed"
method: GET
path: "/events"
tags: ["Events"]
---

# Poll the change feed

`GET /events`

## Request

- Base URL: `https://tasg.app/api/v1`
- URL: `https://tasg.app/api/v1/events`
- Auth: HTTP bearer

Every change in a workspace, oldest first, as a cursor-paginated stream: the building block for one-way sync and the same stream webhooks deliver. Visibility matches the rest of the API: only events in projects you belong to (workspace-level events are visible to any workspace member). Events are retained for 30 days.

**Sync loop:** call without `since` once, store `next_cursor`, then poll with `since=<next_cursor>`; repeat immediately while `has_more` is true. Cursors are per instance, not per workspace, so gaps between consecutive cursors are normal.

## Query parameters

- `workspace_id` string, uuid, required
- `since` string: Return only events after this cursor (exclusive).
- `project_id` string, uuid
- `types` string: Comma-separated list, e.g. `task.created,task.completed`.
- `limit` integer

## Response `200`

A page of events.

- object
  - `events` Event[], required
    - `id` string, uuid, required
    - `cursor` string, required: Opaque, monotonically increasing position. Pass the last one you processed as `since`.
    - `type` 'task.created' | 'task.updated' | 'task.completed' | 'task.reopened' | 'task.deleted' | 'task.dependency_added' | 'task.dependency_removed' | 'comment.created' | 'comment.updated' | 'comment.deleted' | 'attachment.added' | 'attachment.deleted' | 'section.created' | 'section.updated' | 'section.deleted' | 'project.created' | 'project.updated' | 'project.deleted' | 'project.member_added' | 'project.member_removed' | 'tag.created' | 'tag.deleted' | 'category.created' | 'category.deleted' | 'workspace.agent_enabled' | 'workspace.agent_disabled', required: Dotted `<resource>.<verb>` names. New types may be added; consumers should ignore unknown ones.
    - `occurred_at` string, date-time, required
    - `workspace_id` string, uuid, required
    - `project_id` string, uuid, nullable: Null for workspace-level events (tags, categories).
    - `task_id` string, uuid, nullable
    - `actor` object, nullable
      - `id` string, uuid, required
      - `full_name` string, required
      - `avatar_url` string, uri, nullable
    - `via` 'assistant' | 'mcp', nullable: Set when an AI channel performed the action on the actor's behalf; null for direct actions.
    - `data` object, required: Type-specific payload. `task.*` events carry `task` (the same shape as GET /tasks/{id} minus sub-collections) and, for `task.updated`, `changes` = `{ field: { old, new } }` plus an optional `cause` (`dependency_completed`, `dependency_removed`, `date_shift`). `comment.*` carry `comment`; `section.*` carry `section`; `project.*` carry `project` / `member`; `attachment.*` carry `attachment`; `tag.*` / `category.*` carry `tag` / `category`. `*.deleted` payloads contain identifiers and names only.
  - `next_cursor` string, nullable, required: Pass as `since` next time. Null only when there are no events at all yet.
  - `has_more` boolean, required: `true` means call again immediately with `since=next_cursor`.
  - `as_of` string, date-time, required: Server time when the page was produced.

## Other responses

- `400`: Validation failed: the body or query did not match the schema, or a business rule was violated. `error` says which.
- `401`: Missing, invalid, expired or revoked credentials.
- `403`: Authenticated, but not allowed: not a member, insufficient project role or the API token lacks the required scope.
- `429`: Rate limited. Back off and retry after the window in the `RateLimit-*` headers.

---

[API](https://skmtc.dev/tasg/apis/tasg-api.md) · [All operations](https://skmtc.dev/tasg/apis/tasg-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tasg/apis/tasg-api/revisions/cd2a1dbaf5cf?raw)
