Skip to content

Webhook Notifications -- Send Alerts Straight to Your Own Systems

September 17, 2026

Type: New Feature

Overview

SellerLegend notifications can now be delivered to your own systems as they happen. Add a webhook destination (a web address you or your developer control), and every alert you choose -- a lost Buy Box, a suppressed listing, a low-stock warning, a new refund -- is sent there instantly. Use it to post alerts into your team chat, open tickets in your helpdesk, update your own dashboards, or kick off any automation you like.

What's New

A New "Webhook" Channel on the Notifications Screen

  • The Notifications settings screen has a new Webhook column next to In App and Email.
  • Tick it for any notification type, then choose which destinations should receive that alert.
  • Every notification type supports webhooks, including Loss Of Buybox, Listing Changes, Suppressed Listings, Inventory Alerts, Back In Stock Alerts, Returns, Refunds, Excessive Order Quantity, Feedback Notification, Restock Suggestions, Import/Export Report, and Amazon Connections Failure.

Reusable Destinations

  • Give each destination a friendly label (for example Ops Slack or Helpdesk) and an https:// address.
  • Add a destination once and attach it to as many notification types as you like.
  • You can have up to 25 destinations on your account.

Secure, Signed Deliveries

  • Each destination gets its own signing secret. Every delivery is signed with it, so your system can confirm the message really came from SellerLegend and wasn't changed along the way.
  • The secret is shown once, when you create the destination. You can rotate it at any time.

Reliable Delivery

  • If your system is briefly unavailable, SellerLegend automatically tries again: up to 5 attempts over roughly 2.5 hours.
  • If a destination keeps failing, it is paused automatically so it doesn't pile up errors. The destination list clearly marks it as Auto-disabled, and you can switch it back on with one click once the problem is fixed.

How to Use

  1. Go to Account Settings → Notifications.
  2. Find the notification you want (for example Loss Of Buybox) and tick the Webhook checkbox on that row.
  3. The Webhook destinations window opens. Enter a label and your https:// address, then click Add.
  4. Copy the signing secret now and store it somewhere safe -- it is not shown again. Give it to whoever builds the receiving system.
  5. Make sure the destination is ticked for this notification. To reuse a destination for another notification, tick Webhook on that row and tick the existing destination in the window.
  6. To manage a destination later, click the settings icon next to the destination names on any row where Webhook is ticked. From there you can:
    • Rotate the signing secret (the refresh icon) -- the new secret is shown once; update your receiving system straight away.
    • Re-enable an auto-disabled destination (the play icon).
    • Delete a destination -- it is removed from every notification it was attached to.

Benefits

  • React faster -- alerts reach the tools your team already watches, in real time, instead of waiting in an inbox.
  • Automate the follow-up -- open a ticket when a listing is suppressed, reorder when stock runs low, or log every refund automatically.
  • One setup, many alerts -- attach a single destination to all the notifications you care about.
  • Trustworthy -- signed deliveries mean your system only acts on genuine SellerLegend alerts.

Impact

  • Nothing changes unless you turn it on. Your existing In App and Email notification choices stay exactly as they are.
  • Destinations must use https:// and be reachable on the public internet.
  • The receiving system needs a small amount of development work. The section below has everything your developer needs.

For Developers: Receiving Webhooks

The Request

SellerLegend sends an HTTP POST with a JSON body to your destination address.

HeaderExampleMeaning
Content-Typeapplication/jsonBody is JSON
User-AgentSellerLegend-Webhooks/1.0
X-SellerLegend-Eventsl.loss_of_buyboxEvent type
X-SellerLegend-Event-Idevt_2f6c1b9e-...Unique per event. The same on every retry.
X-SellerLegend-Deliverydlv_Q3mX8rT2vLp9sK4wN7bY1cZaUnique per delivery attempt
X-SellerLegend-Webhook-Id1The destination that was called
X-SellerLegend-Signaturet=1789631146,v1=5c1f0e9a...Timestamp and signature (see below)

