Skip to content

Shopify

Esta guía explica cómo integrar Shopify con Botinfy a través de n8n, incluyendo la creación de API keys, conexión con n8n y extracción masiva de datos.

Creación de ApiKey y conexión con n8n

Esta sección explica cómo habilitar acceso a Shopify Admin API (GraphQL) y conectarlo con n8n de forma segura.

Objetivo: obtener un token válido (ApiKey/Access Token) y configurar n8n para poder ejecutar consultas/mutaciones contra el endpoint GraphQL Admin.


Requisitos

  • Una tienda Shopify (ej. tu-tienda.myshopify.com).
  • Acceso de Administrador o un usuario con permisos para Apps.
  • n8n (Cloud o self-hosted) con acceso a Internet.

Conceptos rápidos (para no confundir tokens)

  • Admin API (GraphQL / REST): sirve para leer/escribir datos de la tienda (productos, órdenes, inventario, etc.).
    • Header típico para custom app / token de admin:
      • X-Shopify-Access-Token: <ADMIN_API_ACCESS_TOKEN>
  • Storefront API: sirve para experiencias públicas (catálogo, checkout headless, etc.).
    • Header típico:
      • X-Shopify-Storefront-Access-Token: <STOREFRONT_ACCESS_TOKEN>

En estas guías usaremos Admin API GraphQL.


Endpoint correcto (GraphQL Admin)

La URL siempre sigue este patrón:

https://{shop}.myshopify.com/admin/api/{API_VERSION}/graphql.json

Ejemplo (como en tus flujos):

https://092e31-d5.myshopify.com/admin/api/2025-10/graphql.json

Recomendación:

  • Usar una versión fija (ej. 2025-10) para evitar cambios inesperados.

Dos formas de obtener el acceso (según el cliente)

Opción A — El cliente entrega credenciales/permisos

Usa esta opción cuando el cliente NO quiere darte acceso al admin, pero puede entregarte:

  • Dominio de la tienda: xxxx.myshopify.com
  • Un Admin API access token ya creado (idealmente de un custom app), o
  • Te crea un usuario con permisos limitados y te permite instalar/autorizar una app (según política del cliente).

Checklist para pedir al cliente:

  • Shop domain (obligatorio)
  • Admin API access token (obligatorio)
  • API version a usar (recomendado)
  • Qué datos se van a extraer (productos, órdenes, clientes, inventario, etc.) para definir scopes

Importante: solicitar mínimos permisos. Evitar “Full access” si no es necesario.


Opción B — El cliente te da acceso al Shopify Admin para crear el proyecto y la ApiKey

Usa esta opción cuando el cliente te agrega al admin (ideal) como:

  • Staff (usuario interno) o
  • Collaborator (colaborador)

Paso 1: Entrar al Admin

  1. Ingresar a: https://admin.shopify.com/
  2. Seleccionar la tienda correcta.

Paso 2: Crear una Custom App

Shopify cambió el flujo con el tiempo: lo común hoy es crear una Custom App desde el Admin.

  1. Ir a SettingsApps and sales channels
  2. Seleccionar Develop apps (o similar)
  3. Click en Create an app
  4. Colocar nombre (ej. Botinfy n8n Extractor) y quién la mantiene.

Referencia (video):

Si Shopify pide habilitar “App development”:

  • Aceptar y continuar (esto lo controla un admin).

Paso 3: Configurar permisos (scopes)

  1. Entrar a la app creada.
  2. Ir a Configuration.
  3. En Admin API integration, asignar scopes según el caso.

Ejemplo de scopes típicos (varía por proyecto):

  • Productos: read_products
  • Inventario: read_inventory
  • Colecciones: read_collections
  • Ubicaciones: read_locations
  • Órdenes: read_orders (si aplica)

Regla: si sólo vas a leer, usa scopes read_*.

Paso 4: Instalar la app y generar el token

  1. Dentro de la app, click en Install app.
  2. Confirmar instalación.
  3. Shopify mostrará un Admin API access token.

Acciones recomendadas:

  • Copiar el token una sola vez y guardarlo en un vault (1Password, Bitwarden, etc.).
  • No pegar el token en documentos públicos ni en el repositorio.

Conexión con n8n (paso a paso)

