Creare plugin per WhizzyCommerce
Un plugin è un manifest: un nome, una versione, le impostazioni che chiede a un commerciante, e i hook a cui si aggancia. Un manifest non contiene codice. Un widget di chat è un template di script con le impostazioni del commerciante sostituite dentro; un blog sono tipi di contenuto, una sezione e pagine che disegniamo noi. Tutto quello che deve girare, gira sul suo server, e noi lo chiamiamo su HTTPS firmato.
Il manifest
{
"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"]
}
}Categorie: content, marketing, payments, shipping, messaging, integration, other. Le impostazioni usano gli stessi tipi di campo di quelle di un tema; una chiave nominata in secrets è sigillata a riposo, mascherata nel modulo e mai sostituita dentro uno script. I tipi di sezione e di contenuto devono stare nello spazio dei nomi del suo identificativo, come acme-rates.banner.
Lo staff registra il manifest dall'amministrazione e pubblica una versione. Una versione nuova è un manifest nuovo con un numero più alto; i negozi tengono la versione che hanno installato finché il loro commerciante non decide di aggiornare.
Hook senza server
scripts: un template JavaScript per trigger, con {{settings.key}} sostituito come letterale con escape, messo su ogni pagina della vetrina tranne quella di pagamento, dopo il consenso del cliente dove il negozio lo chiede. sections: lo stesso JSON dichiarativo che è una sezione di tema, elencato nell'editor delle pagine. metaobjects: tipi di contenuto che il commerciante compila. dashboard (una schermata con create ottiene anche una riga nel menu più) e routes sono per i plugin che viaggiano dentro la piattaforma; un'impostazione di testo con chiave route permette al commerciante di spostare il primo di quei percorsi, ed è così che un blog finisce per vivere su /journal.
placement dice dove il commerciante trova il plugin una volta installato: settings (il predefinito) è una riga in fondo alle Impostazioni del negozio, che punta al modulo di impostazioni del plugin; rail gli dà un'icona propria sulla barra del pannello, nominata da icon, con le sue schermate nel pannello accanto. La sezione Plugin in sé non cresce mai.
Pagine che un tema può disporre
Un plugin che porta una pagina nel negozio, un blog, un cercanegozi, un lookbook, deve ai temi una cosa: la sua pagina come dati. Sui propri template mette tutto quello che la pagina mostra in ambito come plugin, in valori semplici, e ne documenta la forma; un tema dispone la pagina sopra con i nodi normali, come fa con un prodotto o con il carrello. Quello che i dati non possono fare, un modulo o qualunque altra cosa che gira, è una parte che la piattaforma possiede e che il tema colloca. La sua disposizione già pronta è costruita esattamente con quei dati e quelle parti, quindi tutto ciò che mostra, un tema può mostrarlo diversamente.
Il blog è il primo: i suoi articoli, un articolo, la barra laterale e le sue parole sono plugin.list, plugin.post, plugin.sidebar e plugin.labels, e l'unica parte che gli è servita è search-form. Veda il riferimento per ogni campo.
Hook con un server
Imposti remote.baseUrl e noi facciamo POST di JSON a {baseUrl}/hooks/<name>. Ogni richiesta porta tre intestazioni:
x-whizzy-timestamp: 1789000000000 milliseconds since the epoch x-whizzy-signature: <hex> HMAC-SHA256 with your signing secret x-whizzy-plugin: acme-rates
La stringa firmata è timestamp + "\n" + "POST" + "\n" + path + "\n" + body, dove path è il percorso URL che abbiamo chiamato (per esempio /whizzy/hooks/event) e body sono i byte esatti. Rifiuti una richiesta il cui timestamp si discosti di più di 60 secondi dal suo orologio, e confronti le firme a tempo costante. Il suo segreto di firma viene mostrato una volta, quando lo staff registra il 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"));
}Risponda 2xx con JSON. Qualsiasi altra cosa è un fallimento; gli eventi vengono ritentati, i preventivi saltati.
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 risposta non-2xx a install rifiuta l'installazione: il commerciante vede il testo della sua risposta e non viene tenuto niente. Le impostazioni vengono rimandate a ogni aggiornamento; un'impostazione segreta non viene mai inviata.
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": { … } } }Eventi: order.paid, order.fulfilled, order.refunded. Consegnati almeno una volta, entro circa un minuto, e ritentati con attesa crescente fino a un giorno; il deliveryId è lo stesso a ogni ritentativo, lo usi per ignorare un duplicato.
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 } ] }Prezzi nelle unità minori della valuta del negozio. Chiesto al checkout con tre secondi di timeout; una risposta lenta sono zero preventivi, non un checkout lento. Il suo preventivo viene offerto accanto ai metodi del commerciante, e l'ordine registra quale è stato scelto come 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" }Dichiari "payment": { "id": "acme-pay", "label": "Pay with Acme" }. L'etichetta è l'opzione che vede il cliente; sceglierla lo manda al suo url, che deve essere https. Quando torna su returnUrl chiamiamo check, e la sua risposta fa fede: per un plugin non c'è webhook. Risponda pending finché non sa; il commerciante può anche segnare l'ordine come pagato 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": "…" }Dichiari "email": true e la posta ai clienti del negozio, ricevute e avvisi di spedizione, viene consegnata a lei invece che al nostro server di posta, un messaggio per chiamata. Una risposta non-2xx viene ritentata come fa la nostra coda. La posta che mandiamo al commerciante resta dalla nostra parte.
sms/send
POST /hooks/sms/send
{ "installId": "…", "tenantId": "…", "to": "+353870000000",
"text": "Kestrel Goods: order #1042 is on its way.", "kind": "order.shipped" }
200 {}Dichiari "sms": true. Inviato quando un ordine viene confermato e quando un pacco parte, al numero che il cliente ha dato, al meglio possibile: un messaggio che non parte non viene ritentato.
Chiamare il negozio
Il accessToken della chiamata di installazione è un token bearer per quel solo negozio. Vive quanto l'installazione e muore con lei. Due porte:
# 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" } }Quello che il token può fare sull'API delle capacità è esattamente hooks.grants dal suo manifest, letto dal negozio a ogni chiamata: catalog.write, pricing.write, orders.fulfil, content.write, design.write, settings.write, customers.write, customers.read, reports.read. Un commerciante che spegne il plugin lascia il token senza alcun permesso. Ogni chiamata di capacità mostra un'anteprima e chiede conferma al commerciante prima che succeda qualcosa che tocchi denaro, come fa per qualsiasi agente.
La chiave di importazione funziona esattamente come quella del connettore di migrazione: un negozio, un lavoro, 24 ore, revocabile. Ne chieda una nuova quando scade.
Pacchettizzazione
Mandi un plugin come zip: manifest.json nella radice, facoltativamente cover.png (o .jpg, .webp; 2:1, sotto i 4 MB) per la sua scheda, facoltativamente sections/*.json con una sezione ciascuno, che vengono ripiegati in hooks.sections. Un readme è benvenuto e ignorato: quello che legge un commerciante è description e docsUrl nel manifest. In un pacchetto non c'è codice; quello che gira è il suo server. Lo staff registra lo zip a catalogo, e un plugin si può riscaricare come lo stesso zip.
Temi
Un plugin che cambia l'aspetto di un negozio di solito è un tema. Quella è una cosa diversa, e più semplice: creare temi.