Reconciler Products, Variants and Media
Thumbnail marked generated but the physical file is missing
Somewhere on your storefront a product image loads fine, but the thumbnail it should shrink down to comes back as a 404. Shopware's own records say the thumbnail exists. The size is right, the width and height are in the database, everything about the row looks done. But the file itself was never actually written to disk or to S3, and because Shopware only checks whether the record exists, not whether the file exists, it never tries to fix it again on its own. Here is why it happens and a small script that finds the orphaned records and repairs them.
When Shopware generates thumbnails, whether through ThumbnailService::updateThumbnails, the media.thumbnail.update message handler, the media:generate-thumbnails console command, or the admin's Generate thumbnails action, it writes a media_thumbnail row for every configured size up front and treats the media as done once those rows exist. If the resize or upload step fails partway, the row and its width, height, and path stay in the database while the file itself never gets written to the configured filesystem. Because the default regeneration logic only asks does a record exist, not does the file exist, the broken thumbnail is skipped on every future run and 404s on the storefront indefinitely. Run a small Python or Node.js script that lists media-thumbnail rows, checks whether each expected file actually exists, and deletes the orphaned records so Shopware's own generator treats them as missing and rebuilds them. Full code, tests, and sources are below.
The problem in plain words
When Shopware needs a thumbnail for a media file, it does the work in two separate steps: it inserts a database row describing the thumbnail (its width, height, and the path it expects the file to live at), and then it actually resizes the source image and writes the result to the configured filesystem, whether that is local disk or a remote adapter like S3. Those two steps are supposed to happen together, but they are not atomic.
If the second step fails, a storage timeout, a full disk, an S3 permission error, an interrupted async message, or even a thumbnail-processing plugin being uninstalled mid-run, the first step already committed. The media_thumbnail row is sitting in the database claiming a file exists at a specific path, and no file is there. Shopware has no built-in trigger that goes back and double-checks that claim.
The result shows up quietly. A customer or a crawler requests the thumbnail URL and gets a 404. Nobody sees an error in the admin, because as far as Shopware is concerned, the thumbnail was already generated. The next time someone runs thumbnail generation again, hoping it will fix itself, it does not, because the row it would have needed to see as missing is right there in the table.
Why it happens
This is a gap between what a database row claims and what actually exists on the filesystem. A few things make it easy to hit:
- The write to storage is not wrapped in the same transaction as the database insert. A timeout, a disk-full condition, or an S3 permission error can interrupt the file write after the row already committed.
- Thumbnail generation for large catalogs often runs through the async message queue, and an interrupted or failed
media.thumbnail.updatemessage can leave a batch of rows half-finished with no automatic retry that re-checks the file. - Uninstalling or disabling a plugin that processes thumbnails mid-run can leave records behind for sizes that plugin was supposed to generate.
- By default, non-strict regeneration in
media:generate-thumbnailsonly asks whether amedia_thumbnailrecord already exists for a given size, not whether the file behind it exists, so it silently skips every media this has already happened to.
This exact gap is documented in Shopware's own issue tracker and was significant enough that the core team shipped a fix for it. See the citations at the end for the exact threads and the pull request that added an opt-in strict mode.
Shopware's fix for this, NEXT-7754, added a --strict flag to media:generate-thumbnails that performs the missing file_exists check before deciding a thumbnail can be skipped. That flag is opt-in on purpose, because checking every file on every run is slower than trusting the database. The Admin API does not expose that flag at all, so the equivalent fix from outside the server is to find the orphaned rows yourself, delete them, and then ask Shopware to regenerate just those media. Once the stale row is gone, Shopware's own generator behaves correctly again.
The fix, as a flow
We do not guess which thumbnails are broken. The script lists media-thumbnail rows with their parent media, resolves the expected file URL for each one, and checks whether that file actually exists. Only rows where the folder configuration expects thumbnails and the file is confirmed missing get deleted, and only the affected media get queued for regeneration.
Build it step by step
Get Admin API credentials
Create an Integration in your Shopware Administration under Settings, System, Integrations. Copy the access key id and secret access key. The script exchanges these for a bearer token at POST /api/oauth/token with a client_credentials grant, so keep them in environment variables, never in the file.
pip install requests
export SHOPWARE_URL="https://your-shop.example.com"
export SHOPWARE_CLIENT_ID="SWIAxxx"
export SHOPWARE_CLIENT_SECRET="your secret access key"
export DRY_RUN="true" # start safe, change to false to write
// Node 18+ has fetch built in, no dependencies needed
export SHOPWARE_URL="https://your-shop.example.com"
export SHOPWARE_CLIENT_ID="SWIAxxx"
export SHOPWARE_CLIENT_SECRET="your secret access key"
export DRY_RUN="true" // start safe, change to false to write
Authenticate and talk to the Admin API
A small helper trades the client id and secret for an access token, then sends it as Authorization: Bearer <token> on every call. We reuse this helper for the searches, the delete, and the regenerate call.
import os, requests
SHOPWARE_URL = os.environ["SHOPWARE_URL"].rstrip("/")
CLIENT_ID = os.environ["SHOPWARE_CLIENT_ID"]
CLIENT_SECRET = os.environ["SHOPWARE_CLIENT_SECRET"]
def get_token():
r = requests.post(
f"{SHOPWARE_URL}/api/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
headers={"Accept": "application/json"},
timeout=30,
)
r.raise_for_status()
return r.json()["access_token"]
def api(method, path, token, json_body=None):
r = requests.request(
method,
f"{SHOPWARE_URL}{path}",
json=json_body,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
},
timeout=30,
)
r.raise_for_status()
return r.json() if r.text else {}
const SHOPWARE_URL = (process.env.SHOPWARE_URL || "").replace(/\/$/, "");
const CLIENT_ID = process.env.SHOPWARE_CLIENT_ID;
const CLIENT_SECRET = process.env.SHOPWARE_CLIENT_SECRET;
async function getToken() {
const res = await fetch(`${SHOPWARE_URL}/api/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Shopware auth ${res.status}`);
const body = await res.json();
return body.access_token;
}
async function api(method, path, token, jsonBody) {
const res = await fetch(`${SHOPWARE_URL}${path}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: jsonBody ? JSON.stringify(jsonBody) : undefined,
});
if (!res.ok) throw new Error(`Shopware ${res.status}`);
const text = await res.text();
return text ? JSON.parse(text) : {};
}
List thumbnail records with their parent media
Search media-thumbnail with the media association included, so each hit carries its mediaId, width, height, and the parent media's path and folder. We page through with page and limit so a large media library is handled safely.
def find_thumbnails(token, page=1, limit=500):
body = {
"page": page,
"limit": limit,
"filter": [],
"associations": {"media": {}},
"sort": [{"field": "createdAt", "order": "ASC"}],
"total-count-mode": 1,
}
return api("POST", "/api/search/media-thumbnail", token, body)
async function findThumbnails(token, page = 1, limit = 500) {
const body = {
page,
limit,
filter: [],
associations: { media: {} },
sort: [{ field: "createdAt", order: "ASC" }],
"total-count-mode": 1,
};
return api("POST", "/api/search/media-thumbnail", token, body);
}
Confirm the physical file is actually missing
For each thumbnail, resolve the expected URL, either from the thumbnail's own computed url when you request the media with associations: {"thumbnails": {}}, or by reconstructing the public path from media.path. A plain HEAD request against the public filesystem URL tells you whether the file exists without downloading it. A 404 while the record still exists is the defect signature.
def thumbnail_file_exists(url):
try:
r = requests.head(url, timeout=15, allow_redirects=True)
return r.status_code < 400
except requests.RequestException:
return False
async function thumbnailFileExists(url) {
try {
const res = await fetch(url, { method: "HEAD", redirect: "follow" });
return res.status < 400;
} catch {
return false;
}
}
Decide, with one pure function
Keep the decision out of the network code entirely. This function takes a thumbnail, whether its file exists, and the folder's configuration, and returns one of three plain outcomes: skip when thumbnails are disabled for the folder or the file is present, or delete and requeue when the file is confirmed missing on a folder that expects thumbnails. There is no I/O inside it, so it is trivially testable against a matrix of inputs.
def decide_thumbnail_repair(thumbnail, file_exists, folder_config):
if not folder_config.get("createThumbnails"):
return {"action": "skip", "reason": "thumbnails disabled for folder"}
if file_exists:
return {"action": "skip", "reason": "file present, record consistent"}
return {
"action": "delete_and_requeue",
"reason": "thumbnail record exists but physical file missing",
}
export function decideThumbnailRepair(thumbnail, fileExists, folderConfig) {
if (!folderConfig.createThumbnails) {
return { action: "skip", reason: "thumbnails disabled for folder" };
}
if (fileExists) {
return { action: "skip", reason: "file present, record consistent" };
}
return {
action: "delete_and_requeue",
reason: "thumbnail record exists but physical file missing",
};
}
Delete the orphaned record and re-trigger generation, guarded by dry run
When the decision is delete_and_requeue and DRY_RUN is off, call DELETE /api/media-thumbnail/{thumbnailId} so Shopware's own generator no longer thinks that size is already handled. Once every affected thumbnail for a media is cleared, call POST /api/_action/media/generate-thumbnails with {"mediaIds": [...]} to force regeneration for just those media. The Admin API has no equivalent of the CLI's --strict flag, so this delete-then-regenerate pair is the required substitute. Where you can, also schedule bin/console media:generate-thumbnails --strict outside the Admin API as the authoritative bulk fix.
Always start with DRY_RUN=true. Confirm the file truly is missing, not just slow to respond, before deleting anything, and only delete rows on folders where mediaFolderConfiguration.createThumbnails is still true. If write access is not appropriate for your situation, fall back to flag mode: log the list of {mediaId, thumbnailId, expectedPath} for a human to review instead of deleting.
The full code
Here is the complete script in one file for each language. It reads settings from the environment, logs what it finds, respects the dry run flag, and is safe to run again and again because it only deletes thumbnail records confirmed to have a missing file, then re-triggers generation for the affected media.
View this code on GitHub Full runnable folder with tests in the shopware-fixes repo.
"""Find Shopware 6 media_thumbnail rows whose physical file is missing and repair them.
ThumbnailService writes a media_thumbnail row for every configured size before the
resize and upload step runs, and Shopware treats the media as done once those rows
exist. If the write to the configured filesystem (local or S3) fails partway, the row
survives while the file never lands. The default regeneration logic only checks
whether a record exists, not whether the file exists, so the broken thumbnail is
skipped forever and 404s on the storefront. This script finds the orphaned records,
confirms the file is missing, deletes the stale row, and re-triggers generation for
the affected media. Run on a schedule. Safe to run again and again.
"""
import os
import logging
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("repair_missing_thumbnail_files")
SHOPWARE_URL = os.environ["SHOPWARE_URL"].rstrip("/")
CLIENT_ID = os.environ["SHOPWARE_CLIENT_ID"]
CLIENT_SECRET = os.environ["SHOPWARE_CLIENT_SECRET"]
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
def get_token():
r = requests.post(
f"{SHOPWARE_URL}/api/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
headers={"Accept": "application/json"},
timeout=30,
)
r.raise_for_status()
return r.json()["access_token"]
def api(method, path, token, json_body=None):
r = requests.request(
method,
f"{SHOPWARE_URL}{path}",
json=json_body,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
},
timeout=30,
)
r.raise_for_status()
return r.json() if r.text else {}
def find_thumbnails(token, page=1, limit=500):
body = {
"page": page,
"limit": limit,
"filter": [],
"associations": {"media": {}},
"sort": [{"field": "createdAt", "order": "ASC"}],
"total-count-mode": 1,
}
return api("POST", "/api/search/media-thumbnail", token, body)
def thumbnail_file_exists(url):
try:
r = requests.head(url, timeout=15, allow_redirects=True)
return r.status_code < 400
except requests.RequestException:
return False
def decide_thumbnail_repair(thumbnail, file_exists, folder_config):
if not folder_config.get("createThumbnails"):
return {"action": "skip", "reason": "thumbnails disabled for folder"}
if file_exists:
return {"action": "skip", "reason": "file present, record consistent"}
return {
"action": "delete_and_requeue",
"reason": "thumbnail record exists but physical file missing",
}
def delete_thumbnail(token, thumbnail_id):
api("DELETE", f"/api/media-thumbnail/{thumbnail_id}", token)
def regenerate_thumbnails(token, media_ids):
if not media_ids:
return
api(
"POST",
"/api/_action/media/generate-thumbnails",
token,
{"mediaIds": list(media_ids)},
)
def run():
token = get_token()
repaired = 0
affected_media = set()
page = 1
while True:
data = find_thumbnails(token, page=page)
rows = data.get("data", [])
if not rows:
break
for row in rows:
media = row.get("media") or {}
folder = (media.get("mediaFolder") or {}) if isinstance(media.get("mediaFolder"), dict) else {}
folder_config = folder.get("configuration") or {"createThumbnails": True}
url = row.get("url") or row.get("path")
if not url:
continue
file_exists = thumbnail_file_exists(url)
decision = decide_thumbnail_repair(row, file_exists, folder_config)
if decision["action"] == "skip":
continue
log.warning(
"Thumbnail %s (media %s) has no physical file. %s",
row["id"], row.get("mediaId"),
"would delete and requeue" if DRY_RUN else "deleting and requeuing",
)
if not DRY_RUN:
delete_thumbnail(token, row["id"])
affected_media.add(row.get("mediaId"))
repaired += 1
if page * 500 >= data.get("total", 0):
break
page += 1
if not DRY_RUN:
regenerate_thumbnails(token, affected_media)
log.info(
"Done. %d thumbnail record(s) %s across %d media.",
repaired, "to repair" if DRY_RUN else "repaired", len(affected_media),
)
if __name__ == "__main__":
run()
/**
* Find Shopware 6 media_thumbnail rows whose physical file is missing and repair them.
*
* ThumbnailService writes a media_thumbnail row for every configured size before the
* resize and upload step runs, and Shopware treats the media as done once those rows
* exist. If the write to the configured filesystem (local or S3) fails partway, the row
* survives while the file never lands. The default regeneration logic only checks
* whether a record exists, not whether the file exists, so the broken thumbnail is
* skipped forever and 404s on the storefront. This script finds the orphaned records,
* confirms the file is missing, deletes the stale row, and re-triggers generation for
* the affected media. Run on a schedule. Safe to run again and again.
*/
import { pathToFileURL } from "node:url";
const SHOPWARE_URL = (process.env.SHOPWARE_URL || "https://demo.example.com").replace(/\/$/, "");
const CLIENT_ID = process.env.SHOPWARE_CLIENT_ID || "SWIAxxx";
const CLIENT_SECRET = process.env.SHOPWARE_CLIENT_SECRET || "secret_dummy";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
export function decideThumbnailRepair(thumbnail, fileExists, folderConfig) {
if (!folderConfig.createThumbnails) {
return { action: "skip", reason: "thumbnails disabled for folder" };
}
if (fileExists) {
return { action: "skip", reason: "file present, record consistent" };
}
return {
action: "delete_and_requeue",
reason: "thumbnail record exists but physical file missing",
};
}
async function getToken() {
const res = await fetch(`${SHOPWARE_URL}/api/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Shopware auth ${res.status}`);
const body = await res.json();
return body.access_token;
}
async function api(method, path, token, jsonBody) {
const res = await fetch(`${SHOPWARE_URL}${path}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: jsonBody ? JSON.stringify(jsonBody) : undefined,
});
if (!res.ok) throw new Error(`Shopware ${res.status}`);
const text = await res.text();
return text ? JSON.parse(text) : {};
}
async function findThumbnails(token, page = 1, limit = 500) {
const body = {
page,
limit,
filter: [],
associations: { media: {} },
sort: [{ field: "createdAt", order: "ASC" }],
"total-count-mode": 1,
};
return api("POST", "/api/search/media-thumbnail", token, body);
}
async function thumbnailFileExists(url) {
try {
const res = await fetch(url, { method: "HEAD", redirect: "follow" });
return res.status < 400;
} catch {
return false;
}
}
async function deleteThumbnail(token, thumbnailId) {
await api("DELETE", `/api/media-thumbnail/${thumbnailId}`, token);
}
async function regenerateThumbnails(token, mediaIds) {
if (mediaIds.length === 0) return;
await api("POST", "/api/_action/media/generate-thumbnails", token, {
mediaIds,
});
}
export async function run() {
const token = await getToken();
let repaired = 0;
const affectedMedia = new Set();
let page = 1;
while (true) {
const data = await findThumbnails(token, page);
const rows = data.data || [];
if (rows.length === 0) break;
for (const row of rows) {
const media = row.media || {};
const folder = media.mediaFolder || {};
const folderConfig = folder.configuration || { createThumbnails: true };
const url = row.url || row.path;
if (!url) continue;
const fileExists = await thumbnailFileExists(url);
const decision = decideThumbnailRepair(row, fileExists, folderConfig);
if (decision.action === "skip") continue;
console.warn(
`Thumbnail ${row.id} (media ${row.mediaId}) has no physical file. ${DRY_RUN ? "would delete and requeue" : "deleting and requeuing"}`
);
if (!DRY_RUN) await deleteThumbnail(token, row.id);
affectedMedia.add(row.mediaId);
repaired++;
}
if (page * 500 >= (data.total || 0)) break;
page++;
}
if (!DRY_RUN) await regenerateThumbnails(token, [...affectedMedia]);
console.log(
`Done. ${repaired} thumbnail record(s) ${DRY_RUN ? "to repair" : "repaired"} across ${affectedMedia.size} media.`
);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((err) => { console.error(err); process.exit(1); });
}
Add a test
The decision rule is the part most worth testing, because it decides whether a database row gets deleted. Because decide_thumbnail_repair is pure, with fileExists and folderConfig precomputed and passed in, no network and no Shopware instance is needed. It just takes plain objects in and checks the answer.
from repair_missing_thumbnail_files import decide_thumbnail_repair
def thumbnail(**over):
base = {
"id": "thumb-1",
"mediaId": "media-1",
"width": 400,
"height": 400,
"expectedPath": "media/thumbnail/product_400x400.jpg",
}
base.update(over)
return base
def test_skip_when_folder_disables_thumbnails():
result = decide_thumbnail_repair(thumbnail(), False, {"createThumbnails": False})
assert result == {"action": "skip", "reason": "thumbnails disabled for folder"}
def test_skip_when_file_present():
result = decide_thumbnail_repair(thumbnail(), True, {"createThumbnails": True})
assert result == {"action": "skip", "reason": "file present, record consistent"}
def test_delete_and_requeue_when_file_missing_and_folder_enabled():
result = decide_thumbnail_repair(thumbnail(), False, {"createThumbnails": True})
assert result == {
"action": "delete_and_requeue",
"reason": "thumbnail record exists but physical file missing",
}
def test_disabled_folder_wins_even_if_file_is_also_missing():
result = decide_thumbnail_repair(thumbnail(), False, {"createThumbnails": False})
assert result["action"] == "skip"
import { test } from "node:test";
import assert from "node:assert/strict";
import { decideThumbnailRepair } from "./repair-missing-thumbnail-files.js";
const thumbnail = (over = {}) => ({
id: "thumb-1",
mediaId: "media-1",
width: 400,
height: 400,
expectedPath: "media/thumbnail/product_400x400.jpg",
...over,
});
test("skip when folder disables thumbnails", () => {
const result = decideThumbnailRepair(thumbnail(), false, { createThumbnails: false });
assert.deepEqual(result, { action: "skip", reason: "thumbnails disabled for folder" });
});
test("skip when file present", () => {
const result = decideThumbnailRepair(thumbnail(), true, { createThumbnails: true });
assert.deepEqual(result, { action: "skip", reason: "file present, record consistent" });
});
test("delete and requeue when file missing and folder enabled", () => {
const result = decideThumbnailRepair(thumbnail(), false, { createThumbnails: true });
assert.deepEqual(result, {
action: "delete_and_requeue",
reason: "thumbnail record exists but physical file missing",
});
});
test("disabled folder wins even if file is also missing", () => {
const result = decideThumbnailRepair(thumbnail(), false, { createThumbnails: false });
assert.equal(result.action, "skip");
});
Case studies
A bucket policy change quietly broke thumbnail uploads for a week
A furniture retailer moved its media storage to a new S3 bucket and updated the IAM policy, but missed one write permission for a specific prefix. Thumbnail generation kept running as scheduled, database rows kept getting created, and nobody noticed anything was wrong until a marketing email went out with broken product thumbnails.
Running the script in dry run against the product folder found several hundred thumbnail records where the HEAD request came back 404. Once the bucket policy was fixed, deleting those orphaned rows and re-triggering generation rebuilt every missing file in one pass, and the next campaign email rendered cleanly.
A restart mid-batch left a scattering of half-generated thumbnails
A home goods store ran a large catalog import that queued thousands of media.thumbnail.update messages. A deploy restarted the message workers partway through, and a small percentage of thumbnails ended up with a database row but no file, scattered unpredictably across the catalog.
Because the failure was silent, nobody could tell which products were affected without checking the storefront by hand. The script found the exact list by checking every thumbnail's URL directly, and only 42 of the several thousand generated needed cleanup, letting the team regenerate just that small set instead of the whole catalog.
After this runs, a thumbnail record no longer gets to lie about a file that was never written. Storage failures during generation stop turning into permanent, silent 404s that regeneration would otherwise skip forever, and every repair only touches records confirmed to be orphaned, never a media entity, a product, or a price. Where possible, pair this with a scheduled media:generate-thumbnails --strict run for the authoritative bulk fix.
FAQ
Why does Shopware 6 think a thumbnail exists when the file returns a 404?
ThumbnailService writes a media_thumbnail row for every configured size as soon as generation starts, and Shopware treats the media as done once those records exist. If the resize or upload step fails partway, such as a storage timeout, a full disk, an S3 permission error, or an interrupted async message, the database row survives but the image file never lands on the configured filesystem. The default regeneration logic only checks whether a record exists, not whether the file exists, so the broken thumbnail is skipped forever and keeps 404ing on the storefront.
Is it safe to delete a media_thumbnail row automatically?
Yes, once you have confirmed the physical file truly does not exist at the expected path and the media's folder configuration still has createThumbnails enabled. Deleting the row does not touch the parent media entity, its price, or its product association, it only removes a stale record so Shopware's own generator will treat that size as missing and rebuild it on the next generate-thumbnails call.
How do I find Shopware 6 thumbnails that are missing their physical file using the Admin API?
Authenticate with POST /api/oauth/token using client_credentials, then POST /api/search/media-thumbnail with the media association included to list every thumbnail record and its parent media. For each media, request GET /api/media/{mediaId} with associations.thumbnails to read each thumbnail's resolved url, then check whether that URL or file actually exists. A thumbnail row with no matching file, on a folder where mediaFolderConfiguration.createThumbnails is true, is the defect signature.
Related field notes
Citations
On the problem:
- Shopware GitHub Issues: Regenerating Thumbnails not working as expected. github.com/shopware/shopware/issues/6134
- Shopware GitHub Pull Request: NEXT-7754, fix thumbnail generation when the physical file is missing, adding the --strict flag. github.com/shopware/shopware/pull/1937
- Shopware GitHub Issues: Thumbnails generate command fail. github.com/shopware/shopware/issues/383
On the solution:
- Shopware Developer Documentation: Working with Media and Thumbnails. developer.shopware.com use-media-thumbnails
- Shopware Admin API Reference: the MediaThumbnail entity. shopware.stoplight.io admin-api media-thumbnail
- Shopware Admin API Reference: Media Management. shopware.stoplight.io admin-api media-management
Stuck on a tricky one?
If you have a problem in Shopware order states, stock, message queue, or data integrity that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this clear a broken thumbnail?
If this saved you a confusing support ticket or a broken product image, you can buy me a coffee. It is the best way to keep these field notes free and growing.
Buy me a coffee on Ko-fi