# Contentful: when to pre-generate images and how to publish
> The Contentful Images API already crops on the fly: see when pre-generating files with SocialCutter pays off and how to publish the asset via the CMA.
- URL: https://socialcutter.theboomer.dev/en/guides/contentful/
- Idioma: en
- Familia: cms
- Actualizado: 2026-09-24
- Palabras clave: Contentful, Images API, Content Management API, assets, uploadFrom, social media images, pre-generate sizes
## What Contentful already does on its own

Contentful has its own Images API: a **read-only** API served from `images.ctfassets.net` to which you append query parameters on the asset file URL (`fields.file.url`) to transform the image on the fly. Nothing new is uploaded and no files are generated: you change the URL and the CDN returns the transformed version.

| Parameter | What it does | Values |
|---|---|---|
| `w` / `h` | Width and height in pixels | 4000 px maximum |
| `fit` | Resizing behaviour | `pad`, `fill`, `scale`, `crop`, `thumb` |
| `f` | Focus of the frame when using `pad`, `fill`, `crop` or `thumb` | the default is `center` |
| `fm` | Output format | `jpg`, `png`, `webp`, `gif`, `avif`, `tiff`; the original by default |
| `q` | Quality | an integer from 1 to 100 |
| `bg` | Background colour for `pad` and rounded corners | RGB values, e.g. `rgb:9090ff` |
| `r` | Rounded corners or a circular crop | pixels, or `max` |
| `fl` | Specific variants | `progressive` for JPEG, `png8` for 8-bit PNG |

An example output URL:

```text
https://images.ctfassets.net/SPACE_ID/ASSET_ID/TOKEN/name.jpg?w=1080&h=1080&fit=fill&fm=webp&q=85
```

Published assets need no authentication on the Images API, so anyone can request those transformations from your site. With `fit=fill` and the default focus (`center`) you get a centered crop equivalent to SocialCutter's. And if the original image is over 100 MB, Contentful treats it as a plain asset and applies no transformations.

**Honest conclusion: for your website and blog you almost never need to pre-generate anything.** One well-formed call from the template solves it. Pre-generating matters in a different scenario.

## Where it falls short for social media

The Images API returns a **transformed URL**. It does not return a downloadable file ready to upload. That is not a flaw: it solves a different problem.

When you publish to Instagram, TikTok, YouTube or LinkedIn, the network does not fetch your URL: you upload a file to it. And publishing schedulers, campaign tools, partners and client dossiers almost always ask for a file with its own name and weight. No query parameter helps there.

## When pre-generating with SocialCutter pays off

| Case | Why | Usual destination |
|---|---|---|
| Pieces for networks that do not go through the Contentful CDN | The network needs a file at the exact size, not a URL | `instagram` `post`, `instagram` `story`, `tiktok` `cover` |
| Publishing schedulers and campaign tools | They only accept a file upload | `facebook` `post`, `linkedin` `post`, `twitter` `post` |
| Campaigns outside Contentful (paid, email, partners) | The asset leaves the CMS and is handed over | `youtube` `thumbnail`, `twitter` `header` |
| Exports and client deliveries | You need a pack of files with readable names and controlled weight | All of the campaign |
| One master feeding several channels | One design, one output per channel, no template changes | `linkedin` `cover`, `facebook` `cover` |
| Partners that cannot use URLs with parameters | Their system does not build the transformation | Any |

The flow is always the same: **one master goes in, SocialCutter returns every size, and the file gets published or delivered**. The `cover` crop (the default mode) is **centered**: it scales and trims the excess evenly on both sides, with no analysis of the image. Leave some air around the edges of the master.

## When you do not need to pre-generate

| Case | What to use instead |
|---|---|
| The image is only served on your website or blog | The Images API parameters (`w`, `h`, `fit`, `fm`, `q`); costs no uses |
| The theme or template already applies its ratio | Nothing: do not duplicate assets |
| You only want different resolutions for different screens | The same asset with `w`+`fit` and a `srcset` |
| Brand archive | Keep the uncropped master and generate at publish time |
| You need retouching, a transparent background or composed text | A photo editor: that is not what SocialCutter does |

One warning that prevents an expensive mistake: **do not replace the Contentful master with a cropped output**. If the campaign's main asset becomes a 1080x1080, the 2560x1440 YouTube banner will come out upscaled and blurry. The master stays the master; sizes are generated when you publish.

