Skip to content

Programmatically Load Images

Introduction

This tutorial outlines how you can load images onto supported entities in VibeIQ using Contrail SDK or cURL.

Simplified Alternative

If you want a simpler approach that combines file linking and primary image assignment into a single step, see the Federated Load From File guide.

Covered Concepts

  • Uploading files with Contrail SDK or cURL.
  • Linking images to Items using Contrail SDK or cURL.
  • Linking images to Colors using Contrail SDK or cURL.
  • Setting an image as the primary image for an Item or Color.

End Product of this Tutorial

  • Images will be uploaded as File entities.
  • Images will be assigned to Items or Colors via Content and will be visible on their respective entity views in VibeIQ applications.
  • One image per entity will be designated as the primary image, which is displayed as the default thumbnail across VibeIQ applications.

Authentication

App Authentication

If your image load is running as part of an app action, you do not need to explicitly authenticate, and can skip this section. App actions are authorized automatically during runtime.

For detailed information on authenticating with Contrail, including how to generate API keys and use them with the SDK, see the Authentication & API Access guide.

Step 1: Create Image Files

Images can be uploaded as File entities to the VibeIQ platform using either the SDK or direct API calls.

The SDK provides the Files#createAndUploadFileFromBuffer method for uploading images.

This example assumes there is a subdirectory called images in the current directory where all the image files are located. The contentType is set to image/jpeg.

upload_image_files_script.ts
import {login, Files, Entities} from '@contrail/sdk';
import * as fs from "fs/promises"
import * as path from "node:path";

const pLimit = require('p-limit');
const limit = pLimit(10);

const loginCredentials = {
  orgSlug: process.env.USER_ORG,
  apiKey: process.env.API_KEY,
};

async function loadImages() {
    await login(loginCredentials);

    let fileUploadOperations = [];
    let contentToCreate = [];
    const contentType = "image/jpeg";
    const imagesToUpload = await fs.readdir('./images');

    for (const fileName of imagesToUpload) {
        const promise = limit(async () => {
            try {
                const arrayBuffer = await fs.readFile(`./images/${fileName}`);

                const imageBuffer = Buffer.from(new Uint8Array(arrayBuffer));
                const createdFile = await new Files().createAndUploadFileFromBuffer(
                    imageBuffer,
                    contentType,
                    fileName,
                    null,
                    24 * 60 * 60 * 1000
                );

                contentToCreate.push({
                    copyFileId: createdFile.id,
                    contentType,
                    fileName,
                })
            } catch (e) {
                console.log(
                    `Failed to upload image`,
                    e
                );
            }
        });
        fileUploadOperations.push(promise);
    }
    await Promise.all(fileUploadOperations);
}

Uploading images via cURL is a two-step process:

Step 1: Request Pre-Signed Information

First, request pre-signed information from the VibeIQ API to get temporary access to upload the image securely to an S3 bucket.

curl -X POST "https://api.vibeiq.com/prod/api/files" \
  -H "x-api-key: $API_KEY" \
  -H "x-api-org: $USER_ORG" \
  -H "Content-Type: application/json" \
  -d '{
    "contentType": "image/jpeg",
    "fileName": "image.jpeg",
    "ttl": 86400
  }'

