Webhook Payloads
When your catalog changes, Malovex delivers a webhook to the endpoints you’ve registered. This page documents exactly what those requests look like and how to verify them.
To register an endpoint and pick which changes it receives, see Webhooks in the panel guide.
The request
Section titled “The request”Every delivery is an HTTP POST to your endpoint URL, with a JSON body:
POST /your/endpoint HTTP/1.1Content-Type: application/jsonX-Malovex-Signature: sha256=3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1bX-Malovex-Timestamp: 1751878500{ "event": "price", "store_id": 12, "product_ids": [1, 2, 3], "occurred_at": "2026-07-07T09:15:00+00:00"}Payload fields
Section titled “Payload fields”| Field | Type | Description |
|---|---|---|
event |
string | The change type: price, content, or removed. |
store_id |
integer | null | The store the change relates to. null for endpoints scoped to all stores where the change isn’t store-specific. |
product_ids |
array of integers | The IDs of the products that changed. |
occurred_at |
string | ISO 8601 timestamp of when the change was dispatched. |
Event types
Section titled “Event types”event |
Meaning |
|---|---|
price |
One or more of the listed products had a price change. |
content |
One or more of the listed products had a content change (title, description, attributes, or images). |
removed |
One or more of the listed products were removed from the store’s catalog. |
Headers
Section titled “Headers”| Header | Description |
|---|---|
Content-Type |
Always application/json. |
X-Malovex-Timestamp |
The Unix timestamp (seconds) when the delivery was signed. |
X-Malovex-Signature |
An HMAC-SHA256 signature of the request, prefixed with sha256=. Use it to verify authenticity. |
Verifying the signature
Section titled “Verifying the signature”Each delivery is signed with the signing secret shown once when you created the endpoint (it begins whsec_). Verify every request before trusting it.
The signature is computed as:
signature = "sha256=" + HMAC_SHA256(secret, timestamp + "." + raw_request_body)Where timestamp is the value of the X-Malovex-Timestamp header and raw_request_body is the exact bytes of the request body — verify against the raw body before any JSON parsing or re-serialization.
-
Read the
X-Malovex-TimestampandX-Malovex-Signatureheaders. -
Compute
HMAC-SHA256over the string{timestamp}.{raw_body}using your signing secret. -
Prefix your result with
sha256=and compare it against theX-Malovex-Signatureheader, using a constant-time comparison. -
Reject the request if they don’t match.
Example (PHP)
Section titled “Example (PHP)”$timestamp = $_SERVER['HTTP_X_MALOVEX_TIMESTAMP'];$signature = $_SERVER['HTTP_X_MALOVEX_SIGNATURE'];$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (! hash_equals($expected, $signature)) { http_response_code(400); exit;}Example (Node.js)
Section titled “Example (Node.js)”const crypto = require('crypto');
const timestamp = req.headers['x-malovex-timestamp'];const signature = req.headers['x-malovex-signature'];const body = req.rawBody; // the raw request body, not the parsed object
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) { return res.status(400).end();}Responding
Section titled “Responding”Return a 2xx status to acknowledge the delivery. Any other status — or a connection failure — is treated as a failed delivery.
Because a delivery may in rare cases be re-sent, make your handler idempotent: use the event plus product_ids to deduplicate, and treat re-processing the same change as a no-op.
Retries and pausing
Section titled “Retries and pausing”Malovex tracks each endpoint’s health:
- A failed delivery increments the endpoint’s consecutive-failure count.
- After repeated failures, your account owners are notified.
- After sustained failures, the endpoint is paused for a cooldown period; deliveries resume automatically on the next change once the cooldown lifts.
- A single successful delivery resets the failure count and clears any pause.
You can review recent deliveries, their status, and the response code on the endpoint’s page in the panel.
Alternative: the changes feed
Section titled “Alternative: the changes feed”If you’d rather pull changes than receive pushes — or want a way to catch up after downtime — use the changes feed instead of (or alongside) webhooks.
GET /v1/changesReturns the latest change per product since a timestamp, plus a cursor to continue from.
Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
store |
string | default store | Slug of the store to read changes for. |
since |
string (ISO 8601) | — | Return changes after this time. Omit on the first call. |
Example request
Section titled “Example request”curl "https://api.malovex.com/v1/changes?store=eu-store&since=2026-07-07T09:00:00Z" \ -H "Authorization: Bearer mlx_xxx"Example response
Section titled “Example response”{ "changes": [ { "product_id": 1, "change_type": "price", "created_at": "2026-07-07T09:15:00+00:00" }, { "product_id": 2, "change_type": "content", "created_at": "2026-07-07T09:16:30+00:00" } ], "next_since": "2026-07-07T09:16:30+00:00", "store": "eu-store"}| Field | Description |
|---|---|
changes |
The latest change per product in this window. Each has product_id, change_type (price, content, or removed), and created_at. |
next_since |
Pass this back as since on your next call to continue where you left off. |
store |
The store slug the feed was read for. |
Polling pattern
Section titled “Polling pattern”-
On your first poll, call without
sinceto establish a starting cursor fromnext_since. -
On each subsequent poll, pass the previous
next_sinceback assince. -
For each returned change, fetch the affected product from Products or Prices to get the new values.
-
Keep polling with the latest
next_sinceon your own schedule.