## Generating a campaign's sizes

```bash
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
  -H "X-API-Key: sc_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: contentful-campaign-2026-09" \
  -d '{
    "source": { "type": "url", "value": "https://images.ctfassets.net/SPACE_ID/ASSET_ID/TOKEN/master.jpg" },
    "destinations": [
      { "platform": "instagram", "format": "post" },
      { "platform": "instagram", "format": "story" },
      { "platform": "linkedin", "format": "post" },
      { "platform": "youtube", "format": "thumbnail" }
    ],
    "options": { "fit_mode": "cover", "format": "jpg", "quality": 85 }
  }' > sc.json

jq -r '.outputs[] | "\(.platform)/\(.format) \(.width)x\(.height) \(.size_bytes) bytes \(.url)"' sc.json
```

That request costs **4 uses** and returns four outputs. Note the useful detail: you can pass the Contentful CDN URL itself as the `source`, so the master keeps living in one place. Every output is public, which is exactly what you need to publish it to a network or upload it as an asset.

## Publishing the asset with the Content Management API

The [Content Management API](https://www.contentful.com/developers/docs/references/content-management-api/) (CMA) uses the base `https://api.contentful.com` and `Authorization: Bearer <token>`. Every call carries `Content-Type: application/vnd.contentful.management.v1+json`. Creating an asset takes **three steps**: create, process and publish.

```bash
export CF="https://api.contentful.com/spaces/SPACE_ID/environments/ENV_ID"
export CF_TOKEN="CFPAT-..."
export LOCALE="en-US"
```

### 1. Upload the binary to the Upload API

If the file is already at a public URL (a SocialCutter output, for instance), you can skip this step and pass the URL directly in the `upload` field when creating the asset. If the file is local, upload it first:

```bash
curl -s -X POST "https://upload.contentful.com/spaces/SPACE_ID/environments/ENV_ID/uploads" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @instagram-post.jpg > upload.json

UPLOAD_ID=$(jq -r '.sys.id' upload.json)
```

The response carries the upload `sys.id`. Watch its expiry: if you do not associate it with an asset and process it within **24 hours**, the file and its metadata are deleted. Clients with EU data residency use `upload.eu.contentful.com`.

### 2. Create the asset pointing at the upload

```bash
curl -s -X POST "$CF/assets" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/vnd.contentful.management.v1+json" \
  -d '{
    "fields": {
      "title": { "en-US": "September campaign · Instagram post 1080x1080" },
      "file": {
        "en-US": {
          "contentType": "image/jpeg",
          "fileName": "september-campaign-instagram-post-1080x1080.jpg",
          "uploadFrom": {
            "sys": { "type": "Link", "linkType": "Upload", "id": "'"$UPLOAD_ID"'" }
          }
        }
      }
    }
  }' > asset.json

ASSET_ID=$(jq -r '.sys.id' asset.json)
VERSION=$(jq -r '.sys.version' asset.json)
```

To choose the ID yourself, use a `PUT` to `/spaces/SPACE_ID/environments/ENV_ID/assets/ASSET_ID`: it creates the asset with that ID or updates the existing one. On update, Contentful does not merge changes: you send the whole resource and must pass the current version in the `X-Contentful-Version` header (optimistic locking).

File names have rules: letters, digits, dots, hyphens and underscores only; any other character is replaced by an underscore. Do not rely on accents or symbols.

### 3. Process and publish

```bash
# Process: mandatory before publishing
curl -s -o /dev/null -w "%{http_code}\n" -X PUT \
  "$CF/assets/$ASSET_ID/files/$LOCALE/process" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/vnd.contentful.management.v1+json" \
  -H "X-Contentful-Version: $VERSION"

# Publish
curl -s -X PUT "$CF/assets/$ASSET_ID/published" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -H "Content-Type: application/vnd.contentful.management.v1+json" \
  -H "X-Contentful-Version: $VERSION" | jq '{id: .sys.id, publishedVersion: .sys.publishedVersion}'
```

Processing is the step that brings the file into Contentful's system and fills `fields.file.url`; the call may return before processing finishes. **Without processing you cannot publish** the asset or preview it in the Media tab. Once published, the asset is available on the Content Delivery API and, if it is an image, on `images.ctfassets.net` with the transformation parameters.

With a CMA token the default limit is **7 requests per second**; exceed it and the API answers `429` and tells you how long to wait in `X-Contentful-RateLimit-Reset`.

### The same flow in Python

```python
import requests

CF = "https://api.contentful.com/spaces/SPACE_ID/environments/ENV_ID"
HDR = {"Authorization": "Bearer CFPAT-...",
       "Content-Type": "application/vnd.contentful.management.v1+json"}
LOCALE = "en-US"

outputs = requests.post(
    "https://api.socialcutter.theboomer.dev/api/v1/images/process",
    headers={"X-API-Key": "sc_...", "Content-Type": "application/json"},
    json={"source": {"type": "url", "value": "https://images.ctfassets.net/.../master.jpg"},
          "destinations": [{"platform": "instagram", "format": "post"},
                           {"platform": "youtube", "format": "thumbnail"}]},
    timeout=60,
).json()["outputs"]

for out in outputs:
    binary = requests.get(out["url"], timeout=60).content
    upload = requests.post(
        "https://upload.contentful.com/spaces/SPACE_ID/environments/ENV_ID/uploads",
        headers={"Authorization": HDR["Authorization"],
                 "Content-Type": "application/octet-stream"},
        data=binary, timeout=120,
    ).json()["sys"]["id"]

    name = f"campaign-{out['platform']}-{out['format']}-{out['width']}x{out['height']}.jpg"
    asset = requests.post(f"{CF}/assets", headers=HDR, json={"fields": {
        "title": {LOCALE: f"Campaign {out['platform']} {out['format']}"},
        "file": {LOCALE: {"contentType": "image/jpeg", "fileName": name,
                          "uploadFrom": {"sys": {"type": "Link", "linkType": "Upload", "id": upload}}}}},
        }, timeout=60).json()

    ver = asset["sys"]["version"]
    requests.put(f"{CF}/assets/{asset['sys']['id']}/files/{LOCALE}/process",
                 headers={**HDR, "X-Contentful-Version": str(ver)}, timeout=60).raise_for_status()
    requests.put(f"{CF}/assets/{asset['sys']['id']}/published",
                 headers={**HDR, "X-Contentful-Version": str(ver)}, timeout=60).raise_for_status()

print("Published", len(outputs), "assets")
```

## Cost

- **1 use per destination** (platform and format) per request; duplicates are not charged twice.
- Contentful's Images API transformations consume no uses: they are Contentful's.
- Failed processing jobs are refunded.
- Every plan includes API and MCP: Free 3 uses/day, Basic 10, Pro 30, Agency 100, from 0 / 3 / 9 / 29 EUR per month.

## Typical errors

| Symptom | Cause | What to do |
|---|---|---|
| `401` on the CMA | Token missing, expired or without access to the environment | Check the personal access token and its environment access |
| `409` / version conflict | Stale `X-Contentful-Version` | Re-read the asset and retry with its current version |
| "Cannot publish until processing" | Publishing was attempted before processing | Process the locale file first |
| The asset stays draft with no URL | The upload expired before processing | Upload the binary again: an upload expires in 24 hours |
| `422` with odd characters in the name | `fileName` with accents or symbols | Use letters, digits, dots, hyphens and underscores only |
| `429` on the CMA | More than 7 requests per second | Wait for what `X-Contentful-RateLimit-Reset` says |
| `Content-Type` comes back as an error | The API version header is missing | Send `application/vnd.contentful.management.v1+json` on every call |
| Blurry YouTube banner | A cropped output was stored as the master | Keep the master and generate the banner from it |
| `413` from SocialCutter | The master is over 5 MB | Shrink the master or use the CDN URL as `source` |

## Next steps

- Sizes: [Social media image sizes: dimensions and ratios](/en/guides/medidas-redes-sociales/)
- Formats: [PNG, JPG or WebP: which format to use on each network](/en/guides/formatos-redes-sociales/)
- Automation: [Automating social media images: the 4 real paths](/en/guides/automatizar-imagenes-redes-sociales/)
- Webflow: [Upload images to Webflow and use SocialCutter](/en/guides/webflow/)
- WordPress: [Integrate SocialCutter with WordPress and WooCommerce](/en/guides/wordpress/)
- Management API reference: https://www.contentful.com/developers/docs/references/content-management-api/
- Images API reference: https://www.contentful.com/developers/docs/references/images-api/