Skip to content

Videos

Videos are file-based widgets that display and play back video content on a Canvus canvas. Use videos for presentations, recorded demos, tutorials, or any motion content that needs to live alongside other canvas widgets.

Video files are uploaded via multipart form data and can be downloaded through a dedicated endpoint. The API provides full playback control including play, pause, stop, and seek, as well as mute/unmute. The server also exposes the video duration once a Canvus desktop client has decoded the file.

Authentication

All endpoints require authentication via the Private-Token header or a CanvusSession cookie, unless the canvas has link sharing enabled (View or Edit permission). Write operations (POST, PATCH, DELETE) require edit access. View-only users receive 403 Forbidden on any write attempt.

Common widget properties

Every video shares these properties with all other canvas widget types.

  • id -- string (uuid, read-only). Unique identifier assigned by the server on creation.
  • widget_type -- string (read-only). Always "Video" for this endpoint.
  • location -- object (patchable). Position relative to the parent widget, with x and y float fields.
  • size -- object (patchable). Dimensions in canvas units, with width and height float fields. Aspect ratio is enforced on PATCH -- the requested size is treated as a bounding box and the widget is fit within it, keeping the widget's current width-to-height ratio. The response contains the actual applied dimensions. On create, size is stored as given.
  • depth -- float (patchable). Z-order among sibling widgets. Higher values render on top. Must be >= 1.0 -- the server returns 400 Bad Request if you set depth below 1.0.
  • scale -- float (patchable). Uniform scale factor applied to the widget. Default: 1.
  • pinned -- boolean (patchable). When true, the widget cannot be moved or resized through touch interaction. API changes still apply. Default: false.
  • state -- string (read-only). Widget state, typically "normal".
  • parent_id -- string (uuid, patchable). ID of the parent widget. Changing this reparents the widget. Note that location coordinates are always relative to the parent -- changing parent_id without updating location will reinterpret the same x/y values in the new parent's coordinate space.
  • auto_raise -- boolean (patchable, PATCH only). When true, the server sets the widget's depth to the highest sibling depth + 1.0, bringing it to the front. Not persisted -- this is a one-shot parameter. Default: false.

Video-specific properties

  • title -- string (patchable). Display title for the video widget. Default: "".
  • original_filename -- string (patchable). The original filename of the uploaded video file. Default: "".
  • hash -- string (read-only). Server-generated content hash of the video file. Empty string immediately after creation while the file is being processed.
  • playback_state -- string (patchable). Current playback state. One of: "PLAYING", "PAUSED", "STOPPED". Default: "STOPPED". Values are case-sensitive: send them in uppercase, as they are returned. Any other value is ignored.
  • playback_position -- float (patchable). Current playback position in seconds. Default: 0. When you set playback_state to "PAUSED" without providing playback_position, the server automatically computes and preserves the current position based on elapsed time since playback started.
  • muted -- boolean (patchable). When true, video audio is silenced. Default: false.
  • duration -- float (read-only). Total duration of the video in seconds. This value is 0.0 until a Canvus desktop client decodes the video file and reports its duration. Do not set this via the API.

Streaming

All GET endpoints support real-time updates via the ?subscribe query parameter. When present, the server holds the connection open and sends newline-delimited JSON whenever the resource changes. The server sends periodic keepalive pings to maintain the connection.


List videos

Retrieves all video widgets on a canvas.

GET /api/v1/canvases/:canvas_id/videos

Path parameters:

  • canvas_id (uuid, required) -- ID of the canvas

Query parameters:

  • subscribe (flag, optional) -- Enable streaming updates. The parameter takes no value; its presence alone turns streaming on, so ?subscribe=false also streams.

Example request:

curl -H "Private-Token: YOUR_TOKEN" \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos

Example response:

[
  {
    "depth": 4.0,
    "duration": 127.5,
    "hash": "863b0937ee95",
    "id": "105a8486-ac84-49f9-a487-763279a6f658",
    "location": {
      "x": 3737.21,
      "y": 2311.91
    },
    "muted": false,
    "original_filename": "product-demo.mp4",
    "parent_id": "34f8909a-6736-49f9-9027-7f7cab5c1acf",
    "pinned": false,
    "playback_position": 0,
    "playback_state": "STOPPED",
    "scale": 1,
    "size": {
      "height": 720,
      "width": 1280
    },
    "state": "normal",
    "title": "Product Demo",
    "widget_type": "Video"
  }
]

