Skip to main content
SocialCutter

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 listingSocialCutter destinationGenerated size
Square view, 1:1 main photoinstagram post1080x1080 (1:1)
Landscape secondary phototwitter post1200x675 (16:9)
Wide band for a header or a cardlinkedin post1200x627 (1.91:1)
Portrait for a short listing video coverinstagram story1080x1920 (9:16)
Very wide banner-style imageyoutube banner2560x1440 (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:

RuleWhat Etsy asks for
Accepted formats.jpg, .gif, .png, .svg and .heic
UnsupportedAnimated GIF and transparent PNG; transparent areas show up black
Recommended listing sizeWidth and height of at least 2000 pixels
First photoAt least 635 pixels wide and tall, or the listing ranks lower in search
File weightAbove 1 MB an upload may not finish, especially on a slow connection
Colour profileEtsy converts to sRGB, so start from sRGB
First photoLandscape (horizontal) or square
ViewsEtsy 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 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

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

CodeSourceWhat happensWhat to do
400EtsySomething is wrong with the request dataCheck the multipart and the fields you send
401EtsyCredentials missing or the token is invalidVerify x-api-key and the Authorization Bearer
403EtsyThe operation is not allowed for that tokenAdd the listings_w scope and authorise again
404EtsyThe resource cannot be foundCheck shop_id and listing_id
409EtsyConflict with the listing’s current stateRe-read the positions before rewriting them
500EtsyInternal error on Etsy’s sideRetry with backoff; the image was not created
The upload never finishesEtsyThe file goes over 1 MB, especially on a slow linkLower options.quality and generate the JPG again
401SocialCutterMissing or invalid X-API-Key headerCheck the value starts with sc_ and is still active
413SocialCutterThe master exceeds 5 MBShrink the image before the call
429SocialCutterWallet quota exhaustedCheck 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

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.