Skip to content
Jump to
Cloud REST

DemoTake Cloud REST API

The HTTP API behind share links, cloud takes and the Team Library. Use it to publish exports, manage links and invitations, and move takes between DemoTake for Mac, DemoTake Web and your own tools.

Overview

Value
Base URL https://api.demotake.my
Version v1 (in the path)
Format JSON with snake_case keys; times are ISO-8601 UTC strings
Auth Authorization: Bearer <session_token>
Share pages https://demotake.my/s/<slug>

Most of this API needs a plan: creating share links needs a plan with share links (Local and up); cloud takes need a subscription with DemoTake Web (Director, Team or Enterprise), apart from each account's one free take; the Team Library needs Team. Watching a link, entering an access code and commenting never need a plan. See Editions and plans.

  1. POST /v1/sharescreate the link
  2. PUT upload.urlupload the file
  3. demotake.my/s/<slug>share it
Publishing an exported video as a link takes two requests.

Authentication

API requests carry a session token as a bearer token. Sign-in is passwordless: request a one-time code by email, then exchange it for a token.

Terminal
# 1. Send a 6-digit code to your email (valid for 5 minutes)
curl -X POST https://api.demotake.my/api/auth/email-otp/send-verification-otp \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "type": "sign-in"}'

# 2. Exchange it for a session token
curl -X POST https://api.demotake.my/api/auth/sign-in/email-otp \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "otp": "123456"}'
# → {"token": "…", "user": {…}}

export DEMOTAKE_TOKEN="…"
curl https://api.demotake.my/v1/me -H "Authorization: Bearer $DEMOTAKE_TOKEN"

GET /v1/me returns your user, your workspaces (with your role and plan) and the active workspace. Revoke a token with POST /v1/auth/signout. Auth endpoints are rate limited.

Warning:

A session token acts as you. Keep it in a secret store, never in a repository, and sign it out when a script no longer needs it.

Errors

Errors are non-2xx responses with a code and a readable message:

JSON
{ "error": { "code": "payment_required", "message": "Your plan doesn't include share links." } }
Status Code Meaning
400 invalid_request The body or parameters are invalid
401 unauthorized Missing or expired token
401 password_required An access-code link without a valid code
402 payment_required Your plan lacks the feature, or storage is full
403 forbidden Not allowed (for example an invite-only link)
404 not_found Doesn't exist, or you can't see it
409 conflict A stale write (see Cloud takes)
410 expired, revoked A share link that ended
429 rate_limited Slow down; honor Retry-After (seconds)

A share link publishes one exported file (a video, a GIF or a Reviewer Pack) at https://demotake.my/s/<slug>. The slug is random; the shr_… id is your handle for managing it.

Terminal
curl -X POST https://api.demotake.my/v1/shares \
  -H "Authorization: Bearer $DEMOTAKE_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "take_id": "take_m3ehbyc0k7qv2x9d",
    "title": "Invoice export",
    "kind": "video",
    "file_name": "invoice-export-16x9.mp4",
    "content_type": "video/mp4",
    "size_bytes": 18234567,
    "expires_in_days": 30,
    "allow_download": true
  }'

The response is the share object plus an upload. For files up to 95 MB it is a single PUT:

JSON
{
  "share_id": "shr_…",
  "url": "https://demotake.my/s/Qm9vZ2xlLXN0eWxlLXNsdWc",
  "status": "pending_upload",
  "visibility": "public",
  "upload": { "method": "PUT", "url": "https://api.demotake.my/v1/uploads/…", "headers": { "Content-Type": "video/mp4" } }
}
Terminal
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @invoice-export-16x9.mp4

Larger files get {"multipart": {"upload_id", "part_size", "create_part_url_template", "complete_url"}}: upload each 1-based part (every part except the last is exactly part_size bytes) with PUT …/parts/{n}, collect the etags, then POST …/complete {"parts": [{"n", "etag"}]}. Upload URLs need no auth header (the token in the URL is the credential), expire after 24 hours and need Content-Length.

Access options

These work on POST /v1/shares, POST /v1/takes/:id/shares and (except invite) PATCH /v1/shares/:id:

