Skip to main content

Set up and receive notification webhooks

Send listing and order notifications to your application. Set up a webhook, verify requests, and troubleshoot delivery.

Written by Patrick Widdoss

Webhooks send Informed notifications directly to your application as signed HTTPS POST requests. Use them to connect notifications to your own workflows, dashboards, or integrations.

You choose which notifications to receive through your notification subscriptions. Adding an endpoint alone does not subscribe it to events. Notifications must be enabled for your account before you can access these controls.


Before you begin

You need a receiver with a public HTTPS URL and a valid TLS certificate, such as https://notifications.example.com/webhook. It must accept POST requests, preserve the raw request body for signature verification, and respond within 10 seconds. Use a URL without embedded credentials, fragments (#...), or whitespace.

Webhook endpoints are shared by everyone in your Informed account. Account members can manage the same endpoints and select verified endpoints for their own subscriptions. Store signing secrets in private server configuration or a secret manager.


Add and verify an endpoint

  1. Open Notification Settings at /r/notifications/settings. Under Delivery Channels, click Add Channel.

  2. Select Webhook, enter your receiver's public HTTPS URL, and click Add.

  3. In Save your webhook signing secret, click Copy secret. Save the complete value, including the whsec_ prefix, in your receiver's environment. The secret is shown once and cannot be retrieved after the dialog closes. Click I saved the secret after saving it.

  4. Configure your receiver to authenticate requests and handle system.webhook.challenge as described below. The endpoint initially has a pending status.

  5. Click Verify on the webhook row. Informed sends a signed challenge request. Your receiver must verify its signature, read payload.challenge, and return that value in a top-level challenge field. When this succeeds, the endpoint becomes verified and is available for subscription delivery.

  6. Click Test to send a signed system.test request. Your receiver should authenticate it and return a 2xx response. The result appears on the webhook row. Tests are available after verification, including when the endpoint is disabled.

  7. Under Subscriptions, expand Saved Views or Orders and edit the notification subscription you want to deliver. Select your webhook URL under Additional Delivery Channels and save. Make sure both the subscription and the webhook channel are enabled. Repeat for each subscription you want delivered to this endpoint.


Authenticate every request

Informed signs challenges, tests, and notification deliveries using the same method. These headers accompany each request:

Header

Meaning

Content-Type

application/json (with an optional UTF-8 charset).

X-Dispatch-Timestamp

The signing time, in Unix seconds.

X-Dispatch-Signature

sha256=<lowercase hexadecimal signature>. During secret rotation, it can contain two comma-separated signatures.

X-Dispatch-Idempotency-Key

A UUID identifying the delivery. It must match the signed body's idempotency_key.

For each configured secret, calculate:

key          = UTF8(complete signing secret, including "whsec_") 
signed_bytes = UTF8(X-Dispatch-Timestamp + ".") + raw_request_body_bytes
signature = lowercase_hex(HMAC_SHA256(key, signed_bytes))

Respond to the verification challenge

Clicking Verify sends an envelope like this. Example IDs, timestamps, and values throughout this guide are illustrative.

{ 
"schema_version": 1,
"notification_id": null,
"delivery_id": null,
"idempotency_key": "3e437909-4a72-4e90-8d12-f463782ece74",
"event_type": "system.webhook.challenge",
"occurred_at": "2026-10-20T14:00:00Z",
"payload": {
"challenge": "<CHALLENGE_KEY>"
},
"metadata": null,
"correlation": {
"event_idempotency_key": null,
"trace_id": null
}
}

First authenticate the request. Then respond with HTTP 200, Content-Type: application/json, and:

{ 
"challenge": "<CHALLENGE_KEY>"
}

Echo payload.challenge exactly, with a single challenge string property at the root of your response. Do not wrap it in payload or data, return just the string, or return the signing secret. Any 2xx status can succeed, but an empty response cannot verify the endpoint. Keep the response uncompressed, within 4 KiB, and within the 10-second request deadline.

Verification and test requests are each sent once, without automatic retries. After correcting a failure, click Verify or Test again. These requests do not appear in Latest Delivery Status.


Request envelope

