Skip to main content
GET
/
builds
/
upload
/
parts
curl -X GET "https://nunu.ai/api/v1/builds/upload/parts?upload_id=ExampleUploadId123&object_key=abc123-def456/large-app.apk&part_numbers=1,2,3"
{
  "upload_urls": [
    {
      "part_number": 1,
      "url": "https://storage.example.com/...?partNumber=1"
    },
    {
      "part_number": 2,
      "url": "https://storage.example.com/...?partNumber=2"
    },
    {
      "part_number": 3,
      "url": "https://storage.example.com/...?partNumber=3"
    }
  ]
}
This endpoint uses the upload_id as authentication. No X-Api-Key header is needed.
Get presigned URLs for uploading parts in a multipart upload. Request URLs in batches for parallel uploads.

Query Parameters

upload_id
string
required
The upload ID returned from initiate upload. Serves as authentication.
object_key
string
required
The object key returned from initiate upload
part_numbers
string
required
Comma-separated list of part numbers to get URLs for (e.g., 1,2,3)

Response

upload_urls
UploadUrl[]
required
Array of presigned URLs for each requested part
curl -X GET "https://nunu.ai/api/v1/builds/upload/parts?upload_id=ExampleUploadId123&object_key=abc123-def456/large-app.apk&part_numbers=1,2,3"
{
  "upload_urls": [
    {
      "part_number": 1,
      "url": "https://storage.example.com/...?partNumber=1"
    },
    {
      "part_number": 2,
      "url": "https://storage.example.com/...?partNumber=2"
    },
    {
      "part_number": 3,
      "url": "https://storage.example.com/...?partNumber=3"
    }
  ]
}

Uploading Parts

After receiving the presigned URLs, upload each part:
PUT {part_url}
Content-Type: application/octet-stream
Content-Length: 104857600

[Binary data for this part]
The response includes an ETag header:
200 OK
ETag: "abc123def456789"
You must save the ETag from each part response. These are required when completing the upload.

Best Practices

Batch Requests

Request 3-10 part URLs at a time for optimal performance

Parallel Uploads

Upload 3-5 parts concurrently for faster uploads

Retry Failed Parts

Implement exponential backoff (1s, 2s, 4s, 8s) for failures

Track Progress

Store ETags and monitor uploaded parts for resumability
Presigned URLs typically expire after 1 hour. Upload immediately after receiving URLs.

Complete Multipart Example

async function uploadMultipart(projectId: string, file: File, name: string, platform: string) {
  const token = process.env.NUNU_API_TOKEN;

  // Step 1: Initiate
  const initRes = await fetch("https://nunu.ai/api/v1/builds/upload", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": token },
    body: JSON.stringify({
      project_id: projectId, name, file_name: file.name,
      file_size: file.size, platform
    }),
  });
  const { upload_id, build_id, object_key, total_parts, part_size } = await initRes.json();

  // Step 2 & 3: Get URLs and upload parts
  const parts = [];
  const concurrency = 3;

  for (let i = 0; i < total_parts; i += concurrency) {
    const partNumbers = Array.from(
      { length: Math.min(concurrency, total_parts - i) },
      (_, j) => i + j + 1
    );

    // Get URLs
    const urlRes = await fetch(
      `https://nunu.ai/api/v1/builds/upload/parts?` +
      new URLSearchParams({ upload_id, object_key, part_numbers: partNumbers.join(",") })
    );
    const { upload_urls } = await urlRes.json();

    // Upload parts in parallel
    const uploaded = await Promise.all(
      upload_urls.map(async ({ part_number, url }) => {
        const start = (part_number - 1) * part_size;
        const chunk = file.slice(start, Math.min(start + part_size, file.size));
        const res = await fetch(url, {
          method: "PUT", body: chunk,
          headers: { "Content-Type": "application/octet-stream" }
        });
        return { part_number, etag: res.headers.get("etag")?.replace(/"/g, "") };
      })
    );
    parts.push(...uploaded);
    console.log(`Uploaded ${parts.length}/${total_parts} parts`);
  }

  // Step 4: Complete
  await fetch("https://nunu.ai/api/v1/builds/upload/complete", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": token },
    body: JSON.stringify({ build_id, upload_id, object_key, parts }),
  });

  return build_id;
}

Query Parameters

upload_id
string
required

The upload ID (serves as authentication)

object_key
string
required

The object key for the upload

part_numbers
string
required

Comma-separated list of part numbers to get URLs for

Response

Presigned URLs for parts

upload_urls
object[]