Skip to content

Uploading videos

Uploading needs a token granted the content:write scope, which a personal access token does not currently hold, so this is an OAuth path today. The same scope covers creating documents and adding YouTube videos; announcements have a scope of their own.

The file never passes through us. You ask for a presigned storage URL, upload the bytes to it directly, then tell us the upload landed.

Whether an uploaded video goes into the library or waits for an administrator is decided for you, from what the person behind the token may do and the audience you asked for. Every response says which happened, in mode:

mode What it means
published The video goes into the library once it has processed.
review The video waits for an administrator to approve it, and is not visible to learners until then.

Someone who may publish to the audience they named gets published. Anyone else who is allowed to submit content gets review, including a team admin asking for a wider audience than their teams. The audience you send is not stored on a video that goes to review: whoever approves it chooses one. A video waiting for review is still fully processed, so the reviewer can watch it, and the person who started the upload can finish it. Nobody else can.

Show the difference to whoever uploaded it. “Your video is live” and “your video is with a reviewer” are not the same message.

Add a sourceKey to the start of an upload (single or multipart): any string you choose that names what the video was generated from, up to 200 characters. The first upload under a key records it on the video. Sending the same key again does not replace that video. A video’s file cannot be swapped without viewers seeing the new one before anyone has approved it, so a repeat is refused with 409 and SOURCE_KEY_UPDATE_UNSUPPORTED, and nothing is created. To make a separate video from the same source, use a new key. Guides work differently: uploading a guide again under the same key does replace it.

The start response includes updatesExisting, which is always false for a video, so a client written for guides keeps working.

Two things about a key are the same as for guides:

  • It is unique per organization, so two uploads racing to create the same key produce one video and one refusal, never two videos.
  • When you cannot publish, the upload is a proposal for an administrator. Uploading again under the same key while your own proposal is still pending replaces it with a fresh one, and the response says replacedPending: true. The earlier proposal is withdrawn only once the new one exists, and an administrator’s decision made in the meantime stands. Send "stack": true to file another beside it, which carries no key. Someone else’s pending proposal is never touched: you get 409 and SOURCE_KEY_AWAITING_REVIEW. If an administrator rejects a first proposal, the key is released.
Code Meaning
SOURCE_KEY_UPDATE_UNSUPPORTED A video already exists under this key and videos cannot be replaced yet.
SOURCE_KEY_BUSY The video is still being processed, or another upload for it is in progress. Try again in a few minutes. An upload that was started and abandoned holds the key for up to four hours.
SOURCE_KEY_AWAITING_REVIEW A proposal for this source is waiting in the review queue. It has to be approved or rejected first.

Under roughly 100 MB, use the single-request flow: one presigned URL, one PUT.

Above that, use multipart. It costs an extra call but each part can be retried on its own, so a dropped connection near the end of a large file does not mean starting over.

POST /v1/videos/upload/init

Creates the video and returns a URL to PUT the file to.

{
"filename": "safety-walkthrough.mp4",
"contentType": "video/mp4",
"public": false,
"teamIds": []
}

public and teamIds are required — the video’s audience is decided when it is created, not afterwards, so there is no default to fall back on.

  • public: true shares the video with everyone in the organization. Only a caller with organization-wide create permission may do this.
  • public: false with one or more teamIds shares it with those teams. A caller whose permission is team-scoped must name at least one team they belong to, or the request is refused.

The response carries the video’s id, the URL, and when the URL stops working:

{
"videoId": "…",
"mode": "review",
"uploadUrl": "https://…",
"method": "PUT",
"expiresIn": 3600,
"expiresAt": "2026-09-01T13:00:00.000Z",
"requiredHeaders": { "Content-Type": "video/mp4" }
}

Send requiredHeaders verbatim on the PUT when it is present. Storage rejects the upload if the content type does not match what the URL was signed for.

POST /v1/videos/{id}/upload/complete

Call this once the PUT has succeeded. We check the object actually arrived and then begin processing — transcription, chaptering, and the rest.

{
"id": "…",
"title": "safety-walkthrough",
"status": "PROCESSING",
"mode": "published",
"url": "https://mereon.ai/videos/…/view"
}

The video is not watchable when this returns. status reflects that, and the title is seeded from the filename until processing produces a better one. url is the video’s page in the web app, so you can hand it to someone; it will say the video is processing for a few minutes. It is present only when mode is published: a video waiting for review is not visible to learners, so there is nothing to link to. The multipart flow returns the same shape when it completes.

An empty or missing object answers 400: the upload genuinely did not land, so retry the PUT. A storage failure on our side answers 503 — retry this call, not the upload.

POST /v1/videos/upload/multipart/init

Same body as the single-request flow plus fileSize in bytes, which we use to decide how the file is divided.

The response tells you the part size and gives a presigned URL per part. Part numbers start at 1. Upload each part and keep the ETag storage returns for it — completing the upload requires all of them.

Part size grows rather than part count when a file is large enough to exceed the storage part limit, so do not assume a fixed size.

POST /v1/videos/{id}/upload/multipart/urls

Part URLs expire. Send the part numbers still outstanding and you get fresh URLs for those, rather than starting the upload again.

{ "partNumbers": [7, 8, 9] }

POST /v1/videos/{id}/upload/multipart/complete

Send every part with its ETag:

{ "parts": [{ "partNumber": 1, "eTag": "\"abc…\"" }] }

The parts are assembled into one object, verified, and processing begins. The response matches the single-request flow.

DELETE /v1/videos/{id}/upload/multipart

Cancels an upload that is still in progress and removes the placeholder video.

This only applies to an upload in flight. A video whose upload has already completed is refused with 400 — deleting finished content is not part of this scope, and is not something the upload endpoints can do.

Status Meaning
400 The body is invalid, the file type is not a video, the audience is not resolvable, or the upload has not landed.
403 The token lacks content:write, or its user can neither add videos nor submit them for review in this organization.
409 A sourceKey you sent is already in use. The error code says how: see above.
404 The video does not exist, or belongs to somewhere the caller cannot see, or is someone else’s upload that is still waiting for review. The cases are deliberately indistinguishable.
503 Storage could not be reached to verify the upload. Retry the call.