All webhook bodies use the following envelope. Envelope field names use snake_case; individual notification payload fields retain the capitalization shown in the payload reference.

Field

Type

Description

schema_version

integer

Currently 1.

notification_id

string or null

Notification ID for an individual notification; null for challenges and tests. Keep IDs as strings to preserve precision.

delivery_id

string or null

ID for this destination's delivery; null for challenges and tests.

idempotency_key

UUID string

The delivery's deduplication key. Matches X-Dispatch-Idempotency-Key. Challenges and tests each get a fresh UUID.

event_type

string

Identifies the event and the shape of payload.

occurred_at

date-time string

UTC notification occurrence time; for a challenge or test, the request creation time. This is separate from the signing timestamp.

payload

JSON value

Event data. The types documented below use objects. This is JSON, not a JSON-encoded string.

metadata

JSON value or null

Additional context, such as links associated with the notification.

correlation.event_idempotency_key

string or null

Source event correlation key. Use the top-level delivery idempotency_key for deduplication.

correlation.trace_id

string or null

Diagnostic trace reference, when available.

Event types

event_type

When to expect it

system.webhook.challenge

You click Verify. Respond with the challenge as described above.

system.test

You click Test on a verified endpoint.

informed.listing.saved_view_change

A listing enters or exits a saved view and matches an enabled subscription.

informed.listing.order

An order matches a subscription, including new and cancelled order notifications.

Shared listing and saved-view fields

All event types related to listings in Informed (e.g. informed.listing.saved_view_change and informed.listing.order) include these fields inside payload:

Field

Type

Description

UserID

integer

Informed seller account ID.

ProductID

integer

Informed product ID.

ListingID

integer

Informed listing ID.

MarketplaceID

integer

Informed marketplace connection ID.

MarketplaceTypeID

integer

Informed marketplace type ID.

MarketplaceTypeName

string

Marketplace type name, such as Amazon or AmazonUK.

ItemID

string

Marketplace item identifier, such as an Amazon ASIN.

Sku

string

Seller SKU.

Title

string

Listing title.

SellerID

string

Marketplace seller identifier.

ViewID

integer

Informed saved-view record ID.

ViewKey

string

Saved-view key used to identify the view in subscriptions and links.

ViewEffectiveDate

date-time string

Effective timestamp of the saved-view definition.

ViewDisplayName

string

Saved-view display name.

EventSource

string

Source of the event; values depend on event type.

EventDate

date-time string

Timestamp reported by the event source.

Listing details come from the data available when the event is generated. Null-valued fields are omitted, so string fields and optional amounts can be absent when unavailable. Date-time examples use UTC (Z); parse date-time strings with an appropriate date-time library.

Saved-view change: informed.listing.saved_view_change

An event describes one listing's change in membership of one saved view. In addition to the shared fields, the payload includes:

Field

Type

Description

EnteredView

boolean

true when the listing entered the view.

ExitedView

boolean

true when the listing exited the view.

EventSource is currently ListingDataInserted, ListingDataUpdated, ViewInserted, or ViewUpdated. A change to a view's definition can therefore generate membership notifications as well as a change to a listing.

{ 
"schema_version": 1,
"notification_id": "789",
"delivery_id": "101",
"idempotency_key": "e788a91b-2cbe-4524-8e2a-53d848f5d9d7",
"event_type": "informed.listing.saved_view_change",
"occurred_at": "2026-10-20T14:00:00Z",
"payload": {
"UserID": 123,
"ProductID": 456,
"ListingID": 7890,
"MarketplaceID": 12,
"MarketplaceTypeID": 2,
"MarketplaceTypeName": "Amazon",
"ItemID": "B0EXAMPLE01",
"Sku": "SKU-001",
"Title": "Example product",
"SellerID": "EXAMPLESELLER",
"ViewID": 42,
"ViewKey": "low-stock",
"ViewEffectiveDate": "2026-10-01T09:00:00Z",
"ViewDisplayName": "Low stock",
"EventSource": "ListingDataUpdated",
"EventDate": "2026-10-20T13:59:58Z",
"EnteredView": true,
"ExitedView": false
},
"metadata": {
"informed_saved_view_url": "https://app.informedrepricer.com/r/listings?view=low-stock",
"amazon_listing_url": "http://www.amazon.com/gp/product/B0EXAMPLE01?th=1&psc=1"
},
"correlation": {
"event_idempotency_key": "29a3788c-f4c1-47ab-b6be-b8eb86a89151",
"trace_id": null
}
}