Get single video

Retrieves a single video widget by ID.

GET /api/v1/canvases/:canvas_id/videos/:video_id

Path parameters:

  • canvas_id (uuid, required) -- ID of the canvas
  • video_id (uuid, required) -- ID of the video

Query parameters:

  • subscribe (flag, optional) -- Enable streaming updates. The parameter takes no value; its presence alone turns streaming on, so ?subscribe=false also streams.

Example request:

curl -H "Private-Token: YOUR_TOKEN" \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

Example response:

{
  "depth": 4.0,
  "duration": 127.5,
  "hash": "863b0937ee95",
  "id": "105a8486-ac84-49f9-a487-763279a6f658",
  "location": {
    "x": 3737.21,
    "y": 2311.91
  },
  "muted": false,
  "original_filename": "product-demo.mp4",
  "parent_id": "34f8909a-6736-49f9-9027-7f7cab5c1acf",
  "pinned": false,
  "playback_position": 0,
  "playback_state": "STOPPED",
  "scale": 1,
  "size": {
    "height": 720,
    "width": 1280
  },
  "state": "normal",
  "title": "Product Demo",
  "widget_type": "Video"
}

Create video

Creates a new video widget by uploading a file. The request must be multipart/form-data with a mandatory data part (the video file) and an optional json part (widget metadata).

POST /api/v1/canvases/:canvas_id/videos

Path parameters:

  • canvas_id (uuid, required) -- ID of the canvas

Multipart form parts:

  • data (file, required) -- The video file to upload (MP4, WebM, MOV, etc.)
  • json (string, optional) -- JSON string with widget properties

JSON properties (all optional):

  • title -- string
  • original_filename -- string
  • location -- object with x and y floats
  • size -- object with width and height floats
  • depth -- float (must be >= 1.0)
  • scale -- float
  • pinned -- boolean
  • parent_id -- string (uuid)
  • playback_state -- string ("STOPPED", "PLAYING", or "PAUSED", uppercase)
  • playback_position -- float (seconds)
  • muted -- boolean

Current behavior

  • Create does not yet read muted; a new video is always unmuted. Set it with an update after creating.
  • Create does not yet read playback_position; it reads seek_position instead.

These are being corrected.

Example request:

curl -X POST \
  -H "Private-Token: YOUR_TOKEN" \
  -F 'json={"title": "Product Demo", "muted": true}' \
  -F 'data=@product-demo.mp4' \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos

Example response:

{
  "depth": 1.0,
  "duration": 0.0,
  "hash": "",
  "id": "370b112b-c6e9-440f-bdad-f6d6cf6a6d9f",
  "location": {
    "x": 5148.75,
    "y": 2807.72
  },
  "muted": true,
  "original_filename": "product-demo.mp4",
  "parent_id": "34f8909a-6736-49f9-9027-7f7cab5c1acf",
  "pinned": false,
  "playback_position": 0,
  "playback_state": "STOPPED",
  "scale": 1,
  "size": {
    "height": 720,
    "width": 1280
  },
  "state": "normal",
  "title": "Product Demo",
  "widget_type": "Video"
}

The hash field is empty immediately after creation and the duration is 0.0. Both are populated once the file is processed (hash) and decoded by a desktop client (duration). Subscribe to the widget or poll with GET to see updated values.


Update video

Updates one or more properties of an existing video widget. Only the fields included in the request body are changed -- all other properties remain untouched.

PATCH /api/v1/canvases/:canvas_id/videos/:video_id

Path parameters:

  • canvas_id (uuid, required) -- ID of the canvas
  • video_id (uuid, required) -- ID of the video to update

Request body (JSON):

Any combination of patchable properties: title, original_filename, location, size, depth, scale, pinned, parent_id, auto_raise, playback_state, playback_position, muted.

Example request -- start playback:

