Skip to main content

Files & storage

Every piece of media the API touches is a storage object identified by a storageId. Media never travels inside a JSON body — you get it into storage first, then reference the id from generations, Applications, and workflow runs.

Two ways in:

  • Import from a URL — one call. The right choice for automation and agents.
  • Ticket → PUT → commit — three calls, for local bytes you can't expose at a URL.

The storage object

FieldTypeDescription
storageIdstringDurable id (UUID). This is what every media parameter takes.
filenamestring | nullOriginal filename, when one was given.
contentTypestringMIME type.
sizeBytesnumberSize on disk.
originenumupload (you put it there) or generated (a job produced it).
urlstringFreshly signed read URL. Expires — re-fetch rather than storing it.
createdAtstringISO 8601.

Limits

LimitValue
Max file size1 GB
Accepted content typesimage/*, video/*, audio/*, plus application/octet-stream and JavaScript types for plugin bundles
Storage quotaFree 5 GB · Standard 100 GB · Pro 500 GB · Max 1 TB

The quota is hard — there's no overage billing. At the cap, uploads and imports fail with storage_limit (402) until you delete objects or upgrade. Generation results always save, so a full account still gets its outputs.

Object lifecycle

An object is staged when a ticket is issued and committed once the bytes are confirmed. Only committed objects can be used as inputs — passing a staged id returns Unknown or uncommitted storageId. POST /v1/uploads/import commits in the same call; the ticket flow needs the explicit commit step.


POST /v1/uploads/import

Fetch an https URL server-side and commit it as an owned asset. One call from URL to usable storageId.

Body

NameTypeRequiredDescription
urlstringyesMust be https. The server fetches it and inspects the response's content type.
filenamestringnoOverrides the name derived from the URL path.

Returns { storageId, url, sizeBytes } — already committed.

Request
curl -s -X POST https://api.corte.so/v1/uploads/import \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example.com/products/trail-shoe.jpg",
"filename": "trail-shoe.jpg"
}'
Response
{
"storageId": "b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8",
"url": "https://media.corte.so/…/trail-shoe.jpg?X-Amz-Signature=…",
"sizeBytes": 482113
}

Errors

CodeWhen
invalid_paramsNot an https URL, unreachable, non-2xx from the origin, or a content type that isn't image/video/audio.
payload_too_largeOver 1 GB, by declared Content-Length or actual bytes.
storage_limitThe account is at its quota.

POST /v1/uploads/ticket

Step 1 of 3 for local bytes. Reserves an object and returns a presigned PUT URL.

Body

NameTypeRequiredDescription
contentTypestringyesMust be an accepted type. 1–200 characters.
sizeBytesnumberyesPositive integer. Checked against the 1 GB cap and the quota before the ticket is issued.
filenamestringnoUp to 300 characters.

Returns { storageId, uploadUrl, expiresAt }.

Request
curl -s -X POST https://api.corte.so/v1/uploads/ticket \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{ "contentType": "video/mp4", "sizeBytes": 18344012, "filename": "raw-take.mp4" }'
Response
{
"storageId": "f2c94a67-0e1b-4d38-8b52-6a7f1c09e4d2",
"uploadUrl": "https://media.corte.so/…/raw-take.mp4?X-Amz-Signature=…",
"expiresAt": "2026-07-28T17:12:44.000Z"
}

Step 2 — PUT the bytes

Upload directly to uploadUrl. Do not send your API key — the presigned URL carries its own authorization, and it goes to storage, not to the Corte API.

curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary @raw-take.mp4

The Content-Type must match what you declared on the ticket.


POST /v1/uploads/:storageId/commit

Step 3 of 3. Confirms the bytes landed and marks the object committed. The server re-reads the real size from storage, so the committed sizeBytes may differ from what the ticket declared.

Returns { storageId, url, sizeBytes }.

Request
curl -s -X POST https://api.corte.so/v1/uploads/f2c94a67-…/commit \
-H "Authorization: Bearer corte_sk_…"
Response
{
"storageId": "f2c94a67-0e1b-4d38-8b52-6a7f1c09e4d2",
"url": "https://media.corte.so/…/raw-take.mp4?X-Amz-Signature=…",
"sizeBytes": 18344012
}

Errors

CodeWhen
not_foundUnknown ticket, not yours, or already deleted.
invalid_paramsNo uploaded data found — the PUT never landed.

GET /v1/storage/usage

List your committed objects and report quota consumption.

Unpaginated by default: omit limit and you get every object. Pass limit to page.

Query parameters

NameTypeDefaultDescription
limitnumberunsetPage size, 1500. Omit for the full list.
offsetnumber0Only meaningful with limit.
sortstringby newestPass size to sort largest-first.

Returns

FieldTypeDescription
objectsobject[]The page (or everything).
totalBytesnumberBytes across all committed objects, not just the page.
includedBytesnumberThe plan's quota in bytes.
totalnumberCount of all committed objects.
Request
curl -s "https://api.corte.so/v1/storage/usage?limit=2&sort=size" \
-H "Authorization: Bearer corte_sk_…"
Response
{
"objects": [
{
"storageId": "f2c94a67-0e1b-4d38-8b52-6a7f1c09e4d2",
"filename": "raw-take.mp4",
"contentType": "video/mp4",
"sizeBytes": 18344012,
"origin": "upload",
"url": "https://media.corte.so/…?X-Amz-Signature=…",
"createdAt": "2026-07-28T17:09:52.771Z"
},
{
"storageId": "c74a1e08-9b6f-4d13-a205-e8f30b7c9d61",
"filename": null,
"contentType": "image/jpeg",
"sizeBytes": 1204338,
"origin": "generated",
"url": "https://media.corte.so/…?X-Amz-Signature=…",
"createdAt": "2026-07-28T14:04:47.019Z"
}
],
"totalBytes": 4187593216,
"includedBytes": 107374182400,
"total": 314
}

Percentage used is totalBytes / includedBytes. Alert before you hit it — crossing the line blocks ingestion, not generation.


GET /v1/storage/:storageId

One object's metadata — the same shape /v1/storage/usage returns in its list, for when you hold an id and nothing else.

Readable if you own the object, or a shared project you belong to has been granted it. That is the difference from the usage listing, which only ever returns your own files: an id a teammate uploaded resolves here and not there.

Anything you cannot read is not_found (404), including ids that do not exist — so probing an id tells you nothing.

Returns { object }.

Request
curl -s https://api.corte.so/v1/storage/c74a1e08-… \
-H "Authorization: Bearer corte_sk_…"
Response
{
"object": {
"storageId": "c74a1e08-…",
"filename": "hero.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320,
"createdAt": "2026-08-07T10:00:00.000Z",
"url": "https://…",
"thumbUrl": "https://…",
"origin": "upload"
}
}

url and thumbUrl are signed and expire — fetch them when you need them rather than storing them.


DELETE /v1/storage/:storageId

Delete an object. Removes the bytes from storage and marks the row deleted.

Only the owner may delete. Not reversible — and anything still referencing the id (a workflow input node, a past job's results) will render as broken media.

Returns { "ok": true }.

Request
curl -s -X DELETE https://api.corte.so/v1/storage/f2c94a67-… \
-H "Authorization: Bearer corte_sk_…"
Response
{ "ok": true }

GET /v1/storage/:storageId/raw

Redirect (302) to a freshly signed read URL. Useful when you want a stable, authenticated address to hand to a downloader instead of a URL that ages out.

Readable if you own the object, or it's linked to a shared project you belong to.

Request
curl -sL https://api.corte.so/v1/storage/c74a1e08-…/raw \
-H "Authorization: Bearer corte_sk_…" \
-o result.jpg

Follow redirects (-L); the response body is the media itself.


Sharing files into a project

Objects are private to their uploader by default. To make one readable by every member of a shared project, link it: POST /v1/projects/:id/storage.