# 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.
- URL: https://socialcutter.theboomer.dev/guias/whatsapp/
- Idioma: es
- Familia: mensajeria
- Actualizado: 2026-09-24
- Palabras clave: 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

| Pieza | Detalle |
|---|---|
| App de Meta | Tipo Business, con el producto WhatsApp añadido |
| WhatsApp Business Account | Con el número de teléfono **verificado** y registrado para la Cloud API |
| Token | Token de usuario del sistema (System User) con `whatsapp_business_messaging`; los tokens de usuario caducan en 24 h |
| Webhook | URL pública HTTPS que verifique `hub.verify_token` y reciba el campo `messages` |
| SocialCutter | Clave `sc_` creada en https://dash.socialcutter.theboomer.dev en **Perfil → API keys** |
| Versión de Graph API | Fija 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 |

```bash
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:

```bash
# 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 "file=@usuario.jpg;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`:

```python
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íntoma | Causa | Solución |
|---|---|---|
| `131047` (re-engagement) | La ventana de 24 horas se cerró | Envía una plantilla aprobada, no una imagen libre |
| `190` | Token inválido o caducado | Usa 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 minutos | Vuelve a pedir la URL y descarga al momento |
| Imagen no visible en el chat | Meta no puede descargar el enlace | Comprueba HTTPS, acceso público sin autenticación y que no sea un acortador |
| `413` de SocialCutter | Fichero de más de 5 MB | Recomprime antes de subir; el límite es por imagen |
| `401` de SocialCutter | Clave `sc_` ausente o revocada | Revisa `X-API-Key`; solo hay una clave activa por cuenta |
| Webhook sin verificar | `hub.verify_token` distinto | Repite 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

- Automatización: [Automatiza el recorte de imágenes con n8n](/guias/n8n/)
- Python: [Procesa imágenes con la API de SocialCutter desde Python](/guias/python/)
- Terminal: [Procesa imágenes con la API desde la terminal (curl)](/guias/curl/)
- Agentes: [Usa SocialCutter desde tu LLM o editor con MCP](/guias/mcp/)
- Documentación: https://docs.socialcutter.theboomer.dev