---
title: "Quickstart to Imagen API"
description: "Step-by-step guide to the Imagen API: list AI Profiles, create a project, upload photos, trigger AI editing, poll for status, download edited files, and export to JPEG - with code examples in 8 languages."
canonical: https://api-docs.imagen-ai.com/docs/quickstart/
last-updated: 2026-08-27
---

# Quickstart to Imagen API

Use our quickstart to learn how the Imagen API works and to customize your flow.

> **Complete the onboarding first** You need to sign up for Imagen, set up your environment, choose an AI Profile, and a few other steps before using this quickstart. See [Onboarding](/docs/onboarding). If you want to use the Imagen API with little coding, here's the [Python, Node.js, and Go SDKs](/docs/onboarding#sdk).

### Get a list of available AI Profiles

Get a list of AI Profiles and their keys.

`GET /v1/profiles/`

**Response example**

The response includes the available AI Profiles. Each AI Profile has a `profile_key` that identifies it. You need this `profile_key` to edit the photos in your project.

```curl
curl 'https://api.imagen-ai.com/v1/profiles/' \
  --header 'x-api-key: $IMAGEN_API_KEY'
```

```python
profiles = await client.get_profiles()
for p in profiles:
    print(f"{p.profile_name} - key: {p.profile_key}")
# -> wedding - key: 5700
```

```typescript
const profiles = await client.getProfiles();
for (const p of profiles) {
  console.log(`${p.profileName} - key: ${p.profileKey}`);
}
// -> wedding - key: 5700
```

```go
profiles, _ := client.GetProfiles(ctx)
for _, p := range profiles {
    fmt.Printf("%s - key: %d\n", p.ProfileName, p.ProfileKey)
}
// -> wedding - key: 5700
```

```java
import java.net.URI;

var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/profiles/"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .GET()
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
// response.body() contains the JSON
```

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/profiles/")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
profiles = JSON.parse(res.body)['data']['profiles']
profiles.each { |p| puts "#{p['profile_name']} - key: #{p['profile_key']}" }
```

```php
<?php
$ch = curl_init('https://api.imagen-ai.com/v1/profiles/');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'x-api-key: ' . getenv('IMAGEN_API_KEY'),
  ],
]);
$response = curl_exec($ch);
curl_close($ch);
$profiles = json_decode($response, true)['data']['profiles'];
foreach ($profiles as $p) {
  echo $p['profile_name'] . ' - key: ' . $p['profile_key'] . PHP_EOL;
}
```

```csharp
using System.Net.Http;
using System.Text.Json;

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

var response = await client.GetAsync("https://api.imagen-ai.com/v1/profiles/");
var json = await response.Content.ReadAsStringAsync();
// Parse json to access data.profiles
```
```json
{
  "data": {
    "profiles": [
      {
        "profile_key": <profile key of the Personal AI Profile>,
        "profile_name": "wedding",
        "profile_type": "Personal",
        "image_type": "RAW"
      },
      {
        "profile_key": 14715,
        "profile_name": "LOVE & LIGHT",
        "profile_type": "Talent",
        "image_type": "RAW"
      },
      {
        "profile_key": 163322,
        "profile_name": "ELEGANT HOME",
        "profile_type": "Talent",
        "image_type": "RAW"
      }
    ]
  }
}
```

### Cull your photos without the API

Cull your photos before uploading them for editing. Culling isn't part of
the Imagen API.

### Create a project and get a project ID

We recommend creating a project for each shoot. A project organizes your
photos with the AI Profile you chose for editing these photos.

`POST /v1/projects/`

**Response example**

The response is the `project_uuid` used to identify this project.
Use this `project_uuid` until you export your photos to JPEG.

```json
{
  "data": {
    "project_uuid": "<project_uuid>"
  }
}
```

### Create temporary upload links to upload photos

You need a temporary link to upload each photo. In the request, in the 
`files_list` param, include the file names of the photos to
upload. Use list format.

`POST /v1/projects/PROJECT_UUID/get_temporary_upload_links`

**Request example**

```curl
curl -X POST \
  'https://api.imagen-ai.com/v1/projects/<PROJECT_UUID>/get_temporary_upload_links' \
  --header 'x-api-key: $IMAGEN_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "files_list": [
      {"file_name": "922A4846.CR2"},
      {"file_name": "922A4832.CR2"},
      {"file_name": "922A4818.CR2"},
      {"file_name": "922A4809.CR2"}
    ]
  }'
