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 Strapi | SocialCutter destination | Size |
|---|---|---|
| Entry featured image | linkedin post | 1200x627 (1.91:1) |
| Image inside the content | facebook post | 1200x630 (1.91:1) |
| Social card (Open Graph) | twitter post | 1200x675 (16:9) |
| Site header | twitter header | 1500x500 (3:1) |
| Product 1:1 image | instagram post | 1080x1080 (1:1) |
| Second vertical image | instagram story | 1080x1920 (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:
| Detail | Strapi 4 | Strapi 5 |
|---|---|---|
| REST response shape | data.attributes | fields flattened onto data |
| Entry reference | numeric id | documentId |
| File upload | POST /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
| Symptom | Cause | Fix |
|---|---|---|
403 on /api/upload | The token lacks the upload action | Grant the upload plugin permission or use a Full access token |
401 from Strapi | Token missing, mistyped or revoked | Send Authorization: Bearer with an active token |
400 “files is required” | JSON was sent instead of multipart | Use -F in curl or FormData in Node, with the files field |
| The entry stores the URL, not the image | A string was sent in the media field | Send the file id, not the URL |
413 from SocialCutter | The master is over 5 MB | Shrink the master before uploading it |
429 from SocialCutter | Wallet quota exhausted | Check 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
- WordPress and WooCommerce: Integrate SocialCutter with WordPress and WooCommerce
- Shopify: Integrate SocialCutter with the Shopify Admin API
- Terminal: Process images with the API from the terminal (curl)
- Automation: Automate image resizing with n8n
- Documentation: https://docs.socialcutter.theboomer.dev
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.