Skip to main content

Locks & Approvals

The controls that make a video library auditable: freeze content while it's being signed off, route it to named approvers, and leave review comments.

Locks​

A lock freezes a video's content. While locked:

Locking cascades: the linked training module and knowledge-base articles freeze with it, so the whole package stays consistent.

Lock​

POST /v1/videos/:id/lock

curl -X POST https://api.vidocu.ai/v1/videos/vid_abc123/lock \
-H "Authorization: Bearer vdo_live_your_key_here"
{
"locked": true,
"lock": { "lockedAt": "2026-08-12T09:14:00.000Z", "lockedBy": "usr_...", "lockedByName": "Dana Levi" }
}

Locking an already-locked video is a no-op, not an error - you asked for it to be locked, and it is.

Requires the canLock permission: owner, admin or creator.

Unlock​

DELETE /v1/videos/:id/lock

Refused with 409 unless the video's latest approval round has been withdrawn.

That rule has a consequence worth knowing before you send anything for approval: only a pending or rejected round can be withdrawn. Once a round is approved, it can't be withdrawn, so the video can no longer be unlocked - and a locked video can't be deleted either.

An approved video is therefore permanent. That is deliberate: an approved package is a signed-off record. Plan for it, because there is no route back.

Latest roundCan unlock?
noneyes
withdrawnyes
pending / rejectedwithdraw it first, then yes
approvedno - permanently

Approvals​

POST /v1/videos/:id/approvals

FieldTypeDescription
approversstring[]Workspace member user ids - every one must approve
messagestringNote to the approvers
curl -X POST https://api.vidocu.ai/v1/videos/vid_abc123/approvals \
-H "Authorization: Bearer vdo_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"approvers": ["usr_a1", "usr_b2"], "message": "Final check before we publish this SOP."}'

The round passes when every named approver approves; a single rejection fails it. Email and display name are resolved from the workspace's own member records, so the request keeps a usable snapshot even if someone leaves later.

Deciding​

POST /v1/approvals/:id/decide with {"decision": "approved" | "rejected", "comment": "..."}

Only a named approver can decide, and only once - a second attempt returns 409.

Withdrawing​

POST /v1/approvals/:id/withdraw clears the video's approval state entirely, so it goes back to showing none. This is what has to happen before a locked video can be unlocked.

Only pending and rejected rounds can be withdrawn; withdrawing an approved one returns 409.

Listing​

GET /v1/approvals (filter by status or videoId) · GET /v1/videos/:id/approvals

Comments​

GET /v1/videos/:id/comments · POST /v1/videos/:id/comments DELETE /v1/videos/:id/comments/:commentId

Review comments, optionally pinned to a moment in the video:

curl -X POST https://api.vidocu.ai/v1/videos/vid_abc123/comments \
-H "Authorization: Bearer vdo_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"text": "Step 4 skips the safety check.", "timestamp": 42.5}'

Who actions are attributed to​

Locks, approvals and comments all record who did them - that's what makes them auditable.

  • OAuth tokens are attributed to the token's user.
  • API keys authenticate as a workspace, not a person, so they're attributed to the workspace owner.

If you need per-person attribution in an audit trail, use OAuth rather than a shared API key.

Errors​

StatusCodeCause
400validation_errorAn approver isn't a member of the workspace, or the body failed validation
403insufficient_roleYour role can't lock content
404not_foundVideo, approval request or comment not found
409conflictUnlock blocked by a round that isn't withdrawn (an approved one never can be); already decided; or not an approver
423lockedYou tried to change locked content

AI provenance​

Text that Vidocu generates - help articles, scripts, subtitle rewrites and translations, quiz questions - is marked as machine-generated in a machine-readable form. This satisfies our obligation as the provider of that generation system under Article 50(2) of the EU AI Act (Regulation (EU) 2024/1689), which applies to systems already on the market from 2 December 2026.

Two things this is not:

  • It is not visible. No badge, no label, no change to how an article, PDF or knowledge-base page looks. Article 50(4)'s visible-labelling duty falls on deployers publishing to inform the public on matters of public interest, and does not reach internal work instructions that pass human review.
  • It is not a workspace setting. The duty is ours as provider, not a customer preference, so it is always on and cannot be switched off.

Synthetic speech is marked too, in the video file and the package record. See AI-generated audio.

The record​

FieldMeaning
aiGeneratedAlways true when present
digitalSourceTypeIPTC Digital Source Type URI - trainedAlgorithmicMedia, meaning the text was produced by a trained AI model
kindarticle, articleTranslation, script, subtitleRewrite, subtitleTranslation, quiz, quizTranslation or transcription
provider / modelThe model that actually ran
generatedAtWhen it was generated
sourceHashHash of the text as generated, ignoring image URLs and whitespace
humanEditedtrue once the text has been edited since. Derived by comparing the current text against sourceHash, so it cannot go stale
approvalApprover names and decision time, read from the approval record

digitalSourceType carries the IPTC controlled-vocabulary term rather than a flag only we understand, so an off-the-shelf provenance reader recognises it.

kind: "transcription" is speech-to-text of a real person speaking. A trained model produced the text, so it is marked, but the kind stays distinct from synthesised prose.

Where the marking appears​

