# 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.
- URL: https://socialcutter.theboomer.dev/en/guides/magento/
- Idioma: en
- Familia: cms
- Actualizado: 2026-09-24
- Palabras clave: 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/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.

```bash
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

```bash
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`).

```bash
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.

```bash
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

```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](/en/guides/n8n/).

## Next steps

- Terminal: [Process images with the API from the terminal (curl)](/en/guides/curl/)
- Python: [Process images with the API from Python](/en/guides/python/)
- Shopify: [Integrate SocialCutter with the Shopify Admin API](/en/guides/shopify/)
- WooCommerce: [Upload catalogue images to WooCommerce](/en/guides/woocommerce/)
- Automation: [Automate image resizing for social media](/en/guides/automatizar-imagenes-redes-sociales/)
- Documentation: https://docs.socialcutter.theboomer.dev