Subscriptions
A subscription asks Andromeda to tell you when entities change. You name what to watch and where to send the message, and every matching write sends one notification. This guide creates a subscription, watches a notification arrive, shapes what the notification holds, and manages the subscription through its life. The exact rules behind every step are on Subscriptions and notifications.
Where notifications go
Notifications need somewhere to go. The endpoint in these examples is http://127.0.0.1:8085/notify, and the Quickstart has a one-line receiver that prints every notification and answers 204. Run it in a second terminal before you create the subscription.
Creating a subscription
Send the subscription to POST /ngsi-ld/v1/subscriptions. Name what to watch in entities or watchedAttributes, name the endpoint in notification, and narrow it with a query if you only care about some of the changes.
- http
- curl
- rust
- python
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": "http://127.0.0.1:8085/notify",
"accept": "application/json"
}
}
}
curl -i http://localhost:1056/ngsi-ld/v1/subscriptions \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:Subscription:fast-vehicles",
"type": "Subscription",
"entities": [
{
"type": "Vehicle"
}
],
"watchedAttributes": [
"speed"
],
"q": "speed>90",
"notification": {
"endpoint": {
"uri": "http://127.0.0.1:8085/notify",
"accept": "application/json"
}
}
}'
let endpoint = Endpoint::builder("http://127.0.0.1:8085/notify").build()?;
let subscription = Subscription::builder()
.id(subscription_id)
.entities(["Vehicle"])
.watched_attributes(["speed"])
.q(q::attr("speed").gt(90).build()?)
.notification(NotificationParams::builder(endpoint).build()?)
.build()?;
client.subscriptions().create(&subscription).send().await?;
response = requests.post(
f"{BASE}/subscriptions",
json={
"id": "urn:ngsi-ld:Subscription:fast-vehicles",
"type": "Subscription",
"entities": [
{
"type": "Vehicle",
}
],
"watchedAttributes": ["speed"],
"q": "speed>90",
"notification": {
"endpoint": {
"uri": "http://127.0.0.1:8085/notify",
"accept": "application/json",
}
},
},
)
assert response.status_code == 201
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles
Leave the id out and Andromeda mints one, naming it in the Location header. The @context you create the subscription under expands its attribute and type names, and it becomes the subscription's jsonldContext unless you set one yourself.
Andromeda checks the whole subscription when you write it, filters included. A malformed q is refused with 400 now, not when the first change arrives.
Seeing a notification arrive
Push a vehicle over the limit.
- http
- curl
- rust
- python
PATCH /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs
Content-Type: application/json
{
"speed": {
"type": "Property",
"value": 95
}
}
curl -i -X PATCH http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs \
-H 'Content-Type: application/json' \
-d '{
"speed": {
"type": "Property",
"value": 95
}
}'
let fragment = EntityFragment::builder()
.property("speed", 95)
.build()?;
let outcome = client
.entities()
.update_attributes(&id, &fragment)
.send()
.await?;
response = requests.patch(
VEHICLE + "/attrs",
json={
"speed": {
"type": "Property",
"value": 95,
}
},
)
assert response.status_code == 204
The write answers 204, and the receiver prints the notification.
{
"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 whole entity as it stands after the write, not only the attributes that changed. The notification was sent because a subscription listens for created and updated attributes by default, and speed was just updated. A write that changed five attributes would still have sent one notification.
Two defaults trip people up. A subscription hears about creations and updates, but not deletions. Deleting an attribute or an entity only notifies a subscription that asks for the attributeDeleted, entityUpdated or entityDeleted trigger. And watchedAttributes decides what can trigger a notification, not what the notification holds. Both are covered under What triggers a notification.
Shaping the notification
Everything inside notification besides endpoint shapes what the notification holds.
{
"notification": {
"endpoint": {
"uri": "http://127.0.0.1:8085/notify"
},
"attributes": [
"speed"
],
"format": "keyValues",
"showChanges": true,
"sysAttrs": true
}
}
attributesnarrowsdatato the members you name.pickandomitdo the same job from the other direction, and both can follow relationships into linked entities.formatpicks the representation,normalizedby default, orconcise, orkeyValues(whose synonym issimplified).showChangesadds what each changed attribute held before the write, in a member such aspreviousValue. It needsnormalizedorconcise; the key-value formats have nowhere to put a previous value.sysAttrsaddscreatedAtandmodifiedAt.
A deletion is notified like any other change, with the NGSI-LD Null in place of the value the attribute held. A deleted entity is notified with its id, its type and deletedAt, and none of its attributes.
What a notification holds has the full picture, including datasetId narrowing, the lang filter and ngsildConformance for clients that speak older versions of NGSI-LD.
Pacing the notifications
throttling is the shortest time, in seconds, between two notifications of one subscription. A change inside the window gets no notification, and nothing is queued for later.
{
"throttling": 30
}
timeInterval makes the subscription periodic instead. Every timeInterval seconds it sends the current state of every entity it matches, whether anything changed or not.
{
"timeInterval": 60,
"entities": [
{
"type": "Vehicle"
}
]
}
A periodic subscription isn't notified of changes at all, and the two members can't sit side by side. A periodic one needs entities, and it takes neither watchedAttributes nor throttling. The schedule starts when the subscription is created; the first notification goes out one interval later.
Throttling, cooldown and periodic notifications has the rest, including how a failed delivery sets its own cooldown.
Reading subscriptions back
GET /ngsi-ld/v1/subscriptions lists them, in identifier order and in pages.
- http
- curl
- rust
- python
GET /ngsi-ld/v1/subscriptions?limit=5
curl -i 'http://localhost:1056/ngsi-ld/v1/subscriptions?limit=5'
let first = client.subscriptions().query().limit(5).send().await?;
let every = client.pages(first).try_collect().await?;
println!("the broker holds {} subscriptions", every.len());
response = requests.get(f"{BASE}/subscriptions", params={"limit": 5})
assert response.status_code == 200
A limit above 1,000 is refused with 403 TooManyResults. Pagination explains how to walk the rest of the list.
After its first notification, a subscription reports how delivery is going:
{
"id": "urn:ngsi-ld:Subscription:fast-vehicles",
"notification": {
"status": "ok"
},
"timesSent": 3,
"timesFailed": 0,
"lastNotification": "2026-10-24T09:15:02.481Z",
"lastSuccess": "2026-10-24T09:15:02.481Z"
}
timesSent and timesFailed count notifications sent and failures, lastNotification, lastSuccess and lastFailure time the latest of each, and notification.status is ok or failed. A subscription that has notified nothing reports none of these, not even a zero, and values you send for them are ignored.
Changing and deleting
PATCH /ngsi-ld/v1/subscriptions/{id} changes only the members you send. This fragment pauses the subscription and narrows its query in one write.
- http
- curl
- rust
- python
PATCH /ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles
Content-Type: application/json
{
"isActive": false,
"q": "speed>110"
}
curl -i -X PATCH http://localhost:1056/ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles \
-H 'Content-Type: application/json' \
-d '{
"isActive": false,
"q": "speed>110"
}'
let pause = SubscriptionFragment::builder()
.activation(Activation::Paused)
.q(q::attr("speed").gt(110).build()?)
.build()?;
client.subscriptions().update(&subscription_id, &pause).send().await?;
response = requests.patch(
f"{BASE}/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles",
json={
"isActive": False,
"q": "speed>110",
},
)
assert response.status_code == 204
The answer is 204. To remove a member, write it as the NGSI-LD Null, urn: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, so an update that would leave them side by side is refused.
Delete with the identifier from the Location header.
- http
- curl
- rust
- python
DELETE /ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles
curl -i -X DELETE http://localhost:1056/ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles
client.subscriptions().delete(&subscription_id).send().await?;
response = requests.delete(f"{BASE}/subscriptions/urn:ngsi-ld:Subscription:fast-vehicles")
assert response.status_code == 204
It answers 204, and stops notifying at once. A subscription deleted and created again under the same id starts with no delivery history.
Other ways to receive
The endpoint's scheme decides how a notification travels. http and https send one POST per notification, and every 2xx answer counts as a success. mqtt and mqtts publish one message to an MQTT broker and topic, with the headers an HTTP delivery would carry folded into a metadata member beside the body. How failures are retried depends on notifications.delivery.mode, which the operator sets. "live" sends once from memory, and "durable" queues and retries.
Delivery has the HTTP headers, the MQTT envelope and the retry modes.
What gets refused
The subscription is checked as a whole when you write it. The refusals that come up most are as follows.
400BadRequestDatafor neitherentitiesnorwatchedAttributes. One of the two is required, unlesslocalOnlyistrue.400BadRequestDataforshowChangesbesidekeyValuesorsimplified. Usenormalizedorconcise.400BadRequestDatafortimeIntervalbesidethrottlingorwatchedAttributes. An update that would leave them side by side is refused too.400BadRequestDatafor anexpiresAtin the past.504LdContextNotAvailablefor ajsonldContextAndromeda can't fetch. It is fetched when you write the subscription, so a broken URL fails at once.
What gets refused has the list, and Subscriptions in Strictness has every rule.
Where next
A subscription can also hear about entities a registered context source holds, and about the registrations themselves. Source subscriptions covers the second; the federation guide explains how entities reach this broker from elsewhere in the first place.