curl -X PATCH \
  -H "Private-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"playback_state": "PLAYING"}' \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

Example request -- pause (position preserved automatically):

curl -X PATCH \
  -H "Private-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"playback_state": "PAUSED"}' \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

When pausing without providing playback_position, the server computes the current position from the seek position plus elapsed time since playback started. There is no need to calculate and send the position yourself.

Current behavior

The computed position is currently only reliable if playback was last started with an explicit playback_position. If you started playback without one, send playback_position when pausing. This is being corrected.

Example request -- seek to specific position and play:

curl -X PATCH \
  -H "Private-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"playback_state": "PLAYING", "playback_position": 45.0}' \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

Example request -- mute:

curl -X PATCH \
  -H "Private-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"muted": true}' \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

Example request -- resize (aspect ratio enforced):

curl -X PATCH \
  -H "Private-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"size": {"width": 640, "height": 640}}' \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

The server keeps the widget's current aspect ratio. The requested size is treated as a bounding box. If you request 640x640 for a widget currently sized at 16:9, the response will contain {"width": 640, "height": 360}. Always read the response body to get the actual applied size.

Example response:

{
  "depth": 4.0,
  "duration": 127.5,
  "hash": "863b0937ee95",
  "id": "105a8486-ac84-49f9-a487-763279a6f658",
  "location": {
    "x": 3737.21,
    "y": 2311.91
  },
  "muted": true,
  "original_filename": "product-demo.mp4",
  "parent_id": "34f8909a-6736-49f9-9027-7f7cab5c1acf",
  "pinned": false,
  "playback_position": 45.0,
  "playback_state": "PLAYING",
  "scale": 1,
  "size": {
    "height": 360,
    "width": 640
  },
  "state": "normal",
  "title": "Product Demo",
  "widget_type": "Video"
}

Validation errors:

  • Setting depth below 1.0 returns 400 Bad Request: Depth must be >= 1, got <value>
  • Setting size with zero or negative dimensions is rejected

Download video

Downloads the original video file as a binary attachment.

GET /api/v1/canvases/:canvas_id/videos/:video_id/download

Path parameters:

  • canvas_id (uuid, required) -- ID of the canvas
  • video_id (uuid, required) -- ID of the video to download

Example request:

curl -H "Private-Token: YOUR_TOKEN" \
  -o downloaded-video.mp4 \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658/download

The response includes a content-disposition: attachment header with the original filename.

Current behavior

The filename comes from the widget's original_filename. The server does not yet take it from the uploaded file's name, so send original_filename when you create the widget; otherwise the download uses an internal name. This is being corrected.


Delete video

Permanently removes a video widget from the canvas. The underlying file is deleted from server storage later, once no canvas references it.

DELETE /api/v1/canvases/:canvas_id/videos/:video_id

Path parameters:

  • canvas_id (uuid, required) -- ID of the canvas
  • video_id (uuid, required) -- ID of the video to delete

Example request:

curl -X DELETE \
  -H "Private-Token: YOUR_TOKEN" \
  https://canvus.example.com/api/v1/canvases/a8b5ed9e-6e57-41c8-96cb-be4aec4bfd03/videos/105a8486-ac84-49f9-a487-763279a6f658

A successful deletion returns an empty response with status 200 OK.


Error responses

All endpoints return errors as JSON with a msg field.

  • 400 Bad Request -- Invalid input (e.g. depth below 1.0, missing file in upload, zero-size dimensions)
  • 401 Unauthorized -- Missing or invalid authentication token
  • 403 Forbidden -- View-only user attempted a write operation
  • 404 Not Found -- Canvas or video does not exist

Current behavior

  • An invalid or expired token returns 403 Forbidden, not 401. See Current behavior.
  • Create does not yet enforce the depth limit. A widget created without depth gets 0, and a value below 1.0 is accepted. Only update rejects it.
  • A zero or negative size is not rejected. Create stores it; update ignores it and still returns 200 OK.
  • An update rejected with 400 Bad Request can still apply other fields sent in the same request.

These are being corrected.

Example error response:

{"msg": "Depth must be >= 1, got 0"}