---
title: useCreateImageContent
description: >-
  The useCreateImageContent hook combines image upload with content creation to
  generate user-generated content entries.
source_url:
  html: 'https://shopify.dev/docs/api/shop-minis/hooks/content/usecreateimagecontent'
  md: >-
    https://shopify.dev/docs/api/shop-minis/hooks/content/usecreateimagecontent.md
api_name: shop-minis
---

# use​Create​Image​Content

The `useCreateImageContent` hook combines image upload with content creation to generate user-generated content entries. Built on top of `useImageUpload`, it extends the basic upload functionality with content management features including titles, descriptions, visibility controls, product associations, and unique IDs for sharing and discovery.

### Parameters

The `createImageContent` function accepts an object with the following fields:

* **`image`** (required): A `File` object representing the image to upload.
* **`contentTitle`** (required): The title for the content entry.
* **`visibility`** (optional): An array of `ContentVisibility` values (see below).
* **`externalId`** (optional): A unique identifier from your own system that lets you look up content later via `ContentWrapper` using an ID you already know. If not provided, the content can only be retrieved by its `publicId`. Each `externalId` must be unique per Mini — creating content with a duplicate `externalId` returns a `DUPLICATE_EXTERNAL_ID` error.
* **`description`** (optional): A text description for the content. Displayed alongside the content in Shop surfaces such as feeds and content detail views. Use this to provide context about the image, such as a caption or review text.
* **`productIds`** (optional): An array of Shopify product GIDs (e.g. `'gid://shopify/Product/123'`) to associate with the content. Maximum 20 products. Associated products appear alongside the content, enabling shoppable content experiences. If any product IDs are ineligible, the mutation will return an `INELIGIBLE_PRODUCTS` error.

### Visibility Options

The `visibility` parameter accepts an optional array of `ContentVisibility` values:

* **`DISCOVERABLE`**: Makes content eligible for Shop's recommendation and discovery systems. Your content can appear in feeds and recommendations across the Shop app.
* **`LINKABLE`**: Enables shareable URLs for the content. When set, the created `Content` object includes a `shareableUrl` field containing a URL that can be shared externally. The shareable page renders OpenGraph meta tags (`og:title`, `og:image`, `og:description`) using the content's `contentTitle`, uploaded image, and `description` — so link previews in social media, messaging apps, and other platforms will display a rich card with the content's title, image, and description. Use the `useShare` hook to trigger the native share sheet with this URL.

### Error Handling

The hook may return `userErrors` in the response with the following codes:

* **`DUPLICATE_EXTERNAL_ID`**: Returned when an `externalId` is provided that already exists for this Mini. Each `externalId` must be unique per content entry.
* **`INELIGIBLE_PRODUCTS`**: Returned when one or more `productIds` refer to products that are not eligible for Shop. The error message includes the specific ineligible product GIDs.

### Use Cases

* **Shareable content with rich link previews**: Include `LINKABLE` in the `visibility` array and provide a `contentTitle`, `description`, and image. The returned `shareableUrl` points to a page with OpenGraph tags, so when shared via social media or messaging apps the link preview displays the content's title, image, and description as a rich card. Pass the `shareableUrl` to the `useShare` hook to open the native share sheet.
* **User reviews with photos**: Provide `description` for the review text, `productIds` to link the reviewed products, and `['DISCOVERABLE', 'LINKABLE']` visibility for maximum reach.
* **Shoppable lookbooks**: Upload styled images with associated `productIds` so users can shop the look.
* **Content lookup by your own ID**: Set `externalId` to an identifier from your system (e.g. a database row ID or slug) so you can retrieve the content later via `ContentWrapper` without storing the `publicId`.

The hook handles both the image upload pipeline and content entity creation in a single operation, automatically associating content with your Mini and integrating with Shop's content systems.

**Caution:**

