> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fourthwall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create design products

> Create design products with the Platform API, including the image upload → register → imageId → create design flow

A **design product** runs your artwork through the design pipeline: it renders each region's image onto a product template (a tee, mug, hoodie, …) and creates a purchasable product. Create one with the [Create a product](/api-reference/platform/products/create-product) endpoint (`POST /open-api/v1.0/products`) by setting `type: "design"`.

Looking to sell downloadable files instead? See [Create digital products](/guides/create-digital-products).

<Note>
  **OAuth scope:** `offer_write` for creating products, plus `media_write` for the image upload steps below.

  API keys have full access to these endpoints. See [Authentication](/guides/authentication) for API keys or [OAuth](/guides/oauth) for multi-shop apps.
</Note>

A design product is built from one or more **regions** (for example `front` and `back`), each rendered from an image you supply. You don't pass an image URL directly — instead you upload the image, **register** it in your media library to obtain an `imageId`, and reference that id per region when you create the product.

```mermaid theme={null}
flowchart LR
    A[Request upload URL] --> B[Upload image bytes]
    B --> C[Register image → imageId]
    C --> D[Create product with regions imageId]
```

<Steps>
  <Step title="Pick a product template">
    Each design product renders onto a product template (the blank tee, mug, etc.). List the available templates with [`GET /open-api/v1.0/product-templates`](/api-reference/platform/product-templates/list-product-templates) and note the `id` you want — you'll pass it as `productTemplateId`.
  </Step>

  <Step title="Request a pre-signed upload URL">
    Call [`POST /open-api/v1.0/media/upload-url`](/api-reference/platform/media-library/request-media-upload-url) with the file's metadata. The response contains a short-lived `uploadUrl` to `PUT` the bytes to, and the `fileUrl` you'll register in the next step.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -u "your_username:your_password" \
        -X POST https://api.fourthwall.com/open-api/v1.0/media/upload-url \
        -H "Content-Type: application/json" \
        -d '{
          "fileName": "my-design.png",
          "contentType": "image/png",
          "size": 482190
        }'
      ```

      ```javascript JavaScript theme={null}
      const credentials = btoa("your_username:your_password");

      const res = await fetch("https://api.fourthwall.com/open-api/v1.0/media/upload-url", {
        method: "POST",
        headers: {
          "Authorization": `Basic ${credentials}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          fileName: "my-design.png",
          contentType: "image/png",
          size: 482190
        })
      });
      const { uploadUrl, fileUrl } = await res.json();
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "uploadUrl": "https://storage.googleapis.com/...&X-Goog-Signature=...",
      "fileUrl": "https://cdn.fourthwall.com/media/.../my-design.png"
    }
    ```
  </Step>

  <Step title="Upload the image bytes">
    `PUT` the raw image bytes to the `uploadUrl`. This request goes **directly to Google Cloud Storage**, not to Fourthwall — do **not** send your Fourthwall credentials with it.

    The URL is signed with two conditions you **must** match exactly, or GCS rejects the upload with `403 SignatureDoesNotMatch`:

    * **`Content-Type`** — must equal the `contentType` you sent in step 2 (here, `image/png`).
    * **`x-goog-content-length-range: 0,<size>`** — must use the same `size` you sent in step 2 (here, `482190`).

    <Warning>
      The `x-goog-content-length-range` header is required and easy to miss — it's baked into the URL's signature. Omitting it (or sending a different size) fails with `403 SignatureDoesNotMatch` even when everything else looks right. The `uploadUrl` is short-lived (\~6 hours); if it expires, request a new one.
    </Warning>

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X PUT "<uploadUrl from previous step>" \
        -H "Content-Type: image/png" \
        -H "x-goog-content-length-range: 0,482190" \
        --data-binary @my-design.png
      ```

      ```javascript JavaScript theme={null}
      const fileBytes = await fs.promises.readFile("my-design.png");

      await fetch(uploadUrl, {
        method: "PUT",
        headers: {
          "Content-Type": "image/png",
          // Required: same size you sent to /media/upload-url. Baked into the
          // signature — omitting it fails with 403 SignatureDoesNotMatch.
          "x-goog-content-length-range": "0,482190"
        },
        body: fileBytes
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Register the image to get an imageId">
    Now persist the uploaded image in your media library with [`POST /open-api/v1.0/media/images`](/api-reference/platform/media-library/save-media-image). Pass the `fileUrl` from step 2 along with the image's pixel dimensions. The response's `id` is the **`imageId`** you'll reference per region.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -u "your_username:your_password" \
        -X POST https://api.fourthwall.com/open-api/v1.0/media/images \
        -H "Content-Type: application/json" \
        -d '{
          "fileUrl": "https://cdn.fourthwall.com/media/.../my-design.png",
          "width": 2400,
          "height": 2400
        }'
      ```

      ```javascript JavaScript theme={null}
      const res = await fetch("https://api.fourthwall.com/open-api/v1.0/media/images", {
        method: "POST",
        headers: {
          "Authorization": `Basic ${credentials}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ fileUrl, width: 2400, height: 2400 })
      });
      const image = await res.json();
      console.log(image.id); // e.g. "img_k66ZW4fsRm6c2def3itltA" — your imageId
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "id": "img_k66ZW4fsRm6c2def3itltA",
      "uri": "https://cdn.fourthwall.com/media/.../my-design.png",
      "width": 2400,
      "height": 2400,
      "thumbnail": "https://cdn.fourthwall.com/...",
      "preview": "https://cdn.fourthwall.com/..."
    }
    ```

    <Tip>
      Register an image once and reuse its `imageId` across multiple regions or multiple products — there's no need to re-upload the same artwork.
    </Tip>
  </Step>

  <Step title="Create the design product">
    Finally, call [`POST /open-api/v1.0/products`](/api-reference/platform/products/create-product) with `type: "design"`. Each entry in `regions` pairs a product `region` with the `imageId` you just registered.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -u "your_username:your_password" \
        -X POST https://api.fourthwall.com/open-api/v1.0/products \
        -H "Content-Type: application/json" \
        -d '{
          "type": "design",
          "productTemplateId": "pro_k66ZW4fsRm6c2def3itltA",
          "name": "My Awesome Design Tee",
          "description": "Limited edition design",
          "regions": [
            { "region": "front", "imageId": "img_k66ZW4fsRm6c2def3itltA", "placementStrategy": "AUTO" }
          ],
          "publishOnCreate": false
        }'
      ```

      ```javascript JavaScript theme={null}
      const res = await fetch("https://api.fourthwall.com/open-api/v1.0/products", {
        method: "POST",
        headers: {
          "Authorization": `Basic ${credentials}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          type: "design",
          productTemplateId: "pro_k66ZW4fsRm6c2def3itltA",
          name: "My Awesome Design Tee",
          description: "Limited edition design",
          regions: [
            { region: "front", imageId: image.id, placementStrategy: "AUTO" }
          ],
          publishOnCreate: false
        })
      });
      const product = await res.json();
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "productId": "off_a1b2c3d4e5f6",
      "customizationId": "cus_a1b2c3d4e5f6",
      "images": [
        {
          "url": "https://cdn.fourthwall.com/.../front-black.png",
          "width": 1200,
          "height": 1200,
          "style": "Unisex Tee",
          "color": "Black",
          "size": null,
          "region": "front"
        }
      ]
    }
    ```
  </Step>
</Steps>

<Warning>
  Pass the registered image's **`id`** (the `imageId`) in `regions[].imageId` — not the `uploadUrl` or `fileUrl`. An `imageId` that isn't a registered media-library image is rejected with a validation error; register it via `POST /open-api/v1.0/media/images` first.
</Warning>

### Placement strategies

By default each region uses `placementStrategy: "AUTO"`, which lets the renderer apply the product's automation defaults. Set it explicitly to control how the image is placed:

| Strategy       | Behavior                                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `AUTO`         | Let the renderer decide using the product's defaults (preferred placement, or fill-all for items like mugs and stickers). |
| `FILL_ALL`     | Apply the image to every placement in the region.                                                                         |
| `FULL_REGION`  | Render the image across the full region, skipping the preferred placement.                                                |
| `PLACEMENT_ID` | Target a single placement named by `placementId` (required for this strategy).                                            |

You can also limit which `colors` and `sizes` are rendered, and set a `profitMargin` (a USD amount, e.g. `10.00`) on top of the base cost. Products are created hidden unless you set `publishOnCreate: true`.

## Next steps

<CardGroup cols={2}>
  <Card title="Create a product reference" icon="code" href="/api-reference/platform/products/create-product">
    Full request and response schema for `POST /products`.
  </Card>

  <Card title="Media library" icon="image" href="/api-reference/platform/media-library/save-media-image">
    Upload and register images to reference by `imageId`.
  </Card>

  <Card title="Product templates" icon="shirt" href="/api-reference/platform/product-templates/list-product-templates">
    Browse templates to find a `productTemplateId`.
  </Card>

  <Card title="Create digital products" icon="download" href="/guides/create-digital-products">
    Sell downloadable files with a creator-set price.
  </Card>
</CardGroup>