For an exit notification, EnteredView is false and ExitedView is true. Route using these booleans and ViewKey, rather than the view's display name.

Order: informed.listing.order

New and cancelled order notifications use the same event type and payload structure. Each event concerns an order line item associated with a listing and a matching saved view. EventSource is orders.

Additional field

Type

Description

OrderID

string

Marketplace order identifier.

OrderItemID

string

Order line-item identifier.

OrderDate

date-time string

Order date reported by the marketplace.

OrderQty

integer

Quantity ordered for the line item.

OrderStatus

string

Order status. The standard New subscription matches Pending or Shipped; Cancelled matches Cancelled. Other recognized status values include Unshipped, PartiallyShipped, Unfulfillable, InvoiceUnconfirmed, PendingAvailability, and Refund.

ItemPrice

number

Item-price amount reported for the order line. Use OrderCurrency to interpret monetary values.

OrderTotal

number, optional

Marketplace order total when greater than zero. Otherwise omitted. This is an order-level value; do not sum it across line-item events.

OrderCurrency

string, optional

Currency code, such as USD, when available.

FulfillmentChannel

string

FBA or MFN (merchant fulfilled).

IsBusinessOrder

boolean

Whether the marketplace identifies it as a business order.

IsNewOrder

boolean

Whether this event represents a newly observed order. Use OrderStatus to identify cancellations.

The following is a complete example payload; it is placed inside the envelope with event_type: "informed.listing.order":

{ 
"UserID": 123,
"ProductID": 456,
"ListingID": 7890,
"MarketplaceID": 12,
"MarketplaceTypeID": 2,
"MarketplaceTypeName": "Amazon",
"ItemID": "B0EXAMPLE01",
"Sku": "SKU-001",
"Title": "Example product",
"SellerID": "EXAMPLESELLER",
"ViewID": 42,
"ViewKey": "low-stock",
"ViewEffectiveDate": "2026-10-01T09:00:00Z",
"ViewDisplayName": "Low stock",
"EventSource": "orders",
"EventDate": "2026-10-20T14:00:00Z",
"OrderID": "111-1234567-1234567",
"OrderItemID": "12345678901234",
"OrderDate": "2026-10-20T13:45:00Z",
"OrderQty": 1,
"OrderStatus": "Shipped",
"ItemPrice": 24.99,
"OrderTotal": 29.98,
"OrderCurrency": "USD",
"FulfillmentChannel": "FBA",
"IsBusinessOrder": false,
"IsNewOrder": true
}

For a cancelled order, OrderStatus is Cancelled; IsNewOrder may be false. Do not interpret every IsNewOrder: false event as a cancellation.

Order metadata contains informed_saved_view_url, informed_listing_url (a link to the listing in Informed), and amazon_listing_url (the marketplace listing link). Saved-view changes contain the first and last of these. The amazon_listing_url field name is retained even when the link points to a different supported marketplace.

The same listing or order may match more than one saved view or subscription. Such deliveries can have different UUIDs and should not be discarded as retries solely because their order or listing IDs match.

Test: system.test

Tests use the common envelope with null notification_id, delivery_id, and metadata, a fresh idempotency_key, and this payload:

{ 
"message": "This is a test notification from Informed."
}

After authenticating, return any 2xx response, such as 204 No Content. No challenge echo is required. A successful test confirms your receiver accepted the test request; trigger a matching subscription event to validate your notification-processing workflow.


Acknowledge deliveries and handle retries

For notification deliveries, return a 2xx status after durable acceptance. An empty 204 No Content response is sufficient; response bodies are ignored. Complete the response within 10 seconds, and process longer-running work from your durable queue.

Receiver outcome

Delivery behavior

HTTP 2xx

Delivery succeeds.

HTTP 429 or 5xx

Informed retries automatically.

