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>
- Header típico para custom app / token de admin:
- Storefront API: sirve para experiencias públicas (catálogo, checkout headless, etc.).
- Header típico:
X-Shopify-Storefront-Access-Token: <STOREFRONT_ACCESS_TOKEN>
- Header típico:
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.jsonEjemplo (como en tus flujos):
https://092e31-d5.myshopify.com/admin/api/2025-10/graphql.jsonRecomendació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 sí 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
- Ingresar a:
https://admin.shopify.com/ - 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.
- Ir a Settings → Apps and sales channels
- Seleccionar Develop apps (o similar)
- Click en Create an app
- 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)
- Entrar a la app creada.
- Ir a Configuration.
- 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
- Dentro de la app, click en Install app.
- Confirmar instalación.
- 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)
- En n8n ir a Credentials → New
- Buscar HTTP Header Auth
- Configurar:
- Name:
Header Auth: Shopify <Cliente> - Header Name:
X-Shopify-Access-Token - Header Value:
<ADMIN_API_ACCESS_TOKEN>
- Name:
Ejemplo visual (cómo debe verse el Header Auth para ejecutar mutations/queries):

Alternativa: puedes setear headers directamente en el nodo, pero es menos seguro y menos reutilizable.
Paso 2: Probar conexión con un nodo GraphQL
- Crear un workflow de prueba.
- Agregar un nodo GraphQL.
- Configurar:
- Authentication:
Header Auth - Endpoint:
- Authentication:
https://{shop}.myshopify.com/admin/api/{API_VERSION}/graphql.json- Query de prueba:
{
shop {
name
myshopifyDomain
}
}- 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í:
- Envías una mutación
bulkOperationRunQuerycon una query grande. - Shopify corre el job async.
- Consultas el estado por
BulkOperation.id. - 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 Triggercada 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:
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.idNota: En n8n, las expresiones con dobles llaves se usan para acceder a datos de nodos anteriores. La variable
$jsonrepresenta los datos del nodo anterior, y.data.bulkOperationRunQuery.bulkOperation.idaccede 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$jsonseguida de.campo).
5) Esperar y consultar estado (polling)
La operación es asíncrona, así que haces loop:
- Nodo Wait (ej. 15s)
- Nodo GraphQL: consulta por
node(id: "<bulk_id>")y pide:status,url,errorCode,objectCount,createdAt,completedAt
- Nodo IF:
- Si
status == COMPLETEDyurlno vacío → seguir - Si no → esperar 12s y volver a consultar
- Si
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.urlaccede 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:
Product→images,collections,variantsVariant→inventory_levelspor ubicación
- Genera un documento final plano por producto (ideal para embeddings):
title,tags,description,image_urls,online_url,price,inventoryQuantityvariants[]con options + peso + disponibilidad por ubicacióntext_for_embeddingen español
Nota: Bulk Operations devuelve filas con
__parentIdpara reconstruir relaciones.
8) (Opcional) Generar embeddings e indexar
En tu ejemplo:
Recursive Character Text SplitterDefault Data LoaderEmbeddings OpenAISupabase Vector Store(modoinsert)
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_idy expresiones con la variable$json. Estas son sintaxis de n8n para acceder a datos:
$('NombreNodo').item.json.campoaccede a datos de un nodo específico por su nombre- La variable
$jsonseguida de.campoaccede 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
{
"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)
- Con inventario:
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.idsinuserErrors. - El polling llega a
COMPLETEDyurlno vacío. - El
Download JSONLtrae un binario válido. - El
Codeproduce una lista de items (1 por producto). - Si indexas, la tabla destino recibe los registros.