# Integra SocialCutter con la Admin API de Shopify
> Flujo con la Admin API de Shopify: sube el maestro a Files y al producto, asigna la salida 1:1 y controla version, scopes y errores. Snippet en Node con fetch.
- URL: https://socialcutter.theboomer.dev/guias/shopify/
- Idioma: es
- Familia: cms
- Actualizado: 2026-09-24
- Palabras clave: Shopify, Admin API, GraphQL, write_products, write_files, imagenes de producto, Node
## Por qué una sola imagen maestra

Una ficha de producto vive en varios sitios: la rejilla del catálogo, la página del producto, la vista móvil y la colección. Cada hueco pide una proporción distinta; recortar una copia por hueco deja ficheros duplicados.

El flujo es: **un maestro entra, SocialCutter devuelve cada medida, Shopify recibe la que toca en cada hueco**. El recorte de `cover` (modo por defecto) es **centrado**: escala y recorta el exceso por igual a los dos lados.

| Uso en la tienda | Destino SocialCutter | Medida |
|---|---|---|
| Imagen principal del producto | `instagram` `post` | 1080x1080 (1:1) |
| Segunda imagen de producto | `instagram` `story` | 1080x1920 (9:16) |
| Banner de colección | `facebook` `post` | 1200x630 (1.91:1) |
| Cabecera de la tienda | `twitter` `header` | 1500x500 (3:1) |
| Miniatura de vídeo | `youtube` `thumbnail` | 1280x720 (16:9) |

Los formatos y medidas salen de `GET /api/v1/platforms`, que es público.

> Nota: el catálogo de SocialCutter no incluye un formato 4:5. Lo vertical es 9:16 (1080x1920) y lo cuadrado es 1:1 (1080x1080). Usa 1:1 como imagen principal y 9:16 como segunda; si necesitas 4:5 exacto, recorta fuera de SocialCutter.

## Antes de empezar: versión y scopes

**Versión de la API.** La Admin API se versiona en la URL y cada versión vive un año. Al escribir esta guía la documentación marca `2026-07` como la última. Fija una versión en tus llamadas:

```
https://tu-tienda.myshopify.com/admin/api/2026-07/graphql.json
```

Shopify publica una versión nueva cada trimestre y retira las antiguas. Antes de subir, revisa https://shopify.dev/docs/api/versioning y comprueba que los argumentos que usas no han cambiado.

**Scopes.** Una app pública o personalizada pide los permisos en su configuración y los recibe al instalarse:

| Scope | Para qué lo necesitas aquí |
|---|---|
| `write_products` | `productUpdate` con media y `productVariantsBulkUpdate` |
| `write_files` | `fileCreate`, para crear ficheros en la página Files |
| `read_products` | Solo si únicamente lees productos |

Los scopes se conceden en la instalación, no por petición: consulta `currentAppInstallation` y su campo `accessScopes` para ver los reales. Docs: https://shopify.dev/docs/api/usage/access-scopes

**Autenticación.** El token va en la cabecera `X-Shopify-Access-Token`. Referencia: https://shopify.dev/docs/api/usage/authentication

```bash
export SHOP="tu-tienda.myshopify.com"
export SHOPIFY_TOKEN="shpat_..."
export API_VERSION="2026-07"
export SC_KEY="sc_tu_clave"
```

## 1. Procesar el maestro con SocialCutter

```bash
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
  -H "X-API-Key: $SC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "type": "url", "value": "https://tu-cdn.com/maestro.jpg" },
    "destinations": [
      { "platform": "instagram", "format": "post" },
      { "platform": "instagram", "format": "story" }
    ]
  }' > sc.json

jq '.image_id, (.outputs[] | {url, platform, format})' sc.json
```

Cada salida es una URL pública. `fileCreate` acepta URLs, así que no hace falta descargar nada.

## 2. Cliente GraphQL en Node

Todas las mutaciones de esta guía son de la **GraphQL Admin API**. Un cliente mínimo con `fetch`:

```js
const SHOP = 'tu-tienda.myshopify.com'
const VERSION = '2026-07'
const TOKEN = process.env.SHOPIFY_TOKEN

async function gql(query, variables = {}) {
  const res = await fetch(`https://${SHOP}/admin/api/${VERSION}/graphql.json`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Shopify-Access-Token': TOKEN
    },
    body: JSON.stringify({ query, variables })
  })
  const json = await res.json()
  if (json.errors) throw new Error(JSON.stringify(json.errors))
  return json.data
}
```

`json.errors` son errores de GraphQL (consulta mal formada, campo inexistente, throttle); los fallos de negocio llegan aparte en `userErrors`. Hay que comprobar los dos.

## 3. Crear los ficheros en Files

`fileCreate` admite varias entradas por llamada (el máximo es 250) y devuelve un `id` por fichero. El procesado es **asíncrono**: mira `fileStatus` para saber si terminó.

```js
const FILE_CREATE = `
  mutation CreateFiles($files: [FileCreateInput!]!) {
    fileCreate(files: $files) {
      files { id fileStatus alt }
      userErrors { field message }
    }
  }`

