API Webhooks: Offer Change Notifications

Appxite

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

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:

  1. 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.
  2. 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.
  3. 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.
  4. Callback URL — an HTTPS endpoint you control where the platform will deliver webhook payloads.
  5. 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 the tenantId is associated with it.
  • organizationType — one of: Vendor, Distributor, or Reseller.
  • 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, use filter expressions to narrow by specific offerId or productId values 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):

  • offerId
  • vendorId
  • productId
  • statusAfter

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.
  • fieldChanges always carries both before and after, including null values. 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 hasChanges to find deltas that contain actual changes. There is no top-level hasChanges.
  • Each removed entry carries its own name (for example, countryName on a removed country), taken from the state before removal.
  • Timestamps (detectedAt) are UTC with a trailing Z.
  • Serialization uses camelCase property names. Null fields are omitted from the payload.
  • Payloads are HMAC-signed with the secretBase64 you 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.

Was this article helpful?

0 out of 0 found this helpful

Add comment

Please sign in to leave a comment.