> ## 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 digital products

> Create a digital product with the Platform API, then attach downloadable files and images before publishing

A **digital product** is sold at a creator-set price (in USD), ready for you to attach downloadable files and images. Create one with the [Create a product](/api-reference/platform/products/create-product) endpoint (`POST /open-api/v1.0/products`) by setting `type: "digital"`.

Selling printed merch from your own artwork instead? See [Create design products](/guides/create-design-products).

<Note>
  **OAuth scope:** `offer_write` for creating the product and managing its digital files. Attaching product images also needs `media_write`, since the image bytes are uploaded through the media library first.

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

## Create the product

Send `type: "digital"` with a creator-set `price` (a USD amount). Products default to hidden. Set `publishOnCreate: true` to make the product visible on the storefront as soon as it's created; otherwise it stays hidden and can only be published from the Fourthwall admin.

<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": "digital",
      "name": "My Digital Product",
      "description": "A detailed description of my digital product",
      "price": 25.00
    }'
  ```

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

  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: "digital",
      name: "My Digital Product",
      description: "A detailed description of my digital product",
      price: 25.0
    })
  });
  const product = await res.json();
  console.log(product.productId); // e.g. "<uuid>"
  ```
</CodeGroup>

```json Response theme={null}
{
  "productId": "<uuid>",
  "customizationId": null,
  "images": []
}
```

## Attach downloadable files

These are the files your buyers download after purchase. Unlike media-library images — which are shop-wide and reusable across products by `imageId` — **a digital file belongs to the one product you upload it against**. The upload URL is scoped to a `productId`, the bytes are stored under that product, and confirming links the file to it.

The flow mirrors the [media upload](/guides/create-design-products) used for design artwork — request a pre-signed URL, `PUT` the bytes to Google Cloud Storage, then confirm — but it's a product-scoped endpoint and the confirm step links the file to the product instead of registering a reusable image.

<Note>
  Digital files are size-limited per shop: **25 MB** on free plans, up to **1 GB** otherwise. The `size` you send is enforced both when the URL is issued and by the signed URL itself — sending more bytes than declared fails the upload.
</Note>

<Steps>
  <Step title="Request a pre-signed upload URL">
    Call [`POST /open-api/v1.0/products/{productId}/digital-files/upload-url`](/api-reference/platform/digital-products/request-digital-file-upload-url) with the file's metadata. The response is the same `{ uploadUrl, fileUrl }` shape as the media flow: a short-lived `uploadUrl` to `PUT` the bytes to, and the `fileUrl` you'll confirm in the last step.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -u "your_username:your_password" \
        -X POST https://api.fourthwall.com/open-api/v1.0/products/<uuid>/digital-files/upload-url \
        -H "Content-Type: application/json" \
        -d '{
          "fileName": "my-ebook.pdf",
          "contentType": "application/pdf",
          "size": 1048576
        }'
      ```

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

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

    ```json Response theme={null}
    {
      "uploadUrl": "https://storage.googleapis.com/...&X-Goog-Signature=...",
      "fileUrl": "<file>",
      "expiresAt": "2026-05-28T12:34:56Z"
    }
    ```

    `expiresAt` is when the pre-signed `uploadUrl` stops accepting PUTs; request a new one if it lapses.
  </Step>

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

    The URL is signed with the same two conditions as the media upload, and missing either fails with `403 SignatureDoesNotMatch`:

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

    <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; if it expires, request a new one.
    </Warning>

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

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

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

  <Step title="Confirm the upload">
    Link the uploaded file to the product with [`POST /open-api/v1.0/products/{productId}/digital-files`](/api-reference/platform/digital-products/confirm-digital-file-upload). Pass the `fileUrl` from step 1 and the `fileName` buyers will see. The response is the full, updated product.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -u "your_username:your_password" \
        -X POST https://api.fourthwall.com/open-api/v1.0/products/<uuid>/digital-files \
        -H "Content-Type: application/json" \
        -d '{
          "fileUrl": "<file>",
          "fileName": "my-ebook.pdf"
        }'
      ```

      ```javascript JavaScript theme={null}
      const res = await fetch(
        `https://api.fourthwall.com/open-api/v1.0/products/${productId}/digital-files`,
        {
          method: "POST",
          headers: {
            "Authorization": `Basic ${credentials}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify({ fileUrl, fileName: "my-ebook.pdf" })
        }
      );
      const product = await res.json();
      ```
    </CodeGroup>
  </Step>
</Steps>

<Tip>
  Buyers receive digital files as downloads (they're served with `Content-Disposition: attachment`). Repeat this flow once per file to attach several downloads to the same product. To remove one, call [`DELETE /open-api/v1.0/products/{productId}/digital-files`](/api-reference/platform/digital-products/remove-digital-file) with its `fileUrl`.
</Tip>

## Attach product images

Product images are the showcase thumbnails shown on the storefront — separate from the downloadable files above. They are **not** uploaded through the digital-files endpoint. Instead, upload the image through the shop-wide [media library](/api-reference/platform/media-library/request-media-upload-url) (`POST /open-api/v1.0/media/upload-url`, then `PUT` the bytes — exactly as in [Create design products](/guides/create-design-products)), then attach the resulting URL to the product by its pixel dimensions:

```bash cURL theme={null}
curl -u "your_username:your_password" \
  -X POST https://api.fourthwall.com/open-api/v1.0/products/<uuid>/images \
  -H "Content-Type: application/json" \
  -d '{
    "images": [
      { "url": "<file>", "width": 800, "height": 600 }
    ]
  }'
```

Pass the `fileUrl` you uploaded as `url`; `width` and `height` are the image's pixel dimensions.

## 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="Digital files" icon="download" href="/api-reference/platform/digital-products/request-digital-file-upload-url">
    Upload and confirm the downloadable files customers receive.
  </Card>

  <Card title="Attach product images" icon="image" href="/api-reference/platform/digital-products/attach-product-images">
    Add showcase images to a digital product.
  </Card>

  <Card title="Create design products" icon="shirt" href="/guides/create-design-products">
    Render your artwork onto tees, mugs, and more.
  </Card>
</CardGroup>
