Saltar al contenido principal
SocialCutter

Mensajeria y bots

Bot de WhatsApp que adapta imágenes con SocialCutter

Recibe una imagen o un enlace por la WhatsApp Cloud API, genera los formatos con SocialCutter y responde con la imagen o el enlace, con la ventana de 24 h.

  • WhatsApp
  • Cloud API
  • Meta
  • webhook
  • plantillas
  • SocialCutter
  • Python

Qué hace este bot

El usuario manda una foto o pega un enlace en WhatsApp y recibe de vuelta la imagen adaptada a las medidas de la red que haya pedido, además de un enlace de descarga. La conversación va por la WhatsApp Cloud API de Meta y el recorte lo hace SocialCutter.

Conviene decirlo claro: SocialCutter no publica en redes sociales. Genera los ficheros con el encuadre centrado —sin detección de sujeto ni modelos de por medio— y devuelve una URL pública por formato. El envío por WhatsApp lo hace tu bot.

Requisitos

PiezaDetalle
App de MetaTipo Business, con el producto WhatsApp añadido
WhatsApp Business AccountCon el número de teléfono verificado y registrado para la Cloud API
TokenToken de usuario del sistema (System User) con whatsapp_business_messaging; los tokens de usuario caducan en 24 h
WebhookURL pública HTTPS que verifique hub.verify_token y reciba el campo messages
SocialCutterClave sc_ creada en https://dash.socialcutter.theboomer.dev en Perfil → API keys
Versión de Graph APIFija una versión (v21.0 y posteriores en los ejemplos oficiales) y revísala en la documentación de Meta; las versiones se retiran con el tiempo
export WA_TOKEN="EAAG...token-de-sistema"
export WA_PHONE_ID="123456789012345"       # Phone number ID, no el numero de telefono
export WA_VERIFY_TOKEN="un-secreto-tuyo"
export SOCIALCUTTER_API_KEY="sc_tu_clave"

Documentación oficial: https://developers.facebook.com/docs/whatsapp/cloud-api/ y su guía de envío de mensajes en https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages.

El flujo, paso a paso

  1. Entra el mensaje. Meta hace POST a tu webhook con messages[0].image.id si es una foto, o messages[0].text.body si es texto con un enlace. Responde 200 y procesa en segundo plano.
  2. Consigue el fichero. Con GET https://graph.facebook.com/{version}/{media-id} obtienes una URL temporal que vive solo 5 minutos y cuya descarga exige el token en la cabecera; el media id que llega por webhook se puede descargar durante 7 días.
  3. Genera los formatos. El fichero se envía a POST /api/v1/images/process/upload (multipart, máximo 5 MB) con destinations y options.
  4. Responde. Un mensaje de tipo image con image.link a la URL de SocialCutter (Meta la descarga y la muestra), o un mensaje de texto con el enlace. Fuera de la ventana de 24 horas solo valen plantillas aprobadas.

La ventana de 24 horas y las plantillas

Cuando el usuario escribe, se abre una ventana de atención al cliente de 24 horas: dentro puedes mandar mensajes libres (text, image, document, interactive) y el reloj se reinicia con cada mensaje suyo. Pasadas 24 horas sin respuesta, solo puedes enviar plantillas aprobadas; una imagen suelta se rechaza. Meta guarda en caché un link durante 10 minutos: si reenvías la misma URL y quieres que la vuelva a descargar, añade un parámetro de consulta distinto.

Referencia: https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages y las plantillas en https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates.

Código: recibir, generar y responder

Primero, el paso de Meta a SocialCutter:

# 1. URL temporal del medio que acaba de mandar el usuario (vive 5 minutos)
curl -s "https://graph.facebook.com/v21.0/$MEDIA_ID" \
  -H "Authorization: Bearer $WA_TOKEN" | jq -r '.url' > media_url.txt

# 2. Descarga con el token: la URL sola no devuelve el fichero
curl -s -o usuario.jpg "$(cat media_url.txt)" -H "Authorization: Bearer $WA_TOKEN"

# 3. Genera los formatos (1 uso por destino; maximo 5 MB)
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process/upload" \
  -H "X-API-Key: $SOCIALCUTTER_API_KEY" \
  -F "[email protected];type=image/jpeg" \
  -F 'destinations=[{"platform":"instagram","format":"post"},{"platform":"linkedin","format":"post"}]' \
  -F 'options={"format":"jpg","quality":88}' > salida.json

jq '.image_id // .id, (.outputs[] | {platform, format, width, height, url})' salida.json

# 4. Responde con la imagen ya adaptada (campo link, HTTPS y publico)
curl -s -X POST "https://graph.facebook.com/v21.0/$WA_PHONE_ID/messages" \
  -H "Authorization: Bearer $WA_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "34600111222",
    "type": "image",
    "image": {"link": "https://api.socialcutter.theboomer.dev/.../instagram-post.jpg",
              "caption": "Instagram post 1080x1080"}
  }'

Flujo completo en Python con requests:

import json
import os

import requests

WA_TOKEN = os.environ["WA_TOKEN"]
WA_PHONE_ID = os.environ["WA_PHONE_ID"]
SC_KEY = os.environ["SOCIALCUTTER_API_KEY"]
VERSION = "v21.0"
SC_URL = "https://api.socialcutter.theboomer.dev"

