# 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.
- URL: https://socialcutter.theboomer.dev/en/guides/strapi/
- Idioma: en
- Familia: cms
- Actualizado: 2026-09-24
- Palabras clave: 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](/en/guides/medidas-redes-sociales/).

## 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) |

```bash
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

```bash
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.

```bash
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:

```js
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:

```bash
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.

```bash
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](/en/guides/automatizar-imagenes-redes-sociales/).

## Next steps

- WordPress and WooCommerce: [Integrate SocialCutter with WordPress and WooCommerce](/en/guides/wordpress/)
- Shopify: [Integrate SocialCutter with the Shopify Admin API](/en/guides/shopify/)
- Terminal: [Process images with the API from the terminal (curl)](/en/guides/curl/)
- Automation: [Automate image resizing with n8n](/en/guides/n8n/)
- Documentation: https://docs.socialcutter.theboomer.dev