Paso 1: Crear credencial “Header Auth” (recomendado)

  1. En n8n ir a CredentialsNew
  2. Buscar HTTP Header Auth
  3. Configurar:
    • Name: Header Auth: Shopify <Cliente>
    • Header Name: X-Shopify-Access-Token
    • Header Value: <ADMIN_API_ACCESS_TOKEN>

Ejemplo visual (cómo debe verse el Header Auth para ejecutar mutations/queries):

Header Auth Shopify en n8n

Alternativa: puedes setear headers directamente en el nodo, pero es menos seguro y menos reutilizable.


Paso 2: Probar conexión con un nodo GraphQL

  1. Crear un workflow de prueba.
  2. Agregar un nodo GraphQL.
  3. Configurar:
    • Authentication: Header Auth
    • Endpoint:
https://{shop}.myshopify.com/admin/api/{API_VERSION}/graphql.json
  1. Query de prueba:
graphql
{
  shop {
    name
    myshopifyDomain
  }
}
  1. Ejecutar.

Si funciona, verás el nombre de la tienda.


Troubleshooting (errores comunes)

401 Unauthorized

Causas típicas:

  • Token inválido / expirado / revocado.
  • Header incorrecto (debe ser X-Shopify-Access-Token).
  • Endpoint mal armado (dominio o versión).

403 Forbidden

Causas típicas:

  • Scopes insuficientes (ej. falta read_products).
  • App no instalada.

429 Too Many Requests

Causas típicas:

  • Demasiadas llamadas.

Soluciones:

  • Usar Bulk Operations (ver guía de extracción).
  • Agregar Wait / backoff en n8n.

Buenas prácticas de seguridad

  • Nunca commits de tokens en Git.
  • Usar credenciales de n8n (en vez de headers hardcoded).
  • Rotar tokens si hay sospecha de exposición.
  • Pedir sólo scopes mínimos.

Extracción de datos

Esta sección documenta un flujo típico de extracción masiva (bulk) desde Shopify usando GraphQL Admin API, procesando el archivo JSONL resultante en n8n, y dejando los datos listos para IA (flatten + embeddings + vector store).

Base: el workflow de ejemplo que compartiste (Bulk Operation + polling + download JSONL + Code node + Supabase).


Cuándo usar Bulk Operations

Usa Bulk Operations cuando:

  • Hay muchos productos/variantes.
  • Necesitas inventario por ubicación.
  • Quieres minimizar rate limits y acelerar.

Bulk Operations funciona así:

  1. Envías una mutación bulkOperationRunQuery con una query grande.
  2. Shopify corre el job async.
  3. Consultas el estado por BulkOperation.id.
  4. Cuando termina, obtienes una URL a un archivo .jsonl.

Requisitos

  • Tener configurado el token y conexión (ver guía: Creación de ApiKey y conexión con n8n).
  • En n8n:
    • Nodo GraphQL
    • Nodo Wait
    • Nodo IF
    • Nodo HTTP Request
    • Nodo Code
  • (Opcional) Supabase + LangChain nodes si vas a indexar en vector store.

Paso a paso (workflow)

1) Trigger del flujo

Puedes disparar de 2 maneras:

  • Schedule Trigger (cada X horas) para refrescar catálogo.
  • Manual (para pruebas).

En tu ejemplo se usa:

  • Schedule Trigger cada 3 horas.

2) (Opcional) Limpiar datos anteriores

En tu ejemplo se borra la data previa en Supabase antes de reinsertar:

  • Nodo borrarViejaDB (HTTP DELETE a REST de Supabase)

Recomendaciones:

  • Si tu tabla crece mucho, esto simplifica consistencia.
  • Asegúrate de que el endpoint y credenciales de Supabase estén en Credentials.

Seguridad: NO pegues service role keys en el workflow; usa credenciales.


3) Ejecutar Bulk Operation (Productos)

Nodo: GraphQL

  • Authentication: headerAuth
  • Endpoint:
https://{shop}.myshopify.com/admin/api/2025-10/graphql.json
  • Query: mutación bulkOperationRunQuery

Punto clave del filtro que usas:

graphql
products(query: "inventory_total:>0")

Eso hace que Shopify retorne sólo productos con inventario total > 0.

Qué datos estás trayendo:

  • Producto: id, title, handle, tags, descripciones, onlineStorePreviewUrl
  • Imágenes
  • Colecciones
  • Variantes: SKU/barcode/precio, opciones, inventario
  • InventoryItem con peso + niveles por ubicación (available)

Salida esperada del nodo:

  • bulkOperationRunQuery.bulkOperation.id

