Skip to main content
SocialCutter

CMS and websites

Upload images to the Strapi media library with SocialCutter

Upload images to the Strapi media library over POST /api/upload with a multipart request and use them in your entries at the right size.

  • Strapi
  • media library
  • api/upload
  • multipart
  • API token
  • media relation
  • Node
  • curl

Why generate the sizes before uploading

A blog entry or a product page shows the same image in four places: the listing card, the article header, the Open Graph card and a social thumbnail. Each slot wants a different ratio. If you upload the master and let CSS crop it, the result depends on the browser and the screen.

The flow is: one master goes in, SocialCutter returns one output per destination, and the Strapi media library receives the file already cropped. The cover fit mode (the default) is a centred crop: it scales the image and splits the excess evenly on both sides. There is no subject detection and no step that decides which part is expendable.

Slot in StrapiSocialCutter destinationSize
Entry featured imagelinkedin post1200x627 (1.91:1)
Image inside the contentfacebook post1200x630 (1.91:1)
Social card (Open Graph)twitter post1200x675 (16:9)
Site headertwitter header1500x500 (3:1)
Product 1:1 imageinstagram post1080x1080 (1:1)
Second vertical imageinstagram story1080x1920 (9:16)

The full catalogue of platforms and formats comes from GET /api/v1/platforms, which is public, and is summarised in the social media sizes guide.

What Strapi does on its own (and what it does not)

Strapi’s upload plugin generates breakpoints: thumbnail, small, medium and large. They are rescalings that keep the original ratio, not crops to a specific one. A 3:2 photo stays 3:2 in all four. That is why they do not replace a per-platform crop: they keep the mobile download small, they do not fill a 1:1 or 9:16 slot.

There is also no 4:5 in the SocialCutter catalogue: vertical is 9:16 (1080x1920) and square is 1:1 (1080x1080). If you need an exact 4:5, crop outside SocialCutter.

Before you start: token, permissions and version

API token. In the Strapi admin, Settings → API Tokens creates a token with type Full access, Read-only or Custom. The value is shown once and travels in the Authorization: Bearer header.

Permissions. A Custom token carries the same permission matrix as a role: grant it the upload action of the upload plugin and, if the same call updates the entry, the update action of the content type. A Full access token needs nothing configured. Reference: https://docs.strapi.io/cms/features/api-tokens

Version. Strapi ships major versions with REST format changes, so pin the one you run and check the docs before moving a 4 to a 5:

DetailStrapi 4Strapi 5
REST response shapedata.attributesfields flattened onto data
Entry referencenumeric iddocumentId
File uploadPOST /api/upload (FormData)POST /api/upload (FormData)
export STRAPI_URL="https://your-strapi.com"
export STRAPI_TOKEN="your_api_token"
export SC_KEY="sc_your_key"

1. Process the master with SocialCutter

curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
  -H "X-API-Key: *** \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "type": "url", "value": "https://your-cdn.com/master.jpg" },
    "destinations": [
      { "platform": "linkedin", "format": "post" },
      { "platform": "instagram", "format": "post" }
    ]
  }' > sc.json

jq '.image_id, (.outputs[] | {url, platform, format, width, height})' sc.json

The response carries image_id and one URL per destination. If the master only exists on your disk, use POST /api/v1/images/process/upload (multipart, 5 MB max).

2. Upload the image to the media library

POST /api/upload is multipart and the only required field is files. It accepts several entries in the same request and returns one object per file with id, url and the formats block.

curl -s -o linkedin.jpg "$(jq -r '.outputs[0].url' sc.json)"

curl -s -X POST "$STRAPI_URL/api/upload" \
  -H "Authorization: Bearer $STRAPI_TOKEN" \
  -F "files=@linkedin.jpg" > media.json

jq '.[0] | {id, url, mime, width, height}' media.json

On Node 18 or newer FormData and Blob are global, so the multipart needs no dependency. The trick is to download the output as a Blob and append it with a filename:

const SC_KEY = process.env.SC_KEY
const STRAPI_URL = process.env.STRAPI_URL
const STRAPI_TOKEN = process.env.STRAPI_TOKEN

