Saltar al contenido principal
SocialCutter

API y desarrollo

Procesa imágenes con la API de SocialCutter desde Python

Cliente de la API de SocialCutter en Python con requests: salud, plataformas, procesado por URL y fichero, lotes, historial, monedero y reintentos con backoff.

  • Python
  • requests
  • API
  • SocialCutter
  • procesar imagenes
  • multipart
  • API key

Requisitos

Instala requests y guarda las credenciales en variables de entorno:

pip install requests
export SOCIALCUTTER_API_URL="https://api.socialcutter.theboomer.dev"
export SOCIALCUTTER_API_KEY="sc_tu_clave"

La clave se crea en https://dash.socialcutter.theboomer.dev, en Perfil → API keys. Empieza por sc_ y se muestra una sola vez. La referencia completa de la API está en https://docs.socialcutter.theboomer.dev.

Cliente mínimo

Una sesión reutilizable evita repetir la cabecera y las credenciales en cada llamada:

import os
import requests

API_URL = os.environ.get("SOCIALCUTTER_API_URL", "https://api.socialcutter.theboomer.dev")
API_KEY = os.environ["SOCIALCUTTER_API_KEY"]  # nunca la escribas en el codigo

session = requests.Session()
session.headers.update({"X-API-Key": API_KEY})

def get(path, **params):
    r = session.get(f"{API_URL}{path}", params=params, timeout=30)
    r.raise_for_status()
    return r.json()

def post_json(path, payload):
    r = session.post(f"{API_URL}{path}", json=payload, timeout=60)
    r.raise_for_status()
    return r.json()

raise_for_status() lanza una excepción con el código HTTP cuando la respuesta no es 2xx, así no se procesan respuestas de error como si fueran válidas.

Salud y credenciales

# Publico: no requiere clave
print(get("/api/v1/health"))          # status, version, uptime, database

# Identidad de la cuenta autenticada
print(get("/api/v1/auth/me"))

# Usos disponibles
print(get("/api/v1/credits"))

/api/v1/health devuelve el estado del servicio y la conexión con la base de datos. Si /api/v1/auth/me responde 401, la clave falta, está mal formada o fue revocada.

Listar plataformas y formatos

data = get("/api/v1/platforms")
for plataforma, formatos in data["platforms"].items():
    for f in formatos:
        print(plataforma, f["format"], f"{f['width']}x{f['height']}", f["aspect_ratio"])

/platforms, /formats y /fit-modes son públicos. Úsalos para construir la lista de destinos sin codificar medidas a mano.

Procesar una imagen por URL

resp = post_json("/api/v1/images/process", {
    "source": {"type": "url", "value": "https://example.com/foto.jpg"},
    "destinations": [
        {"platform": "instagram", "format": "post"},
        {"platform": "tiktok", "format": "cover"},
    ],
})
print(resp["id"], resp["status"])

Procesar un fichero local (multipart)

with open("foto.jpg", "rb") as fh:
    r = session.post(
        f"{API_URL}/api/v1/images/process/upload",
        files={"file": ("foto.jpg", fh, "image/jpeg")},
        data={"destinations": '[{"platform":"linkedin","format":"post"}]'},
        timeout=60,
    )
    r.raise_for_status()
    resp = r.json()

En multipart el fichero va en el campo file y destinations es una cadena JSON en un campo del formulario. El límite de subida es 5 MB; por encima la API responde 413.

Procesar un lote

lote = post_json("/api/v1/images/batch", {
    "images": [
        {"source": {"type": "url", "value": "https://example.com/a.jpg"},
         "destinations": [{"platform": "instagram", "format": "post"}]},
        {"source": {"type": "url", "value": "https://example.com/b.jpg"},
         "destinations": [{"platform": "twitter", "format": "post"}]},
    ]
})

Leer la respuesta y descargar

La respuesta trae el identificador del trabajo en id, un status y una lista outputs, una entrada por destino con platform, format, url, width, height y size_bytes. Los tiempos van en metadata.

resp = post_json("/api/v1/images/process", {
    "source": {"type": "url", "value": "https://example.com/foto.jpg"},
    "destinations": [{"platform": "instagram", "format": "post"}],
})

for out in resp["outputs"]:
    print(out["platform"], out["format"], f"{out['width']}x{out['height']}")
    img = session.get(out["url"], stream=True, timeout=60)
    img.raise_for_status()
    with open(f"{out['platform']}-{out['format']}.webp", "wb") as fh:
        for chunk in img.iter_content(8192):
            fh.write(chunk)

Historial y monedero

# Ultimas 10 imagenes
print(get("/api/v1/history", limit=10))

# Solo las creadas desde la API
print(get("/api/v1/history", origin="api", limit=10))

# Cuota diaria, bolsa extra y saldo comprado
print(get("/api/v1/wallet"))

limit admite de 1 a 100 y skip sirve para paginar. El filtro origin distingue browser (dashboard) de api.

Errores y reintentos con backoff

Captura el código de estado y decide si merece un reintento. Los errores de cliente (4xx, salvo 429) no se reintentan: repetir la misma petición no la va a arreglar.

import time

def con_reintentos(fn, intentos=4, base=0.5):
    for i in range(intentos):
        try:
            return fn()
        except requests.HTTPError as e:
            code = e.response.status_code
            if code == 429 or code >= 500:
                if i == intentos - 1:
                    raise
                time.sleep(base * (2 ** i))   # 0.5, 1, 2, 4 s
                continue
            raise

con_reintentos(lambda: post_json("/api/v1/images/process", payload))

Para reintentos automáticos en toda la sesión, requests delega en urllib3.Retry, que aplica espera exponencial y respeta la cabecera Retry-After:

from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=4,
    backoff_factor=0.5,
    status_forcelist=(429, 500, 502, 503, 504),
    allowed_methods=frozenset(["GET", "POST"]),
    respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))

Patrón documentado en https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#urllib3.util.Retry. Los nombres y opciones pueden cambiar entre versiones de la librería; confírmalos en esa documentación.

Coste

  • 1 uso por destino (plataforma y formato) por petición.
  • Destinos repetidos en la misma petición no se cobran dos veces.
  • Los procesamientos fallidos se devuelven.

Errores típicos

CódigoSignificado
400Payload inválido: plataforma, formato o modo desconocido, JSON mal formado, o clave ya activa
401Credenciales ausentes, mal formadas, caducadas o revocadas
404Recurso no encontrado (id de imagen o de fichero)
413El fichero supera 5 MB
422Error de validación de la petición
429Cuota del monedero agotada
500Fallo de procesamiento; los usos de esa petición se devuelven

Siguientes pasos

Preguntas frecuentes

¿Qué librería necesito en Python?

requests, que instala con pip install requests. Para el multipart no hace falta nada más: requests arma el formulario si pasas files y data.

¿Dónde pongo la API key?

En la variable de entorno SOCIALCUTTER_API_KEY, nunca en el código. Se envía en cada petición en la cabecera X-API-Key.

¿Cómo subo una imagen que está en disco?

Con POST /api/v1/images/process/upload usando files={'file': ...} y destinations como campo de formulario en JSON. El límite es 5 MB.

¿Cómo se cobra un lote?

1 uso por destino (combinación de plataforma y formato) y por imagen. Un lote de 3 imágenes con 2 destinos cada una son 6 usos.

¿Por qué recibo 429?

Porque la cuota del monedero está agotada. Consulta GET /api/v1/credits y GET /api/v1/wallet antes de lotes grandes.