```

```python
upload_summary = await client.upload_images(
    project_uuid,
    image_paths=["922A4846.CR2", "922A4832.CR2", "922A4818.CR2", "922A4809.CR2"],
    max_concurrent=5,
)
print(f"Uploaded {upload_summary.successful}/{upload_summary.total}")
```

```typescript
const summary = await client.uploadImages(
  projectUuid,
  ['922A4846.CR2', '922A4832.CR2', '922A4818.CR2', '922A4809.CR2'],
  { maxConcurrent: 5 }
);
console.log(`Uploaded ${summary.successful}/${summary.total}`);
```

```go
import imagen "github.com/imagenai/imagen-ai-sdk/sdks/go"

links, _ := client.GetUploadLinks(ctx, projectUUID, []imagen.FileUploadInfo{
    {FileName: "922A4846.CR2"},
    {FileName: "922A4832.CR2"},
    {FileName: "922A4818.CR2"},
    {FileName: "922A4809.CR2"},
})
fmt.Printf("got %d upload links\n", len(links))
// UploadImages (next step) requests links and uploads in one call
```

```java
import java.net.URI;

var body = """
    {
        "files_list": [
            {"file_name": "922A4846.CR2"},
            {"file_name": "922A4832.CR2"},
            {"file_name": "922A4818.CR2"},
            {"file_name": "922A4809.CR2"}
        ]
    }
    """;
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/get_temporary_upload_links"))
    .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());
```

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/get_temporary_upload_links")
req = Net::HTTP::Post.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']
req['Content-Type'] = 'application/json'
req.body = {
  files_list: [
    { file_name: '922A4846.CR2' },
    { file_name: '922A4832.CR2' },
    { file_name: '922A4818.CR2' },
    { file_name: '922A4809.CR2' },
  ],
}.to_json

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)['data']
```

```php
<?php
$ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/get_temporary_upload_links');
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([
    'files_list' => [
      ['file_name' => '922A4846.CR2'],
      ['file_name' => '922A4832.CR2'],
      ['file_name' => '922A4818.CR2'],
      ['file_name' => '922A4809.CR2'],
    ],
  ]),
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)['data'];
```

```csharp
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 {
    files_list = new[] {
        new { file_name = "922A4846.CR2" },
        new { file_name = "922A4832.CR2" },
        new { file_name = "922A4818.CR2" },
        new { file_name = "922A4809.CR2" },
    },
});
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
    $"https://api.imagen-ai.com/v1/projects/{projectUuid}/get_temporary_upload_links",
    content
);
var json = await response.Content.ReadAsStringAsync();
```

**Response example**

The response includes a temporary link for each photo to Imagen's S3 bucket
on AWS. This link is the AWS presigned URL.

```json
{
  "data": {
    "files_list": [
      {
        "file_name": "922A4846.raw",
        "upload_link": "<presigned URL for 922A4846.raw>"
      },
      {
        "file_name": "922A4832.raw",
        "upload_link": "<presigned URL for 922A4832.raw>"
      },
      {
        "file_name": "922A4818.raw",
        "upload_link": "<presigned URL for 922A4818.raw>"
      },
      {
        "file_name": "922A4809.raw",
        "upload_link": "<presigned URL for 922A4809.raw>"
      }
    ]
  }
}
```

### Upload each photo to its temporary upload link

Upload photos with a PUT request to Imagen's S3 bucket on AWS. Use the
temporary upload links from the response in the previous step.

> **Content-Type requirements** **Testing in Postman:** Disable `Content-Type` in the headers. Do not send `Content-Type` in the query params. 
**In code:** Either omit `Content-Type` entirely, or set it to an empty string (`""`). Both work - see the language tabs above for the idiomatic approach per language.
```curl
# PUT directly to the presigned URL - no x-api-key needed
# The -H 'Content-Type;' flag removes the Content-Type header
curl -X PUT "<presigned_upload_link>" \
  -H 'Content-Type;' \
  --upload-file 922A4846.CR2
```

```python
import httpx

async def upload_one(path: str, url: str):
    with open(path, "rb") as f:
        content = f.read()
    # Content-Type must be empty - no x-api-key needed for S3
    async with httpx.AsyncClient() as http:
        await http.put(url, content=content, headers={"Content-Type": ""})

# The SDK's upload_images() handles all of this concurrently for you
```

