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.mp4Audio
| 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.mp3Transcript
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
audioorvideo
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/createUnmatched 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/createTeam & 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/createWebhooks
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"
}
}