Verificando acceso interno...
Shopify — configuración técnica
Estado: PLANIFICADO, no implementado todavía. Tenemos
preparada la infraestructura genérica de conectores
(voice_connectors, voice_tools_config) y la
verificación HMAC genérica, pero no hay tablas, endpoints ni handlers
Shopify-específicos a fecha de hoy. Este doc define cómo lo vamos
a hacer, qué Cloudflare bindings hace falta y cuál es el plan para
el primer cliente real (Vila Sen Vento).
1. Estado actual y dónde está mencionado
Lo que ya existe en el repo:
- Tabla
voice_connectors(migrations/0153_voice_assistant_core.sql) con un campoconnectorTypeque admite'shopify'entre los valores futuros. - Tabla
voice_tools_config(misma migración) que ya está pensada para activar/desactivar tools por conector — incluyendo tools tipoget_order_statusylookup_product. - Helpers de verificación HMAC genéricos en
functions/utils/webhookSecurity.js(funciónverifyHMAC()) que soportan SHA-256 — sirven para la firmaX-Shopify-Hmac-SHA256sin tocar nada nuevo. - Documentación de visión que planifica Shopify como connector tipo Stub:
storefronts/voice-admin/docs/internal/voice-v2-vision.html. - Propuesta cliente que promete tools Shopify en fase 1: propuesta Vila Sen Vento.
Lo que NO existe todavía:
- Migración con tablas
shopify_orders,shopify_products,shopify_webhooks_log. - Endpoints OAuth (
/api/shopify/oauth/install,/api/shopify/oauth/callback). - Webhook handler
/api/webhooks/shopify. - Implementación de las tools
get_order_status,lookup_product,create_order_draftpara conectores Shopify.
2. Arquitectura objetivo
Tres bloques, cada uno en su sitio:
- App Shopify pública (en Shopify Partners) que el cliente instala desde el Shopify App Store o vía URL directa. La app sólo guarda el access token y dispara los suscritos a webhooks.
- Webhook handler en
functions/api/webhooks/shopify.jsque verifica HMAC con el shared secret de la app y persiste eventos en D1. - Tools del asistente en
functions/api/voice/tools/que, cuando ElevenLabs las llama, leen de D1 cacheado y/o golpean directamente la Admin API de Shopify si los datos no están al día.
Modelo de datos: cache-first. Tenemos D1 con la "verdad" sincronizada vía webhooks. Si necesitamos algo en tiempo real (por ejemplo, stock real al segundo) golpeamos la API de Shopify; si no, leemos D1 (más rápido y barato, sin rate limits).
3. OAuth de la app Shopify
- El cliente entra en
https://cadences.app/api/shopify/oauth/install?shop=vilasenvento.myshopify.com(o vía botón "Conectar Shopify" en el panel). - Redirigimos a
https://{shop}/admin/oauth/authorize?client_id=...&scope=read_orders,write_draft_orders,read_products,read_customers&redirect_uri=...&state=<CSRF>. - El cliente autoriza → Shopify redirige a
/api/shopify/oauth/callback?code=...&shop=...&state=.... - Verificamos
state(anti-CSRF) y la firma HMAC del query string. - Intercambiamos
codeporaccess_tokenpermanente víaPOST /admin/oauth/access_token. - Insertamos / actualizamos en
voice_connectorsconconnectorType='shopify',credentialsEncrypted={ shop, access_token }(cifrado con clave del workspace). - Suscribimos los webhooks (sección 4) vía Admin GraphQL
webhookSubscriptionCreate. - Lanzamos backfill inicial: traer últimos 90 días de pedidos + catálogo entero a las tablas D1 cache.
Secretos a configurar (Pages project cadences):
| Secret | Para qué |
|---|---|
SHOPIFY_API_KEY | Client ID de la app Shopify Partners. |
SHOPIFY_API_SECRET | Client secret + se usa para verificar HMAC de webhooks y OAuth callback. |
SHOPIFY_APP_SCOPES | String CSV de scopes que pedimos. Por defecto: read_orders,write_draft_orders,read_products,read_customers,read_inventory. |
SHOPIFY_REDIRECT_URI | https://cadences.app/api/shopify/oauth/callback. |
4. Webhooks que vamos a recibir
Endpoint único: POST /api/webhooks/shopify. La cabecera X-Shopify-Topic nos dice qué evento es. La firma X-Shopify-Hmac-SHA256 se verifica con SHOPIFY_API_SECRET sobre el body crudo.
| Topic | Para qué lo queremos | Tabla destino |
|---|---|---|
orders/create | Saber al instante que hay pedido nuevo (para mandar WhatsApp post-venta, alertar al staff). | shopify_orders |
orders/updated | Cambios de estado (pagado, parcial, devuelto). | shopify_orders |
orders/fulfilled | Pedido enviado: el asistente puede dar tracking si el cliente pregunta. | shopify_orders |
orders/cancelled | Cancelaciones (refund, etc). | shopify_orders |
products/create + update | Mantener cache de catálogo + reindexar la knowledge base si la ficha cambia. | shopify_products + Vectorize re-embed |
customers/create + update | Saber del cliente (email, teléfono, dirección, etiquetas) cuando el asistente le atienda. | shopify_customers |
app/uninstalled | Marcar el connector como revoked y limpiar. | voice_connectors |
Reglas: respuesta 200 en menos de 5 s (Shopify reintenta si no), todo el procesamiento pesado va a una cola/Queue. Verificación de firma siempre, también en dev. Loguear todos los eventos en shopify_webhooks_log para auditoría 30 días.
5. Tools del asistente sobre Shopify
Las tools que ElevenLabs va a llamar (ver convenciones generales):
get_order_status— Input:order_numberoemail. Lee deshopify_orderscache. Devuelve estado + tracking + items resumidos.lookup_product— Input:query(texto natural) osku. Siquery, primero búsqueda semántica en Vectorize (knowledge base = catálogo indexado), luego enriquecer con stock real desde D1.create_order_draft— Input:customer_id+line_items. Crea draft order vía Admin API (POST /admin/api/2025-01/draft_orders.json), devuelveinvoice_url.get_customer_orders— Devuelve los últimos N pedidos del cliente (por phone/email) para que el asistente diga "veo que la última vez pediste X".recommend_products— (fase 2) recomendación basada en historial + RAG.
Importante: el asistente nunca debe poder modificar
precios, hacer descuentos discrecionales, ni cancelar pedidos sin escalar.
Esos casos van a escalate_to_human y los atiende el cliente.
6. Esquema D1 que hay que crear
Migración propuesta (migrations/0160_shopify_connector.sql):
CREATE TABLE shopify_orders (
id TEXT PRIMARY KEY, -- gid://shopify/Order/...
tenant_id TEXT NOT NULL, -- FK a voice_assistants
shop TEXT NOT NULL, -- e.g. vilasenvento.myshopify.com
order_number TEXT NOT NULL,
email TEXT,
phone TEXT,
total_price_cents INTEGER,
currency TEXT,
financial_status TEXT, -- paid / pending / refunded
fulfillment_status TEXT, -- fulfilled / partial / unfulfilled
tracking_number TEXT,
tracking_url TEXT,
line_items_json TEXT, -- JSON: [{title,qty,sku}]
raw_json TEXT, -- payload completo para debugging
created_at TEXT NOT NULL, -- ISO de Shopify
updated_at TEXT NOT NULL,
synced_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_shopify_orders_tenant ON shopify_orders(tenant_id, updated_at DESC);
CREATE INDEX idx_shopify_orders_phone ON shopify_orders(tenant_id, phone);
CREATE INDEX idx_shopify_orders_email ON shopify_orders(tenant_id, email);
CREATE TABLE shopify_products (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
shop TEXT NOT NULL,
title TEXT,
vendor TEXT,
product_type TEXT,
handle TEXT,
status TEXT, -- active / draft / archived
total_inventory INTEGER,
variants_json TEXT, -- JSON: [{sku,price_cents,inventory}]
body_html TEXT, -- usado para reindexar RAG
tags TEXT,
updated_at TEXT NOT NULL,
synced_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_shopify_products_tenant ON shopify_products(tenant_id, updated_at DESC);
CREATE TABLE shopify_customers (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
shop TEXT NOT NULL,
email TEXT,
phone TEXT,
first_name TEXT,
last_name TEXT,
orders_count INTEGER,
total_spent_cents INTEGER,
tags TEXT,
raw_json TEXT,
updated_at TEXT NOT NULL,
synced_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_shopify_customers_tenant_phone ON shopify_customers(tenant_id, phone);
CREATE INDEX idx_shopify_customers_tenant_email ON shopify_customers(tenant_id, email);
CREATE TABLE shopify_webhooks_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id TEXT,
topic TEXT NOT NULL,
shop TEXT,
status TEXT, -- ok / signature_failed / handler_error
payload TEXT, -- JSON o crudo si falla parseo
error TEXT,
received_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_shopify_webhooks_log_recv ON shopify_webhooks_log(received_at DESC);
Cifrado del access token: usar credentialsEncrypted en
voice_connectors con la clave maestra del workspace, no
guardarlo en plano nunca.
7. Plan de implementación (Vila Sen Vento)
Vila Sen Vento es nuestro primer cliente real con Shopify (ver propuesta). Roadmap en 3 sprints:
- Sprint 1 — App Shopify + OAuth + cache (3-4 días). Crear app en Partners, endpoints OAuth, migración 0160, suscripción a webhooks
orders/*+products/*+customers/*. Probar con tienda dev de Shopify. - Sprint 2 — Tools y backfill (3-4 días). Implementar
get_order_status,lookup_product,get_customer_orders. Script de backfill para traer últimos 90 días al instalar. Reindexar catálogo en Vectorize (RAG). - Sprint 3 — Acciones + escalado (2-3 días).
create_order_draftcon link de pago vía WhatsApp. Reglas de escalado (descuentos, cancelaciones → siempre humano). Test E2E con voz del agente leyendo el estado de un pedido real.
Tarea pre-requisito: dar de alta la app en Shopify Partners
con cuenta de [email protected] y configurar URLs
apuntando a cadences.app.
8. Riesgos y decisiones pendientes
- Rate limits: Shopify GraphQL = 50 puntos/segundo por tienda. Para backfill grandes hay que paginar y respetar el bucket. Cache-first ayuda.
- App pública vs custom app: arrancamos con app personalizada (más rápido, no review de Shopify). Cuando tengamos > 5 clientes Shopify, crear app pública en App Store para distribución.
- Multi-tienda por tenant: el modelo soporta 1 tenant ↔ N tiendas (clave compuesta
tenant_id + shop). No frecuente pero hay que tenerlo previsto. - GDPR webhooks: Shopify exige soportar
customers/redact,shop/redactycustomers/data_requestaunque sea custom app. Hay que implementarlos para no perder la cuenta de partner. - Reindexado del RAG: cuando cambia un producto, reindexar sólo el chunk de ese producto, no todo el catálogo. Endpoint a definir.