Imagen API docs
Navigation menu
Reference

Uploading

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.

Send only file_name for each file. No MD5 or extra metadata is required. The endpoint path depends on your flow:

POST /v1/projects/PROJECT_UUID/get_temporary_upload_links
POST /v1/i2i/projects/PROJECT_UUID/get_temporary_upload_links

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.

POST /v1/i2i/projects/PROJECT_UUID/multipart_uploads
curl -X POST 'https://api.imagen-ai.com/v1/i2i/projects/$PROJECT_UUID/multipart_uploads' \
  --header 'x-api-key: $IMAGEN_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"file_name": "DSC_0001.NEF", "part_count": 8}'
import httpx, os

async with httpx.AsyncClient() as http:
    resp = await http.post(
        f"https://api.imagen-ai.com/v1/i2i/projects/{project_uuid}/multipart_uploads",
        headers={"x-api-key": os.environ["IMAGEN_API_KEY"]},
        json={"file_name": "DSC_0001.NEF", "part_count": 8},
    )
    data = resp.json()["data"]
    upload_id, parts = data["upload_id"], data["parts"]
const res = await fetch(
  `https://api.imagen-ai.com/v1/i2i/projects/${projectUuid}/multipart_uploads`,
  {
    method: 'POST',
    headers: {
      'x-api-key': process.env.IMAGEN_API_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ file_name: 'DSC_0001.NEF', part_count: 8 }),
  }
);
const { data } = (await res.json()) as {
  data: { upload_id: string; key: string; parts: { part_number: number; upload_url: string }[] };
};
import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

body, _ := json.Marshal(map[string]any{"file_name": "DSC_0001.NEF", "part_count": 8})
req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
    "https://api.imagen-ai.com/v1/i2i/projects/"+projectUUID+"/multipart_uploads",
    bytes.NewReader(body),
)
req.Header.Set("x-api-key", os.Getenv("IMAGEN_API_KEY"))
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
import java.net.URI;
import java.net.http.*;

var body = """
    {"file_name": "DSC_0001.NEF", "part_count": 8}
    """;
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/i2i/projects/" + projectUuid + "/multipart_uploads"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/i2i/projects/#{project_uuid}/multipart_uploads")
req = Net::HTTP::Post.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']
req['Content-Type'] = 'application/json'
req.body = { file_name: 'DSC_0001.NEF', part_count: 8 }.to_json

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)['data']
<?php
$url = 'https://api.imagen-ai.com/v1/i2i/projects/' . $projectUuid . '/multipart_uploads';
$ch = curl_init($url);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'x-api-key: ' . getenv('IMAGEN_API_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode(['file_name' => 'DSC_0001.NEF', 'part_count' => 8]),
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)['data'];
using System.Net.Http;
using System.Text;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key",
    Environment.GetEnvironmentVariable("IMAGEN_API_KEY"));

var payload = JsonSerializer.Serialize(new { file_name = "DSC_0001.NEF", part_count = 8 });
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
    $"https://api.imagen-ai.com/v1/i2i/projects/{projectUuid}/multipart_uploads", content);
var json = await response.Content.ReadAsStringAsync();
{
  "data": {
    "upload_id": "abc123...",
    "key": ".../DSC_0001.NEF",
    "parts": [
      { "part_number": 1, "upload_url": "https://<s3-presigned>" },
      { "part_number": 2, "upload_url": "https://<s3-presigned>" }
    ]
  }
}

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.

3. Complete the upload

POST /v1/i2i/projects/PROJECT_UUID/multipart_uploads/UPLOAD_ID/complete
curl -X POST 'https://api.imagen-ai.com/v1/i2i/projects/$PROJECT_UUID/multipart_uploads/$UPLOAD_ID/complete' \
  --header 'x-api-key: $IMAGEN_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"file_name": "DSC_0001.NEF"}'
import httpx, os

async with httpx.AsyncClient() as http:
    await http.post(
        f"https://api.imagen-ai.com/v1/i2i/projects/{project_uuid}/multipart_uploads/{upload_id}/complete",
        headers={"x-api-key": os.environ["IMAGEN_API_KEY"]},
        json={"file_name": "DSC_0001.NEF"},
    )
await fetch(
  `https://api.imagen-ai.com/v1/i2i/projects/${projectUuid}/multipart_uploads/${uploadId}/complete`,
  {
    method: 'POST',
    headers: {
      'x-api-key': process.env.IMAGEN_API_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ file_name: 'DSC_0001.NEF' }),
  }
);
import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

