CoAP Integration Guide
GET, PUT, POST and DELETE resources on constrained devices and gateways over UDP, and start a pipeline on every notification of an observed resource.
- Protocol
- CoAP client over UDP (RFC 7252)
- Default port
- 5683
- Operations
- GET · PUT · POST · DELETE
- Triggers
- Observe, once per notification
CoAP (Constrained Application Protocol) is the web transfer protocol for constrained devices: sensors, actuators, gateways and building controllers on lossy or low-power networks. It looks like HTTP (resources addressed by path, the methods GET, PUT, POST and DELETE, response codes such as 2.05 Content and 4.04 Not Found), but it runs over UDP with compact binary messages. MaestroHub connects as a CoAP client to a device or gateway (the CoAP server). This guide covers connection setup, how messages are sent and retransmitted, how payloads are encoded and decoded, the five function types, and how the matching nodes and trigger slot into the Pipeline Designer.
Overview
The CoAP connector provides:
- GET, PUT, POST and DELETE on any resource path, with query options
- Observe trigger (RFC 7641): one pipeline event per notification, registered again on its own after an outage
- Confirmable or non-confirmable messages, with RFC 7252 retransmission for confirmable ones
- Block-wise transfer (RFC 7959) for payloads larger than one datagram
- Content-Format aware payloads:
text/plain,application/json,application/cborandapplication/octet-streamsent; text, JSON and CBOR decoded into values on the way back - Client only: no listening port, and cloud-metadata and loopback destinations refused by default
A client that never listens
MaestroHub never binds port 5683, or any other known port. Each connection opens one UDP socket with an ephemeral local port chosen by the operating system, connected to the device's address. Every request leaves from that socket, Observe registrations included, and replies and Observe notifications return to it: a CoAP server answers the endpoint a request came from. The kernel only delivers datagrams from the connected device address to that socket.
So a firewall only has to allow outbound UDP to the device's port and the replies to it. A device that sends requests of its own to MaestroHub gets 4.04 Not Found: the connector serves no resources.
The socket carries one request at a time (RFC 7252's NSTART = 1, a client keeps at most one outstanding interaction with a server), so per-connection concurrency is 1. Observe notifications arrive on the same socket and do not hold it.
Confirmable and non-confirmable messages
Every request function has a Message Type:
| Message Type | What happens | Use it for |
|---|---|---|
confirmable (default) | The server acknowledges the message. Until it does, MaestroHub sends it again every ACK Timeout, with the same message ID, up to Max Retransmissions times. The server's duplicate detection makes the copies one request. | Writes, and reads on lossy networks |
nonConfirmable | Sent once, with no acknowledgement and no retransmission. MaestroHub still waits for the response until the timeout. | Frequent reads on a reliable network, where a lost reading is replaced by the next one |
A request still unanswered when its function's Timeout ends fails, as a transient error. The default Timeout of 2 minutes covers RFC 7252's worst case for the default transmission parameters (MAX_TRANSMIT_WAIT, 93 seconds with ACK Timeout 2s and Max Retransmissions 4).
Block-wise transfer
A payload larger than one block is sent and received in blocks of Block Size bytes (RFC 7959), and reassembled before it reaches the pipeline. It is transparent to the function: a GET of a 20 KB document returns the whole document. A payload sent or reassembled cannot exceed the connection's Max Payload Size; a larger response fails instead of filling memory.
Content-Format: how payloads are read
A CoAP response declares its payload's Content-Format. MaestroHub decodes the payload by that declaration and never guesses:
| Declared Content-Format | value is | valueEncoding |
|---|---|---|
text/plain, application/link-format, other text/* | A string | text |
application/json, and +json formats such as application/senml+json | The parsed JSON value | json |
application/cbor, and +cbor formats such as application/senml+cbor | The CBOR value, converted to JSON | json |
application/octet-stream, any other format, or no Content-Format at all | The bytes, as base64 | base64 |
valueEncodingalways says which of the threevalueis, so a pipeline never has to inspect the value to know how to read it.- An empty payload has no
valueand novalueEncoding. - A payload that does not parse as its declared format (invalid UTF-8 for text, malformed JSON, malformed CBOR) is an error, never a best-effort value.
- CBOR conversion: byte strings become base64, integer map keys become their decimal text (the shape SenML and LwM2M use), a time tag becomes an RFC 3339 UTC string, other tags become
{"tag": n, "value": v}. NaN and the infinities, which JSON cannot carry, make the payload an error rather than an invented stand-in.
Outcomes: what counts as success
| Response or failure | Outcome | Retried by the node settings? |
|---|---|---|
2.xx (2.01 Created, 2.02 Deleted, 2.04 Changed, 2.05 Content, …) | Success | - |
4.xx: the server refused the request (4.00 Bad Request, 4.04 Not Found, 4.05 Method Not Allowed, 4.15 Unsupported Content-Format, …) | Permanent: it did not act, and the same request will be refused again | No |
4.08 Request Entity Incomplete (a block was lost) and 4.29 Too Many Requests | Transient | Yes |
5.xx (5.00 Internal Server Error, 5.03 Service Unavailable, 5.04 Gateway Timeout, …) | Transient | Yes |
| A timeout after the request was sent, a lost connection, an ICMP unreachable, an undecodable response | Transient: the server may have acted on the request, or on an earlier retransmitted copy | Yes for GET, PUT and DELETE; never for POST |
| A refused destination (egress guard), a host name that does not resolve, a function parameter that fails validation | Permanent: nothing was sent | No |
Connection Configuration
Creating a CoAP Connection
Navigate to Connections → New Connection → CoAP and configure the form. The settings are grouped into Connection, Transmission and Security; the Functions, Scaling and Health tabs unlock after the connection is saved.
1. Profile Information
| Field | Default | Description |
|---|---|---|
| Profile Name | - | A descriptive name for this connection profile (required). Must be unique across all connections. |
| Description | - | Optional description for this CoAP connection |
| Labels | - | Key-value pairs to categorize and organize the connection, e.g. site: building-a, device: gateway-1 |
2. Connection
| Field | Default | Description |
|---|---|---|
| Host | - | Device or gateway host name or IP address (required). Example: 10.0.5.30. A host name, not a URI: coap://… is refused, and the port goes in Port. |
| Port | 5683 | UDP port of the CoAP server, 1-65535. 5683 is the CoAP default. 5684 is CoAP over DTLS, which this version does not support. |
| Connect Timeout | 10s | How long Connect waits for the server to answer a CoAP ping (an empty confirmable message every server must answer with a Reset), 1s-5m |
3. Transmission
| Field | Default | Description |
|---|---|---|
| ACK Timeout | 2s | RFC 7252 ACK_TIMEOUT: how long to wait for the acknowledgement of a confirmable message before sending it again, 100ms-1m. Raise it on slow or duty-cycled networks. |
| Max Retransmissions | 4 | RFC 7252 MAX_RETRANSMIT: how many times a confirmable message is sent again, with the same message ID, while it is not acknowledged, 0-10 |
| Block Size | 1024 | RFC 7959 block size in bytes for payloads larger than one datagram: 16, 32, 64, 128, 256, 512 or 1024. Lower it for devices with small buffers. |
| Max Payload Size | 1048576 (1 MiB) | Largest payload, in bytes, sent or reassembled from blocks, 1024-16777216. Must be at least the Block Size. A larger response fails instead of filling memory; a larger payload to send is refused before anything is sent. |
4. Security: Egress Guard
Both toggles are off by default.
| Field | Default | Description |
|---|---|---|
| Allow Loopback Destinations | false | Permit 127.0.0.0/8 and ::1. Off by default: a loopback address is almost never the intended target of an outbound device connector. |
| Allow Cloud-Metadata Destinations | false | Permit 169.254.169.254 (AWS) and metadata.google.internal (GCP). Off by default: these endpoints return cloud credentials without authentication. |
The guard runs on every connect: the host name is resolved once, and both the name and the address chosen are checked before the socket is opened, so a name cannot resolve to an allowed address for the check and a blocked one for the socket. A host re-pointed at a blocked address is caught on the next reconnect. A refusal is a permanent error that names the fix:
connect to CoAP server localhost:5683: coap: destination blocked by egress policy: "localhost" resolves to loopback IP 127.0.0.1; set allowLoopback=true to permit
Testing the Connection
Click Test Connection to try the current settings before saving. Connect opens the socket and sends a CoAP ping: an empty confirmable message that every CoAP server must answer with a Reset. It sends no request, so testing never reads or changes a resource. A server that does not answer within Connect Timeout fails the test.
Once the connection runs, its health check sends the same ping and reports the round trip. A ping that is not answered drops the socket, and the connection reconnects on a fresh one.
Function Builder
Creating CoAP Functions
After the connection is saved:
- Open the connection and navigate to the Functions tab
- Click New Function to open the function type selection dialog
- Choose GET, PUT, POST, DELETE, or Observe
- Fill the Basic tab (name, description, labels) and the Configuration tab (operation-specific parameters)
- Use the Test button to run the function against the live device before saving
Every parameter is checked before anything is sent: a bad path, query, Content-Format or payload is refused with the reason, and nothing reaches the device.
Resource Path takes a path, not a URI: the host and port come from the connection, and the query has its own field. Each /-separated segment becomes one Uri-Path option, with percent-escapes decoded (/a%20b is the segment a b). / alone is the root resource. A scheme (coap://), ? or #, an empty segment (//), a . or .. segment, or a segment longer than 255 bytes is refused.
Query takes key=value parts separated by & (a leading ? is ignored). Each part becomes one Uri-Query option, percent-decoded, at most 255 bytes.
GET (coap.get)
Purpose: Reads a resource and returns the response code, the decoded payload and its Content-Format. Large representations are fetched block by block.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Resource Path | String | Yes | - | Path of the resource, e.g. /sensors/temp. Supports ((parameter)) syntax. |
| Query | String | No | - | Optional query, e.g. unit=C&avg=60. Supports ((parameter)) syntax. |
| Accept | Select | No | server default | Ask the server for this Content-Format: text/plain, application/json, application/cbor, application/octet-stream or application/link-format. Leave empty to take its default. |
| Message Type | Select | No | confirmable | confirmable or nonConfirmable; see Confirmable and non-confirmable messages |
| Timeout | Duration | No | 2m | Bound on this request, including retransmissions and every block, 1s-1h |
Examples: read a temperature sensor at /sensors/temp; list a device's resources from /.well-known/core (returned as application/link-format text); read a gateway status document as JSON.
PUT (coap.put)
Purpose: Replaces the state of a resource, such as an actuator's set-point, with the given payload in the chosen Content-Format. PUT is idempotent.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Resource Path | String | Yes | - | Path of the resource, e.g. /actuators/valve. Supports ((parameter)) syntax. |
| Query | String | No | - | Optional query, e.g. mode=fast. Supports ((parameter)) syntax. |
| Payload | String | Yes | - | What to send, as text; see Payload by Content-Format. Supports ((parameter)) syntax. A PUT without a payload is refused. |
| Content-Format | Select | No | text/plain | Content-Format of the payload, sent as the Content-Format option: text/plain, application/json, application/cbor or application/octet-stream |
| Accept | Select | No | server default | Ask the server for this Content-Format in its response |
| Message Type | Select | No | confirmable | confirmable or nonConfirmable |
| Timeout | Duration | No | 2m | Bound on this request, including retransmissions and every block, 1s-1h |
Examples: set a dimmer to 75 at /lights/1/level; write a JSON configuration document to /config.
POST (coap.post)
Purpose: Asks the resource to process the payload: create a child resource, run an action, or accept a record. A 2.01 Created response names what it created in location.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Resource Path | String | Yes | - | Path of the resource, e.g. /actuators/pump/start. Supports ((parameter)) syntax. |
| Query | String | No | - | Optional query, e.g. id=7. Supports ((parameter)) syntax. |
| Payload | String | No | - | What to send, as text; see Payload by Content-Format. Leave empty to POST without a payload. Supports ((parameter)) syntax. |
| Content-Format | Select | No | text/plain | Content-Format of the payload, sent as the Content-Format option when there is a payload |
| Accept | Select | No | server default | Ask the server for this Content-Format in its response |
| Message Type | Select | No | confirmable | confirmable or nonConfirmable |
| Timeout | Duration | No | 2m | Bound on this request, including retransmissions and every block, 1s-1h |
Examples: start a pump cycle with POST /actuators/pump/start; create a schedule entry under /schedules.
POST is not idempotent: sending it again can run the action again. MaestroHub sends it once per call. If its outcome is unknown (no response before the timeout, a lost connection), the node fails with coap.post command outcome unknown and the POST is never sent again automatically: not by the connector, not by a node's Retry on Fail or Retry on Timeout, and not by a ForEach retry. A POST the server refused (4.xx) fails with the device refused the coap.post command and is not retried either. Check the device before running it again.
Within one call, a confirmable POST may be retransmitted with the same message ID until it is acknowledged; the server's duplicate detection treats the copies as one request.
DELETE (coap.delete)
Purpose: Deletes a resource and returns the server's response code. DELETE is idempotent. A refusal such as 4.04 Not Found or 4.05 Method Not Allowed is reported as the device's answer.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Resource Path | String | Yes | - | Path of the resource to delete, e.g. /schedules/7. Supports ((parameter)) syntax. |
| Query | String | No | - | Optional query. Supports ((parameter)) syntax. |
| Message Type | Select | No | confirmable | confirmable or nonConfirmable |
| Timeout | Duration | No | 2m | Bound on this request, including retransmissions, 1s-1h |
Examples: remove a schedule entry at /schedules/7; clear a stored alarm at /alarms/42.
Observe (coap.observe)
Purpose: Starts a pipeline for every notification of an observed resource (RFC 7641): the current value at registration, then each change. This function type is a pipeline trigger: see Pipeline Integration and the CoAP Observe Trigger node.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Resource Path | String | Yes | - | Path of the observable resource, e.g. /sensors/temp |
| Query | String | No | - | Optional query sent with the registration |
| Accept | Select | No | server default | Ask the server for this Content-Format in its notifications |
How an observation is kept alive:
- Registration is a GET with the Observe option from the connection's own socket, and notifications return to that socket. A path that does not exist or is refused fails the subscription with the server's code, and it is retried. A resource that answers without the Observe option is not observable: it sent its value once but will not notify, and the subscription fails with that reason.
- Re-registration: the observation is registered again when the socket is lost (a reconnect), when the server ends it (an error notification, or a final notification without the Observe option), and when a notification is overdue: no notification for the last one's Max-Age (60 seconds when the server sends none) plus 10 seconds, the point at which RFC 7641 lets a client assume the observation was lost. Attempts back off from 0.5 seconds to 30 seconds while the device stays away.
- No repeat on re-registration: a re-registration answers with the current value. When it is the value already delivered, it is not emitted again, so a quiet resource does not start a run every minute.
- One Bad event per outage: when the observation fails (the link stays lost, a re-registration is refused or unanswered, the server ends it with an error code, or a notification does not parse as its Content-Format), the trigger fires once with quality
badand the cause, carrying the last value delivered. A reconnect that restores the observation within half a second raises no event at all. The first good notification afterwards is the recovery event, with qualitygood, delivered even when its value has not changed.
Each event's payload:
{
"path": "/sensors/temp",
"code": "2.05",
"status": "Content",
"contentFormat": "application/json",
"value": { "t": 21.5 },
"valueEncoding": "json",
"bytes": 10,
"observe": 17,
"etag": "a1b2",
"maxAge": 30,
"serverTimestamp": "2026-10-20T10:14:03.240Z"
}
See the CoAP Observe Trigger node for every field and the _metadata.
Payload by Content-Format
PUT and POST take the payload as text, in the form the Content-Format names:
| Content-Format | Payload text | Sent on the wire |
|---|---|---|
text/plain (default) | Any UTF-8 text | The text's bytes |
application/json | JSON text; must parse | The JSON text as given |
application/cbor | One JSON value, as JSON text | That value encoded as CBOR; a whole number stays a CBOR integer |
application/octet-stream | Base64 (padded or not) | The decoded bytes |
A payload that does not match its Content-Format (payload is not valid JSON, application/octet-stream takes the bytes as base64, and the payload is not valid base64) is refused before anything is sent. A payload larger than the connection's Max Payload Size is refused the same way.
Using Parameters
Resource Path, Query and Payload on the GET, PUT, POST and DELETE functions support ((parameterName)) placeholders. Parameters are detected as you type and appear in the Function Parameters panel on the Configuration tab, where you set each one's type, requiredness, default, and description. At execution the pipeline node binds them, so one function serves many devices or set-points: for example Resource Path /lights/((lightId))/level and Payload ((level)). A value that still holds a placeholder is checked again, in full, when the call runs.
Per-call timeouts
The Timeout on a function is one layer of the deadline MaestroHub composes for every call; the pipeline, node, function, and connection layers each contribute, and the tightest bound wins. See Connection Timeouts for the full hierarchy. It bounds the whole request: every retransmission and every block. Keep it longer than ACK Timeout × (Max Retransmissions + 1), so a confirmable request has time for all its retransmissions.
Pipeline Integration
Use the CoAP functions you configure here as nodes inside the Pipeline Designer. GET, PUT, POST and DELETE are exposed as matching connector nodes, and Observe functions drive the CoAP trigger node:
- CoAP GET (
connected.coap.get): executes acoap.getfunction - CoAP PUT (
connected.coap.put): executes acoap.putfunction - CoAP POST (
connected.coap.post): executes acoap.postfunction - CoAP DELETE (
connected.coap.delete): executes acoap.deletefunction - CoAP Observe Trigger (
trigger.coap): starts a pipeline for each notification of acoap.observefunction
For node-level details (output structure, execution settings), see the CoAP Nodes and CoAP Observe Trigger pages.
PUT, POST and DELETE are realtime device writes:
- They are never buffered by Store & Forward. A write issued while the device is unreachable fails; it is not replayed later, when the value may no longer be right.
- A pipeline run started by a late (replayed) value refuses them, unless the node allows late input.
- PUT and DELETE are idempotent: the same PUT leaves the resource in the same state, and a second DELETE leaves it deleted (the device may answer
2.02or4.04). A node's retry settings may send them again after a transient failure. - POST is a control command: it is sent at most once and never re-sent automatically; see POST.
For orchestration strategies that mix CoAP with other data sources, see the Nodes page.
Common Use Cases
Sensor Telemetry to the UNS
Author an Observe function on each sensor resource (for example /sensors/temp), then use the CoAP Observe Trigger node to route every notification to the Unified Namespace. The device pushes each change, so there is no polling; an outage arrives as one bad event that a UNS Publish node stores with its quality.
Polling Devices That Do Not Notify
For resources that are not observable, drive a GET from a Schedule trigger. Use nonConfirmable on a reliable network to keep traffic down, and branch on result.valueEncoding when a device may answer in more than one Content-Format.
Set-Points and Actuators
Receive a set-point from an upstream system or operator UI, validate it with a Condition node, then PUT it to the actuator resource (/lights/1/level, /actuators/valve). Because PUT is idempotent, a retry after a transient failure leaves the device in the same state.
Actions and Records
Use POST for what the device does once per request: start a pump cycle, acknowledge an alarm, add a schedule entry. Read result.location to learn the path of an entry the device created, and DELETE it later by that path. Since a POST whose outcome is unknown is never re-sent, read the device state with a GET before running it again.
Resource Discovery
GET /.well-known/core returns the device's resource list in CoRE Link Format as text, which a JavaScript node can parse to find the paths to observe.
Troubleshooting
Error Reference
| What you see | Meaning | Retried? |
|---|---|---|
coap.get /sensors/x: server answered 4.04 NotFound | The server refused the request (also 4.00, 4.01, 4.03, 4.05, 4.15, …) | No, it will not change on retry |
server answered 4.29 TooManyRequests / 5.03 ServiceUnavailable | The server is busy or failed | Yes |
response payload: payload declared application/json is not valid JSON | The response arrived but does not parse as its declared Content-Format | Yes for GET, PUT and DELETE (the request may have acted); never for POST |
coap.post command outcome unknown | A POST failed after it may have reached the device | Never, whatever the retry settings: check the device |
the device refused the coap.post command | The server answered a POST with 4.xx | No |
not connected to CoAP server host:port | The connection is down | Yes, after reconnect |
coap: destination blocked by egress policy | The host resolves to loopback or a cloud-metadata address | No; see Security: Egress Guard |
the server answered without an Observe option: the resource is not observable | An Observe function names a resource that does not notify | No; poll it with a GET instead |
payload is N bytes, over the connection's max payload size of M | A PUT or POST payload is larger than Max Payload Size | No; shorten it or raise the limit |
Connection Issues
| Symptom | Possible Cause | Solution |
|---|---|---|
Test Connection fails with did not answer a ping | Wrong host or port, the device's CoAP service is not running, or a firewall drops UDP | Check the port (5683 by default), and that outbound UDP to it and its replies are allowed. |
The device only listens on 5684 | The device requires CoAP over DTLS (coaps) | Not supported in this version. Enable plain CoAP on the device, or use a gateway that does. |
host … must be a host name or IP address, not a URI | Host holds coap://… | Enter only the host name or IP address; the port goes in Port and the path in each function. |
| Requests time out on a slow or sleepy network | ACK Timeout too short for the network, so retransmissions run out | Raise ACK Timeout (and the function's Timeout with it). |
| Large responses fail | The device's buffers cannot take the Block Size, or the response exceeds Max Payload Size | Lower Block Size, or raise Max Payload Size. |
| The device logs a new Observe registration about every 70 seconds | Expected for a quiet resource: no notification for Max-Age (60 seconds when the device sends none) plus 10 seconds, so the observation is registered again. An unchanged value is not emitted again. | Nothing to fix. A device that sends a longer Max-Age with its notifications is re-registered less often. |
The Observe trigger reports bad and no value follows | The observation could not be restored: the device is unreachable, or it refuses the re-registration | The cause is in error on the bad event. Registration is retried with backoff up to every 30 seconds; the next good notification is the recovery event. |
value is base64 when text or JSON was expected | The device sent no Content-Format, or one the connector does not decode | Set Accept to the format you want, or decode the base64 in a JavaScript node. valueEncoding says base64. |
Not in this version
- DTLS (
coaps://, port 5684) and other security modes - CoAP over TCP or WebSockets (RFC 8323)
- OSCORE (RFC 8613) object security
- Multicast requests and multicast discovery
- A Resource Directory (RFC 9176)
- Acting as a CoAP server: MaestroHub serves no resources and does not accept requests from devices