Multipart uploads
Files too large for a single PUT are uploaded in parts, driven by an upload session. The flow is explicit and retry-friendly.
APULODI session ids (upload_...) are the only upload identifiers you ever
see — raw storage-provider upload ids are never exposed.
1. Initiate a session
POST /v1/uploads/multipart
{
"filename": "demo-video.mp4",
"contentType": "video/mp4",
"size": 671088640,
"path": "media"
}
curl -X POST https://api.apulodi.com/v1/uploads/multipart \
-H "Authorization: Bearer $APULODI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "demo-video.mp4",
"contentType": "video/mp4",
"size": 671088640,
"path": "media"
}'
Returns 201 Created with presigned URLs for every part:
{
"data": {
"file": {
"id": "file_8f2b…",
"status": "pending",
"filename": "demo-video.mp4",
"contentType": "video/mp4",
"size": 671088640,
"path": "media",
"version": 1
},
"upload": {
"id": "upload_5c1a…",
"partSize": 8388608,
"parts": [
{ "partNumber": 1, "url": "https://…", "expiresAt": "…" },
{ "partNumber": 2, "url": "https://…", "expiresAt": "…" }
],
"expiresAt": "…"
}
}
}
partSize is chosen based on your file size. The number of parts is
ceil(size / partSize). All parts except the last must be exactly partSize
bytes.
2. PUT each part to storage
Upload part N to parts[N-1].url directly:
curl -X PUT "$PART_1_URL" \
-H "Content-Type: video/mp4" \
--data-binary @part_01
Each successful PUT returns an ETag header. Keep it — you need every
etag to complete the session. To have the SDK collect them automatically use
apulodi.files.upload(), which switches to multipart for files over 8 MiB.
3. Complete the session
POST /v1/uploads/multipart/:uploadId/complete
{
"parts": [
{ "partNumber": 1, "etag": "…" },
{ "partNumber": 2, "etag": "…" }
]
}
curl -X POST https://api.apulodi.com/v1/uploads/multipart/upload_5c1a…/complete \
-H "Authorization: Bearer $APULODI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"parts":[{"partNumber":1,"etag":"…"},{"partNumber":2,"etag":"…"}]}'
The server validates the part count and contiguity, assembles the object,
verifies its real size, and returns the file with status: "uploaded".
Resuming / retrying
Presigned URLs expire. Get a fresh URL for specific parts:
POST /v1/uploads/multipart/:uploadId/parts
{ "partNumbers": [1] }
Ask storage which parts it already holds:
GET /v1/uploads/multipart/:uploadId/parts
{
"data": {
"parts": [ { "partNumber": 1, "etag": "…", "size": 8388608 } ]
}
}
Abort a session
DELETE /v1/uploads/multipart/:uploadId
curl -X DELETE https://api.apulodi.com/v1/uploads/multipart/upload_5c1a… \
-H "Authorization: Bearer $APULODI_API_KEY"
{ "data": { "id": "upload_5c1a…", "aborted": true } }
Abort discards uploaded parts and marks the file failed. It is idempotent
and safe to call more than once (interrupted aborts are cleaned up
automatically). Completing an aborted session returns 409.
Errors
| Status | Code | Why |
|---|---|---|
404 | UPLOAD_NOT_FOUND | No session in your project |
409 | UPLOAD_NOT_ACTIVE | Session isn't active (aborted/completed) |
409 | UPLOAD_PARTS_INCOMPLETE | Wrong number of parts / not contiguous |
409 | UPLOAD_SIZE_MISMATCH | Assembled size differs from the declared size |
400 | VALIDATION_ERROR | Missing/duplicate part numbers or invalid body |
Next: Errors — the full error code reference.