Saltar al contenido

Guía · Connector API v1

Construye un conector, paso a paso

De un proyecto vacío a un conector que pasa la comprobación de conformidad. Cada paso tiene su objetivo, un diagrama, el código mínimo, cómo saber que funciona y enlaces a la especificación.

Paso 0 de 11

Qué vas a construir

Conocer las piezas de un conector, cuáles son obligatorias y cuáles opcionales, antes de escribir código.

Un conector vive dentro de una plataforma de tienda (un módulo, una app, un plugin, un backend headless…) y la conecta con Spaidoo. Declara las capacidades que implementa, y Spaidoo actúa según esa declaración, nunca según el nombre de la plataforma (§1).

La imagen completa: tu conector entre la tienda, el navegador del cliente y SpaidooNavegador del cliente. Plataforma de tienda + tu conector. Spaidoo. Página de la tienda con el widget. Notificaciones push desde el dominio de la tienda. Páginas de la tienda storefront.saveAnchors. Ruta de identidad identity. Service worker de push push. Feed de productos catalog.feed. Ruta customers customers. Ruta de verificación domainVerification. Llamadas a Spaidoo handshake · catalog.changes customerErase · wishlistImport. Loader del widget scriptUrl. Sync de Spaidoo. API de Spaidoo /connector/v1. la página carga el widget. script, SpaidooConfig, anclajes de guardado. ¿quién navega?. lo registra. lo lee cada día. llamada firmada. lee el código. Bearer <apiKey>Navegador del clientePlataforma de tienda + tu conectorSpaidooPágina de la tiendacon el widgetNotificaciones pushdesde el dominio de la tiendaPáginas de la tiendastorefront.saveAnchorsRuta de identidadidentityService worker de pushpushFeed de productoscatalog.feedRuta customerscustomersRuta de verificacióndomainVerificationLlamadas a Spaidoohandshake · catalog.changescustomerErase · wishlistImportLoader del widgetscriptUrlSync de SpaidooAPI de Spaidoo/connector/v1la página carga el widgetscript, SpaidooConfig,anclajes de guardado¿quién navega?lo registralo lee cada díallamada firmadalee el códigoBearer <apiKey>
Las flechas van de quien pregunta a quien responde. Las capacidades están donde viven: rutas que sirve la tienda, lo que imprime en sus páginas y las llamadas que el conector hace a Spaidoo.
CapacidadQué le da a la tienda¿Hace falta?
catalog.feedSpaidoo conoce los productos de la tienda: precio, stock, imágenes.Obligatoria
catalog.changesLos cambios de precio y de stock llegan a Spaidoo en segundos en lugar de en la siguiente lectura diaria.Opcional
identityUn cliente con sesión iniciada conserva sus productos guardados en todos sus dispositivos, sin cuenta de Spaidoo.Opcional
customersSpaidoo puede pedir el email y el consentimiento de marketing de un cliente identificado cuando el comerciante lo necesita.Opcional, con identity
customerEraseBorrar un cliente en la tienda borra sus datos en Spaidoo para esa tienda.Opcional
wishlistImportLas listas de una extensión de wishlist anterior se traen a Spaidoo.Opcional, con identity
pushLas notificaciones web push se sirven desde el propio dominio de la tienda.Opcional
domainVerificationLa tienda demuestra que es dueña de su dominio sin cambios de DNS.Opcional
storefront.saveAnchorsTu tema imprime los botones de guardar; el widget los dibuja y gestiona el clic.Opcional

Cuánto se tarda

Depende de tu plataforma, pero el trabajo se reparte bien. El mínimo son los pasos 2 a 5 (registro, emparejamiento, handshake, feed de productos) más el script del paso 8: con eso, el widget funciona en la tienda con su catálogo. Cada una de las demás capacidades es una pieza independiente: la construyes, la declaras en el handshake y la compruebas por separado.

Por el camino te ayudan tres cosas: la especificación, que es el contrato y siempre manda; la comprobación de conformidad en línea; y, para los partners de Spaidoo, el asistente.

Cómo sabes que funciona

  • Sabes qué capacidades vas a declarar, y que catalog.feed es la única obligatoria.

Paso 1 de 11

Antes de empezar

Tener una tienda de prueba a la que Spaidoo pueda llegar, y las cuentas que necesitas.

  • Una instalación de prueba de tu plataforma, accesible desde internet por https. Spaidoo llama a la tienda (lee el feed, llama a las rutas firmadas, consulta la ruta de verificación), y la comprobación de conformidad también: localhost no basta (§8).
  • Un dominio de staging, o un túnel hacia una tienda que corre en tu máquina: ngrok o Cloudflare Tunnel le dan un host https público.
  • Una cuenta de partner de Spaidoo con acceso al área de desarrolladores. Allí registras tu conector (paso 2), y te abre el asistente (paso 11).
  • Una cuenta de Spaidoo para la tienda de prueba. La creas al emparejar la tienda (paso 3), como lo haría un comerciante.
Una tienda local accesible desde internet a través de un túnelTu tienda, en local http://localhost:8080. Túnel ngrok · Cloudflare Tunnel. Host público https://<tu-host>. Spaidoo API · sync · comprobación. Configura la tienda con el host público como dominio: todas las URL declaradas tienen que ser https en él.. https. reenvíaTu tienda, en localhttp://localhost:8080Túnelngrok · Cloudflare TunnelHost públicohttps://<tu-host>SpaidooAPI · sync · comprobaciónConfigura la tienda con el host público como dominio: todas las URL declaradas tienen que ser https en él.httpsreenvía
Spaidoo y la comprobación de conformidad llaman al host https público de la tienda; el túnel reenvía esas peticiones a la tienda de tu máquina.
Shell
# Either one, pointing at the shop on your machine (port 8080 here)
ngrok http 8080
cloudflared tunnel --url http://localhost:8080

Configura la tienda para que use el host del túnel como su propio dominio. Todas las URL de escaparate que declara el conector tienen que ser https en uno de los dominios de la tienda (§4.1), y también la vuelta de un conector instalado en cada tienda (§3.1).

