Uploads
For local files, you can’t pass a url to POST /v1/clips — the pipeline
needs an http-reachable source. The uploads endpoint mints a one-time
presigned S3 PUT URL you can stream the file to. The resulting upload_id
flows into POST /v1/clips.
There are two ways to upload:
- Single PUT — omit
size_bytes. One URL, files up to 5 GB. - Multipart — send
size_bytes. You get one URL per part (at most 100 parts), PUT each part, then callPOST /v1/uploads/:id/complete. Files up to 10 GB. Use this for long recordings and anything over 5 GB.
POST /v1/uploads
Section titled “POST /v1/uploads”Auth: Bearer JWT or API key
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
content_type | string | yes | A video MIME type, e.g. video/mp4, video/quicktime. application/octet-stream is also accepted. |
file_name | string | no | The file’s name, e.g. team offsite.mp4. The clip job’s folder is named after it. |
size_bytes | number | no | Total file size in bytes. Makes the upload multipart. Max 10,000,000,000. |
curl -X POST https://api2.choppity.com/v1/uploads \ -H "Authorization: Key $CHOPPITY_KEY" \ -H "Content-Type: application/json" \ -d '{ "content_type": "video/mp4" }'Response · 201 Created
Section titled “Response · 201 Created”| Field | Type | Description |
|---|---|---|
upload_id | string | Pass this as source.upload_id to POST /v1/clips |
upload_url | string | Single PUT only: presigned S3 PUT URL |
expires_at | number | Unix-ms timestamp when the URL(s) stop working |
part_size | number | Multipart only: bytes per part (the last part may be smaller) |
parts | array | Multipart only: { part_number, upload_url } for every part |
{ "upload_id": "9f8e7d6c-5b4a-3c2b-1a09-0f8e7d6c5b4a", "upload_url": "https://choppity-assets-dev.s3.amazonaws.com/tmp/api-uploads/...", "expires_at": 1745803600000}URL TTL
Section titled “URL TTL”The signed PUT URL is valid for 1 hour. If your upload may take longer, mint the URL right before you start streaming.
The upload_id itself stays valid as long as the underlying S3 object
exists — typically until the next overnight tmp cleanup.
Multipart uploads
Section titled “Multipart uploads”Part N is bytes (N-1) × part_size to N × part_size − 1 of the file.
PUT each part to its upload_url and keep the ETag response header S3
returns — you need every one to finish.
# part 1 of a file with part_size 104857600dd if=./recording.mp4 bs=104857600 skip=0 count=1 2>/dev/null \ | curl -sS -X PUT --data-binary @- -D - "$PART_1_URL" | grep -i etagPart URLs are valid for 4 hours. If some expire before you’re done, re-sign just those:
POST /v1/uploads/:upload_id/part-urls
Section titled “POST /v1/uploads/:upload_id/part-urls”| Field | Type | Description |
|---|---|---|
part_numbers | number[] | Up to 100 part numbers to re-sign |
Returns { upload_id, expires_at, parts: [{ part_number, upload_url }] }.
POST /v1/uploads/:upload_id/complete
Section titled “POST /v1/uploads/:upload_id/complete”| Field | Type | Description |
|---|---|---|
parts | array | Every part exactly once: { part_number, etag } |
Returns { upload_id, status: "complete" }. Calling it again is harmless.
Until it succeeds, POST /v1/clips refuses the upload with 409 CONFLICT.
If a part is missing or an etag doesn’t match what S3 received, it answers
400 BAD_REQUEST: upload that part again (re-sign it first if its URL has
expired) and call complete with its new etag. An upload that has expired or
was cancelled answers 409 CONFLICT.
DELETE /v1/uploads/:upload_id
Section titled “DELETE /v1/uploads/:upload_id”Cancels an unfinished multipart upload and discards its parts. Returns
{ upload_id, status: "aborted" }.
End-to-end flow
Section titled “End-to-end flow”-
Mint the upload URL.
Terminal window curl -X POST https://api2.choppity.com/v1/uploads \-H "Authorization: Key $CHOPPITY_KEY" \-H "Content-Type: application/json" \-d '{ "content_type": "video/mp4" }' \| tee upload.json -
PUT the file directly to S3.
Terminal window curl -X PUT --upload-file ./podcast.mp4 \-H "Content-Type: video/mp4" \"$(jq -r .upload_url upload.json)"The
Content-Typeheader must match what you sent in step 1 — S3 rejects PUTs that disagree. -
Start a clip job using the upload.
Terminal window curl -X POST https://api2.choppity.com/v1/clips \-H "Authorization: Key $CHOPPITY_KEY" \-H "Content-Type: application/json" \-d "{\"source\": { \"upload_id\": \"$(jq -r .upload_id upload.json)\" },\"criteria\": \"funny moments\"}"The pipeline streams the uploaded file in via a short-lived presigned GET URL — no public hosting required on your side.
Each upload feeds one clip job. Starting a second job with the same
upload_idanswers409 CONFLICT; upload the file again instead.
Errors
Section titled “Errors”| Code | When |
|---|---|
BAD_REQUEST | content_type missing or not a video type; a part missing or its etag not matching on complete; size_bytes not a positive integer or over 10 GB; part_numbers or parts incomplete or out of range |
CONFLICT | Completing a cancelled or expired upload, cancelling a completed one, starting a clip job before complete, or starting a second clip job on the same upload |
NOT_FOUND | Unknown upload_id |
UNAUTHORIZED | Missing or invalid credential |