Webhooks
Subscribe an https endpoint to account events, verify the HMAC signature on each delivery, and understand the retry schedule and the auto-disable rule.
Outbound webhooks: we POST to your endpoint when something happens in your account, so you do not have to poll for it. Managing subscriptions needs WebhooksWrite.
POST /api/webhooks/Create
{
"Url": "https://example.com/hooks/marketplacehub",
"EventTypes": ["OrdersDownloaded", "MarketplaceUnhealthy", "MarketplaceRestored"]
}
Urlmust be an absolute https URL on a publicly reachable host. A host that resolves to a private or reserved address is refused — at registration, and again at every delivery, which is what covers a host that only starts resolving privately later. https is required rather than recommended: the signature proves a delivery came from us and was not altered, but it does nothing to keep the body private, and these payloads describe your trading activity.EventTypesis a list of event names, at least one. An unknown name is rejected rather than ignored — silently dropping one would leave you waiting for an event that was never subscribed.
The response carries the subscription and its Secret — the HMAC signing secret for this endpoint. It is shown once and can never be retrieved again; it is stored encrypted, so nobody — including us — can read it back. If you lose it, delete the subscription and create another, which also rotates the secret. An Idempotency-Key replay returns the subscription without the secret, with a warning saying so — retry-safety without the secret outliving its one response.
POST /api/webhooks/List and /api/webhooks/Delete
List returns every subscription with its health:
{
"Subscriptions": [
{
"SubscriptionId": "6a73...",
"Url": "https://example.com/hooks/marketplacehub",
"EventTypes": ["OrdersDownloaded"],
"IsActive": true,
"ConsecutiveFailures": 0,
"DisabledDate": null,
"DisabledReason": null,
"LastSuccessDate": "2026-08-07T10:00:00Z",
"CreatedDate": "2026-08-01T00:00:00Z"
}
]
}
An active subscription with no LastSuccessDate and a climbing ConsecutiveFailures usually means the URL was wrong from the start. Delete takes the SubscriptionId; it is permanent and stops delivery at once.
Events
| Event | Fires when |
|---|---|
OrdersDownloaded | New orders were pulled from a marketplace. |
ListingPublished | A publish batch to a marketplace finished. |
FeedOutcome | A marketplace feed submission was rejected outright or processed with per-item errors. A clean feed is silent. |
MarketplaceUnhealthy | A marketplace connection stopped working — usually authorisation being revoked. |
MarketplaceRestored | That connection started working again. |
The names are the wire contract — events may be added over time but are never renamed, so filter on the name and ignore ones you do not recognise.
Payloads are small notifications, not documents. They tell you that something happened and where; fetch the data itself through the API. An OrdersDownloaded receiver, for example, follows up with POST /api/orders/List and UpdatedSince:
{
"Marketplace": "Amazon United Kingdom",
"Platform": "Amazon",
"Country": "United Kingdom",
"NewOrderCount": 3,
"UserPlatformId": "6a73..."
}
Per event: ListingPublished carries JourneyId, UserPlatformId, PublishedCount and FailedCount. FeedOutcome carries Marketplace, UserPlatformId, FeedKind (Product listing, Price & stock or Order updates), Outcome (Rejected — nothing was processed — or ProcessedWithErrors), Status and FailedCount; branch on Outcome, not on the human-facing text. The two marketplace-health events carry Marketplace, Platform, UserPlatformId, Reason and Detail, and fire only on an actual change of state — a connection that stays broken does not repeat the event.
The delivery request
Each delivery is a POST with a JSON body and these headers:
| Header | Carries |
|---|---|
X-MH-Signature | v1= plus the hex HMAC — see below. |
X-MH-Timestamp | When the request was signed, in Unix seconds. |
X-MH-Event | The event name, so you can route before parsing the body. |
X-MH-Delivery-Id | One logical delivery. The same across retries — this is your de-duplication key. |
X-MH-Attempt | Which attempt this is, starting at 1. |
Answer with any 2xx within 10 seconds. A receiver that takes longer is treated as down and retried — so acknowledge first and do the work after, not the other way round. The body's bytes are frozen when the event fires: every attempt of a delivery POSTs identical bytes, which is what makes the signature and your de-duplication stable across retries.
Verifying the signature
Verify every delivery. The URL is guessable; the signature is not.
- Reject the request if
X-MH-Timestampis more than 5 minutes from now, either way. The signature covers the timestamp precisely so an old delivery cannot be replayed at you. - Derive the signing key:
key = HMAC-SHA256(key: your secret, message: "marketplacehub.webhook.v1"). The key is derived, not the raw secret — signing with a context-named key means the same secret used anywhere else can never produce a signature that verifies here. - Compute
expected = "v1=" + lowercase hex of HMAC-SHA256(key, "{timestamp}." + raw request body)— the timestamp string, a dot, then the body's raw bytes exactly as received. Parse nothing first. - Compare against
X-MH-Signaturewith a constant-time comparison. A compare that returns early on the first wrong byte leaks how much of the signature was correct, which is enough to forge one a byte at a time.
The v1= prefix names the scheme, so it can be rotated without ambiguity if the algorithm ever changes.
Retries, and when we give up
Anything other than a 2xx is retried on a backoff of 1m, 5m, 15m, 1h, 3h, 6h — seven attempts over roughly half a day, long enough to ride out a deploy or a certificate renewal. With one carve-out: a 4xx is not retried, except 408 and 429 — a receiver saying "bad request" will say it again, while those two explicitly mean "not now" rather than "not ever". A transport failure with no status at all is always retried, because that is what a restart or a DNS blip looks like.
After 6 consecutive failed deliveries the subscription is disabled: IsActive goes false and DisabledReason says why. The counter resets on any success, so a merely flaky endpoint stays active. Re-enabling is a delete and a re-create — which also rotates the secret.
Every attempt is recorded in API → Webhooks in the app: status, response snippet and timing per attempt, kept for 30 days. When deliveries are not arriving, that log answers whether we could not reach you or you answered something other than a 2xx.