Ten presentes los dos secretos desde el principio: la API key y el identity secret son secretos de la tienda, se guardan en el servidor y nunca se imprimen en las páginas (§2.1).

Cómo sabes que funciona

  • La tienda se abre por su URL https pública desde otra red (tu móvil con datos, por ejemplo).

Paso 2 de 11

Registra tu conector

Tu conector tiene un client_id, con el que se conecta cada tienda.

Las tiendas se conectan con OAuth 2 y PKCE, como app registrada (§3). Registra tu conector una vez, en Mis conectores, y guarda su client_id: es público, va en la URL que empieza la conexión.

TipoCuándoVuelta
Instalado en cada tienda (shop)Tu conector funciona en el servidor de cada tienda: un módulo, un plugin.Una URL https en el dominio de la tienda que se conecta. No hay nada que registrar.
Alojado (fixed)Tu conector funciona en tus servidores o en los de la plataforma, y el admin del comerciante no está en el dominio de la tienda.Las URL exactas que registras.
  • Dominios de servidor: donde viven tus rutas de servidor (urls.customers), si no están en el dominio de la tienda. Las rutas de escaparate van siempre en los dominios de la tienda (§4.1).
  • La platform de la app (my-platform) es texto libre, solo informativo.

Cómo sabes que funciona

Paso 3 de 11

Empareja la tienda

El comerciante conecta la tienda desde tu admin y tu servidor recibe su API key.

El emparejamiento es un flujo OAuth 2 con PKCE, en un popup: tu admin abre Spaidoo en un popup, Spaidoo devuelve el popup a tu vuelta con un código de un solo uso y tu servidor lo canjea por la API key. El popup solo avisa a tu página de admin de que ya está, y se cierra. La key nunca pasa por el navegador (§3.2).

Emparejamiento: OAuth 2 con PKCE, la key nunca en el navegador1. Servidor de la tienda: state + code_verifier, guardados en el servidor 2. Servidor de la tienda → Spaidoo admin.spaidoo.com/connect: popup a /connect ?client_id&redirect_uri&state &code_challenge&domain 3. Spaidoo admin.spaidoo.com/connect: El comerciante se registra o inicia sesión y acepta los términos 4. Spaidoo admin.spaidoo.com/connect → Servidor de la tienda: redirect_uri?code&state 5. Servidor de la tienda → API de Spaidoo: POST /pairing/token client_id, code, code_verifier 6. API de Spaidoo → Servidor de la tienda: { apiKey, shopId } 7. Servidor de la tienda: Guarda la API key, en el servidor 8. Servidor de la tienda → API de Spaidoo: POST /handshake (siguiente paso)Servidor de la tiendaSpaidooadmin.spaidoo.com/connectAPI de Spaidoo1state + code_verifier,guardados en el servidorpopup a /connect?client_id&redirect_uri&state&code_challenge&domain23El comerciante se registra oinicia sesión y acepta los términosredirect_uri?code&state4POST /pairing/tokenclient_id, code, code_verifier5{ apiKey, shopId }67Guarda la API key,en el servidorPOST /handshake(siguiente paso)8
El código sirve para un solo canje, durante 60 segundos, y solo con el code_verifier que empezó el flujo.
  1. Tu servidor genera un state aleatorio y un code_verifier, guarda los dos (ligados a la sesión del comerciante) y calcula code_challenge = base64url(SHA-256(code_verifier)).
  2. Tu página de admin abre un popup (window.open, con el clic del comerciante) en https://admin.spaidoo.com/connect con client_id, redirect_uri, state, code_challenge, code_challenge_method=S256, el domain de la tienda y, si quieres, name, email, mode y verify (tu urls.verify, solo con domainVerification).
  3. El comerciante se registra o inicia sesión en Spaidoo y acepta los términos.
  4. Spaidoo envía el popup a tu redirect_uri con code y state (o error=access_denied si el comerciante cancela). Comprueba el state.
  5. Tu servidor llama a POST /pairing/token con client_id, code, code_verifier y la misma redirect_uri, en menos de 60 segundos, y recibe { apiKey, shopId }.
  6. Guarda la key y llama a POST /handshake (siguiente paso). La página de vuelta avisa a tu admin con postMessage (solo a tu propio origen y nunca con la key) y cierra el popup.
JavaScript
import { createHash, randomBytes } from "node:crypto";

const CLIENT_ID = "sc_…"; // from My connectors
const REDIRECT_URI = "https://shop.example.com/admin/spaidoo/callback";

// In your admin page: the merchant's click opens the popup.
//   const popup = window.open("/admin/spaidoo/connect", "spaidoo", "width=460,height=780");
//   window.addEventListener("message", (e) => {
//     if (e.origin !== location.origin || e.source !== popup) return;
//     if (e.data?.type === "spaidoo-connected" && e.data.connected) location.reload();
//   });

// 1. The popup starts here and goes on to Spaidoo.
app.get("/admin/spaidoo/connect", (req, res) => {
  const state = randomBytes(16).toString("hex");
  const verifier = randomBytes(48).toString("base64url");
  req.session.spaidoo = { state, verifier };
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  res.redirect(
    "https://admin.spaidoo.com/connect?" +
      new URLSearchParams({
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        state,
        code_challenge: challenge,
        code_challenge_method: "S256",
        domain: "shop.example.com",
        name: "My shop",
        email: "merchant@example.com",
        verify: "https://shop.example.com/spaidoo/verify", // only with domainVerification
      }),
  );
});

// 2. The popup comes back from Spaidoo.
app.get("/admin/spaidoo/callback", async (req, res) => {
  const pending = req.session.spaidoo;
  if (!pending || req.query.state !== pending.state) return res.status(400).send("Start again");

  // A shop of Spaidoo's directory proves its domain first (§3.3).
  if (req.query.verification_code) {
    store.set("verificationCode", req.query.verification_code); // served at urls.verify
    return res.redirect(req.query.resume); // back to Spaidoo, same state
  }

  delete req.session.spaidoo;
  let connected = false;
  if (!req.query.error) {
    const answer = await fetch("https://api-plugin.spaidoo.com/connector/v1/pairing/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        client_id: CLIENT_ID,
        code: req.query.code,
        code_verifier: pending.verifier,
        redirect_uri: REDIRECT_URI,
      }),
    }).then((r) => r.json());
    store.set("apiKey", answer.apiKey); // server-side, never in a page
    await handshake(); // next step
    connected = true;
  }

  // 3. Tell the admin page (same origin, never the key) and close the popup.
  res.send(`<script>
    window.opener?.postMessage({ type: "spaidoo-connected", connected: ${connected} }, location.origin);
    window.close();
  </script>`);
});

