Crear plugins para WhizzyCommerce
Un plugin es un manifiesto: un nombre, una versión, los ajustes que le pide a un comerciante, y los hooks a los que se engancha. Un manifiesto no lleva código. Un widget de chat es una plantilla de script con los ajustes del comerciante sustituidos dentro; un blog son tipos de contenido, una sección y páginas que dibujamos nosotros. Todo lo que tiene que ejecutarse se ejecuta en su servidor, y lo llamamos por HTTPS firmado.
El manifiesto
{
"slug": "acme-rates",
"name": "Acme live rates",
"version": "1.0.0",
"category": "shipping",
"description": "Live delivery prices from Acme.",
"author": { "name": "Acme", "url": "https://acme.example" },
"docsUrl": "https://acme.example/whizzy",
"placement": "settings",
"settings": [
{ "key": "account", "label": "Acme account", "type": "text" },
{ "key": "api_key", "label": "API key", "type": "text" }
],
"secrets": ["api_key"],
"remote": { "baseUrl": "https://acme.example/whizzy" },
"hooks": {
"shipping": true,
"events": ["order.paid", "order.fulfilled"],
"grants": ["orders.fulfil", "reports.read"]
}
}Categorías: content, marketing, payments, shipping, messaging, integration, other. Los ajustes usan los mismos tipos de campo que los de un tema; una clave nombrada en secrets se sella en reposo, se enmascara en el formulario y nunca se sustituye dentro de un script. Los tipos de sección y de contenido deben ir en el espacio de nombres de su identificador, como acme-rates.banner.
El equipo registra el manifiesto desde la administración y publica una versión. Una versión nueva es un manifiesto nuevo con un número mayor; las tiendas se quedan con la versión que instalaron hasta que su comerciante decide actualizar.
Hooks sin servidor
scripts: una plantilla de JavaScript por disparador, con {{settings.key}} sustituido como literal escapado, puesto en cada página de la tienda salvo la de pago, tras el consentimiento del comprador donde la tienda lo pide. sections: el mismo JSON declarativo que es una sección de tema, listado en el editor de páginas. metaobjects: tipos de contenido que rellena el comerciante. dashboard (una pantalla con create también gana una fila en el menú de más) y routes son para plugins que van dentro de la plataforma; un ajuste de texto con la clave route deja al comerciante mover la primera de esas rutas, que es como un blog pasa a vivir en /journal.
placement dice dónde encuentra el comerciante el plugin una vez instalado: settings (el valor por defecto) es una fila al pie de los Ajustes de la tienda, apuntando al formulario de ajustes del plugin; rail le da un icono propio en la barra del panel, nombrado por icon, con sus pantallas en el panel de al lado. La sección de Plugins en sí nunca crece.
Páginas que un tema puede disponer
Un plugin que trae una página a la tienda, un blog, un localizador de tiendas, un lookbook, les debe una cosa a los temas: su página como datos. En sus propias plantillas pone todo lo que la página muestra en ámbito como plugin, en valores simples, y documenta la forma; un tema dispone la página encima con los nodos normales, igual que dispone un producto o el carrito. Lo que los datos no pueden hacer, un formulario o cualquier otra cosa que se ejecute, es una parte que posee la plataforma y que el tema coloca. Su propia disposición ya hecha está construida exactamente con esos datos y esas partes, así que todo lo que él muestra, un tema puede mostrarlo de otra manera.
El blog es el primero: sus entradas, una entrada, su barra lateral y sus palabras son plugin.list, plugin.post, plugin.sidebar y plugin.labels, y la única parte que necesitó es search-form. Vea la referencia para cada campo.
Hooks con servidor
Ponga remote.baseUrl y hacemos POST de JSON a {baseUrl}/hooks/<name>. Cada petición lleva tres cabeceras:
x-whizzy-timestamp: 1789000000000 milliseconds since the epoch x-whizzy-signature: <hex> HMAC-SHA256 with your signing secret x-whizzy-plugin: acme-rates
La cadena firmada es timestamp + "\n" + "POST" + "\n" + path + "\n" + body, donde path es la ruta de la URL que llamamos (por ejemplo /whizzy/hooks/event) y body son los bytes exactos. Rechace una petición cuya marca de tiempo esté a más de 60 segundos de su reloj, y compare firmas en tiempo constante. Su secreto de firma se muestra una vez, cuando el equipo registra el plugin.
// Node
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, req, rawBody) {
const ts = req.headers["x-whizzy-timestamp"];
if (Math.abs(Date.now() - Number(ts)) > 60000) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}\nPOST\n${req.path}\n${rawBody}`).digest("hex");
const given = String(req.headers["x-whizzy-signature"] ?? "");
return given.length === expected.length
&& timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex"));
}Responda 2xx con JSON. Cualquier otra cosa es un fallo; los eventos se reintentan, las cotizaciones se omiten.
install and uninstall
POST /hooks/install
{ "installId": "ipl_…", "tenantId": "ten_…", "shopUrl": "https://shop.example",
"pluginVersion": "1.0.0", "settings": { "account": "…" },
"accessToken": "…" // first install only, shown once }
POST /hooks/uninstall
{ "installId": "ipl_…", "tenantId": "ten_…" }Una respuesta no-2xx a install rechaza la instalación: el comerciante ve el texto de su respuesta y no se guarda nada. Los ajustes se envían de nuevo en cada actualización; un ajuste secreto no se envía nunca.
event
POST /hooks/event
{ "installId": "ipl_…", "tenantId": "ten_…", "deliveryId": "pdl_…",
"event": "order.paid", "occurredAt": "2026-09-15T21:00:00.000Z",
"payload": { "orderId": "…", "orderNumber": 1042, "grossAmount": 8400,
"grossCurrency": "EUR", "order": { … } } }Eventos: order.paid, order.fulfilled, order.refunded. Entregados al menos una vez, en alrededor de un minuto, y reintentados con espera creciente hasta un día; el deliveryId es el mismo en cada reintento, úselo para ignorar un duplicado.
shipping-quote
POST /hooks/shipping-quote
{ "installId": "…", "tenantId": "…",
"input": { "destination": { "country": "IE", "province": null, "postalCode": "D02" },
"subtotal": 8400, "weightGrams": 1200 },
"lines": [ { "sku": "TOTE-NAT", "quantity": 1, "weightGrams": 400 } ] }
200 { "quotes": [ { "id": "next-day", "name": "Next day", "description": "By 1pm", "price": 795 } ] }Precios en unidades menores de la moneda de la tienda. Se pide en el checkout con tres segundos de tiempo límite; una respuesta lenta son cero cotizaciones, no un checkout lento. Su cotización se ofrece junto a los métodos del propio comerciante, y el pedido registra cuál se eligió como plugin:acme-rates:next-day.
payment/start and payment/check
POST /hooks/payment/start
{ "installId": "…", "tenantId": "…", "orderId": "ord_…", "orderNumber": 1042,
"amount": 8400, "currency": "EUR", "email": "ana@example.com", "shopName": "Kestrel Goods",
"returnUrl": "https://shop.example/checkout/return?order=ord_…",
"cancelUrl": "https://shop.example/checkout?cancelled=1", "webhookUrl": "…" }
200 { "url": "https://pay.example/p/abc", "providerPaymentId": "abc" }
POST /hooks/payment/check
{ "installId": "…", "tenantId": "…", "providerPaymentId": "abc" }
200 { "status": "paid" | "pending" | "failed", "reason": "Declined" }Declare "payment": { "id": "acme-pay", "label": "Pay with Acme" }. La etiqueta es la opción que ve el comprador; elegirla lo manda a su url, que debe ser https. Cuando vuelve a returnUrl llamamos a check, y su respuesta es la autoridad: no hay webhook para un plugin. Responda pending hasta que lo sepa; el comerciante también puede marcar el pedido como pagado a mano.
email/send
POST /hooks/email/send
{ "installId": "…", "tenantId": "…",
"email": { "to": "ana@example.com", "toName": "Ana Silva", "fromName": "Kestrel Goods",
"from": "…", "replyTo": "…", "subject": "…", "html": "…", "text": "…" } }
200 { "messageId": "…" }Declare "email": true y el correo a clientes de la tienda, recibos y avisos de envío, se le entrega a usted en vez de ir por nuestro servidor de correo, un mensaje por llamada. Una respuesta no-2xx se reintenta como lo hace nuestra propia cola. El correo que le enviamos al comerciante se queda de nuestro lado.
sms/send
POST /hooks/sms/send
{ "installId": "…", "tenantId": "…", "to": "+353870000000",
"text": "Kestrel Goods: order #1042 is on its way.", "kind": "order.shipped" }
200 {}Declare "sms": true. Se envía cuando se confirma un pedido y cuando sale un paquete, al número que dio el comprador, con el mejor esfuerzo: un mensaje que no sale no se reintenta.
Llamar a la tienda
El accessToken de la llamada de instalación es un token portador para esa única tienda. Vive lo que viva la instalación y muere con ella. Dos puertas:
# The shop's capability API, the same MCP server merchants connect Claude to
POST https://app.whizzycommerce.com/mcp/{tenantId}
Authorization: Bearer <accessToken>
# A product import key, for a plugin with "hooks": { "import": true }
POST https://app.whizzycommerce.com/api/plugins/{tenantId}/import-key
Authorization: Bearer <accessToken>
200 { "key": "wz_…", "expiresAt": "…", "header": "X-Whizzy-Import-Key",
"endpoints": { "hello": "…/import/hello", "batch": "…/import/batch", "finish": "…/import/finish" } }Lo que el token puede hacer en la API de capacidades es exactamente hooks.grants de su manifiesto, leído de la tienda en cada llamada: catalog.write, pricing.write, orders.fulfil, content.write, design.write, settings.write, customers.write, customers.read, reports.read. Un comerciante que apaga el plugin deja el token sin ningún permiso. Cada llamada de capacidad previsualiza y le pide confirmación al comerciante antes de que pase nada con dinero de por medio, igual que hace con cualquier agente.
La clave de importación funciona igual que la del conector de migración: una tienda, un trabajo, 24 horas, revocable. Pida una nueva cuando caduque.
Empaquetado
Envíe un plugin como zip: manifest.json en la raíz, opcionalmente cover.png (o .jpg, .webp; 2:1, menos de 4 MB) para su tarjeta, opcionalmente sections/*.json con una sección cada uno, que se pliegan en hooks.sections. Un readme es bienvenido y se ignora: lo que lee un comerciante es description y docsUrl en el manifiesto. No hay código en un paquete; lo que se ejecuta es su servidor. El equipo registra el zip en el catálogo, y un plugin se puede descargar de vuelta como el mismo zip.
Temas
Un plugin que cambia el aspecto de una tienda suele ser un tema. Eso es algo distinto, y más sencillo: crear temas.