The API returns a response containing:

  • File metadata (including fileKey and id)
  • A pre-signed S3 URL and upload form information (uploadPost)
  • AWS authorization fields
{
  "contentType": "image/jpeg",
  "fileName": "image.jpeg",
  "ttl": 86400,
  "fileKey": "0FYWOhu9p50/some-file-key/775d-8284-4c8d-b454-4509f9ebe92e",
  "fileBucket": "bucket-name",
  "fileUrl": "https://api.vibeiq.com/prod/api/files/downloadUrl/0FYWOhu9NMilc8d-b454-4509f9ebe92e",
  "createdOn": "2025-03-17T21:43:18.511Z",
  "updatedOn": "2025-03-17T21:43:18.511Z",
  "createdById": "9-4sss39qP",
  "updatedById": "9-4sunF4439qP",
  "orgId": "0FYWOhilep50",
  "id": "fkS4cOA25o5IX",
  "uploadPost": {
    "url": "https://s3.amazonaws.com/prod-contrail-file-content-bucket",
    "fields": {
      "key": "0FYWOhu9p50/some-file-key/775d-8284-4c8d-b454-4509f9ebe92e",
      "bucket": "prod-contrail-file-content-bucket",
      "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
      "X-Amz-Credential": "A0000000/17000/us-east-1/s3/aws4_request",
      "X-Amz-Date": "20250317T214318Z",
      "X-Amz-Security-Token": "IQoJbAIncgQjVi3some-security-code6G2dGuFFO/XolLglSgc+jNb0W4sF7rGvcWauKvqByjx4ob5yIAE4qAO7mSawTHKwIyy1Q==",
      "Policy": "eyJleHBpcmF0aW9uIjoiMjAyNS0wMy0xN1Lsome-policy-codeSxbImVxIiwiJQiLCIwiaW1hZ2UvanBTzdtU2F3VEhLd0l5eTFRPT0ifV19",
      "X-Amz-Signature": "b1c224f08a25d94f3387b908some-signature-68ce560c50fa65676c1b497401db3b",
      "x-amz-meta-fileName": "image.jpeg",
      "x-amz-meta-uploaderId": "9-4NZnF4439qP",
      "x-amz-meta-orgId": "0FYWOhilep50",
      "x-amz-meta-contentType": "image/jpeg",
      "x-amz-meta-fileId": "fkS4c25o5IX"
    }
  },
  "downloadUrl": "https://prod-contrail-file-content-bucket.s3.amazonaws.com/0FYWOh9Milep50de10d-8284-4c8d-b454-4509f9ebe92e?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAXGPNJ5%2F20250317%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250317T214318Z&X-Amz-Expires=86400&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEPb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJIMEYCIQCYBp%2FRbTHTUUrpWBy9c3tqpfuLcnhIv2ONaNzsiaOfMgIhAIncgQjVi36G2dGz2U9EfHfGaFCoZulRa3Rz3Q8iKiF3KpMDCE8QAxoMNDk0OTU1NzgyMDA2Igy5rGbRF9IWTdl7KTkq8AKDSBmbz7bRyonZiiQuROgPuFFO%2FXolLg%2Bm%2FDQ%2FOa1a3jn6pUp2Ij0jigzC417R7y%2B9oITWQ3Ixe%2BLwzY4C5mob2kyW%2FUDB3PaO9T306xvT2wPSgc%2BjNb0W4sF7rGvcWKtHCctPJ6TR4899GKDUnmgfTpIbokDC1ypARaAMeBAuId1q7%2Fm298Qi2WPGYhur985IigzEtJ16pVeMGYCSfz1GF8lvfTYLxZFISf52aqglXAQDtKSo0A24bOi%2F96Zm8m8GSIB75oUYaWkrErt3aI8ieu8i%2BJzWEy2K7s9VqxCYuAyxOWW4muhVMhYTVKhvjZpA%2FcTGZDb66mtJjPuKOmfrNpml8JTPpmL7oSA3FVE3v7a423l0EMjrt5DFXeAuvHE7iGYWjws5u%2B3QwVQQ0PuTKUTm7E6eL8TTcCumqee4rrP7sEhiVbpXKvn9jLI8tTtjWCtwWeDMFvLFUaYfabJCdfwxyBSztxfqi7rSN9o5vzCxquK%2BBjqcAZasSaGGDmjjAQF%2B%2BpHdIxzkZowSSAWAt6qN7VA7e9%2BPHztDDgrSZnyM0gCpoTZWK7DcWCxQCCpC6MNlXmGsUIHmNM4JFm%2F9xBsim%2BUkYLTAWC23mZkk7xpN%2FtMiyF1C%2BwtcDlWB79kAFU7zsPG32PKcU8znJ1jP7zYzOT0ofG6eMAPauKvqByjx4ob5yIAE4qAO7mSawTHKwIyy1Q%3D%3D&X-Amz-Signature=9aa9bc64b8c34e47aecd1ba2588f9a3cd912d63da120de03be2e7a2e439e6967&X-Amz-SignedHeaders=host&response-content-disposition=attachment%3B%20filename%3Dimage.jpeg",
  "adminDownloadUrl": "https://admin.vibeiq.com/org/your-org-name/files/fkS4o5IX/download"
}

Step 2: Upload the File to S3

