IA y agentes
Dropbox a SocialCutter: entrada y salida de imágenes
Lee la imagen nueva de una carpeta de Dropbox con files/list_folder y su cursor, procésala con SocialCutter y sube cada formato a otra carpeta con files/upload.
- Dropbox
- API v2
- files/list_folder
- cursor
- files/upload
- Python
- recorte centrado
Un flujo de entrada y una carpeta de salida
El patrón se repite: alguien deja el diseño en una carpeta compartida de Dropbox y de ahí tienen que salir las versiones de cada red. SocialCutter no entra en tu Dropbox ni lo vigila: recibe una imagen y una lista de destinos y devuelve una URL por salida. Mover los ficheros es cosa de tu script.
El circuito tiene cuatro pasos:
- Detectar el fichero nuevo en la carpeta de entrada.
- Traer el binario (o un enlace temporal).
- Procesarlo con SocialCutter.
- Subir cada salida a la carpeta de destino.
Si prefieres no programar, el mismo circuito se monta con nodos: Automatiza el recorte de imágenes con n8n o con Make.
Por qué pre-generar antes de subir
Dropbox guarda y sincroniza el fichero tal cual, no recorta. Si subes un solo maestro y lo reutilizas para todos los huecos, cada destino lo estirará o lo recortará a su manera. Generar las medidas por adelantado te da ficheros con la medida exacta y el recorte centrado decidido por ti.
| Hueco donde va la imagen | Destino SocialCutter | Medida |
|---|---|---|
| Cuadrado para una ficha o una miniatura | instagram post | 1080x1080 (1:1) |
| Vertical para un story de la carpeta | instagram story | 1080x1920 (9:16) |
| Apaisada estándar para una ficha | twitter post | 1200x675 (16:9) |
| Apaisada ancha para un banner | linkedin post | 1200x627 (1.91:1) |
| Cabecera estrecha de la carpeta | linkedin cover | 1128x191 (5.9:1) |
Los destinos y las medidas salen del catálogo público: GET /api/v1/platforms devuelve 6 plataformas y 13 destinos con su anchura, altura y proporción.
Listar la carpeta con files/list_folder
files/list_folder es un endpoint RPC: los argumentos van en el cuerpo JSON y la respuesta también es JSON.
POST https://api.dropboxapi.com/2/files/list_folder
Scope: files.metadata.read
El cuerpo acepta, entre otros, path (la carpeta; la cadena vacía es la raíz), recursive, include_deleted, limit (aproximado, hasta 2000 entradas) e include_non_downloadable_files.
La respuesta es un ListFolderResult con tres campos que importan:
entries: los ficheros y subcarpetas. Cada entrada traename,path_lower,path_displayy un.tagque distinguefile,folderydeleted.cursor: el testigo de paginación.has_more: si es verdadero, quedan entradas.
Cuando has_more es verdadero se sigue con el cursor:
POST https://api.dropboxapi.com/2/files/list_folder/continue
Scope: files.metadata.read
Ese endpoint recibe {"cursor": "..."} y devuelve otro ListFolderResult. El mismo cursor sirve para dos cosas: terminar la paginación de una carpeta grande y, en la siguiente vuelta del proceso, pedir solo los cambios desde la última consulta. Guárdalo entre ejecuciones y no vuelvas a recorrer la carpeta entera.
Dos avisos de la documentación oficial que ahorran depuraciones: si el cursor se invalida, la respuesta trae el error reset y hay que empezar de nuevo con files/list_folder; y si dos llamadas idénticas a list_folder coinciden en el tiempo, Dropbox puede responder un error de límite de peticiones, así que el reintento debe esperar a que termine la anterior.
Traer el binario con files/download
files/download es un endpoint de contenido: va por otro dominio y los argumentos viajan en la cabecera Dropbox-API-Arg (JSON serializado, con los caracteres no ASCII escapados), no en el cuerpo.
POST https://content.dropboxapi.com/2/files/download
Dropbox-API-Arg: {"path": "/Disenos/entrada/maestro.jpg"}
Scope: files.content.read
El cuerpo de la respuesta es el fichero, y los metadatos llegan en la cabecera Dropbox-API-Result. Es el camino cuando quieres que el binario viaje dentro de tu proceso, sin exponer ningún enlace.
files/download solo funciona con ficheros descargables: los documentos que Dropbox guarda como enlace externo hay que exportarlos antes. Trabaja con raster (JPG, PNG, WebP); si la carpeta es de documentos, no es tu caso.
Dos caminos para pasar la imagen a SocialCutter
Multipart, sin enlaces. El binario se envía a POST /api/v1/images/process/upload con la cabecera X-API-Key, el fichero en el campo file y la lista de destinos en el campo destinations como cadena JSON. El techo es 5 MB; por encima responde 413.
Por URL con enlace temporal. files/get_temporary_link es un RPC que recibe {"path": "..."} y devuelve link y metadata. Ese enlace caduca a las cuatro horas y después responde 410 Gone, así que se pide justo antes de la llamada y se pasa como source de tipo url:
{ "source": { "type": "url", "value": "<link temporal>" },
"destinations": [ { "platform": "instagram", "format": "post" } ] }
Es la vía más cómoda cuando quieres que el fichero no pase dos veces por tu proceso, pero el enlace queda expuesto durante esas cuatro horas.
Subir las salidas a otra carpeta
files/upload vuelve a ser un endpoint de contenido: el binario va en el cuerpo con Content-Type: application/octet-stream y los argumentos en Dropbox-API-Arg, que en este caso es un CommitInfo.
{ "path": "/Disenos/salida/maestro-instagram-post.webp",
"mode": "add",
"autorename": true,
"mute": true }
path: ruta de destino. Debe empezar por barra.mode:add(por defecto, falla si ya existe),overwrite, oupdatecon la revisión del fichero.autorename: si hay conflicto, Dropbox renombra en lugar de fallar. Útil en carpetas compartidas donde alguien puede haber dejado un fichero con el mismo nombre.mute: no notifica a los clientes de escritorio. Recomendable en procesos desatendidos que escriben muchos ficheros.strict_conflict: endurece cómo se comparan los conflictos.
No se debe usar este endpoint para ficheros de más de 150 MB; por encima hay que montar una sesión con upload_session/start. Las salidas de SocialCutter son imágenes de pocos cientos de kilobytes, así que no es un límite que te vaya a tocar.
Un nombre de destino útil conserva el original y añade la plataforma y el formato:
maestro-instagram-post.webp
maestro-twitter-post.webp
maestro-linkedin-post.webp
Requisitos de OAuth y de permisos
- Scopes. La app de la App Console se declara con permisos en la pestaña
Permissionsy quedan fijados en el token:files.metadata.read— listar la carpeta y seguir el cursor.files.content.read— descargar y pedir el enlace temporal.files.content.write— subir las salidas.
- App Folder o Full Dropbox. Si la app solo toca su propia carpeta
/apps, con el acceso App Folder es suficiente. Para leer y escribir en una carpeta que ya existe en la cuenta (el caso de esta guía) hay que elegir Full Dropbox. - Token de larga duración. Para procesos en segundo plano, cuando no hay nadie delante, conviene pedir el token con
token_access_type=offline: así la respuesta del endpoint de token trae unrefresh_tokencon el que renovar el token corto sin volver a autorizar. - Reautorización. Si el usuario revoca el acceso de la app desde su cuenta, las llamadas empiezan a devolver 401 y hay que volver a autorizar. Los scopes se pueden ampliar después con el parámetro
scopesde la URL de autorización. - Límite de subida a SocialCutter. 5 MB por imagen; por encima responde 413.
Snippet completo en Python
import json, os, requests
API = "https://api.socialcutter.theboomer.dev"
RPC = "https://api.dropboxapi.com/2"
CONTENIDO = "https://content.dropboxapi.com/2"
ENTRADA = "/Disenos/entrada"
SALIDA = "/Disenos/salida"
DESTINOS = [
{"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 listar(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 descargar(path):
# Los argumentos viajan en la cabecera, no en el cuerpo
r = dbx.post(f"{CONTENIDO}/files/download",
headers={"Dropbox-API-Arg": json.dumps({"path": path})},
timeout=120)
r.raise_for_status()
return r.content # el binario; los metadatos, en Dropbox-API-Result
def subir(path, data):
r = dbx.post(f"{CONTENIDO}/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 enlace_temporal(path):
r = dbx.post(f"{RPC}/files/get_temporary_link",
json={"path": path}, timeout=60)
r.raise_for_status()
return r.json()["link"] # caduca a las cuatro horas
resultado = listar(ENTRADA)
while True:
for entrada in resultado["entries"]:
if entrada[".tag"] != "file" or not entrada["name"].lower().endswith(".jpg"):
continue
binario = descargar(entrada["path_lower"])
r = sc.post(f"{API}/api/v1/images/process/upload",
headers={"X-API-Key": os.environ["SOCIALCUTTER_API_KEY"]},
files={"file": (entrada["name"], binario, "image/jpeg")},
data={"destinations": json.dumps(DESTINOS)}, timeout=120)
r.raise_for_status()
for salida in r.json()["outputs"]:
img = sc.get(salida["url"], timeout=120)
img.raise_for_status()
destino = f"{SALIDA}/{entrada['name']}-{salida['platform']}-{salida['format']}.webp"
print(subir(destino, img.content)["path_display"])
if not resultado["has_more"]:
break
resultado = listar(ENTRADA, cursor=resultado["cursor"])
Guarda el cursor de la última vuelta junto al estado del proceso: la siguiente ejecución puede retomar desde ahí en lugar de volver a mirar toda la carpeta.
Alternativa con enlace temporal y curl
# 1. Enlace temporal del maestro (caduca en 4 horas)
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":"/Disenos/entrada/maestro.jpg"}' | jq -r .link)
# 2. Genera los formatos
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\"}]}" \
> salida.json
jq -r '.outputs[] | .platform + " " + .format + " " + .url' salida.json
# 3. Sube la primera salida a la carpeta de destino
URL=$(jq -r '.outputs[0].url' salida.json)
NOMBRE=$(jq -r '"\(.outputs[0].platform)-\(.outputs[0].format).webp"' salida.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\":\"/Disenos/salida/$NOMBRE\",\"mode\":\"add\",\"autorename\":true}" \
--data-binary @"salida.webp" | jq -r '.path_display, .size'
Si vas a subir varios ficheros seguidos, recuerda que las llamadas de escritura compiten entre sí: espácialas o agrupa la subida de los ficheros de una misma imagen.
Coste
- 1 uso por destino (plataforma y formato) por petición a SocialCutter. Destinos repetidos no se cobran dos veces y los procesamientos fallidos se devuelven.
- Planes: 0 EUR (3 usos/día), 3 EUR (10/día), 9 EUR (30/día) y 29 EUR (100/día), todos con API y servidor MCP incluidos.
- La API de Dropbox no se cobra por llamada para apps normales, pero en equipos de Dropbox Business las subidas cuentan en el límite mensual de llamadas de transporte de datos.
Errores típicos
| Código | Origen | Qué pasa | Qué hacer |
|---|---|---|---|
| 400 | Dropbox | Cuerpo o cabecera mal formados, o JSON fuera de validación | Revisar el payload; reintentar no lo arregla |
| 401 | Dropbox | Token caducado, revocado o sin permisos suficientes | Refrescar el token con el refresh_token o volver a autorizar |
| 403 | Dropbox | La cuenta o el equipo no tiene acceso a esa llamada o a ese recurso | Revisar el scope y la ruta; la app puede estar en App Folder y no ver la carpeta |
| 409 | Dropbox | Error específico del endpoint: el detalle va en error y error_summary | Es el caso de path_not_found: alguien ha movido o borrado el fichero |
| 429 | Dropbox | Demasiadas llamadas o demasiadas escrituras simultáneas | Esperar lo que indique Retry-After o aplicar espera exponencial |
| 500 | Dropbox | Error interno, suele ser breve | Reintentar con espera, no en bucle rápido |
reset | Dropbox | Cursor invalidado | Empezar de nuevo con files/list_folder y guardar el cursor nuevo |
| 410 Gone | Enlace temporal | Han pasado más de cuatro horas desde que se pidió | Volver a llamar a files/get_temporary_link justo antes de usarlo |
| 401 | SocialCutter | Falta la cabecera X-API-Key o la clave no vale | Comprobar que el valor empieza por sc_ y sigue activo |
| 413 | SocialCutter | El maestro supera 5 MB | Reducir la imagen antes de enviarla |
| 429 | SocialCutter | Cuota del monedero agotada | Consultar GET /api/v1/wallet antes de lotes grandes |
Lo que SocialCutter no hace
El recorte es centrado y determinista: escala la imagen y recorta el exceso por igual a los dos lados. No analiza el contenido de la imagen para decidir qué conservar, no edita la foto (no retoca color, no quita fondos, no compone texto), no publica en redes sociales y no acepta ficheros de más de 5 MB. Genera las versiones con la medida exacta de cada destino y devuelve sus URLs: el intercambio con Dropbox y la publicación son de tu script.
Siguientes pasos
- El mismo circuito con Drive: Procesa imágenes de Google Drive con SocialCutter
- Código: Automatiza SocialCutter con Python y desde la terminal con curl
- Sin código: Automatiza el recorte de imágenes con n8n, con Make y con Zapier
- Panorama: Automatizar imágenes para redes sociales: los 4 caminos
- Documentación de la API de SocialCutter: https://docs.socialcutter.theboomer.dev
- Referencia HTTP de Dropbox: https://www.dropbox.com/developers/documentation/http/documentation
Preguntas frecuentes
¿Tengo que hacer pública la imagen para que SocialCutter la lea?
No. Si el fichero ya está en Dropbox, el camino corto es files/get_temporary_link: devuelve un enlace temporal que lleva su propio token dentro y se puede pasar como source de tipo url. Si prefieres no exponer ni un enlace temporal, descarga el binario con files/download y súbelo por multipart a /api/v1/images/process/upload.
¿Qué scopes necesita la app de Dropbox?
files.metadata.read para listar la carpeta y seguir el cursor, files.content.read para descargar el fichero o pedir su enlace temporal, y files.content.write para subir las salidas. Se marcan en la pestaña Permissions de la App Console.
¿Cómo vigilo la carpeta sin recorrerla entera cada vez?
Guarda el cursor que devuelve files/list_folder y llama a files/list_folder/continue en cada vuelta: solo llegan los cambios desde la última consulta. Si el cursor se invalida, la respuesta trae el error reset y hay que pedir uno nuevo con files/list_folder.
¿Cuánto dura el enlace temporal de Dropbox?
Cuatro horas. Después responde 410 Gone, así que pídelo justo antes de llamar a SocialCutter y no lo guardes en una cola. La propia documentación de Dropbox avisa de que no sirve para mostrar contenido directamente en el navegador.
¿Cuánto cuesta preparar los formatos de una imagen?
1 uso por destino, es decir por cada pareja de plataforma y formato. Una imagen con tres destinos consume 3 usos. Los planes incluyen la API y el servidor MCP.