SurfaceCarrier
Article PDFXMP packet (Iptc4xmpExt:DigitalSourceType + vidocu:*)
Article DOCXdocProps/custom.xml custom properties, plus core description and keywords
Article HTML<meta name="ai-*"> tags in <head>
Article MarkdownYAML front matter
Knowledge-base page<meta name="ai-*"> tags in <head>
WebVTTA NOTE block after the WEBVTT header
Package ZIPaiProvenance in manifest.json, per artifact
REST APIaiProvenance in the JSON body and an X-Content-Provenance response header
Help-centre publishAn HTML comment ahead of the article body

AI-generated audio​

A video's sound is marked as AI-generated when any of these is true:

  • A Vidocu voiceover. The narration is read by an AI voice (ElevenLabs). Detected from the video itself; nothing to set. A "Keep my own voice" video is not counted: its voice is the speaker's own recording.
  • AI avatar speech (HeyGen). Detected the same way.
  • Declared by the owner. An imported video whose own audio was already synthesized with another tool. Tick "The voice in this recording is AI-generated" in the upload window next to "Keep my own voice", or "The video's own audio is AI-generated" in the editor's Video tab, or send aiGeneratedAudio: true on POST /v1/videos/upload or PATCH /v1/videos/:id. Who declared it and when are recorded. A locked video can't change it.
SurfaceCarrier
Exported MP4Container metadata: comment and description (a plain sentence), ai_generated_audio=true, ai_audio_sources, digital_source_type (the IPTC trainedAlgorithmicMedia URI). Written with a stream copy, nothing is re-encoded.
Package ZIPaiProvenance.audio in manifest.json: sources (vidocu-voiceover, vidocu-avatar, declared-by-customer), providers, declaredBy, declaredAt
REST APIaiGeneratedAudio on the video (the owner's declaration)

Declaring or withdrawing marks the last export as out of date, so the next export carries the change.

This is a metadata declaration, not a watermark in the sound itself. ElevenLabs separately embeds its own inaudible watermark (SynthID) in the voice it generates. It survives our export steps, but background music loud enough under the voice can make it undetectable, so do not rely on it alone.

Courses and Studio​

Courses. The planner and storyboarder write the course title, summary, module objectives, scene copy and the narration script. The marking sits on the plan (aiProvenance on the course) and per module (on the storyboard), and it appears on GET /v1/courses, GET /v1/courses/:id, GET /v1/courses/:id/storyboard, the public course share payload, and the corresponding MCP tools. When a module is generated into a video, the narration carries the marking onto that video's subtitles, so every video surface - SRT and VTT download, the package manifest, X-Content-Provenance - picks it up too.

Studio. The Studio assistant can write text clips, captions and slide copy. The marking is recorded at project level, not per clip: the editor saves the whole document at once, and by the time it reaches the server an assistant-written clip is indistinguishable from one the user typed. What the record claims is therefore the checkable thing - this project contains text written by that model. It appears on GET /v1/studio/projects/:id and its MCP tool, and it travels with a project cloned from a template. The Studio share page and the MP4 export are burned-in pixels, so there is no text there to mark.

One known gap: importing a video into Studio turns its AI-written narration into caption clips, and that project is not marked from the import alone. The server has no way to verify which model wrote the imported text, and stamping it with the Studio assistant's model would record something untrue - so it records nothing. The source video keeps its own marking.

AI-generated footage​

Stills and clips from AI footage are synthetic images and video, and we are the provider of the system that makes them. Each one is recorded server-side when it is generated.

  • Every /v1/ai-media item carries aiGenerated: true and the IPTC trainedAlgorithmicMedia term.
  • A Studio export whose timeline uses AI footage carries a sentence in the MP4's comment and description metadata naming the models, with the IPTC term for an edit that mixes in AI-generated elements (compositeWithTrainedAlgorithmicMedia).
  • Which media counts as AI is decided from our generation records, never from anything the browser sends, so a request can neither hide AI footage nor mark a real recording as AI.
  • The image and video models also add their own invisible watermark (SynthID for Google's models) to the raw files.
  • Animated scenes are text Claude wrote. A Studio project that contains one carries the project aiProvenance record (kind: "studioText"), set by the server from the timeline itself.

Where it deliberately does not appear​

Article 50(2) requires marking that is effective "as far as this is technically feasible". Two formats have no slot that satisfies it, and rather than damage the file we record the marking beside it:

  • SRT has no comment syntax. A pseudo-comment would risk a player, NLE or subtitle importer rejecting the file. SRT bytes are therefore left exactly as they were, and the marking travels in the X-Content-Provenance response header, in the package manifest, and in a subtitles.srt.provenance.json sidecar inside the package ZIP. The same applies to ASS/SSA, SUB and SBV from the subtitle-translator tool.
  • Notion pages are lists of block objects with no comment or metadata block. Every carrier the Notion API offers - callout, paragraph, code, icon, page property - renders to the reader, and the marking must not change what the reader sees.

Watermarking​

Text watermarking in the SynthID sense has to be applied while the model is generating, with access to its output probabilities. Vidocu generates text through third-party model APIs that do not expose that, so it is not available to us at any price. The June 2026 Transparency Code of Practice recognises provenance metadata as the other accepted mechanism, and that is what is described above. If the models we use ever expose watermarking, we will add it alongside this.