Header names are case-insensitive. Some frameworks and proxies rewrite them, for example to X-Sellerlegend-Signature.

Body:

json
{
  "event_id": "evt_2f6c1b9e-5d0a-4c8e-9a51-2b7f0e6d3c11",
  "event_type": "sl.loss_of_buybox",
  "emitted_at": "2026-09-17T07:45:46+00:00",
  "account": {
    "id": 12345,
    "seller_id": "A1EXAMPLE",
    "marketplace": "ATVPDKIKX0DER",
    "currency": "USD"
  },
  "notification_type": "Loss Of Buybox",
  "data": { },
  "links": {
    "in_app": "https://app.sellerlegend.com/notifications/index"
  }
}
  • event_type is sl. followed by the notification name in lowercase, with spaces and punctuation turned into underscores. For example, Loss Of Buybox becomes sl.loss_of_buybox, and Inventory Alerts (on days) becomes sl.inventory_alerts_on_days.
  • account.marketplace is the Amazon marketplace ID.
  • data holds the details of the alert. Its fields depend on the notification type.

Verifying the Signature

Always verify the signature before you trust a delivery. The header looks like this:

X-SellerLegend-Signature: t=1789631146,v1=5c1f0e9a7d3b2e64c8a1f05b9e7d2c4a6b8f1e3d5c7a9b0e2f4d6c8a1b3e5f7d
  • t is the Unix timestamp (in seconds) when the delivery was signed.
  • v1 is a hex-encoded HMAC-SHA256. The key is your signing secret and the message is <t>.<raw request body>.

To verify:

  1. Read the raw request body exactly as received, before any JSON parsing.
  2. Split the header on , and then on the first =, to get t and v1.
  3. Build the signed message: the value of t, a literal ., then the raw body.
  4. Compute the HMAC-SHA256 of that message. Use your entire signing secret as the key, including the whsec_ prefix. Hex-encode the result.
  5. Compare your result with v1 using a constant-time comparison.
  6. Reject the delivery if t is more than 5 minutes away from your server's current time. This stops an intercepted request from being replayed later.

Use the raw body

Compute the signature over the body bytes you received, not over JSON you have parsed and re-encoded. Re-encoding changes spacing, key order or escaping, and a genuine delivery will then fail verification. Most web frameworks parse JSON automatically, so turn that off (or keep a copy of the raw body) on your webhook route.

Every attempt, including a retry, is signed again with a fresh timestamp, so the 5-minute window never rejects a legitimate retry.

Node.js (Express)

js
const crypto = require('crypto');
const express = require('express');

const SECRET = process.env.SELLERLEGEND_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;