DNS lookup failure, connection failure, or request timeout

Informed retries automatically.

Other HTTP statuses, including redirects, 400, 401, 403, 404, and 408

Terminal failure; no automatic retry.

Invalid TLS certificate or blocked destination

Terminal failure; no automatic retry.

Retryable deliveries stop after five failed attempts in total. Retries use exponential backoff; actual send times depend on scheduling. Check Next retry in the delivery status view instead of relying on a fixed schedule. Challenges and explicit tests are excluded from automatic retries.

Delivery is at least once: your receiver can receive the same delivery again even after it returned 2xx, for example if the acknowledgment could not be recorded. Deduplicate by the top-level idempotency_key, and return 2xx for an already accepted delivery. The key and logical event content remain the same across retries; signing timestamps and signatures are refreshed. Do not use the signature, timestamp, or source correlation key as your deduplication key. Do not assume deliveries arrive in event-time order.


Check delivery status

In Notification Settings → Delivery Channels, click the webhook row's View delivery status list icon to open Latest Delivery Status. Use Refresh to check the status, failed attempts, latest HTTP response or error code, and next retry time.

sent means delivery succeeded; dead means it has stopped retrying. pending and failed can be waiting for a send or retry; processing or sending indicates work in progress. Failed attempts counts failures, so a delivery that succeeds on its first attempt shows 0.

Verification and test outcomes appear on the webhook row instead of this list. The status view shows current delivery outcomes, without a full attempt timeline or receiver response bodies. There is no manual resend action.


Rotate, disable, or replace an endpoint

Rotate a secret: Click Rotate on the webhook row. Copy and save the new secret immediately, then install it on your receiver before the previous secret's displayed expiry. The expiry is shown in your local time zone. During the overlap period, requests can carry signatures for both secrets; accept either valid signature. To cover requests already in flight, you can configure both secrets on your receiver (WEBHOOK_SECRET and WEBHOOK_PREVIOUS_SECRET in the example), then remove the previous secret after expiry. Rotation preserves verification. Another rotation can be blocked while the overlap period is active.

Recover a lost secret: The original value cannot be displayed again. Rotate and save a new secret. If a rotation is already in its overlap period, wait until another rotation is allowed or create and verify a replacement endpoint. If a create or rotate request reports an unknown outcome, refresh the channel state before trying again; retrying cannot recover a secret that was already issued.

Disable a channel: Use its enabled toggle. A disabled channel can still be selected in subscriptions, but selection does not enable it. Manually disabling a shared webhook affects all account members and can permanently reject queued deliveries, so it is not a pause-and-replay mechanism.

Recover from automatic disablement: Informed automatically disables a webhook after five consecutive distinct deliveries end in qualifying receiver, network, or destination failures. This counts completed failed deliveries, rather than five retry attempts on one delivery. Previously queued work can continue after automatic disablement, but new deliveries stop. Fix the receiver, use Test, inspect delivery status, then explicitly re-enable the channel. Tests and verification do not reset the failure streak or re-enable the channel.

Change a URL or delete an endpoint: The URL cannot be edited. Create and verify a replacement, update the affected subscriptions, and allow outstanding deliveries to finish before deleting the old endpoint. References from other account members, disabled subscriptions, or queued deliveries can block deletion.


Troubleshooting

Verification stays pending or changes to failed

Confirm that the URL is publicly reachable over HTTPS, the receiver is running, and the complete secret is installed. Inspect the result on the webhook row, then click Verify after fixing the issue.

invalid_challenge_response

Return JSON with the exact payload.challenge string in a root challenge field. Check for an empty body, a nested field, duplicate challenge properties, or a response rewritten by middleware.

response_too_large or unsupported_response_encoding during verification

Return a small, uncompressed JSON challenge response within 4 KiB.

Signature validation fails

Check the complete whsec_ value, raw body bytes, signing header names, server clock, and support for either comma-separated signature during rotation.

timeout, dns_error, or connection_error

Check DNS, firewall rules, receiver availability, and the 10-second deadline.

tls_error, blocked_destination, or invalid_url

Check the certificate and URL. The hostname must resolve only to allowed public addresses; use the final destination URL directly.