Una tienda que ya está en el directorio de Spaidoo

Si el dominio ya pertenece a una tienda del directorio de Spaidoo, solo se entrega cuando se demuestra el dominio. Cuando pasas verify, Spaidoo vuelve primero a tu vuelta con verification_code y resume: guarda el código para que urls.verify lo sirva (paso 9), conserva el code_verifier y envía el popup a resume. El código llega después, con el mismo state (§3.3).

Qué guardar, y dónde

  • La API key, en el servidor (la configuración o la base de datos de tu plataforma), nunca en una página. Va como Authorization: Bearer <apiKey> en cada llamada a https://api-plugin.spaidoo.com/connector/v1 (§2, §2.1).
  • Un 401 o un 403 de Spaidoo significa que la key ya no es válida: muestra la tienda como desconectada. Un 5xx o un fallo de red solo significa «prueba más tarde»: la tienda tiene que seguir funcionando cuando Spaidoo no responde (§2).

Cómo sabes que funciona

  • Tu servidor tiene una key y GET /status con ella responde 200 con el store de la tienda (§4.3).

Paso 4 de 11

El handshake

Decirle a Spaidoo qué puede hacer tu conector y dónde, y quedarte con lo que acepta.

POST /handshake declara tus capacidades, las URL de tus rutas y los ajustes de la tienda. Es idempotente (§4.1).

El handshake: declarar, quedarse con lo aceptado, confirmar el secreto1. Servidor de la tienda → API de Spaidoo: POST /handshake capabilities, urls, storefront 2. API de Spaidoo → Servidor de la tienda: 200 shopId, capacidades aceptadas, identitySecret (cuando toca) 3. Servidor de la tienda: Guarda el id de tienda, las capacidades aceptadas y el secreto 4. Servidor de la tienda → API de Spaidoo: POST /identity-secret/ack { fingerprint } 5. API de Spaidoo → Servidor de la tienda: { acknowledged: true } 6. Servidor de la tienda → API de Spaidoo: GET /status 7. API de Spaidoo → Servidor de la tienda: connector: tu declaración, sin identitySecret si confirmadoServidor de la tiendaAPI de SpaidooPOST /handshakecapabilities, urls, storefront1200 shopId, capacidades aceptadas,identitySecret (cuando toca)23Guarda el id de tienda,las capacidades aceptadasy el secretoPOST /identity-secret/ack{ fingerprint }4{ acknowledged: true }5GET /status6connector: tu declaración,sin identitySecret si confirmado7
Spaidoo responde con las capacidades que ha aceptado y, cuando toca, el identity secret, que sigue devolviendo hasta que lo confirmas.

Empieza por poco. Este cuerpo declara solo la capacidad obligatoria; activa las demás a medida que las construyas:

JSON
{
  "contractVersion": 1,
  "platform": { "id": "my-platform", "version": "3.2.0" },
  "connector": { "version": "1.0.0" },
  "storeUrl": "https://shop.example.com",
  "capabilities": {
    "catalog.feed": true,
    "catalog.changes": false,
    "identity": false,
    "customers": false,
    "customerErase": false,
    "wishlistImport": false,
    "push": false,
    "domainVerification": false,
    "storefront.saveAnchors": false
  },
  "urls": {},
  "storefront": { "excludePaths": ["^/checkout", "^/cart"] },
  "identitySecretFingerprint": null
}
  • Declara solo lo que implementas. Una URL solo es obligatoria para una capacidad declarada que la necesite. Todas tienen que ser https: las de escaparate en uno de los dominios de la tienda, las de servidor (customers) allí o en los dominios de servidor de tu app. storefront.domains añade otros dominios en los que se sirve el escaparate (§4.1).
  • login tiene que contener la marca literal {back}, sin codificar (paso 6).
  • storefront.excludePaths: expresiones regulares (sin distinguir mayúsculas) sobre la ruta + query donde el widget no debe ejecutarse nunca (checkout, pago).
  • identitySecretFingerprint: el SHA-256 (hex) del identity secret que guardas, o null si no guardas ninguno. Así un conector reinstalado consigue un secreto nuevo por su cuenta.

Spaidoo responde con el id de la tienda, las capacidades que ha aceptado y, cuando tienes que guardar uno nuevo, el identity secret:

JSON
{
  "shopId": "7f3c…",
  "contractVersion": 1,
  "capabilities": { "catalog.feed": true },
  "identitySecret": "b64url…"
}
JavaScript
import { createHash } from "node:crypto";

const API = "https://api-plugin.spaidoo.com/connector/v1";