```typescript
import { readFile } from 'fs/promises';

const fileContent = await readFile('922A4846.CR2');

// No x-api-key needed for S3 presigned URLs - Content-Type must be empty
await fetch(presignedUploadLink, {
  method: 'PUT',
  headers: { 'Content-Type': '' },
  body: fileContent,
});

// The SDK's uploadImages() handles all of this concurrently for you
```

```go
import imagen "github.com/imagenai/imagen-ai-sdk/sdks/go"

// Requests the presigned links and PUTs each file to S3 concurrently
summary, _ := client.UploadImages(ctx, projectUUID,
    []string{"922A4846.CR2", "922A4832.CR2", "922A4818.CR2", "922A4809.CR2"},
    &imagen.UploadOptions{MaxConcurrency: 5},
)
fmt.Printf("Uploaded %d/%d\n", summary.Successful, summary.Total)
```

```java
import java.net.URI;

var fileBytes = Files.readAllBytes(Path.of("922A4846.CR2"));
// No x-api-key for S3 presigned URLs - Content-Type must be empty
var request = HttpRequest.newBuilder()
    .uri(URI.create(presignedUploadLink))
    .header("Content-Type", "")
    .PUT(HttpRequest.BodyPublishers.ofByteArray(fileBytes))
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
```

```ruby
require 'net/http'
require 'uri'

uri = URI(presigned_upload_link)
file_content = File.binread('922A4846.CR2')

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_content

Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
```

```php
<?php
$fileContent = file_get_contents('922A4846.CR2');
$ch = curl_init($presignedUploadLink);
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 => $fileContent,
]);
curl_exec($ch);
curl_close($ch);
```

```csharp
using System.Net.Http;
using System.IO;

using var client = new HttpClient();
var fileBytes = await File.ReadAllBytesAsync("922A4846.CR2");

// No x-api-key for S3 presigned URLs - pass "" for empty Content-Type
var content = new ByteArrayContent(fileBytes);
content.Headers.ContentType = null;

var response = await client.PutAsync(presignedUploadLink, content);
```

### Edit the photos in your project

Before sending your photos to edit, you need:

- The value of your `project_uuid`

- The value of your `profile_key`. See the response from step 1 for your `profile_key`.

- AI tools. See [AI tool reference for Imagen API](/docs/ai-tools) to choose the right tools for your project.