ORDENES = {
    "ig": [{"platform": "instagram", "format": "post"}],
    "story": [{"platform": "instagram", "format": "story"}],
    "li": [{"platform": "linkedin", "format": "post"}],
}


def descargar_media(media_id):
    url = requests.get(f"https://graph.facebook.com/{VERSION}/{media_id}",
                       headers={"Authorization": f"Bearer {WA_TOKEN}"}, timeout=30).json()["url"]
    r = requests.get(url, headers={"Authorization": f"Bearer {WA_TOKEN}"}, timeout=60)
    r.raise_for_status()
    return r.content


def generar(imagen, destinos):
    r = requests.post(
        f"{SC_URL}/api/v1/images/process/upload",
        headers={"X-API-Key": SC_KEY},
        files={"file": ("entrada.jpg", imagen, "image/jpeg")},
        data={"destinations": json.dumps(destinos),
              "options": json.dumps({"format": "jpg", "quality": 88})},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()["outputs"]


def responder(to, salidas):
    for out in salidas:
        r = requests.post(
            f"https://graph.facebook.com/{VERSION}/{WA_PHONE_ID}/messages",
            headers={"Authorization": f"Bearer {WA_TOKEN}"},
            json={"messaging_product": "whatsapp", "to": to, "type": "image",
                  "image": {"link": out["url"],
                            "caption": f"{out['platform']} {out['format']} · {out['width']}x{out['height']}"}},
            timeout=60,
        )
        r.raise_for_status()
        if r.json().get("error"):
            print("Meta rechazo el envio:", r.json()["error"])


def on_message(msg):
    to = msg["from"]
    if "image" in msg:
        imagen = descargar_media(msg["image"]["id"])
    elif "text" in msg:                       # enlace pegado en el chat
        imagen = requests.get(msg["text"]["body"].strip(), timeout=60).content
    else:
        return
    destinos = ORDENES.get(msg.get("button", {}).get("text", "ig"), ORDENES["ig"])
    responder(to, generar(imagen, destinos))

Limita los destinos por mensaje: cada uno es 1 uso. Con enlaces, valida antes que sean http/https y de un dominio de confianza.

Errores típicos

Código o síntomaCausaSolución
131047 (re-engagement)La ventana de 24 horas se cerróEnvía una plantilla aprobada, no una imagen libre
190Token inválido o caducadoUsa un token de usuario del sistema y rótalo
100 con “media”El media id caducó (7 días) o la URL temporal pasó de 5 minutosVuelve a pedir la URL y descarga al momento
Imagen no visible en el chatMeta no puede descargar el enlaceComprueba HTTPS, acceso público sin autenticación y que no sea un acortador
413 de SocialCutterFichero de más de 5 MBRecomprime antes de subir; el límite es por imagen
401 de SocialCutterClave sc_ ausente o revocadaRevisa X-API-Key; solo hay una clave activa por cuenta
Webhook sin verificarhub.verify_token distintoRepite la verificación con el token correcto y responde en texto plano el hub.challenge

Privacidad

Las salidas de SocialCutter se sirven como URLs públicas y sin autenticación: cualquiera con el enlace puede abrir la imagen, así que trátalas como material público. Las imágenes del usuario se descargan de Meta y viajan a la API de SocialCutter para generar los recortes, y Meta aplica su propia política de retención: revísala antes de procesar fotos de clientes y avisa en el chat de que la imagen pasa por un servicio externo.

Coste

  • 1 uso por destino (plataforma y formato) por imagen; los destinos repetidos no se cobran dos veces.
  • Los procesamientos fallidos se devuelven.
  • Todos los planes incluyen API y MCP: Free 3 usos/día, Basic 10, Pro 30, Agency 100.

Siguientes pasos

Preguntas frecuentes

¿SocialCutter publica en redes sociales?

No. SocialCutter genera cada formato con el encuadre centrado y devuelve una URL pública por salida. Enviar la imagen al usuario por WhatsApp lo hace tu bot con la Cloud API; publicar en una red social es otro paso, con la API de esa plataforma.

¿Cómo recibo las imágenes que manda el usuario?

El webhook de la Cloud API entrega un mensaje de tipo image con un media id. Se pide la URL temporal con GET /{media-id}, que solo vive 5 minutos y exige el token en la descarga.

¿Puedo responder con la imagen directamente?

Sí, con un mensaje de tipo image y el campo image.link apuntando a la URL pública de SocialCutter. Meta descarga el fichero, así que la URL debe ser HTTPS, pública y sin acortadores; también acepta image.id si subes antes el fichero al endpoint de medios de Meta.

¿Por qué Meta rechaza mi respuesta?

Lo más habitual es que la ventana de atención al cliente de 24 horas se haya cerrado: en ese caso solo se pueden enviar plantillas aprobadas. Otros motivos son un enlace que Meta no puede descargar o un token caducado.

¿Cuánto cuesta procesar una imagen?

1 uso por destino, es decir por cada combinación de plataforma y formato. Una imagen con Instagram post y LinkedIn post son 2 usos, y los destinos repetidos en la misma petición no se cobran dos veces.