Every project uploads its photos the same way: you request presigned S3 links,
then PUT each file’s bytes directly to its link. This applies to both the
profile-based flow and
Smart Editing.
Unique filenames
Every filename in a project must be unique
Send one file_name per file, using the original filenames from the
camera or device. A duplicate returns a 400 (File ”…” appears N
times.). For HDR brackets, this means each frame needs a distinct
filename. Upload the full bracket set, not just the middle exposure.
Request upload links
Send only file_name for each file. No MD5 or extra metadata is required. The endpoint
path depends on your flow:
Both take the same body and return the same shape: a presigned upload_link per
file. See Quickstart or
Smart Editing for the full request
in all languages.
Upload with a single PUT
For each entry in the response, PUT the binary to its upload_link. Use an
explicit empty Content-Type header, because the presigned URL is signed with
an empty content-type. Don’t add an x-api-key header on the PUT. The signature
is in the URL. Files can be uploaded in parallel.
# PUT directly to the presigned URL — no x-api-key needed.# The -H 'Content-Type;' flag sends an empty Content-Type header.curl -X PUT "<presigned_upload_link>" \ -H 'Content-Type;' \ --upload-file DSC_0001.NEF
import httpx# Content-Type must be empty — no x-api-key needed for S3 presigned URLs.with open("DSC_0001.NEF", "rb") as f: content = f.read()async with httpx.AsyncClient() as http: await http.put(upload_link, content=content, headers={"Content-Type": ""})# The Python SDK's upload_images() handles this concurrently for you.
import { readFile } from 'fs/promises';const fileContent = await readFile('DSC_0001.NEF');// No x-api-key needed for S3 presigned URLs — Content-Type must be empty.await fetch(uploadLink, { method: 'PUT', headers: { 'Content-Type': '' }, body: fileContent,});
import ( "bytes" "net/http" "os")fileContent, _ := os.ReadFile("DSC_0001.NEF")req, _ := http.NewRequestWithContext(ctx, http.MethodPut, uploadLink, bytes.NewReader(fileContent))// No x-api-key for S3 presigned URLs — Content-Type must be empty.req.Header.Set("Content-Type", "")resp, _ := http.DefaultClient.Do(req)defer resp.Body.Close()
import java.net.URI;import java.net.http.*;import java.nio.file.*;// No x-api-key for S3 presigned URLs — Content-Type must be empty.var request = HttpRequest.newBuilder() .uri(URI.create(uploadLink)) .header("Content-Type", "") .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("DSC_0001.NEF"))) .build();var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString());
require 'net/http'require 'uri'uri = URI(upload_link)req = Net::HTTP::Put.new(uri)# No x-api-key for S3 presigned URLs — Content-Type must be empty.req['Content-Type'] = ''req.body = File.binread('DSC_0001.NEF')Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
<?php$ch = curl_init($uploadLink);curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PUT', // No x-api-key for S3 presigned URLs — Content-Type must be empty. CURLOPT_HTTPHEADER => ['Content-Type:'], CURLOPT_POSTFIELDS => file_get_contents('DSC_0001.NEF'),]);curl_exec($ch);curl_close($ch);
using System.Net.Http;using System.IO;using var client = new HttpClient();var fileBytes = await File.ReadAllBytesAsync("DSC_0001.NEF");// No x-api-key for S3 presigned URLs — null Content-Type sends an empty header.var content = new ByteArrayContent(fileBytes);content.Headers.ContentType = null;var response = await client.PutAsync(uploadLink, content);
Multipart upload (for large files)
If an individual file is large enough that a single PUT would time out, use multipart upload. Camera RAWs over ~100 MB are a common case. This is the path the real estate
web app takes for large HDR brackets. Multipart is currently exposed under the
Smart Editing namespace (/v1/i2i/projects/...).
S3 constraints to plan for:
part_count must be between 1 and 10000.
Every part except the last must be >= 5 MB. The final part can be smaller.
1. Start the upload
Pass the file name and how many parts you’ll split it into.
You get one presigned PUT URL per part, plus an upload_id you use to complete or
abort.
2. Upload each part
PUT each chunk to its upload_url, exactly like the single PUT
above. Use an empty Content-Type and no x-api-key. Parts can upload in parallel.
You don't need to track ETags
Raw S3 multipart upload usually requires the client to remember each part’s
ETag and pass them to CompleteMultipartUpload. Imagen’s API handles that
server-side. It lists the uploaded parts directly from S3 and completes the
upload itself. Just PUT every part and call /complete.