- (Optional) Callback URL. See [Add a callback URL to get status messages](#step-callback).

`POST /v1/projects/PROJECT_UUID/edit`
> **Content-Type must be empty** This endpoint requires an empty `Content-Type` header. The Python SDK handles this automatically.
```curl
curl -X POST \
  'https://api.imagen-ai.com/v1/projects/$PROJECT_UUID/edit' \
  --header 'x-api-key: $IMAGEN_API_KEY' \
  --header 'Content-Type;' \
  --data '{
    "profile_key": 163322,
    "hdr_merge": true,
    "photography_type": "REAL_ESTATE",
    "callback_url": "https://your.app/imagen/hook",
    "perspective_correction": true,
    "sky_replacement": true,
    "sky_replacement_template_id": 2
  }'
```

```python
from imagen_sdk import EditOptions, PhotographyType

await client.start_editing(
    project_uuid,
    profile_key=163322,
    photography_type=PhotographyType.REAL_ESTATE,
    edit_options=EditOptions(
        hdr_merge=True,
        perspective_correction=True,
        sky_replacement=True,
        sky_replacement_template_id=2,
        callback_url="https://your.app/imagen/hook",
    ),
)
```

```typescript
import { PhotographyType } from 'imagen-ai-sdk';

await client.startEditing(projectUuid, {
  profileKey: 163322,
  photographyType: PhotographyType.REAL_ESTATE,
  editOptions: {
    hdr_merge: true,
    perspective_correction: true,
    sky_replacement: true,
    sky_replacement_template_id: 2,
    callback_url: 'https://your.app/imagen/hook',
  },
});
```

```go
import imagen "github.com/imagenai/imagen-ai-sdk/sdks/go"

edit := imagen.EditRequest{
    ProfileKey:      163322,
    PhotographyType: imagen.PhotographyTypeRealEstate,
}
edit.HDRMerge = imagen.Bool(true)
edit.PerspectiveCorrection = imagen.Bool(true)
edit.SkyReplacement = imagen.Bool(true)
edit.SkyReplacementTemplateID = imagen.Int(2)
edit.CallbackURL = imagen.String("https://your.app/imagen/hook")

if err := client.StartEditing(ctx, projectUUID, edit); err != nil {
    log.Fatal(err)
}
```

```java
import java.net.URI;

var body = """
    {
        "profile_key": 163322,
        "hdr_merge": true,
        "photography_type": "REAL_ESTATE",
        "callback_url": "https://your.app/imagen/hook",
        "perspective_correction": true,
        "sky_replacement": true,
        "sky_replacement_template_id": 2
    }
    """;
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/edit"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .header("Content-Type", "")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
```

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/edit")
req = Net::HTTP::Post.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']
req['Content-Type'] = ''
req.body = {
  profile_key: 163322,
  hdr_merge: true,
  photography_type: 'REAL_ESTATE',
  callback_url: 'https://your.app/imagen/hook',
  perspective_correction: true,
  sky_replacement: true,
  sky_replacement_template_id: 2,
}.to_json

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)['data']
```

```php
<?php
$ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/edit');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'x-api-key: ' . getenv('IMAGEN_API_KEY'),
    'Content-Type:',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'profile_key' => 163322,
    'hdr_merge' => true,
    'photography_type' => 'REAL_ESTATE',
    'callback_url' => 'https://your.app/imagen/hook',
    'perspective_correction' => true,
    'sky_replacement' => true,
    'sky_replacement_template_id' => 2,
  ]),
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)['data'];
```

```csharp
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 {
    profile_key = 163322,
    hdr_merge = true,
    photography_type = "REAL_ESTATE",
    callback_url = "https://your.app/imagen/hook",
    perspective_correction = true,
    sky_replacement = true,
    sky_replacement_template_id = 2,
});
// Pass "" as the media type to send an empty Content-Type header
var content = new StringContent(payload, Encoding.UTF8, "");
var response = await client.PostAsync(
    $"https://api.imagen-ai.com/v1/projects/{projectUuid}/edit",
    content
);
var json = await response.Content.ReadAsStringAsync();
```

The response is a message that the project was edited successfully. If there is an error, email our customer success team.

```json
{"project_uuid": "<the project UUID>", "status": "Failed/Completed/Pending", "action": "edit/export"}
```

#### Add a callback URL to get status messages

The `callback_url` is a POST endpoint that Imagen can use to send you status messages. This param is optional. If you use it, you don't need to check the edit status. This endpoint must be open to Imagen.

Possible statuses sent in the request are:

- `Failed`: Email the customer success team.

- `Completed`: Imagen API has finished editing the photos. Continue to one of the following: Get temporary download links to download edited photos or XMPs to tweak edits and export photos with your editing software, OR Export photos for delivery to create exported JPEGs for customers.

### Check the edit status

If you didn't add a callback URL to your edit request, get the current
status of the editing process. Continue calling this endpoint with long
polling until the status is `Completed`.

`GET /v1/projects/PROJECT_UUID/edit/status`

Here are the statuses in the response:

<table>
<thead>
<tr>
<th>Status</th>
<th>What it means</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`Pending`
</td>
<td>
The editing process hasn't started. Once the editing starts, the
status will change to In Progress.
</td>
</tr>
<tr>
<td>
`In Progress`
</td>
<td>Imagen API is in the process of editing the photos.</td>
</tr>
<tr>
<td>
`Failed`
</td>
<td>Email the customer success team.</td>
</tr>
<tr>
<td>
`Completed`
</td>
<td>
Imagen API has finished editing the photos. Continue to step 8 or step
10.
</td>
</tr>
</tbody>
</table>
```curl
# Poll until status is "Completed" or "Failed"
curl 'https://api.imagen-ai.com/v1/projects/$PROJECT_UUID/edit/status' \
  --header 'x-api-key: $IMAGEN_API_KEY'
```

```python
# The SDK polls edit status internally inside start_editing()
# To check the raw status endpoint yourself:

async with httpx.AsyncClient() as http:
    resp = await http.get(
        f"https://api.imagen-ai.com/v1/projects/{project_uuid}/edit/status",
        headers={"x-api-key": os.environ["IMAGEN_API_KEY"]},
    )
    data = resp.json()["data"]
    print(data["status"])  # Pending | In Progress | Completed | Failed
```

```typescript
// The SDK polls edit status internally inside startEditing().
// To check the raw status endpoint yourself:
const res = await fetch(
  `https://api.imagen-ai.com/v1/projects/${projectUuid}/edit/status`,
  { headers: { 'x-api-key': process.env.IMAGEN_API_KEY! } }
);
const { data } = (await res.json()) as { data: { status: string } };
console.log(data.status); // Pending | In Progress | Completed | Failed
```

```go
status, _ := client.EditStatus(ctx, projectUUID)
fmt.Println(status.Status) // Pending | In Progress | Completed | Failed