async function processMaster(masterUrl) {
  const res = await fetch('https://api.socialcutter.theboomer.dev/api/v1/images/process', {
    method: 'POST',
    headers: { 'X-API-Key': SC_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      source: { type: 'url', value: masterUrl },
      destinations: [{ platform: 'linkedin', format: 'post' }]
    })
  })
  if (!res.ok) throw new Error(`SocialCutter ${res.status}`)
  return res.json()
}

async function uploadToMediaLibrary(imageUrl, filename) {
  const bin = await fetch(imageUrl)
  const blob = await bin.blob()

  const form = new FormData()
  form.append('files', blob, filename)

  const res = await fetch(`${STRAPI_URL}/api/upload`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${STRAPI_TOKEN}` },
    body: form
  })
  if (!res.ok) throw new Error(`Strapi upload ${res.status}`)
  const [file] = await res.json()
  return file // { id, documentId?, url, formats }
}

Endpoint reference: https://docs.strapi.io/cms/api/rest/upload

3. Link the image to the entry

There are two paths and neither needs a plugin.

In the same upload. /api/upload accepts ref (the content type UID), refId (the entry reference, documentId on Strapi 5) and field (the media field name). The file is linked at birth:

curl -s -X POST "$STRAPI_URL/api/upload" \
  -H "Authorization: Bearer $STRAPI_TOKEN" \
  -F "files=@linkedin.jpg" \
  -F "ref=api::article.article" \
  -F "refId=abc123xyz" \
  -F "field=cover"

In a second call. Upload first, keep the file id and update the entry. A single media field takes the id; a multiple one takes an array of ids. The body shape depends on the version: the data wrapper is the same in both, the response format is not.

FILE_ID=$(jq -r '.[0].id' media.json)

curl -s -X PUT "$STRAPI_URL/api/articles/abc123xyz?populate=cover" \
  -H "Authorization: Bearer $STRAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"data\": {\"cover\": $FILE_ID}}"

With ?populate=cover the response carries the whole media object and you can check the id matches.

Cost

  • 1 use per destination (platform and format) per request; duplicates are not charged twice.
  • Failed processings are refunded.
  • Plans 0/3/9/29 EUR, all with API and MCP.

Common errors

SymptomCauseFix
403 on /api/uploadThe token lacks the upload actionGrant the upload plugin permission or use a Full access token
401 from StrapiToken missing, mistyped or revokedSend Authorization: Bearer with an active token
400 “files is required”JSON was sent instead of multipartUse -F in curl or FormData in Node, with the files field
The entry stores the URL, not the imageA string was sent in the media fieldSend the file id, not the URL
413 from SocialCutterThe master is over 5 MBShrink the master before uploading it
429 from SocialCutterWallet quota exhaustedCheck GET /api/v1/credits or upgrade

No code

An automation tool chains the same steps with nodes: a trigger, an HTTP node to /api/v1/images/process and an HTTP node to /api/upload sending the file as multipart. The general pattern is in the automating social media images guide.

Next steps

Frequently asked questions

Why not let Strapi resize the image I upload?

Strapi generates breakpoints (thumbnail, small, medium, large) that scale the master while keeping its aspect ratio. They do not crop to the exact ratio a social network wants, so a 3:2 photo stays 3:2 in all of them and works neither as a 1:1 post nor as a 9:16 story. SocialCutter returns each exact size before you upload.

Which permissions does the API token need?

The token must be able to use the upload plugin (the upload action of the upload plugin) and, if you also update the entry in the same call, the update action of that content type. A Full access token covers both; a Custom one needs those actions ticked by hand.

Is the image linked to the entry while uploading or in a second call?

Both work. POST /api/upload accepts ref, refId and field to create the file and link it in the same request. If you prefer to upload first and link later, keep the file id and update the entry's media field.

What changes between Strapi 4 and Strapi 5?

Mostly the REST response shape. Strapi 4 wraps fields in data.attributes and uses a numeric id; Strapi 5 flattens the fields onto the object, uses documentId as the stable reference and returns it from the upload endpoint too. The upload endpoint itself is still POST /api/upload with FormData on both.

What does it cost to process the image for one entry?

1 use per destination, meaning per platform and format pair. Asking for Instagram post and LinkedIn post from the same master spends 2 uses.