# Dropbox to SocialCutter: images in, formats out
> Watch a Dropbox folder with files/list_folder and its cursor, process each image with SocialCutter and upload every size to a second folder.
- URL: https://socialcutter.theboomer.dev/en/guides/dropbox/
- Idioma: en
- Familia: ia
- Actualizado: 2026-09-24
- Palabras clave: Dropbox, API v2, files/list_folder, cursor, files/upload, Python, centred crop
## One folder in, one folder out

The pattern repeats everywhere: someone drops the design in a shared Dropbox folder and the versions for each network have to come out of it. SocialCutter does not reach into your Dropbox and does not watch it: hand it an image and a list of destinations and it returns one URL per output. Moving the files is your script's job.

The circuit has four steps:

1. Detect the new file in the input folder.
2. Fetch the binary (or a temporary link).
3. Process it with SocialCutter.
4. Upload each output to the destination folder.

If you would rather not write code, the same circuit is built from nodes: [Automate image resizing with n8n](/en/guides/n8n/) or [with Make](/en/guides/make/).

## Why pre-generate before uploading

Dropbox stores and syncs the file as it is; it does not crop. If you upload a single master and reuse it for every slot, each destination will stretch or trim it its own way. Generating the sizes up front hands you files at the exact measurement with a **centred** crop that you decided.

| Where the image goes | SocialCutter destination | Size |
|---|---|---|
| Square for a card or a thumbnail | `instagram` `post` | 1080x1080 (1:1) |
| Portrait for a folder story | `instagram` `story` | 1080x1920 (9:16) |
| Standard landscape for a card | `twitter` `post` | 1200x675 (16:9) |
| Wide landscape for a banner | `linkedin` `post` | 1200x627 (1.91:1) |
| Narrow folder header | `linkedin` `cover` | 1128x191 (5.9:1) |

Destinations and sizes come from the public catalogue: `GET /api/v1/platforms` returns 6 platforms and 13 destinations with their width, height and aspect ratio.

## Listing the folder with files/list_folder

`files/list_folder` is an RPC endpoint: the arguments travel as JSON in the request body and so does the response.

```
POST https://api.dropboxapi.com/2/files/list_folder
Scope: files.metadata.read
```

The body accepts, among others, `path` (the folder; the empty string is the root), `recursive`, `include_deleted`, `limit` (approximate, up to 2000 entries) and `include_non_downloadable_files`.

The response is a `ListFolderResult` with three fields that matter:

- `entries`: the files and subfolders. Each entry carries `name`, `path_lower`, `path_display` and a `.tag` distinguishing `file`, `folder` and `deleted`.
- `cursor`: the pagination token.
- `has_more`: if true, entries are still pending.

When `has_more` is true you continue with the cursor:

```
POST https://api.dropboxapi.com/2/files/list_folder/continue
Scope: files.metadata.read
```

That endpoint takes `{"cursor": "..."}` and returns another `ListFolderResult`. The same cursor does two jobs: finishing the pagination of a large folder and, on the next pass, asking for **only what changed since the last query**. Store it between runs instead of walking the whole folder again.

Two warnings from the official documentation save debugging time: if the cursor is invalidated the response carries the `reset` error and you start over with `files/list_folder`; and if two identical `list_folder` calls overlap, Dropbox may answer with a rate limit error, so the retry must wait for the previous request to finish.

## Fetching the binary with files/download

`files/download` is a content endpoint: it lives on another domain and its arguments travel in the `Dropbox-API-Arg` header (serialised JSON, with non-ASCII characters escaped), not in the body.

```
POST https://content.dropboxapi.com/2/files/download
Dropbox-API-Arg: {"path": "/Designs/input/master.jpg"}
Scope: files.content.read
```

The response body **is the file**, and the metadata arrives in the `Dropbox-API-Result` header. This is the path to take when you want the binary to travel inside your own process without exposing any link.

`files/download` only works on downloadable files: documents that Dropbox keeps as an external link have to be exported first. SocialCutter works with raster images (JPG, PNG, WebP); a folder full of documents is not your case.

## Two ways to hand the image to SocialCutter

**Multipart, no links.** The binary goes to `POST /api/v1/images/process/upload` with the `X-API-Key` header, the file in the `file` field and the destination list in the `destinations` field as a JSON string. The ceiling is 5 MB; above that it answers 413.

**By URL with a temporary link.** `files/get_temporary_link` is an RPC that takes `{"path": "..."}` and returns `link` and `metadata`. That link expires after four hours and then answers 410 Gone, so you request it right before the call and pass it as a `source` of type `url`:

```json
{ "source": { "type": "url", "value": "<temporary link>" },
  "destinations": [ { "platform": "instagram", "format": "post" } ] }
```

It is the handiest route when you do not want the file to pass through your process twice, but the link stays exposed for those four hours.

## Uploading the outputs to another folder

`files/upload` is a content endpoint again: the binary goes in the body with `Content-Type: application/octet-stream` and the arguments go in `Dropbox-API-Arg`, which here is a `CommitInfo`.

```json
{ "path": "/Designs/output/master-instagram-post.webp",
  "mode": "add",
  "autorename": true,
  "mute": true }
```

- `path`: target path. It must start with a slash.
- `mode`: `add` (the default, fails if the file already exists), `overwrite`, or `update` with the file revision.
- `autorename`: on a conflict, Dropbox renames instead of failing. Useful in shared folders where someone may have left a file with the same name.
- `mute`: does not notify desktop clients. Worth setting on unattended processes that write many files.
- `strict_conflict`: hardens how conflicts are compared.

This endpoint must not be used for files larger than 150 MB; above that you set up a session with `upload_session/start`. SocialCutter outputs are images of a few hundred kilobytes, so that ceiling will not come up.

A useful target name keeps the original and appends the platform and format:

```
master-instagram-post.webp
master-twitter-post.webp
master-linkedin-post.webp
```

## OAuth and permission requirements

- **Scopes.** An App Console app declares its permissions on the `Permissions` tab and they are fixed into the token:
  - `files.metadata.read` — list the folder and follow the cursor.
  - `files.content.read` — download and request the temporary link.
  - `files.content.write` — upload the outputs.
- **App Folder or Full Dropbox.** If the app only touches its own `/apps` folder, App Folder access is enough. To read and write a folder that already exists in the account (this guide's case) you need Full Dropbox.
- **Long-lived token.** For background processes with nobody at the keyboard, request the token with `token_access_type=offline`: the token endpoint response then carries a `refresh_token` you use to mint new short-lived tokens without asking the user to authorise again.
- **Re-authorisation.** If the user revokes the app's access from their account, calls start answering 401 and you have to authorise again. Scopes can be widened later with the `scopes` parameter on the authorisation URL.
- **Upload limit to SocialCutter.** 5 MB per image; above that it answers 413.

## Full Python snippet

```python
import json, os, requests

API = "https://api.socialcutter.theboomer.dev"
RPC = "https://api.dropboxapi.com/2"
CONTENT = "https://content.dropboxapi.com/2"
INPUT = "/Designs/input"
OUTPUT = "/Designs/output"
DESTINATIONS = [
    {"platform": "instagram", "format": "post"},
    {"platform": "twitter", "format": "post"},
]

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

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

def list_folder(path, cursor=None):
    if cursor is None:
        url, body = f"{RPC}/files/list_folder", {"path": path}
    else:
        url, body = f"{RPC}/files/list_folder/continue", {"cursor": cursor}
    r = dbx.post(url, json=body, timeout=60)
    r.raise_for_status()
    return r.json()

def download(path):
    # The arguments travel in the header, not in the body
    r = dbx.post(f"{CONTENT}/files/download",
                 headers={"Dropbox-API-Arg": json.dumps({"path": path})},
                 timeout=120)
    r.raise_for_status()
    return r.content          # the binary; metadata lands in Dropbox-API-Result

def upload(path, data):
    r = dbx.post(f"{CONTENT}/files/upload",
                 headers={"Dropbox-API-Arg": json.dumps({
                     "path": path, "mode": "add",
                     "autorename": True, "mute": True}),
                     "Content-Type": "application/octet-stream"},
                 data=data, timeout=120)
    r.raise_for_status()
    return r.json()

def temporary_link(path):
    r = dbx.post(f"{RPC}/files/get_temporary_link",
                 json={"path": path}, timeout=60)
    r.raise_for_status()
    return r.json()["link"]   # expires after four hours

result = list_folder(INPUT)
while True:
    for entry in result["entries"]:
        if entry[".tag"] != "file" or not entry["name"].lower().endswith(".jpg"):
            continue

        binary = download(entry["path_lower"])
        r = sc.post(f"{API}/api/v1/images/process/upload",
                    headers={"X-API-Key": os.environ["SOCIALCUTTER_API_KEY"]},
                    files={"file": (entry["name"], binary, "image/jpeg")},
                    data={"destinations": json.dumps(DESTINATIONS)}, timeout=120)
        r.raise_for_status()

        for out in r.json()["outputs"]:
            img = sc.get(out["url"], timeout=120)
            img.raise_for_status()
            target = f"{OUTPUT}/{entry['name']}-{out['platform']}-{out['format']}.webp"
            print(upload(target, img.content)["path_display"])

    if not result["has_more"]:
        break
    result = list_folder(INPUT, cursor=result["cursor"])
```

Store the `cursor` from the last pass next to your process state: the next run can resume from there instead of scanning the whole folder again.

## The temporary-link route with curl

```bash
# 1. Temporary link to the master (expires in 4 hours)
LINK=$(curl -s -X POST "https://api.dropboxapi.com/2/files/get_temporary_link" \
  -H "Authorization: Bearer $DROPBOX_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"path":"/Designs/input/master.jpg"}' | jq -r .link)

# 2. Generate the formats
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
  -H "X-API-Key: $SOCIALCUTTER_API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"source\":{\"type\":\"url\",\"value\":\"$LINK\"},\"destinations\":[{\"platform\":\"instagram\",\"format\":\"post\"}]}" \
  > out.json

jq -r '.outputs[] | .platform + " " + .format + " " + .url' out.json

# 3. Upload the first output to the target folder
URL=$(jq -r '.outputs[0].url' out.json)
NAME=$(jq -r '"\(.outputs[0].platform)-\(.outputs[0].format).webp"' out.json)
curl -s -X POST "https://content.dropboxapi.com/2/files/upload" \
  -H "Authorization: Bearer $DROPBOX_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  -H "Dropbox-API-Arg: {\"path\":\"/Designs/output/$NAME\",\"mode\":\"add\",\"autorename\":true}" \
  --data-binary @out.webp | jq -r '.path_display, .size'
```

If you are uploading several files in a row, remember that write calls compete with each other: space them out or group the uploads belonging to the same image.

## Cost

- **1 use per destination** (platform and format) per SocialCutter request. Repeated destinations are not charged twice and failed processings are refunded.
- Plans: Free 3 uses/day, 3 EUR 10/day, 9 EUR 30/day and 29 EUR 100/day, all with the API and the MCP server included.
- The Dropbox API charges nothing per call for normal apps, but on Dropbox Business teams uploads count against the monthly data transport limit.

## Common errors

| Code | Source | What happens | What to do |
|---|---|---|---|
| 400 | Dropbox | Malformed body or header, or JSON failing validation | Fix the payload; retrying will not help |
| 401 | Dropbox | Token expired, revoked or short on permissions | Refresh it with the `refresh_token` or authorise again |
| 403 | Dropbox | The account or team cannot reach that call or resource | Check the scope and the path; the app may be in App Folder and cannot see the folder |
| 409 | Dropbox | Endpoint-specific error: the detail sits in `error` and `error_summary` | This is the `path_not_found` case: someone moved or deleted the file |
| 429 | Dropbox | Too many calls or too many simultaneous writes | Wait for the seconds in `Retry-After`, or apply exponential backoff |
| 500 | Dropbox | Internal error, usually brief | Retry with backoff, not in a tight loop |
| `reset` | Dropbox | Cursor invalidated | Start again with `files/list_folder` and keep the new cursor |
| 410 Gone | Temporary link | More than four hours since it was issued | Call `files/get_temporary_link` again right before using it |
| 401 | SocialCutter | Missing `X-API-Key` header or an invalid key | Check the value starts with `sc_` and is still active |
| 413 | SocialCutter | The master exceeds 5 MB | Shrink the image before sending it |
| 429 | SocialCutter | Wallet quota exhausted | Check `GET /api/v1/wallet` before large batches |

## What SocialCutter does not do

The crop is **centred and deterministic**: it scales the image and trims the excess equally on both sides. It does not read the content of the image to decide what to keep, it does not edit the photo (no colour work, no background removal, no text compositing), it does not post to social networks and it does not accept files over 5 MB. It produces the versions at the exact size of each destination and returns their URLs: the exchange with Dropbox and the publishing are your script's job.

## Next steps

- The same circuit with Drive: [Process Google Drive images with SocialCutter](/en/guides/google-drive/)
- Code: [Automate SocialCutter with Python](/en/guides/python/) and [from the terminal with curl](/en/guides/curl/)
- No code: [Automate image resizing with n8n](/en/guides/n8n/), [with Make](/en/guides/make/) and [with Zapier](/en/guides/zapier/)
- Big picture: [Automating social media images: the 4 routes](/en/guides/automatizar-imagenes-redes-sociales/)
- SocialCutter API docs: https://docs.socialcutter.theboomer.dev
- Dropbox HTTP reference: https://www.dropbox.com/developers/documentation/http/documentation