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>
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
- API docs: https://api-docs.kitsu.cloud/
- OpenAPI spec: https://api-docs.kitsu.cloud/source.json
- Developer guides: https://dev.kitsu.cloud/
- gazu docs: https://gazu.cg-wire.com/
- Zou source: https://github.com/cgwire/zou
- gazu source: https://github.com/cgwire/gazu
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-tokento 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— emailTRACKER_PASSWORD— passwordTRACKER_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 stringname— display namecreated_at/updated_at— ISO timestampsdata— custom metadata dict (calledcustom_datain 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
Search
# 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()renamesdata→custom_datato avoid collision with Python dict usage - Task normalization:
retake_count→nb_retakes,for_entity→for_shotsboolean - Comment normalization:
attachment_files→attachments, adds index-basedidto checklist items, renames previeworiginal_name→name - Shot creation: Uses raw
gazu.client.post()because gazu'snew_shotdidn'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}"]