async function spaidoo(method, path, body) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${store.get("apiKey")}`, "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (res.status === 401 || res.status === 403) store.set("connected", false); // show "disconnected"
  if (!res.ok) throw new Error(`Spaidoo ${res.status}`); // 5xx, network: try later, never break the shop
  return res.status === 204 ? null : res.json();
}

const answer = await spaidoo("POST", "/handshake", handshakeBody());
store.set("shopId", answer.shopId);
// Act on what Spaidoo accepted, not on what you declared.
store.set("capabilities", answer.capabilities);

if (answer.identitySecret) {
  store.set("identitySecret", answer.identitySecret); // server-side only, never printed
  const fingerprint = createHash("sha256").update(answer.identitySecret).digest("hex");
  await spaidoo("POST", "/identity-secret/ack", { fingerprint });
}
  • Compórtate según la lista aceptada. Una capacidad se rechaza (se omite, no es un error) cuando Spaidoo la tiene desactivada; customers y wishlistImport se rechazan sin identity.
  • Confirma cada secreto que recibas, después de guardarlo: POST /identity-secret/ack con su huella. Hasta entonces, Spaidoo lo sigue devolviendo en /handshake y /status (§4.2).
  • Tras una rotación, Spaidoo sigue aceptando el secreto anterior hasta que se confirma el nuevo, y 10 minutos más: una respuesta perdida no rompe la tienda.

Cuándo repetir el handshake

  • Después del emparejamiento.
  • Después de cada actualización de tu conector.
  • Cada vez que cambie una URL o una capacidad.

Errores que hay que tratar: 400 CONNECTOR_CATALOG_REQUIRED (sin catalog.feed), 400 CONNECTOR_URL_INVALID con el field que falla, y 400 CONNECTOR_EXCLUDE_PATH_INVALID.

Cómo sabes que funciona

  • POST /handshake responde 200 con tus capacidades en capabilities.
  • GET /status muestra tu declaración en connector y deja de traer identitySecret cuando lo has confirmado.
  • La comprobación de conformidad (paso 10) comprueba el handshake.

Paso 5 de 11

El catálogo

Spaidoo conoce los productos de la tienda: por un feed que lee cada día, y después por los cambios que le envías.

El feed: catalog.feed (obligatoria)

Un feed de productos de Google Merchant (RSS 2.0 o Atom) con un ítem por variante. Sirve uno por idioma y registra la lista completa: un idioma que no esté en ella se elimina (§4.5).

HTTP
PUT /connector/v1/catalog/feeds
Authorization: Bearer <apiKey>
Content-Type: application/json

{ "feeds": [ { "language": "en", "url": "https://shop.example.com/spaidoo/feed?lang=en" } ] }
XML
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
  <channel>
    <title>My shop</title>
    <link>https://shop.example.com</link>
    <description>My shop's products</description>
    <item>
      <g:id>123-1</g:id>
      <g:item_group_id>123</g:item_group_id>
      <g:title>Blue shirt</g:title>
      <g:link>https://shop.example.com/p/123</g:link>
      <g:price>24.90 EUR</g:price>
      <g:sale_price>19.90 EUR</g:sale_price>
      <g:availability>in_stock</g:availability>
      <g:image_link>https://shop.example.com/img/123.jpg</g:image_link>
    </item>
  </channel>
</rss>
  • g:item_group_id es el id de producto para Spaidoo (una fila por producto); sin él, lo es g:id. Ese id de producto es el que se usa en todo lo demás: data-sp-id, catalog.changes, importación de wishlists.
  • Obligatorios: g:id, g:title, g:link (la URL canónica del producto), g:price en la moneda base de la tienda y g:availability (in_stock · out_of_stock · preorder · backorder).
  • Los precios en otras monedas van en spaidoo:price currency="USD" (namespace https://spaidoo.com/ns/feed/1).
El feed: se registra una vez y se lee más o menos una vez al día1. Servidor de la tienda → API de Spaidoo: PUT /catalog/feeds una URL de feed por idioma 2. Sync de Spaidoo → Servidor de la tienda: GET <url del feed> una vez al día, más o menos 3. Servidor de la tienda → Sync de Spaidoo: 202 + Retry-After todavía se genera 4. Sync de Spaidoo: Vuelve tras el Retry-After; conserva el catálogo 5. Sync de Spaidoo → Servidor de la tienda: GET <url del feed> 6. Servidor de la tienda → Sync de Spaidoo: 200 RSS / Atom, un ítem por varianteServidor de la tiendaAPI de SpaidooSync de SpaidooPUT /catalog/feedsuna URL de feed por idioma1GET <url del feed>una vez al día, más o menos2202 + Retry-Aftertodavía se genera34Vuelve tras elRetry-After; conservael catálogoGET <url del feed>5200 RSS / Atom,un ítem por variante6
Mientras se genera un feed grande, un 202 con Retry-After le dice a Spaidoo que vuelva más tarde en lugar de leer un catálogo vacío.

Un feed grande tarda en generarse. Mientras se genera, responde 202 (o 503/429) con Retry-After: Spaidoo vuelve entonces y no lo toma como un catálogo vacío. Cualquier otro error conserva el catálogo anterior.

HTTP
HTTP/1.1 202 Accepted
Retry-After: 120

POST /catalog/feeds/read pide una lectura ya (un botón en tu panel, por ejemplo). Si se vuelve a pedir en menos de 3 minutos, responde 429 con Retry-After.

Cambios de catálogo: catalog.changes

Cuando cambia un precio o el stock en la tienda, díselo a Spaidoo en el momento: los avisos de bajada de precio salen en minutos en lugar de en la siguiente lectura diaria, que se queda como conciliación (§4.6).

Cambios de catálogo: se anotan durante la petición y se envían al final1. Back-office de la tienda → Servidor de la tienda: El comerciante guarda precios o stock 2. Servidor de la tienda: Anota los ids de producto, responde al comerciante 3. Servidor de la tienda → API de Spaidoo: al final de la petición POST /catalog/changes 4. API de Spaidoo → Servidor de la tienda: 202 { accepted, unknown } 5. API de Spaidoo: Ids desconocidos: programa una lecturaBack-office de la tiendaServidor de la tiendaAPI de SpaidooEl comerciante guardaprecios o stock12Anota los ids de producto,responde al comercianteal final de la peticiónPOST /catalog/changes3202 { accepted, unknown }45Ids desconocidos:programa una lectura
El comerciante nunca espera a Spaidoo. La lectura diaria del feed se queda como conciliación.
JSON
{
  "changes": [
    {
      "type": "product.updated",
      "id": "123",
      "at": 1759219200,
      "price": { "amount": "24.90", "currency": "EUR" },
      "salePrice": { "amount": "19.90", "currency": "EUR" },
      "availability": "in_stock"
    },
    { "type": "product.deleted", "id": "456", "at": 1759219230 }
  ]
}
  • product.updated lleva solo price, salePrice (null = sin oferta), prices en otras monedas, availability y las variants que han cambiado, por sku. Todos los campos son opcionales; uno presente sustituye el valor guardado.
  • product.deleted: el producto ya no se vende (borrado o desactivado).
  • at es cuándo ocurrió el cambio. Spaidoo ignora un cambio más antiguo que el que ya aplicó, así que los reintentos y la entrega desordenada son seguros.
  • Hasta 100 cambios por petición y 60 peticiones por minuto. Agrupa las ráfagas: una edición masiva de 2 000 productos son 20 peticiones, no 2 000.
  • Los productos nuevos, y los cambios de título, enlace, imágenes o categorías, llegan con la siguiente lectura del feed.

Anota los ids de producto mientras corre la petición del comerciante y envíalos al final, cuando la respuesta ya ha salido, así el comerciante nunca espera a Spaidoo:

JavaScript
// While the merchant's request runs: only note which products changed.
const pending = new Map(); // product id → unix time of the change

onProductSaved((product) => pending.set(String(product.id), Math.floor(Date.now() / 1000)));

// Once the response has gone out: up to 100 changes per call.
onRequestEnd(async () => {
  if (!store.get("capabilities")["catalog.changes"]) return;
  const changes = [...pending].map(([id, at]) => changeOf(id, at)); // product.updated or product.deleted
  for (let i = 0; i < changes.length; i += 100) {
    await spaidoo("POST", "/catalog/changes", { changes: changes.slice(i, i + 100) });
  }
});

Spaidoo responde 202 { accepted, unknown }: unknown lista los ids que todavía no tiene, y programa una lectura del feed. Si falla, reintenta con espera creciente durante 1 hora como máximo y luego descártalo: la lectura diaria del feed lo corrige todo.

Cómo sabes que funciona

  • GET /status lista tus feeds en store.feeds, con lastReadAt, products y status.
  • La comprobación de conformidad lee tus feeds y sus ítems.
  • POST /catalog/changes responde 202 con unknown vacío para productos que Spaidoo conoce. Es una llamada a Spaidoo: la comprobación la lista con cómo comprobarla a mano (§8).

Paso 6 de 11

Identidad del cliente

Un cliente con sesión iniciada en la tienda conserva sus productos guardados en todos sus dispositivos, sin cuenta de Spaidoo.

Mientras la tienda tiene un conector con identity, manda la sesión de la tienda: Spaidoo nunca le pide al cliente que inicie sesión en la tienda en su nombre (§5).

Identidad: la tienda dice quién navega, firmado con el identity secret1. Navegador del cliente → Servidor de la tienda: GET <urls.identity>?anon=<uuid> mismo origen, cookies de la tienda 2. Servidor de la tienda → Navegador del cliente: 200 { assertion } con sesión 200 { anonymous: true } nadie 3. Navegador del cliente → API de Spaidoo: El widget entrega la aserción a Spaidoo 4. API de Spaidoo: Comprueba la firma, exp y jti 5. Navegador del cliente: Nadie con sesión, y el cliente quiere conservar la lista 6. Navegador del cliente → Servidor de la tienda: GET <urls.login> {back} = la página actual 7. Servidor de la tienda → Navegador del cliente: Tras iniciar sesión: vuelve a esa páginaNavegador del clienteServidor de la tiendaAPI de SpaidooGET <urls.identity>?anon=<uuid>mismo origen, cookies de la tienda1200 { assertion } con sesión200 { anonymous: true } nadie2El widget entrega laaserción a Spaidoo34Comprueba la firma,exp y jti5Nadie con sesión, yel cliente quiereconservar la listaGET <urls.login>{back} = la página actual6Tras iniciar sesión:vuelve a esa página7
La ruta de identidad se llama desde la tienda con sus propias cookies. Cuando no hay nadie con sesión y el cliente quiere conservar su lista, el widget lo manda al inicio de sesión de la tienda.

La ruta de identidad

El widget llama a GET <urls.identity>?anon=<uuid> desde la tienda: mismo origen, con las cookies de la tienda. Responde 200 { "assertion": "…" } cuando hay un cliente con sesión iniciada, 200 { "anonymous": true } cuando no hay nadie (los invitados cuentan como nadie), y 410 cuando el conector está desconectado (§5.1).

Envía Cache-Control: no-store, private y Vary: Cookie. La ruta no debe guardarla nunca una caché de página ni un CDN.

La aserción

Un payload JSON y su HMAC-SHA256 con el identity secret (como texto UTF-8), los dos en base64url sin relleno, unidos por un punto (§5.2):

JavaScript
import { createHmac, randomBytes } from "node:crypto";

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const b64url = (text) => Buffer.from(text).toString("base64url"); // no padding

function assertion(secret, shopId, customerId, anon) {
  const iat = Math.floor(Date.now() / 1000);
  const payload = b64url(JSON.stringify({
    v: 1,
    shopId,
    customerId: String(customerId),
    email: "", // always empty: Spaidoo asks the customers route when it needs one
    anonId: UUID.test(anon ?? "") ? anon : null,
    iat,
    exp: iat + 60,
    jti: randomBytes(16).toString("hex"),
  }));
  const signature = createHmac("sha256", secret).update(payload).digest("base64url");
  return `${payload}.${signature}`;
}

// GET <urls.identity>?anon=<uuid> — same origin, with the shop's cookies.
app.get("/spaidoo/identity", (req, res) => {
  res.set({ "Cache-Control": "no-store, private", Vary: "Cookie" });
  if (!store.get("connected")) return res.sendStatus(410);
  const customer = signedInCustomer(req); // a guest counts as nobody
  if (!customer) return res.json({ anonymous: true });
  res.json({ assertion: assertion(store.get("identitySecret"), store.get("shopId"), customer.id, req.query.anon) });
});
  • exp - iat es 60 s como máximo; Spaidoo tolera 30 s de desfase de reloj. jti es de un solo uso.
  • anonId repite ?anon= cuando es un UUID; si no, null.
  • email es siempre "": Spaidoo no guarda los emails de los clientes de la tienda (paso 7).

La pista de sesión (opcional, recomendada)

Para que el widget note un inicio o un cierre de sesión sin preguntar en cada página, imprime SpaidooConfig.shopSession (paso 8): "" cuando no hay nadie con sesión iniciada; si no, los primeros 16 caracteres hex de HMAC-SHA256(identity secret, "session:" + customerId). Es una pista, no una prueba (§5.3).

JavaScript
const shopSession = customer
  ? createHmac("sha256", secret).update("session:" + customer.id).digest("hex").slice(0, 16)
  : "";

La URL de inicio de sesión

Declara urls.login con la marca literal {back}. Cuando un cliente sin sesión quiere conservar su lista, el widget lo manda allí con {back} sustituido por la página donde estaba, codificada como URL. Después del inicio de sesión, la tienda tiene que redirigir de vuelta a ella, solo en el mismo dominio (§5.4).

Cómo sabes que funciona

  • Haz curl a la ruta de identidad sin cookies: {"anonymous": true} y las dos cabeceras de caché.
  • Con la cabecera Cookie de un cliente de prueba con sesión iniciada: una aserción cuyo payload (decodifica en base64url la parte anterior al punto) lleva tu shopId, el customerId y "email": "".
  • La comprobación de conformidad prueba la ruta y sus aserciones cuando le das esa cabecera Cookie.

Paso 7 de 11

La ruta customers

Spaidoo puede pedirle a la tienda el email y el consentimiento de marketing de clientes identificados, y nadie más puede.

Spaidoo no guarda los emails de los clientes de la tienda. Cuando el comerciante los necesita (una exportación, la audiencia de una campaña, una sincronización con su herramienta de email), Spaidoo pregunta a tu urls.customers, usa la respuesta en ese momento y no la guarda (§6.2).

La ruta customers: Spaidoo pregunta, firmado, cuando el comerciante necesita emails1. API de Spaidoo: El comerciante necesita emails: exportación, audiencia, sync 2. API de Spaidoo → Servidor de la tienda: POST <urls.customers> X-Spaidoo-Timestamp · Nonce · Signature { customerIds: [...] } 3. Servidor de la tienda: Timestamp, firma (cuerpo en bruto), nonce 4. Servidor de la tienda → API de Spaidoo: 200 { customers: [...] } en menos de 5 s 5. API de Spaidoo: Los usa en el momento, no los guardaAPI de SpaidooServidor de la tienda1El comerciante necesita emails:exportación, audiencia, syncPOST <urls.customers>X-Spaidoo-Timestamp · Nonce · Signature{ customerIds: [...] }23Timestamp, firma(cuerpo en bruto), nonce200 { customers: [...] }en menos de 5 s45Los usa en el momento,no los guarda
Spaidoo usa la respuesta en ese momento y no guarda los emails.

Verifica cada llamada

Cada llamada de Spaidoo a una URL del conector (salvo las públicas verify y feed) lleva X-Spaidoo-Timestamp, X-Spaidoo-Nonce y X-Spaidoo-Signature: el HMAC-SHA256 en base64url de timestamp + "." + nonce + "." + rawBody con el identity secret (§6.1).

Verificar una llamada firmada de SpaidooLlega una llamada firmada. ¿Timestamp a menos de 300 s de nuestro reloj?. ¿Firma del cuerpo en bruto igual, en tiempo constante?. ¿Nonce no visto en los últimos 600 s?. Responde { customers }. 401. sí. no. sí. no. sí. noLlega unallamada firmada¿Timestamp a menosde 300 s denuestro reloj?¿Firma del cuerpoen bruto igual, entiempo constante?¿Nonce no vistoen los últimos600 s?Responde{ customers }401sínosínosíno
Cualquier comprobación que falla responde 401 antes siquiera de leer el cuerpo como JSON.
JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";

// The RAW body: the signature covers the bytes Spaidoo sent, not re-serialised JSON.
app.post("/spaidoo/customers", express.raw({ type: "*/*" }), async (req, res) => {
  const secret = store.get("identitySecret");
  if (!store.get("connected") || !secret) return res.sendStatus(410);

  const timestamp = req.get("X-Spaidoo-Timestamp") ?? "";
  const nonce = req.get("X-Spaidoo-Nonce") ?? "";
  const signature = Buffer.from(req.get("X-Spaidoo-Signature") ?? "");

  // 1. A timestamp more than 300 s away from our clock.
  const now = Math.floor(Date.now() / 1000);
  if (!/^\d+$/.test(timestamp) || Math.abs(now - Number(timestamp)) > 300) return res.sendStatus(401);

  // 2. The signature, compared in constant time.
  const expected = Buffer.from(
    createHmac("sha256", secret).update(`${timestamp}.${nonce}.`).update(req.body).digest("base64url"),
  );
  if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) return res.sendStatus(401);

  // 3. A nonce seen in the last 600 s is a replay.
  if (!(await nonces.addIfNew(nonce, 600))) return res.sendStatus(401);

  const { customerIds } = JSON.parse(req.body.toString("utf8"));
  const customers = await findAccounts(customerIds); // no guests, no deleted ones
  res.json({
    customers: customers.map((c) => ({
      customerId: String(c.id),
      email: c.email,
      marketingConsent: c.newsletter ?? null, // null: the platform does not know
    })),
  });
});
  • Firma sobre el cuerpo en bruto, tal como llega: parsear y volver a serializar el JSON cambia los bytes.
  • Compara las firmas en tiempo constante.
  • Recuerda los nonces 600 s y rechaza uno repetido, para que una petición capturada no se pueda reenviar.

La respuesta

JSON
{ "customers": [ { "customerId": "123", "email": "ana@example.com", "marketingConsent": true } ] }
  • Hasta 500 ids por llamada. Omite los clientes que ya no existen, los invitados y los borrados.
  • marketingConsent: true el cliente aceptó los emails de marketing de la tienda, false no los aceptó, null la plataforma no lo sabe.
  • Responde en menos de 5 s; 410 si el conector está desconectado.

Cómo sabes que funciona

  • Un POST sin firmar a la ruta responde 401.
  • Con un id de cliente de la tienda de prueba, la comprobación de conformidad prueba el nonce, la firma, el cuerpo en bruto, la antigüedad del timestamp y los límites.

Paso 8 de 11

La tienda

Imprimir el widget en las páginas de la tienda, nunca en el checkout, y servir el service worker de push.

La tienda: lo que imprime y sirve al navegador del cliente1. Navegador del cliente → Servidor de la tienda: GET una página de producto 2. Servidor de la tienda: ¿Checkout o pago? Ahí no se imprime nada 3. Servidor de la tienda → Navegador del cliente: HTML con SpaidooConfig, el loader, anclajes 4. Navegador del cliente → Widget de Spaidoo: carga el widget scriptUrl 5. Navegador del cliente: Dibuja los iconos de guardar en los anclajes 6. Navegador del cliente → Servidor de la tienda: GET <urls.pushWorker> registra el worker de push 7. Servidor de la tienda → Navegador del cliente: Service-Worker-Allowed: <pushScope>Navegador del clienteServidor de la tiendaWidget de SpaidooGET una página de producto12¿Checkout o pago?Ahí no se imprime nadaHTML con SpaidooConfig,el loader, anclajes3carga el widgetscriptUrl45Dibuja los iconos deguardar en los anclajesGET <urls.pushWorker>registra el worker de push6Service-Worker-Allowed:<pushScope>7
Las páginas de checkout y de pago no reciben nada de Spaidoo; excludePaths es la red de seguridad.

En todas las páginas salvo las excluidas, imprime la configuración y el loader. scriptUrl viene de GET /status y puede cambiar: usa siempre el último (§4.3, §7).

HTML
<script data-spaidoo-config='{"currency":"EUR","shopSession":"","push":{"swUrl":"https://shop.example.com/spaidoo/push-sw.js","swScope":"/spaidoo-push/"}}'>
  (function (s) { window.SpaidooConfig = Object.assign(window.SpaidooConfig || {}, JSON.parse(s.getAttribute('data-spaidoo-config'))); })(document.currentScript);
</script>
<script src="{scriptUrl}" async></script>
  • currency: el código ISO de la moneda en la que navega el visitante; el widget muestra los precios en ella (de spaidoo:price).
  • shopSession: la pista de sesión del paso 6.
  • preview: true solo para el personal de la tienda mientras el widget está en modo preview.
  • push: { swUrl, swScope } cuando se declara push.
  • Una caché de página completa tiene que guardar una copia por moneda, como ya hace con los precios.

No imprimas nunca el script en las páginas de checkout o de pago. Inclúyelas también en storefront.excludePaths en el handshake: es la red de seguridad si se escapa alguna página.

Botones de guardar (opcional)

Donde el tema deba mostrar el icono de guardar de Spaidoo, imprime un ancla vacía con el id de producto del feed: el widget dibuja el icono y gestiona el clic y el estado. Dale estilo con las propiedades CSS --sp-save-* y declara storefront.saveAnchors. Cualquier elemento con data-spaidoo-library abre la lista del cliente.

HTML
<span data-spaidoo-save
      data-sp-context="product"
      data-sp-id="123"
      data-sp-url="https://shop.example.com/p/123"
      data-sp-name="Blue shirt"
      data-sp-image="https://shop.example.com/img/123.jpg"></span>

data-sp-context es "product" en la página de producto y "list" en las fichas de producto; data-sp-name y data-sp-image son opcionales.

El service worker de push: push

El web push se registra en el propio dominio de la tienda, así que el conector sirve el service worker de Spaidoo en urls.pushWorker (§7.1):

HTTP
GET /spaidoo/push-sw.js

HTTP/1.1 200 OK
Content-Type: application/javascript
Service-Worker-Allowed: /spaidoo-push/
Cache-Control: no-cache

<the reference worker, copied verbatim from https://cdn.spaidoo.com/connector/v1/push-sw.js>
  • El cuerpo es el worker de referencia publicado en https://cdn.spaidoo.com/connector/v1/push-sw.js: cópialo tal cual; no lo cargues con importScripts desde otro origen.
  • Recibe mensajes solo de datos { title, body, link, icon, image, badge, shopId }.
  • Service-Worker-Allowed solo hace falta cuando pushScope está fuera del directorio del propio worker. Un worker servido dentro de su alcance (detrás del proxy de apps de una plataforma, por ejemplo: /apps/spaidoo/push-sw.js con pushScope /apps/spaidoo/) va sin ella.

Cómo sabes que funciona

  • El código fuente de una página de producto muestra data-spaidoo-config y el loader; el del carrito y el checkout, ninguno de los dos.
  • curl -I <urls.pushWorker> muestra las cabeceras.
  • Con una página de producto y la URL del carrito o del checkout, la comprobación de conformidad prueba el loader, SpaidooConfig, los anclajes de guardado, que no se imprime nada en el checkout, y el service worker de push.

Paso 9 de 11

Verificación de dominio, borrado e importación de wishlists

Tres capacidades cortas: demostrar el dominio, trasladar los borrados, traer las wishlists antiguas.

Verificación de dominio: domainVerification

La tienda demuestra que es dueña de su dominio sin cambios de DNS. Pide un código a Spaidoo, sírvelo en urls.verify como text/plain sin nada más en el cuerpo, y pide a Spaidoo que lo compruebe (§4.9).

Verificación de dominio: Spaidoo lee el código en la tienda1. Servidor de la tienda → API de Spaidoo: POST /domain-verification 2. API de Spaidoo → Servidor de la tienda: { verificationCode, domain } 3. Servidor de la tienda: Guarda el código y sírvelo en urls.verify como text/plain 4. Servidor de la tienda → API de Spaidoo: POST /domain-verification/check 5. API de Spaidoo → Servidor de la tienda: GET <urls.verify> 6. Servidor de la tienda → API de Spaidoo: spaidoo-verify-… 7. API de Spaidoo → Servidor de la tienda: { verified: true }Servidor de la tiendaAPI de SpaidooPOST /domain-verification1{ verificationCode, domain }23Guarda el código ysírvelo en urls.verifycomo text/plainPOST /domain-verification/check4GET <urls.verify>5spaidoo-verify-…6{ verified: true }7
La ruta de verificación responde el código como texto plano y nada más; Spaidoo no sigue redirecciones.
HTTP
POST /connector/v1/domain-verification
→ { "verified": false, "verificationCode": "spaidoo-verify-…", "domain": "shop.example.com" }

GET https://shop.example.com/spaidoo/verify
→ 200, Content-Type: text/plain
spaidoo-verify-…

POST /connector/v1/domain-verification/check
→ { "verified": true }
  • urls.verify tiene que estar en el dominio que se demuestra (o en su www.) y no puede contener el código. Spaidoo no sigue redirecciones.
  • Para un dominio que ya está en el directorio de Spaidoo, la prueba va antes que la key (paso 3): la ruta tiene que responder mientras se empareja la tienda (§3.3).
  • Sin la capacidad, el comerciante puede subir igualmente el código como https://<domain>/spaidoo-verify.txt.

Borrado de clientes: customerErase

Cuando la tienda borra un cliente (o recibe una solicitud de supresión RGPD), llama a POST /customers/erase. Spaidoo borra la lista del cliente anclada a esta tienda y desvincula de ella cualquier cuenta de Spaidoo. Los invitados no hace falta enviarlos (§4.7).

HTTP
POST /connector/v1/customers/erase
{ "customerId": "123" }
→ 204

Importación de wishlists: wishlistImport

POST /wishlists/import trae las listas de una extensión de wishlist anterior: hasta 50 clientes por llamada, 100 listas por cliente y 1 000 productos por lista, con los ids de producto del feed. Importar la misma lista dos veces no la duplica. Necesita identity (§4.8).

JSON
{
  "customers": [
    { "customerId": "123", "lists": [
      { "id": "1", "name": "My wishlist", "isDefault": true, "products": ["123", "789"] }
    ] }
  ]
}

Cómo sabes que funciona

  • POST /domain-verification/check responde { "verified": true }, y la comprobación de conformidad prueba la ruta de verificación.
  • POST /customers/erase responde 204. El borrado y la importación son llamadas a Spaidoo: la comprobación las lista con cómo comprobarlas a mano (§8).

Paso 10 de 11

Compruébalo

Pasar la comprobación de conformidad en línea hasta que no haya fallos en ninguna capacidad que declares.

Un conector es compatible con Spaidoo v1 cuando la comprobación de conformidad en línea pasa sin fallos en todas las capacidades que declara. Spaidoo ya tiene tu handshake y tu identity secret: solo pegas la API key de la tienda de prueba, nunca un secreto (§8).

El ciclo de la conformidadTienda de prueba emparejada con tu conector. Ejecuta la comprobación con su API key. Lee el informe correcta · fallo · aviso · omitida. Sin fallos: compatible con Spaidoo v1. Arregla lo que falla (cada línea enlaza la spec). ninguno. fallos. otra vezTienda de pruebaemparejada con tu conectorEjecuta la comprobacióncon su API keyLee el informecorrecta · fallo · aviso · omitidaSin fallos: compatiblecon Spaidoo v1Arregla lo que falla(cada línea enlaza la spec)ningunofallosotra vez
Ejecuta, lee, arregla y vuelve a ejecutar, hasta que ninguna capacidad que declaras tenga fallos.
  1. Abre la comprobación de conformidad y pega la API key de la tienda de prueba.
  2. Rellena los campos opcionales para probar más: la URL de una página de producto (loader, SpaidooConfig, anclajes de guardado), la URL del carrito o del checkout (ahí no se imprime nada), la cabecera Cookie de un cliente de prueba con sesión iniciada (identidad) y un id de cliente (ruta customers).
  3. Ejecútala: tarda un par de minutos.
  4. Lee el informe, arregla lo que falla y vuelve a ejecutarla. Hay diez ejecuciones por hora y tienda.
Captura de un informe de conformidad real de una tienda de prueba local: no conforme, con los totales de comprobaciones correctas, fallos, avisos y omitidas, y las primeras comprobaciones del grupo del handshake, cada una enlazada a su sección de la especificación.
El principio de un informe real de la comprobación de conformidad, ejecutada contra una tienda de prueba local. Cada línea enlaza su sección de la especificación.

Cómo leer el informe

MarcaQué significa
CorrectaLa comprobación ha pasado.
FalloEl conector no hace lo que pide la especificación. La línea enlaza su sección: arréglalo y vuelve a ejecutarla. Basta un fallo para no ser conforme.
AvisoNo es un fallo: la conformidad no depende de él, pero léelo.
OmitidaNo se ha comprobado en esta ejecución. Rellena los campos opcionales para que se compruebe todo lo que declaras.

Las llamadas que tu conector hace a Spaidoo (catalog.changes, customerErase, wishlistImport) no se pueden probar desde fuera: el informe las lista con cómo comprobarlas a mano.

La misma comprobación está abierta a tus propias herramientas: POST /connector/v1/conformance inicia una ejecución (202 { runId }) y GET /connector/v1/conformance/{runId} devuelve el informe, que se guarda una hora.

Cómo sabes que funciona

  • El informe dice Conforme con la Connector API v1.

Referencia

Especificación

Paso 11 de 11

¿Atascado?

Pregunta al asistente: responde a partir de la especificación.

Los partners de Spaidoo con acceso al área de desarrolladores pueden hablar con el asistente de la Connector API. Inicia sesión con tu cuenta de partner de Spaidoo: el acceso lo concede Spaidoo. Sus respuestas enlazan las secciones de la especificación, y tus conversaciones se guardan.

Captura del asistente de la Connector API: la lista de conversaciones a la izquierda y una conversación con un informe de conformidad adjunto y una respuesta que enlaza secciones de la especificación.
El asistente, con un informe de conformidad adjunto a la pregunta (conversación de ejemplo).

Desde un informe de la comprobación de conformidad, Pregunta al asistente por este informe abre una conversación nueva con el informe adjunto y una pregunta preparada: ¿por qué fallan estas comprobaciones y cómo lo arreglo?

¿Todavía sin acceso? Todo lo que sabe el asistente está en la especificación, que el recuadro Referencia de cada paso enlaza.

Cómo sabes que funciona

  • Las respuestas del asistente citan las secciones de la especificación como enlaces §x.y.

Referencia

Especificación