Skip to main content

Change Events

Quantaprice provides two mechanisms for tracking changes to your data: a polling changelog and push webhooks. Both cover all entity types — articles, pricelists, prices, currencies, VAT rates, tax areas, rounding profiles, and settings.

Overview

MechanismHow it worksBest for
ChangelogPoll a per-entity GET /{entity}/changelog with a cursorReliable sync, guaranteed ordering, backfill
WebhooksRegister a URL, receive POST on changeReal-time notifications, event-driven workflows

Both systems use the same underlying change feed. A change recorded in the changelog will also trigger any matching webhooks.

Changelog (Polling)

The changelog is an ordered, append-only feed of changes, with a dedicated endpoint per entity type. You consume it by polling with a cursor.

Changelog endpoints

EndpointEntity
GET /article/changelogArticles
GET /pricelist/changelogPricelists
GET /price/changelogPrices
GET /currency/changelogCurrencies
GET /vatclass/changelogVAT classes
GET /vatrate/changelogVAT rates
GET /taxarea/changelogTax areas
GET /fx/changelogFX rates
GET /rounding/changelogRounding profiles
GET /settings/changelogSettings
GET /article-taxclass/changelogArticle–tax-class mappings

Parameters (same for all changelog endpoints)

ParameterTypeDefaultDescription
cursorstringOpaque cursor from a previous response. Omit for first request.
sinceISO-8601Timestamp to start from (alternative to cursor on first request)
limitinteger100Max items per page (1–1000)

Basic usage

First request (start from a specific time):

GET /article/changelog?since=2026-01-01T00:00:00Z&limit=100

Response:

{
"items": [
{
"change_type": "created",
"code": "SKU-001",
"updated_at": "2026-01-15T10:30:00Z",
"updated_by": "api-key-abc"
},
{
"change_type": "updated",
"code": "SKU-002",
"updated_at": "2026-01-15T10:31:00Z",
"updated_by": "api-key-abc"
}
],
"next_cursor": "eyJzIjogMTIzNDV9",
"has_more": true
}

Subsequent requests (pass the cursor from the previous response):

GET /article/changelog?cursor=eyJzIjogMTIzNDV9&limit=100

Continue polling until has_more is false, then poll periodically (e.g. every few seconds) to pick up new changes.

Event fields

FieldDescription
change_typecreated, updated, or deleted
codeBusiness identifier — SKU for articles/prices, pricelist code for pricelists, currency code for currencies, etc.
updated_atISO-8601 timestamp of when the change occurred
updated_byWho made the change (API key, user, or system)

Price changelog format

The price changelog (GET /price/changelog) uses a richer format since a price is identified by both a SKU and a pricelist:

{
"items": [
{
"change_type": "updated",
"sku": "SKU-001",
"pricelist": "PL-RETAIL",
"updated_at": "2026-01-15T10:31:00Z",
"updated_by": "api-key-abc"
}
],
"next_cursor": "eyJzIjogMjM0NTZ9",
"has_more": false
}
FieldDescription
change_typecreated, updated, or deleted
skuSKU code
pricelistPricelist code
updated_atISO-8601 timestamp
updated_byWho made the change

Changelog retention

The changelog holds a fixed number of entries determined by your plan tier. When the changelog is full, the oldest entries are evicted to make room for new ones.

TierMax entriesTypical window at 100K changes/day
Starter1,000,000~10 days
Standard10,000,000~100 days
Enterprise100,000,000~2.7 years

For most workloads (continuous price updates, article changes), the changelog window spans weeks to months. During large bulk imports, the window shortens temporarily as old entries are evicted, then stabilizes again.

Cursor expiry

If you don't poll frequently enough and your cursor falls behind the oldest available entry, you'll receive:

HTTP 410 Gone
{
"error": "Cursor has expired. Resume from a newer position."
}

What to do when your cursor expires:

  1. Perform a full sync of the data you need (e.g. re-fetch all articles, prices for your pricelists)
  2. Start polling again without a cursor (or use since with the current timestamp)
  3. Consider polling more frequently to avoid expiry
loop:
response = GET /{entity}/changelog?cursor={last_cursor}&limit=1000

if response.status == 410:
perform_full_sync()
last_cursor = null
continue

for event in response.items:
process(event)

last_cursor = response.next_cursor

if not response.has_more:
sleep(5 seconds)

Webhooks (Push)

Webhooks deliver change events to your HTTP endpoint in near real-time. Events are batched (collected over a short window) and delivered as a single POST request, signed with HMAC-SHA256.

Register a webhook

POST /webhooks
{
"url": "https://your-app.com/webhook",
"secret": "your-signing-secret",
"entity_type_filter": null
}
FieldTypeRequiredDescription
urlstringYesHTTPS endpoint to receive events
secretstringNoSecret for HMAC-SHA256 signature verification
entity_type_filterstringNoOnly receive events for this entity type. null = all types.