body, _ := json.Marshal(map[string]any{"file_name": "DSC_0001.NEF"})
url := "https://api.imagen-ai.com/v1/i2i/projects/" + projectUUID +
    "/multipart_uploads/" + uploadID + "/complete"
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
req.Header.Set("x-api-key", os.Getenv("IMAGEN_API_KEY"))
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
import java.net.URI;
import java.net.http.*;

var body = """
    {"file_name": "DSC_0001.NEF"}
    """;
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/i2i/projects/" + projectUuid
        + "/multipart_uploads/" + uploadId + "/complete"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/i2i/projects/#{project_uuid}/multipart_uploads/#{upload_id}/complete")
req = Net::HTTP::Post.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']
req['Content-Type'] = 'application/json'
req.body = { file_name: 'DSC_0001.NEF' }.to_json

Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
<?php
$url = 'https://api.imagen-ai.com/v1/i2i/projects/' . $projectUuid
  . '/multipart_uploads/' . $uploadId . '/complete';
$ch = curl_init($url);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'x-api-key: ' . getenv('IMAGEN_API_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode(['file_name' => 'DSC_0001.NEF']),
]);
curl_exec($ch);
curl_close($ch);
using System.Net.Http;
using System.Text;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key",
    Environment.GetEnvironmentVariable("IMAGEN_API_KEY"));

var payload = JsonSerializer.Serialize(new { file_name = "DSC_0001.NEF" });
var content = new StringContent(payload, Encoding.UTF8, "application/json");
await client.PostAsync(
    $"https://api.imagen-ai.com/v1/i2i/projects/{projectUuid}/multipart_uploads/{uploadId}/complete",
    content);
{ "data": null }

Abort a multipart upload

If something goes wrong mid-upload, abort with the key returned by start.

DELETE /v1/i2i/projects/PROJECT_UUID/multipart_uploads/UPLOAD_ID
curl -X DELETE 'https://api.imagen-ai.com/v1/i2i/projects/$PROJECT_UUID/multipart_uploads/$UPLOAD_ID' \
  --header 'x-api-key: $IMAGEN_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"key": "<the key returned by start>"}'
import httpx, os

async with httpx.AsyncClient() as http:
    await http.request(
        "DELETE",
        f"https://api.imagen-ai.com/v1/i2i/projects/{project_uuid}/multipart_uploads/{upload_id}",
        headers={"x-api-key": os.environ["IMAGEN_API_KEY"]},
        json={"key": key},
    )
await fetch(
  `https://api.imagen-ai.com/v1/i2i/projects/${projectUuid}/multipart_uploads/${uploadId}`,
  {
    method: 'DELETE',
    headers: {
      'x-api-key': process.env.IMAGEN_API_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ key }),
  }
);
import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

body, _ := json.Marshal(map[string]any{"key": key})
url := "https://api.imagen-ai.com/v1/i2i/projects/" + projectUUID +
    "/multipart_uploads/" + uploadID
req, _ := http.NewRequestWithContext(ctx, http.MethodDelete, url, bytes.NewReader(body))
req.Header.Set("x-api-key", os.Getenv("IMAGEN_API_KEY"))
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
import java.net.URI;
import java.net.http.*;

var body = """
    {"key": "<the key returned by start>"}
    """;
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/i2i/projects/" + projectUuid
        + "/multipart_uploads/" + uploadId))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .header("Content-Type", "application/json")
    .method("DELETE", HttpRequest.BodyPublishers.ofString(body))
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/i2i/projects/#{project_uuid}/multipart_uploads/#{upload_id}")
req = Net::HTTP::Delete.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']
req['Content-Type'] = 'application/json'
req.body = { key: key }.to_json

Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
<?php
$url = 'https://api.imagen-ai.com/v1/i2i/projects/' . $projectUuid
  . '/multipart_uploads/' . $uploadId;
$ch = curl_init($url);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'DELETE',
  CURLOPT_HTTPHEADER => [
    'x-api-key: ' . getenv('IMAGEN_API_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode(['key' => $key]),
]);
curl_exec($ch);
curl_close($ch);
using System.Net.Http;
using System.Text;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key",
    Environment.GetEnvironmentVariable("IMAGEN_API_KEY"));

var payload = JsonSerializer.Serialize(new { key });
var request = new HttpRequestMessage(HttpMethod.Delete,
    $"https://api.imagen-ai.com/v1/i2i/projects/{projectUuid}/multipart_uploads/{uploadId}")
{
    Content = new StringContent(payload, Encoding.UTF8, "application/json"),
};
await client.SendAsync(request);