Criar plugins para a WhizzyCommerce

Um plugin é um manifesto: um nome, uma versão, as definições que pede ao comerciante, e os hooks em que se liga. Um manifesto não leva código. Um widget de chat é um modelo de script com as definições do comerciante substituídas lá dentro; um blogue são tipos de conteúdo, uma secção e páginas que nós desenhamos. Tudo o que tem de correr, corre no seu servidor, e nós chamamo-lo por HTTPS assinado.

O manifesto

{
  "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"]
  }
}

Categorias: content, marketing, payments, shipping, messaging, integration, other. As definições usam os mesmos tipos de campo das definições de um tema; uma chave nomeada em secrets é selada em repouso, mascarada no formulário e nunca substituída num script. Os tipos de secção e de conteúdo têm de estar no espaço de nomes do seu identificador, como acme-rates.banner.

O pessoal regista o manifesto a partir da administração e publica uma versão. Uma versão nova é um manifesto novo com um número mais alto; as lojas ficam com a versão que instalaram até o comerciante decidir atualizar.

Hooks sem servidor

scripts: um modelo de JavaScript por gatilho, com {{settings.key}} substituído como literal escapado, posto em todas as páginas da loja exceto a de pagamento, depois do consentimento do comprador quando a loja o pede. sections: o mesmo JSON declarativo que uma secção de tema é, listado no editor de páginas. metaobjects: tipos de conteúdo que o comerciante preenche. dashboard (um ecrã com create também ganha uma linha no menu de mais) e routes são para plugins que vão dentro da plataforma; uma definição de texto com a chave route deixa o comerciante mover o primeiro desses caminhos, que é como um blogue passa a viver em /journal.

placement diz onde o comerciante encontra o plugin depois de instalado: settings (o padrão) é uma linha no fundo das Definições da loja, a apontar para o formulário de definições do plugin; rail dá-lhe um ícone próprio na barra do painel, nomeado por icon, com os ecrãs dele no painel ao lado. A secção de Plugins em si nunca cresce.

Páginas que um tema pode arranjar

Um plugin que traz uma página à loja, um blogue, um localizador de lojas, um lookbook, deve aos temas uma coisa: a página dele como dados. Nos modelos dele põe tudo o que a página mostra em âmbito como plugin, em valores simples, e documenta a forma; um tema faz o arranjo da página por cima disso com os nós normais, tal como faz com um produto ou com o carrinho. O que os dados não conseguem fazer, um formulário ou qualquer outra coisa que corra, é uma parte que a plataforma possui e que um tema coloca. O arranjo já feito dele é construído exatamente com esses dados e essas partes, por isso tudo o que ele mostra, um tema pode mostrar de outra maneira.

O blogue é o primeiro: os artigos, um artigo, a barra lateral e as palavras são plugin.list, plugin.post, plugin.sidebar e plugin.labels, e a única parte de que precisou é search-form. Veja a referência para cada campo.

Hooks com servidor

Defina remote.baseUrl e nós fazemos POST de JSON para {baseUrl}/hooks/<name>. Cada pedido leva três cabeçalhos:

x-whizzy-timestamp: 1789000000000        milliseconds since the epoch
x-whizzy-signature: <hex>                HMAC-SHA256 with your signing secret
x-whizzy-plugin:    acme-rates

A cadeia assinada é timestamp + "\n" + "POST" + "\n" + path + "\n" + body, onde path é o caminho do URL que chamámos (por exemplo /whizzy/hooks/event) e body são os bytes exatos. Recuse um pedido cujo carimbo esteja mais de 60 segundos fora do seu relógio, e compare assinaturas em tempo constante. O seu segredo de assinatura é mostrado uma vez, quando o pessoal regista o 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 com JSON. Qualquer outra coisa é uma falha; os eventos são repetidos, as cotações são ignoradas.

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_…" }

Uma resposta não-2xx ao install recusa a instalação: o comerciante vê o texto da sua resposta e nada fica guardado. As definições são enviadas outra vez em cada atualização; uma definição secreta nunca é enviada.

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. Entregues pelo menos uma vez, dentro de cerca de um minuto, e repetidos com recuo até um dia; o deliveryId é o mesmo em cada repetição, use-o para ignorar um 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 } ] }

Preços em unidades menores da moeda da loja. Pedido no checkout com três segundos de tempo limite; uma resposta lenta são zero cotações, não um checkout lento. A sua cotação é oferecida ao lado dos métodos do próprio comerciante, e a encomenda regista qual foi escolhida 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" }. A etiqueta é a opção que o comprador vê; escolhê-la manda-o para o seu url, que tem de ser https. Quando volta a returnUrl nós chamamos check, e a resposta dele é a autoridade: não há webhook para um plugin. Responda pending até saber; o comerciante também pode marcar a encomenda como paga à mão.

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 e o correio para clientes da loja, recibos e notas de expedição, passa a ser-lhe entregue em vez de ir pelo nosso servidor de correio, uma mensagem por chamada. Uma resposta não-2xx é repetida como a nossa própria fila repete. O correio que enviamos ao comerciante fica do nosso 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. Enviado quando uma encomenda é confirmada e quando uma encomenda é despachada, para o número que o comprador deu, com o melhor esforço: uma mensagem que não sai não é repetida.

Chamar a loja

O accessToken da chamada de instalação é um token portador para essa loja apenas. Vive enquanto a instalação viver e morre com ela. Duas portas:

# 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" } }

O que o token pode fazer na API de capacidades é exatamente hooks.grants do seu manifesto, lido da loja em cada chamada: catalog.write, pricing.write, orders.fulfil, content.write, design.write, settings.write, customers.write, customers.read, reports.read. Um comerciante que desligue o plugin deixa o token sem permissão nenhuma. Cada chamada de capacidade pré-visualiza e pede confirmação ao comerciante antes de acontecer qualquer coisa com dinheiro, tal como faz para qualquer agente.

A chave de importação funciona exatamente como a do conector de migração: uma loja, um trabalho, 24 horas, revogável. Peça uma nova quando expirar.

Empacotar

Envie um plugin como zip: manifest.json na raiz, opcionalmente cover.png (ou .jpg, .webp; 2:1, abaixo de 4 MB) para o cartão dele, opcionalmente sections/*.json com uma secção cada, que são dobrados em hooks.sections. Um readme é bem-vindo e ignorado: o que o comerciante lê é description e docsUrl no manifesto. Não há código num pacote; o que corre é o seu servidor. O pessoal regista o zip no catálogo, e um plugin pode ser descarregado de volta como o mesmo zip.

Temas

Um plugin que muda o aspeto de uma loja é normalmente um tema. Isso é uma coisa diferente, e mais simples: criar temas.

Criar plugins · WhizzyCommerce