How an IoT device gets onto a Sutraatman CSE and starts sending data. Four steps, in this order:
- Register the device — create an
<AE>(application entity). This is the device's account on the CSE. - Create a container — create a
<container>under it. This is where one kind of reading is stored. - Send data — create a
<contentInstance>in that container. One reading, one instance. - Subscribe — create a
<subscription>so the CSE pushes new readings to whoever needs them.
Every example below is a real request against a running Sutraatman 1.0.0, with the response the CSE actually sent. HTTP is shown first and in full; the same four steps over MQTT, WebSocket and CoAP are at the end.
1. Before you start
You need three things from whoever runs the platform:
| What | Example | Where it comes from |
|---|---|---|
| the address of the CSE | http://10.0.0.5:8080 |
the infrastructure node, or a middle node directly |
| the resource path to register under | /~/plant1-mncse/plant1-mn |
the middle node the device belongs to |
| a credential, if the deployment uses them | device-secret-9f2c |
the administrator, per device (see Credentials) |
Where to address the request
Every request is sent to the address of the parent resource. Three spellings reach the same place:
| Form | Example | Use it when |
|---|---|---|
| through the infrastructure node | http://<in>:8080/~/plant1-mncse/plant1-mn |
the usual case: devices only need to know the INCSE |
| straight to the middle node | http://<mn>:8101/~/plant1-mncse/plant1-mn |
the device is on the same site as the middle node |
| on the infrastructure node itself | http://<in>:8080/~/Sutraatman/cdac_incse |
devices that register centrally, with no middle node |
/~/<CSE-ID>/<resourceName>/… is the SP-relative form and is what the rest of this guide uses. The INCSE forwards anything addressed to a registered middle node, so a device can use one address for its whole life even if the middle node moves to another host.
A fresh installation already has mumbai-1 and mumbai-2 with five units each, so
/~/mumbai-1-mncse/mumbai-1-mn works out of the box for trying this out.
2. The request envelope
Headers the device sends
| Header | Required | What it is |
|---|---|---|
X-M2M-Origin |
yes, except on the very first registration | the originator: the device's AE-ID, e.g. CTempSensor07 |
X-M2M-RI |
yes | request identifier, unique per request; it comes back on the response so you can match them |
X-M2M-RVI |
yes | release version, 2 |
Content-Type |
on create | application/json;ty=<resource type> — the ty is what makes it a create |
Accept |
optional | application/json (JSON is the only serialisation here) |
X-Sutraatman-Key |
only when the device has a credential | the device's key, or the administrator's key for CAdmin |
ty values used in this guide: 2 AE, 3 container, 4 contentInstance, 23 subscription. Without a ty parameter a POST is read as a notification, not a create, and comes back 5001 notify target not supported — this is the single most common mistake.
Headers the CSE sends back
| Header | What it is |
|---|---|
X-M2M-RSC |
the oneM2M response status code — this is the real result, e.g. 2001 created |
X-M2M-RI |
the request identifier you sent |
X-M2M-RVI |
2 |
Content-Location |
address of the resource that was created |
Content-Type |
application/json when there is a body |
The HTTP status code is a mapping of X-M2M-RSC (TS-0009), so 201, 403, 409 and so on are a summary.
Always read X-M2M-RSC: several different oneM2M codes map onto the same HTTP status. HTTP header names are case-insensitive, so a client may see them as x-m2m-rsc.
3. Step 1 — register the device
An <AE> is created under the CSEBase. Two attributes are mandatory:
| Attribute | Meaning | Rule |
|---|---|---|
api |
App-ID, which kind of application this is | must start with R (registered) or N (non-registered) |
rr |
requestReachability: can the CSE send requests to this device? | true or false, no default |
Useful optional ones: rn (resource name, how it appears in the tree; A-Z a-z 0-9 _ - . @, up to 128 characters, and not la, ol, fopt), poa (points of access where the CSE can reach the device — a list, even for one entry),
lbl (labels; the management console groups units by a product:<name> label).
Who chooses the AE-ID
| You send | You get |
|---|---|
no X-M2M-Origin (or just C) |
the CSE assigns one, e.g. Cmv415hr1mlfwxs9y |
X-M2M-Origin: CTempSensor07 |
that is your AE-ID, if nothing else has taken it |
X-M2M-Origin: CAdmin + admin key, with "aei" in the body |
the AE-ID you put in aei (administrator only) |
Read the aei out of the response and use it as X-M2M-Origin for everything afterwards.
The request
POST /~/mumbai-1-mncse/mumbai-1-mn HTTP/1.1
Host: 10.0.0.5:8080
X-M2M-Origin: CTempSensor07
X-M2M-RI: req-0002
X-M2M-RVI: 2
Content-Type: application/json;ty=2
Accept: application/json
{"m2m:ae":{"rn":"TempSensor07","api":"Nsensor.acme.com","rr":false,
"poa":["http://10.0.0.21:8000/"],
"lbl":["product:WaterPump","site:plant-1"]}}
The response
HTTP/1.1 201 Created
X-M2M-RSC: 2001
X-M2M-RI: req-0002
X-M2M-RVI: 2
Content-Location: /mumbai-1-mn/TempSensor07
Content-Type: application/json
{"m2m:ae":{"api":"Nsensor.acme.com","rr":false,"poa":["http://10.0.0.21:8000/"],
"lbl":["product:WaterPump","site:plant-1"],"aei":"CTempSensor07",
"ri":"mv415hu4lrogrj1b","rn":"TempSensor07","ty":2,"pi":"mumbai-1-mncse",
"ct":"20261011T162103","lt":"20261011T162103","acpi":["mv415hu4jwjj2enz"]}}
The CSE also created an access control policy (acpi) that lets this AE and the administrator do everything with this AE's subtree, and nobody else. Registration is logged on the node and the device appears in the console.
When it goes wrong
X-M2M-RSC |
HTTP | m2m:dbg |
Cause |
|---|---|---|---|
| 4000 | 400 | api (App-ID) is mandatory and must start with 'R' or 'N' |
api missing or malformed |
| 4000 | 400 | rr (requestReachability) is mandatory for AE |
rr missing |
| 4105 | 409 | AE-ID already registered: CTempSensor07 |
that AE-ID is taken — reuse it instead of registering again |
| 4103 | 403 | AE credential mismatch |
the AE has a key and it was wrong or missing |
| 6100 | 403 | capacity | the node is at its IoT-unit limit (1500 by default) |
A device that restarts does not register again: it keeps its AE-ID and carries on at step 3. 4105 on start-up is the normal sign that it is already registered.
Credentials
By default any originator may claim any unused AE-ID, which is fine on a closed network. To tie an AE-ID to a secret, the administrator sets x_key on the AE:
curl -X POST 'http://10.0.0.5:8080/~/mumbai-1-mncse/mumbai-1-mn'
-H 'X-M2M-Origin: CAdmin' -H 'X-Sutraatman-Key: <admin key>'
-H 'X-M2M-RI: 1' -H 'X-M2M-RVI: 2' -H 'Content-Type: application/json;ty=2'
-d '{"m2m:ae":{"rn":"LockedSensor","aei":"CLockedSensor","api":"Nsensor.acme.com",
"rr":false,"x_key":"device-secret-9f2c"}}'
From then on every request from CLockedSensor must carry X-Sutraatman-Key: device-secret-9f2c, on every path — straight to the middle node or forwarded through the infrastructure node — or it is refused with 4103 AE credential mismatch. The key is never returned by a read. It can be changed later with an UPDATE of the AE and cleared by setting it to "".
4. Step 2 — create a container
A <container> holds one series of readings. Create one per measurement stream (telemetry, alarms, status).
| Attribute | Meaning | Default |
|---|---|---|
rn |
resource name | generated if absent |
mni |
maxNrOfInstances — how many readings to keep | 1000 |
mbs |
maxByteSize — bytes of content to keep; a reading bigger than this is refused | unlimited |
mia |
maxInstanceAge in seconds | the CSE's retention default (30 days) |
lbl |
labels | — |
mni/mbs/mia must be non-negative integers. The container is a ring buffer: when it is full the oldest instances are deleted automatically, so a device never has to clean up after itself.
POST /~/mumbai-1-mncse/mumbai-1-mn/TempSensor07 HTTP/1.1
X-M2M-Origin: CTempSensor07
X-M2M-RI: req-0006
X-M2M-RVI: 2
Content-Type: application/json;ty=3
{"m2m:cnt":{"rn":"telemetry","mni":500,"mbs":65536,"lbl":["unit:degC"]}}
HTTP/1.1 201 Created
X-M2M-RSC: 2001
Content-Location: /mumbai-1-mn/TempSensor07/telemetry
{"m2m:cnt":{"mni":500,"mbs":65536,"lbl":["unit:degC"],"cni":0,"cbs":0,"st":0,
"ri":"mv415hxzzihb5l41","rn":"telemetry","ty":3,"pi":"mv415hu4lrogrj1b",
"ct":"20261011T162103","lt":"20261011T162103"}}
cni (how many instances it holds), cbs (how many bytes) and st (state tag, bumped on every change) are maintained by the CSE — do not send them.
X-M2M-RSC |
Cause |
|---|---|
4000 mni must be a non-negative integer |
a negative or non-numeric mni/mbs/mia |
4105 resource already exists |
that rn is taken under this parent |
| 4103 | the originator may not create below this resource |
5. Step 3 — send a reading
One <contentInstance> is one reading. They are immutable: create them, never update them.
| Attribute | Meaning |
|---|---|
con |
mandatory — the content. A string (JSON encoded as a string, CSV, a number) or a JSON object |
cnf |
contentInfo, <media type>:<encoding>; defaults to text/plain:0. Use application/json:0 for JSON |
The CSE fills in cs (content size in bytes), et (expiry time, from the container's mia or the node default),
st and the generated rn.
POST /~/mumbai-1-mncse/mumbai-1-mn/TempSensor07/telemetry HTTP/1.1
X-M2M-Origin: CTempSensor07
X-M2M-RI: req-0008
X-M2M-RVI: 2
Content-Type: application/json;ty=4
{"m2m:cin":{"cnf":"application/json:0","con":"{"temperature":31.4,"flow":62}"}}
HTTP/1.1 201 Created
X-M2M-RSC: 2001
Content-Location: /mumbai-1-mn/TempSensor07/telemetry/cin_mv415hztrd3vq1co
{"m2m:cin":{"cnf":"application/json:0","con":"{"temperature":31.4,"flow":62}",
"cs":30,"et":"20261110T162103","ri":"mv415hztrd3vq1co","rn":"cin_mv415hztrd3vq1co",
"ty":4,"pi":"mv415hxzzihb5l41","ct":"20261011T162103","lt":"20261011T162103","st":1}}
This is the request a device repeats for the rest of its life. It is also what makes the device count as active in the console: a registered device with no readings shows as inactive.
Reading data back
| Address | Returns |
|---|---|
…/telemetry/la |
the latest instance |
…/telemetry/ol |
the oldest one still kept |
…/telemetry |
the container, with cni and cbs |
…/telemetry?rcn=4 |
the container together with its instances |
GET /~/mumbai-1-mncse/mumbai-1-mn/TempSensor07/telemetry/la HTTP/1.1
X-M2M-Origin: CTempSensor07
X-M2M-RI: req-0010
X-M2M-RVI: 2
X-M2M-RSC |
Cause |
|---|---|
4000 con is mandatory for contentInstance |
no con |
| 5207 | the content is larger than the container's mbs |
| 4108 | the target is not a container — a contentInstance can only be a child of a container |
| 4004 | the container does not exist (check the path and the AE's rn) |
6. Step 4 — subscribe
A <subscription> under a resource tells the CSE to push a notification whenever something happens to it.
| Attribute | Meaning |
|---|---|
nu |
mandatory — notificationURI, a list of targets (see below) |
enc.net |
which events; default [1]. 3 is the one you want for new readings |
nct |
notification content: 1 the whole resource (default), 3 only its identifier |
rl |
rate limit {"mnn":<max notifications>,"tww":<seconds>} |
bn |
batch {"num":<count>,"dur":<seconds>} — send them in groups |
exc |
expiration counter: delete the subscription after this many notifications |
Event types (enc.net): 1 the resource itself was updated · 2 it was deleted · 3 a child was created (a new reading) · 4 a child was deleted.
Subscribe to the container, not to a contentInstance: instances are immutable and not subscribable, and
…/la is a virtual resource that refuses creates (4005).
What nu may contain
| Value | Where the notification goes |
|---|---|
http://host:port/path |
an HTTP POST to that URL |
coap://host:port/path |
a confirmable CoAP POST |
an AE-ID, e.g. CTempSensor07 |
the AE's poa; or, if it has none, the MQTT or WebSocket channel that AE has open on the CSE that hosts the resource |
That last rule matters: the subscription is served by the CSE the container lives on. If the container is on
mumbai-1 and your device's MQTT connection is to the infrastructure node, the hosting node cannot reach it. Either give the AE an HTTP poa, or connect to the middle node's own MQTT/WebSocket port. When a target resolves to nothing the CSE writes a warning to its log (source notification) and discards the notification, so check the node's log on the console's Log management page if a subscription looks silent.
Creating it
POST /~/mumbai-1-mncse/mumbai-1-mn/TempSensor07/telemetry HTTP/1.1
X-M2M-Origin: CTempSensor07
X-M2M-RI: req-0012
X-M2M-RVI: 2
Content-Type: application/json;ty=23
{"m2m:sub":{"rn":"to-analytics","nu":["http://127.0.0.1:7000/notify"],
"nct":1,"enc":{"net":[3]}}}
HTTP/1.1 201 Created
X-M2M-RSC: 2001
Content-Location: /mumbai-1-mn/TempSensor07/telemetry/to-analytics
{"m2m:sub":{"nu":["http://127.0.0.1:7000/notify"],"nct":1,"enc":{"net":[3]},
"cr":"CTempSensor07","ri":"mv415i4ejj68qyn9","rn":"to-analytics","ty":23,
"pi":"mv415hxzzihb5l41","ct":"20261011T162103","lt":"20261011T162103"}}
The verification request
Before the subscription is created the CSE posts a verification request to every target and refuses the whole subscription if one does not answer. The receiver must be listening before you subscribe. It is skipped when
nu is the subscriber's own AE-ID.
POST /notify HTTP/1.1
X-M2M-Origin: /mumbai-1-mncse
X-M2M-RI: mv415i48j1nqgxbi
X-M2M-RVI: 2
Content-Type: application/json
{"m2m:sgn":{"vrq":true,"sur":"mumbai-1-mn/TempSensor07/telemetry/to-analytics",
"cr":"CTempSensor07"}}
Answer 200 with X-M2M-RSC: 2000. Anything else and the subscription is refused with
5204 verification failed for <url>.
The notification
Every new reading then arrives like this:
POST /notify HTTP/1.1
X-M2M-Origin: /mumbai-1-mncse
X-M2M-RI: mv415i7odl3rfvyh
X-M2M-RVI: 2
Content-Type: application/json
{"m2m:sgn":{"nev":{"net":3,
"rep":{"m2m:cin":{"cnf":"application/json:0","con":"{"temperature":32.1,"flow":58}",
"cs":30,"et":"20261110T162103","ri":"mv415i6zfn71rzwh",
"rn":"cin_mv415i6zfn71rzwh","ty":4,"pi":"mv415hxzzihb5l41",
"ct":"20261011T162103","lt":"20261011T162103","st":2}}},
"sur":"mumbai-1-mn/TempSensor07/telemetry/to-analytics","cr":"CTempSensor07"}}
nev.net is the event, nev.rep the resource (with nct:3, only {"m2m:uri":"<id>"}), sur the subscription it came from. Deleting the subscription sends a last notification with "sud":true. Notifications that are not answered go to a retry queue.
X-M2M-RSC |
Cause |
|---|---|
4000 nu (notificationURI) is mandatory for subscription |
nu missing or empty |
4000 invalid notificationEventType |
an enc.net value outside 1–8 |
| 5204 | the target did not answer the verification request |
| 5203 | the parent is not subscribable (a contentInstance, another subscription) |
| 4005 | you aimed at la/ol — subscribe to the container instead |
7. The same four steps on the other bindings
MQTT, WebSocket and CoAP carry the same primitive, so only the envelope changes. In JSON: op (1 create, 2 retrieve, 3 update, 4 delete, 5 notify), to, fr (originator), rqi (request id), rvi, ty, pc (the body). A credential goes in sutraatmanKey instead of a header. These bindings are off unless the node enables them.
MQTT
Topics: the device sends on /oneM2M/req/<AE-ID>/<CSE-ID>/json and reads its answers on
/oneM2M/resp/<AE-ID>/<CSE-ID>/json. Registration may use reg_req / reg_resp in place of req / resp.
fr must equal the originator in the topic.
{"op":1,"to":"/~/Sutraatman/cdac_incse","fr":"CMqttSensor02","rqi":"m1","rvi":"2","ty":2,
"pc":{"m2m:ae":{"rn":"MqttSensor02","api":"Nsensor.acme.com","rr":true}}}
{"rsc":2001,"rqi":"m1","rvi":"2","fr":"/Sutraatman",
"pc":{"m2m:ae":{"api":"Nsensor.acme.com","rr":true,"aei":"CMqttSensor02", … }}}
Steps 2–4 are the same message with ty 3, 4 and 23 and the matching pc, sent on the req topic.
Notifications arrive with the roles swapped — the CSE is the originator and the device the receiver, so they come on /oneM2M/req/<CSE-ID>/<AE-ID>/json, and the device answers on /oneM2M/resp/<CSE-ID>/<AE-ID>/json. Subscribing to your own req topic is the usual reason a device never sees its notifications.
{"op":5,"to":"CMqttSensor02","fr":"/Sutraatman","rqi":"mv41qmx0a23q53ug","rvi":"2",
"pc":{"m2m:sgn":{"nev":{"net":3,"rep":{"m2m:cin":{ … }}},
"sur":"cdac_incse/MqttSensor02/telemetry/to-me","cr":"CMqttSensor02"}}}
WebSocket
Connect with sub-protocol oneM2M.R2.0.json. One primitive per text frame, wrapped as {"m2m:rqp":{…}}, answered with {"m2m:rsp":{…}} carrying the same rqi.
{"m2m:rqp":{"op":1,"to":"/~/Sutraatman/cdac_incse","fr":"CWsSensor02","rqi":"w1","rvi":"2","ty":2,
"pc":{"m2m:ae":{"rn":"WsSensor02","api":"Nsensor.acme.com","rr":true}}}}
The connection becomes that AE's push channel after its first successful request, so a subscription with
"nu":["CWsSensor02"] needs no poa: the notification arrives on the same socket as a request frame ({"m2m:rqp":{"op":5,…}}) and the device answers with an {"m2m:rsp":{"rsc":2000,"rqi":…}} frame.
CoAP (UDP)
POST + option oneM2M-TY creates, GET/PUT/DELETE retrieve/update/delete. The target goes in Uri-Path (~, <CSE-ID>, <resourceName>, …), a credential in Uri-Query as sutraatmanKey=<key>.
| Option | Number | Carries |
|---|---|---|
oneM2M-FR |
256 | the originator |
oneM2M-RQI |
257 | the request identifier |
oneM2M-RSC |
265 | the response status code |
oneM2M-TY |
267 | the resource type on a create |
Content-format 10001 (application/vnd.onem2m-res+json). A create answers 2.01 with the address in
Location-Path and oneM2M-RSC: 2001; a retrieve answers 2.05 with 2000:
POST ~/Sutraatman/cdac_incse/CoapSensor02/telemetry oneM2M-TY: 4
→ 2.01 oneM2M-RSC 2001
Location-Path /cdac_incse/CoapSensor02/telemetry/cin_mv41rdxcyxe8f4mq
No block-wise transfer, so one reading must fit in a datagram (~60 kB); no DTLS.
8. Status codes you will meet
| RSC | HTTP | Meaning |
|---|---|---|
| 2000 | 200 | read or updated |
| 2001 | 201 | created |
| 2002 | 200 | deleted |
| 4000 | 400 | the request or an attribute is malformed — read m2m:dbg |
| 4004 | 404 | no such resource: wrong path |
| 4005 | 405 | not allowed on this resource (e.g. creating under la) |
| 4103 | 403 | not authorised: wrong key, or the ACP does not allow it |
| 4105 | 409 | already exists — AE-ID or resource name taken |
| 4108 | 403 | wrong parent for this resource type |
| 5203 | 403 | the parent cannot be subscribed to |
| 5204 | 500 | the notification target failed verification |
| 5207 | 406 | content larger than the container's mbs |
| 6100 | 403 | the node is full (max IoT units) |
Errors carry a human-readable reason in the body as {"m2m:dbg":"…"} and are written to the node's log, which the console shows under Log management with the originator and the address.
9. Troubleshooting
| What you see | Why |
|---|---|
5001 notify target not supported on a create |
ty= is missing from Content-Type, so the POST was read as a notification |
4103 on every request from a device that used to work |
the AE was given a key; send X-Sutraatman-Key |
4105 at start-up |
the device is already registered — reuse the AE-ID and carry on |
4004 although the AE exists |
the path uses the AE-ID instead of its rn; address resources by resource name |
5103 target not reachable |
the middle node in the path is not running or has not registered with the INCSE |
| subscription created, no notifications | enc.net is [1] (updates) instead of [3] (new children); or the target cannot be reached from the hosting CSE — look for a notification warning in that node's log |
| notifications stop after a while | exc ran out, or the rate limit rl is clipping them |
| the device shows as inactive in the console | it is registered but has sent no contentInstance yet |
| readings disappear after a while | the container's mni/mbs ring buffer, or et expiry — raise mni, mbs or mia |
10. The whole thing as a script
#!/bin/bash
CSE=http://10.0.0.5:8080
BASE=/~/mumbai-1-mncse/mumbai-1-mn
AE=CTempSensor07
h=(-H "X-M2M-Origin: $AE" -H 'X-M2M-RVI: 2')
# 1. register (4105 simply means it is already registered)
curl -s -o /dev/null -w 'register %{http_code}n' -X POST "$CSE$BASE" "${h[@]}"
-H 'X-M2M-RI: 1' -H 'Content-Type: application/json;ty=2'
-d '{"m2m:ae":{"rn":"TempSensor07","api":"Nsensor.acme.com","rr":false,"lbl":["product:WaterPump"]}}'
# 2. container
curl -s -o /dev/null -w 'container %{http_code}n' -X POST "$CSE$BASE/TempSensor07" "${h[@]}"
-H 'X-M2M-RI: 2' -H 'Content-Type: application/json;ty=3'
-d '{"m2m:cnt":{"rn":"telemetry","mni":500}}'
# 3. subscribe (the receiver must already be listening)
curl -s -o /dev/null -w 'subscribe %{http_code}n' -X POST "$CSE$BASE/TempSensor07/telemetry" "${h[@]}"
-H 'X-M2M-RI: 3' -H 'Content-Type: application/json;ty=23'
-d '{"m2m:sub":{"rn":"to-analytics","nu":["http://10.0.0.30:7000/notify"],"enc":{"net":[3]}}}'
# 4. send a reading every 5 seconds
while true; do
curl -s -o /dev/null -w 'reading %{http_code}n' -X POST "$CSE$BASE/TempSensor07/telemetry" "${h[@]}"
-H "X-M2M-RI: $(date +%s%N)" -H 'Content-Type: application/json;ty=4'
-d "{"m2m:cin":{"cnf":"application/json:0","con":"{\"temperature\":$((20 + RANDOM % 18))}"}}"
sleep 5
done
To try all of this without writing any device code, the IoT unit simulator does exactly these four steps over any of the four bindings and is configured from a web page — see SIMULATOR.md.