Skip to main content
SocialCutter

AI and agents

Attach SocialCutter output images to Airtable records

Generate the formats with SocialCutter and attach the output URL to an attachment field through the Airtable API: direct flow, Python, curl and errors.

  • Airtable
  • attachment
  • API
  • personal access token
  • Python
  • curl
  • SocialCutter

Why the flow is direct in Airtable

An attachment field (multipleAttachments) accepts, on write, a list of objects with a url. Airtable downloads the file from that URL and keeps its own copy. Since SocialCutter returns one public URL per generated format, there is no need to upload binaries or build an intermediary: you generate, you attach, done.

The boundary is the same as in every other integration: SocialCutter generates images, it does not publish. The crop is centred, with no subject detection and no content analysis. Publishing or attaching is the caller’s job.

Why pre-generate before attaching

Airtable shows the attachment thumbnail, but it does not crop the image to the ratio each slot asks for. If you store a single master and reuse it for the gallery thumbnail, a reel brief and the record cover, every view scales it its own way.

Slot in the recordSocialCutter destinationSize
Square gallery thumbnailinstagram post1080x1080 (1:1)
Vertical image for a reel briefinstagram story1080x1920 (9:16)
Landscape card or gallery viewtwitter post1200x675 (16:9)
Record or view coverfacebook post1200x630 (1.91:1)
Wide banner headerlinkedin cover1128x191 (5.9:1)

Pre-generating those five costs 5 uses and comes back in a single request, so the record ends up complete with every size already resolved.

How the API writes to an attachment field

  • Write shape: an array of objects. url is enough; filename is optional but recommended so you control the attachment name.
  • Airtable downloads the file. The URL has to be reachable from outside, with no login and no expired signature, and it has to return an image Content-Type.
  • What you send is what stays. Attachments you leave out of the array are removed from the field. To keep them, send them again with their id: the object the read returns works as is.
  • On read, URLs come from v5.airtableusercontent.com and expire after two hours. They are for downloading, not for embedding on another site.
  • Plan limits: up to 5 GB per file, with per-base attachment storage ranging from 1 GB on Free to 1 TB on Enterprise. The full reference is at https://airtable.com/developers/web/api/field-model.

Create the personal access token

Go to https://airtable.com/create/tokens, add the data.records:write scope (plus schema.bases:read if you want to list the table’s fields) and grant access to the specific base. It goes in the Authorization: Bearer pat... header. Keep the token and the base id in environment variables, never in the code.

Direct flow with Python

import os
import requests

SC = "https://api.socialcutter.theboomer.dev"
BASE = os.environ["AIRTABLE_BASE_ID"]
TABLE = os.environ["AIRTABLE_TABLE_ID"]

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

at = requests.Session()
at.headers["Authorization"] = f"Bearer {os.environ['AIRTABLE_TOKEN']}"

# 1. Generate every format in one request
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": "twitter", "format": "post"},
    ],
}, timeout=60)
resp.raise_for_status()
outputs = resp.json()["outputs"]

# 2. Attach the outputs: Airtable downloads and rehosts them
files = [
    {"url": out["url"], "filename": f"{out['platform']}-{out['format']}.webp"}
    for out in outputs
]

r = at.patch(f"https://api.airtable.com/v0/{BASE}/{TABLE}/{os.environ['RECORD_ID']}",
             json={"fields": {"Attachments": files}}, timeout=60)
r.raise_for_status()
for att in r.json()["fields"]["Attachments"]:
    print(att["filename"], att["size"], att["type"])

# 3. Create a new record with the square image already attached
new = at.post(f"https://api.airtable.com/v0/{BASE}/{TABLE}", json={
    "records": [{"fields": {
        "Name": "September campaign",
        "Attachments": [{"url": outputs[0]["url"], "filename": "instagram-post.webp"}],
    }}],
}, timeout=60)
new.raise_for_status()

The batch endpoints take 10 records per request, and it pays to space them out so you stay under 5 requests per second per base.

Direct flow with curl

curl -s -X PATCH "https://api.airtable.com/v0/$BASE_ID/$TABLE_ID/$RECORD_ID" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"fields\":{\"Attachments\":[{\"url\":\"$OUTPUT_URL\",\"filename\":\"instagram-post.webp\"}]}}" \
  | python3 -m json.tool

Replace $BASE_ID with app..., $TABLE_ID with tbl... and $RECORD_ID with rec.... For the field you can use its fld... id instead of the name, which is the most stable choice if someone renames the column.

When Airtable cannot download the URL

If the field stays empty and the response talks about a failed upload, the usual cause is that Airtable could not download the file. Check that the URL is public, that it does not depend on a session, and that it returns the image with its Content-Type. The UI warning is normally “Couldn’t upload. Try adding again” with a 403 from Airtable’s upload domain behind it; support documents it at https://support.airtable.com/docs/attachment.

When the URL cannot be exposed, direct upload remains:

curl -s -X POST \
  "https://content.airtable.com/v0/$BASE_ID/$RECORD_ID/$FIELD_ID/uploadAttachment" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"contentType":"image/webp","file":"<base64>","filename":"instagram-post.webp"}'

That endpoint takes up to 5 MB per file, exactly the same ceiling as the master SocialCutter accepts, so the fallback covers the same range as the URL flow.

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 Airtable API is not billed per call, but it does count against your plan’s monthly call limit (1,000 calls per month on Free).

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
401AirtableToken missing, malformed or without access to that base
403AirtableAirtable could not download the attachment URL
404AirtableThe base, table or record does not exist
422AirtableUnknown field, or a value that does not fit the attachment type
429AirtableMore than 5 requests per second per base: wait around 30 seconds

Next steps

Frequently asked questions

Does Airtable keep the URL or the file?

The file. When you write a URL into an attachment field, Airtable downloads it and rehosts its own copy, so the SocialCutter URL does not have to stay online after the write.

Can I send the binary instead of a URL?

Yes, with the uploadAttachment endpoint, which takes the file as base64 up to 5 MB. Above that size you have to go through a public URL, which is exactly what SocialCutter returns.

What are the Airtable API limits?

5 requests per second per base on every plan, with a maximum of 10 records per request on the batch endpoints. Going over returns 429 and you should wait around 30 seconds before retrying.

What happens to attachments already in the field?

When you write, the list you send is what stays: attachments you leave out are removed. To keep them, send them again in the same array with their id, exactly as the read returned them.

What does preparing one record'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.