Endpoint is absent from the subscription channel list

It must be verified and belong to the current account. Refresh settings after successful verification.

A successful test produces no notification work

system.test is a separate connectivity check. Enable and select the channel on a matching subscription, then trigger an actual event.

Notifications stop

Check the subscription rules, subscription enabled state, channel enabled state, and latest delivery outcomes. An automatically disabled channel must be re-enabled after recovery.


Optional receiver example for developers

This example is a starting point for connecting Informed to your application. It uses only Node.js built-in modules and can be adapted to different hosting environments.

Where the Node.js example runs

As written, the receiver listens at 127.0.0.1:8080. This is a local address that only the computer running the receiver can reach. This example assumes that an HTTPS reverse proxy runs on that same server. A reverse proxy is the public-facing service that accepts the secure connection and passes the request to your receiver:

Informed → https://notifications.example.com/webhook → receiver at 127.0.0.1:8080

The developer or hosting provider needs to configure that public URL, its TLS certificate, and forwarding to the receiver. The proxy must preserve /webhook, the signing headers, and the exact request body bytes so signature verification succeeds.

Start the example

  1. Save the code below as receiver.cjs on a host with Node.js available.

  2. Set the host's WEBHOOK_SECRET environment variable to the complete signing secret copied from Informed. Keep this value private rather than pasting it into the source code directly.

  3. Run node receiver.cjs and configure HTTPS forwarding as described above. For ongoing use, configure your host to keep the receiver running and restart it if it stops.

  4. Use Verify and Test in Informed to check that requests reach the receiver and pass authentication.

The example handles verification and tests immediately. It does not yet save notifications or carry out your workflow. Complete the integration steps after the code before enabling notification subscriptions. Until then, ordinary notification requests receive HTTP 503 so they are not incorrectly acknowledged as saved. These failed deliveries are still subject to Informed's retry limit.

The 1 MiB incoming-body limit is an example receiver limit, not an Informed payload-size guarantee.

const http = require('node:http'); 
const { createHmac, timingSafeEqual } = require('node:crypto');

function verifySignature({ rawBody, timestamp, signatureHeader, secrets,
nowSeconds = Math.floor(Date.now() / 1000) }) {
if (!Buffer.isBuffer(rawBody) || typeof timestamp !== 'string' ||
!/^(0|[1-9][0-9]{0,15})$/.test(timestamp) ||
typeof signatureHeader !== 'string' || signatureHeader.length > 150 ||
/[\r\n]/.test(signatureHeader)) return false;

const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || Math.abs(nowSeconds - seconds) > 300) {
return false;
}
const candidates = signatureHeader.split(',').map(value => value.trim());
if (candidates.length < 1 || candidates.length > 2 ||
candidates.some(value => !/^sha256=[0-9a-f]{64}$/.test(value))) return false;

return secrets.some(secret => {
// Sign the raw bytes with the complete displayed secret, without decoding it.
const expected = createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(timestamp + '.', 'utf8').update(rawBody).digest();
return candidates.some(value =>
timingSafeEqual(expected, Buffer.from(value.slice(7), 'hex')));
});
}

async function acceptDeliveryOnce(envelope) {
// Replace this with a durable transaction that records envelope.idempotency_key
// and enqueues the work atomically. An already-recorded UUID is a successful no-op.
// Keep this failure until that storage is connected; returning early would tell
// Informed the notification was accepted even though no work had been saved.
throw new Error('Configure durable delivery handling before subscribing.');
}

