Shop · Extend
Extending Shop
Add payment gateways, carriers, tax providers, promotions, and product types from your own plugin.
Another plugin can add commerce features without changing Shop. Register filters in your plugin’s activate(); the host removes them when your plugin is deactivated. Types come from justflows.shop/contracts.
Payment gateways
ctx.hooks.filter("justflows.shop.payments.gateways", (gateways) => [
...gateways,
{
id: "acme-pay",
name: "Acme Pay",
manual: false,
managed: false,
capabilities: ["intent", "capture", "refund", "webhooks", "sandbox"],
fields: [
{ id: "publicKey", label: "Public key", secret: false },
{ id: "secretKey", label: "Secret key", secret: true },
],
adapter: acmePayAdapter, // a ShopPaymentAdapter
},
]);| Field | Rules |
|---|---|
id | Lower-kebab, such as acme-pay. Duplicates are dropped. |
name | Required, up to 80 characters. |
manual | true for offline methods such as bank transfer. Manual gateways may not have secret fields. |
managed | true for a managed account from a companion plugin. |
capabilities | Any of intent, confirm, authorize, capture, partial_capture, void, refund, partial_refund, webhooks, reconcile, tokenize, pending, disputes, fees, sandbox, recurring. |
fields | Settings on Commerce → Payments. secret: true values go to plugin secrets and never reach the browser. |
A descriptor alone does not take payments. A ShopPaymentAdapter implements link, create, read, action, and webhook. Creation receives server-calculated money, order and customer ids, redirect and webhook URLs, and a durable idempotency key, and returns a hosted HTTPS checkout URL and a provider reference. Webhook handling must verify the signature or fetch the trusted payment; Shop then checks the gateway, payment, amount, and currency before applying it. An optional recurring adapter charges saved methods for subscriptions.
Shipping carriers
ctx.hooks.filter("justflows.shop.shipping.carriers", (carriers) => [
...carriers,
{
id: "acme-post",
name: "Acme Post",
origins: ["NL", "BE"],
services: [{ id: "standard", label: "Acme Standard" }],
capabilities: ["rates", "tracking"],
adapter: acmePostAdapter, // a ShopCarrierAdapter
},
]);origins are the ISO countries the carrier collects from (empty means any); capabilities are any of rates, labels, tracking, pickup, returns, customs, and address. A ShopCarrierAdapter implements link to test the connection, and Shop offers more when the adapter has the method:
| Method | What Shop does with it |
|---|---|
rates(http, credentials, quote) | A live rate for a zone's carrier method: the cheapest rate in the store currency plus the handling fee, for the parcel weight rounded up to 100 g. Kept for 15 minutes; checkout waits at most 6 seconds, then falls back to the fixed amount or hides the method. |
createLabel(http, credentials, input) | Staff make a label when shipping. Send input.idempotencyKey (the fulfillment id) as your reference; return the tracking number, a public tracking link, and the PDF as base64, or unknown: true when the outcome is unclear. |
track(http, credentials, trackingNumber) | An hourly job checks parcels for up to 45 days, each at most every 3 hours. Return pre_transit, in_transit, out_for_delivery, delivered, exception, returned, or unknown. |
Tax providers
justflows.shop.tax.providers adds a ShopTaxProvider that answers rates, not amounts: for an address and Shop’s tax class slugs it returns { ok: true, rates: { [classSlug]: ShopTaxRate[] } }, where a rate has name, rateBps (2100 is 21%), compound, shipping, and priority. Shop keeps the arithmetic, rounding, and invoice lines. Answers are reused for 15 minutes per address. When the chosen provider is missing, refuses, throws, or takes over 6 seconds, checkout stops instead of charging the wrong tax.
Promotions
justflows.shop.discounts.promotions adds a provider whose promotions(request) receives the cart lines (SKU, categories, quantity, unit price in minor units), email, country, and entered code, and returns percent, fixed_cart, fixed_product, or free_shipping promotions, automatic or behind a code. Shop applies them with its own eligibility, stacking, tax, and allocation rules, and records them in the order’s data.promotions. A provider that fails or takes over 4 seconds adds nothing.
Product types
justflows.shop.catalog.product_types adds a type (id, name, ships, downloads, optional validate) to the product editor, such as tickets or bookings. It uses Shop’s catalog, cart, checkout, orders, and stock. validate(item) runs when the item goes in the cart and when the order is placed; return a message to refuse it. While the type’s plugin is inactive, its products cannot be bought.
Subscription entitlements
ctx.hooks.action("justflows.shop.subscriptions.entitlements", async (event) => {
// { subscriptionId, customerId, userId, email, status, active,
// limits: [{ key, limit }], roles: string[], custom: [{ key, value }], tenantId }
if (event.custom.some((item) => item.key === "seats")) { /* … */ }
});active is true while the subscription is active, payment is due, or it is cancelled but still running. Platform limits are already written to the workspace by Shop. A plugin meter registered with ctx.quotas.register can be chosen as a limit on a plan, so a plugin can sell its own limit per plan without listening at all.
Unpaid orders
A plugin that bills through Shop calls POST /api/shop/orders/unpaid with lines of { title, quantity, unitMinor } and gets orderId, number, and paymentUrl back at the top level, without an Idempotency-Key. The v1 route, Create an unpaid order, nests them under data and needs the key.