Joseph HENRY b749d3d296 Trim skill descriptions and drop dead reference
Tighten flamenco-api and kitsu-api descriptions per Anthropic's "concise
is key" guidance: drop redundant trigger-list sentences and collapse to a
single "what + when" line. Also remove the flamenco-api reference to
usage-docs.md, which was never bundled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:39:47 +02:00

12 KiB

name, description
name description
kitsu-api Kitsu production tracking API (Zou backend) and gazu Python SDK reference. Use when integrating with Kitsu/CGWire, debugging API calls, building tools that talk to the tracker, or working with shots, assets, tasks, comments, previews, playlists, or casting. Covers the REST API (493 endpoints) and the gazu wrapper.

Kitsu API & gazu SDK Reference

Kitsu is an open-source production tracking tool for animation/VFX studios, built by CGWire. The backend is called Zou and exposes a REST API. gazu is the official Python client SDK that wraps this API.

Quick Reference

Authentication

User Login (email/password → JWT)

import gazu

gazu.set_host("https://kitsu.mystudio.com/api")
gazu.log_in("user@studio.com", "password")

Under the hood this POSTs to /auth/login with {"email": "...", "password": "..."} and receives a JWT access token + refresh token.

Bot Authentication (pre-generated token)

Bots are non-physical users that don't count against subscription seats. Create them in Admin > Bots. Use the token directly:

gazu.set_host("https://kitsu.mystudio.com/api")
gazu.set_token("bot_jwt_token_here")

Token Lifecycle

  • Access token: Short-lived JWT, passed as Authorization: Bearer <token> header
  • Refresh token: Use GET /auth/refresh-token to get a new access token
  • Logout: GET /auth/logout
  • Check auth: GET /auth/authenticated (200 = valid, 401 = expired)

2FA Support

Kitsu supports TOTP, email OTP, FIDO/WebAuthn, and recovery codes. See /auth/totp, /auth/email-otp, /auth/fido endpoints.

ADM Pipeline Authentication

In the ADM Gadget pipeline, credentials come from environment variables set by adm_env.yml:

  • TRACKER_URL — Kitsu instance URL (must end with /api)
  • TRACKER_LOGIN — email
  • TRACKER_PASSWORD — password
  • TRACKER_PROJECT — project name to connect to

The Kitsu adapter class (gadget/src/gadget/resources/trackers/kitsu.py) wraps gazu and handles connection/reconnection.

Core Concepts

Entity Hierarchy

Project
├── Episodes (optional, for TV shows)
│   └── Sequences
│       └── Shots
├── Sequences (for shorts/features)
│   └── Shots
├── Asset Types
│   └── Assets
├── Edits
├── Concepts
└── Scenes

Data Model

Every entity in Kitsu is a dict with at minimum:

  • id — UUID string
  • name — display name
  • created_at / updated_at — ISO timestamps
  • data — custom metadata dict (called custom_data in Gadget after normalization)
  • project_id — parent project UUID

Key Relationships

  • Shot → belongs to Sequence (via parent_id) → belongs to Episode (optional)
  • Asset → belongs to Asset Type (via entity_type_id)
  • Task → belongs to Entity (shot/asset) + Task Type + has Task Status
  • Comment → belongs to Task, may have Preview Files and Attachments
  • Casting → links Assets to Shots (with nb_occurences)

gazu SDK Patterns

Project Operations

# List/find projects
projects = gazu.project.all_projects()
open_projects = gazu.project.all_open_projects()
project = gazu.project.get_project_by_name("My Project")
project = gazu.project.get_project(project_id)

# Create project
project = gazu.project.new_project("Name", production_type="short")  # short|featurefilm|tvshow

# Team management
gazu.project.add_person_to_team(project, person)
gazu.project.remove_person_from_team(project, person)
team = gazu.project.get_team(project)

Shot Operations

# Episodes (TV shows only)
episode = gazu.shot.new_episode(project, "E01")
episodes = gazu.shot.all_episodes_for_project(project)