Field Values
visibility public, code or invite. Default: code when a code is given or generated, invite when people are given, else public
access_code / generate_code Your own code (4–128 characters), or true to get a generated one like ABCD-EFGH, returned once
invite Up to 50 email addresses (or names of people you already work with)
note Up to 500 characters, included in the invitation email
expires_in_days 1–365 (default 30, clamped to your plan); null for no expiry (subscriptions only)
allow_download Default true

Codes are stored hashed: nobody can read one back, you can only set or generate a new one. Wrong codes are limited to 5 guesses per visitor and 50 per link every 15 minutes.

HTTP
GET    /v1/shares?workspace_id=&take_id=      list, with views, comments and invite counts
GET    /v1/shares/:id                         one link with its invites
PATCH  /v1/shares/:id                         title, allow_download, expires_in_days, visibility, access_code, generate_code,
                                              analytics_enabled, show_badge, cta, chapters, tracks
GET    /v1/shares/:id/analytics?range=30      views, unique viewers, watch time, completion, drop-off, … (7, 30 or 90 days)
POST   /v1/shares/:id/revoke                  stop the link now
DELETE /v1/shares/:id                         delete the link and the media it owns
GET    /v1/shares/:id/invites
POST   /v1/shares/:id/invites                 {"people": [...], "note"?}   (invite links only)
DELETE /v1/shares/:id/invites/:invite_id
POST   /v1/shares/:id/review                  {"status": "approved|changes_requested", "note"}
Terminal
# Switch a link to an access code and get the new code back once
curl -X PATCH https://api.demotake.my/v1/shares/shr_… \
  -H "Authorization: Bearer $DEMOTAKE_TOKEN" -H "Content-Type: application/json" \
  -d '{"visibility": "code", "generate_code": true}'

views counts unique visitors per day, without cookies and without storing IP addresses; views by your own workspace don't count. Revoking stops the page and its media immediately.

Embeds, calls to action and analytics

Video and interactive demo links come with embed_url (https://demotake.my/s/<slug>/embed, which may be framed on any site; options autoplay=1, t=<seconds>, title=0) and embed_html, a responsive <iframe> snippet. GET https://demotake.my/oembed?url=<share URL>&format=json answers oEmbed for live public links, so Notion, Slack and Medium embed the player from the plain link.

Terminal
# A "Book a demo" button at the end and a link 30 seconds in, with UTM parameters (Director, Team, Enterprise)
curl -X PATCH https://api.demotake.my/v1/shares/shr_… \
  -H "Authorization: Bearer $DEMOTAKE_TOKEN" -H "Content-Type: application/json" \
  -d '{"cta": {"end": {"kind": "lead", "text": "Book a demo", "url": "https://example.com/demo"},
               "mid": {"text": "See pricing", "url": "https://example.com/pricing", "at": 30},
               "utm": {"campaign": "launch"}}}'

CTA links must be https://; viewers open them in a new tab. "cta": null removes them, "show_badge": false hides "Made with DemoTake" (subscriptions), "analytics_enabled": false stops analytics for the link. Videos can also carry chapters ([{"title","start"}], scene starts in seconds) and tracks ([{"lang","label","vtt"}], WebVTT subtitles).

GET /v1/shares/:id/analytics?range=7|30|90 returns views (page and embed), unique viewers, average watch time, completion rate, the drop-off curve mapped to scenes with the biggest drop, referrer hosts, countries, devices, languages and CTA clicks (views and unique viewers only on Local). DemoTake keeps daily counts only: no cookies, IP addresses or user agents. Viewers with Do Not Track or Global Privacy Control count as a view and nothing more.

Public endpoints

No token needed. These power the share page, and you can use them to embed or check a link.

HTTP
GET  /v1/public/shares/:slug                 title, media_url (signed, 1 hour), comments, …
GET  /v1/public/shares/:slug/meta            link preview data (public, live links only); never counts a view
GET  /v1/public/shares/:slug/poster          the poster image (public, live links only)
POST /v1/public/shares/:slug/unlock          {"code"} → {"grant", "expires_at"}   (valid 12 hours)
POST /v1/public/shares/:slug/comments        {"name", "text", "timecode"?}
POST /v1/public/shares/:slug/events          analytics beacon from share pages and embeds (see API.md §13.9)

