HTTP API
The web interface uses this API, and so can you.
- Endpoints under
/api take and return JSON, except the three upload
endpoints (…/blob, …/parts/{n}, …/thumbnail), which take the raw
bytes.
- Authentication is the
vistilla_session cookie set by
POST /api/auth/login (or by finishing sign-up).
X-Vistilla-Lang: ar (or en), sent by the site and the app with
every request, picks the language of the emails a request sends
(invitations, shared videos, sign-up codes, password resets); their links
then carry ?lang=ar.
- Errors look like
{"error": "message"}, with the status codes listed
in Backend → Errors.
Legend:
- 🔓 public: no session needed;
- ◐ optional: works with or without a session;
- everything else needs a session.
Accounts
| Method |
Path |
Notes |
| POST |
/api/auth/register 🔓 |
{email, name, password, org_name?, invite_token?}. Without an invite this emails a 6-digit code and answers {verify: true, email}. With a valid invite it creates the account and signs in. |
| POST |
/api/auth/register/verify 🔓 |
{email, code}. Creates the account and workspace, and signs in. |
| POST |
/api/auth/login 🔓 |
{email, password}. |
| POST |
/api/auth/logout 🔓 |
Ends the current session. |
| POST |
/api/auth/forgot 🔓 |
{email}. Emails a reset link. Answers {ok, found, email_configured, message}. |
| GET |
/api/auth/reset/{token} 🔓 |
Is this reset link still valid? |
| POST |
/api/auth/reset 🔓 |
{token, password}. |
| GET |
/api/me |
Me, my workspaces and teams with roles. |
| PATCH |
/api/me |
{name?, current_password?, new_password?}. |
| PUT, DELETE |
/api/me/avatar |
Set (JPEG, PNG or WebP body, up to 512 KB) or remove my photo. |
| GET |
/api/users/{id}/avatar?v=… 🔓 |
A person's photo, cached for a year; the v changes with each new photo. Wherever a person appears next to their avatar_hue, their photo's address (or null) comes too: avatar_url on /api/me's user, members, comment authors and a video's uploader; actor_avatar_url on notifications; viewer_avatar_url in insights. |
| GET |
/api/workspace-name?name= 🔓 |
Is a workspace name free? |
| GET |
/api/team-name?name= |
Can a workspace (a team on screen) be given this name? {available, reason}: one word, not used anywhere on Vistilla. |
Workspaces, teams and members
| Method |
Path |
Notes |
| GET, POST |
/api/orgs |
List, or create a workspace (a team on screen) {name?, team_name?} (when sign-up is open, or for anyone who can record in a workspace). Its first team (a channel on screen) is team_name, else "General". |
| GET |
/api/orgs/{org_id}/storage |
Space used: {workspace_bytes, workspace_videos, total_bytes, quota_bytes} (any member). |
| DELETE |
/api/orgs/{org_id} |
Deletes the workspace (a team on screen) with its teams, recordings and their files. Its owner only; 409 when it is the last workspace they are in. |
| PATCH |
/api/orgs/{org_id} |
{accent}: the team's color, one of teal, blue, indigo, violet, pink, red, orange, green, slate (teal clears it). Owners and admins. GET /api/me gives each workspace's accent. |
| GET, POST |
/api/orgs/{org_id}/teams |
List or create teams (channels on screen; create: workspace admin+). Names are unique inside the workspace. |
| GET, PATCH, DELETE |
/api/teams/{team_id} |
Read, rename or delete a team. Names are unique in a workspace, ignoring case (409 otherwise). DELETE (workspace admin+, or the team's owner) removes its recordings and their files too, and is refused (409) for a workspace's last team. |
| GET |
/api/teams/{team_id}/members |
Members and roles. |
| PATCH, DELETE |
/api/teams/{team_id}/members/{user_id} |
Change a role, or remove someone. |
| GET, POST |
/api/teams/{team_id}/invites |
List, or send an invitation (admin+): {email, role?}. |
| GET |
/api/invites/{token} 🔓 |
Look at an invitation. |
| POST |
/api/invites/{token} |
Accept it. |
| POST |
/api/invites/{token}/decline 🔓 |
Decline it. |
| POST |
/api/invites/{invite_id}/revoke |
Revoke a pending invitation. |
Collections
| Method |
Path |
Notes |
| GET, POST |
/api/teams/{team_id}/collections |
List (with can_add, and can_manage on each) or create. Creating, renaming and deleting are for the channel's owners. |
| PATCH, DELETE |
/api/collections/{collection_id} |
Rename or delete (owners). Videos stay. |
| GET, PUT, DELETE |
/api/collections/{collection_id}/image |
The collection's picture, shown on its folder: get it (anyone in the channel), set it (owners; the body is a JPEG, PNG or WebP of at most 1 MB) or remove it (owners). The list gives each collection's image_url, or null. |
Videos
| Method |
Path |
Notes |
| GET |
/api/videos?team_id=&collection_id=&q=&tag=&mine=&loose=&sort=&limit=&offset= |
The library, with search. sort is new (the default), old, az, za (by title; a title starting with a number goes by that number), long, short or views. loose=true (without collection_id) leaves out videos that are in a collection, as All recordings does. limit defaults to 40, at most 100. Each video has uploaded_bytes: while status is uploading, the size of the parts that have arrived; after, the whole size. |
| POST |
/api/videos |
Reserve a video: {team_id, title?, content_type?, size_bytes?, visibility?, collection_id?, live?, reply_to_comment_id?}. Returns the upload URLs: proxy_put_url (through the server), direct_put_url (presigned, straight to the bucket; S3 only), and part_url (uploads in parts, live: true), plus video_id, complete_url, content_type, and the limits max_upload_bytes (one request) and max_recording_bytes (in parts). |
| PUT |
/api/videos/{id}/blob |
Upload the whole file. |
| PUT |
/api/videos/{id}/parts/{n} |
Upload part n of a live upload. |
| POST |
/api/videos/{id}/cut |
{start_ms?, end_ms?}: cuts a published recording (editors only) down to that part, for good. The video is processing until the worker has cut it; comments, chapters, reactions and transcript lines then move to the new timeline. 409 while it is not ready or its transcript or summary is being written; 400 for a cut that keeps everything or under a second, or a recording too large to cut. |
| POST |
/api/videos/{id}/complete |
{duration_ms?, width?, height?, size_bytes?, title?, trim_start_ms?, trim_end_ms?, partial?}; partial: true publishes the parts that arrived, up to the first missing one. |
| PUT |
/api/videos/{id}/thumbnail |
Poster image. |
| GET, PATCH, DELETE |
/api/videos/{id} |
Read, edit (title, description, tags, collection_id, team_id (move to another channel of the same team; it leaves its collection), visibility, ctas: up to 5 link buttons [{label, url, icon?}], icon a picked logo as a small PNG/JPEG/WebP/GIF data: URL, an empty list removes them; the older single cta_label + cta_url still works), or delete. A video carries ctas (every button) and cta (the first). |
| POST |
/api/videos/bulk-delete |
{ids: [...]}, up to 200. |
| GET, POST |
/api/videos/{id}/chapters |
List or add chapters. |
| DELETE |
/api/chapters/{chapter_id} |
Remove a chapter. |
| Method |
Path |
Notes |
| GET, POST |
/api/videos/{id}/comments |
{body, timecode_ms?, parent_id?, mentions?, images?}. images: up to 4 pictures as data:image/…;base64, URLs (JPEG, PNG, WebP or GIF, 4 MB each; the request may be 24 MB); body may then be empty. Each comment lists its images as addresses. |
| PATCH, DELETE |
/api/comments/{id} |
Edit (author) or delete. |
| POST |
/api/comments/{id}/resolve |
{value: true\|false}. |
| POST |
/api/comments/{id}/pin |
{value: true\|false}: pinned to the top of the thread. |
| DELETE |
/api/comments/{id}/moment |
Takes the comment off its moment (timecode_ms becomes null). Author, the video's owner or an admin. |
| PUT |
/api/comments/{id}/moment |
{timecode_ms}: pins the comment to that moment, or moves it there (clamped to the video). Not for replies. Author, the video's owner or an admin. |
| POST |
/api/comments/{id}/reactions |
{emoji}. Same emoji again removes it. |
| GET, POST |
/api/videos/{id}/moments |
Emoji on the timeline: {emoji, timecode_ms}. One per person per video: a new one replaces theirs, and the same emoji again removes it (answers emoji: null). |
Sharing
| Method |
Path |
Notes |
| GET, POST |
/api/videos/{id}/shares |
List or create links: {password?, expires_in_days?, allow_download?, allow_comments?}. |
| POST |
/api/videos/{id}/invites |
{emails: [...], message?}: personal links by email (up to 20). |
| PATCH, DELETE |
/api/shares/{share_id} |
Change options {allow_download?, allow_comments?, expires_in_days?, password?}, or revoke. |
| GET |
/api/share/{token} ◐ |
The shared video, or {locked: true}. |
| POST |
/api/share/{token}/unlock 🔓 |
{password}. Sets a 12-hour unlock cookie. |
| GET, POST |
/api/share/{token}/comments ◐ |
Guest comments: {body, timecode_ms?, parent_id?, guest_name?}. |
| POST |
/api/share/{token}/comments/{comment_id}/reactions ◐ |
Guest reactions. |
| GET, POST |
/api/share/{token}/moments ◐ |
Guest moments. |
| POST |
/api/share/{token}/views ◐ |
Report viewing progress: {session_key, watched_ms?, completed?, guest_name?}. |
Transcripts, summaries and AI settings
| Method |
Path |
Notes |
| GET, POST |
/api/videos/{id}/transcript |
Read it (with segments and captions_url), or queue it again. ?lang=en or ?lang=ar reads it in that language: see Translation in the AI pipeline. |
| GET, POST |
/api/videos/{id}/summary |
Read it, or queue it again. Takes ?lang= as the transcript does. |
GET /api/videos/{id}/chapters?lang= names the AI's chapters in that language, from the summary's translated topics (translation: "pending" while it is made).
| GET | /api/search/transcripts?team_id=&q= | Search spoken words (at least 2 characters, 50 results). |
| GET | /api/orgs/{org_id}/ai | Providers on offer and the workspace's settings (admin+). |
| PUT, DELETE | /api/orgs/{org_id}/ai/{purpose} | purpose is transcription or summary. Body: {vendor, model, api_key?, base_url?}. |
| POST | /api/orgs/{org_id}/ai/{purpose}/test | Check the key works. |
Notifications and insights
| Method |
Path |
Notes |
| GET |
/api/notifications?unread_only=&limit= |
Activity, default 30. |
| POST |
/api/notifications/read |
{ids?} or {video_id}. No ids means everything; video_id marks the notices about that video's comments (not watched). Answers the new unread count. |
| GET |
/api/videos/{id}/analytics |
Insights (uploader or admin+). |
| POST |
/api/videos/{id}/views |
{session_key, watched_ms?, completed?}. |
| GET |
/api/healthz 🔓 |
{"status": "ok"}. |
| Method |
Path |
Notes |
| GET |
/media/videos/{id} |
Stream, with Range support or a 307 to the bucket. |
| GET |
/media/videos/{id}/download |
Download with a file name. |
| GET |
/media/comment-images/{team}/{file} |
A comment's picture. Its name has a long random part and is only given out with the comment, so it needs no session (a shared link's guests see them). |
| GET |
/media/thumbs/{id} |
Poster image. thumbnail_url adds ?v= naming the picture, so a new one (PUT /api/videos/{id}/thumbnail, the owner) is fetched afresh; each picture is stored under a name of its own and the one it replaces is deleted. |
| GET |
/media/favicon/{host} |
A website's small logo, for its link button: a logo remembered for the site (site_logos) first, then the icon its home page names (<link rel="icon">), else /favicon.ico, else Google's favicon service. The server fetches it, public addresses only, so viewers' browsers do not; cached for a week. Signed-in callers may ask for any site; others only for one a recording links to. |
| GET |
/media/videos/{id}/captions.vtt |
Captions. |
| GET |
/media/share/{token} ◐ |
Stream through a share link (the token, and the unlock cookie for a password-protected link, grant access). |
| GET |
/media/share/{token}/download ◐ |
Only if the link allows downloads. |
| GET |
/media/share/{token}/captions.vtt 🔓 |
Captions through a share link. |
Pages and files
| Path |
Serves |
/, /app, /record, /settings, /tour |
Pages. |
/v/{id}, /s/{token} |
Video and share pages, with link-preview tags. |
/join/{token}, /reset/{token} |
Invitation and password-reset pages. |
/preview/v/{id}, /preview/s/{token} |
Poster image for link previews. |
/assets/* |
Scripts, styles, icons. |
/docs, /docs/* |
This documentation (/docs redirects to /docs/). |
/sw.js, /favicon.svg |
Service worker, icon. |
Example: upload a file with curl
B=https://vistilla.example.com
# Sign in and keep the cookie.
curl -c jar -H 'content-type: application/json' \
-d '{"email":"me@example.com","password":"…"}' $B/api/auth/login
# Find your team id.
curl -b jar $B/api/me
# Reserve, upload, complete.
curl -b jar -H 'content-type: application/json' \
-d '{"team_id":"tm_…","title":"Demo","content_type":"video/mp4","size_bytes":1234567}' \
$B/api/videos # → {"video_id": "vid_…", "proxy_put_url": …}
curl -b jar -X PUT -H 'content-type: video/mp4' --data-binary @demo.mp4 \
$B/api/videos/vid_…/blob
curl -b jar -H 'content-type: application/json' \
-d '{"duration_ms":30000,"width":1920,"height":1080}' \
$B/api/videos/vid_…/complete