// Or let the SDK poll with backoff until it finishes:
if _, err := client.WaitForEditing(ctx, projectUUID, nil); err != nil {
    log.Fatal(err) // a Failed status surfaces as an error
}
```

```java
import java.net.URI;

long interval = 10_000;
while (true) {
    var request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/edit/status"))
        .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
        .GET()
        .build();
    var response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());
    String status = parseStatus(response.body());
    if ("Completed".equals(status)) break;
    if ("Failed".equals(status)) throw new RuntimeException("Edit failed - contact support");
    Thread.sleep(interval);
    interval = Math.min((long)(interval * 1.2), 60_000);
}
```

```ruby
require 'net/http'
require 'json'
require 'uri'

interval = 10
loop do
  uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/edit/status")
  req = Net::HTTP::Get.new(uri)
  req['x-api-key'] = ENV['IMAGEN_API_KEY']

  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
  status = JSON.parse(res.body)['data']['status']
  puts "Status: #{status}"

  break if status == 'Completed'
  raise 'Edit failed - contact support' if status == 'Failed'
  sleep interval
  interval = [interval * 1.2, 60].min
end
```

```php
<?php
$interval = 10;
while (true) {
  $ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/edit/status');
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('IMAGEN_API_KEY')],
  ]);
  $response = curl_exec($ch);
  curl_close($ch);

  $status = json_decode($response, true)['data']['status'];
  echo "Status: $status
";

  if ($status === 'Completed') break;
  if ($status === 'Failed') throw new RuntimeException('Edit failed - contact support');
  sleep($interval);
  $interval = min($interval * 1.2, 60);
}
```

```csharp
using System.Net.Http;
using System.Text.Json;

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

var interval = TimeSpan.FromSeconds(10);
while (true) {
    var response = await client.GetAsync(
        $"https://api.imagen-ai.com/v1/projects/{projectUuid}/edit/status");
    var json = await response.Content.ReadAsStringAsync();
    var doc = JsonDocument.Parse(json);
    var status = doc.RootElement.GetProperty("data").GetProperty("status").GetString();

    Console.WriteLine($"Status: {status}");
    if (status == "Completed") break;
    if (status == "Failed") throw new Exception("Edit failed - contact support");
    await Task.Delay(interval);
    interval = interval.TotalSeconds < 60
        ? TimeSpan.FromSeconds(interval.TotalSeconds * 1.2)
        : TimeSpan.FromSeconds(60);

}
```

### Get temporary download links to download edited photos or XMPs

After receiving a `Completed` status, get the temporary download
links to download the edited photos.

`GET /v1/projects/PROJECT_UUID/edit/get_temporary_download_links`

**Request example**

```curl
curl 'https://api.imagen-ai.com/v1/projects/$PROJECT_UUID/edit/get_temporary_download_links' \
  --header 'x-api-key: $IMAGEN_API_KEY'
```

```python
links = await client.get_download_links(project_uuid)
# links is a list of presigned XMP download URLs
```

```typescript
const links = await client.getDownloadLinks(projectUuid);
// links is an array of presigned XMP download URLs
```

```go
links, _ := client.GetDownloadLinks(ctx, projectUUID)
// links.FilesList pairs each file name with a presigned XMP download URL
files, _ := client.DownloadFiles(ctx, links.FilesList, "edited", nil)
fmt.Printf("saved %d XMPs\n", len(files))
```

```java
import java.net.URI;

var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/edit/get_temporary_download_links"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .GET()
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() to extract data.files_list
```

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/edit/get_temporary_download_links")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
files = JSON.parse(res.body)['data']['files_list']

# files: [{ "file_name" => "...", "download_link" => "..." }, ...]
```

```php
<?php
$ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/edit/get_temporary_download_links');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('IMAGEN_API_KEY')],
]);
$response = curl_exec($ch);
curl_close($ch);
$files = json_decode($response, true)['data']['files_list'];
```

```csharp
using System.Net.Http;
using System.Text.Json;

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

var response = await client.GetAsync(
    $"https://api.imagen-ai.com/v1/projects/{projectUuid}/edit/get_temporary_download_links");
var json = await response.Content.ReadAsStringAsync();
// Parse json to access data.files_list
```

**Response example**

