Skip to main content
SocialCutter

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 storeSocialCutter destinationSize
Main imageinstagram post1080x1080 (1:1)
Portrait product imageinstagram story1080x1920 (9:16)
Category bannerfacebook post1200x630 (1.91:1)
CMS headertwitter header1500x500 (3:1)
Product video thumbnailyoutube thumbnail1280x720 (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/token with {"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.

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

SymptomCauseFix
401 UnauthorizedToken expired or mistypedRenew the token or the integration one
403 ForbiddenThe role does not cover Catalog or Media GalleryEdit the integration resources
400 with “Decoding failed”Base64 broken into linesEncode without newlines: base64 -w0
404 on the productSKU with unencoded charactersEncode the SKU (quote_plus)
The image does not showEmpty types or stale cacheSet image/small_image/thumbnail and flush the cache
Old photos disappearPartial media_gallery_entriesRebuild the full list
413 from SocialCutterThe master is over 5 MBShrink the image before uploading it
429 from SocialCutterWallet quota exhaustedCheck 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

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.