EverShop v2.3: Webhooks, Address Formats and Unified Payments
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
2xxresponse within 10 seconds counts as success, so your receiver should be idempotent.X-EverShop-Deliverystays 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.allowPrivateNetworksto test against a local receiver.passwordfields 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_pendingstatus 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.