# Sequences
sequence = gazu.shot.new_sequence(project, "SQ010", episode=episode)
sequences = gazu.shot.all_sequences_for_project(project)

# Shots
shot = gazu.shot.new_shot(project, sequence, "SH0010",
                          nb_frames=100, frame_in=101, frame_out=200,
                          data={"custom_field": "value"})
shot = gazu.shot.get_shot(shot_id)
shot = gazu.shot.get_shot_by_name(sequence, "SH0010")
shots = gazu.shot.all_shots_for_sequence(sequence)
gazu.shot.update_shot(shot)  # after modifying dict fields
gazu.shot.remove_shot(shot_id, force=True)

Asset Operations

# Asset types
asset_type = gazu.asset.new_asset_type("Character")
asset_types = gazu.asset.all_asset_types_for_project(project)

# Assets
asset = gazu.asset.new_asset(project, asset_type, "Hero",
                             description="Main character",
                             extra_data={"variant": "default"})
asset = gazu.asset.get_asset(asset_id)
asset = gazu.asset.get_asset_by_name(project, "Hero", asset_type)
assets = gazu.asset.all_assets_for_project(project)
gazu.asset.remove_asset(asset_id, force=True)

Task Operations

# Task types & statuses
task_type = gazu.task.get_task_type_by_name("Animation")
wip = gazu.task.get_task_status_by_short_name("wip")
done = gazu.task.get_task_status_by_short_name("done")

# Create tasks
task = gazu.task.new_task(entity, task_type, task_status=wip, assignees=[person])

# Find tasks
tasks = gazu.task.all_tasks_for_shot(shot)
tasks = gazu.task.all_tasks_for_asset(asset)
task = gazu.task.get_task_by_name(entity, task_type)  # get_task_by_entity in some versions

# Assignment
gazu.task.assign_task(task, person)

# Time tracking
gazu.task.set_time_spent(task, person, "2024-03-18", duration=8*3600)  # seconds
gazu.task.add_time_spent(task, person, "2024-03-18", duration=3600)

# Scheduling
task["start_date"] = "2024-03-01"
task["due_date"] = "2024-03-15"
task["estimation"] = 5 * 8 * 3600  # 5 days in seconds
gazu.task.update_task(task)

Comments & Publishing

# Add comment (also changes task status)
comment = gazu.task.add_comment(task, task_status,
                                comment="Looking good!",
                                person=person,
                                attachments=["/path/to/file.pdf"],
                                created_at=None)

# Add preview to comment
preview = gazu.task.add_preview(task, comment, "/path/to/render.mp4")
gazu.task.set_main_preview(preview)  # set as entity thumbnail

# One-step publish shortcut
comment, preview = gazu.task.publish_preview(task, wip,
    comment="WIP update",
    preview_file_path="/path/to/file.mp4")

# Read comments
comments = gazu.task.all_comments_for_task(task)
last = gazu.task.get_last_comment_for_task(task)

# Download previews
gazu.files.download_preview_file(preview, "/path/to/output.mp4")
gazu.files.download_preview_file_thumbnail(preview, "/path/to/thumb.png")

Casting (Breakdown)

# Get casting
casting = gazu.casting.get_shot_casting(shot)       # assets in a shot
cast_in = gazu.casting.get_asset_cast_in(asset)      # shots using an asset

# Update casting
gazu.casting.update_shot_casting(project, shot_id, [
    {"asset_id": asset1_id, "nb_occurences": 2},
    {"asset_id": asset2_id, "nb_occurences": 1},
])

Persons & Teams

person = gazu.person.new_person("John", "Doe", "john@studio.com",
                                 role="user", departments=[dept])
persons = gazu.person.all_persons()
person = gazu.person.get_person_by_full_name("John Doe")
gazu.person.add_person_to_department(person, department)

Playlists

playlist = gazu.playlist.new_playlist(project, "Daily Review",
                                       for_client=False, for_entity="shot")
gazu.playlist.add_entity_to_playlist(playlist, entity, preview_file=preview)

Event Listeners (Real-time)

gazu.set_event_host("https://kitsu.mystudio.com")  # note: no /api suffix
event_client = gazu.events.init()