The response includes a temporary link for each photo to Imagen's S3 bucket
on AWS. This link is the AWS presigned URL.

```json
{
  "data": {
    "files_list": [
      {
        "file_name": "922A4846.DNG",
        "download_link": "<presigned URL for 922A4846.DNG>"
      },
      {
        "file_name": "922A4832.DNG",
        "download_link": "<presigned URL for 922A4832.DNG>"
      },
      {
        "file_name": "922A4818.DNG",
        "download_link": "<presigned URL for 922A4818.DNG>"
      },
      {
        "file_name": "922A4809.DNG",
        "download_link": "<presigned URL for 922A4809.DNG>"
      }
    ]
  }
}
```

### Download edited photos locally

When Imagen returns the edited files, these are the respective formats:

<table>
<thead>
<tr>
<th>File format before edit</th>
<th>File format returned after edit</th>
</tr>
</thead>
<tbody>
<tr>
<td>RAW</td>
<td>
XMP

<small>
Note: When editing photos with HDR Merge, the returned format is
always DNG, even though the file format before editing was RAW.
</small>
</td>
</tr>
<tr>
<td>RAW + DNG</td>
<td>DNG with embedded XMP</td>
</tr>
<tr>
<td>DNG</td>
<td>DNG with embedded XMP</td>
</tr>
<tr>
<td>JPEG</td>
<td>JPEG with embedded XMP</td>
</tr>
</tbody>
</table>

Download photos from Imagen's S3 bucket on AWS with the temporary download
links from the response in the previous step. Use any method you like.

Review the edits in Adobe editing software. If you open the photos with the
default photo viewer on your computer, you won't see the edits.

### Optional: Export final JPEGs for delivery

Imagen API exports all the photos in a project to JPEG format. This step is optional. Some photographers tweak the edits and export them with their editing software. After exporting the final JPEGs, you deliver them or upload them to a gallery.

#### 1. Export the photos in the project

`POST /v1/projects/PROJECT_UUID/export`
> **Content-Type must be empty** This endpoint requires an empty `Content-Type` header. The Python SDK handles this automatically.
```curl
curl -X POST \
  'https://api.imagen-ai.com/v1/projects/$PROJECT_UUID/export' \
  --header 'x-api-key: $IMAGEN_API_KEY' \
  --header 'Content-Type;'
```

```python
await client.export_project(project_uuid)
```

```typescript
await client.exportProject(projectUuid);
```

```go
// Trigger the export and poll until it finishes:
if err := client.ExportAndWait(ctx, projectUUID, nil); err != nil {
    log.Fatal(err)
}
// client.StartExport(ctx, projectUUID) fires it without waiting
```

```java
import java.net.URI;

var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/export"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .header("Content-Type", "")
    .POST(HttpRequest.BodyPublishers.noBody())
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
```

```ruby
require 'net/http'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/export")
req = Net::HTTP::Post.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']
req['Content-Type'] = ''

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
```

```php
<?php
$ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/export');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'x-api-key: ' . getenv('IMAGEN_API_KEY'),
    'Content-Type:',
  ],
  CURLOPT_POSTFIELDS => '',
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)['data'];
```

```csharp
using System.Net.Http;
using System.Text;

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

// Pass "" as the media type to send an empty Content-Type header
var content = new StringContent("", Encoding.UTF8, "");
var response = await client.PostAsync(
    $"https://api.imagen-ai.com/v1/projects/{projectUuid}/export",
    content
);
var json = await response.Content.ReadAsStringAsync();
```

The response includes the `project_uuid` and a message that the export was successful. If there is an error, email our customer success team.

#### 2. Check the export status

If you didn't add a callback URL in your edit request, get the export status.

`GET /v1/projects/PROJECT_UUID/export/status`

Possible statuses are:

- **In Progress:** Imagen API is in the process of editing the photos.

- **Failed:** Email the customer success team.

- **Completed:** Imagen API has finished exporting the photos to JPEG.

```curl
# Poll until status is "Completed" or "Failed"
curl 'https://api.imagen-ai.com/v1/projects/$PROJECT_UUID/export/status' \
  --header 'x-api-key: $IMAGEN_API_KEY'
```

```python
# The SDK polls export status internally inside export_project()
# To check the raw status endpoint yourself:

async with httpx.AsyncClient() as http:
    resp = await http.get(
        f"https://api.imagen-ai.com/v1/projects/{project_uuid}/export/status",
        headers={"x-api-key": os.environ["IMAGEN_API_KEY"]},
    )
    data = resp.json()["data"]
    print(data["status"])  # In Progress | Completed | Failed
```

