Skip to main content
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 endpoint (POST /open-api/v1.0/products) by setting type: "design". Looking to sell downloadable files instead? See Create digital products.
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 for API keys or OAuth for multi-shop apps.
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.
1

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 and note the id you want — you’ll pass it as productTemplateId.
2

Request a pre-signed upload URL

Call POST /open-api/v1.0/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.
size is the file’s exact length in bytes — read it from the file rather than hardcoding it, and hold on to the value: you must send the same number again in the x-goog-content-length-range header when you PUT the bytes (step 3). Compute it from whatever you’re uploading:
  • Node (Buffer): fileBytes.byteLength (or (await fs.promises.stat("my-design.png")).size without reading the file)
  • Browser (File/Blob): file.size
  • Shell: wc -c < my-design.png
# size must be the file's exact byte count — derive it, don't guess
SIZE=$(wc -c < my-design.png)

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\": $SIZE
  }"
const credentials = btoa("your_username:your_password");

const fileBytes = await fs.promises.readFile("my-design.png");
const size = fileBytes.byteLength; // exact byte count — reused in the PUT below

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
  })
});
const { uploadUrl, fileUrl } = await res.json();
Response
{
  "uploadUrl": "https://storage.googleapis.com/...&X-Goog-Signature=...",
  "fileUrl": "https://cdn.fourthwall.com/media/.../my-design.png"
}
3

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 (byte count) you sent in step 2. Reuse the exact value; don’t recompute it a different way.
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.
# $SIZE is the same value you sent to /media/upload-url in step 2
curl -X PUT "<uploadUrl from previous step>" \
  -H "Content-Type: image/png" \
  -H "x-goog-content-length-range: 0,$SIZE" \
  --data-binary @my-design.png
// Reuse fileBytes and size from step 2 — same bytes, same byte count.
await fetch(uploadUrl, {
  method: "PUT",
  headers: {
    "Content-Type": "image/png",
    // Required: the same `size` you sent to /media/upload-url. Baked into
    // the signature — a different value fails with 403 SignatureDoesNotMatch.
    "x-goog-content-length-range": `0,${size}`
  },
  body: fileBytes
});
4

Register the image to get an imageId

Now persist the uploaded image in your media library with POST /open-api/v1.0/media/images. 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.
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
  }'
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
Response
{
  "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/..."
}
Register an image once and reuse its imageId across multiple regions or multiple products — there’s no need to re-upload the same artwork.
5

Create the design product

Finally, call POST /open-api/v1.0/products with type: "design". Each entry in regions pairs a product region with the imageId you just registered.
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
  }'
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();
Response
{
  "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"
    }
  ]
}
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.

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:
StrategyBehavior
AUTOLet the renderer decide using the product’s defaults (preferred placement, or fill-all for items like mugs and stickers).
FILL_ALLApply the image to every placement in the region.
FULL_REGIONRender the image across the full region, skipping the preferred placement.
PLACEMENT_IDTarget 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

Create a product reference

Full request and response schema for POST /products.

Media library

Upload and register images to reference by imageId.

Product templates

Browse templates to find a productTemplateId.

Create digital products

Sell downloadable files with a creator-set price.