def on_task_status_changed(data):
    print(f"Task {data['task_id']} status changed")

gazu.events.add_listener(event_client, "task:status-changed", on_task_status_changed)
gazu.events.run_client(event_client)  # blocks current thread

Event naming: entity:action (e.g., asset:new, shot:update, comment:new, task:assign). Most entities emit new, update, delete. See references/events.md for the full list.

Caching

gazu.cache.enable()       # enable in-memory cache for all read ops
gazu.cache.disable()
gazu.cache.clear_all()

# Per-function control
gazu.asset.all_assets.set_expire(120)  # seconds
gazu.asset.all_assets.clear_cache()
gazu.asset.all_assets.disable_cache()

Low-level Client

When gazu doesn't wrap an endpoint, use the raw client:

# GET
data = gazu.client.get("data/projects")
data = gazu.client.fetch_all("tasks", {"project_id": pid, "task_type_id": ttid})
data = gazu.client.fetch_one("projects", project_id)
data = gazu.client.fetch_first("projects", {"name": "MyProject"})

# POST
result = gazu.client.post("data/projects", {"name": "New Project"})

# PUT
result = gazu.client.put(f"data/entities/{entity_id}", updated_data)

# Pagination
page1 = gazu.client.fetch_all("tasks?page=1")  # 100 per page
# Full-text search
results = gazu.search.search_entities("bird")
# Returns: {"persons": [...], "assets": [...], "shots": [...]}

# REST API search
results = gazu.client.post("data/search", {
    "query": "hero",
    "project_id": project_id,
    "limit": 10,
    "offset": 0,
    "index_names": ["assets"]  # filter to specific entity types
})

Roles & Permissions

Role Access Level
Admin (Studio Manager) Full read/write to all productions and settings
Manager (Production Manager) Create assets/shots, manage tasks, post comments — no studio settings
Supervisor (Department Lead) Read/write within assigned departments
User (Artist) Comment/upload on assigned tasks only
Vendor Like User but can only see assigned tasks
Client View-only on assigned productions, client playlists only

Custom Actions

Custom Actions trigger HTTP POST requests from the Kitsu web UI to your endpoint. Created by admins in Admin > Custom Actions.

Payload sent to your endpoint:

{
  "personid": "uuid",
  "personemail": "user@studio.com",
  "projectid": "uuid",
  "currentpath": "/productions/{id}/assets",
  "currentserver": "kitsu.mystudio.com",
  "selection": ["task_uuid_1", "task_uuid_2"],
  "entitytype": "asset"
}

ADM Gadget Integration

The Gadget pipeline's Kitsu adapter (gadget/src/gadget/resources/trackers/kitsu.py) wraps gazu with these conventions:

  • ID normalization: get_id() accepts strings (UUID), dicts (extracts ["id"]), or Gadget entity objects (uses .id)
  • Data normalization: _norm_data() renames datacustom_data to avoid collision with Python dict usage
  • Task normalization: retake_countnb_retakes, for_entityfor_shots boolean
  • Comment normalization: attachment_filesattachments, adds index-based id to checklist items, renames preview original_namename
  • Shot creation: Uses raw gazu.client.post() because gazu's new_shot didn't support all fields needed

Key Gadget patterns:

# Gadget entity-level usage
project = Project.from_env()                    # loads from env vars
shot = project.shots.get(name="sq170_sh0224")
task = shot.tasks.fetch("Anim3D")
task.comments.new(comment="...", preview=["/path/to/img.png"], set_main_preview=True)

Detailed References

For the complete endpoint catalog (all 493 endpoints), see references/endpoints.md. For the full event types list, see references/events.md.

When you need details about a specific endpoint's parameters or response format, fetch the OpenAPI spec:

curl -s https://api-docs.kitsu.cloud/source.json | python3 -m json.tool

Or read it programmatically:

import json, urllib.request
spec = json.loads(urllib.request.urlopen("https://api-docs.kitsu.cloud/source.json").read())
endpoint = spec["paths"]["/data/shots/{shot_id}"]