Using the uploadPost data from the previous step, upload the file to S3.

curl -X POST 'https://s3.amazonaws.com/prod-contrail-file-content-bucket' \
  -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundary123456' \
  -F 'key=0FYWOhu9p50/some-file-key/775d-8284-4c8d-b454-4509f9ebe92e' \
  -F 'bucket=prod-contrail-file-content-bucket' \
  -F 'x-amz-meta-contentType=image/jpeg' \
  -F 'Content-Type=image/jpeg' \
  -F 'X-Amz-Algorithm=AWS4-HMAC-SHA256' \
  -F 'X-Amz-Credential=A0000000/17000/us-east-1/s3/aws4_request' \
  -F 'X-Amz-Date=20250317T214318Z' \
  -F 'X-Amz-Security-Token=IQoJbAIncgQjVi3some-security-code6G2dGuFFO/XolLglSgc+jNb0W4sF7rGvcWauKvqByjx4ob5yIAE4qAO7mSawTHKwIyy1Q==' \
  -F 'Policy=eyJleHBpcmF0aW9uIjoiMjAyNS0wMy0xN1Lsome-policy-codeSxbImVxIiwiJQiLCIwiaW1hZ2UvanBTzdtU2F3VEhLd0l5eTFRPT0ifV19' \
  -F 'X-Amz-Signature=b1c224f08a25d94f3387b908some-signature-68ce560c50fa65676c1b497401db3b' \
  -F 'x-amz-meta-fileName=image.jpeg' \
  -F 'x-amz-meta-uploaderId=9-4NZnF4439qP' \
  -F 'x-amz-meta-orgId=0FYWOhilep50' \
  -F 'x-amz-meta-fileId=fkS4c25o5IX' \
  -F 'file=@/path/to/image.jpeg'

You can link images to items or colors by creating a Content entity. The Content entity acts as a link between a File and an Item or Color.

To create a Content entity, you need to provide the following data:

  • contentHolderReference: A reference to the entity the image will be linked to, in the format item:<item ID> or color:<color ID>.
  • contentType: The content type of the image being uploaded (e.g., image/jpeg).
  • copyFileId: The ID of the file you created in the previous step.
image_load_script.ts
  const tasks = contentToCreate.map((content) =>
  limit(async () => {
    try {
      const itemFederatedId = content.fileName.split("_")[0];

      // Fetch the item based on its federated ID
      const item = await new Entities().get({
        entityName: 'item',
        federatedId: itemFederatedId,
      });

      // Create the content linked to the item
      const resultingContent = await new Entities().create({
        entityName: 'content',
        object: {
          contentHolderReference: `item:${item.id}`,
          contentType,
          copyFileId: content.copyFileId,
        },
      });
    } catch (e) {
      console.log(`Failed to create content for ${content.fileName}`, e);
    }
  })
);

// Wait for all the mapped tasks to finish
await Promise.all(tasks);
}

This will successfully attach an image to each of the Items provided the Items federated IDs are indicated in the file names.

image_load_script.ts
  const tasks = contentToCreate.map((content) =>
  limit(async () => {
    try {
      const colorFederatedId = content.fileName.split("_")[0];

      // Fetch the color based on its federated ID
      const color = await new Entities().get({
        entityName: 'color',
        federatedId: colorFederatedId,
      });

      // Create the content linked to the color
      const resultingContent = await new Entities().create({
        entityName: 'content',
        object: {
          contentHolderReference: `color:${color.id}`,
          contentType,
          copyFileId: content.copyFileId,
        },
      });
    } catch (e) {
      console.log(`Failed to create content for ${content.fileName}`, e);
    }
  })
);

// Wait for all the mapped tasks to finish
await Promise.all(tasks);
}

This will successfully attach an image to each of the Colors provided the Colors federated IDs are indicated in the file names.

curl -X POST '{{baseURL}}/content' \
  -H 'x-api-key: $API_TOKEN' \
  -H 'x-api-org: $USER_ORG' \
  -H 'Content-Type: application/json' \
  -d '{
  "contentHolderReference": "item:mkzvjUlEFzKv-jUc",
  "contentType": "image/jpeg",
  "copyFileId": "jhQntcifRfjQkQ0G"
}'

Some iterative method will be required to run this request automatically for all items and images.