For code links, send the grant as X-Share-Grant on the GET and comment requests. Invite-only links work for signed-in accounts on the invite list. Media URLs support HEAD and Range requests for seeking.

Scheduled re-recording

Team and Enterprise (ci_regeneration). A schedule re-runs a cloud take (its demotake.json, or the take's own recorded steps) on a cron timetable in a time zone, after a deploy webhook, or on demand. A runner (a Mac with DemoTake Agent, or demotake runner in CI on a Mac) claims each run, re-records the scenes whose UI changed, keeps the edits, exports, compares the scenes with the last run and reports back. A successful run can update the take's share links in place: same URL, embeds and analytics, a new version, the old one kept for rollback.

HTTP
GET    /v1/schedules?take_id=…                  schedules, this month's usage, the plan's limits, runners online
POST   /v1/schedules                            {"take_id","cron"?,"timezone"?,"webhook"?,"options"?,"publish_mode"?,"notify"?,…}
PATCH  /v1/schedules/:id                        any of the same fields
DELETE /v1/schedules/:id
POST   /v1/schedules/:id/run                    run now (a run already waiting is returned instead)
POST   /v1/schedules/:id/rotate-token           new trigger token (shown once)
GET    /v1/schedules/:id/runs                   recent runs
GET    /v1/runs/:id                             one run: scenes changed/unchanged/failed with before/after stills, checks, exports
POST   /v1/runs/:id/approve | reject | rollback | cancel
GET    /v1/runners  ·  POST /v1/runners {"name"}  (token shown once)  ·  DELETE /v1/runners/:id

options: only_changed (default true), record (false = test only, never publishes), target_url (staging or a preview deploy), formats (16:9, 9:16, 4:3, 1:1, gif, reviewer_pack), languages, interactive. publish_mode: approval (default), auto or off. notify: on (changes, always, failures, never), email (admins, creator, none), slack_url (an incoming webhook), webhook_url (signed JSON). executor: "cloud" checks a web spec in DemoTake's cloud browser without a Mac (checks and scene comparison only).

Deploy webhooks

With "webhook": true the create response has trigger_token and hook_url, shown once (the server keeps a hash). Trigger a run after a deploy in any of these ways:

Terminal
# Bearer token (GitHub Actions, scripts)
curl -X POST https://api.demotake.my/v1/schedules/sch_…/trigger -H "Authorization: Bearer $DEMOTAKE_TRIGGER_TOKEN"

# Signed: X-DemoTake-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(token, "<t>.<body>")>, within 5 minutes
# GitHub repository webhook (deployment_status): secret = the trigger token (X-Hub-Signature-256 is checked)
# Vercel or Netlify: paste hook_url (https://api.demotake.my/v1/hooks/schedules/dtt_…) as the deploy notification URL

Signed deliveries can't be replayed (409). Only finished, successful deploys start a run; a trigger while a run is still waiting returns that run (coalesced). Webhook notifications carry X-DemoTake-Signature signed with the schedule's signing_secret (shown once when you set notify.webhook_url).

Cloud takes

A cloud take is a .take package stored in DemoTake Cloud: take.json plus media/, tracks/, snapshots/, exports/ and thumbnail.jpg. It's what DemoTake Web edits and what Sync to DemoTake Web uploads from the Mac. All of these endpoints need a subscription with DemoTake Web on the take's workspace (402 otherwise). The one exception is an account's free take: once it's recorded in DemoTake Web, its owner can list, open, save, upload exports to and share that one take without a subscription. GET /v1/takes/:id then adds "trial": true, and its exports carry the “Made with DemoTake” watermark. Anything else for that account (another take, imports, syncing from the Mac, collaborators) answers 402 with a "reason" such as "trial_used". GET /v1/entitlements describes the free take in "web_trial".

HTTP
GET    /v1/takes?workspace_id=&q=             list: name, goal, purpose, status, duration, scene_count, assertions, source
POST   /v1/takes                              create from {"name", "goal"?, "purpose"?, "outputs"?, "scenes"?}, or {"take": {…take.json…}}
GET    /v1/takes/:id                          take.json, its etag, your access, and signed media URLs (1 hour)
PUT    /v1/takes/:id                          replace take.json; needs If-Match: <etag>
DELETE /v1/takes/:id
POST   /v1/takes/:id/files                    {"files": [{"path", "size_bytes", "content_type"}]} → upload URLs
POST   /v1/takes/import/uploads               {"size_bytes"} → an upload for a .take.zip
POST   /v1/takes/import                       {"upload_path"} | {"library_id"} → a new cloud take
POST   /v1/takes/:id/shares                   share an uploaded export without re-uploading it
Terminal
# Create a cloud take with two scenes, ready to record in DemoTake Web
curl -X POST https://api.demotake.my/v1/takes \
  -H "Authorization: Bearer $DEMOTAKE_TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Checkout", "purpose": "marketing", "scenes": [{"title": "Add to cart"}, {"title": "Pay", "intent": "Pay with the test card"}]}'