4) Guardar el bulk_id

Nodo: Set (también conocido como "Edit Fields" en algunos casos)

Acción: Guardas el id para reusarlo en el polling.

Ejemplo de mapeo:

bulk_id = $json.data.bulkOperationRunQuery.bulkOperation.id

Nota: En n8n, las expresiones con dobles llaves se usan para acceder a datos de nodos anteriores. La variable $json representa los datos del nodo anterior, y .data.bulkOperationRunQuery.bulkOperation.id accede al ID de la operación bulk que se generó. En n8n se escriben con el formato: dobles llaves + expresión (ejemplo: dobles llaves seguidas de la variable $json seguida de .campo).


5) Esperar y consultar estado (polling)

La operación es asíncrona, así que haces loop:

  1. Nodo Wait (ej. 15s)
  2. Nodo GraphQL: consulta por node(id: "<bulk_id>") y pide:
    • status, url, errorCode, objectCount, createdAt, completedAt
  3. Nodo IF:
    • Si status == COMPLETED y url no vacío → seguir
    • Si no → esperar 12s y volver a consultar

Recomendación práctica:

  • Agrega un máximo de intentos (si tu n8n lo permite) para evitar loops infinitos.

6) Descargar el JSONL

Nodo: HTTP Request

  • URL:
$json.data.node.url
  • Response format: file

Nota: La expresión $json.data.node.url accede a la URL del archivo JSONL que Shopify generó después de completar la operación bulk. Esta URL se obtiene del nodo GraphQL anterior que consulta el estado de la operación.

Esto te entrega un binario con el archivo JSONL.


7) Parsear JSONL y reconstruir estructura

Nodo: Code (en tu ejemplo se llama "Rebuild + Flatten (for AI)")

Qué hace el script:

  • Decodifica el binario base64.
  • Parseo por líneas (cada línea es un JSON).
  • Reconstruye:
    • Productimages, collections, variants
    • Variantinventory_levels por ubicación
  • Genera un documento final plano por producto (ideal para embeddings):
    • title, tags, description, image_urls, online_url, price, inventoryQuantity
    • variants[] con options + peso + disponibilidad por ubicación
    • text_for_embedding en español

Nota: Bulk Operations devuelve filas con __parentId para reconstruir relaciones.


8) (Opcional) Generar embeddings e indexar

En tu ejemplo:

  • Recursive Character Text Splitter
  • Default Data Loader
  • Embeddings OpenAI
  • Supabase Vector Store (modo insert)

Checklist:

  • Definir el nombre de la tabla (ej. documents_<cliente>)
  • Confirmar el tamaño de chunk (chunkSize) según tu contenido
  • Revisar campos metadata que vas a guardar

Workflow completo - Ejemplo de configuración

A continuación, el workflow de ejemplo basado en tu configuración.

Importante: este ejemplo contiene campos sensibles en tu versión original (ej. apikey, Authorization, ids de credenciales). En documentación de repo se recomienda usar placeholders.

