Skip to main content
SocialCutter

AI and agents

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.

  • 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 NotionSocialCutter destinationSize
Page cover or post headerfacebook post1200x630 (1.91:1)
Square image in a galleryinstagram post1080x1080 (1:1)
Vertical block, story or reelinstagram story1080x1920 (9:16)
Landscape thumbnail for a table or cardtwitter post1200x675 (16:9)
Wide page headertwitter header1500x500 (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

PathObjectWhat Notion storesWhen to use it
External URLexternalOnly the URL, no copy of the fileThe image lives on your CDN or on SocialCutter and needs no permissions
File Upload APIfile_uploadA copy in the workspace storageThe 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.

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

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

CodeOriginMeaning
400SocialCutterInvalid payload: unknown platform or format
401SocialCutterThe X-API-Key header is missing or the key is wrong
413SocialCutterThe master is over 5 MB
429SocialCutterQuota exhausted: check GET /api/v1/wallet
400 validation_errorNotionMalformed body, incompatible version or a parameter over its limit
401 unauthorizedNotionThe integration token is not valid
403 restricted_resourceNotionThe integration has no access to that page or database: share it with the integration
404 object_not_foundNotionThe page, block or database id does not exist
429 rate_limitedNotionToo many requests: wait for the Retry-After value

Next steps

Frequently asked questions

Does SocialCutter upload the images to Notion?

No. It generates the files at the exact size of each destination and returns one URL per output. Uploading to Notion is the caller's job: either an external URL or the File Upload API.

Does Notion copy the file when I use a URL?

No. A file object of type external is only the reference to that URL and Notion downloads nothing, so if the URL stops responding the image disappears from the page even though the block is still there.

How long do the URLs of Notion-hosted files last?

They are temporary: the reference states a one-hour lifetime and advises against caching them. Re-fetch the file object to refresh the URL instead of treating it as a CDN.

What value should I send in Notion-Version?

One fixed value, whichever matches your integration, and you change it when you migrate. The header is required and the API changes between versions: in recent ones, page creation inside a database goes through the data source.

What does preparing one page's formats cost?

1 use per destination, meaning each platform and format pair. Plans are €0, €3, €9 and €29 per month and all of them include API and MCP access.