# Upload SocialCutter sizes to an Etsy listing (API v3)
> Generate the sizes with SocialCutter and upload them to an Etsy listing through API v3 as multipart: image requirements, OAuth, jpg vs webp and errors.
- URL: https://socialcutter.theboomer.dev/en/guides/etsy/
- Idioma: en
- Familia: cms
- Actualizado: 2026-09-24
- Palabras clave: Etsy, API v3, listing images, listings_w, multipart, Bearer, jpg
## Why generate the ratios before uploading

Etsy crops your photos for its own views: the square thumbnail, the portrait one and the landscape one. Its requirements page says so plainly: an image should have enough border to be cropped to square, portrait and landscape without losing product, with the important part of the item in the centre.

Upload a single master and Etsy decides the crop; you only see the result afterwards. Generate the exact ratio of the slot up front and the file already fits, so there is nothing left to trim. SocialCutter's crop is **centred**: it scales the image and trims the excess equally on both sides, without reading the content of the image.

| Slot in the listing | SocialCutter destination | Generated size |
|---|---|---|
| Square view, 1:1 main photo | `instagram` `post` | 1080x1080 (1:1) |
| Landscape secondary photo | `twitter` `post` | 1200x675 (16:9) |
| Wide band for a header or a card | `linkedin` `post` | 1200x627 (1.91:1) |
| Portrait for a short listing video cover | `instagram` `story` | 1080x1920 (9:16) |
| Very wide banner-style image | `youtube` `banner` | 2560x1440 (16:9) |

Two honest notes on the sizes:

- **There is no 4:5 in the catalogue.** The portrait options are 9:16 (1080x1920) and the square one is 1:1 (1080x1080). If you need exactly 4:5 you will have to crop it outside SocialCutter.
- **The destination fixes the size and nothing is upscaled.** If Etsy recommends 2000 pixels in width and height, the catalogue's square (1080x1080) sits below that recommendation even though it clears the 635-pixel floor for the first photo with room to spare. When the 2000-pixel zoom is a requirement, upload the main photo at full resolution and use SocialCutter for the sizes the other channels need and for the secondary photos.

## Etsy's image requirements

What its help centre publishes:

| Rule | What Etsy asks for |
|---|---|
| Accepted formats | `.jpg`, `.gif`, `.png`, `.svg` and `.heic` |
| Unsupported | Animated GIF and transparent PNG; transparent areas show up black |
| Recommended listing size | Width and height of at least 2000 pixels |
| First photo | At least 635 pixels wide and tall, or the listing ranks lower in search |
| File weight | Above 1 MB an upload may not finish, especially on a slow connection |
| Colour profile | Etsy converts to sRGB, so start from sRGB |
| First photo | Landscape (horizontal) or square |
| Views | Etsy crops to square, portrait and landscape, so the image needs margin |

The practical consequence for the flow is blunt: **ask for JPG or PNG**, not WebP. Etsy does not list WebP among the formats it accepts and SocialCutter's default output is WebP. One request sets both the format and the quality:

```json
{
  "source": { "type": "url", "value": "https://your-cdn.com/master.jpg" },
  "destinations": [ { "platform": "instagram", "format": "post" } ],
  "options": { "fit_mode": "cover", "format": "jpg", "quality": 88 }
}
```

`quality` runs from 1 to 100 and defaults to 85. Lowering it is the quickest lever to stay under the megabyte Etsy treats as safe; on a product photo a JPG at 85-90 usually weighs well under 1 MB with nothing visible lost.

## The upload through API v3

```
POST https://openapi.etsy.com/v3/application/shops/{shop_id}/listings/{listing_id}/images
```

- **Body:** `multipart/form-data`, with the file in the `image` field.
- **Optional fields:**
  - `rank`: position in the listing; 1 shows left-most. Defaults to 1.
  - `overwrite`: when true, replaces the image already sitting at that `rank`. Defaults to `false`.
  - `alt_text`: alt text, maximum 500 characters.
  - `listing_image_id`: reassign an image you already deleted instead of uploading a new one.
  - `is_watermarked`: watermark flag, defaults to `false`.
- **Authentication:** two things at once. The `x-api-key` header formatted as `keystring:shared_secret` and the `Authorization: Bearer <token>` header.
- **Permission:** the OAuth token needs the `listings_w` scope.
- **Response:** `201` with a listing image object carrying `listing_image_id`, `rank`, `alt_text`, `full_width`, `full_height` and `url_fullxfull` (up to 3000 pixels per side).

If you send both `image` and `listing_image_id` in the same request, the API uploads the one in the `image` field and ignores the id.

Read the note in the reference carefully: when uploading a new image, computed data (colours, measurements) may come back as `null` because Etsy processes it asynchronously. You fetch them afterwards with the listing image lookup endpoint.

## Etsy OAuth

- **Authorisation:** `https://www.etsy.com/oauth/connect`.
- **Token:** `https://openapi.etsy.com/v3/public/oauth/token`.
- **API base:** `https://openapi.etsy.com`, with routes under `/v3/application/`.

The token expires and has to be renewed; the `refresh_token` from the authorisation code flow is what lets you do that without sending the user through the consent screen again. Keep the keystring, the shared secret and the token in environment variables, never in the code.

## Full Python snippet

