Status Endpoint Migration

This guide helps you migrate from v1 status checking endpoints to the unified /v2/status/{jobId} endpoint.

Overview

In v1, different services had their own status endpoints. V2 consolidates all job status checking into a single endpoint regardless of the operation type.

V1 Endpoints:

V2 Endpoint:

Benefits of the unified endpoint

Basic migration example

Key differences

Base URL:

Endpoint:

Response Structure:

V1 approach

Lightroom Status:

curl -X GET \
  https://image.adobe.io/lrService/status/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $token" \
  -H "x-api-key: $apiKey"

Photoshop Status:

curl -X GET \
  https://image.adobe.io/pie/psdService/status/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $token" \
  -H "x-api-key: $apiKey"

V2 approach

Unified Status (all operations):

curl -X GET \
  https://photoshop-api.adobe.io/v2/status/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $token" \
  -H "x-api-key: $apiKey"

Response format

Job status values

Possible Status Values:

Successful job response

{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "succeeded",
  "createdTime": "2025-01-15T10:30:00.000Z",
  "modifiedTime": "2025-01-15T10:30:45.000Z",
  "result": {
    "outputs": [
      {
        "url": "https://storage.example.com/output.jpg",
        "mediaType": "image/jpeg"
      }
    ]
  }
}

Failed job response

{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "createdTime": "2025-01-15T10:30:00.000Z",
  "modifiedTime": "2025-01-15T10:30:45.000Z",
  "errorDetails": [
    {
      "errorCode": "400401",
      "message": "The value provided is not valid."
    }
  ]
}

Running job response

{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "running",
  "createdTime": "2025-01-15T10:30:00.000Z",
  "modifiedTime": "2025-01-15T10:30:15.000Z"
}

Polling pattern

async function pollJobStatus(jobId, maxAttempts = 60, intervalMs = 5000) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(
      `https://photoshop-api.adobe.io/v2/status/${jobId}`,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          "x-api-key": apiKey,
        },
      }
    );

    const status = await response.json();

    if (status.status === "succeeded") {
      return status.result;
    }

    if (status.status === "failed") {
      throw new Error(`Job failed: ${JSON.stringify(status.errorDetails)}`);
    }

    // Status is 'pending' or 'running', wait and retry
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error("Job polling timeout");
}

Best Practices:

Polling with exponential backoff

async function pollWithBackoff(jobId, maxAttempts = 20) {
  let interval = 2000; // Start with 2 seconds
  const maxInterval = 30000; // Cap at 30 seconds

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(
      `https://photoshop-api.adobe.io/v2/status/${jobId}`,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          "x-api-key": apiKey,
        },
      }
    );

    const status = await response.json();

    if (status.status === "succeeded" || status.status === "failed") {
      return status;
    }

    await new Promise((resolve) => setTimeout(resolve, interval));
    interval = Math.min(interval * 1.5, maxInterval);
  }

  throw new Error("Job polling timeout");
}

Output handling

External storage outputs

When using external storage, outputs contain URLs to download results:

{
  "result": {
    "outputs": [
      {
        "url": "https://storage.example.com/output.jpg",
        "mediaType": "image/jpeg"
      }
    ]
  }
}

Downloading Outputs:

curl -X GET "https://storage.example.com/output.jpg" -o output.jpg

Hosted storage outputs

With hosted storage, outputs contain temporary Adobe-managed URLs:

{
  "result": {
    "outputs": [
      {
        "url": "https://adobe-hosted-storage.example.com/temp/output.jpg?expires=...",
        "mediaType": "image/jpeg"
      }
    ]
  }
}
data-variant=warning
data-slots=text
Hosted storage URLs expire based on the validityPeriod specified in your request. Download outputs before expiration.

Embedded storage outputs

With embedded storage, outputs contain data directly in the response:

{
  "result": {
    "outputs": [
      {
        "destination": {
          "embedded": "base64",
          "content": "iVBORw0KGgoAAAANSUhEUgAA..."
        },
        "mediaType": "image/jpeg"
      }
    ]
  }
}

or for JSON outputs:

{
  "result": {
    "outputs": [
      {
        "destination": {
          "embedded": "json",
          "content": {
            "document": {...},
            "layers": [...]
          }
        },
        "mediaType": "application/json"
      }
    ]
  }
}

