CMS and websites
Product images in Magento 2 through the REST API
Upload a master with POST /rest/V1/products/<sku>/media, generate every size with SocialCutter and attach them as mediaGalleryEntries.
- Magento 2
- REST API
- Bearer token
- media_gallery_entries
- base64
- product images
- Python
Why pre-generate the sizes before uploading
A Magento product page shows up in the category grid, the product page, the search results, the cart and the related-products widget. The theme applies its own ratios and crops the master on the fly. That is fine until you need one exact size.
The flow is: one master goes in, SocialCutter returns every size, and Magento gets the right one in each slot. The cover crop (the default mode) is centred: it scales and trims the excess evenly on both sides. There is no subject detection, so leave some air around the edges of the master.
| Where it goes in the store | SocialCutter destination | Size |
|---|---|---|
| Main image | instagram post | 1080x1080 (1:1) |
| Portrait product image | instagram story | 1080x1920 (9:16) |
| Category banner | facebook post | 1200x630 (1.91:1) |
| CMS header | twitter header | 1500x500 (3:1) |
| Product video thumbnail | youtube thumbnail | 1280x720 (16:9) |
The real formats and sizes come from GET /api/v1/platforms, which is public. The catalogue has no 4:5 format: square is 1:1 and portrait is 9:16.
Before you start: integration, token and permissions
Base and version. Calls go to https://your-store.com/rest/V1/..., or to https://your-store.com/rest/<store_code>/V1/... when you run multiple stores. Field names and endpoint availability differ between 2.3 and 2.4, so pin the version you run and check it against the official reference: https://developer.adobe.com/commerce/webapi/rest/
Authentication. There are two tokens and both travel as Authorization: Bearer <token>:
- Integration. In the admin, System → Extensions → Integrations. Activating it generates the Access Token, which does not expire unless you revoke it. Use this one for scheduled jobs.
- Admin.
POST /rest/V1/integration/admin/tokenwith{"username","password"}returns the token as a JSON string. It expires according to the store’s configured lifetime.
Permissions. The integration role decides which resources it may write. This flow needs access to products and to the catalogue Media Gallery (read and write). If the token does not cover those resources you get 401 Unauthorized or 403 Forbidden even with a valid token: check the role, not the key.
export MAGENTO_URL="https://your-store.com"
export MAGENTO_TOKEN="eyJraWQ..." # integration Access Token
export SC_KEY="sc_your_key"
export SKU="TEE-2026-01"
1. Generate the sizes with SocialCutter
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
-H "X-API-Key: *** \
-H "Content-Type: application/json" \
-H "Idempotency-Key: magento-$SKU" \
-d '{
"source": { "type": "url", "value": "https://your-cdn.com/master.jpg" },
"destinations": [
{ "platform": "instagram", "format": "post" },
{ "platform": "instagram", "format": "story" },
{ "platform": "twitter", "format": "header" }
],
"options": { "quality": 90, "format": "jpg" }
}' > sc.json
jq -r '.id, (.outputs[] | "\(.platform)/\(.format) \(.width)x\(.height) \(.url)")' sc.json
Every output carries url, width, height and size_bytes. For a local master use POST /api/v1/images/process/upload (multipart, file field, 5 MB max). For batches of SKUs use POST /api/v1/images/batch. The Idempotency-Key header makes retries safe.
2. Upload each file to the product (base64)
The media endpoint takes JSON, so the file travels encoded. A sku with slashes is encoded in the URL (10000/100/S → 10000%2F100%2FS).
SQUARE=$(jq -r '.outputs[0].url' sc.json)
curl -s "$SQUARE" -o square.jpg
B64=$(base64 -w0 square.jpg)
curl -s -X POST "$MAGENTO_URL/rest/V1/products/$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote_plus(sys.argv[1]))" "$SKU")/media" \
-H "Authorization: Bearer $MAGENTO_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"entry\":{
\"media_type\":\"image\",
\"label\":\"T-shirt, front view\",
\"position\":1,
\"disabled\":false,
\"types\":[\"image\",\"small_image\",\"thumbnail\"],
\"file\":\"tee-front.jpg\",
\"content\":{
\"base64_encoded_data\":\"$B64\",
\"type\":\"image/jpeg\",
\"name\":\"tee-front.jpg\"
}}}"
The response returns the file (relative path inside pub/media/catalog/product) and the entry id. Tag image, small_image and thumbnail on one image only: that is the one Magento uses as the main one. Base64 grows the file by 33 %; if your PHP post_max_size is small, upload only the outputs you need or shrink them first.
3. Rebuild the gallery with media_gallery_entries
To order the images and fix the main one, PUT the product with media_gallery_entries. It is a full replacement: any entry you leave out disappears. Read the existing ones first (GET /rest/V1/products/<sku>/media) and send them all back.
jq -n --arg f "tee-front.jpg" '{product:{media_gallery_entries:[
{ id: 42, media_type:"image", label:"T-shirt, front view",
position:1, disabled:false, types:["image","small_image","thumbnail"], file:$f }
]}}' > payload.json
curl -s -X PUT "$MAGENTO_URL/rest/V1/products/$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote_plus(sys.argv[1]))" "$SKU")" \
-H "Authorization: Bearer $MAGENTO_TOKEN" \
-H "Content-Type: application/json" \
-d @payload.json | jq '.media_gallery_entries[] | {id, position, types, file}'
REST reference (endpoints and structures — check the version you run): https://developer.adobe.com/commerce/webapi/rest/
4. The same flow in Python
import base64, json, urllib.parse, requests
SC = "https://api.socialcutter.theboomer.dev/api/v1/images/process"
API = "https://your-store.com/rest/V1"
SKU = "TEE-2026-01"
HDR = {"Authorization": "Bearer eyJraWQ...", "Content-Type": "application/json"}
outputs = requests.post(
SC,
headers={"X-API-Key": "sc_...", "Content-Type": "application/json"},
json={"source": {"type": "url", "value": "https://your-cdn.com/master.jpg"},
"destinations": [{"platform": "instagram", "format": "post"},
{"platform": "instagram", "format": "story"}]},
timeout=60,
).json()["outputs"]
sku_url = urllib.parse.quote_plus(SKU)
for pos, out in enumerate(outputs, start=1):
content = base64.b64encode(requests.get(out["url"], timeout=60).content).decode()
requests.post(
f"{API}/products/{sku_url}/media",
headers=HDR,
data=json.dumps({"entry": {
"media_type": "image",
"label": f"Product {SKU} {out['format']}",
"position": pos,
"disabled": False,
"types": ["image", "small_image", "thumbnail"] if pos == 1 else [],
"file": f"{SKU}-{out['format']}.jpg",
"content": {"base64_encoded_data": content,
"type": "image/jpeg",
"name": f"{SKU}-{out['format']}.jpg"}}}),
timeout=120,
).raise_for_status()
print("Uploaded", len(outputs), "images to", SKU)
Cost
- 1 use per destination (platform and format) per request; repeated destinations are not charged twice.
- Failed processing is refunded.
- Every plan includes API and MCP: Free 3 uses/day, Basic 10, Pro 30, Agency 100, from 0 / 3 / 9 / 29 EUR per month.
Typical errors
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized | Token expired or mistyped | Renew the token or the integration one |
403 Forbidden | The role does not cover Catalog or Media Gallery | Edit the integration resources |
400 with “Decoding failed” | Base64 broken into lines | Encode without newlines: base64 -w0 |
404 on the product | SKU with unencoded characters | Encode the SKU (quote_plus) |
| The image does not show | Empty types or stale cache | Set image/small_image/thumbnail and flush the cache |
| Old photos disappear | Partial media_gallery_entries | Rebuild the full list |
413 from SocialCutter | The master is over 5 MB | Shrink the image before uploading it |
429 from SocialCutter | Wallet quota exhausted | Check your quota in the dashboard or upgrade |
No code
An automation tool (n8n, Make, Zapier) chains the same steps: a catalogue trigger, an HTTP node to /api/v1/images/process and an HTTP node to Magento’s REST with the token in Bearer. The pattern is in the n8n automation guide.
Next steps
- Terminal: Process images with the API from the terminal (curl)
- Python: Process images with the API from Python
- Shopify: Integrate SocialCutter with the Shopify Admin API
- WooCommerce: Upload catalogue images to WooCommerce
- Automation: Automate image resizing for social media
- Documentation: https://docs.socialcutter.theboomer.dev
Frequently asked questions
Which token do I authenticate with — integration or admin?
With the Access Token of an integration created under System → Extensions → Integrations: it is generated when you activate the integration and travels as Authorization: Bearer. The admin token from POST /rest/V1/integration/admin/token also works, but it expires according to the store's configuration.
Why must I base64-encode the image?
Because the body of POST /rest/V1/products/<sku>/media is JSON and does not take binary. The field entry.content.base64_encoded_data carries the file encoded, together with its mime type and name. Base64 grows the file by a third, so check your PHP upload limit before sending a large master.
Does a PUT with media_gallery_entries delete the images the product already had?
Yes. It replaces the whole gallery: send a partial list and you lose every entry you leave out. Rebuild the array with the old photos plus the new ones before saving.
Does Magento not generate its own sizes already?
It does: the theme defines the product, category and thumbnail sizes. Pre-generate with SocialCutter when you need an exact size the theme does not produce, or when the same master also feeds channels outside the website.
What does it cost to prepare one product's images?
1 use per destination, meaning per platform and format pair. The master goes in once and every requested size adds a use; repeated destinations in the same request are not charged twice.