For the complete documentation index, see llms.txt. This page is also available as Markdown.

Introduction

React to events in VARIO Cloud instead of polling — registration, delivery, verification.

A webhook lets VARIO Cloud call your service when something happens in the ERP — a document is created, an article changes, stock moves. Instead of polling, you subscribe once and get notified.

Webhooks are also how scheduled work is triggered for Apps: a cron webhook is the same delivery mechanism, fired by a timer instead of a data change.

Concepts

A subscription has two parts, together called a destination:

Part
Meaning

destinationQueue

The event, e.g. article.update or sales_channel.create

destinationOwner

Who owns the queue. Defaults to #vario# — the ERP's own events

Plus the url VARIO should call, and your appIdentifier.

Discovering available events

You don't have to guess event names. Ask the ERP:

GET /cmn/system/app-message-webhook/destinations

It returns every queue the backend offers:

[
  { "owner": "#vario#", "queue": "article.update", "description": "" },
  { "owner": "#vario#", "queue": "sales_channel.create", "description": "" }
]

All webhook endpoints live under your tenant base URL with the versioned prefix:

→ See REST API Introduction for base URLs. {version} is latest unless you pin one.

The destinations list is generated from the backend's definitions. For documents it contains every possible combination of category and transition — several thousand entries, and not all of them are ever emitted. A subscription to a topic that never fires registers successfully and simply stays silent. Verify against a real event before assuming your handler is broken.

Registering

Verb
Path
Purpose

POST

…/app-message-webhook/register

Subscribe

POST

…/app-message-webhook/deregister

Unsubscribe

GET

…/app-message-webhook/destinations

List available queues

Body for register and deregister:

url and destinationQueue are required; destinationOwner defaults to #vario#.

Only http://, https:// and ws://topic/ URLs are accepted. Keep URLs short — the stored columns are limited to 255 characters.

From an App

The framework wraps this in migration helpers, which also prefix your path with the app's public domain:

Register webhooks inside always() rather than setMigration(), and prefer registerWebhookIfNotExists() — that combination is idempotent and re-registers after a reinstall.

Registration is idempotent

Registering the same combination twice does not create a duplicate: an active subscription is returned unchanged, and a previously removed or deactivated one is reactivated.

The identity is appIdentifier + destinationOwner + destinationQueue + the full URL. Change the URL and you get a second subscription — the old one keeps firing. This is the usual cause of handlers running twice.

Event names

The grammar is dotted and lowercase:

Examples: article.update, article-price.create, category-tree.delete, sales_channel.stockChange, shipment.parcel.completed.

Some events are filtered, taking a parameter in parentheses:

Substitute the value yourself when registering. The value must be a plain word — numeric ids work, values containing dashes do not match.

For articles, the plain and the filtered event are mutually exclusive for a given change: a listing on a non-system sales channel fires the filtered form, a change on the system sales channel fires the plain article.update. Subscribing to both does not double-deliver, but subscribing to the wrong one delivers nothing.

Document events are composed from the document category and its transition:

What your handler receives

A POST with Content-Type: application/json and these headers:

Header
When

Authorization: Bearer <appToken>

Only if the subscription has an appIdentifier

x-user-id

Only if a user triggered the event — absent for system and scheduled events

The body is small — normally just identifiers, not the full entity:

Event
Body

document.<category>.<transition>

{ "documentId": … }

article.*

{ "articleId": …, "listingId": …, … }

account.*

{ "id": … }

sales_channel.create / .update / .delete

{ "salesChannelId": … }

sales_channel.stockChange

{ "salesChannelId": …, "articleId": … }

shipment.parcel.completed

{ "shipmentId": …, "parcelId": … }

Fetch what you need with VQL or the REST API. An event with no payload arrives as {}.

Verifying the call

There is no HMAC signature. Authenticity comes from the bearer token, which is a JWT:

  • signed with ES256,

  • verifiable with the JWK set you received when creating the App,

  • whose aud claim equals your appIdentifier,

  • with a valid exp.

Reject anything that fails those checks. If you build on the App Framework this already happens for every /api/* route — see App Authentication.

Delivery semantics

Read this section before you write a handler.

Property
Behaviour

Timeout

5 seconds, for connect and response

Attempts

3, then the delivery is permanently abandoned

Guarantee

At least once

Ordering

None — do not assume events arrive in the order they happened

Backoff

No fixed schedule; retries happen when the sender next runs

Because delivery is at-least-once and a timeout cannot distinguish "not received" from "received and slow", handlers must be idempotent. Note that duplicate protection held in memory does not span processes — if you run more than one instance, you need a shared guard.

A retry only happens the next time the sender runs, which is triggered by new activity. On a quiet tenant a failed delivery can wait a long time. After the third failure it is never retried.

Preventing infinite loops

If your app reacts to article.update by writing back to the article, that write triggers article.update again. Break the cycle with a header on your write:

This suppresses only your own subscriptions — other apps still receive the event.

Bulk imports have a separate switch: an import rule set can suppress webhooks for the whole run.

Cron webhooks

A cron webhook fires on a schedule instead of a data change. It needs two things:

  1. A subscription with owner APP and your own queue name:

  1. A scheduled task with the same destination and a cron expression.

Because the owner is not #vario#, the queue name is not validated — a typo registers happily and never fires.

Inspecting what happened

Subscriptions and deliveries are queryable with VQL:

A subscription is ACTIVE, DEACTIVATED (switched off by a user), REVOKED (switched off by the platform) or REMOVED (deregistered).

A delivery is QUEUED, SENT, FAILED (will be retried), ERRONEOUS (gave up) or INACTIVE. Failed attempts store the error in response — that is where to look first when a handler "never gets called".

When nothing arrives

Work through this in order:

  1. Is the App active for the tenant? Events for inactive apps are dropped before a delivery record is even created — so you will find no failed deliveries either.

  2. Is the subscription there and ACTIVE? Query system.queryAppMessageWebhook.

  3. Does the topic actually fire? Especially for document events, the topic may be listed but never emitted.

  4. Are deliveries failing? Query system.queryAppMessageEntry and read recipients.response.

  5. Are you returning 401? If the subscription lacks appIdentifier, no token is sent and the framework rejects the call.

  6. Are you too slow? Anything over 5 seconds counts as failed.

  7. Did you suppress it yourself? Check whether your own write set the suppression header.

  • REST API Introduction — base URL and versioned paths

  • App Authentication — verifying the inbound token

  • VQL — fetching the entity behind an event

  • FAQ

Last updated

Was this helpful?