Designing CoAP APIs follows REST principles: structure resources as nouns (e.g., /sensors/temp/value), use CoAP methods as verbs (GET, POST, PUT, DELETE), and select compact content formats like CBOR over JSON to minimize payload size on constrained networks. Proper response codes (2.xx/4.xx/5.xx), DTLS security, and rate limiting are essential for robust production IoT APIs.
48.1 Learning Objectives
By the end of this chapter, you will be able to:
Design RESTful CoAP APIs: Construct resource hierarchies using nouns with proper URI conventions and versioning strategies
Distinguish Content Formats: Compare JSON, CBOR, and text/plain payloads and select the appropriate format for constrained versus development environments
Implement Error Handling: Apply proper CoAP response codes (2.xx/4.xx/5.xx) and construct helpful error payloads for production systems
Configure Security Controls: Configure DTLS, rate limiting, and per-device access control for production CoAP deployments
Calculate Energy Tradeoffs: Calculate battery life impact of CON versus NON message types and justify the selection for different IoT use cases
Diagnose API Pitfalls: Identify and resolve common CoAP API problems such as Observe notification floods and proxy caching issues
For Beginners: CoAP API Design
Designing a CoAP API means deciding how IoT devices expose their data and accept commands. Think of it as creating a menu for your sensor – you define endpoints like /temperature or /status that other devices can read. Good API design makes your IoT system easy to use and understand, even for developers who have never seen your device before.
Sensor Squad: Building Sammy’s Data Menu
“I want other devices to read my temperature, but I also want them to set my sampling rate,” said Sammy the Sensor. “How do I organize all that?”
Max the Microcontroller sketched out a plan. “You create a resource tree, Sammy! Your root is /sensor, and under it you have /sensor/temperature for reading data and /sensor/config for settings. When someone sends a GET to /sensor/temperature, they get your latest reading. When they send a PUT to /sensor/config, they can change your sampling rate.”
“Keep your URIs short!” advised Bella the Battery. “Every extra character in the path costs bytes, and CoAP packets should stay small enough to fit in a single UDP datagram. Use /temp instead of /temperature-reading-in-celsius. Every byte saved is energy saved!”
Lila the LED added: “And don’t forget content negotiation! Some devices want your data in JSON, others in CBOR – which is like compressed JSON. Your API should let the requester choose the format using the Accept option. One sensor, multiple formats, everyone is happy!”
48.2 Prerequisites
Before diving into this chapter, you should be familiar with:
Core Concept: CoAP follows REST principles - design resources as nouns, use HTTP-like methods as verbs. The URI identifies WHAT you’re accessing, the method specifies HOW.
Why It Matters: Consistent RESTful design makes APIs intuitive for developers, enables caching, supports proxying, and allows standard tooling to work seamlessly.
Key Takeaway: Good resource design: coap://sensor.local/v1/temperature (noun). Bad design: coap://sensor.local/getTemperature (verb in URL).
coap://sensor.local/getTemperature # Verb in URL - wrong!
coap://actuator.local/turnOnLED # Verb in URL - wrong!
REST operations on resources:
GET coap://sensor.local/v1/temperature # Read current value
PUT coap://actuator.local/v1/led/state # Update LED state
POST coap://gateway.local/v1/logs # Create new log entry
DELETE coap://gateway.local/v1/logs/2025-01 # Delete old logs
48.3.2 URI Naming Conventions
CoAP URIs should be short (constrained bandwidth) but descriptive:
Example - Temperature reading in different formats:
# Text/plain (4 bytes) - best for simple values"23.5"# JSON (42 bytes) - good for debugging{"device":"temp42","value":23.5,"unit":"C"}# CBOR (20 bytes) - production choiceA3 666465766963656674656D703432...
Recommendation:
Battery sensors: Use text/plain or CBOR (minimize bytes)
Development: Use JSON (easy debugging)
Production: Use CBOR (efficient, supports rich structures)
IoT devices often run for years - plan for API evolution:
URI versioning (recommended for CoAP):
coap://sensor.local/v1/temperature # Original API
coap://sensor.local/v2/temperature # New version with metadata
Why URI versioning for IoT:
Simple for embedded clients
No custom header parsing needed
Clear in logs and debugging
Works with all CoAP libraries
Version migration example:
# v1: Simple temperature reading
GET coap://sensor.local/v1/temperature
Response: "23.5"
# v2: Rich metadata added
GET coap://sensor.local/v2/temperature
Response (CBOR): {"value":23.5,"unit":"C","timestamp":1642259400}
# Legacy devices continue using v1
# New devices adopt v2 when convenient
48.4 Error Handling
Use proper CoAP response codes and provide helpful error payloads:
48.4.1 Standard Response Codes
2.01 Created # POST created new resource
2.04 Changed # PUT updated resource
2.05 Content # GET successful with payload
4.00 Bad Request # Invalid syntax
4.01 Unauthorized # Authentication required
4.04 Not Found # Resource doesn't exist
4.05 Method Not Allowed # GET on write-only resource
5.00 Internal Server Error
48.4.2 Error Payload Format
JSON for human readability:
{"error":{"code":"SENSOR_OFFLINE","message":"Device has not reported in 5 minutes","timestamp":"2025-01-15T10:30:00Z","device_id":"temp42","retry_after":300}}
Scenario: 200 soil moisture sensors across a farm, battery-powered, reporting to a central gateway.
API Design:
# Resource structure
coap://gateway.local/v1/sensors/{sensor_id}/moisture
coap://gateway.local/v1/sensors/{sensor_id}/battery
coap://gateway.local/v1/sensors/{sensor_id}/config
# Normal operation (NON messages for efficiency)
Sensor -> Gateway: NON POST /v1/sensors/field3-42/moisture
Payload (CBOR): {value: 35, unit: "%", timestamp: 1642259400}
# Critical alerts (CON for reliability)
Sensor -> Gateway: CON POST /v1/sensors/field3-42/alert
Payload (CBOR): {type: "LOW_MOISTURE", threshold: 20, current: 15}
# Gateway observes battery levels
Gateway -> Sensor: GET /v1/sensors/field3-42/battery (Observe: 0)
Sensor -> Gateway: Notifications when battery drops below thresholds
# Versioning for future expansion
v1: Basic moisture reporting
v2: Adds soil temperature and pH
(v1 sensors continue working while v2 rolls out)
Why this works:
NON messages save battery (no ACK overhead)
CON ensures critical alerts aren’t lost
CBOR minimizes bandwidth
Observe pattern prevents polling battery status
Versioning allows gradual upgrades
48.8 Working Code: Python CoAP Client and Server
Real request/response examples using aiocoap (Python) and coap-client (CLI).
48.8.1 Python CoAP Server (Gateway)
# pip install aiocoapimport asyncioimport aiocoapimport aiocoap.resource as resourceimport jsonimport timeclass TemperatureResource(resource.ObservableResource):"""CoAP resource for temperature readings."""def__init__(self):super().__init__()self.value =22.5self.last_updated = time.time()asyncdef render_get(self, request):"""GET /v1/temperature -> returns latest reading.""" payload = json.dumps({"value": self.value,"unit": "C","timestamp": int(self.last_updated) }).encode('utf-8')# Response code 2.05 Contentreturn aiocoap.Message( payload=payload, content_format=50# application/json )asyncdef render_put(self, request):"""PUT /v1/temperature -> updates reading from sensor.""" data = json.loads(request.payload)self.value = data['value']self.last_updated = time.time()self.updated_state() # Triggers Observe notificationsreturn aiocoap.Message(code=aiocoap.CHANGED)class ConfigResource(resource.Resource):"""CoAP resource for device configuration."""def__init__(self):super().__init__()self.interval =60# secondsasyncdef render_get(self, request): payload = json.dumps({"reporting_interval": self.interval}).encode()return aiocoap.Message(payload=payload, content_format=50)asyncdef render_put(self, request): data = json.loads(request.payload)self.interval = data.get('reporting_interval', self.interval)return aiocoap.Message(code=aiocoap.CHANGED)asyncdef main(): root = resource.Site() root.add_resource(['v1', 'temperature'], TemperatureResource()) root.add_resource(['v1', 'config'], ConfigResource())await aiocoap.Context.create_server_context(root, bind=('::', 5683))print("CoAP server running on port 5683")await asyncio.get_event_loop().create_future() # Run foreverasyncio.run(main())
48.8.2 Python CoAP Client (Sensor)
import asyncioimport aiocoapimport jsonasyncdef read_temperature():"""GET request to read temperature.""" context =await aiocoap.Context.create_client_context() request = aiocoap.Message( code=aiocoap.GET, uri='coap://localhost/v1/temperature' ) response =await context.request(request).responseprint(f"Response code: {response.code}") # 2.05 Contentprint(f"Payload: {response.payload.decode()}")# Output: {"value": 22.5, "unit": "C", "timestamp": 1698765432}asyncdef send_temperature(value):"""PUT request to update temperature.""" context =await aiocoap.Context.create_client_context() payload = json.dumps({"value": value}).encode() request = aiocoap.Message( code=aiocoap.PUT, uri='coap://localhost/v1/temperature', payload=payload, content_format=50# application/json ) response =await context.request(request).responseprint(f"Update response: {response.code}") # 2.04 Changed# What to observe:# - GET returns 2.05 Content with JSON payload# - PUT returns 2.04 Changed (no payload needed)# - Wrong URI returns 4.04 Not Found# - Unsupported method returns 4.05 Method Not Allowed
48.8.3 CLI Testing with coap-client
# Install: apt install libcoap2-bin (or brew install libcoap)# GET temperature readingcoap-client-m get coap://localhost/v1/temperature# Output: {"value":22.5,"unit":"C","timestamp":1698765432}# PUT new readingcoap-client-m put coap://localhost/v1/temperature \-e'{"value":23.1}'# Output: (empty, 2.04 Changed)# Observe (subscribe to changes for 60 seconds)coap-client-m get -s 60 coap://localhost/v1/temperature# Receives notification each time temperature changes# Discovery: list all resourcescoap-client-m get coap://localhost/.well-known/core# Output: </v1/temperature>,</v1/config>
Putting Numbers to It: CoAP Message Types and Battery Life
Scenario: Battery sensor reports every 60 seconds for 1 year using CR2032 (220 mAh @ 3V).