Uploads

Overview

Uploads allow you to send Grain a video, an audio file, or a transcript to create a recording.

An Upload moves through a short review pipeline: you create it, send the prepared media (if any), and Grain processes it. Once processing finishes, you can review or edit its attributes before it's promoted to a recording, or let that happen automatically.

Media Types

Type
video A video file. mp4 with specific format requirements. (See Prepare the media)
audio An audio file. mp3 only.
transcript A transcript submitted directly as Upload Attributes.

Walkthrough

0. Prepare the media

Grain only accepts media that meets its format requirements — anything that doesn't will be rejected, and the upload marked as failure. Below are the requirements for each media type, along with an ffmpeg command to produce a file that meets them, where applicable.

Video

Requirement
Container mp4
Video codec h264
Video tracks Exactly 1
Audio track Required, aac codec
Resolution ≤720p (height)
Duration At least 10s.
Maximum depends on your plan. (See https://grain.com/pricing)
File size ≤4 GiB
Keyframes Max gap of 6s between keyframes
ffmpeg -i input.mov \
  -vf "scale=-2:min(720,ih)" \
  -c:v libx264 -preset medium -crf 23 -profile:v high -level 4.0 \
  -force_key_frames "expr:gte(t,n_forced*6)" -sc_threshold 0 \
  -c:a aac -b:a 128k -ac 2 \
  output.mp4

Audio

Requirement
Container mp3
Codec mp3
Duration At least 10s.
Maximum depends on your plan. (See https://grain.com/pricing)
File size ≤4 GiB
ffmpeg -i input.wav \
  -c:a libmp3lame -b:a 128k \
  output.mp3

Transcript

N/A

1. Create Upload

Call the Create Upload endpoint with the desired media_type and attributes.

Auto Promote

auto_promote controls whether an upload moves from staged to promoting automatically, or waits for it to be promoted manually. (See Promote to Recording)

Media Type

For audio / video, the response includes a URL to send the file to. (See Send the media)

For transcript, the transcript is submitted directly as attributes, thus the transcript object is a mandatory attribute for this media type.

Attributes

These attributes will be directly populated into the recording. (See Upload Attributes)

2. Send the media

NOTE: Only for audio or video

Using the URL present in the response, upload the prepared media file via a PUT request with the appropriate headers:

curl -X PUT \
  -H "If-None-Match: *" \
  --upload-file recording.mp4 \
  "https://example.com/generated_url"

3. Review or Edit attributes

NOTE: Only when auto_promote: false

Once the upload reaches the staged status, it stops there if it was created with auto_promote: false. In this state, it can be reviewed and its attributes may be updated through the Update Upload endpoint.

4. Promote to Recording

NOTE: Only when auto_promote: false

Call Promote Upload to initialize the promoting process, which ends on the creation of the recording.

Lifecycle

Statuses

Status
initial The upload was just created. Transient — moves on immediately.
awaiting Waiting for the file to be sent.
audio / video only
processing The file was received and Grain is processing it.
audio / video only
staged Processing finished. The upload may be reviewed or edited.
promoting Being promoted into a recording. Transient.
success Upload has been promoted into a recording.
failure Something went wrong.
stateDiagram-v2
  [*] --> initial
  initial --> staged: transcript
  initial --> awaiting: audio / video
  awaiting --> processing: file received
  awaiting --> failure: upload url expired
  processing --> staged: file validated
  processing --> failure
  staged --> promoting
  staged --> failure
  promoting --> success
  promoting --> failure

Expiration & Cleanup

Uploads are automatically marked as failure if the upload URL expires without a file being received — checked 24 hours after expiry, to allow slack for large files finishing over a slow connection.

Uploads are also marked as failure if the uploaded media itself expires before being promoted.

Grain periodically deletes stale uploads that never reached success.

Creation Examples

Below are a few Create Upload requests, each highlighting one attributes detail at a time.

Multiple Speakers

participants are mapped to transcript.segments via speaker_key:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -H "Public-Api-Version: 2026-10-01" \
  --data '{
    "media_type": "transcript",
    "attributes": {
      "title": "Battle of Utapau",
      "participants": [
        {"name": "Obi-Wan Kenobi", "speaker_key": "s1"},
        {"name": "General Grievous", "speaker_key": "s2"}
      ],
      "transcript": {
        "language": "en",
        "segments": [
          {"speaker_key": "s1", "start": 0, "end": 1200, "text": "Hello there!"},
          {"speaker_key": "s2", "start": 1500, "end": 3000, "text": "General Kenobi."},
          {"speaker_key": "s1", "start": 3200, "end": 5000, "text": "You are a bold one."}
        ]
      }
    }
  }' \
  https://api.grain.com/_/public-api/v2/uploads/create

Unmatched Speaker

A speaker_key referenced by a segment but not present on any participant is not rejected — it's attributed to an auto-generated "Unknown Speaker":

curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -H "Public-Api-Version: 2026-10-01" \
  --data '{
    "media_type": "audio",
    "attributes": {
      "title": "Council Chambers",
      "participants": [
        {"name": "Obi-Wan Kenobi", "speaker_key": "s1"}
      ],
      "transcript": {
        "language": "en",
        "segments": [
          {"speaker_key": "s1", "start": 0, "end": 1500, "text": "Hello there."},
          {"speaker_key": "s2", "start": 1600, "end": 3000, "text": "Wait, who said that?"}
        ]
      }
    }
  }' \
  https://api.grain.com/_/public-api/v2/uploads/create

Team & Recorder Attribution

team_id and recorder_ids attach the resulting recording to a team and credit specific users as recorders — recorder_ids is required for workspace scope, since there's no implicit caller to default to:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -H "Public-Api-Version: 2026-10-01" \
  --data '{
    "media_type": "video",
    "attributes": {
      "title": "Sales Sync",
      "team_id": "aaaa1111-bb22-cc33-dd44-eeee55555555",
      "recorder_ids": ["kkkk1111-ll22-mm33-nn44-oooo55555555"]
    }
  }' \
  https://api.grain.com/_/public-api/v2/uploads/create

Webhooks

Upload progress can be automatically tracked via Hooks.

Uploads fire upload_added (on creation) and upload_updated (on every status change or attribute edit) hooks, in the same shape as any other hook. (See Create Hook)

Example Payload

{
  "type": "upload_updated",
  "user_id": "eeee1111-ff22-gg33-hh44-iiii55555555",
  "data": {
    "id": "eeee1111-ff22-gg33-hh44-iiii55555555",
    "media_type": "video",
    "title": "Battle of Utapau",
    "status": "staged",
    "auto_promote": false,
    "recording_id": null,
    "created_datetime": "2026-01-01T09:30:00Z",
    "updated_datetime": "2026-01-01T09:40:00Z"
  }
}