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).
| Capacidad | Qué le da a la tienda | ¿Hace falta? |
|---|---|---|
catalog.feed | Spaidoo conoce los productos de la tienda: precio, stock, imágenes. | Obligatoria |
catalog.changes | Los cambios de precio y de stock llegan a Spaidoo en segundos en lugar de en la siguiente lectura diaria. | Opcional |
identity | Un cliente con sesión iniciada conserva sus productos guardados en todos sus dispositivos, sin cuenta de Spaidoo. | Opcional |
customers | Spaidoo puede pedir el email y el consentimiento de marketing de un cliente identificado cuando el comerciante lo necesita. | Opcional, con identity |
customerErase | Borrar un cliente en la tienda borra sus datos en Spaidoo para esa tienda. | Opcional |
wishlistImport | Las listas de una extensión de wishlist anterior se traen a Spaidoo. | Opcional, con identity |
push | Las notificaciones web push se sirven desde el propio dominio de la tienda. | Opcional |
domainVerification | La tienda demuestra que es dueña de su dominio sin cambios de DNS. | Opcional |
storefront.saveAnchors | Tu 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.feedes 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:
localhostno 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.
# Either one, pointing at the shop on your machine (port 8080 here)
ngrok http 8080
cloudflared tunnel --url http://localhost:8080Configura 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.
| Tipo | Cuándo | Vuelta |
|---|---|---|
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
platformde la app (my-platform) es texto libre, solo informativo.
Cómo sabes que funciona
- Mis conectores muestra tu app con su
client_id.
Referencia
Especificación
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).
- Tu servidor genera un
statealeatorio y uncode_verifier, guarda los dos (ligados a la sesión del comerciante) y calculacode_challenge = base64url(SHA-256(code_verifier)). - Tu página de admin abre un popup (
window.open, con el clic del comerciante) enhttps://admin.spaidoo.com/connectconclient_id,redirect_uri,state,code_challenge,code_challenge_method=S256, eldomainde la tienda y, si quieres,name,email,modeyverify(tuurls.verify, solo condomainVerification). - El comerciante se registra o inicia sesión en Spaidoo y acepta los términos.
- Spaidoo envía el popup a tu
redirect_uriconcodeystate(oerror=access_deniedsi el comerciante cancela). Comprueba elstate. - Tu servidor llama a
POST /pairing/tokenconclient_id,code,code_verifiery la mismaredirect_uri, en menos de 60 segundos, y recibe{ apiKey, shopId }. - Guarda la key y llama a
POST /handshake(siguiente paso). La página de vuelta avisa a tu admin conpostMessage(solo a tu propio origen y nunca con la key) y cierra el popup.
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 ahttps://api-plugin.spaidoo.com/connector/v1(§2, §2.1). - Un
401o un403de Spaidoo significa que la key ya no es válida: muestra la tienda como desconectada. Un5xxo 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 /statuscon ella responde200con elstorede 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).
Empieza por poco. Este cuerpo declara solo la capacidad obligatoria; activa las demás a medida que las construyas:
{
"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.domainsañade otros dominios en los que se sirve el escaparate (§4.1). logintiene 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, onullsi 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:
{
"shopId": "7f3c…",
"contractVersion": 1,
"capabilities": { "catalog.feed": true },
"identitySecret": "b64url…"
}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;
customersywishlistImportse rechazan sinidentity. - Confirma cada secreto que recibas, después de guardarlo:
POST /identity-secret/ackcon su huella. Hasta entonces, Spaidoo lo sigue devolviendo en/handshakey/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 /handshakeresponde200con tus capacidades encapabilities.GET /statusmuestra tu declaración enconnectory deja de traeridentitySecretcuando 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).
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 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_ides el id de producto para Spaidoo (una fila por producto); sin él, lo esg: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:priceen la moneda base de la tienda yg:availability(in_stock·out_of_stock·preorder·backorder). - Los precios en otras monedas van en
spaidoo:price currency="USD"(namespacehttps://spaidoo.com/ns/feed/1).
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/1.1 202 Accepted
Retry-After: 120POST /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).
{
"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.updatedlleva soloprice,salePrice(null= sin oferta),pricesen otras monedas,availabilityy lasvariantsque han cambiado, porsku. Todos los campos son opcionales; uno presente sustituye el valor guardado.product.deleted: el producto ya no se vende (borrado o desactivado).ates 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:
// 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 /statuslista tus feeds enstore.feeds, conlastReadAt,productsystatus.- La comprobación de conformidad lee tus feeds y sus ítems.
POST /catalog/changesresponde202conunknownvacío para productos que Spaidoo conoce. Es una llamada a Spaidoo: la comprobación la lista con cómo comprobarla a mano (§8).
Referencia
Especificación
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).
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):
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 - iates 60 s como máximo; Spaidoo tolera 30 s de desfase de reloj.jties de un solo uso.anonIdrepite?anon=cuando es un UUID; si no,null.emailes 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).
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
curla la ruta de identidad sin cookies:{"anonymous": true}y las dos cabeceras de caché. - Con la cabecera
Cookiede un cliente de prueba con sesión iniciada: una aserción cuyo payload (decodifica en base64url la parte anterior al punto) lleva tushopId, elcustomerIdy"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).
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).
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
{ "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:trueel cliente aceptó los emails de marketing de la tienda,falseno los aceptó,nullla plataforma no lo sabe.- Responde en menos de 5 s;
410si el conector está desconectado.
Cómo sabes que funciona
- Un
POSTsin firmar a la ruta responde401. - 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.
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).
<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 (despaidoo:price).shopSession: la pista de sesión del paso 6.preview:truesolo para el personal de la tienda mientras el widget está en modopreview.push:{ swUrl, swScope }cuando se declarapush.- 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.
<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):
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 conimportScriptsdesde otro origen. - Recibe mensajes solo de datos
{ title, body, link, icon, image, badge, shopId }. Service-Worker-Allowedsolo hace falta cuandopushScopeestá 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.jsconpushScope/apps/spaidoo/) va sin ella.
Cómo sabes que funciona
- El código fuente de una página de producto muestra
data-spaidoo-configy 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).
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.verifytiene que estar en el dominio que se demuestra (o en suwww.) 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).
POST /connector/v1/customers/erase
{ "customerId": "123" }
→ 204Importació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).
{
"customers": [
{ "customerId": "123", "lists": [
{ "id": "1", "name": "My wishlist", "isDefault": true, "products": ["123", "789"] }
] }
]
}Cómo sabes que funciona
POST /domain-verification/checkresponde{ "verified": true }, y la comprobación de conformidad prueba la ruta de verificación.POST /customers/eraseresponde204. 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).
- Abre la comprobación de conformidad y pega la API key de la tienda de prueba.
- 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 cabeceraCookiede un cliente de prueba con sesión iniciada (identidad) y un id de cliente (ruta customers). - Ejecútala: tarda un par de minutos.
- Lee el informe, arregla lo que falla y vuelve a ejecutarla. Hay diez ejecuciones por hora y tienda.

Cómo leer el informe
| Marca | Qué significa |
|---|---|
| Correcta | La comprobación ha pasado. |
| Fallo | El 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. |
| Aviso | No es un fallo: la conformidad no depende de él, pero léelo. |
| Omitida | No 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.
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.

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.