Nota sobre expresiones n8n: En el ejemplo de configuración encontrarás expresiones como $('Edit Fields').item.json.bulk_id y expresiones con la variable $json. Estas son sintaxis de n8n para acceder a datos:

  • $('NombreNodo').item.json.campo accede a datos de un nodo específico por su nombre
  • La variable $json seguida de .campo accede a datos del nodo anterior usando expresiones
  • En n8n, estas expresiones se escriben con dobles llaves o con el prefijo ={{ seguido de la expresión para que n8n las evalúe en lugar de tratarlas como texto literal
json
{
  "nodes": [
    {
      "parameters": {
        "authentication": "headerAuth",
        "endpoint": "https://092e31-d5.myshopify.com/admin/api/2025-10/graphql.json",
        "query": "mutation {\n  bulkOperationRunQuery(\n    query: \"\"\"\n    {\n      products(query: \\\"inventory_total:>0\\\") { # <-- AQUÍ ESTÁ LA MODIFICACIÓN\n        edges {\n          node {\n            id\n            title\n            handle\n            tags\n            description\n            descriptionHtml\n            onlineStorePreviewUrl\n            images { edges { node { id url altText } } }\n            collections { edges { node { id title handle } } }\n            variants {\n              edges {\n                node {\n                  id\n                  title\n                  sku\n                  barcode\n                  price\n                  compareAtPrice\n                  inventoryQuantity\n                  inventoryPolicy\n                  selectedOptions { name value }\n                  inventoryItem {\n                    id\n                    measurement { weight { value unit } }\n                    inventoryLevels {\n                      edges {\n                        node {\n                          location { id name }\n                          quantities(names: [\\\"available\\\"]) { name quantity }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n    \"\"\"\n  ) {\n    bulkOperation { id status }\n    userErrors { field message }\n  }\n}"
      },
      "type": "n8n-nodes-base.graphql",
      "typeVersion": 1.1,
      "position": [
        -1472,
        1104
      ],
      "id": "d9526015-5694-4362-98aa-ea10a78fa5cf",
      "name": "Shopify – Bulk run (Products)",
      "credentials": {
        "httpHeaderAuth": {
          "id": "<N8N_CREDENTIAL_ID>",
          "name": "Header Auth: Shopify <CLIENTE>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "headerAuth",
        "endpoint": "https://092e31-d5.myshopify.com/admin/api/2025-10/graphql.json",
        "query": "={\\n  node(id: \\\"{{ $('Edit Fields').item.json.bulk_id }}\\\") {\\n    ... on BulkOperation {\\n      id\\n      status\\n      url\\n      errorCode\\n      objectCount\\n      createdAt\\n      completedAt\\n      type\\n    }\\n  }\\n}\\n"
      },
      "type": "n8n-nodes-base.graphql",
      "typeVersion": 1.1,
      "position": [
        -800,
        1104
      ],
      "id": "3ff18b27-d80c-4ed5-8ec1-9b223fffbfbb",
      "name": "Shopify – Status (by id)",
      "credentials": {
        "httpHeaderAuth": {
          "id": "<N8N_CREDENTIAL_ID>",
          "name": "Header Auth: Shopify <CLIENTE>"
        }
      }
    },
    {
      "parameters": {
        "amount": 12,
        "unit": "seconds"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        -352,
        1200
      ],
      "id": "42f3b933-6ed4-472f-b19b-7acb91b0ea02",
      "name": "Wait 12s (loop)"
    },
    {
      "parameters": {
        "url": "={{ $json.data.node.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -352,
        976
      ],
      "id": "9daa32d5-92d6-48bd-aafb-daaaaf1b46c9",
      "name": "Download JSONL"
    },
    {
      "parameters": {
        "jsCode": "// Parse JSONL y reconstruir Producto → imágenes/colecciones/variantes → niveles de inventario\nconst b64 = items[0].binary.data.data;\nconst text = Buffer.from(b64, 'base64').toString('utf8');\nconst rows = text.trim().split('\\n').filter(Boolean).map(l => JSON.parse(l));\n\nconst products = new Map();\nconst variants = new Map();\nconst invItemToVariant = new Map();\nconst imagesByProduct = new Map();\nconst collsByProduct = new Map();\n\nfor (const r of rows) {\n  const pid = r.__parentId;\n  const id = r.id || null;\n\n  if (!pid && id && id.includes('/Product/')) {\n    products.set(id, {\n      id: id,\n      title: r.title,\n      handle: r.handle,\n      tags: r.tags || [],\n      description: r.description || null,\n      descriptionHtml: r.descriptionHtml || null,\n      onlineStorePreviewUrl: r.onlineStorePreviewUrl || null,\n      images: [],\n      collections: [],\n      variants: []\n    });\n  } else if (id && id.includes('/ProductVariant/')) {\n    const v = {\n      id: id,\n      title: r.title,\n      sku: r.sku || null,\n      barcode: r.barcode || null,\n      price: r.price != null ? parseFloat(r.price) : null,\n      compareAtPrice: r.compareAtPrice != null ? parseFloat(r.compareAtPrice) : null,\n      inventoryQuantity: r.inventoryQuantity ?? null,\n      inventoryPolicy: r.inventoryPolicy || null,\n      options: Object.fromEntries((r.selectedOptions || []).map(o => [o.name, o.value])),\n      weight_value: null,\n      weight_unit: null,\n      inventory_levels: []\n    };\n    variants.set(id, v);\n    const p = products.get(pid);\n    if (p) p.variants.push(v);\n  } else if (id && id.includes('/InventoryItem/')) {\n    // mapear InventoryItem → Variant y copiar peso\n    const variantId = pid;\n    invItemToVariant.set(id, variantId);\n    const v = variants.get(variantId);\n    if (v) {\n      v.weight_value = r.measurement?.weight?.value ?? null;\n      v.weight_unit  = r.measurement?.weight?.unit  ?? null;\n    }\n  } else if (r.location && r.quantities && pid && pid.includes('/InventoryItem/')) {\n    // niveles por ubicación\n    const variantId = invItemToVariant.get(pid);\n    if (variantId) {\n      const v = variants.get(variantId);\n      const available = (r.quantities || []).find(q => q.name === 'available')?.quantity ?? null;\n      v?.inventory_levels.push({\n        location_id: r.location?.id,\n        location_name: r.location?.name,\n        available\n      });\n    }\n  } else if (id && id.includes('/ProductImage/')) {\n    const arr = imagesByProduct.get(pid) || [];\n    arr.push({ id, url: r.url, altText: r.altText || null });\n    imagesByProduct.set(pid, arr);\n  } else if (id && id.includes('/Collection/')) {\n    const arr = collsByProduct.get(pid) || [];\n    arr.push({ id, title: r.title, handle: r.handle });\n    collsByProduct.set(pid, arr);\n  }\n}\n\n// ensamblar y aplanar\nconst out = [];\nfor (const [id, p] of products) {\n  p.images = imagesByProduct.get(id) || [];\n  p.collections = collsByProduct.get(id) || [];\n\n  // métricas de precio e inventario a nivel producto\n  const prices = p.variants.map(v => v.price).filter(v => typeof v === 'number');\n  const primary_price = prices.length ? Math.min(...prices) : null;\n  const inventory_total = p.variants.reduce((acc, v) => acc + (v.inventoryQuantity ?? 0), 0);\n\n  const primary_image_url = p.images[0]?.url || null;\n\n  // documento plano para IA\n  const doc = {\n    doc_type: \"product\",\n    product_gid: p.id,\n    product_id: p.id.split('/').pop(),\n    title: p.title,\n    tags: p.tags,\n    description: p.description,\n    image_url: primary_image_url,\n    image_urls: p.images.map(i => i.url),\n    online_url: p.onlineStorePreviewUrl,\n    price: primary_price,\n    inventoryQuantity: inventory_total,\n    collections: p.collections.map(c => ({ title: c.title, handle: c.handle })),\n    variants: p.variants.map(v => ({\n      id: v.id,\n      title: v.title,\n      sku: v.sku,\n      price: v.price,\n      compareAtPrice: v.compareAtPrice,\n      inventoryQuantity: v.inventoryQuantity,\n      options: v.options,\n      weight_value: v.weight_value,\n      weight_unit: v.weight_unit,\n      availability_by_location: v.inventory_levels\n    }))\n  };\n\n  const text_for_embedding = [\n    `TÍTULO: ${doc.title}`,\n    doc.tags?.length ? `TAGS: ${doc.tags.join(', ')}` : null,\n    doc.description ? `DESCRIPCIÓN: ${doc.description}` : null,\n    doc.collections?.length ? `COLECCIONES: ${doc.collections.map(c => c.title).join(', ')}` : null,\n    (doc.price != null) ? `PRECIO DESDE: ${doc.price}` : null,\n    `INVENTARIO TOTAL: ${doc.inventoryQuantity}`,\n    doc.online_url ? `URL: ${doc.online_url}` : null,\n    doc.image_url ? `IMAGEN: ${doc.image_url}` : null\n  ].filter(Boolean).join('\\n');\n\n  out.push({ json: { ...doc, text_for_embedding } });\n}\n\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -128,
        976
      ],
      "id": "40974afb-b006-456a-9fd0-20c1e1c6850c",
      "name": "Rebuild + Flatten (for AI)"
    }
  ]
}

Personalizaciones típicas

  • Cambiar filtro de productos:

    • Con inventario: inventory_total:>0
    • Por tag: tag:Liquidacion
    • Por colección (depende del modelo de datos)
  • Cambiar el documento final para IA:

    • Agregar marca/vendor, tipo, SEO fields, etc.
  • Cambiar el destino:

    • Guardar en SQL, en S3, en Google Drive, etc.

Checklist de validación

  • El nodo Bulk devuelve bulkOperation.id sin userErrors.
  • El polling llega a COMPLETED y url no vacío.
  • El Download JSONL trae un binario válido.
  • El Code produce una lista de items (1 por producto).
  • Si indexas, la tabla destino recibe los registros.

Documentación de Botinfy