Subscriptions and notifications
A subscription tells Andromeda which changes you care about and where to send them. Each matching write produces one notification. You receive it at an endpoint over HTTP or MQTT.
Two things surprise most people at first. A subscription hears about creations and updates by default, not deletions. And data holds the whole entity after the write, not only the attributes that changed. The sections below explain both.
Creating a subscription
Send the subscription to POST /ngsi-ld/v1/subscriptions. Name what to watch in entities or watchedAttributes, or both, and name the endpoint in notification.
POST /ngsi-ld/v1/subscriptions
Content-Type: application/json
{
"id": "urn:ngsi-ld:Subscription:fast-vehicles",
"type": "Subscription",
"entities": [
{
"type": "Vehicle"
}
],
"watchedAttributes": [
"speed"
],
"q": "speed>90",
"notification": {
"endpoint": {
"uri": "https://example.org/hook",
"accept": "application/json"
}
}
}
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles
Leave out id and Andromeda mints one (see Identifiers). The @context you create the subscription under expands its attribute and type names. It also becomes the subscription's jsonldContext, unless you set one yourself. A name it can't expand gets 400 BadRequestData. Every member comes back exactly as you wrote it.
Andromeda checks the whole subscription when you write it, filters included. A malformed q or an incomplete geoQ is refused straight away, not when the first change arrives. Strictness lists every rule.
What triggers a notification
A write is owed to a subscription when it raises one of the subscription's triggers on an attribute the subscription watches. The six triggers are entityCreated, entityDeleted, attributeCreated, attributeUpdated, attributeDeleted and entityUpdated. The last one stands for the three attribute triggers together.
Without notificationTrigger, a subscription listens to attributeCreated and attributeUpdated, and reads back with that default. Deleting an attribute or an entity only notifies a subscription that asks for attributeDeleted, entityUpdated or entityDeleted.
Without watchedAttributes, every attribute counts. With it, only the named attributes can trigger a notification. A change to any other attribute of the same entity is ignored. watchedAttributes decides what triggers a notification, not what the notification contains.
One notification per write
A write sends each matching subscription one notification per entity, however many attributes it touched. Each of these cases notifies once.
- creating an entity with three attributes, or with several
datasetIdinstances of one attribute - a Merge Entity that updates one attribute and appends another
- a Replace Entity that updates, adds and removes attributes in one go
- a subscription listing both
entityCreatedandattributeCreated, when an entity is created
A batch notifies once for each entity it changes. A write that leaves an entity as it was, such as setting an attribute to the value it already holds, notifies nobody (see Writes).
Writes to one entity are matched against the subscriptions in the order they were committed. With durable delivery, their notifications are then sent side by side, up to notifications.delivery.max_concurrent at a time, and a failed one is retried later, so two notifications about one entity can arrive in either order. A receiver that keeps the latest state shouldn't rely on arrival order.
Filters
q, geoQ, scopeQ and csf are read against the entity as the write left it. A subscription with q=speed>90 hears about a vehicle that reached 95, and not about one that stayed at 80.
Two kinds of write are also read against the entity as it was before.
- A deleted entity is read as it stood before the deletion. A subscription with
q=state=="retired"and theentityDeletedtrigger hears when a retired entity is deleted, and not when an active one is. - A write that removed instances is also matched against the state before it, for a subscription that listens to the removal. That is a subscription with
attributeDeletedorentityUpdatedthat watches an attribute the write removed. A subscription filtering on a removed attribute still hears of its removal.
A Replace Entity removes every attribute it doesn't restate, but it is matched against the state before it only for such a subscription. With the default triggers, or with attributeCreated and attributeUpdated alone, a Replace is read against the entity as it left it. A subscription with q=state=="active" hears nothing when a Replace sets state to "retired".
Instances whose expiresAt has passed don't count. A geoQ that only an expired location would satisfy notifies nothing.
Rare cases
- A change is notified only if the subscription was active when the change was committed. A change made before the subscription existed, or while it was paused or expired, isn't notified, even if the broker is still processing it when the subscription is created or made active again.
- A change committed before an update that changes
entities,watchedAttributes,notificationTrigger,q,geoQ,scopeQ,csforlocalOnlyis judged by the selection that stood when it was committed, never by one written after it, even when the broker gets to it only after the update. The notification goes to the endpoint, in the format and with the settings the subscription has when it is sent. - With live delivery the broker keeps the replaced selection while changes committed before the update are still waiting to be processed, then drops it. A change that reaches the broker later isn't notified. That covers a write whose transaction started before the update but finished after the replaced selection was dropped, a change replayed after a restart, and one processed after the broker had to reload its subscriptions.
- With durable delivery the match is made when the change is committed, so an update doesn't affect it. The one exception is a
qorcsfthat follows a relationship into another entity. That part of the match needs the linked entity, so it is made when the notification is sent, and an update in between drops the notification. - Writes through the Temporal API notify when they change the current entity. Create Temporal Evolution of a new entity notifies like an entity creation, with the most recent value of each attribute. An appended observation newer than the current value notifies
attributeUpdated, and one older than it notifies nothing. See Temporal evolution. - Operations on the history alone notify nothing. That covers Modify Attribute Instance, Delete Attribute Instance, deleting an attribute from the temporal evolution, and Delete Temporal Evolution.
- A batch sent with
Prefer: respond-asyncis answered before its notifications are raised. A worker raises them afterwards, and keeps retrying if that fails, never dropping them. See Asynchronous history. idtakes precedence overidPatternin one entity selector. A selector holding both selects only the entity theidnames.- A subscription with
localOnlyset totruehears only about changes made on this broker, never about changes relayed from a context source.
What a notification holds
{
"id": "urn:ngsi-ld:Notification:5d3e1c0a-6f2b-4a8e-9c47-1b2d3e4f5a6b",
"type": "Notification",
"subscriptionId": "urn:ngsi-ld:Subscription:fast-vehicles",
"notifiedAt": "2026-10-24T09:15:02.481Z",
"data": [
{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"speed": {
"type": "Property",
"value": 95
}
}
]
}
data holds the entity the write changed, in full, as it stands after the write. If you only want some attributes, use notification.attributes, pick or omit. They narrow the entity to the members you name (see Linked entities).
notification.format picks the representation. It is normalized by default, or concise, or keyValues, whose synonym is simplified. notification.sysAttrs adds createdAt and modifiedAt, as the notified change left them. When several writes land before a notification goes out, each notification carries the modifiedAt of its own write, not of the latest one.
endpoint.accept picks the media type. It is application/json by default, or application/ld+json or application/geo+json, spelled exactly so. Any other value is refused with 400 BadRequestData. Where the @context goes for each of them is covered under Notifications in JSON-LD @context.
Deletions
A deleted attribute is notified with the NGSI-LD Null in place of its value.
"data": [
{
"id": "urn:ngsi-ld:Building:1",
"type": "Building",
"height": {
"type": "Property",
"value": 10
},
"colour": {
"type": "Property",
"value": "urn:ngsi-ld:null"
}
}
]
Each deleted instance is listed with its datasetId. A Relationship gets the Null as its object, a VocabProperty as its vocab, a JsonProperty as its json, a ListProperty or ListRelationship a list of one Null, and a LanguageProperty the languageMap {"@none": "urn:ngsi-ld:null"}.
In concise, a deleted Property or GeoProperty that carries nothing else is the bare Null. One that carries more, such as a datasetId, stays an object, and a GeoProperty then keeps "type": "GeoProperty", because {"value": "urn:ngsi-ld:null"} alone reads back as a Property.
In keyValues, a deleted instance with a datasetId is written in the dataset map, {"dataset": {"urn:ngsi-ld:Dataset:gps": "urn:ngsi-ld:null"}}, even when it is the only instance, so the datasetId isn't lost. The bare Null would say the whole attribute was deleted.
A deleted entity is notified with its id, its type and deletedAt, and none of its attributes. With sysAttrs, it also carries createdAt and modifiedAt. A notification about a deletion follows no Relationships, whatever join asks for.
Previous values
notification.showChanges adds what each changed attribute held before the write. The member matches the attribute's type, and is one of previousValue, previousObject, previousLanguageMap, previousJson, previousVocab, previousValueList or previousObjectList. A newly created entity or attribute has nothing before it, so it carries none of them.
showChanges is refused with keyValues and simplified, because those formats have nowhere to put a previous value.
Narrowing to instances and languages
datasetId keeps only the instances with the listed datasetIds. @none names the default instance.
lang turns each LanguageProperty into a Property holding the string in that language, with lang naming it. An entity with no string in that language gets its @none string instead, with lang set to @none.
Older versions of NGSI-LD
ngsildConformance names a version of the specification, such as "1.4". Every notification is then reshaped to what that version defines. A member or attribute type introduced later is dropped, or turned into its older form.
Under "1.0", for example, an entity keeps only its first type and loses scope and expiresAt. A LanguageProperty becomes a Property holding the language map. A ListRelationship becomes a Relationship whose object holds every target of the list in order (one target as a single URI, an empty list as null), and only the default instance of each attribute is kept. This applies to periodic notifications and to every format.
Throttling, cooldown and periodic notifications
throttling is the shortest time, in seconds, between two notifications of one subscription. A change inside that window gets no notification, and nothing is queued for later. Fractions of a second work. The window is measured from lastNotification.
endpoint.cooldown is the time, in milliseconds, to wait after a failed delivery. A change inside that window after the failure gets no notification either. The window is measured from lastFailure.
timeInterval makes the subscription periodic. Every timeInterval seconds it sends the current state of every entity it matches, whether anything changed or not. A periodic subscription isn't notified of changes at all.
-
The schedule starts when the subscription is created. The first notification goes out one interval later, and then one every interval.
-
A notification that goes out late doesn't move the ones after it.
-
Each notification goes out when its own interval is reached, however many other periodic subscriptions the tenant holds. An interval below one second, such as
0.5, is kept too. -
A slow endpoint delays only its own subscription's periodic notifications. A tenant sends up to 20 periodic notifications at once, for entity and registration subscriptions together, and a notification due beyond that waits only until one of them is answered.
-
While a subscription's notification still waits for its endpoint's answer, its next one isn't sent. It goes out as soon as the answer comes, and the schedule after it doesn't move.
-
A tenant whose periodic notification is due within
tenancy.residency.min_wake_intervalstays loaded while the broker unloads other tenants to make room. Only when every tenant it could unload has one does it unload the one used least recently. That tenant's next notification can then be late by up tomin_wake_interval, as long as the broker loads tenants as fast as their work falls due. -
When no entity matches, nothing is sent, and the counters don't move.
-
A periodic subscription needs
entities, and it takes neitherwatchedAttributesnorthrottling.
Delivery
The scheme of endpoint.uri decides how a notification travels.
HTTP
http and https send one POST per notification. The body is the notification, with the Content-Type that accept asks for. The POST also carries these headers.
- a
Linkheader naming the@context, forapplication/json NGSILD-Tenant, when the subscription belongs to a tenant other than the default one- every
receiverInfopair as a header, which is where a token or a routing key goes
A receiverInfo pair named Link joins the @context link on one line. The HTTP edge page explains how Link values are laid out.
Any 2xx answer is a success. A 5xx or a 429 is a failure worth retrying. Any other answer is a failure that a retry wouldn't change. Andromeda never follows a redirect, so a 3xx counts as a failure.
A notification larger than 10 MiB isn't sent, and counts as a failure. endpoint.timeout, in milliseconds, can shorten the time Andromeda waits for an answer. It can't lengthen the wait past notifications.delivery.request_timeout.
By default (notifications.delivery.private_addresses = "allowed") a subscription can send notifications to any host the broker can reach, internal services included. With private_addresses = "refused" the broker doesn't notify an HTTP endpoint or an MQTT broker in loopback, private or reserved address space. notifications.delivery.allowed_addresses exempts addresses and subnets from that, and notifications.delivery.denied_addresses refuses addresses and subnets whatever private_addresses says, public ones included. A denied address wins over an allowed one.
The check runs when a notification is sent, on the addresses the host name resolves to at that moment, not on the host name. A subscription whose endpoint points at a refused address is still created, and each of its deliveries fails and is recorded like any other failure. Security describes the same check.
MQTT
mqtt and mqtts publish one message to the MQTT broker and topic the URI names. The defaults are port 1883 for mqtt and 8883 for mqtts. A user name and password go in the URI.
Each message is a JSON object with two members. body is the notification, exactly as the HTTP POST would carry it. metadata holds what HTTP would put in headers, namely the Link, NGSILD-Tenant and every receiverInfo pair.
{
"metadata": {
"Link": "<https://example.org/context.jsonld>; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\""
},
"body": {
"id": "urn:ngsi-ld:Notification:5d3e1c0a-6f2b-4a8e-9c47-1b2d3e4f5a6b",
"type": "Notification",
"...": "..."
}
}
Under MQTT 5, the same metadata also travels as user properties, and the media type travels as the content type. Two notifierInfo pairs choose the protocol.
MQTT-Version:mqtt5.0(the default) ormqtt3.1.1.MQTT-QoS:0(the default),1or2.
At QoS 0, a notification counts as delivered once it is published. At QoS 1 and 2, it counts once the MQTT broker acknowledges it. Andromeda waits 30 seconds for that, or less if the endpoint's timeout is shorter.
A notification's whole PUBLISH packet must fit notifications.delivery.max_mqtt_packet. An MQTT 5 broker can announce a smaller Maximum Packet Size when the connection opens, and MQTT 5.0 forbids sending anything larger, so the smaller of the two applies to that connection. Mosquitto, for one, announces 2,000,000 bytes unless its max_packet_size says otherwise. When the announced maximum is the smaller one, Andromeda logs it at info with the endpoint and both figures as the connection opens.
A notification over the limit is never sent and isn't retried. The log line and the failure stored with a queued delivery name the limit and where it came from, for example notification packet of 8389548 bytes exceeds the maximum packet size of 2000000 bytes the MQTT server announced (notifications.delivery.max_mqtt_packet is 134217728 bytes). An HTTP notification over notifications.delivery.max_http_body is refused the same way, naming that key.
Retries
How a failed delivery is handled depends on notifications.delivery.mode, which the operator sets.
live(the default): the notification is sent once, from memory. A failure is recorded and nothing is retried.durable: every notification goes into a queue in the database, in the same transaction as the write that raised it. A failure worth retrying is retried with a growing wait, up tonotifications.delivery.max_attemptstimes. Every other failure is final at once.
Live delivery
Every committed change is evaluated. When evaluation falls more than notifications.evaluation.max_queued_changes changes behind, writes wait for it to catch up rather than skip a notification. While the broker runs, no committed change is skipped.
A broker that stops, and a tenant unloaded from the runtime cache, first evaluate every change already committed, including the ones writes are still waiting to hand over. That drain is bounded by notifications.evaluation.drain_timeout (default 30s, never below notifications.delivery.request_timeout). A stopping broker exits within server.graceful_drain_timeout plus that bound, 60 seconds by default, so give it the two together plus 10 seconds before your orchestrator kills it.
Changes still waiting when the bound passes are dropped, logged in one warning with their number and counted in andromeda_live_changes_abandoned_total. A tenant with changes waiting is unloaded to make room only after every tenant without any. A broker that crashes, or is killed before the drain ends, loses the changes it had not evaluated, so live is best effort across crashes. Use durable for a guarantee.
Durable delivery
The broker keeps notifications.delivery.max_concurrent notifications of each tenant in flight for as long as the queue holds notifications that are due, and starts the next one as soon as any of them is answered and its outcome is recorded. Outcomes that arrive together, or while the previous ones are being recorded, are recorded together, and the notifications that take their slots are fetched in the same database transaction.
The slots are shared by the tenant's subscriptions. Notifications are sent in the order they became due until one subscription holds half the slots. From then on, each slot that frees goes to the subscription with the fewest notifications in flight that has one due.
While another subscription has a notification in flight, or had one answered within request_timeout, a subscription holding half the slots gets no more, so an endpoint that answers slowly leaves the tenant's other subscriptions at least half the slots even when they are written in bursts. A subscription with no other active beside it uses every slot.
While an endpoint keeps failing, its subscription's queue waits behind one notification. The first failure worth retrying makes that notification the subscription's probe. The subscription's other queued notifications are set aside without using up any attempts, and notifications queued for it later wait with them.
Only the probe is retried, so an endpoint that never answers gets one attempt per retry step however many notifications it is owed, and the subscription's other notifications don't take up the max_concurrent slots the tenant's other subscriptions need. The first attempt the endpoint accepts sends the waiting notifications in the order they were queued, each once. A probe that runs out of attempts is dead-lettered on its own, and the oldest waiting notification becomes the next probe one retry_interval later. A failure that is final at once, such as a notification over max_http_body, doesn't hold the queue.
A queued notification is owed for the subscription as it stood when the write committed. Updating what the subscription selects before the notification is sent doesn't withdraw it, though it is sent with the endpoint and notification settings the subscription has by then. It is dropped, without counting as a failure, if the subscription is deleted, paused or expired before it is sent, or given a timeInterval.
A change is queued once for each subscription it matches, and a notification the endpoint accepted is not sent again. Two cases can still repeat one. Either the endpoint received it but its answer didn't arrive within the timeout, so the attempt counts as failed and is retried. Or the broker stopped, or couldn't reach its database for the whole lease, before it recorded the notification as sent.
A repeat is another attempt of the same notification and carries the same notification id, so an endpoint that has already accepted an id can answer 2xx and drop the repeat. Answering with an error turns a harmless repeat into a failure that can dead-letter the notification. notifiedAt changes on every attempt, and when the subscription was updated in between, the format, attrs or endpoint can change too, so two attempts sharing an id can differ in body.
The id of a notification is random in live delivery, where each notification is sent once. In durable delivery it is stable per queued notification. It is a name-based (version 5) UUID, written urn:ngsi-ld:Notification:<uuid>, derived from the write that made the change, the change's position within that write and the id of the subscription the notification is owed to. Two subscriptions matching one change get different ids, and so do two changes of one write for the same subscription.
Either mode
Either way, every failure moves lastFailure and sets status to failed. Notifications in the configuration reference has the settings.
Delivery status
After its first notification, a subscription reports how delivery is going.
timesSentandtimesFailedcount notifications sent and failures.lastNotification,lastSuccessandlastFailureare the times of the latest of each.notification.statusisokafter a success andfailedafter a failure.
The counters move once the endpoint has answered. Notifications to one subscription that are answered at about the same time are counted together in one update of its counters, so the counters can trail the endpoint by the time of one or two database writes.
A subscription that has notified nothing reports none of these, not even a zero. Values you send for them are ignored, whatever they hold. Any other member a subscription body does not define is refused with 400 BadRequestData, not ignored (see Strictness). A subscription deleted and created again under the same id starts with none.
Pausing, expiring and updating
A subscription's status is active, paused or expired. Only an active one notifies.
isActiveset tofalsepauses it, andtruemakes it active again. Changes made while it was paused are never notified, not even once it's active again.- Once its
expiresAtpasses, it reads back asexpired. ItsisActivedoesn't change that. - Writing a new
expiresAtin the future makes an expired subscription active again. It is paused instead if you also setisActivetofalse. - Removing
expiresAtwith the NGSI-LD Null keeps the subscription for good. It doesn't wake an expired one.
PATCH /ngsi-ld/v1/subscriptions/{id} changes only the members you send. To remove a member, write it as the NGSI-LD Null. The rules that tie members together, such as timeInterval excluding throttling, are checked against the subscription as it stands after the update. An update read under a different @context uses that context to expand the fragment, but the subscription keeps its jsonldContext.
GET /ngsi-ld/v1/subscriptions lists every subscription, in identifier order and in pages (see Pagination).
Subscriptions that reach context sources
A subscription can also hear about entities a registered context source holds. Andromeda places a copy of the subscription at each source whose registration matches it, and offers the subscription operations there. Each copy names the broker as its endpoint. Andromeda relays every notification a copy sends to your endpoint, under your subscription's id.
This happens only when two things are true. localOnly isn't true, and the operator has set server.public_url, the address the sources send their notifications back to.
What you get matches a local change. Your attributes, pick and omit, format, lang, ngsildConformance and join are applied by this broker when it relays the notification, not by the source.
showChanges is the exception, because only the source knows what an attribute held before it changed there. The copy asks the source for previous values, and Andromeda relays the ones it sends. When entities are split, the attributes Andromeda adds from its own data and from the other sources carry no previous value.
A source reports a deleted attribute in one of two ways, and Andromeda relays each the way it arrived. The bare Null in place of the attribute, "owner": "urn:ngsi-ld:null", means the whole attribute is gone, and it stays the bare Null in every format. JSON-LD reads ["urn:ngsi-ld:null"] and {"@id": "urn:ngsi-ld:null"} as the same Null, so they are relayed as the bare Null too.
The object form, with the Null in the attribute's value member, is what a source sends when it also reports a datasetId, system attributes or a previous value. It deletes that one instance and is relayed under the rules of Deletions. A deletion spelled in another family's member, such as "value": "urn:ngsi-ld:null" on a VocabProperty, or a languageMap that is the bare Null, is no deletion. The entity carrying it is dropped from the relayed notification.
A copy watches only the entities and attributes its registration declares. It carries your q, geoQ and scopeQ, unless entities are split across sources. When they are split, Andromeda completes each relayed entity from its own data and from the other sources, applies the filters itself, and drops the entities that fail them. Distributed operations explains split entities.
Updating your subscription updates every copy, and deleting it deletes every copy. When it expires, every copy is deleted too, so the sources stop matching and notifying for it. Writing a new expiresAt in the future places the copies again, even when the update arrives while the expired copies are still being deleted. A source whose contextSourceAlias is already listed in the request's Via header gets no copy. That stops a subscription from going round in circles between brokers registered with each other.
Subscribing to registrations
/ngsi-ld/v1/csourceSubscriptions takes subscriptions to context source registrations. They work like the ones above, with four differences.
- Each notification is a
ContextSourceNotification. ItstriggerReasonisnewlyMatching,updatedornoLongerMatching, and itsdataholds the registrations. - A new registration subscription is notified at once of the registrations it already matches. A periodic one is notified at once even when nothing matches, with an empty
data. endpoint.acceptisapplication/jsonorapplication/ld+json.application/geo+jsonis refused with400BadRequestData.temporalQandcsfselect registrations. Without atemporalQ, only registrations that declare no time interval match (see Registrations and time).
What gets refused
Subscriptions in Strictness lists what a subscription must satisfy. These come up most often.
400BadRequestDatafor neitherentitiesnorwatchedAttributes. One of the two is required, unlesslocalOnlyistrue, and a periodic subscription needsentities.400BadRequestDataforshowChangeswithkeyValuesorsimplified. Usenormalizedorconciseinstead.400BadRequestDatafortimeIntervalbesidethrottlingorwatchedAttributes. An update that would leave them side by side is refused too.400BadRequestDatafor anexpiresAtin the past. Write a time in the future, or leaveexpiresAtout.504LdContextNotAvailablefor ajsonldContextAndromeda can't fetch. It is fetched when you write the subscription, so a broken URL fails at once.
Configuration
notifications.delivery.mode(default"live"):"live"sends each notification once, from memory."durable"queues it and retries it.notifications.delivery.request_timeout(default"10s"): the longest a delivery may take. An endpoint'stimeoutcan only shorten it.notifications.evaluation.drain_timeout(default"30s"): the longest a stopping broker, or a tenant being unloaded, spends evaluating the changes it already holds underlivedelivery. At leastnotifications.delivery.request_timeout.notifications.delivery.private_addresses(default"allowed"),allowed_addressesanddenied_addresses: which hosts a notification may be sent to (see HTTP).server.public_url(not set by default): needed for subscriptions to be copied to context sources.
The Notifications page has the queue and retry settings.
Where Andromeda interprets the specification
Subscriptions are defined by the specification, with the data type, the notification parameters and the endpoint described separately.
The default triggers are set, and entityUpdated stands for the three attribute triggers. Watched attributes are "those that trigger a new notification when they are changed". Neither says how many notifications one write raises, so Andromeda sends one per entity per write, carrying every change.
A notification is owed to subscribed entities "that changed and that match" the query conditions. A deleted entity exists only in its previous state, so Andromeda reads the filters against that state. Expired instances don't count.
cooldown is defined with the rule that "if requests are received before the cooldown period has expired, no notification is sent". Andromeda gives throttling the same reading. It is defined as the "minimal period of time in seconds which shall elapse between two consecutive notifications", and a notification held back would break that.
A success is counted on "200 OK". Andromeda counts any 2xx, since 204 No Content is the usual answer of an endpoint that has nothing to return. The timeout rule says "the NGSI-LD system can override this value".
The MQTT envelope is the MQTT binding, which also defines MQTT-Version and MQTT-QoS.
temporalQ is limited to registration subscriptions, and endpoint.accept is closed to its three media types, of which a registration subscription is offered only the first two.
The specification doesn't order notifications, so durable delivery sends them concurrently. A removed attribute exists only in the state before the write, so a subscription the removal is owed to also reads its filters there. For any other subscription the removal is no news, and only the state after the write counts.
The NGSI-LD Null is used "in all places where URI strings are valid JSON values", which covers vocab. A JsonProperty json is "a raw JSON object (or array of objects)", so the string urn:ngsi-ld:null there is never data, and Andromeda reads it as the deletion too, and writes the deletion of those two families that way in its own notifications. A datasetId in a keyValues attribute appears only in the dataset map, and a deleted attribute is reported with its datasetId, so a deleted instance of a datasetId takes the map form even when it stands alone.
When a subscription is copied to context sources, only attributes, pick and omit are removed from the copy's notification, so showChanges stays on the copy, and the received notification is "copied", previous values included. When the source reports a deleted attribute as an object, the deletion takes the place of the instance with the same datasetId that the other sources hold. Their other instances stay.