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.
- POST /v1/sharescreate the link
- PUT upload.urlupload the file
- demotake.my/s/<slug>share it
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.
# 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.
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:
{ "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) |
Share links
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.
Create a link and upload the file
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:
{
"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" } }
}curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @invoice-export-16x9.mp4Larger 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.
Manage links
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"}# 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.
# 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.
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.
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/:idoptions: 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:
# 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 URLSigned 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".
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# 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 | ✓ | ✓ | ✓ | ✓ |
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 onInvitations 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.
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-kitWorkspaces
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 removedWhere a request takes an optional workspace_id, it defaults to your active workspace.