const { fileCreate } = await gql(FILE_CREATE, {
  files: [
    { originalSource: cuadrado, contentType: 'IMAGE', alt: 'Camiseta, vista frontal' },
    { originalSource: vertical, contentType: 'IMAGE', alt: 'Camiseta, detalle' }
  ]
})
console.log(fileCreate.files, fileCreate.userErrors)
```

Requiere `write_files`. Documentación: https://shopify.dev/docs/api/admin-graphql/latest/mutations/fileCreate

### Imágenes por URL, sin subir el binario

Cada salida de SocialCutter ya es una URL pública. En ese caso `fileCreate` la descarga, la procesa y la almacena por ti: no necesitas `stagedUploadsCreate` ni tocar el binario. Basta con asignar `originalSource` a la URL de SocialCutter.

### Cuándo hace falta stagedUploadsCreate

`stagedUploadsCreate` es el flujo en dos pasos para cuando el fichero **no** está en una URL accesible: vive en tu disco, en una red poco fiable, o es grande y quieres subirlo directo. Devuelve `stagedTargets`, cada uno con `url`, `resourceUrl` y `parameters`:

```js
const STAGED = `
  mutation StagedUploads($input: [StagedUploadInput!]!) {
    stagedUploadsCreate(input: $input) {
      stagedTargets { url resourceUrl parameters { name value } }
      userErrors { field message }
    }
  }`

const { stagedUploadsCreate } = await gql(STAGED, {
  input: [{ filename: 'cuadrado.jpg', mimeType: 'image/jpeg', httpMethod: 'PUT', resource: 'IMAGE' }]
})

const target = stagedUploadsCreate.stagedTargets[0]
```

La subida a `url` cambia según el tipo de fichero:

| Tipo | Método de subida |
|---|---|
| Imágenes | `PUT` a `url`, con los `parameters` como cabeceras |
| Vídeos y modelos 3D | `POST` multipart a `url` |

Después de subir el binario, el fichero todavía no existe para Shopify: hay que registrarlo con `fileCreate` usando `resourceUrl` como `originalSource`, que es el paso de arriba. Para vídeos y modelos 3D el `fileSize` es obligatorio en la entrada de `stagedUploadsCreate`; para imágenes no.

## 4. Adjuntar la media al producto

`productUpdate` acepta un argumento `media` con la lista de ficheros que se añaden al producto. **El orden importa**: la primera del listado es la imagen principal. Para reordenar después existe `productReorderMedia`.

```js
const PRODUCT_UPDATE = `
  mutation AttachMedia($product: ProductUpdateInput!, $media: [CreateMediaInput!]) {
    productUpdate(product: $product, media: $media) {
      product { id media(first: 10) { nodes { id alt } } }
      userErrors { field message }
    }
  }`

const PRODUCT_ID = 'gid://shopify/Product/108828309'

await gql(PRODUCT_UPDATE, {
  product: { id: PRODUCT_ID },
  media: [
    { originalSource: cuadrado, contentType: 'IMAGE', alt: 'Camiseta, vista frontal' },
    { originalSource: vertical, contentType: 'IMAGE', alt: 'Camiseta, detalle' }
  ]
})
```

Requiere `write_products`. Documentación: https://shopify.dev/docs/api/admin-graphql/latest/mutations/productUpdate

> Aviso: `productCreateMedia` y `productUpdateMedia` siguen existiendo, pero las versiones recientes de la GraphQL Admin API las marcan como **obsoletas**. La documentación de media de producto apunta a `productUpdate`, `productSet` o `productCreate` con el argumento `media`. Si tu integración usa las antiguas, planifica la migración.

## 5. Asociar la media a las variantes

Cada variante se asocia a una media concreta con `mediaId` (o `mediaSrc`) para que el selector enseñe la imagen correcta. La mutación es `productVariantsBulkUpdate` y requiere `write_products`.

```js
const VARIANT_MEDIA = `
  mutation AttachVariantMedia($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
    productVariantsBulkUpdate(productId: $productId, variants: $variants) {
      productVariants { id }
      userErrors { field message }
    }
  }`

await gql(VARIANT_MEDIA, {
  productId: PRODUCT_ID,
  variants: [
    { id: 'gid://shopify/ProductVariant/43729076', mediaId: 'gid://shopify/MediaImage/1234' }
  ]
})
```

Documentación: https://shopify.dev/docs/api/admin-graphql/latest/mutations/productVariantsBulkUpdate y https://shopify.dev/docs/api/admin-graphql/latest/input-objects/ProductVariantsBulkInput

## Coste

- **1 uso por destino** (plataforma y formato) por petición; los repetidos no se cobran dos veces.
- Los procesamientos fallidos se devuelven.
- Planes con API y MCP: Free 3 usos/día, Basic 10, Pro 30, Agency 100.

## Errores típicos

| Síntoma | Causa | Solución |
|---|---|---|
| `userErrors` con "Access denied" | Falta el scope en la instalación | Añade `write_products` o `write_files` y reinstala la app |
| `THROTTLED` en `errors` | Has agotado el coste del bucket de la API | Aplica backoff y espacia las mutaciones |
| El fichero existe pero no se ve | `fileStatus` aún no es `READY` | Es asíncrono: reintenta la lectura pasados unos segundos |
| La imagen principal no es la que quieres | Orden de la lista `media` | Reordena con `productReorderMedia` |
| `originalSource` rechazado | La URL no es pública o no es una imagen | Comprueba que apunta a la salida de SocialCutter |
| `429` en SocialCutter | Cuota del monedero agotada | Consulta `GET /api/v1/credits` o sube de plan |

## Sin código

Un automatizador encadena los mismos pasos con nodos: disparador, nodo HTTP a `/api/v1/images/process` y nodos de Shopify para subir el fichero y asociarlo al producto. El patrón está en la [guía de automatización con n8n](/guias/n8n/).

## Siguientes pasos

- WordPress y WooCommerce: [Integra SocialCutter con WordPress y WooCommerce](/guias/wordpress/)
- Ghost: [Sube imágenes a Ghost y publica el post correcto](/guias/ghost/)
- Node: [Procesa imágenes con la API desde Node](/guias/node/)
- Automatización: [Automatiza el recorte de imágenes con n8n](/guias/n8n/)
- Documentación: https://docs.socialcutter.theboomer.dev