```python
import json, os, requests

API = "https://api.socialcutter.theboomer.dev"
ETSY = "https://openapi.etsy.com/v3/application"
SHOP_ID = os.environ["ETSY_SHOP_ID"]
LISTING_ID = os.environ["ETSY_LISTING_ID"]

sc = requests.Session()
sc.headers["X-API-Key"] = os.environ["SOCIALCUTTER_API_KEY"]

etsy = requests.Session()
etsy.headers["x-api-key"] = os.environ["ETSY_API_KEY"]          # keystring:shared_secret
etsy.headers["Authorization"] = f"Bearer {os.environ['ETSY_ACCESS_TOKEN']}"

# 1. Sizes as jpg: WebP is not among the formats Etsy accepts
r = sc.post(f"{API}/api/v1/images/process", json={
    "source": {"type": "url", "value": "https://your-cdn.com/master.jpg"},
    "destinations": [
        {"platform": "instagram", "format": "post"},    # 1080x1080 (1:1)
        {"platform": "twitter", "format": "post"},      # 1200x675 (16:9)
    ],
    "options": {"fit_mode": "cover", "format": "jpg", "quality": 88},
}, timeout=120)
r.raise_for_status()

# 2. Upload each output to the listing at its rank
for slot, out in enumerate(r.json()["outputs"], start=1):
    img = sc.get(out["url"], timeout=120)
    img.raise_for_status()

    name = f"{out['platform']}-{out['format']}.jpg"
    up = etsy.post(
        f"{ETSY}/shops/{SHOP_ID}/listings/{LISTING_ID}/images",
        files={"image": (name, img.content, "image/jpeg")},
        data={
            "rank": slot,
            "overwrite": "true",
            "alt_text": "Cotton t-shirt, front view",
        },
        timeout=120,
    )
    up.raise_for_status()
    data = up.json()
    print(data["listing_image_id"], data["rank"], data["url_fullxfull"])
```

## Snippet with curl

```bash
# 1. Generate the sizes as jpg
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
  -H "X-API-Key: $SOCIALCUTTER_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"source":{"type":"url","value":"https://your-cdn.com/master.jpg"},
           "destinations":[{"platform":"instagram","format":"post"}],
           "options":{"fit_mode":"cover","format":"jpg","quality":88}}' \
  > out.json

curl -s "$(jq -r '.outputs[0].url' out.json)" -o listing-1.jpg

# 2. Upload it to the listing as multipart
curl -s -X POST \
  "https://openapi.etsy.com/v3/application/shops/$ETSY_SHOP_ID/listings/$ETSY_LISTING_ID/images" \
  -H "x-api-key: $ETSY_API_KEY" \
  -H "Authorization: Bearer $ETSY_ACCESS_TOKEN" \
  -F "image=@listing-1.jpg;type=image/jpeg" \
  -F "rank=1" \
  -F "overwrite=true" \
  -F "alt_text=Cotton t-shirt, front view" \
  | jq '{listing_image_id, rank, url_fullxfull}'
```

To fill several positions, repeat the second call with a different `rank` (2, 3, 4...) and file. The `overwrite` field is only needed when you want to replace whatever already sits in that slot.

## Cost

- **1 use per destination** (platform and format) per SocialCutter request. Repeated destinations are not charged twice and failed processings are refunded.
- Plans: Free 3 uses/day, 3 EUR 10/day, 9 EUR 30/day and 29 EUR 100/day, all with the API and the MCP server included.
- The Etsy API charges nothing per call; it applies its own usage limits, so space the uploads out if you are filling many positions back to back.

## Common errors

| Code | Source | What happens | What to do |
|---|---|---|---|
| 400 | Etsy | Something is wrong with the request data | Check the multipart and the fields you send |
| 401 | Etsy | Credentials missing or the token is invalid | Verify `x-api-key` and the Authorization Bearer |
| 403 | Etsy | The operation is not allowed for that token | Add the `listings_w` scope and authorise again |
| 404 | Etsy | The resource cannot be found | Check `shop_id` and `listing_id` |
| 409 | Etsy | Conflict with the listing's current state | Re-read the positions before rewriting them |
| 500 | Etsy | Internal error on Etsy's side | Retry with backoff; the image was not created |
| The upload never finishes | Etsy | The file goes over 1 MB, especially on a slow link | Lower `options.quality` and generate the JPG again |
| 401 | SocialCutter | Missing or invalid `X-API-Key` header | Check the value starts with `sc_` and is still active |
| 413 | SocialCutter | The master exceeds 5 MB | Shrink the image before the call |
| 429 | SocialCutter | Wallet quota exhausted | Check `GET /api/v1/wallet` |

## What SocialCutter does not do

The crop is **centred and deterministic**: it does not read the content of the image to decide what to keep, it does not edit the photo (no colour work, no background removal, no text compositing), it does not publish to social networks or to Etsy, and it does not accept files over 5 MB. It produces the versions at the exact size of each destination and returns their URLs: uploading them to the listing is your script's job.

## Next steps

- Other stores: [Integrate SocialCutter with the Shopify API](/en/guides/shopify/), [WooCommerce](/en/guides/woocommerce/) and [BigCommerce](/en/guides/bigcommerce/)
- Sizes: [Social media sizes: dimensions and ratios](/en/guides/medidas-redes-sociales/)
- Formats: [PNG, JPG or WebP: which format to use on each network](/en/guides/formatos-redes-sociales/)
- Code: [Automate SocialCutter with Python](/en/guides/python/) and [from the terminal with curl](/en/guides/curl/)
- SocialCutter API docs: https://docs.socialcutter.theboomer.dev
- Etsy API v3 reference: https://developer.etsy.com/documentation/reference/
- Etsy image requirements: https://help.etsy.com/hc/en-us/articles/115015663347