function verifySellerLegendSignature(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
  if (!header) return false;

  const parts = {};
  for (const item of header.split(',')) {
    const i = item.indexOf('=');
    if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
  }
  if (!/^\d+$/.test(parts.t || '') || !parts.v1) return false;

  if (Math.abs(now - Number(parts.t)) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.`)
    .update(rawBody) // Buffer, exactly as received
    .digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(parts.v1, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

// express.raw keeps req.body as a Buffer, so the signature is checked over the exact bytes.
app.post('/webhooks/sellerlegend', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySellerLegendSignature(req.body, req.get('X-SellerLegend-Signature'), SECRET)) {
    return res.status(400).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200); // acknowledge quickly, then do the work
  handleEvent(event);  // your code; skip events whose event_id you've already processed
});

app.listen(3000);

Python (Flask)

python
import hashlib
import hmac
import os
import time
from typing import Optional

from flask import Flask, abort, request

SECRET = os.environ["SELLERLEGEND_WEBHOOK_SECRET"]  # whsec_...
TOLERANCE_SECONDS = 300

app = Flask(__name__)


def verify_sellerlegend_signature(raw_body: bytes, header: str, secret: str, now: Optional[int] = None) -> bool:
    if not header:
        return False

    parts = dict(item.strip().split("=", 1) for item in header.split(",") if "=" in item)
    timestamp, signature = parts.get("t", ""), parts.get("v1", "")
    if not timestamp.isdigit() or not signature:
        return False

    now = int(time.time()) if now is None else now
    if abs(now - int(timestamp)) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        timestamp.encode("utf-8") + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/webhooks/sellerlegend")
def sellerlegend_webhook():
    raw_body = request.get_data()  # raw bytes, before JSON parsing
    if not verify_sellerlegend_signature(raw_body, request.headers.get("X-SellerLegend-Signature", ""), SECRET):
        abort(400)

    event = request.get_json()
    handle_event(event)  # your code; keep it fast or hand it to a background job
    return "", 200

PHP

php
<?php

const TOLERANCE_SECONDS = 300;

function verify_sellerlegend_signature(string $raw_body, string $header, string $secret, ?int $now = null): bool
{
    $parts = [];
    foreach (explode(',', $header) as $item) {
        $pair = explode('=', $item, 2);
        if (count($pair) === 2) {
            $parts[trim($pair[0])] = trim($pair[1]);
        }
    }

    $timestamp = $parts['t'] ?? '';
    $signature = $parts['v1'] ?? '';
    if (!ctype_digit($timestamp) || $signature === '') {
        return false;
    }

    if (abs(($now ?? time()) - (int) $timestamp) > TOLERANCE_SECONDS) {
        return false;
    }

    $expected = hash_hmac('sha256', $timestamp . '.' . $raw_body, $secret);
    return hash_equals($expected, $signature);
}

$secret   = getenv('SELLERLEGEND_WEBHOOK_SECRET'); // whsec_...
$raw_body = file_get_contents('php://input');      // raw bytes, before json_decode
$header   = $_SERVER['HTTP_X_SELLERLEGEND_SIGNATURE'] ?? '';

if (!verify_sellerlegend_signature($raw_body, $header, $secret)) {
    http_response_code(400);
    exit('Invalid signature');
}

$event = json_decode($raw_body, true);
http_response_code(200);
// handle $event here; skip events whose event_id you've already processed

Keeping the Secret Safe

  • Store the signing secret like a password: in an environment variable or a secrets manager. Never put it in source code, front-end code, logs or tickets.
  • If the secret may have been exposed, rotate it from the Webhook destinations window.
  • Rotation takes effect immediately: from that moment, deliveries are signed only with the new secret. Update your receiver right away. Deliveries that fail in the meantime are retried, and each retry is signed with the new secret, so updating within the retry window loses nothing.
  • Each destination has its own secret. Look up the right one using the X-SellerLegend-Webhook-Id header if one receiver handles several destinations.

Responding and Retries

  • Reply with any 2xx status within 10 seconds (5 seconds to connect). The response body is ignored.
  • Anything else counts as a failure: a non-2xx status, a timeout, a connection error, or a redirect (redirects are not followed).
  • A failed delivery is retried roughly 1 minute, 5 minutes, 30 minutes and 2 hours after each failure, for up to 5 attempts in total.
  • If you reply 429 with a Retry-After header, the next attempt waits for the time you asked for.
  • Do slow work (database writes, calls to other services) after responding, or in a background job, so deliveries don't time out.

Handling Duplicates

A retry carries the same event_id as the original attempt. A delivery can occasionally arrive twice, for example if your server processed it but the response was lost. Record the event_ids you have processed and skip any you have already seen. X-SellerLegend-Delivery changes on every attempt, so don't use it for this.

Automatic Disabling

A destination is auto-disabled when either of these happens:

  • all 5 attempts for a single delivery fail, or
  • more than half of its last 50 deliveries fail.

It is also disabled straight away if its address stops resolving to a public internet address. While a destination is auto-disabled, nothing is sent to it. Fix the problem on your side, then click Re-enable in the Webhook destinations window. Its failure history is cleared when you re-enable it.

Testing Tips

  • For a first test, point a destination at a request inspector you control. It must be reachable over public https://.
  • Log the raw body and headers of your first few deliveries, and check that your verification code accepts them.
  • Change a single character of the body or the secret and confirm that verification fails.