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

http
POST /v1/uploads/multipart
json
{
  "filename": "demo-video.mp4",
  "contentType": "video/mp4",
  "size": 671088640,
  "path": "media"
}
bash
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:

json
{
  "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:

bash
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

http
POST /v1/uploads/multipart/:uploadId/complete
json
{
  "parts": [
    { "partNumber": 1, "etag": "…" },
    { "partNumber": 2, "etag": "…" }
  ]
}
bash
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:

http
POST /v1/uploads/multipart/:uploadId/parts
json
{ "partNumbers": [1] }

Ask storage which parts it already holds:

http
GET /v1/uploads/multipart/:uploadId/parts
json
{
  "data": {
    "parts": [ { "partNumber": 1, "etag": "…", "size": 8388608 } ]
  }
}

Abort a session

http
DELETE /v1/uploads/multipart/:uploadId
bash
curl -X DELETE https://api.apulodi.com/v1/uploads/multipart/upload_5c1a… \
  -H "Authorization: Bearer $APULODI_API_KEY"
json
{ "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

StatusCodeWhy
404UPLOAD_NOT_FOUNDNo session in your project
409UPLOAD_NOT_ACTIVESession isn't active (aborted/completed)
409UPLOAD_PARTS_INCOMPLETEWrong number of parts / not contiguous
409UPLOAD_SIZE_MISMATCHAssembled size differs from the declared size
400VALIDATION_ERRORMissing/duplicate part numbers or invalid body

Next: Errors — the full error code reference.