# 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.
- URL: https://socialcutter.theboomer.dev/en/guides/airtable/
- Idioma: en
- Familia: ia
- Actualizado: 2026-09-24
- Palabras clave: 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 record | SocialCutter destination | Size |
|---|---|---|
| Square gallery thumbnail | `instagram` `post` | 1080x1080 (1:1) |
| Vertical image for a reel brief | `instagram` `story` | 1080x1920 (9:16) |
| Landscape card or gallery view | `twitter` `post` | 1200x675 (16:9) |
| Record or view cover | `facebook` `post` | 1200x630 (1.91:1) |
| Wide banner header | `linkedin` `cover` | 1128x191 (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

```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

```bash
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:

```bash
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

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

## Next steps

- Code path: [Process images with the SocialCutter API from Python](/en/guides/python/) and [from the terminal with curl](/en/guides/curl/)
- No-code: [Automate image resizing with n8n](/en/guides/n8n/), [with Zapier](/en/guides/zapier/) and [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
- Airtable attachment field: https://airtable.com/developers/web/api/field-model