Webhooks
Send recovery events to up to ten endpoints, such as your CRM, email marketing, Zapier, Make or n8n. Each endpoint has its own events and signing secret. Webhooks are signed, sent in the background and retried automatically.
Adding an endpoint
- Get an endpoint URL
From your tool, for example a Zapier "Catch Hook", a Make "Custom webhook", an n8n "Webhook" node or your own server. It must start with
https://. - Add it
Click Add endpoint, paste the Endpoint URL, keep Send events to this endpoint on and tick the Events you want. Then click Save Changes.
- Generate its secret
In the endpoint's Signing secret row, click Generate secret and copy it now: "New secret generated. Copy it now: it will not be shown again."
- Send a test event
Click the endpoint's Send test event. You should see "Test event delivered." Your tool now knows the payload fields.
- Webhook Endpoints card before any endpoint is added ("No endpoints yet.", Add endpoint)
- Recent Deliveries in the sidebar
- The "Zapier, Make or n8n" setup card
Endpoint settings
You can add up to 10 endpoints. Each is numbered ("Endpoint 1", "Endpoint 2"…) and shows a state: New (not saved yet), Off, Needs a secret or Active.
Must be a public address starting with https://. Addresses on private, loopback or local networks are refused. If an endpoint's URL isn't valid, nothing is saved and you see, for example, "Endpoint 2: enter a public HTTPS URL (starting with https://). Nothing was saved."
Pause one endpoint without removing it.
Cart abandoned, Cart restored from a recovery link and Checkout completed are ticked for a new endpoint.
Each endpoint has its own. See Rotating the secret.
Remove takes an endpoint off the list; it is deleted, with its secret, when you click Save Changes. Sites that used the single webhook of version 1.0.0 see it as Endpoint 1, with the same secret and queued deliveries.
Zapier, Make or n8n
- In your tool, start a workflow with a "catch webhook" trigger: Zapier Webhooks by Zapier → Catch Hook, Make Webhooks → Custom webhook, or the n8n Webhook node (POST).
- Copy the HTTPS URL it gives you, add it here as an endpoint, choose events and save.
- Generate the endpoint's secret, then click Send test event so the tool learns the payload fields.
- Optional but recommended: verify the signature in a code step, and ignore requests that don't match.
Events
| Event | Sent when | Extra fields |
|---|---|---|
cart.abandoned | A cart is marked abandoned | — |
campaign.enrolled | A cart joins a campaign | campaign {id, name} |
email.sent | A campaign email or the Free email is sent | step (1-based); campaign for campaign emails |
cart.recovered | A cart is restored from a recovery link | — |
cart.completed | Checkout finished | cart.order_id, cart.order_total filled in |
Payload
Every event is a JSON POST:
{
"id": "3f2b8c1e-9a4d-4f6b-8e2a-1c7d5b9e0f42",
"event": "cart.abandoned",
"occurred_at": "2026-10-02T10:00:00+00:00",
"site": "https://your-store.com/",
"cart": {
"id": 7,
"status": "abandoned",
"email": "sarah@example.com",
"first_name": "Sarah",
"customer_id": 0,
"total": 35.0,
"currency": "USD",
"items": [
{ "product_id": 419, "variation_id": 0, "name": "Canvas Tote", "quantity": 1, "line_total": 35.0 }
],
"created_at": "2026-10-02T08:52:11+00:00",
"abandoned_at": "2026-10-02T10:00:00+00:00",
"recovered_at": null,
"completed_at": null,
"order_id": null,
"order_total": null
}
}
The payload includes the customer's email and first name, which CRM and email tools need. It never includes a recovery link or token. A test event adds "test": true and uses sample data.
Headers
| Header | Value |
|---|---|
X-Siteryn-Event | The event name, e.g. cart.abandoned |
X-Siteryn-Delivery | A unique ID, the same as the payload id. Use it to ignore duplicates. |
X-Siteryn-Timestamp | Unix time in seconds |
X-Siteryn-Signature | sha256= + HMAC-SHA256 of "{timestamp}.{raw body}" with your secret |
X-Siteryn-Signature-Previous | The same, signed with the old secret, for 24 hours after a rotation |
Verifying the signature
Always verify before trusting a request. Compare in constant time and reject old timestamps.
$secret = getenv( 'SITERYN_WEBHOOK_SECRET' ); // whsec_…
$body = file_get_contents( 'php://input' );
$timestamp = $_SERVER['HTTP_X_SITERYN_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SITERYN_SIGNATURE'] ?? '';
$previous = $_SERVER['HTTP_X_SITERYN_SIGNATURE_PREVIOUS'] ?? '';
$expected = 'sha256=' . hash_hmac( 'sha256', $timestamp . '.' . $body, $secret );
$valid = hash_equals( $expected, $signature ) || ( $previous && hash_equals( $expected, $previous ) );
// Reject requests older than 5 minutes.
if ( ! $valid || abs( time() - (int) $timestamp ) > 300 ) {
http_response_code( 401 );
exit;
}
$event = json_decode( $body, true );
http_response_code( 200 ); // Any 2xx counts as delivered.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.SITERYN_WEBHOOK_SECRET;
app.post('/siteryn', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Siteryn-Timestamp') || '';
const sig = req.get('X-Siteryn-Signature') || '';
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + req.body).digest('hex');
const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
Rotating the secret
Each endpoint's secret starts with whsec_ and is shown masked (first 6 and last 4 characters) once saved. Click the endpoint's Rotate secret and confirm "Generate a new secret for this endpoint? The current one keeps working for 24 hours." For the next 24 hours each request is signed with both secrets (the old one in X-Siteryn-Signature-Previous), so you can update your receiver without missing events.
Delivery and retries
- Events are queued separately for each endpoint and sent in the background, never while a shopper is waiting. Up to 20 are sent per five-minute run.
- Requests time out after 10 seconds and don't follow redirects. Any 2xx response counts as delivered.
- A failed delivery is retried after 5 min, 15 min, 1 h, 6 h and 24 h. After the sixth failure it is marked failed.
- Each delivery is claimed before it is sent, so it is never delivered twice by overlapping runs.
- Delivered and failed records are deleted after 30 days.
Recent Deliveries
The sidebar card shows the last 10 deliveries across all endpoints, with a status pill (Delivered, Failed, Retrying or Queued), the event, when, the endpoint ("Webhook: hooks.zapier.com") and the last error, such as "The endpoint answered 500." Test events are sent directly and are not listed.
Custom integrations
Developers can add their own connector, for example straight to a CRM API, with the siteryn_abandoned_cart_pro_integrations filter. Pro handles queueing, retries and logging. See the Developer guide.