Webhooks
A webhook tells another system that something happened in your store. When an order is placed, a shipment is created or a product is updated, EverShop sends an HTTP POST request with a JSON body to a URL you choose, so your ERP, accounting tool, Slack bot or automation platform can react right away.
You set webhooks up in the admin panel, with no code. Each request is signed, failed requests are retried automatically, and every delivery is logged so you can see what was sent and what happened.
What it does
- Sends 21 store events to up to 10 URLs. Each webhook chooses which events it wants.
- Signs every request with an HMAC-SHA256 signature so your server can verify that it came from your store.
- Retries failed deliveries up to 5 times, then lets an admin retry by hand.
- Keeps a delivery log per webhook, with the payload, attempts and result of each delivery.
- Only talks to public HTTPS addresses by default, so a webhook cannot be used to reach your internal network.
- Is extensible: extensions can add their own events to the list.
Set up a webhook
- Go to Settings → Webhooks in the admin panel.
- Click New webhook.
- Fill in the form:
- Name: a label for you, for example
Order sync. - URL: where EverShop sends the requests. It must start with
https://. - Status: enabled or disabled.
- Events: tick the events to send. The list is grouped (Orders, Shipments, Products, Categories, Customers); open a group to see its events. A group shows how many of its events are selected.
- Name: a label for you, for example
- Click Save. You are taken to the edit page.
- On the edit page, copy the Signing secret. Your server needs it to verify requests.
- Click Send test event to check the URL and the secret. EverShop sends a
webhook_pingevent right away and shows the result.
The edit page also lists the webhook's Deliveries, newest first. You can filter by status, view the exact JSON that was sent, and retry failed deliveries.
You can create at most 10 webhooks. The grid shows each webhook's name, URL, number of events, status, and how many deliveries are pending or failed.
Order, shipment and customer events contain personal data such as names, email addresses and addresses. Only send them to systems that are allowed to receive that data.
What EverShop sends
Every delivery is an HTTP POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | EverShop-Webhook/1.0 |
X-EverShop-Topic | The event name, for example order_placed. |
X-EverShop-Delivery | A unique ID for this delivery. It is the same on every retry, so use it to detect duplicates. |
X-EverShop-Signature | The signature, in the form t=1791424730,v1=f7a4b5f9.... See Verify the signature. |
The body is an envelope around the event data:
{
"id": "6f1c0a9e-3f0b-4e55-9a57-0d9a6a3c1e10",
"event_id": "b7d5c3a4-81e2-4d0b-a3f6-5c2d9e7f4a18",
"topic": "order_status_updated",
"created_at": "2026-10-08T09:30:00.000Z",
"data": {
"orderId": 2070,
"before": "processing",
"after": "completed"
}
}
| Field | Description |
|---|---|
id | The delivery ID. Same value as the X-EverShop-Delivery header, and the same on every retry. |
event_id | The ID of the underlying EverShop event. null for a test event. |
topic | The event name. |
created_at | When the delivery was queued (ISO 8601, UTC). Not the time of the attempt. |
data | The event data. The shape depends on the event; see Available events. |
The body of a retry is exactly the same as the first attempt. Only the signature timestamp changes.
Verify the signature
Anyone who knows your URL could send a fake request to it. The signature lets you prove a request really came from your store.
X-EverShop-Signature looks like t=1791424730,v1=f7a4b5f9...:
tis the Unix time (in seconds) when the request was signed.v1is the hex-encoded HMAC-SHA256 of the string<t>.<raw body>, using the webhook's signing secret as the key.
To verify a request:
- Read the raw body exactly as received. Do not parse and re-serialize it, because any change in whitespace or key order changes the signature.
- Split the header into
tandv1. - Compute
HMAC-SHA256(secret, t + "." + rawBody)and compare it withv1in constant time. - Reject the request if
tis too old (five minutes is a good limit). This stops someone from replaying a request they captured.
import crypto from 'crypto';
import express from 'express';
const SECRET = process.env.EVERSHOP_WEBHOOK_SECRET as string;
const TOLERANCE_SECONDS = 300;
function verifyEverShopSignature(
secret: string,
header: string | undefined,
rawBody: string
): boolean {
const parts = Object.fromEntries(
(header ?? '').split(',').map((part) => part.split('='))
);
const timestamp = Number(parts.t);
const received = parts.v1 ?? '';
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(received);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// express.raw keeps the body as the bytes that were signed.
app.post(
'/webhooks/evershop',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
if (
!verifyEverShopSignature(
SECRET,
req.header('X-EverShop-Signature'),
rawBody
)
) {
return res.sendStatus(400);
}
const deliveryId = req.header('X-EverShop-Delivery');
const event = JSON.parse(rawBody);
// 1. If deliveryId was already processed, return 200 and stop.
// 2. Otherwise save it, hand the work to a queue, and respond.
res.sendStatus(200);
}
);
The algorithm is plain HMAC-SHA256, so the same check works in any language.
Respond quickly, expect duplicates
How your server answers decides what happens next:
- Reply with a
2xxstatus within 10 seconds. That counts as delivered. The response body is ignored and never stored. - Anything else counts as a failure: a
4xxor5xxstatus, a timeout, a connection or DNS error. Note that4xxresponses are retried too, not treated as final. - Redirects are not followed. A
301or302answer is a failure. Put the final URL in the webhook. - Do slow work after you respond. Save the delivery, answer
200, then process it from a queue.
Once a delivery is queued, EverShop sends it at least once, not exactly once. In rare cases, for example when the server restarts in the middle of a request, the same delivery can arrive twice. Store the X-EverShop-Delivery value and ignore a delivery you have already handled.
Events are also not ordered. Two events that happen close together can arrive in either order, and a retried delivery arrives later than newer ones. If order matters, use a timestamp in the event data, such as updated_at, to decide which version is newer.
Retries
If an attempt fails, the delivery is retried automatically. A background job checks for due retries every minute, so each retry can be up to a minute later than the times below.
| Attempt | When |
|---|---|
| 1 | Immediately when the event happens |
| 2 | 1 minute after attempt 1 failed |
| 3 | 5 minutes after attempt 2 failed |
| 4 | 30 minutes after attempt 3 failed |
| 5 | 2 hours after attempt 4 failed |
After the fifth failed attempt the delivery is marked Failed. Open the webhook, find the delivery and click Retry to give it another full round of 5 attempts. Retry is only available for failed deliveries of an enabled webhook.
If you disable a webhook, its pending deliveries are canceled for good (a canceled delivery cannot be retried), and events that happen while it is disabled are not queued. If your server is only down for a while, leave the webhook enabled instead: failed deliveries are kept and can be retried once the server is back.
Delivery log
Each delivery is stored with its payload, so the edit page of a webhook doubles as a log.
| Status | Meaning |
|---|---|
| Pending | Waiting to be sent, or waiting for its next retry. The row shows the attempt count and the next try time. |
| Sending | An attempt is in progress. |
| Delivered | Your server answered with a 2xx status. |
| Failed | All 5 attempts failed. The last HTTP status or error is shown. Can be retried. |
| Canceled | The webhook was disabled before the delivery was sent. |
How long entries are kept:
- Delivered and canceled deliveries: the newest 100 per webhook. Change the number under Settings → Webhooks → Delivery log (between 10 and 1000).
- Failed deliveries: 30 days, or until you retry them.
- Pending and sending deliveries are never deleted, because they are waiting to be sent.
Deleting a webhook deletes its deliveries too.
Stored payloads contain the same personal data that was sent. They are removed by the rules above, so choose a log size that fits your privacy policy.
Available events
These are the events an admin can choose. Each sends the same data as the matching event in Events and Subscribers, which shows example payloads.
| Group | Event | Sent when / data |
|---|---|---|
| Orders | order_placed | An order is placed: payment succeeded, or the payment method (such as Cash on Delivery) does not need it up front. Use this for "new order" notifications. Sends the order. |
order_created | An order record is created, whatever its payment state. For online payments this can happen before order_placed. Sends the order. | |
order_status_updated | An order's status changes. Sends the order ID and the previous and new status. | |
order_canceled | An order is canceled. Sends the order ID and the reason, if one was given. | |
order_refunded | An order is refunded, fully or partly. Sends the order ID, amount, currency, whether it is a full refund, the transaction ID and the payment method. | |
| Shipments | shipment_created | A shipment is created. Sends the shipment ID, the order ID and whether the customer should be notified. |
shipment_status_changed | A shipment's status changes. Sends the shipment ID, order ID, previous and new status, and the phase. | |
shipment_delivered | A shipment reaches the delivered phase. Sends the shipment ID and the order ID. | |
shipment_label_created | A carrier label is purchased. Sends the shipment ID, order ID, label URL (can be empty) and tracking number. | |
shipment_label_voided | An admin voids a shipping label. Sends the shipment ID, order ID and tracking number. | |
| Products | product_created | A product is created. Sends the product. |
product_updated | A product is updated. Sends the product. | |
product_deleted | A product is deleted. Sends the deleted product. | |
inventory_updated | A product's inventory changes. Sends the inventory before and after. This event also fires when a product is saved without a stock change, so compare old and new. | |
| Categories | category_created | A category is created. Sends the category. |
category_updated | A category is updated. Sends the category. | |
category_deleted | A category is deleted. Sends the deleted category. | |
| Customers | customer_created | A customer account is created, whatever its status. Sends the customer. |
customer_registered | A customer account is created and active. Sends the customer. | |
customer_updated | A customer account is updated. Sends the customer. | |
customer_deleted | A customer account is deleted. Sends the deleted customer. |
Customer payloads never include the password. EverShop removes any field named password from every payload before it is stored or sent.
The test event
Send test event sends a webhook_ping event:
{
"id": "0b3f8f1e-2c64-4b8a-9a0e-7d1d6a4c5e22",
"event_id": null,
"topic": "webhook_ping",
"created_at": "2026-10-08T09:30:00.000Z",
"data": {
"message": "Test event from EverShop",
"webhook_uuid": "3c9a6a52-1f0d-4d3e-8c55-2b7e1a9d4f60"
}
}
It ignores the event selection, works even if the webhook is disabled, is not retried, and appears in the delivery list like any other delivery. The admin waits for the result, so the request can take up to 10 seconds.