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:
- Analyze, voiceover,
translate, article and
script edits all return
423 Locked - Exporting still works - rendering an artifact from frozen content is the point
- Moving the video between projects still works - filing isn't content
- Deleting is refused, including through a cascading project delete
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 round | Can unlock? |
|---|---|
| none | yes |
| withdrawn | yes |
| pending / rejected | withdraw it first, then yes |
| approved | no - permanently |
Approvals
POST /v1/videos/:id/approvals
| Field | Type | Description |
|---|---|---|
approvers | string[] | Workspace member user ids - every one must approve |
message | string | Note 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
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | An approver isn't a member of the workspace, or the body failed validation |
| 403 | insufficient_role | Your role can't lock content |
| 404 | not_found | Video, approval request or comment not found |
| 409 | conflict | Unlock blocked by a round that isn't withdrawn (an approved one never can be); already decided; or not an approver |
| 423 | locked | You 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 a separate matter: the provider of that generation system is the text-to-speech vendor, and its marking is theirs to apply.
The record
| Field | Meaning |
|---|---|
aiGenerated | Always true when present |
digitalSourceType | IPTC Digital Source Type URI - trainedAlgorithmicMedia, meaning the text was produced by a trained AI model |
kind | article, articleTranslation, script, subtitleRewrite, subtitleTranslation, quiz, quizTranslation or transcription |
provider / model | The model that actually ran |
generatedAt | When it was generated |
sourceHash | Hash of the text as generated, ignoring image URLs and whitespace |
humanEdited | true once the text has been edited since. Derived by comparing the current text against sourceHash, so it cannot go stale |
approval | Approver 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
| Surface | Carrier |
|---|---|
| Article PDF | XMP packet (Iptc4xmpExt:DigitalSourceType + vidocu:*) |
| Article DOCX | docProps/custom.xml custom properties, plus core description and keywords |
| Article HTML | <meta name="ai-*"> tags in <head> |
| Article Markdown | YAML front matter |
| Knowledge-base page | <meta name="ai-*"> tags in <head> |
| WebVTT | A NOTE block after the WEBVTT header |
| Package ZIP | aiProvenance in manifest.json, per artifact |
| REST API | aiProvenance in the JSON body and an X-Content-Provenance response header |
| Help-centre publish | An HTML comment ahead of the article body |
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.
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-Provenanceresponse header, in the package manifest, and in asubtitles.srt.provenance.jsonsidecar 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.