# Store SocialCutter output sizes in a Notion database
> Generate each format with SocialCutter, then take it to Notion as an external URL or through the File Upload API: hosting limits, versions and Python code.
- URL: https://socialcutter.theboomer.dev/en/guides/notion/
- Idioma: en
- Familia: ia
- Actualizado: 2026-09-24
- Palabras clave: Notion, File Upload API, database, external URL, Python, SocialCutter, automation
## The boundary: SocialCutter generates, Notion stores

SocialCutter takes an image, crops it **centred** to the exact dimensions of each destination and returns one URL per output. The crop is centred with no subject detection: nothing analyses the content to decide what gets cut. It does not publish anywhere and it does not write to Notion.

So the integration has two clearly separated halves: producing the files at the right size and deciding where you keep them. This guide covers the second one with the Notion API.

## Why pre-generate before it reaches Notion

Notion scales images to fit the box you drop them into; it does not crop them to a given ratio. If you upload the same master to a vertical block, a wide cover and a table thumbnail, the result depends entirely on the container.

| Slot in Notion | SocialCutter destination | Size |
|---|---|---|
| Page cover or post header | `facebook` `post` | 1200x630 (1.91:1) |
| Square image in a gallery | `instagram` `post` | 1080x1080 (1:1) |
| Vertical block, story or reel | `instagram` `story` | 1080x1920 (9:16) |
| Landscape thumbnail for a table or card | `twitter` `post` | 1200x675 (16:9) |
| Wide page header | `twitter` `header` | 1500x500 (3:1) |

Pre-generating those five costs 5 uses and comes back in a single request, instead of reworking the image every time the page template changes.

## The two paths for an image to reach Notion

| Path | Object | What Notion stores | When to use it |
|---|---|---|---|
| External URL | `external` | Only the URL, no copy of the file | The image lives on your CDN or on SocialCutter and needs no permissions |
| File Upload API | `file_upload` | A copy in the workspace storage | The image has to live inside Notion |

Files you drag in by hand in the UI are of type `file` and they also consume workspace storage, which counts against your Notion plan.

## Path 1: the output URL as an external image

This is the short route: write the URL into a URL property of a database, or use it in an image block.

```bash
curl -s -X PATCH "https://api.notion.com/v1/blocks/$PAGE_ID/children" \
  -H "Authorization: Bearer $NOTION_TOKEN" \
  -H "Notion-Version: 2022-06-28" \
  -H "Content-Type: application/json" \
  -d "{\"children\":[{\"object\":\"block\",\"type\":\"image\",\"image\":{\"type\":\"external\",\"external\":{\"url\":\"$OUTPUT_URL\"}}}]}"
```

Upside: zero bytes in Notion storage and the same URL works for the CMS, the social network or an email. Downside: if the URL expires or the master is taken down, the image vanishes from the page.

## Path 2: the File Upload API

Three steps, exactly as Notion documents them:

1. `POST /v1/file_uploads` creates the object in `pending` state and returns an `upload_url`. This lets you reserve the slot before you actually hold the file.
2. Send the contents to that `upload_url` with `Content-Type: multipart/form-data` and the file under the `file` field.
3. Use the uploaded file's `id` in an `image` block of type `file_upload`, or in a file property.

The default `single_part` mode accepts up to 20 MB. Above that you need `multi_part`, which splits the file into 5 to 20 MB parts and reaches 5 GB in paid workspaces. Because SocialCutter accepts masters of 5 MB at most, its outputs always fit the simple path. The file has to be attached **within one hour** of being created or it expires.

## Python snippet

