timeline
GET /timeline
The sequences the caller may read, newest first.
Q11. The caller's own, and every studio's when it holds `gallery.read_all`. Deleted sequences are not listed.
Needs the timeline capability.
| Go | List(ctx context.Context, params *TimelineListParams) (*TimelinePage, error) |
|---|---|
| Python | def list(self, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any |
| JavaScript | list(params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| limit | query | integer | |
| cursor | query | string | The `next_cursor` of the previous page, unchanged. |
| Status | Body | Means |
|---|---|---|
| 200 | TimelinePage | A page of sequences. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
POST /timeline
Create a sequence from clips, or from whole tracks.
R44, R45, 05 §7. Either `clips`, which are laid end to end from 0 on V1 for video and images and on A1 for sound, or `tracks` — not both. Neither makes an empty sequence, which is valid to store and cannot be exported until it has a video clip. `target.sample_rate` defaults to 48000 (Q9). Every clip must name an asset the caller may read (Q11); one it may not is 422 `invalid_timeline`, worded the same whether or not the asset exists.
Needs the timeline capability.
| Go | Create(ctx context.Context, body TimelineCreate) (*Timeline, error) |
|---|---|
| Python | def create(self, body: Dict[str, Any]) -> Any |
| JavaScript | create(body) |
Request body: TimelineCreate (application/json)
| Status | Body | Means |
|---|---|---|
| 201 | Timeline | The new sequence, as it was stored after snapping. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |
GET /timeline/{id}
One sequence. One the caller may not read is 404.
Needs the timeline capability.
| Go | Get(ctx context.Context, id string) (*Timeline, error) |
|---|---|
| Python | def get(self, id: str) -> Any |
| JavaScript | get(id) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID |
| Status | Body | Means |
|---|---|---|
| 200 | Timeline | The sequence. `ETag` is its revision. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
PATCH /timeline/{id}
Edit a sequence. Every edit is a revision.
05 §6, Q10. A JSON merge patch over `name`, `target` and `tracks`; `target` members merge, and `tracks` replaces. `If-Match` is required, and a stale one is 409 `etag_mismatch`. The document the patch produces is validated and snapped as a whole, and the document it replaces is kept as the previous revision. A clip this write adds must name an asset the caller may read; one already in the sequence may stay (Q11).
Needs the timeline capability.
| Go | Update(ctx context.Context, id string, body map[string]any, params *TimelineUpdateParams) (*Timeline, error) |
|---|---|
| Python | def update(self, id: str, body: Dict[str, Any], *, if_match: Any) -> Any |
| JavaScript | update(id, body, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| If-Match | header · required | string | The revision last read, as the `ETag` header gave it. Required on a timeline write, because a merge patch replaces `tracks` whole and a blind write would drop another editor's clips (docs/decisions.md M8 Q10). A stale one is 409 `etag_mismatch`. |
Request body: TimelineUpdate (application/merge-patch+json)
| Status | Body | Means |
|---|---|---|
| 200 | Timeline | The sequence as it was stored. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 409 | Error | Codes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`. |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |
DELETE /timeline/{id}
Delete a sequence. Its exports and their lineage stay.
Q12. A soft delete: the row stays, so an exported item keeps naming it, and its clips stop holding their footage against reclaim. There is no restore. `If-Match` is checked when sent.
Needs the timeline capability.
| Go | Delete(ctx context.Context, id string, params *TimelineDeleteParams) error |
|---|---|
| Python | def delete(self, id: str, *, if_match: Optional[Any] = None) -> Any |
| JavaScript | delete(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| If-Match | header | string | The etag last read. When given and stale, the write is refused with 409 `etag_mismatch`. |
| Status | Body | Means |
|---|---|---|
| 204 | Deleted. | |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 409 | Error | Codes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`. |
GET /timeline/{id}/exports
The sequence's export jobs, newest first.
Q17. Under `timeline`, so a studio that exports need not also hold `jobs` to watch what it started. The same jobs appear in `/jobs` for a studio that does hold it.
Needs the timeline capability.
| Go | Exports(ctx context.Context, id string, params *TimelineExportsParams) (*JobPage, error) |
|---|---|
| Python | def exports(self, id: str, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any |
| JavaScript | exports(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| limit | query | integer | |
| cursor | query | string | The `next_cursor` of the previous page, unchanged. |
| Status | Body | Means |
|---|---|---|
| 200 | JobPage | A page of export jobs. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
POST /timeline/{id}/exports/{job}:cancel
Stop a running export and leave nothing behind.
Q16, Q17. The ffmpeg leading the export's process group is stopped and its partial file removed; the job ends `cancelled`. A finished job is 409 `job_finished`. The launcher's `POST /launcher/jobs/{id}:cancel` reaches the same runner.
Needs the timeline capability.
| Go | CancelExport(ctx context.Context, id string, job string) error |
|---|---|
| Python | def cancel_export(self, id: str, job: str) -> Any |
| JavaScript | cancelExport(id, job) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| job | path · required | ULID |
| Status | Body | Means |
|---|---|---|
| 204 | Cancellation requested. | |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 409 | Error | Codes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`. |
GET /timeline/{id}/revisions
The sequence's earlier revisions, newest first.
Q10. Each write stores the document it replaced; the newest 100 are kept. Undo is `:revert`.
Needs the timeline capability.
| Go | Revisions(ctx context.Context, id string, params *TimelineRevisionsParams) (*TimelineRevisionPage, error) |
|---|---|
| Python | def revisions(self, id: str, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any |
| JavaScript | revisions(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| limit | query | integer | |
| cursor | query | string | The `next_cursor` of the previous page, unchanged. |
| Status | Body | Means |
|---|---|---|
| 200 | TimelineRevisionPage | A page of revisions. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
POST /timeline/{id}:export
Export the sequence. Answers with the job that renders it.
R46, R48, Q16, Q17. Export is a Job, so progress, cancellation and the log are the ones that already exist. The output is written where nothing looks for it and becomes an asset only after ffmpeg exits 0 and the file is probed against the target; then it is adopted, and recorded as a gallery item carrying `timeline_id` with one `clip` input per distinct asset. A sequence with no video clip is 422 `invalid_timeline`. A clip the caller may no longer read is 422. One export at a time per sequence: another is 409 `already_exporting`. Needs ffmpeg (501 `unsupported`).
Needs the timeline capability.
| Go | Export(ctx context.Context, id string, body ExportRequest) (*Job, error) |
|---|---|
| Python | def export(self, id: str, body: Dict[str, Any]) -> Any |
| JavaScript | export(id, body) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID |
Request body: ExportRequest (application/json)
| Status | Body | Means |
|---|---|---|
| 202 | Job | The export job. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 409 | Error | Codes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`. |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |
| 501 | Error | This provider cannot serve this. Code `unsupported`, with a reason in `message`. |
POST /timeline/{id}:open
Ask the framework to show its editor on this sequence.
R45, 05 §5a, §7, Q19. The data operation always works and only the presentation degrades: until the launcher has a Timeline screen, which waits for M9's cookie (Q20), this answers 501 `unsupported` with a reason, and a studio hides its button. From then it publishes the sequence to a connected launcher page and answers `{opened, surface}`, or 501 when no page is connected.
Needs the timeline capability.
| Go | Open(ctx context.Context, id string) (*TimelineOpened, error) |
|---|---|
| Python | def open(self, id: str) -> Any |
| JavaScript | open(id) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID |
| Status | Body | Means |
|---|---|---|
| 200 | TimelineOpened | A framework surface opened the sequence. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 501 | Error | This provider cannot serve this. Code `unsupported`, with a reason in `message`. |
GET /timeline/{id}:plan
Which path an export would take, and why.
R47, Q13. Every video clip is probed, and the answer is `copy` when every one of them matches the target and each other — codec, profile, level, size, aspect ratio, field order, pixel format, time base, frame rate, colour tags and parameter sets — and is used whole, with no hold, image or transition. Otherwise `conform`, with a reason per clip. The picture is copied; sound always goes through the conform graph, because a copied AAC stream carries its own priming and padding into every cut. So the chip reads "video stream copy", not "no re-encode". Needs ffmpeg: without it, 501 `unsupported` with `details.tool`.
Needs the timeline capability.
| Go | Plan(ctx context.Context, id string, params *TimelinePlanParams) (*ExportPlan, error) |
|---|---|
| Python | def plan(self, id: str, *, preset: Optional[Any] = None) -> Any |
| JavaScript | plan(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| preset | query | ExportPreset |
| Status | Body | Means |
|---|---|---|
| 200 | ExportPlan | The plan. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |
| 501 | Error | This provider cannot serve this. Code `unsupported`, with a reason in `message`. |
POST /timeline/{id}:revert
Write an earlier revision back as the newest one.
Q10. Undo is the previous revision, written forward rather than rewound, so nothing is lost. A revision naming an asset that has since been reclaimed, or one the caller may not read, is refused with the clips named (422 `invalid_timeline`). `If-Match` is required.
Needs the timeline capability.
| Go | Revert(ctx context.Context, id string, body TimelineRevert, params *TimelineRevertParams) (*Timeline, error) |
|---|---|
| Python | def revert(self, id: str, body: Dict[str, Any], *, if_match: Any) -> Any |
| JavaScript | revert(id, body, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| If-Match | header · required | string | The revision last read, as the `ETag` header gave it. Required on a timeline write, because a merge patch replaces `tracks` whole and a blind write would drop another editor's clips (docs/decisions.md M8 Q10). A stale one is 409 `etag_mismatch`. |
Request body: TimelineRevert (application/json)
| Status | Body | Means |
|---|---|---|
| 200 | Timeline | The sequence, at its new revision. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 409 | Error | Codes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`. |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |
POST /timeline:append
Add one asset to the end of a track, without stealing focus.
R45, Q19. With `timeline_id`, that sequence; without one, the most recently updated sequence the caller owns, and 409 `no_timeline` when it owns none. `track` names a track by kind and position — `V1`, `A1`, `A2` — and defaults to `V1` for a video or image asset and `A1` for sound. It applies to whatever the current revision is, so it needs no `If-Match`.
Needs the timeline capability.
| Go | Append(ctx context.Context, body TimelineAppend) (*Timeline, error) |
|---|---|
| Python | def append(self, body: Dict[str, Any]) -> Any |
| JavaScript | append(body) |
Request body: TimelineAppend (application/json)
| Status | Body | Means |
|---|---|---|
| 200 | Timeline | The sequence, with the clip appended. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 409 | Error | Codes: `etag_mismatch`, `name_taken`, `job_finished`, `already_running`, `not_running`, `port_conflict`, `heavy_conflict` (with `details.heavy`), `not_installed`, `conflict`, `in_use`, `missing`, `confirm_required` and `preview_changed` (with `details.preview`), `not_reclaimable`, and from M7: `approval_required` (with `details.approval`), `id_taken`, `not_fetched`, `already_exists`. |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |