Saltar al contenido principal
SocialCutter

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:

  1. Detectar el fichero nuevo en la carpeta de entrada.
  2. Traer el binario (o un enlace temporal).
  3. Procesarlo con SocialCutter.
  4. 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 imagenDestino SocialCutterMedida
Cuadrado para una ficha o una miniaturainstagram post1080x1080 (1:1)
Vertical para un story de la carpetainstagram story1080x1920 (9:16)
Apaisada estándar para una fichatwitter post1200x675 (16:9)
Apaisada ancha para un bannerlinkedin post1200x627 (1.91:1)
Cabecera estrecha de la carpetalinkedin cover1128x191 (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 trae name, path_lower, path_display y un .tag que distingue file, folder y deleted.
  • 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, o update con 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 Permissions y 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 un refresh_token con 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 scopes de 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ódigoOrigenQué pasaQué hacer
400DropboxCuerpo o cabecera mal formados, o JSON fuera de validaciónRevisar el payload; reintentar no lo arregla
401DropboxToken caducado, revocado o sin permisos suficientesRefrescar el token con el refresh_token o volver a autorizar
403DropboxLa cuenta o el equipo no tiene acceso a esa llamada o a ese recursoRevisar el scope y la ruta; la app puede estar en App Folder y no ver la carpeta
409DropboxError específico del endpoint: el detalle va en error y error_summaryEs el caso de path_not_found: alguien ha movido o borrado el fichero
429DropboxDemasiadas llamadas o demasiadas escrituras simultáneasEsperar lo que indique Retry-After o aplicar espera exponencial
500DropboxError interno, suele ser breveReintentar con espera, no en bucle rápido
resetDropboxCursor invalidadoEmpezar de nuevo con files/list_folder y guardar el cursor nuevo
410 GoneEnlace temporalHan pasado más de cuatro horas desde que se pidióVolver a llamar a files/get_temporary_link justo antes de usarlo
401SocialCutterFalta la cabecera X-API-Key o la clave no valeComprobar que el valor empieza por sc_ y sigue activo
413SocialCutterEl maestro supera 5 MBReducir la imagen antes de enviarla
429SocialCutterCuota del monedero agotadaConsultar 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

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.