Response (201 Created):

{
"id": 1,
"url": "https://your-app.com/webhook",
"entity_type_filter": null,
"active": true,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}

Delivery format

When changes occur, your endpoint receives a POST request:

POST https://your-app.com/webhook
Content-Type: application/json
X-Signature-256: sha256=a1b2c3d4e5f6...
{
"webhook_id": 1,
"delivered_at": "2026-01-15T10:30:05Z",
"events": [
{
"entity_type": "price",
"change_type": "updated",
"entity_code": "SKU-001",
"composite_key": "PL-RETAIL",
"changed_at": "2026-01-15T10:30:00Z",
"changed_by": "api-key-abc",
"content_hash": "e5f6a7b8"
},
{
"entity_type": "price",
"change_type": "updated",
"entity_code": "SKU-002",
"composite_key": "PL-RETAIL",
"changed_at": "2026-01-15T10:30:01Z",
"changed_by": "api-key-abc",
"content_hash": "c9d0e1f2"
}
]
}

Events are batched — a single delivery may contain multiple events that occurred within a short window (~500ms).

Webhook event fields

FieldDescription
entity_typeThe type of entity that changed (article, price, pricelist, currency, vat_rate, vat_class, tax_area, fx_rate, rounding_profile, settings)
change_typecreated, updated, or deleted
entity_codeThe primary business identifier (SKU code, pricelist code, etc.)
composite_keySecondary identifier when needed — e.g. pricelist code for price changes
changed_atISO-8601 timestamp of when the change occurred
changed_byWho made the change (API key, user, or system)
content_hashHash of the entity's content. Same hash = same content. Useful for deduplication.

Signature verification

If you provided a secret, every delivery includes an X-Signature-256 header. To verify:

  1. Compute HMAC-SHA256 of the raw request body using your secret
  2. Compare with the value after sha256= in the header
import hmac, hashlib

expected = hmac.new(
secret.encode(),
request.body,
hashlib.sha256
).hexdigest()

signature = request.headers["X-Signature-256"].removeprefix("sha256=")
assert hmac.compare_digest(expected, signature)

Retry behavior

If your endpoint returns a non-2xx status or times out (10 second limit), the delivery is retried with exponential backoff:

AttemptDelay
11 second
22 seconds
34 seconds
48 seconds
516 seconds
632 seconds
764 seconds
8128 seconds
9256 seconds
10300 seconds (max)

After 10 failed attempts, the delivery is dropped. The webhook remains active for future events.

Filtering

Use entity_type_filter to receive only specific event types. For example, to receive only price changes:

{
"url": "https://your-app.com/price-webhook",
"secret": "secret",
"entity_type_filter": "price"
}

Register multiple webhooks with different filters to route events to different endpoints.

Managing webhooks

OperationEndpoint
List allGET /webhooks
Get oneGET /webhooks/{id}
UpdatePUT /webhooks/{id}
DeactivateDELETE /webhooks/{id}
TestPOST /webhooks/{id}/test

The test endpoint sends a synthetic event to your URL so you can verify your endpoint is working and signature verification is correct.

Best Practices

Choosing between changelog and webhooks

  • Use the changelog for reliable data synchronization. It's ordered, paginated, and you control the pace. If your consumer goes down, you resume from your last cursor.
  • Use webhooks for real-time reactions — invalidating caches, triggering downstream processes, alerting. Webhooks are faster (near real-time) but less reliable than polling.
  • Use both for the best of both worlds: webhooks for low-latency notification that something changed, changelog for reliable catch-up if you miss anything.

Handling bulk imports

During large data imports (e.g. annual price updates), a high volume of events will flow through the system. Your consumers should be prepared for:

  • Higher event volume in both changelog and webhook deliveries
  • Possible cursor expiry if you don't poll frequently enough during the import window
  • Batched webhook deliveries with many events per request

If you know a bulk import is coming, consider increasing your polling frequency temporarily.

Idempotent processing

Design your event handlers to be idempotent. You may occasionally receive the same logical change more than once (e.g. after a cursor expiry and resync, or if a webhook is retried). The webhook content_hash field helps detect duplicates.

What's NOT in the changelog event

Changelog events are notifications, not data carriers. They tell you what changed, not what the new value is. To get the current state, fetch the entity using the appropriate API endpoint:

EntityEndpoint to fetch current state
ArticleGET /article/{sku}
PricelistGET /pricelist/{code}
PriceGET /price/{sku}/{pricelist}
CurrencyGET /currency/{code}
VAT rateGET /vatrate/{taxArea}/{vatClass}
VAT classGET /vatclass/{code}
Tax areaGET /taxarea/{code}

This keeps changelog events lightweight.