Skip to main content

EverShop v2.3: Webhooks, Address Formats and Unified Payments

ยท 11 min read
EverShop Team
Maintainer

EverShop v2.3 is out. It adds signed outbound webhooks, a country-aware address system, one capture, void and refund flow for every payment method (with a rebuilt PayPal integration), redesigned emails in 17 languages, and a media library.

It includes everything in v2.2.2, released the same day, which fixes a start-up failure on fresh installs and several security vulnerabilities. This release also changes the database schema and removes some API routes, so please read Breaking changes and Upgrading before you update.

๐Ÿ”” Webhooksโ€‹

A new webhook core module lets your store tell other systems when something happens. In Settings โ†’ Webhooks you can add up to 10 webhooks. Each one has an HTTPS URL, a signing secret and the events it should receive.

There are 21 event topics, covering products, categories, customers, orders, inventory and shipments โ€” for example order_placed, order_refunded, shipment_delivered and inventory_updated. The admin includes a test event, a delivery list with filters, a payload viewer and a Retry button.

Delivery is built to be reliable:

  • Every event is stored as a delivery first, then sent, and retried from that stored row: after 1, 5, 30 and 120 minutes, up to 5 attempts.
  • Delivery is at-least-once. A 2xx response within 10 seconds counts as success, so your receiver should be idempotent. X-EverShop-Delivery stays the same across retries, which makes it a good de-duplication key.
  • Failed deliveries are kept for 30 days.
  • Only HTTPS and public addresses are allowed. Set system.webhook.allowPrivateNetworks to test against a local receiver. password fields are removed from every payload.

Every request is signed. The X-EverShop-Signature header looks like t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <timestamp>.<body>, keyed with the webhook secret. Verify it against the raw request body, and reject old timestamps so a captured request cannot be replayed:

import crypto from "node:crypto";