```typescript
// The SDK polls export status internally inside exportProject().
// To check the raw status endpoint yourself:
const res = await fetch(
  `https://api.imagen-ai.com/v1/projects/${projectUuid}/export/status`,
  { headers: { 'x-api-key': process.env.IMAGEN_API_KEY! } }
);
const { data } = (await res.json()) as { data: { status: string } };
console.log(data.status); // In Progress | Completed | Failed
```

```go
status, _ := client.ExportStatus(ctx, projectUUID)
fmt.Println(status.Status) // In Progress | Completed | Failed

// Or let the SDK poll with backoff until it finishes:
if _, err := client.WaitForExport(ctx, projectUUID, nil); err != nil {
    log.Fatal(err) // a Failed status surfaces as an error
}
```

```java
import java.net.URI;

long interval = 10_000;
while (true) {
    var request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/export/status"))
        .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
        .GET()
        .build();
    var response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());
    String status = parseStatus(response.body());
    if ("Completed".equals(status)) break;
    if ("Failed".equals(status)) throw new RuntimeException("Export failed - contact support");
    Thread.sleep(interval);
    interval = Math.min((long)(interval * 1.2), 60_000);
}
```

```ruby
require 'net/http'
require 'json'
require 'uri'

interval = 10
loop do
  uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/export/status")
  req = Net::HTTP::Get.new(uri)
  req['x-api-key'] = ENV['IMAGEN_API_KEY']

  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
  status = JSON.parse(res.body)['data']['status']
  puts "Status: #{status}"

  break if status == 'Completed'
  raise 'Export failed - contact support' if status == 'Failed'
  sleep interval
  interval = [interval * 1.2, 60].min
end
```

```php
<?php
$interval = 10;
while (true) {
  $ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/export/status');
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('IMAGEN_API_KEY')],
  ]);
  $response = curl_exec($ch);
  curl_close($ch);

  $status = json_decode($response, true)['data']['status'];
  echo "Status: $status
";

  if ($status === 'Completed') break;
  if ($status === 'Failed') throw new RuntimeException('Export failed - contact support');
  sleep($interval);
  $interval = min($interval * 1.2, 60);
}
```

```csharp
using System.Net.Http;
using System.Text.Json;

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

var interval = TimeSpan.FromSeconds(10);
while (true) {
    var response = await client.GetAsync(
        $"https://api.imagen-ai.com/v1/projects/{projectUuid}/export/status");
    var json = await response.Content.ReadAsStringAsync();
    var doc = JsonDocument.Parse(json);
    var status = doc.RootElement.GetProperty("data").GetProperty("status").GetString();

    Console.WriteLine($"Status: {status}");
    if (status == "Completed") break;
    if (status == "Failed") throw new Exception("Export failed - contact support");
    await Task.Delay(interval);
    interval = interval.TotalSeconds < 60
        ? TimeSpan.FromSeconds(interval.TotalSeconds * 1.2)
        : TimeSpan.FromSeconds(60);

}
```

#### 3. Get temporary download links to download final JPEG photos

Get the temporary download links when the export status is returned as `Completed`.

`GET /v1/projects/PROJECT_UUID/export/get_temporary_download_links`

In the request, include the exported photos to download in the `files_list` param. Use list format.

```curl
curl 'https://api.imagen-ai.com/v1/projects/$PROJECT_UUID/export/get_temporary_download_links' \
  --header 'x-api-key: $IMAGEN_API_KEY'
```

```python
links = await client.get_export_links(project_uuid)
# links is a list of presigned JPEG download URLs
```

```typescript
const links = await client.getExportLinks(projectUuid);
// links is an array of presigned JPEG download URLs
```

```go
links, _ := client.GetExportDownloadLinks(ctx, projectUUID)
// links.FilesList pairs each file name with a presigned JPEG download URL
files, _ := client.DownloadFiles(ctx, links.FilesList, "exported", nil)
fmt.Printf("saved %d JPEGs\n", len(files))
```

```java
import java.net.URI;

var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/projects/" + projectUuid + "/export/get_temporary_download_links"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .GET()
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() to extract data.files_list
```

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/projects/#{project_uuid}/export/get_temporary_download_links")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
files = JSON.parse(res.body)['data']['files_list']

# files: [{ "file_name" => "...", "download_link" => "..." }, ...]
```

