CMS and websites
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.
- 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:
{
"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 theimagefield. - Optional fields:
rank: position in the listing; 1 shows left-most. Defaults to 1.overwrite: when true, replaces the image already sitting at thatrank. Defaults tofalse.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 tofalse.
- Authentication: two things at once. The
x-api-keyheader formatted askeystring:shared_secretand theAuthorization: Bearer <token>header. - Permission: the OAuth token needs the
listings_wscope. - Response:
201with a listing image object carryinglisting_image_id,rank,alt_text,full_width,full_heightandurl_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
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
# 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 "[email protected];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, WooCommerce and BigCommerce
- Sizes: Social media sizes: dimensions and ratios
- Formats: PNG, JPG or WebP: which format to use on each network
- Code: Automate SocialCutter with Python and from the terminal with 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
Frequently asked questions
Which file formats does Etsy accept?
Its requirements page lists .jpg, .gif, .png, .svg and .heic. WebP is not on that list, so SocialCutter's default output (webp) will not do here: pass options.format with jpg or png in the same request.
What size does Etsy want for a listing image?
Etsy recommends a width and a height of at least 2000 pixels and asks that the first photo be at least 635 pixels wide and tall so the listing does not rank lower in search. SocialCutter fixes the measurement from the destination list and never upscales, so the size comes from the catalogue, not the other way round.
Why does the colour or size of a freshly uploaded image come back null?
Because Etsy processes the image asynchronously. The API reference itself warns that fields such as the colours and the size may return as null when uploading; you read them afterwards with the listing image lookup endpoint.
How do I replace a photo that already sits in a listing position?
With rank to set the position and overwrite set to true, which replaces the image already in that slot. Without it Etsy adds a new image instead of substituting anything.
What does it cost to prepare the sizes for one listing?
1 use per destination, meaning per platform and format pair. Two measurements for the same listing spend 2 uses. Plans include the API and the MCP server.