You must run the [`setup`](https://shopify.dev/docs/api/shop-minis/commands/setup) CLI command before using this hook so the content can be associated with the Mini.

## use​Create​Image​Content()

### Returns

* **UseCreateImageContentReturns**

### UseCreateImageContentReturns

* createImageContent

  Upload an image and create content.

  ```ts
  (params: CreateImageContentParams) => Promise<{ data: Content; userErrors?: ContentCreateUserErrors[]; }>
  ```

* loading

  Whether the content is being created.

  ```ts
  boolean
  ```

### CreateImageContentParams

* contentTitle

  The title for the content entry.

  ```ts
  string
  ```

* description

  A text description for the content. Displayed alongside the image in Shop surfaces such as feeds and content detail views. Use this to provide context about the image, such as a caption or review text.

  ```ts
  string
  ```

* externalId

  A unique identifier from your own system that lets you look up content later via \`ContentWrapper\` using an ID you already know. If not provided, the content can only be retrieved by its \`publicId\`. Each \`externalId\` must be unique per Mini — creating content with a duplicate returns a \`DUPLICATE\_EXTERNAL\_ID\` error.

  ```ts
  string
  ```

* image

  The image file to upload.

  ```ts
  File
  ```

* productIds

  An array of Shopify product GIDs (e.g. \`'gid://shopify/Product/123'\`) to associate with the content. Maximum 20 products. Associated products appear alongside the content, enabling shoppable content experiences.

  ```ts
  string[]
  ```

* visibility

  Visibility options for the content. Use \`\['DISCOVERABLE']\` to appear in recommendations, \`\['LINKABLE']\` to enable shareable URLs, or both. Pass \`null\` or \`\[]\` to keep content private within your Mini.

  ```ts
  ContentVisibility[] | null
  ```

### ContentVisibility

```ts
'DISCOVERABLE' | 'LINKABLE'
```

### Content

* description

  ```ts
  string | null
  ```

* externalId

  ```ts
  string | null
  ```

* image

  ```ts
  ContentImage
  ```

* products

  ```ts
  ContentProduct[] | null
  ```

* publicId

  ```ts
  string
  ```

* shareableUrl

  ```ts
  string | null
  ```

* status

  ```ts
  MinisContentStatus | null
  ```

* title

  ```ts
  string
  ```

* visibility

  ```ts
  ContentVisibility[]
  ```

### ContentImage

* altText

  ```ts
  string | null
  ```

* height

  ```ts
  number | null
  ```

* id

  ```ts
  string | null
  ```

* thumbhash

  ```ts
  string | null
  ```

* url

  ```ts
  string
  ```

* width

  ```ts
  number | null
  ```

### ContentProduct

* featuredImage

  ```ts
  ContentImage | null
  ```

* id

  ```ts
  string
  ```

* title

  ```ts
  string
  ```

### MinisContentStatus

* PENDING

  ```ts
  PENDING
  ```

* READY

  ```ts
  READY
  ```

* REJECTED

  ```ts
  REJECTED
  ```

### ContentCreateUserErrors

* code

  ```ts
  ContentCreateUserErrorCode
  ```

* message

  ```ts
  string
  ```

### ContentCreateUserErrorCode

* DUPLICATE\_EXTERNAL\_ID

  ```ts
  DUPLICATE_EXTERNAL_ID
  ```

* INELIGIBLE\_PRODUCTS

  ```ts
  INELIGIBLE_PRODUCTS
  ```

Examples

### Examples

* ####

  ##### tsx

  ```tsx
  import {
    useCreateImageContent,
    useImagePicker,
    Button,
  } from '@shopify/shop-minis-react'

  export default function MyComponent() {
    const {createImageContent, loading} = useCreateImageContent()
    const {openCamera, openGallery} = useImagePicker()

    const handleCameraCapture = async () => {
      try {
        const file = await openCamera()
        const result = await createImageContent({
          image: file,
          contentTitle: 'Photo from camera',
          visibility: ['DISCOVERABLE', 'LINKABLE'],
          // Optional: set an external ID for deduplication and lookup
          externalId: 'my-unique-id-123',
          // Optional: add a description
          description: 'A photo captured with the camera',
          // Optional: associate up to 20 products (by GID)
          productIds: ['gid://shopify/Product/1'],
        })
        console.log({data: result.data, userErrors: result.userErrors})
      } catch (error) {
        console.error('Failed to capture and upload image:', error)
      }
    }

    const handleGallerySelect = async () => {
      try {
        const file = await openGallery()
        const result = await createImageContent({
          image: file,
          contentTitle: 'Photo from gallery',
          // Visibility options:
          // - ['DISCOVERABLE'] - Appears in Shop recommendations
          // - ['LINKABLE'] - Enables shareable URLs
          // - ['DISCOVERABLE', 'LINKABLE'] - Both features
          // - null or [] - Private within Mini only
          visibility: ['DISCOVERABLE', 'LINKABLE'],
          description: 'A photo selected from the gallery',
        })
        console.log({data: result.data, userErrors: result.userErrors})
      } catch (error) {
        console.error('Failed to select and upload image:', error)
      }
    }

    return (
      <>
        <Button onClick={handleCameraCapture} disabled={loading}>
          Take Photo and Upload
        </Button>
        <Button onClick={handleGallerySelect} disabled={loading}>
          Select from Gallery and Upload
        </Button>
        {loading && <p>Uploading image...</p>}
      </>
    )
  }
  ```

* ####

  ##### Description

  A minimal example that uploads an image with a title and visibility.

  ##### tsx

  ```tsx
  import {
    useCreateImageContent,
    useImagePicker,
    Button,
  } from '@shopify/shop-minis-react'

  export default function BasicUpload() {
    const {createImageContent, loading} = useCreateImageContent()
    const {openGallery} = useImagePicker()

    const handleUpload = async () => {
      try {
        const file = await openGallery()
        const result = await createImageContent({
          image: file,
          contentTitle: 'My photo',
          visibility: ['DISCOVERABLE', 'LINKABLE'],
        })
        console.log('Created content:', result.data.publicId)
      } catch (error) {
        console.error('Upload failed:', error)
      }
    }

    return (
      <Button onClick={handleUpload} disabled={loading}>
        {loading ? 'Uploading...' : 'Upload Photo'}
      </Button>
    )
  }
  ```

* ####

  ##### Description

  Set \`externalId\` to an identifier from your own system so you can look up the content later via \`ContentWrapper\` without needing to store the \`publicId\`.

  ##### tsx

  ```tsx
  import {
    useCreateImageContent,
    useImagePicker,
    ContentWrapper,
    Button,
    Image,
  } from '@shopify/shop-minis-react'

  /**
   * Use `externalId` to look up content later using an ID you already know,
   * without needing to store the `publicId`.
   *
   * For example, if your Mini lets users upload a profile photo, you can set
   * the `externalId` to the user's ID in your system and later retrieve
   * the content with `<ContentWrapper externalId="user-42">`.
   */
  export default function WithExternalId() {
    const {createImageContent, loading} = useCreateImageContent()
    const {openCamera} = useImagePicker()

    // In practice this would come from your own user/session data
    const userId = 'user-42'

    const handleCapture = async () => {
      try {
        const file = await openCamera()
        const result = await createImageContent({
          image: file,
          contentTitle: 'Profile photo',
          visibility: ['DISCOVERABLE'],
          // Set externalId to an identifier from your own system.
          // You can then look up this content later via ContentWrapper
          // using the same ID — no need to persist the publicId.
          // Each externalId must be unique per Mini; duplicates return
          // a DUPLICATE_EXTERNAL_ID error.
          externalId: userId,
        })
        console.log('Created content:', result.data.publicId)
      } catch (error) {
        console.error('Upload failed:', error)
      }
    }

    return (
      <>
        <Button onClick={handleCapture} disabled={loading}>
          {loading ? 'Uploading...' : 'Upload Profile Photo'}
        </Button>

        {/* Later, retrieve the content by the same externalId */}
        <ContentWrapper externalId={userId}>
          {({content, loading: contentLoading}) => {
            if (contentLoading) return <p>Loading...</p>
            if (!content) return null
            return <Image src={content.image.url} alt={content.title} />
          }}
        </ContentWrapper>
      </>
    )
  }
  ```

* ####

  ##### Description

  Add a \`description\` to provide a caption or review text displayed alongside the image in Shop surfaces.

  ##### tsx

  ```tsx
  import {useState} from 'react'

  import {
    useCreateImageContent,
    useImagePicker,
    Button,
  } from '@shopify/shop-minis-react'

  /**
   * Add a description to provide context alongside the image.
   * Descriptions are displayed in Shop feeds and content detail views.
   */
  export default function WithDescription() {
    const {createImageContent, loading} = useCreateImageContent()
    const {openGallery} = useImagePicker()
    const [caption, setCaption] = useState('')

    const handleUpload = async () => {
      try {
        const file = await openGallery()
        const result = await createImageContent({
          image: file,
          contentTitle: 'Product review',
          // The description appears as body text alongside
          // the image in Shop surfaces like feeds and detail views.
          description: caption || 'Check out this product!',
          visibility: ['DISCOVERABLE', 'LINKABLE'],
        })
        console.log('Created content:', result.data.publicId)
      } catch (error) {
        console.error('Upload failed:', error)
      }
    }

    return (
      <>
        <input
          type="text"
          placeholder="Write a caption..."
          value={caption}
          onChange={e => setCaption(e.target.value)}
        />
        <Button onClick={handleUpload} disabled={loading}>
          {loading ? 'Posting...' : 'Post Review'}
        </Button>
      </>
    )
  }
  ```

* ####

  ##### Description

  Associate products with content using \`productIds\` to create shoppable images. Users can browse and purchase products directly from the content.

  ##### tsx

  ```tsx
  import {useState} from 'react'

  import {
    useCreateImageContent,
    useImagePicker,
    Button,
  } from '@shopify/shop-minis-react'

  /**
   * Associate products with content to create shoppable images.
   * Users can browse products directly from the content in Shop.
   */
  export default function WithProducts() {
    const {createImageContent, loading} = useCreateImageContent()
    const {openGallery} = useImagePicker()
    const [error, setError] = useState<string | null>(null)

    // These would typically come from your Mini's product selection UI
    const selectedProductIds = [
      'gid://shopify/Product/111',
      'gid://shopify/Product/222',
    ]

    const handleUpload = async () => {
      setError(null)
      try {
        const file = await openGallery()
        const result = await createImageContent({
          image: file,
          contentTitle: 'Shop the look',
          description: 'Styled outfit featuring our latest collection',
          visibility: ['DISCOVERABLE', 'LINKABLE'],
          // Associate up to 20 products by their Shopify GIDs.
          // If any product is ineligible, userErrors will contain
          // an INELIGIBLE_PRODUCTS error with the specific GIDs.
          productIds: selectedProductIds,
        })

        if (result.userErrors?.length) {
          setError(result.userErrors.map(e => e.message).join(', '))
          return
        }

        console.log('Created shoppable content:', result.data.publicId)
      } catch (err) {
        console.error('Upload failed:', err)
      }
    }

    return (
      <>
        <Button onClick={handleUpload} disabled={loading}>
          {loading ? 'Creating...' : 'Create Shoppable Post'}
        </Button>
        {error && <p style={{color: 'red'}}>{error}</p>}
      </>
    )
  }
  ```

* ####

  ##### Description

  Create content with \`LINKABLE\` visibility to get a \`shareableUrl\`. The URL renders OpenGraph meta tags (\`og:title\`, \`og:image\`, \`og:description\`) from the content's title, image, and description, so link previews in social media and messaging apps display a rich card. Use \`useShare\` to trigger the native share sheet.

  ##### tsx

  ```tsx
  import {useState} from 'react'

  import {
    useCreateImageContent,
    useImagePicker,
    useShare,
    Button,
  } from '@shopify/shop-minis-react'

  /**
   * Create shareable content with rich link previews.
   *
   * When `LINKABLE` is included in the `visibility` array, the created content
   * includes a `shareableUrl`. This URL points to a page that renders OpenGraph
   * meta tags (`og:title`, `og:image`, `og:description`) from the content's
   * title, image, and description — so link previews in social media, messaging
   * apps, and other platforms display a rich card automatically.
   */
  export default function ShareableContent() {
    const {createImageContent, loading} = useCreateImageContent()
    const {openGallery} = useImagePicker()
    const {share} = useShare()
    const [shareableUrl, setShareableUrl] = useState<string | null>(null)

    const handleCreateAndShare = async () => {
      try {
        const file = await openGallery()

        // 1. Create the content with LINKABLE visibility.
        //    The title, image, and description you provide here will appear
        //    in OpenGraph link previews when the shareableUrl is shared.
        const result = await createImageContent({
          image: file,
          contentTitle: 'My favorite outfit this week',
          description:
            'Loving this spring look — the colors are perfect for the season!',
          visibility: ['DISCOVERABLE', 'LINKABLE'],
        })

        const url = result.data.shareableUrl

        if (!url) {
          console.warn('No shareable URL returned — is LINKABLE set?')
          return
        }

        setShareableUrl(url)

        // 2. Open the native share sheet with the shareable URL.
        //    Recipients will see a rich preview card with the content's
        //    title, image, and description rendered via OpenGraph tags.
        await share({
          title: result.data.title,
          url,
        })
      } catch (error) {
        console.error('Failed to create or share content:', error)
      }
    }

    return (
      <>
        <Button onClick={handleCreateAndShare} disabled={loading}>
          {loading ? 'Creating...' : 'Upload & Share'}
        </Button>
        {shareableUrl && (
          <p>
            Shareable link: <a href={shareableUrl}>{shareableUrl}</a>
          </p>
        )}
      </>
    )
  }
  ```

***

## Related

[ImageContentWrapper](https://shopify.dev/docs/api/shop-minis/components/primitives/imagecontentwrapper)

[useShare](https://shopify.dev/docs/api/shop-minis/hooks/util/useshare)

***