```php
<?php
$ch = curl_init('https://api.imagen-ai.com/v1/projects/' . $projectUuid . '/export/get_temporary_download_links');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('IMAGEN_API_KEY')],
]);
$response = curl_exec($ch);
curl_close($ch);
$files = json_decode($response, true)['data']['files_list'];
```

```csharp
using System.Net.Http;
using System.Text.Json;

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

var response = await client.GetAsync(
    $"https://api.imagen-ai.com/v1/projects/{projectUuid}/export/get_temporary_download_links");
var json = await response.Content.ReadAsStringAsync();
// Parse json to access data.files_list
```

**Response example**

The response includes a temporary link for each photo to Imagen's S3 bucket on AWS. This link is the AWS presigned URL.

```json
{
  "data": {
    "files_list": [
      {
        "file_name": "922A4846.JPG",
        "download_link": "<presigned URL for 922A4846.JPG>"
      },
      {
        "file_name": "922A4832.JPG",
        "download_link": "<presigned URL for 922A4832.JPG>"
      },
      {
        "file_name": "922A4818.JPG",
        "download_link": "<presigned URL for 922A4818.JPG>"
      },
      {
        "file_name": "922A4809.JPG",
        "download_link": "<presigned URL for 922A4809.JPG>"
      }
    ]
  }
}
```

#### 4. Download final JPEG photos locally

Download photos from Imagen's S3 bucket on AWS with the temporary download links from the response in the previous step. Use any method you like.

## List your projects

The flow above is fire-and-forget: you create a project, get its `project_uuid`
back, and use it. If you lose a UUID or want to reconcile what you've created,
list your projects instead of tracking every UUID yourself. Results are
paginated and newest-first.

`GET /v1/projects?size=20&page=0&is_archived=false`

Query parameters: `size` (1-100, default 20), `page` (>= 0, default 0),
`is_archived` (default false), and `get_thumbnail` (default true - pass `false`
to skip thumbnails for a faster response).

```curl
curl 'https://api.imagen-ai.com/v1/projects?size=20&page=0&is_archived=false' \
  --header 'x-api-key: $IMAGEN_API_KEY'
```

```python
listing = await client.list_projects(size=20, page=0, is_archived=False)
projects, pagination = listing.projects, listing.pagination
```

```typescript
const listing = await client.listProjects({ size: 20, page: 0, isArchived: false });
const { projects, pagination } = listing;
```

```go
import imagen "github.com/imagenai/imagen-ai-sdk/sdks/go"

listing, _ := client.ListProjects(ctx, &imagen.ListProjectsOptions{
    Size:       imagen.Int(20),
    Page:       imagen.Int(0),
    IsArchived: imagen.Bool(false),
})
fmt.Println(listing.Pagination.Total, "projects")
```

```java
import java.net.URI;

var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.imagen-ai.com/v1/projects?size=20&page=0&is_archived=false"))
    .header("x-api-key", System.getenv("IMAGEN_API_KEY"))
    .GET()
    .build();
var response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
```

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.imagen-ai.com/v1/projects")
uri.query = URI.encode_www_form(size: 20, page: 0, is_archived: false)
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = ENV['IMAGEN_API_KEY']

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)['data']
```

```php
<?php
$url = 'https://api.imagen-ai.com/v1/projects?'
  . http_build_query(['size' => 20, 'page' => 0, 'is_archived' => 'false']);
$ch = curl_init($url);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('IMAGEN_API_KEY')],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)['data'];
```

```csharp
using System.Net.Http;

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

var response = await client.GetAsync(
    "https://api.imagen-ai.com/v1/projects?size=20&page=0&is_archived=false");
var json = await response.Content.ReadAsStringAsync();
```
```json
{
  "data": {
    "projects": [
      {
        "project_id": 42,
        "project_uuid": "8f3a1c2d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
        "name": "Smith Wedding",
        "status": "Completed",
        "created_at": "2026-05-31T10:42:11Z",
        "number_of_images": 480,
        "number_of_images_edited": 480,
        "profile": "wedding",
        "ai_tools": ["crop", "straighten"],
        "customer_reference_id": 1234,
        "thumbnail_src": "https://<s3-presigned>",
        "export_status": "Completed"
      }
    ],
    "pagination": { "total": 137, "size": 20, "page": 0 }
  }
}
```

> **Looking up a UUID by name?** If you know a project's exact name, `GET /v1/projects/PROJECT_NAME/uuid` returns just its `project_uuid` without paging the full list.
