Flask API Reference¶
REST endpoints served by the Plant Tracer Flask application are mounted under
/api/. Most are defined in src/app/flask_api.py; admin-specific endpoints
are defined in src/app/admin_api.py. This document covers authentication,
the standard response envelope, and every endpoint.
The root-level /ver endpoint returns plain text with the application and
Python versions, stack name, UTC deployment timestamp, and DynamoDB table prefix.
Local runs report Stack deployed at: unknown unless
PLANTTRACER_DEPLOYED_AT is set.
For the Lambda (frame/video processing) endpoints, see ClientLambdaAPI.md.
Authentication¶
Most endpoints require an api_key parameter. Pass it as a POST body field or
a query-string parameter.
Unauthenticated endpoints:
GET|POST /api/verGET|POST /api/config-checkGET|POST /api/registerGET|POST /api/resend-linkValid key -> request proceeds, user identity resolved from the key.
Invalid or missing key on authenticated API routes ->
{"error": true, "message": "Invalid api_key"}with HTTP 403.
API keys are issued per-user and stored in the api_keys DynamoDB table. A user may hold
multiple keys (e.g. after re-sending a login link). Keys are sent as a cookie after first login.
Response Envelope¶
Most endpoints return JSON with "error": false on success or "error": true
plus "message" on failure. Exceptions:
/api/verreturns{"__version__": "...", "git_commit": "...", "sys_version": "...", "stack_name": "...", "DYNAMODB_TABLE_PREFIX": "..."}./api/get-movie-trackpointsreturns CSV by default.
{ "error": false, ... }
{ "error": true, "message": "Human-readable reason" }
Endpoints¶
Admin¶
GET /api/admin/summary¶
Return the minimal read-only admin landing-page data. This endpoint backs /admin
and is intentionally read-only.
Authorization
superadminandsuperauditorusers receive the cross-course view.Course administrators receive a view limited to courses they administer, users enrolled in those courses, and movies assigned to those courses. A visible user’s memberships and default course are also limited to that administered-course scope.
Regular users receive HTTP 403. Course-admin access does not depend on a
superadminorsuperauditorexisting.
Query parameters
Name |
Required |
Description |
|---|---|---|
|
No |
Page size for each requested course, user, or movie list. Defaults to 25, maximum 100. |
|
No |
Return rows for |
|
No |
Opaque, course-table-bound restart marker from the previous |
|
No |
Opaque, user-table-bound restart marker from the previous |
|
No |
Opaque, movie-table-bound restart marker from the previous |
Unknown sections and malformed, non-object, or wrong-table restart markers receive HTTP 400.
Response
{
"error": false,
"viewer": {
"user_id": "u...",
"user_name": "Course Admin",
"email": "teacher@example.edu",
"super_role": "none",
"all_courses": false,
"course_ids": ["PlantTracer 101"]
},
"counts": { "courses": 1, "users": 2, "movies": 3 },
"courses": {
"items": [
{
"course_id": "PlantTracer 101",
"course_key": "spring-beans-2026",
"course_name": "Intro Biology",
"enrollment_count": 42,
"max_enrollment": 100,
"admin_count": 1,
"created_at": 1784800000,
"last_movie_activity_at": null
}
],
"restart_marker": null
},
"users": {
"items": [
{
"user_id": "u...",
"user_name": "Alice",
"email": "alice@example.edu",
"enabled": true,
"default_course_id": "PlantTracer 101",
"super_role": "none",
"created_at": 1784800100,
"last_movie_activity_at": null,
"courses": [
{
"course_id": "PlantTracer 101",
"is_admin": false
}
]
}
],
"restart_marker": null
},
"movies": {
"items": [
{
"movie_id": "m...",
"title": "Bean Growth",
"course_id": "PlantTracer 101",
"user_id": "u...",
"owner_name": "Alice",
"state": "published",
"status": "ready",
"created_at": 1784800200,
"uploaded_at": 1784800300,
"last_activity_at": 1784800400,
"total_frames": 1441,
"total_bytes": 12500000,
"fpm": "60",
"has_traced_movie": true,
"description": "Daily bean measurement",
"fps": "30",
"width": 640,
"height": 480,
"rotation": 0,
"trim_start_frame": 0,
"trim_end_frame": 1440,
"needs_retracing": false,
"research_use": 1,
"credit_by_name": 1,
"attribution_name": "Alice"
}
],
"restart_marker": null
}
}
For a course administrator, viewer.all_courses is false,
viewer.course_ids contains the administered course IDs, and all three counts
and result lists describe only that scope. Restart markers remain opaque and
table-bound for global readers; for course administrators they page the
corresponding scoped result set.
The movie list includes published, hidden, and deleted DynamoDB records. The
API continues to use the published field: 1 is published and 0 is hidden.
The admin summary reports the same states as published, hidden, or
deleted.
state reports that visibility/deletion state; status reports processing state.
The summary deliberately omits object URNs and API keys. The default table view
stays compact: its Verbose details control reveals stable IDs, named course
administrators, and movie metadata including description, dimensions, trimming,
rotation, retrace state, and research attribution. GET /api/admin/movies/<movie_id>/storage-health
loads the verbose-only per-object storage health and pending-upload age on demand.
Storage health reports only present, missing, or not created and never
exposes raw S3 URIs. Course enrollment counts are
read consistently from the course_users table. User memberships and movies
carry course_id; the admin page joins those IDs to the separately downloaded
course names after all bounded pages arrive.
The same browser-side join derives each course’s first upload and latest movie
activity and each user’s latest movie activity. A course’s displayed creation
date uses created_at, falling back to its first movie upload for legacy rows.
Movies without uploaded_at are pending uploads and are displayed with a red
background. Movie elapsed time is (total_frames - 1) / fpm; encoded playback
fps is deliberately not used.
DynamoDB scan order is not stable. The admin page requests bounded pages until
all three tables are loaded, then sorts complete result sets in the browser;
clients must treat restart markers as opaque. Course rows include the registration
course_key; callers must treat it as a secret because anyone with the key can
request enrollment in that course. The admin page masks each course key by default;
its per-row eye control reveals or hides the value without changing it.
Admin tables grow and shrink with the available page width down to a 1024-pixel
minimum. Narrower pages keep the table at that minimum and enable a table-local
horizontal scrollbar. Each column has a drag/keyboard resize handle, and the
table’s right edge is a drag/keyboard handle that proportionally resizes the
whole table. Course administrators appear one per line. Course links open
/list?course_id=... in a new tab without changing the user’s persisted default
course. Course, user, and movie rows use a visible ⋮ Actions menu for their
authorized operations. Movie rows use one-line ellipsized titles, 24-hour
timestamps, and compact frames / MB / min measurements; verbose details
retain the complete title.
POST /api/admin/courses¶
Create a course and assign its initial course administrator. Only a
superadmin may call this endpoint; superauditor, course-administrator, and
ordinary users receive HTTP 403.
{
"course_id": "BIO101",
"course_name": "Plant Biology",
"admin_email": "teacher@example.edu",
"admin_name": "Course Teacher"
}
The server normalizes the email address. If it belongs to a registered user,
the stored user_name is authoritative and the submitted admin_name is
ignored. A disabled registered account or a legacy account without a stored
name is rejected with HTTP 409; disabled administrators are not offered by the
selector. Otherwise, both administrator email and name are used to create the
administrator account. The server generates the course registration key and
uses the configured default maximum enrollment.
A new course returns HTTP 201. Retrying the same course ID, name, and
administrator is idempotent and returns HTTP 200; reusing an ID with a
different name returns HTTP 409. Invalid fields return HTTP 400 without
creating a course. Successful database changes write a course.created or
course.admin.assigned audit event attributed to the acting superadmin.
The endpoint emails the administrator a course setup/login link. Database
success is not rolled back if mail delivery subsequently fails; in that case
email_sent is false and message reports that the course was created or
updated but the email was not sent.
{
"error": false,
"course": {
"course_id": "BIO101",
"course_name": "Plant Biology",
"course_key": "6e908735",
"admins_for_course": ["u..."],
"max_enrollment": 100,
"created_at": 1784800000
},
"administrator": {
"user_id": "u...",
"email": "teacher@example.edu",
"user_name": "Course Teacher"
},
"created": true,
"email_sent": true,
"message": "Course created and administrator email sent"
}
Course rows show administrator names and email addresses directly. A
superadmin receives a Manage action for every course. Course administrators
receive the action for courses they administer; superauditor viewers have
no write actions.
PUT /api/admin/courses/{course_id}/administrators¶
Assign a registered user by exact email address. This form lets a course administrator add a registered user who is not yet enrolled and therefore is not visible in that administrator’s scoped user table.
{ "email": "alice@example.edu" }
PUT /api/admin/courses/{course_id}/administrators/{user_id}¶
Assign an existing enabled user by ID. For both assignment forms, the caller
must be a superadmin or an administrator of the identified course. The
mutation atomically adds the course to the user’s admin_for_courses and
courses lists, adds the user to the course’s admins_for_course list,
ensures the course_users membership exists, and writes an attributed
course.admin.assigned audit event. Repeating an already-complete assignment
succeeds without another audit event and returns changed: false.
DELETE /api/admin/courses/{course_id}/administrators/{user_id}¶
Remove course-administrator status. The caller must be a superadmin or an
administrator of the identified course. Administrators may remove themselves
when another administrator remains. The mutation removes the two mirrored
administrator references but retains course enrollment, the course_users
row, and the user’s default course. The final administrator cannot be removed.
Repeating an already-complete removal succeeds without another audit event and
returns changed: false.
Both endpoints return:
{
"error": false,
"course_id": "PlantTracer 101",
"administrator": {
"user_id": "u...",
"user_name": "Alice",
"email": "alice@example.edu",
"enabled": true,
"default_course_id": "PlantTracer 101",
"super_role": "none",
"courses": [
{ "course_id": "PlantTracer 101", "is_admin": true }
],
"created_at": 1784800100,
"last_movie_activity_at": null
},
"assigned": true,
"changed": true
}
Missing records return HTTP 404. Disabled target users, final-administrator
removal, and repeated concurrent conflicts return HTTP 409. Invalid identifiers
or assignment email payloads return HTTP 400. Callers who are neither a
superadmin nor an administrator of the identified course receive HTTP 403.
The actor’s authority is included in the DynamoDB transaction condition so a
concurrent role removal cannot authorize a stale request.
PUT /api/admin/users/{user_id}/superadmin¶
Grant superadmin to any registered user. Only a current superadmin may
call this endpoint.
DELETE /api/admin/users/{user_id}/superadmin¶
Remove superadmin from a registered user. A superadmin may remove their own
role when another superadmin remains; removing the final superadmin returns
HTTP 409. Removing the role from a user who is not currently a superadmin is an
idempotent no-op and does not remove superauditor.
PUT /api/admin/users/{user_id}/superauditor¶
Grant superauditor to any registered user. Only a current superadmin
may call this endpoint. Granting either super role replaces the other because
the roles are mutually exclusive.
DELETE /api/admin/users/{user_id}/superauditor¶
Remove superauditor from a registered user. Removing the role from a user
who is not currently a superauditor is an idempotent no-op and does not remove
superadmin.
All four role endpoints update the user and versioned superadmin registry
atomically, condition the write on the actor’s current authority, and write an
attributed user.superadmin.* or user.superauditor.* audit event in the
reserved global audit scope. Each event records old_super_role and
new_super_role. Successful responses contain the complete admin user
summary plus those role values and changed. Repeated no-op requests do not
create audit events. Missing users return HTTP 404, stale concurrent changes
return HTTP 409, and non-superadmins receive HTTP 403. Switching or removing
the final superadmin returns HTTP 409.
GET /api/admin/movies/{movie_id}/media¶
Return fresh five-minute signed URLs for the original movie and, when present,
the traced movie. The response does not expose stored S3 URNs. Both
superauditor and superadmin may use this authenticated, read-only endpoint
for any movie. Course administrators may use it only for movies in courses they
administer; ordinary membership in another course is not sufficient.
{
"error": false,
"movie_id": "m...",
"play_url": "https://...",
"traced_download_url": "https://..."
}
User & Registration¶
PATCH /api/default-course¶
Change the signed-in user’s profile default. The selected course must
already be present in that user’s course memberships. The endpoint uses the
existing authentication cookie and does not grant course membership.
PATCH /api/current-course is a temporary compatibility alias.
JSON request
{ "course_id": "PlantTracer 101" }
Success response
{
"error": false,
"course": {
"course_id": "PlantTracer 101",
"course_name": "Intro Biology"
}
}
An invalid or non-member course receives HTTP 400. Missing authentication receives HTTP 403.
POST /api/register¶
Register a new user by email address and course key. Sends a login link by email.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Email address to register |
|
Yes |
Course registration passphrase |
|
No |
User’s display name |
|
No |
Base URL for login link in email (defaults to server hostname) |
Response
{ "error": false, "message": "Registration key sent to alice@example.com ...", "user_id": "u..." }
Returns error: true if the email is invalid, the course key is invalid, or the course is full.
Returns error: true (but still registers the user) if the mailer is not configured.
POST /api/resend-link¶
Resend a login link to an already-registered email address.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Email address of the existing user |
|
No |
Base URL for login link in email |
Response
{ "error": false, "message": "If you have an account, a link was sent. ..." }
Always returns the same message regardless of whether the email exists (prevents enumeration).
POST /api/bulk-register¶
Register multiple users at once. Requires the caller to be a course admin.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must belong to an admin of |
|
Yes |
Target course |
|
Yes |
Newline-delimited list of email addresses. Also accepts comma- or semicolon-delimited values. |
|
No |
Newline-delimited list of display names, positionally matched to |
|
No |
Base URL for login links in emails |
Response
{
"error": false,
"message": "Registered 3 email address(es) and sent login links.",
"user_ids": ["u...", "u...", "u..."]
}
If the mailer is not configured, users are registered but message will note the email failure.
POST /api/check-api_key¶
Validate an API key and return the associated user record.
Response
{ "error": false, "userinfo": { "user_id": "u...", "email": "...", ... } }
User Listing¶
Course context¶
Authenticated browser requests that operate on course data send course_id.
The server validates that course against the authenticated user’s memberships.
For reads, an omitted or deleted course falls back to the profile’s valid
default_course_id and then to the first valid membership. Mutations reject a
deleted explicit course with HTTP 409, and all operations reject an existing
course outside the caller’s authority with HTTP 403.
The pull-down stores the active course in tab-scoped sessionStorage. It does
not update the user profile. PATCH /api/default-course with
{"course_id": "..."} is the explicit operation for changing the profile
default. PATCH /api/current-course remains a compatibility alias during the
migration.
POST /api/list-users¶
POST /api/list-users-courses¶
Both routes are equivalent. Return users and courses visible to the caller.
Behavior by role
Admin: Returns users enrolled in the requested administered course.
Non-admin: Returns only the caller’s own record for the resolved course.
Response
{
"error": false,
"users": [
{
"user_id": "u...",
"user_name": "Alice",
"email": "alice@example.com",
"default_course_id": "PlantTracer-101",
"courses": ["PlantTracer-101"],
"admin_for_courses": [],
"first": 1714000000,
"last": 1714500000
}
],
"courses": [
{ "course_id": "PlantTracer-101", "course_name": "Intro Biology", ... }
]
}
first and last are Unix epoch seconds of the user’s first and most recent login, respectively
(aggregated across all their API keys). Both are null if the user has never logged in.
Movies¶
POST /api/camera/new-movie¶
Create a fresh uploading movie row for a browser camera capture. This route
requires a non-demo authenticated user and accepts title, description,
and optional course_id form fields. It sets the capture interval to four
frames per minute (one frame every 15 seconds), marks the row as a camera
capture, assigns a new movie_id, and records the durable source movie URN.
It returns movie_id; the browser uses that ID for every frame upload and
the STOP request. Calling this endpoint again creates an independent movie.
POST /api/camera/frame-upload¶
Return a short-lived presigned S3 POST for one JPEG frame. Required form fields
are movie_id and frame_number (zero-based, below MAX_FRAMES). The
caller must own or have edit permission for a camera movie that is still in
uploading state. The policy restricts the object to the movie’s numbered
frame key, image/jpeg, and at most 2 MiB. The response contains the
presigned_post URL and fields. The browser uploads each captured frame
directly to S3; capture timing does not wait for these uploads.
POST /api/new-movie¶
Create a movie record and obtain a presigned S3 POST URL for uploading the
video file to a deployment-scoped staging key. The row initially has
created_at, upload_staging_urn, and the durable movie_data_urn, but not
uploaded_at. In AWS, an S3 Object Created EventBridge event makes
lambda-resize verify the exact byte count, move the object to its durable key,
record upload metadata, and queue post-upload processing. In local MinIO mode,
the browser invokes the authenticated /resize-api/v1/process-upload
compatibility adapter. The browser polls metadata until processing is complete,
then requests the first frame and links the user to Analyze.
The optional rotation parameter selects 0 (default), 90, 180, or 270 degrees
clockwise before upload processing. Invalid values return HTTP 400. Processing
saves source dimensions, measured frame_height_px, and completion state together.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
No |
Movie title |
|
No |
Form field: |
|
No |
Movie description |
|
Yes |
SHA-256 hex digest of the video file (64 chars) |
|
Yes |
Exact movie byte length, from 1 through the configured upload limit. The returned S3 policy accepts exactly this size. |
|
No |
|
|
No |
|
|
No |
Attribution name (only stored when |
|
No |
Capture interval in frames/minute (time-lapse). Positive number, fractional allowed; stored on the movie and included as |
Response
{
"error": false,
"movie_id": "m...",
"presigned_post": {
"url": "https://s3.amazonaws.com/...",
"fields": { ... }
},
"upload_completion_mode": "eventbridge"
}
upload_completion_mode is eventbridge in deployed AWS stacks and http in
local MinIO development.
POST /api/list-movies¶
List all movies visible to the caller. An optional course_id selects one
course for the tab; the caller must be a member or have a
superauditor/superadmin read role. The query does not alter the user’s
default course. A missing deleted course falls back to the valid default.
Response
{ "error": false, "movies": [ { "movie_id": "m...", "title": "...", ... } ] }
Each movie dict contains all DynamoDB metadata fields. In addition, if the movie has a traced MP4 stored in S3 (movie_traced_urn starts with s3:), the response injects a short-lived presigned URL. Clients should treat needs_retracing=1 as user-visible only when this URL is present; before the first traced MP4 exists there is no stale traced artifact to warn about.
The movie-list client treats uploaded_at (or legacy date_uploaded) as the
availability boundary. Until one is present, Play and Analyze are disabled and
the stored status remains visible.
Field |
Description |
|---|---|
|
Presigned S3 URL for downloading the traced MP4; only present when a traced MP4 exists |
POST /api/get-movie-metadata¶
The analyzer loads up to 50,000 frames of trackpoint metadata in 1,000-frame
pages using frame_start and frame_count, avoiding a single oversized Lambda
response. The
get_all_if_tracking_completed window likewise covers 50,000 frames (previously
10,000). Trackpoints remain in individual DynamoDB frame records. Browser polling
stops with a warning after 30 seconds without frame progress, including stalled
HTTP requests; this does not cancel server tracing. Terminal tracing failures
show tracing_failure_summary immediately.
Get metadata and optionally per-frame trackpoints for a specific movie.
Explicitly cleared frames are returned as frames[frame_number].markers: [],
distinct from absent, never-annotated frames. An empty marker save persists an
empty_marker_annotation boundary without advancing the tracked frontier.
Nonempty saves and reset operations remove that boundary.
Trackpoints and empty boundaries are read together in one DynamoDB range traversal
per metadata page, bounded by frame_start through frame_start + frame_count - 1.
Legacy records with an explicitly present trackpoints: [] also remain empty.
Frames whose stored points are all deleted markers return the same explicit empty
entry, preserving their annotation boundary across reload.
metadata.frame_height_px is the positive pixel height of the resized, rotated
analysis coordinate space used by the trackpoints, or null when unknown.
metadata.trackpoint_origin identifies the coordinate origin. These fields are
present even when no frame range is requested. Metadata-only requests use the
stored height or source dimensions, without reading JPEG/ZIP objects or caching
height. When requesting frames, a legacy record’s height may be recovered from
stored JPEG frames or its ZIP and cached in DynamoDB. The measured height is fixed:
repeated identical measurements are accepted, and a conflicting measurement is
rejected with HTTP 409 identifying inconsistent stored frame height. This is a
data-consistency error, not a request to rotate/re-upload or a transient retry. Invalid frame ranges, including negative frame_start,
are rejected before recovery or caching. Heights are JSON integers.
Upload completion fixes rotation; processing records source dimensions and the
analysis-frame height. Legacy trackpoints retain the existing per-frame conditional conversion to bottom-left
coordinates, so retries do not flip an already converted frame again. Trackpoint
downloads use the same height recovery and coordinate conversion.
Frame-range requests and trackpoint downloads return HTTP 409 before recovery,
caching, or migration if neither uploaded_at nor legacy date_uploaded is set.
Metadata-only polling remains available without mutating coordinate state.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
|
|
Yes |
|
|
No |
First frame number to return trackpoints for |
|
No |
Number of frames (required if |
|
No |
If |
Response
{
"error": false,
"metadata": { "movie_id": "m...", "title": "...", "status": "...", ... },
"frames": {
"0": { "markers": [ { "x": 100.0, "y": 200.0, "label": "Apex", ... } ] }
}
}
frames is only present when frame_start is provided.
While an active trace lease exists, metadata has status: "tracing" and a
tracking_lock object with acquired_at and started_by_user_name. Clients
use this to present Analyze as read-only.
An active browser analysis lease adds analysis_lock with active, owned,
acquired_at, and started_by_user_name. owned is true only when the
request includes the holder’s analysis_lease_id.
POST /api/acquire-movie-analysis-lease¶
Acquire Analyze’s movie-row lease immediately on page entry. The successful
holder receives an opaque lease_id. When another browser already holds the
lease, this still returns HTTP 200 with lease_id: null and an analysis_lock
object so the caller can display Analyze as view-only.
A movie without uploaded_at (or legacy date_uploaded) is not yet in durable
movie storage. Lease acquisition returns HTTP 409 with error: true and
message: "Movie upload processing is not complete." for such a record.
POST /api/heartbeat-movie-analysis-lease renews the holder’s lease, and
POST /api/release-movie-analysis-lease releases it on page exit. Both require
api_key, movie_id, and analysis_lease_id.
POST /api/get-movie-trackpoints¶
Download all trackpoints for a movie as CSV (default), XLSX, or JSON.
All formats include frame_height_px and trackpoint_origin. Frame height is
always in pixels, even when calibrated position columns use millimeters. Unknown
height is null in JSON and blank in CSV/XLSX. The JSON response adds a metadata
object containing these two fields alongside the existing trackpoint_dicts.
XLSX retains them on its Metadata sheet. Height is resolved before legacy
coordinate migration, and height recovered from stored frames/ZIPs is persisted
so subsequent downloads can work without those artifacts.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
|
|
Yes |
|
|
No |
|
Response: CSV with columns frame_number, <label> x (<unit>), <label> y (<unit>) for each marker label, followed by frame_height_px and trackpoint_origin on every row, served with Content-Type: text/csv and Content-Disposition: attachment; filename="trackpoints.csv" so the browser downloads it rather than displaying it inline.
With format=xlsx, returns an Excel workbook served with Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet and Content-Disposition: attachment; filename="trackpoints.xlsx". The workbook contains:
Trackpoints: the frame and marker columns, values, trim filtering, and unit conversion from CSV; coordinate metadata appears only on the Metadata sheet.Metadata: export context including movie id, title, trim bounds, exported frame count, marker count, coordinate origin, inferred frame height, calibration status, units, scale, and capture interval (fpm) when available.Markers: one row per marker label with marker type (apex,ruler,inflection point, ormarker), graphable status, color, marker id, ruler size, undeletable status, frame range, trackpoint count, and any status/error values found in exported trackpoints.Chart Data: displacement from each graphable marker’s first exported position, using frames as the x-axis or minutes whenfpmis set. Ruler markers are excluded from chart data.Charts: native Excel line charts for X Position and Y Position, backed byChart Data.
Units (#763): each value column header is annotated with its unit, (mm) or (px):
Ruler XXmmmarker columns are always in pixels ((px)).Other markers’ columns are in millimeters (
(mm), value × scale) when the analysis is ruler-calibrated — i.e. there are ≥ 2Ruler XXmmmarkers and the lowest and highest are both off their default positions; otherwise they are in pixels ((px)). The scale is derived from the lowest and highest ruler markers in the first trimmed frame (mirrors the Analyze marker table). mm values are rounded to 2 decimals.
With format=json: { "error": "False", "trackpoint_dicts": [...], "metadata": { "frame_height_px": 480, "trackpoint_origin": "bottom-left" } } — JSON values are raw pixel coordinates (no unit conversion).
POST /api/put-frame-trackpoints¶
Returns HTTP 409 while the movie is still in upload setup, before a current or legacy upload-completion marker exists. No frame or tracking metadata is written.
Write trackpoints for a single frame. Used by the client before requesting re-tracking.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
Yes |
|
|
Yes |
Zero-based frame index |
|
Yes |
JSON array of trackpoint objects: |
Response
{ "error": false, "message": "trackpoints recorded: 2 " }
Side effect: sets needs_retracing=1, advances last_activity_at, and
changes render_revision on the movie record. These invalidate a previously
traced MP4; the client uses needs_retracing to show the retracing warning
when movie_traced_url is also present.
The tracer UI disables marker editing and reset actions while a trace request is active in that browser session, and when loaded movie metadata has status="tracing". This prevents normal same-session marker edits while Lambda is tracing, so Lambda does not finish by clearing needs_retracing for a traced MP4 computed from an earlier marker state.
Returns HTTP 409 when an active trace lease makes the movie read-only, or when
another browser owns the active analysis lease. The same rule applies to marker
rename, trim, and capture-interval writes; the owning browser includes its
analysis_lease_id with those requests.
The marker-map, frame, and movie updates are committed together only if no
trace lease is active and the supplied Analyze lease is still current. A stale
Analyze lease returns HTTP 409 with lease_reacquire_required: true; the
browser becomes read-only until Analyze is reopened. An edit racing a trace
request cannot change its source frame. A browser with legacy coordinates must reload Analyze so its
annotations are migrated before saving; this returns HTTP 409.
Concurrent marker-map changes also return HTTP 409 so the browser can reload
the latest annotations before retrying.
POST /resize-api/v1/prepare-analysis¶
Analyze calls this endpoint before acquiring its editing lease. The JSON body
contains movie_id; the x-api-key header must authorize access to the movie.
For a completed upload, a current-version analysis descriptor and existing S3 object return
HTTP 200 with ready: true. A missing descriptor, object, or older encoder version starts asynchronous
recoding and returns HTTP 202 with ready: false and
“Recoding is in progress, come back in a few minutes.”
A conditional 15-minute processing reservation deduplicates concurrent page openings and excludes active editing/tracing. Queue deliveries claim execution once; stale or duplicate jobs do no encoding. After a worker timeout, a later page opening can reserve a replacement job. Terminal processing failures are reported without automatic re-enqueueing; administrators must investigate and clear the processing failure before retrying. S3 permission/service errors are not treated as missing objects. The browser does not poll or acquire an editing lease while recoding is pending. Source objects, saved frame data, annotations, and the prior completed tracing status are preserved; geometry conflicts fail without rewriting saved coordinates. Encoder version 2 uses zero-based burned-in frame numbers. Unknown saved-point geometry requires recovery before recoding.
Analyze links lacking course_id redirect to the authorized movie’s course,
so an unrelated default course does not cause a lease-context conflict. Explicit
course conflicts remain rejected by the metadata/editing APIs.
POST /resize-api/v1/reset-tracing¶
Reset annotations with one JSON request authenticated by the x-api-key header
(non-demo, movie editor). Parameters: movie_id, inclusive zero-based
frame_start and frame_end, seed_frame inside that range, replacement
trackpoints for the seed (1–100), and the owning browser’s analysis_lease_id.
The range must fit within the movie and the 50,000-frame application limit.
The Analyze button sends the entire movie range and seeds the first trimmed frame
with the default markers. Other frames in the range lose only their trackpoints
attribute; frame URNs, source/analysis/traced MP4s, and frames outside the range
are preserved. The traced download is marked stale (needs_retracing=1).
Returns HTTP 202 with { "error": false, "job_id": "...", "state": "running", "next_frame": 0, "frame_end": 49999 }. Invalid input returns 400; an active
trace or another browser’s analysis lease returns 409. The operation takes the
exclusive tracing lease, preventing concurrent marker/trim edits and tracing.
The resize worker queries at most 80 frame records per batch and atomically removes annotations with a movie checkpoint. It yields after 100 batches or 60 seconds and queues a continuation. Retries resume the durable cursor; each transaction checks the job, cursor, and unexpired lease, so duplicate or late workers cannot clear newer edits. Acquiring a new analysis lease also invalidates the expired worker token, including after that browser releases its lease. Seed markers and completion are committed atomically. DynamoDB has no range-delete operation: this still incurs per-item transactional write capacity (higher than ordinary writes), but no per-frame HTTP requests or S3 work. There is no storage-format migration.
GET /resize-api/v1/reset-tracing¶
Use the same authentication header with query parameters movie_id and job_id.
Returns the small checkpoint response above, with state running, completed,
failed, expired, or superseded. This reads movie metadata only; it does not
query frame annotations. The browser polls every two seconds and reacquires its
analysis lease on completion without reloading the MP4. On an uncertain result,
expired lease, or failure, it becomes view-only and asks the user to reopen
Analyze. A partial reset can be repeated safely. Leaving the page does not cancel
the background job; leases expire after 15 minutes without worker progress.
POST /api/delete-marker¶
Delete the named marker throughout a movie, including frames outside the trim
range. Parameters: api_key, movie_id, label, and the current
analysis_lease_id when an editing lease is active. Requires movie edit permission;
demo writes, active tracing, foreign editing leases, and protected (undeletable)
markers are rejected. Success returns { "error": false }; repeat deletion is
idempotent. Invalid/protected labels return 400; concurrent marker-map edits return 409.
Deletion records a tombstone in the stable marker map and atomically sets
needs_retracing=1. All trackpoint reads, exports and subsequent tracing exclude
the deleted identity, including legacy label aliases. Frame data is retained;
there are no per-frame writes or S3 changes. The client removes the table row,
paths and graph series after success. A new marker with the same name receives
a new identity and does not revive the old trace. Existing downloaded movies
retain their rendered overlays until tracing regenerates them.
POST /api/rename-marker¶
Rename one marker label across all stored trackpoints for a movie. Other marker properties, such as coordinates, color, undeletable, status, and error metadata, are preserved.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
Yes |
|
|
Yes |
Existing marker label to rename |
|
Yes |
New marker label. Must not already exist on the movie. |
Response
{ "error": false, "frames_updated": 3, "trackpoints_updated": 3 }
Side effect: when any stored trackpoints are renamed, sets needs_retracing=1 on the movie record. Marker labels are stored in the movie_frames marker-map item at frame_number=-100, so rename updates that marker map and does not rewrite each frame record.
POST /api/rotate-movie¶
Set rotation before upload completion, while processing has not begun.
The change is conditional in DynamoDB. Once upload completion is recorded or
processing begins (also recognizing legacy date_uploaded), including completed
and legacy movies with dimensions or any saved frames, return HTTP 409 without
changing rotation or clearing tracking.
The upload form chooses rotation before uploading and supplies it to /api/new-movie.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
Yes |
|
|
Yes |
Degrees: |
Response
{ "error": false }
POST /api/delete-movie¶
Delete or undelete a movie.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
Yes |
|
|
No |
|
Response
{ "error": false }
POST /api/set-research-metadata¶
Set research_use (and optionally credit_by_name) for a movie. Only the movie’s uploader may call this endpoint — course admins are not permitted to change another user’s research metadata. When research_use is set to anything other than "1", credit_by_name is automatically cleared server-side; attribution_name is left intact.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must belong to the movie’s uploader |
|
Yes |
Movie to update |
|
No |
|
|
No |
|
Response
{ "error": false }
POST /api/set-movie-trim¶
Set one inclusive trim bound for a movie. Exactly one of trim_start_frame or
trim_end_frame must be provided per call.
Moving the start backward copies stored markers from the old start only when the destination has neither saved points nor an explicit empty annotation. An explicitly empty source is copied as a durable empty boundary too, so reopening Analyze cannot revive markers from an earlier frame at the new start. The browser waits for pending marker saves before requesting this copy. The copy reads source and destination consistently, preserving the latest save. Deleted markers are filtered through the marker map before copying, including legacy points without marker IDs. A source containing only deleted markers produces an explicit empty destination boundary.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
Yes |
|
|
Cond. |
Zero-based first frame to include in trim (provide this or |
|
Cond. |
Zero-based last frame to include in trim (inclusive; provide this or |
Response
{ "error": false, "metadata": { "movie_id": "m...", "trim_start_frame": 0, "trim_end_frame": 42, ... } }
Returns HTTP 400 with error: true if both or neither trim frame parameter is provided, or if
the resulting trim bounds are invalid (e.g. trim_start_frame > trim_end_frame).
POST /api/set-movie-fpm¶
Set the capture interval (frames/minute) for a movie. Owner or course admin only.
Editing this value only rescales the Analyze time axis and Rate statistics; it does
not require retracing. See docs/Development/AnalysisResults.rst.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
Must not be the demo key |
|
Yes |
|
|
Yes |
Capture interval in frames/minute. Positive number, fractional allowed (e.g. |
Response
{ "error": false, "metadata": { "movie_id": "m...", "fpm": "30", ... } }
Returns HTTP 400 with error: true if fpm is missing, non-numeric, not positive, or above the
allowed maximum.
POST /api/set-metadata¶
Source width and height are read-only for clients (HTTP 403), including when
a legacy record is missing one dimension. Processing supplies these values.
Set a single metadata property on a movie or user record.
Parameters
Name |
Required |
Description |
|---|---|---|
|
Yes |
|
|
Cond. |
Movie to update (provide this or |
|
Cond. |
User to update (provide this or |
|
Yes |
Property name to set |
|
Yes |
New value |
Logging¶
POST /api/get-logs¶
Return audit log entries. At least one index filter is required by the database
layer (log_user_id, course_id, or ipaddr). If the request provides none,
the API defaults to the caller’s own log_user_id. Course administrators and
super read roles may request course-wide logs. Other course members remain
restricted to their own logs even when they supply course_id.
Parameters (all optional filters)
start_time, end_time, course_id, course_key, movie_id, log_user_id, ipaddr,
count, offset
Response
{ "error": false, "logs": [ { "log_id": "...", "time_t": 1714000000, ... } ] }
POST /api/get-log¶
Legacy route that calls odb.get_logs(user_id=get_user_id()) with no request
filters. Because the database function requires an index filter, prefer
/api/get-logs.
Infrastructure¶
GET|POST /api/ver¶
Return the application version and the source commit embedded in the Lambda-web
artifact. No authentication required. git_commit is a full 40-character SHA
for deployed Lambda artifacts; local Flask runs return unavailable.
Response
{ "__version__": "0.9.7.6.2", "git_commit": "857d5637ee1949eb5bc883875eee9e7b562cd8f5", "sys_version": "3.12.x ...", "stack_name": "prod", "DYNAMODB_TABLE_PREFIX": "prod-" }
GET|POST /api/config-check¶
Check DynamoDB connectivity, S3 CORS configuration, and S3 bucket region. No authentication required.
Response
{
"dynamodb_ok": true, "dynamodb_message": "...",
"cors_ok": true, "cors_message": "...",
"bucket_region_ok": true, "bucket_region_message": "..."
}
Untraced MP4 playback contract¶
New uploads remain in processing until the shared analysis encoder has decoded every
source frame and validated its output. /api/get-movie-metadata returns
metadata.analysis_mp4 (URN, width, height, frame_count, fps, applied rotation,
SHA-256, generated_at, encoder_version, profile, pixel_format and b_frames) and
metadata.analysis_mp4_url, an authenticated signed playback URL. The source
movie_data_urn and source dimensions remain separate and unchanged. The analysis
MP4 is H.264 baseline/yuv420p, 15 fps, no B-frames, GOP 30 and CRF 18. It fits within
640 by 640 pixels, including enlargement of smaller inputs (320 by 240 becomes
640 by 480). frame_height_px is its decoded height.
All source frames survive; trim controls select analysis ranges rather than
removing frames from this derivative. Burned-in labels, API indices, and
trackpoint frame indices all count from zero.
The production analyzer requires the untraced MP4 and a compatible WebCodecs browser (tested with Chrome on macOS and Windows). It exposes a visible error if encoding is incomplete or decoding is unavailable. It never downloads a ZIP. It saves the selected frame’s markers before requesting tracing; a first marker may be saved on any frame after upload completion. Trace completion refreshes marker metadata while retaining the same analysis pixels. Frame endpoints select the derivative with no additional scaling or rotation. Tracking decodes the same untraced MP4; the traced movie renders source pixels through the same transform with marker overlays, avoiding the analysis movie’s burned-in frame numbers. No new trace generates a ZIP. Legacy backfill and bulk S3 cleanup are separate operations and are not performed by deployment of this change.
Tracing validation and saved coordinate data¶
Queueing a trace acquires its lease and marks the movie as tracing, but preserves
existing trackpoints. The worker validates the decoded frame height before
clearing subsequent points, for queued and direct tracing alike. A height
mismatch records tracing failure and releases the lease without deleting points.
Legacy first_frame_urn also finalizes geometry: rotation and source
initialization reject such records even if the frame table and dimensions are absent.
The geometry-conflict response names upload completion, processing, and saved
frames as finalization conditions, including legacy rows whose status is still
uploading. Select rotation before uploading a new movie.
When frame_height_px is missing, a validated analysis_mp4.height takes
precedence over legacy JPEG/ZIP recovery and source-dimension scaling.
Metadata-only reads use this descriptor without caching or artifact reads;
frame-range requests and downloads may persist it during coordinate migration.
Movie processing failures return status: "processing failed", processing_failed_at, and a bounded processing_failure_summary in movie metadata. The upload page stops polling and displays the reason. Retrying processing clears these failure fields and preserves the original source object.
Trackpoint writes, coordinate migration, and marker-map creation or renaming
require an upload-completion marker regardless of status or coordinate origin.
Rejected API requests return HTTP 409 without changing movie or frame records.
Legacy frame-height recovery also checks the movie-level first_frame_urn JPEG
before the ZIP fallback; metadata-only reads still avoid artifact IO.
A processing measurement that conflicts with saved frame_height_px raises the distinct height-consistency error, preserves saved coordinates, and records status: "processing failed" with processing_failed_at and processing_failure_summary. Upload polling stops and displays the failure reason. Retrying cannot overwrite the fixed height.
Conflicts with saved source width or height likewise record processing failure,
with a dimension-specific reason, even when the analysis height is missing or
matches. Retrying preserves saved dimensions, points, and source bytes.
The saved-frame check counts only non-negative frame numbers. The marker-map
companion item at frame -100 alone does not finalize geometry or block source
initialization; other finalization conditions still apply.
Traced download preparation¶
POST /resize-api/v1/download-traced (lambda-resize) accepts JSON movie_id
and optional analysis_lease_id, authenticated with x-api-key. Movie read
permission is required. It returns 200 with {ready: true, url, message: "", error: false} for a current export, or 202 with ready: false and
“Re-rendering; download the traced movie in a few minutes.” for a newly queued
or already active render. Conflicting editing/tracing/untraced-render work
returns 409; invalid requests return 400, missing authentication 401.
The server selects the source, current saved markers and inclusive trim. It never trusts a client-supplied output range or URL. Taking the render lease releases the requesting browser’s matching editing lease. Rendering preserves source bytes, frame records, trace progress, and the needs-retracing flag. Both analyzer and movie-list downloads use this endpoint instead of cached S3 URLs.
list-movies and get-movie-metadata include the worker’s named purpose in
tracking_lock. Their displayed status distinguishes rendering/reset work from
tracing. Existing analysis_mp4 fields refer to the untraced MP4; their names
remain unchanged for compatibility. Trim changes update export freshness without
enqueuing work; marker writes and renames/deletions update render_revision.