```python
import os
import requests

SC = "https://api.socialcutter.theboomer.dev"
NOTION = "https://api.notion.com/v1"
NOTION_VERSION = os.environ.get("NOTION_VERSION", "2022-06-28")

sc = requests.Session()
sc.headers["X-API-Key"] = os.environ["SOCIALCUTTER_API_KEY"]

nt = requests.Session()
nt.headers.update({
    "Authorization": f"Bearer {os.environ['NOTION_TOKEN']}",
    "Notion-Version": NOTION_VERSION,
})

# 1. One SocialCutter request, one URL per destination
resp = sc.post(f"{SC}/api/v1/images/process", json={
    "source": {"type": "url", "value": "https://example.com/master.jpg"},
    "destinations": [
        {"platform": "instagram", "format": "post"},
        {"platform": "instagram", "format": "story"},
        {"platform": "facebook", "format": "post"},
    ],
}, timeout=60)
resp.raise_for_status()
outputs = resp.json()["outputs"]

# 2. A page in a database with one URL property per destination
props = {"Name": {"title": [{"text": {"content": "September campaign"}}]}}
for out in outputs:
    props[f"{out['platform']}-{out['format']}"] = {"url": out["url"]}

page = nt.post(f"{NOTION}/pages", json={
    "parent": {"database_id": os.environ["NOTION_DB_ID"]},
    "properties": props,
}, timeout=30)
page.raise_for_status()
print(page.json()["id"])

# 3. Alternative: upload the file and attach it as a page image
def upload_to_notion(url, name, content_type):
    up = nt.post(f"{NOTION}/file_uploads", json={
        "mode": "single_part", "filename": name, "content_type": content_type,
    }, timeout=30)
    up.raise_for_status()
    payload = sc.get(url, timeout=60).content
    send = requests.post(up.json()["upload_url"], headers={
        "Authorization": nt.headers["Authorization"],
        "Notion-Version": NOTION_VERSION,
    }, files={"file": (name, payload, content_type)}, timeout=120)
    send.raise_for_status()
    return up.json()["id"]

first = outputs[0]
fid = upload_to_notion(first["url"], f"{first['platform']}-{first['format']}.webp", "image/webp")

nt.patch(f"{NOTION}/blocks/{page.json()['id']}/children", json={
    "children": [{"object": "block", "type": "image",
                  "image": {"type": "file_upload", "file_upload": {"id": fid}}}],
}, timeout=30).raise_for_status()
```

Property names depend on your database: the title property and the URL properties have to exist beforehand, or you send field ids instead of names.

## Notion's image hosting limits

- **Notion is not a CDN.** Images you upload count towards your workspace storage, which is plan-based.
- An `external` image **is not copied**: Notion keeps the reference and loads it from outside every time.
- URLs for files hosted by Notion are **temporary** (one hour). Do not cache them or embed them on another site.
- Request URLs allow up to 2000 characters, so a long signed output URL goes in without trouble.
- If the image has to survive the master being taken down, upload the file with the File Upload API instead of linking it.

## Notion API versions

The `Notion-Version` header is required on every call and it is what pins the contract. The API evolves: the examples in the documentation use `2022-06-28`, and newer versions introduce the **data source** as the parent when creating a page inside a database, replacing `database_id`. If you pin a version and bump it without reviewing the request body, page creation is the first thing that breaks.

Pin the version in an environment variable, as in the snippet, and check the reference before migrating: https://developers.notion.com/reference/file-upload.

Request limits belong to Notion, not to SocialCutter: 180 requests per minute per connection on standard plans (an average of 3 per second) and 600 per minute on Business and Enterprise. Going over returns `429` with the `rate_limited` code, so honour `Retry-After` instead of retrying in a loop.

## Cost

- **1 use per destination** (platform and format) per SocialCutter request. Repeated destinations are not charged twice and failed items are refunded.
- Plans: €0 (3 uses/day), €3 (10/day), €9 (30/day) and €29 (100/day), all with API and MCP access.
- The Notion API is not billed per call: it counts against your Notion plan limits.

## Common errors

| Code | Origin | Meaning |
|---|---|---|
| 400 | SocialCutter | Invalid payload: unknown platform or format |
| 401 | SocialCutter | The `X-API-Key` header is missing or the key is wrong |
| 413 | SocialCutter | The master is over 5 MB |
| 429 | SocialCutter | Quota exhausted: check `GET /api/v1/wallet` |
| 400 `validation_error` | Notion | Malformed body, incompatible version or a parameter over its limit |
| 401 `unauthorized` | Notion | The integration token is not valid |
| 403 `restricted_resource` | Notion | The integration has no access to that page or database: share it with the integration |
| 404 `object_not_found` | Notion | The page, block or database id does not exist |
| 429 `rate_limited` | Notion | Too many requests: wait for the `Retry-After` value |

## Next steps

- Code path: [Process images with the SocialCutter API from Python](/en/guides/python/)
- Terminal: [Process images with the API from the terminal (curl)](/en/guides/curl/)
- No-code: [Automate image resizing with Zapier](/en/guides/zapier/) and [Automate image resizing with Make](/en/guides/make/)
- Big picture: [Automating social media images: the 4 real paths](/en/guides/automatizar-imagenes-redes-sociales/)
- SocialCutter API reference: https://docs.socialcutter.theboomer.dev
- Notion File Upload API: https://developers.notion.com/reference/file-upload