# → 201 {"take_id": "take_…", "take": {…}, "etag": "…"}

Concurrency. Writes to take.json are compare-and-swap: PUT needs If-Match with the etag you last read. If someone saved in between, you get 409 conflict with the current etag; read again, merge, retry. If-Match: * overwrites on purpose. Media files are last-writer-wins per path.

Files. Paths live under media/, tracks/, snapshots/ or exports/ (or are thumbnail.jpg), up to 200 per request. Video in media/ and exports/ can be up to 5 GB per file. Everything counts toward your workspace's storage.

Import. Upload a .take.zip (for example from demotake export <take> --format package or take_export_package) to the URL from POST /v1/takes/import/uploads, then POST /v1/takes/import with the returned upload_path. Each import makes an independent copy; anything unexpected in the package is skipped with a warning.

Share an export. POST /v1/takes/:id/shares {"export_path": "exports/checkout-16x9.mp4", "title", "kind": "video"} plus the access options above. The link is ready at once and uses the take's file, so it takes no extra storage.

Collaborators and comments

Invite people outside your workspace to one cloud take as a viewer, commenter or editor. Editors need their own subscription to edit; viewers and commenters need no plan.

Role Open and watch Comment Edit Delete, share, invite
viewer ✓
commenter ✓ ✓
editor ✓ ✓ ✓ (with their own subscription)
workspace member ✓ ✓ ✓ ✓
HTTP
GET    /v1/takes/:id/collaborators
POST   /v1/takes/:id/collaborators            {"people": ["ana@example.com"], "role": "commenter"}
PATCH  /v1/takes/:id/collaborators/:cid       {"role"}
DELETE /v1/takes/:id/collaborators/:cid       remove someone (or leave, as that collaborator)
GET    /v1/takes/:id/comments
POST   /v1/takes/:id/comments                 {"text", "timecode"?}
GET    /v1/shared-with-me                     private links shared with you and takes you collaborate on

Invitations are limited to 10 requests a minute and 100 new people a day, and at most 200 people per link or take.

Team Library

On the Team plan, takes can be published to a shared library as zipped .take packages with a version history and a review status.

HTTP
POST /v1/library/takes                        publish a version → upload URLs for the zip and thumbnail
GET  /v1/library/takes?workspace_id=&q=&tag=  search
GET  /v1/library/takes/:library_id            one take with a signed download URL and its versions
POST /v1/library/takes/:library_id/review     {"status", "note"}
GET  /v1/library/activity?workspace_id=       recent activity
GET  /v1/workspaces/:id/brand-kit             the team's shared style and edit rules
PUT  /v1/workspaces/:id/brand-kit

Workspaces

HTTP
GET    /v1/me                                 you, your workspaces and roles, the active workspace
PATCH  /v1/me                                 {"name"?, "active_workspace_id"?}
POST   /v1/workspaces                         {"name"}
GET    /v1/workspaces/:id/members
POST   /v1/workspaces/:id/invites             {"email", "role"}
DELETE /v1/workspaces/:id/members/:user_id    owners and admins; owners can't be removed

Where a request takes an optional workspace_id, it defaults to your active workspace.