API Webhooks: Offer Change Notifications
Introduction
The Platform publishes real-time webhook notifications whenever a vendor's published offers change. These notifications cover offer creation, updates, unpublishing, archiving, deletion, and vendor synchronization. By subscribing to the productcatalog.offer.updated event, you can keep external marketplaces and downstream systems aligned with the latest catalog data without polling.
This article walks through the end-to-end process of subscribing to the offer change webhook — from obtaining credentials through creating a subscription to receiving and interpreting the delivered payload — with examples for vendors such as Microsoft, Adobe, and Acronis.
In this article
- Who is this for
- Prerequisites
- Overview
- Create a subscription
- Subscription examples
- How scoping works
- Filter expressions
- Delivered payload
- Payload field reference
- Events and triggers
- No event for
- Change types
- Notes for implementers
Who is this for
This webhook is intended for Distributor-level users that need visibility into a vendor's full offer catalog. The typical subscriber is a Product Manager responsible for a vendor's offers, or a system that synchronizes catalog data on behalf of a Distributor.
WARNING! Scoping is at the vendor level, not the individual-offer level. A subscription bound to a vendor's ID receives change events for all of that vendor's offers, including specialized, private, and restricted offers. The webhook does not filter by per-seller catalog entitlement. It is not designed for individual Sellers who should only see a subset of a vendor's catalog.
Prerequisites
Before you can create a subscription, make sure you have the following:
- Bearer Token — this token is issued exclusively by the Support Team upon request. It is provided directly to users with the Product Manager role for the vendors associated with that role. For security, the token is delivered only to the designated Product Manager.
- Vendor ID — the GUID of the vendor whose offers you want to track. As a Product Manager for that vendor, you should already have this. If you are unsure, contact the Support Team.
-
Organization ID — your organization's GUID. The API validates that you are a member of this organization and that the
tenantId(vendor ID) belongs to it. - Callback URL — an HTTPS endpoint you control where the platform will deliver webhook payloads.
- HMAC secret — a Base64-encoded secret you generate. The platform uses it to sign each payload so you can verify authenticity on your end.
Overview
When a published offer is created, modified, or removed, the platform captures a before/after snapshot of the offer, computes the delta, and publishes a productcatalog.offer.updated event. The webhook service matches the event against your active subscriptions and delivers the payload to your callback URL.
NOTE! There is no platform UI for managing webhook subscriptions. All subscription management is done through the API.
Create a subscription
To subscribe to offer-change notifications, send a POST request to the webhook subscriptions endpoint:
POST /api/v1/webhooks/subscriptions/ Headers: Authorization: Bearer <your-token> Content-Type: application/json
Request body:
{
"tenantId": "<vendor-id>",
"name": "my-offer-subscription",
"targetUrl": "https://your-endpoint.example.com/webhook",
"eventType": "productcatalog.offer.updated",
"organizationId": "<your-organization-id>",
"organizationType": "Vendor",
"secretBase64": "<your-base64-encoded-secret>",
"filter": null,
"allTenants": false
}A successful request returns 201 Created.
NOTE! Currently, it's possible to create up to 20 subscriptions per organization.
Required fields
-
tenantId— the vendor ID of the vendor whose offers you want to track. Must belong to your organization. -
targetUrl— the HTTPS endpoint where the platform delivers webhook payloads. -
eventType— must be"productcatalog.offer.updated". -
organizationId— your organization's ID. The API validates that the caller is a member of this organization and that thetenantIdis associated with it. -
organizationType— one of:Vendor,Distributor, orReseller. -
secretBase64— a Base64-encoded secret used to HMAC-sign delivered payloads, so you can verify authenticity.
NOTE! If you create a subscription with the same organizationId, eventType, and targetUrl as an existing one, the API returns 409 Conflict. Change the URL path or delete the existing subscription first.
Subscription examples
The examples below show how to subscribe to offer changes for different vendors. Replace the placeholder GUIDs with the actual values for your organization and vendor. All examples use allTenants: false and no filter, meaning you receive all offer events for that specific vendor.
Example: Microsoft offers
{
"tenantId": "<microsoft-vendor-id>",
"name": "microsoft-offer-changes",
"targetUrl": "https://your-endpoint.example.com/webhooks/microsoft",
"eventType": "productcatalog.offer.updated",
"organizationId": "<your-organization-id>",
"organizationType": "Vendor",
"secretBase64": "<your-base64-encoded-secret>",
"filter": null,
"allTenants": false
}Example: Adobe offers
{
"tenantId": "<adobe-vendor-id>",
"name": "adobe-offer-changes",
"targetUrl": "https://your-endpoint.example.com/webhooks/adobe",
"eventType": "productcatalog.offer.updated",
"organizationId": "<your-organization-id>",
"organizationType": "Vendor",
"secretBase64": "<your-base64-encoded-secret>",
"filter": null,
"allTenants": false
}Example: Acronis offers
{
"tenantId": "<acronis-vendor-id>",
"name": "acronis-offer-changes",
"targetUrl": "https://your-endpoint.example.com/webhooks/acronis",
"eventType": "productcatalog.offer.updated",
"organizationId": "<your-organization-id>",
"organizationType": "Vendor",
"secretBase64": "<your-base64-encoded-secret>",
"filter": null,
"allTenants": false
}Example: filter to a specific offer
To receive events for only one specific offer, add a filter expression:
{
"tenantId": "<vendor-id>",
"name": "single-offer-tracking",
"targetUrl": "https://your-endpoint.example.com/webhooks/filtered",
"eventType": "productcatalog.offer.updated",
"organizationId": "<your-organization-id>",
"organizationType": "Vendor",
"secretBase64": "<your-base64-encoded-secret>",
"filter": "{\"offerId\":{\"$eq\":\"<offer-id>\"}}",
"allTenants": false
}How scoping works
The tenantId field on your subscription is the primary scoping mechanism. The platform only delivers events where the offer's vendor ID matches your subscription's tenantId. This check happens server-side before delivery. The API also validates that the tenantId belongs to your organizationId, so you can only subscribe to vendors associated with your own organization.
Vendor-level scoping
Scoping operates at the vendor level. When you subscribe with a vendor's ID, you receive change events for all of that vendor's published offers — there is no per-offer filtering based on catalog assignments, entitlements, or seller visibility. If you need only specific offers, use a filter expression to narrow by offerId, productId, or statusAfter.
Separate vendor IDs vs. shared vendor ID
How offer events are isolated between partners depends on how vendors are configured on the platform:
-
Separate vendor IDs — if two Distributors (for example, Distributor A and Distributor B) each have their own vendor profile for the same vendor, their offers have different vendor IDs. Distributor A subscribes with
tenantId= their vendor ID, Distributor B subscribes with their own. The subscriptions are fully isolated — a change to Distributor A's offer reaches only Distributor A. -
Shared vendor ID — if both Distributors share the same vendor profile (same vendor ID), then both subscriptions use the same
tenantId, and a change to any offer under that vendor is delivered to both. In this case, usefilterexpressions to narrow by specificofferIdorproductIdvalues relevant to your organization.
allTenants
Setting allTenants: true bypasses vendor scoping entirely. The subscription then receives events for every vendor's offers of the specified event type. This is intended for internal and platform-level consumers only.
WARNING! With allTenants set to true, your subscription receives every vendor's offer events across the platform. Only use this for internal or platform-level integrations.
Filter expressions
To narrow your subscription beyond vendor scoping, use the filter field. Set it to null to receive all events for the vendor, or provide a filter expression to match specific offers or statuses.
The filter value must be a JSON string (not a JSON object) and must use an operator expression. Passing a raw object returns an "Invalid JSON" error, and passing a bare value without an operator returns a 400 error.
To track a specific offer:
"filter": "{\"offerId\": {\"$eq\": \"<offer-id>\"}}"To receive only events where the offer ends in Published status:
"filter": "{\"statusAfter\": {\"$eq\": \"Published\"}}"Allowed filter fields (root-level only):
offerIdvendorIdproductIdstatusAfter
Delivered payload
Your callback URL receives a flat JSON object containing offer identity fields, status transitions, and granular change deltas. There is no outer envelope or wrapper — all fields sit at the root level of the JSON body.
Example delivered payload:
{
"tenantId": "<vendor-id>",
"offerId": "<offer-id>",
"vendorId": "<vendor-id>",
"productId": "<product-id>",
"changeType": "Changed",
"offerName": "Example Offer Name",
"productName": "Example Product",
"customId": "<external-identifier>",
"statusBefore": "Published",
"statusAfter": "Published",
"detectedAt": "2026-08-19T10:45:10.4105680Z",
"renewalSettings": {
"isAutoRenewable": true,
"isAutoRenewalEditable": false,
"renewalStatusChangeDeadline": 0,
"minDaysBeforeRenewalChanges": 1,
"maxDaysBeforeRenewalChanges": 2
},
"fieldChanges": [
{ "field": "unspscCode", "before": 12345, "after": 12346 },
{ "field": "learnMoreUrl", "before": "https://example.com/old", "after": null }
],
"countryChanges": {
"added": [{ "countryId": "<country-id>", "countryName": "Latvia" }],
"removed": [{ "countryId": "<country-id>", "countryName": "Estonia" }],
"hasChanges": true
},
"industryChanges": { "added": [], "removed": [], "hasChanges": false },
"billingCycleChanges": { "added": [], "removed": [], "hasChanges": false },
"domainChanges": { "added": ["example.com"], "removed": [], "hasChanges": true },
"masterOfferRelationChanges": {
"added": [{
"offerId": "<related-offer-id>",
"offerName": "Example Upgrade Offer",
"relationType": "CanBeUpgradedTo",
"allowChangeConfiguration": true,
"restriction": "OnlyOnRenewal"
}],
"removed": [],
"hasChanges": true
},
"slaveOfferRelationChanges": { "added": [], "removed": [], "hasChanges": false },
"localizationChanges": {
"added": [{ "language": "en", "field": "shortName", "value": "New text" }],
"removed": [{ "language": "en", "field": "shortName", "value": "Old text" }],
"hasChanges": true
},
"billingRulesConfigChanges": [
{ "field": "billingCycle", "before": "Monthly", "after": "Yearly" }
]
}NOTE! The renewalSettings block appears only when a renewal-related field changed. It reflects the current state of the renewal configuration, not a before/after delta.
Payload field reference
Identity and status fields
| Field | Type | Description |
|---|---|---|
tenantId |
string (GUID) | The vendor's ID. Always equals vendorId. |
offerId |
string (GUID) | The unique identifier of the offer. |
vendorId |
string (GUID) | The vendor that owns this offer. |
productId |
string | null | The product this offer belongs to. Null if unlinked. |
changeType |
string | One of: Added, Changed, or Removed. |
offerName |
string | Display name of the offer. |
productName |
string | Display name of the product. |
customId |
string | Vendor-defined external identifier for the offer. |
statusBefore |
string | Offer status before the change (enum name). |
statusAfter |
string | Offer status after the change (enum name). |
detectedAt |
string (ISO 8601) | UTC timestamp when the change was detected. |
Delta collections
Each delta collection is always present in the payload, even when empty. Use the hasChanges boolean to identify which deltas contain actual changes.
| Field | Type | Description |
|---|---|---|
fieldChanges |
array of objects | Scalar field changes: { field, before, after }. Both sides always present, including nulls. |
countryChanges |
object | { added: [{ countryId, countryName }], removed: [...], hasChanges } |
industryChanges |
object |
{ added: [string], removed: [string], hasChanges }. String arrays. |
billingCycleChanges |
object |
{ added: [string], removed: [string], hasChanges }. String arrays. |
domainChanges |
object |
{ added: [string], removed: [string], hasChanges }. String arrays. |
masterOfferRelationChanges |
object | { added: [{ offerId, offerName, relationType, allowChangeConfiguration, restriction }], removed, hasChanges } |
slaveOfferRelationChanges |
object | Same structure as masterOfferRelationChanges. |
localizationChanges |
object |
{ added: [{ language, field, value }], removed: [...], hasChanges }. Field is one of: shortName, fullName, description, termsAndConditions, marketing, specification. |
billingRulesConfigChanges |
array of objects |
{ field, before, after }. Same structure as fieldChanges. |
Events and triggers
An event is published when an offer that is (or was) in Published status is:
- Created and published
- Updated — any offer-form field, including countries, domains, localization, marketing (image, CTA, learn-more URL), UNSPSC code, add-on flag, order-cancellation support, renewal settings, provisioning email or method, billing rules, offer relations, product, or status
- Published, unpublished, archived, or deleted
- Scheduled for publish or unpublish (including schedule edits and cancellations)
- Synced from a vendor
Events are triggered by both the portal (old and new UI) and the API.
No event for
The following changes do not produce events:
- Draft offers — changes to offers that are not and have never been in Published status.
- No-op saves — if the before and after snapshots are identical, no event is published.
- Industry changes on a published offer — industries are immutable once an offer is published.
- Video and icon uploads — these are stored by file path, not on the offer entity.
Change types
The changeType field indicates what kind of change occurred:
-
Added— the offer became visible (first publish or re-publish). The event carries the offer's full current state, so the first message you receive describes the entire offer. -
Changed— an edit to an already-published offer. -
Removed— the offer stopped being visible (unpublish, archive, or delete).
Notes for implementers
- Status fields are enum names (for example,
"Published"), not integers. -
fieldChangesalways carries bothbeforeandafter, includingnullvalues. A cleared field appears as"after": null, so you can distinguish "cleared" from "not reported." - Simple collections (
industryChanges,billingCycleChanges,domainChanges) use string arrays. Structured collections (countryChanges, relation deltas,localizationChanges) use object arrays with display names on each entry. - Every delta object is always present in the payload, even when empty. Use
hasChangesto find deltas that contain actual changes. There is no top-levelhasChanges. - Each removed entry carries its own name (for example,
countryNameon a removed country), taken from the state before removal. - Timestamps (
detectedAt) are UTC with a trailingZ. - Serialization uses camelCase property names. Null fields are omitted from the payload.
- Payloads are HMAC-signed with the
secretBase64you provided during subscription creation, so you can verify authenticity on your end. - Webhook publishing is non-blocking: if the platform encounters a delivery issue, it retries automatically. A failed delivery never delays or affects the underlying offer operation.
Summary
The offer change webhook lets Distributors and platform integrators receive real-time notifications when published offers are created, updated, or removed from the catalog. To get started, request a Bearer Token from the Support Team, then create a subscription through the API with a tenantId matching the target vendor's ID. Scoping is at the vendor level — you receive all offer events for that vendor. Use filter expressions to narrow to specific offers, products, or statuses. The delivered payload is a flat JSON object containing identity fields, status transitions, and granular deltas for every tracked change.
Add comment
Please sign in to leave a comment.