curl -X POST '{{baseURL}}/content' \
  -H 'x-api-key: $API_TOKEN' \
  -H 'x-api-org: $USER_ORG' \
  -H 'Content-Type: application/json' \
  -d '{
  "contentHolderReference": "color:mkzvjUlEFzKv-jUc",
  "contentType": "image/jpeg",
  "copyFileId": "jhQntcifRfjQkQ0G"
}'

Some iterative method will be required to run this request automatically for all colors and images.

Step 3: Set the Primary Image

When multiple images are attached to an entity, one image serves as the primary image — the default thumbnail displayed across VibeIQ applications. Without this step, entities will show images in the order they were attached, but no image will be explicitly marked as the primary.

The primary image is controlled by the primaryViewableId field on the owner entity (Item or Color). Setting this field to a Content entity's ID designates that Content's image as the primary.

First Image Behavior

If an entity has no primaryViewableId set and you attach a single image, it will appear as the only image. However, explicitly setting the primary image is recommended, especially when loading multiple images per entity.

After creating Content entities in Step 2, update each Item's primaryViewableId to the desired Content ID:

image_load_script.ts
const primaryImageTasks = contentToCreate.map((content) =>
  limit(async () => {
    try {
      const itemFederatedId = content.fileName.split("_")[0];

      // Fetch the item based on its federated ID
      const item = await new Entities().get({
        entityName: 'item',
        federatedId: itemFederatedId,
      });

      // Create the content linked to the item
      const resultingContent = await new Entities().create({
        entityName: 'content',
        object: {
          contentHolderReference: `item:${item.id}`,
          contentType,
          copyFileId: content.copyFileId,
        },
      });

      // Set the content as the primary image on the item
      await new Entities().update({
        entityName: 'item',
        id: item.id,
        object: {
          primaryViewableId: resultingContent.id,
        },
      });
    } catch (e) {
      console.log(`Failed to create content for ${content.fileName}`, e);
    }
  })
);

await Promise.all(primaryImageTasks);

Selective Primary Assignment

If you're loading multiple images per Item and only the first should be primary, track which Items have already had a primary set and skip the update for subsequent images.

After creating Content entities in Step 2, update each Color's primaryViewableId to the desired Content ID:

image_load_script.ts
const primaryImageTasks = contentToCreate.map((content) =>
  limit(async () => {
    try {
      const colorFederatedId = content.fileName.split("_")[0];

      // Fetch the color based on its federated ID
      const color = await new Entities().get({
        entityName: 'color',
        federatedId: colorFederatedId,
      });

      // Create the content linked to the color
      const resultingContent = await new Entities().create({
        entityName: 'content',
        object: {
          contentHolderReference: `color:${color.id}`,
          contentType,
          copyFileId: content.copyFileId,
        },
      });

      // Set the content as the primary image on the color
      await new Entities().update({
        entityName: 'color',
        id: color.id,
        object: {
          primaryViewableId: resultingContent.id,
        },
      });
    } catch (e) {
      console.log(`Failed to create content for ${content.fileName}`, e);
    }
  })
);

await Promise.all(primaryImageTasks);

After creating Content in Step 2, update the Item's primaryViewableId with the Content ID returned from the create call:

curl -X PUT 'https://api.vibeiq.com/prod/api/entities/item/<ITEM_ID>' \
  -H 'x-api-key: $API_TOKEN' \
  -H 'x-api-org: $USER_ORG' \
  -H 'Content-Type: application/json' \
  -d '{
  "primaryViewableId": "<CONTENT_ID>"
}'

Replace <ITEM_ID> with the Item's ID and <CONTENT_ID> with the ID of the Content entity returned in Step 2.

After creating Content in Step 2, update the Color's primaryViewableId with the Content ID returned from the create call:

curl -X PUT 'https://api.vibeiq.com/prod/api/entities/color/<COLOR_ID>' \
  -H 'x-api-key: $API_TOKEN' \
  -H 'x-api-org: $USER_ORG' \
  -H 'Content-Type: application/json' \
  -d '{
  "primaryViewableId": "<CONTENT_ID>"
}'

Replace <COLOR_ID> with the Color's ID and <CONTENT_ID> with the ID of the Content entity returned in Step 2.