Subscriptions and notifications
Every example below runs against the quickstart broker
(examples/quickstart/compose.yml, or cargo run -p antares-broker) with
its three TemperatureSensor entities from seed.sh. Notifications are
received by a small HTTP server on port 9380 that answers 200 and prints
each body. $U is http://localhost:9090/ngsi-ld/v1 and $CTX the core
context URL used by seed.sh.
Create a subscription
curl -si -X POST $U/subscriptions -H 'Content-Type: application/ld+json' -d '{
"id": "urn:ngsi-ld:Subscription:hot", "type": "Subscription",
"entities": [{"type": "TemperatureSensor"}],
"q": "temperature>30",
"watchedAttributes": ["temperature"],
"notification": {
"attributes": ["temperature"],
"endpoint": {"uri": "http://localhost:9380/notify", "accept": "application/json"}
},
"@context": "'$CTX'"}'
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:hot
A change that matches sends one notification. After
PATCH $U/entities/urn:ngsi-ld:TemperatureSensor:qs:1/attrs with
temperature 31.7, the receiver gets:
{
"id": "urn:ngsi-ld:Notification:e5fb42a7-f1c4-49d2-bbb8-bf2255448669",
"type": "Notification",
"subscriptionId": "urn:ngsi-ld:Subscription:hot",
"notifiedAt": "2026-08-26T15:55:28.489Z",
"data": [
{"id": "urn:ngsi-ld:TemperatureSensor:qs:1", "type": "TemperatureSensor",
"temperature": {"type": "Property", "unitCode": "CEL", "value": 31.7}}
]
}
With accept: application/json the @context travels in the Link
header:
Content-Type: application/json
Link: <https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
With accept: application/ld+json it is a member of the body instead.
What triggers a notification
entities(type, id, idPattern) andq,geoQ,scopeQselect the entities;watchedAttributeslimits which attribute changes count; with nowatchedAttributesevery attribute of a matching entity counts.notificationTriggerdefaults to["attributeCreated", "attributeUpdated"], as the stored subscription above shows; addattributeDeleted,entityCreated,entityUpdatedorentityDeletedto hear about those.- An update that changes nothing sends nothing.
timeIntervalreplaces change-driven delivery with a periodic one, see below. A subscription cannot carry bothtimeIntervalandwatchedAttributes.
Notification shape
notification.format picks the entity representation:
| format | data entry |
|---|---|
normalized (default) | full attributes, as above |
keyValues (alias simplified) | "temperature": 29.0, GeoProperty values as bare GeoJSON |
concise | attributes without the type member where it can be inferred |
notification.attributes restricts the attributes in each entry.
sysAttrs: true adds createdAt/modifiedAt at entity and attribute
level. showChanges: true adds previousValue (or previousObject,
previousLanguageMap) next to the new value; it requires a normalized
format:
HTTP/1.1 400 Bad Request
{"detail":"showChanges cannot be true when format is keyValues (5.2.14)","title":"BadRequestData",...}
One patch of temperature to 29.0, seen by a showChanges + sysAttrs
subscription and by a keyValues one:
{"id": "urn:ngsi-ld:TemperatureSensor:qs:1", "type": "TemperatureSensor",
"createdAt": "2026-08-26T15:55:28.402Z", "modifiedAt": "2026-08-26T15:56:04.370Z",
"temperature": {"type": "Property", "value": 29.0, "previousValue": 31.7, "unitCode": "CEL",
"createdAt": "2026-08-26T15:55:28.402Z", "modifiedAt": "2026-08-26T15:56:04.370Z"}}
{"id": "urn:ngsi-ld:TemperatureSensor:qs:1", "type": "TemperatureSensor",
"location": {"type": "Point", "coordinates": [19.15, 48.73]}, "temperature": 29.0}
Grouped delivery
One request produces at most one notification per subscription, however
many entities it touched. A batch upsert of two sensors reaches the
keyValues subscription as one POST:
{
"id": "urn:ngsi-ld:Notification:c06c6141-a3fc-4553-8369-ddeb69aa56a2",
"type": "Notification",
"subscriptionId": "urn:ngsi-ld:Subscription:kv",
"notifiedAt": "2026-08-26T15:56:06.437Z",
"data": [
{"id": "urn:ngsi-ld:TemperatureSensor:qs:2", "type": "TemperatureSensor", "temperature": 27.5},
{"id": "urn:ngsi-ld:TemperatureSensor:qs:3", "type": "TemperatureSensor", "temperature": 34.9}
]
}
and timesSent moves by one. A notification body is capped at 4 MiB,
the same limit the broker accepts on inbound bodies. A grouped delivery
over the cap is split at whole-entity boundaries into several
notifications; a single entity larger than the cap is sent alone.
Periodic delivery: timeInterval
{"id": "urn:ngsi-ld:Subscription:tick", "type": "Subscription",
"entities": [{"type": "TemperatureSensor"}], "timeInterval": 2,
"notification": {"format": "keyValues",
"endpoint": {"uri": "http://localhost:9380/tick", "accept": "application/json"}}}
Every 2 seconds the broker sends all matching entities, changed or not; five seconds after creation the receiver holds two notifications with all three sensors each. Any interval greater than zero is accepted, fractions included; the broker checks due subscriptions twice a second, so an interval below that is served at the tick rate. With NATS and several broker pods, one pod claims each tick, so an interval fires once per fleet.
Throttling
"throttling": 30 sends at most one notification per 30 seconds per
subscription; changes inside the window are dropped, not queued. Three
patches in a row produced one delivery and timesSent: 1. With several
pods on NATS the window is kept per pod.
Delivery bookkeeping
GET $U/subscriptions/{id} shows the fields of CIM 009 5.8.6. After the
first successful notification:
"notification": {
"endpoint": {"accept": "application/json", "uri": "http://localhost:9380/notify"},
"lastNotification": "2026-08-26T15:55:28.489Z",
"lastSuccess": "2026-08-26T15:55:28.489Z",
"status": "ok",
"timesSent": 1
}
A subscription pointing at a closed port after one matching change:
"notification": {
"endpoint": {"accept": "application/json", "uri": "http://localhost:9399/nobody"},
"lastNotification": "2026-08-26T15:55:37.662Z",
"lastFailure": "2026-08-26T15:55:37.663Z",
"status": "failed",
"timesFailed": 1,
"timesSent": 1
}
timesSent counts notifications, not attempts: a delivery that is retried
and then succeeds still adds one. status flips back to ok on the next
successful delivery.
Retry and dead letters
The default is one attempt per notification. ANTARES_NOTIFY_ATTEMPTS,
ANTARES_NOTIFY_BACKOFF_MS and ANTARES_NOTIFY_MAX_AGE_SECS turn on
retries with exponential backoff on a separate task; a notification whose
attempts or age run out becomes a dead letter, listed and replayable
through /q/dead-letters and counted on /q/health as deadLetters.
See Operations and the
Admin API.
Egress
Delivery goes through the same egress policy as @context fetches and
federation forwards: http, https, mqtt and mqtts only, redirects
capped, DNS pinned, response size capped, and a per-host breaker that
pauses a failing endpoint. Private and loopback destinations are allowed
by default; ANTARES_EGRESS_ALLOW_PRIVATE=false denies them for an
internet-facing deployment, and a refused delivery is booked as a failure.
A scheme no notification binding serves is refused when the subscription is
created — the endpoint is input data that does not meet the requirements of
the operation (5.8.1.4, Table 5.5.2-1), so the error is BadRequestData:
{"detail":"no notification binding registered for endpoint scheme \"ftp\" (6.3.8)","status":400,"title":"BadRequestData",...}
MQTT endpoints
An endpoint URI of the form mqtt[s]://[user[:pass]@]host[:port]/topic
delivers notifications as MQTT publishes (CIM 009 clause 7). The message
is {"metadata": {...}, "body": <Notification>}; protocol parameters go
in notifierInfo:
"endpoint": {
"uri": "mqtt://localhost:1883/antares/hot",
"accept": "application/json",
"notifierInfo": [{"key": "MQTT-Version", "value": "mqtt5.0"}, {"key": "MQTT-QoS", "value": "1"}]
}
The broker validates the URI and the parameters at creation (201 above,
with no MQTT broker running) and connects on the first delivery. Sessions
are pooled per endpoint and credentials; the password never appears in
error bodies or logs. The binding is the mqtt cargo feature, on by
default.