CMS and websites
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.
- 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:
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
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 (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.
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:
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
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
# 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
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
- Formats: PNG, JPG or WebP: which format to use on each network
- Automation: Automating social media images: the 4 real paths
- Webflow: Upload images to Webflow and use SocialCutter
- WordPress: Integrate SocialCutter with WordPress and WooCommerce
- 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/
Frequently asked questions
If Contentful already resizes in the URL, why would I want SocialCutter?
For everything that does not go through its CDN. The Images API returns a transformed URL, not a file: when the social network, the publishing scheduler, the partner or the client delivery demands a file at the exact size, that file has to be generated elsewhere.
Should I replace the Contentful master with the cropped version?
No, that is a mistake. If you store a 1080x1080 asset as the main one, you cannot produce the 2560x1440 YouTube banner from it without upscaling. Keep the uncropped master and generate the sizes when you publish.
How many calls does it take to publish an asset through the API?
Three: create the asset, process it and publish it. If the file is not at a reachable URL there is a fourth call first: upload the binary to the Upload API to get an upload_id. Processing is mandatory: nothing can be published before it.
What goes in the Content-Type header of the Management API?
application/vnd.contentful.management.v1+json on every CMA call, with Authorization: Bearer <token>. That Content-Type is what pins the API version, so it is best to always send it explicitly instead of leaving it to the default.
What does it cost to prepare a campaign with SocialCutter?
1 use per destination, meaning per platform and format pair. Duplicate destinations in the same request are not charged twice and failed processing is refunded. Contentful's Images API transformations do not consume uses.
Can I publish the asset with an ID I choose?
Yes. Besides the POST that generates the ID automatically, the Management API lets you create or update an asset with your own ID through a PUT to /spaces/{space_id}/environments/{environment_id}/assets/{asset_id}. Handy for matching the campaign ID.