Error handling

Error response structure

{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "createdTime": "2025-01-15T10:30:00.000Z",
  "modifiedTime": "2025-01-15T10:30:45.000Z",
  "errorDetails": [
    {
      "errorCode": "400401",
      "message": "The value provided is not valid."
    },
    {
      "errorCode": "400420",
      "message": "Required fields missing on the API request."
    }
  ]
}

Common error codes

Error Code
Description
400400
Invalid JSON in request payload
400401
Invalid value provided
400410
Required fields missing
400411
Request blocked by content filters
403401
User not authorized
403421
Quota exhausted
404
Job not found
422404
Unable to process outputs
500600
Internal server error

Error handling example

async function handleJobStatus(jobId) {
  try {
    const status = await pollJobStatus(jobId);

    if (status.status === "failed") {
      console.error("Job failed with errors:");
      status.errorDetails.forEach((error) => {
        console.error(`- ${error.errorCode}: ${error.message}`);
      });
      throw new Error("Job processing failed");
    }

    return status.result.outputs;
  } catch (error) {
    if (error.response?.status === 404) {
      throw new Error("Job not found. Invalid job ID.");
    }
    throw error;
  }
}

HTTP status codes

The status endpoint itself can return different HTTP status codes:

HTTP Status
Meaning
200
Status retrieved successfully
400
Invalid job ID format
401
Invalid or missing access token
404
Job not found
500
Internal server error

Job lifecycle

Understanding the job lifecycle helps with proper status handling:

1. Job Submitted
   ↓
2. pending → Job queued, waiting to start
   ↓
3. running → Job actively processing
   ↓
4. succeeded → Job completed successfully
   OR
4. failed → Job encountered errors

Typical Timing:

Complete polling example

// Complete example with proper error handling
async function processJob(jobId) {
  const maxAttempts = 60;
  const pollInterval = 5000;

  console.log(`Starting to poll job ${jobId}`);

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      const response = await fetch(
        `https://photoshop-api.adobe.io/v2/status/${jobId}`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
            "x-api-key": apiKey,
          },
        }
      );

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error(`Job ${jobId} not found`);
        }
        throw new Error(`Status check failed: ${response.statusText}`);
      }

      const status = await response.json();
      console.log(`Job status: ${status.status} (attempt ${attempt + 1})`);

      switch (status.status) {
        case "succeeded":
          console.log("Job completed successfully");
          return status.result.outputs;

        case "failed":
          console.error("Job failed:", status.errorDetails);
          throw new Error(
            `Job failed: ${status.errorDetails
              .map((e) => e.message)
              .join(", ")}`
          );

        case "pending":
        case "running":
          // Continue polling
          await new Promise((resolve) => setTimeout(resolve, pollInterval));
          break;

        default:
          throw new Error(`Unknown job status: ${status.status}`);
      }
    } catch (error) {
      console.error(`Error polling job status:`, error);
      throw error;
    }
  }

  throw new Error(
    `Job polling timeout after ${(maxAttempts * pollInterval) / 1000} seconds`
  );
}

// Usage
try {
  const outputs = await processJob(jobId);
  console.log("Outputs:", outputs);
} catch (error) {
  console.error("Job processing failed:", error);
}

Migration checklist

Common migration issues

Issue: Using service-specific endpoint

Problem: Still using separate endpoints per service

// Wrong
const url = `https://image.adobe.io/lrService/status/${jobId}`;

Solution: Use unified endpoint

// Correct
const url = `https://photoshop-api.adobe.io/v2/status/${jobId}`;

Issue: Incorrect status value handling

Problem: Only checking for "success" and "failure"

Solution: Handle all status values

if (status.status === 'succeeded') { ... }
else if (status.status === 'failed') { ... }
else if (status.status === 'running' || status.status === 'pending') {
  // Continue polling
}

Issue: Not handling error details array

Problem: Expecting single error message

Solution: Handle array of errors

if (status.errorDetails) {
  status.errorDetails.forEach((error) => {
    console.error(`${error.errorCode}: ${error.message}`);
  });
}

Next steps

Need help?

Contact the Adobe DI ART Service team for technical support with status endpoint migration.