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
| Field | Type | Description |
|---|---|---|
storageId | string | Durable id (UUID). This is what every media parameter takes. |
filename | string | null | Original filename, when one was given. |
contentType | string | MIME type. |
sizeBytes | number | Size on disk. |
origin | enum | upload (you put it there) or generated (a job produced it). |
url | string | Freshly signed read URL. Expires — re-fetch rather than storing it. |
createdAt | string | ISO 8601. |
Limits
| Limit | Value |
|---|---|
| Max file size | 1 GB |
| Accepted content types | image/*, video/*, audio/*, plus application/octet-stream and JavaScript types for plugin bundles |
| Storage quota | Free 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
| Name | Type | Required | Description |
|---|---|---|---|
url | string | yes | Must be https. The server fetches it and inspects the response's content type. |
filename | string | no | Overrides the name derived from the URL path. |
Returns { storageId, url, sizeBytes } — already committed.
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"
}'
{
"storageId": "b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8",
"url": "https://media.corte.so/…/trail-shoe.jpg?X-Amz-Signature=…",
"sizeBytes": 482113
}
Errors
| Code | When |
|---|---|
invalid_params | Not an https URL, unreachable, non-2xx from the origin, or a content type that isn't image/video/audio. |
payload_too_large | Over 1 GB, by declared Content-Length or actual bytes. |
storage_limit | The 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
| Name | Type | Required | Description |
|---|---|---|---|
contentType | string | yes | Must be an accepted type. 1–200 characters. |
sizeBytes | number | yes | Positive integer. Checked against the 1 GB cap and the quota before the ticket is issued. |
filename | string | no | Up to 300 characters. |
Returns { storageId, uploadUrl, expiresAt }.
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" }'
{
"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 }.
curl -s -X POST https://api.corte.so/v1/uploads/f2c94a67-…/commit \
-H "Authorization: Bearer corte_sk_…"
{
"storageId": "f2c94a67-0e1b-4d38-8b52-6a7f1c09e4d2",
"url": "https://media.corte.so/…/raw-take.mp4?X-Amz-Signature=…",
"sizeBytes": 18344012
}
Errors
| Code | When |
|---|---|
not_found | Unknown ticket, not yours, or already deleted. |
invalid_params | No 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
| Name | Type | Default | Description |
|---|---|---|---|
limit | number | unset | Page size, 1–500. Omit for the full list. |
offset | number | 0 | Only meaningful with limit. |
sort | string | by newest | Pass size to sort largest-first. |
Returns
| Field | Type | Description |
|---|---|---|
objects | object[] | The page (or everything). |
totalBytes | number | Bytes across all committed objects, not just the page. |
includedBytes | number | The plan's quota in bytes. |
total | number | Count of all committed objects. |
curl -s "https://api.corte.so/v1/storage/usage?limit=2&sort=size" \
-H "Authorization: Bearer corte_sk_…"
{
"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 }.
curl -s https://api.corte.so/v1/storage/c74a1e08-… \
-H "Authorization: Bearer corte_sk_…"
{
"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 }.
curl -s -X DELETE https://api.corte.so/v1/storage/f2c94a67-… \
-H "Authorization: Bearer corte_sk_…"
{ "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.
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.
Related
- Generations — every parameter that takes a
storageId - Storage guide — quotas and cleanup in the app