export function verifySignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
return false; // missing or too old: could be a replay
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest();
const received = Buffer.from(parts.v1 ?? "", "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

With Express, read the body with express.raw({ type: "application/json" }) and pass req.body.toString("utf8") as rawBody.

Extensions can add their own topics with addProcessor('webhookTopics'). Event subscribers placed in subscribers/_all/ now receive every event, and every subscriber gets { name, uuid } as an optional second argument.

๐ŸŒ Address formatsโ€‹

Addresses are no longer one-size-fits-all. A single format record per country (in the shape of Google's libaddressinput) now drives the storefront form, server-side validation, display, GraphQL, emails and every integration mapping.

Checkout and the account address book use one schema-driven form, with country-specific fields, region lists, and telephone and postal-code patterns.

In the admin:

  • Settings โ†’ Customer controls the name format, telephone, company, address lines, required fields and the default country.
  • Settings โ†’ Shipping โ†’ Sell to countries limits where your store sells. When a change would leave a shipping zone without a country, you get a confirmation that lists those zones.
  • Shipment emails now print the delivery address.

For extension developers there is a public API at @evershop/evershop/lib/address: patchAddressFormat, registerRegionProvider, registerAddressField, resolveAddressSchema, validateAddress, formatAddress and toIntegrationAddress. Like the other registries, registration is locked after bootstrap. See the Address Formats guide.

This change renames address columns in the database. Read Breaking changes before you upgrade.

๐Ÿ’ณ Unified paymentsโ€‹

Capture, void and refund now belong to core (captureOrder, refundOrder) and work the same way for every payment method. The order page shows generic Capture and Refund buttons, which appear based on canCapture and canRefund. Payment methods declare optional capture, void and refund handlers in registerPaymentMethod, and two new events, order_refunded and order_canceled, let you react to the result. See Payment Method Development.

PayPal was rebuilt:

  • Payment is finalized in-process on the return page. The old approach called the store's own URL over HTTP, which failed behind a bot challenge or proxy.
  • A verified webhook at POST /api/paypal/webhook.
  • Admin refunds with partial-refund tracking, and a paypal_pending status for pending captures.
  • Idempotent transaction recording, and current Orders v2 payloads with exact-amount breakdowns, including zero-decimal currencies.
  • A reconciliation job, every 30 minutes, that captures approved orders and cancels and restocks abandoned ones (system.paypal.abandonedOrderTtlHours, default 6).

Stripe now takes the charge amount and currency from the order instead of the browser. The webhook verifies them, locks the order row, and handles payment_intent.payment_failed and charge.refunded. Cash on Delivery joins the same contract with its own cod_* statuses.

The PayPal and Stripe guides describe how each integration is wired.

โœ‰๏ธ Redesigned emailsโ€‹

A shared email layout โ€” header and logo, content, footer, with button, divider and items-table partials and a {{t}} translation helper โ€” now renders the order confirmation, welcome, reset password, shipment created and shipment delivered emails.

Two new emails join them: refund and cancellation. Each can be switched with notification_emails.order_refunded and notification_emails.order_canceled (enabled, templatePath).

The copy is translated into all 17 languages. The translations are machine-assisted, so the less common languages deserve a native-speaker review. See the Email System guide.

๐Ÿ–ผ๏ธ Media libraryโ€‹

The file browser is redesigned and paginated for every storage provider โ€” local, S3, Azure Blob and Google Cloud Storage โ€” so a folder with thousands of files no longer loads all at once.

You can now rename files (the extension is kept and an existing name is never overwritten), paste to upload, and open a details panel with the full URL, size and dimensions. A Media library entry joins the CMS menu.

The upload types you configure under System Setting โ†’ File Uploads (PDF, video, audio) are now honored by the browser. Before, only images worked. See File Storage.

๐Ÿ›๏ธ Storefront and catalogโ€‹

  • All products page at /products: editable in the page builder, with sorting, filters, pagination, a category tree and an attribute facet. It is included in the sitemap and offered in the link picker.
  • Virtual collections ("newest", "on sale", a category's products) for widgets and the page builder, so a new store's shelves are never empty.
  • Smarter product cards. Simple products get Add to Cart ("Sold out" when out of stock). Products with variants get Select options, a link to the product page. Several widgets gained the button.
  • Contact form widget with stored submissions. Submissions are saved before the email is attempted, and the admin grid shows unread, read and spam entries.
  • Replace homepage with a landing page, with an automatic disabled backup and a one-click restore.

๐Ÿงฑ Theme building blocksโ€‹

For theme authors, the storefront pages are now a thin shell plus movable blocks:

  • themes/<id>/layouts.json moves any storefront page component to another Area or sort order without forking the file, with hot reload in development.
  • theme.json can ship landing pages, upload assets through the store's storage provider (theme-asset:<path> tokens), and reference products or collections by a stable key (store-ref:<entity>/<by>/<key>).
  • Shared widget classes (evershop-widget__eyebrow, __heading, __subtext, __item-heading) let one rule restyle every widget. The Section widget follows the page column through --page-max-width and --page-gutter.
  • evershop seed can be re-run and understands themes: widgets, variant groups and category images are handled idempotently.

See the Theme Overview.

โšก Performanceโ€‹

  • On a 300,000-product catalog, the product listing count query went from about 50.7 ms to 26.5 ms, and the category facet from about 79 ms to 2.5 ms. The store-wide attribute facet went from about 48 ms to 2 ms.
  • Rich text sanitizing no longer bundles sanitize-html into the storefront: about 200 KB less minified (67 KB gzipped).
  • Image and static-asset requests no longer create a session or send a Set-Cookie, so CDNs can cache product images.

๐Ÿ”’ Securityโ€‹

Please upgrade promptly. This release fixes:

  • A PayPal capture action that could be called without authentication using only an order UUID. The public PayPal capture and authorize routes were removed.
  • The Stripe charge amount, which now comes from the order, not from client data.
  • ?limit= on public pages, which could request the whole catalog in one response. It is now capped.

v2.3 also includes the v2.2.2 fixes: order data exposure through the public GraphQL API (CVE-2025-12919), an authorization bypass in API route middleware, SQL injection through the query builder's raw-SQL escape hatch, and server-side code evaluation in getContextValue(...). If you are upgrading from v2.2.1 or earlier, the Node.js and API changes from v2.2.2 apply to you too (see the table below).

Breaking changesโ€‹

ChangeWhat to do
Address columns renamed on customer_address, cart_address and order_address: full_name โ†’ recipient, address_1 / address_2 โ†’ address_line_1 / address_line_2, city โ†’ locality, province โ†’ administrative_area, postcode โ†’ postal_code. shipping_zone_province became shipping_zone_region.Update custom SQL, extensions and themes that read the old names. The GraphQL Address types and address REST payloads changed too, and types/customerAddress and lib/locale/countries were removed: use @evershop/evershop/lib/address. v2.2.1 and v2.2.2 code starts against the new schema, but every address reads back empty.
Payment routes replaced. Removed: the Stripe capture and refund routes, POST /api/cod/captures, and the PayPal authorization and capture routes.Use POST /api/orders/:id/capture and POST /api/orders/:id/refunds (admin only). Payment methods declare capture, void and refund handlers in registerPaymentMethod.
PayPal return page renamed from /paypal/proccessing to /paypal/processing.There is no alias, so update anything that links to the old path.
Storefront pages are a shell plus blocks. The product, category, cart, checkout, account, order list, blog and search pages keep only the query, the provider and the Areas.If your theme forked one of these page files (ProductView.tsx, CategoryView.tsx, ShoppingCart.tsx, Checkout.tsx and so on), delete the inline pieces from your copy, or the page renders them twice.
Fresh installs create no sample data. The install no longer adds the Men, Women and Kids categories, or the Color and Size attributes.Existing stores keep what they have. A new store gets its catalog from the theme (theme.json), so pick a theme that provides one.
Public ?limit= is capped at system.max_collection_size (default 200).Page through large collections.
Rich text uses xss instead of sanitize-html.The allow-list is the same, but output differs slightly (<br> instead of <br />), and style values containing url(javascript:) or expression() are dropped.
Prices and dates follow the active locale (the request locale, then the Store Setting language, then shop.language).No action for most stores. Admin pages format in the admin language.
From v2.2.2: raw SQL needs sql(), getContextValue(...) takes data literals only, Query.order(uuid) needs proven access, and API route middleware is authenticated.Replace hand-written { isSQL: true, value: '...' } objects with sql('...'), and use grantOrderAccess for custom order pages. Node.js 20.9 or newer is required.

Upgradingโ€‹

npm install @evershop/evershop@2.3.0
npm run build
npm run start

Or with Docker:

docker pull evershop/evershop:2.3.0
Back up your database first

This release applies 6 database migrations, including one that renames address columns. After they run, do not start v2.2.1 or v2.2.2 against the database: it starts, but every address reads back empty. To go back, restore your backup.

As an alternative, run scripts/address-formats/rollback-schema.sql from the repository (psql --single-transaction -f) while you redeploy the old release. It reverses the address renames, but it does not undo the Cash on Delivery status change or remove the new tables.

After upgrading:

  1. Stripe: enable payment_intent.succeeded, payment_intent.amount_capturable_updated, payment_intent.payment_failed, payment_intent.canceled and charge.refunded on your webhook endpoint.
  2. PayPal: create a webhook in the PayPal dashboard that points at /api/paypal/webhook, and save its ID in the PayPal settings (or in system.paypal.webhookId). Until you do, the endpoint answers 503 and the reconciliation job is the only fallback. The old paypalWebhookSecret setting is gone.
  3. Review your shipping zones under Settings โ†’ Shipping. Provinces became regions, and Sell to countries limits which countries checkout accepts.
  4. Check your header if your theme relied on order-first to place the center Area: Areas now render left, center, right in the markup.
  5. Check rich text that relied on <br /> output.

Thanksโ€‹

Thanks to everyone who contributed to this release and to those who reported the issues it fixes. If you hit a problem while upgrading, open an issue or ask on Discord.