function createReceiver({
secrets = [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_PREVIOUS_SECRET]
.filter(Boolean),
acceptOnce = acceptDeliveryOnce,
} = {}) {
if (!secrets.length) throw new Error('Set WEBHOOK_SECRET.');

return http.createServer(async (request, response) => {
if (request.method !== 'POST' || request.url !== '/webhook') {
response.writeHead(404).end();
return;
}
try {
const chunks = [];
let size = 0;
for await (const chunk of request) {
size += chunk.length;
if (size > 1024 * 1024) {
response.writeHead(413).end();
return;
}
chunks.push(chunk);
}
const rawBody = Buffer.concat(chunks);
if (!verifySignature({ rawBody, secrets,
timestamp: request.headers['x-dispatch-timestamp'],
signatureHeader: request.headers['x-dispatch-signature'] })) {
response.writeHead(401).end();
return;
}

let envelope;
try { envelope = JSON.parse(rawBody.toString('utf8')); }
catch { response.writeHead(400).end(); return; }
const key = request.headers['x-dispatch-idempotency-key'];
if (!envelope || envelope.schema_version !== 1 ||
typeof envelope.event_type !== 'string' || typeof key !== 'string' ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(key) ||
key !== envelope.idempotency_key) {
response.writeHead(400).end();
return;
}

if (envelope.event_type === 'system.webhook.challenge') {
if (typeof envelope.payload?.challenge !== 'string') {
response.writeHead(400).end();
return;
}
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ challenge: envelope.payload.challenge }));
return;
}
if (envelope.event_type !== 'system.test') await acceptOnce(envelope);
// Acknowledge only after durable acceptance, or for a signed test request.
response.writeHead(204).end();
} catch {
response.writeHead(503).end();
}
});
}

if (require.main === module) {
// This local listener relies on a same-host proxy for public HTTPS access.
createReceiver().listen(8080, '127.0.0.1');
}
module.exports = { verifySignature, createReceiver };

Complete the production integration

The function named acceptDeliveryOnce is where you connect the receiver to your system. The example deliberately leaves this function unfinished because the action to take and the place to save work depend on your application. A successful Verify or Test confirms connectivity and authentication; it does not confirm that this connection to your workflow is complete.

Complete these steps before enabling notification subscriptions:

  1. Choose what each notification should do. For example, an order notification might update an internal dashboard, while a saved-view change might create a task. Use event_type to select the action and payload to read its details. The payload reference in this guide describes the available fields.

  2. Save the notification before accepting it. Replace the placeholder error in acceptDeliveryOnce with code that saves the event and the work to be done in a database or other durable storage. "Durable" means the saved work survives application restarts. Printing a notification to a log or keeping it only in memory does not complete this step.

  3. Prevent the same delivery from creating work twice. Record envelope.idempotency_key with the saved work, and enforce its uniqueness in the database. Save the key and the work in one transaction, meaning both are saved together or neither is saved. This also protects against two copies arriving at the same time. If the key has already been accepted, finish successfully without creating the work again. If your receiver applies business changes directly instead of queueing work, save those changes and the key in the same transaction.

  4. Report acceptance only after saving succeeds. Let acceptDeliveryOnce finish successfully only after that transaction is committed. The example then returns HTTP 204, telling Informed that your system has accepted responsibility for the notification. If storage fails, let the function throw an error; the receiver returns 503, allowing Informed to retry within its retry limit. Do not simply remove the placeholder error or return early to make delivery appear successful.

  5. Run and monitor any work you queued. A queue is a saved list of jobs your application processes separately. This lets the receiver respond within the 10-second deadline even when a job takes longer. Once the receiver returns 204, failures in that background job must be retried or handled by your application; Informed considers the delivery accepted. Give your team a way to see and recover jobs that fail.

  6. Check a real notification before wider use. Enable one subscription with the webhook selected, trigger a matching event, and confirm that the intended action completes in your system. Have your developer also check that a repeated delivery creates no duplicate work and that a temporary storage failure is retried. Expand to other subscriptions after these checks pass.

Treat system.test as a connectivity check, without creating orders or running your normal notification workflow. When adding event handlers, allow additional JSON fields and safely acknowledge event types you intentionally ignore after authentication.

Adapt the receiver for your environment

The example uses a single server with a reverse proxy on the same host. If your application follows a different receiver architecture that:

  • directly listens on port 443 and handles TLS itself

  • runs in a container or as a serverless function

  • sits behind a load balancer

  • has requests routed to it by an API gateway

  • uses an existing web framework

you must adapt the listener and request-body capture shown above to that platform. For example, a proxy in a separate container cannot reach this receiver through its own 127.0.0.1. The public HTTPS requirement, signature checks, and challenge response stay the same.


Related articles

Did this answer your question?