Docs / Contact / protocols/rest

REST Protocol Guide

Send and receive messages over HTTPS using Contact's REST API. This is useful for server-side applications, request-response workflows, or any environment where HTTP is preferred over MQTT.

Base URL

code

https://app.tendrl.com/api

Authentication

All message endpoints require an entity API key passed as a Bearer token:

code

Authorization: Bearer YOUR_ENTITY_API_KEY

Getting Your API Key

Contact issues an API key automatically when you create the entity. To retrieve it:

  1. Open the entity in the Contact dashboard
  2. Click Connection Instructions
  3. Copy the API Key value (the secret)
Caution

The secret is shown once at creation. If you lose it, rotate the key from Access Control → API Keys.

---

Sending Messages

Single Message

code

POST /api/entities/message

Send one message at a time. Use this for heartbeats or when you need the server response immediately.

bash

curl -X POST https://app.tendrl.com/api/entities/message \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "msg_type": "publish",
    "data": {
      "temperature": 23.5,
      "humidity": 65
    },
    "context": {
      "tags": ["sensor", "building-a"]
    }
  }'

Response (200 OK):

json

{
  "code": 200,
  "content": "<message id>"
}

Batch Messages

code

POST /api/entities/messages

Send multiple messages in a single request. More efficient for high-throughput scenarios.

bash

curl -X POST https://app.tendrl.com/api/entities/messages \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "msg_type": "publish",
      "data": {"temperature": 23.5},
      "context": {"tags": ["sensor"]}
    },
    {
      "msg_type": "publish",
      "data": {"temperature": 24.1},
      "context": {"tags": ["sensor"]}
    }
  ]'

---

Message Types

msg_type Purpose Batching
publish Standard data message: telemetry, events, commands Yes
heartbeat Health check with system metrics. Updates entity online status. Yes
state_new Replace the entity's state table with data Yes
state_update Merge data into the entity's existing state table Yes

Publish Message

The default message type. Validated against service rules (if assigned), stored in message history, and can trigger flows.

json

{
  "msg_type": "publish",
  "data": {
    "temperature": 23.5,
    "humidity": 65,
    "location": "Building A"
  }
}

Send to a Specific Entity

Add the dest field to route the message to another entity by name:

json

{
  "msg_type": "publish",
  "data": {
    "alert": "Temperature threshold exceeded",
    "value": 98.6
  },
  "dest": "control-panel-01"
}

Heartbeat

Heartbeats update the entity's online status and are stored for monitoring. They skip validation rules. Required fields: mem_free, mem_total, disk_free, disk_size.

json

{
  "msg_type": "heartbeat",
  "data": {
    "mem_free": 1024.0,
    "mem_total": 4096.0,
    "disk_free": 50000.0,
    "disk_size": 100000.0
  }
}

Response (200 OK): Empty body.

State New

Replaces the entity's entire state table with the contents of data:

json

{
  "msg_type": "state_new",
  "data": {
    "firmware_version": "1.2.0",
    "mode": "active",
    "last_calibrated": "2024-01-10"
  }
}

State Update

Merges data into the entity's existing state table. Only the specified keys are updated:

json

{
  "msg_type": "state_update",
  "data": {
    "mode": "standby"
  }
}

---

State Table

The state table is a persistent key-value store on each entity. You can update it via messages (state_new, state_update) or manage it directly with these endpoints.

Read State Table

code

GET /api/entities/status-table

Returns the authenticated entity's current state table.

bash

curl https://app.tendrl.com/api/entities/status-table \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY"

Response (200 OK):

json

{
  "statusTable": {
    "firmware_version": "1.2.0",
    "mode": "active",
    "last_calibrated": "2024-01-10"
  }
}

Update State Table (Merge)

code

PATCH /api/entities/status-table

Merges the provided fields into the existing state table. Keys not included are left unchanged.

bash

curl -X PATCH https://app.tendrl.com/api/entities/status-table \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "standby"}'

Replace State Table

code

PUT /api/entities/status-table

Replaces the entire state table with the provided object.

bash

curl -X PUT https://app.tendrl.com/api/entities/status-table \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firmware_version": "2.0.0", "mode": "active"}'
Info

The state_new and state_update message types go through the message pipeline (validation, flow triggers). The direct PATCH/PUT endpoints update the state table immediately without message processing.

---

Receiving Messages

Check for Pending Messages

code

GET /api/entities/check_messages?limit=5

Poll for messages sent to your entity. Messages are atomically marked as processed when retrieved, so each message is delivered only once.

bash

curl https://app.tendrl.com/api/entities/check_messages?limit=5 \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY"

Response (200 OK):

json

{
  "messages": [
    {
      "msg_type": "publish",
      "data": {
        "command": "reboot",
        "reason": "firmware update"
      },
      "tags": ["maintenance"],
      "source": "123456:us-1:entity:control-panel",
      "timestamp": "2024-01-15T10:30:45.123456Z"
    }
  ]
}

If no messages are pending, messages will be an empty array.

Parameter Type Default Description
limit integer 1 Maximum number of messages to retrieve

List Message History

code

GET /api/entities/messages

Paginated list of all non-heartbeat messages in your account.

bash

curl "https://app.tendrl.com/api/entities/messages?limit=20" \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY"
Note

This endpoint uses the same entity API key as every other endpoint, but requires the entity:ListMessages permission. The default entity role does not include it, so an admin must grant it explicitly before an entity can list or fetch message history.

Parameter Type Description
entity_name string Filter by entity name
start_date string Start date (RFC 3339)
end_date string End date (RFC 3339)
limit integer Results per page (default: 10)
next_cursor string Cursor for next page

Get a Single Message

code

GET /api/entities/messages/:id
bash

curl https://app.tendrl.com/api/entities/messages/MESSAGE_ID \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY"

---

File Transfer

Entities can also send files (not just JSON messages). Files are malware-scanned by Surface before delivery and deleted once downloaded:

code

POST /api/entities/files            # upload (multipart: file, dest=recipient entity)
GET  /api/entities/files            # receiver inbox (clean files for you)
GET  /api/entities/files/download/:id  # download (consumes the file)
GET  /api/files?kind=clip              # account clip gallery
GET  /api/files/:id/video                # MP4 preview (jpeg_burst clips)
GET  /api/files/:id/poster               # poster thumbnail
GET  /api/files/:id/raw                  # original zip or legacy GIF

See File Transfer for the full reference, limits, and error codes.

---

Message Payload Reference

Outbound (Entity to Contact)

Field Type Required Description
msg_type string Yes publish, heartbeat, state_new, or state_update
data object Yes* Your message payload (arbitrary key-value pairs). *Required fields for heartbeat.
timestamp string No RFC 3339 timestamp. Defaults to server time if omitted.
dest string No Destination entity name for directed messages
context object No Additional context (see below)

Context Object

Field Type Description
tags string[] Tags for routing, filtering, and fanout fan-out (max 10)

Inbound (Contact to Entity via check_messages)

Field Type Description
msg_type string Message type
data object Message payload
tags string[] Tags from the original message
source string Sender entity resource path
timestamp string Original message timestamp

---

Validation

Messages from entities with a service assigned are validated automatically. The response to check_messages includes validation results in the data field when validation fails.

Status Meaning
passed All validation rules passed
failed One or more rules failed; actionResults included in data
skipped No validation applied (heartbeats, state messages, or no service)

---

Quick Start Examples

Python (requests)

python


API_KEY = "your-entity-api-key"
BASE_URL = "https://app.tendrl.com/api"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

# Send a message
resp = requests.post(f"{BASE_URL}/entities/message", headers=HEADERS, json={
    "msg_type": "publish",
    "data": {"temperature": 23.5, "humidity": 65},
    "context": {"tags": ["sensor"]}
})
print(resp.status_code, resp.json())

# Check for incoming messages
resp = requests.get(f"{BASE_URL}/entities/check_messages?limit=5", headers=HEADERS)
messages = resp.json().get("messages", [])
for msg in messages:
    print(f"From {msg['source']}: {msg['data']}")

JavaScript (fetch)

javascript

const API_KEY = "your-entity-api-key";
const BASE_URL = "https://app.tendrl.com/api";
const headers = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
};

// Send a message
await fetch(`${BASE_URL}/entities/message`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    msg_type: "publish",
    data: { temperature: 23.5, humidity: 65 },
    context: { tags: ["sensor"] },
  }),
});

// Check for incoming messages
const resp = await fetch(`${BASE_URL}/entities/check_messages?limit=5`, { headers });
const { messages } = await resp.json();
messages.forEach((msg) => console.log(`From ${msg.source}:`, msg.data));

curl

bash

# Send a message
curl -X POST https://app.tendrl.com/api/entities/message \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"msg_type":"publish","data":{"temperature":23.5},"context":{"tags":["sensor"]}}'

# Send a heartbeat
curl -X POST https://app.tendrl.com/api/entities/message \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"msg_type":"heartbeat","data":{"mem_free":1024,"mem_total":4096,"disk_free":50000,"disk_size":100000}}'

# Check for messages
curl https://app.tendrl.com/api/entities/check_messages?limit=5 \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY"

# Send a batch
curl -X POST https://app.tendrl.com/api/entities/messages \
  -H "Authorization: Bearer YOUR_ENTITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[{"msg_type":"publish","data":{"temp":22.5}},{"msg_type":"publish","data":{"temp":23.1}}]'

---

Error Responses

Status Code Meaning
400 Bad request: invalid message format or unsupported message type
401 Unauthorized: missing or invalid API key
413 Message data exceeds the 5 KB per-message limit (all plans)
429 Too many requests: monthly data limit exceeded. Only telemetry publishes are paused: heartbeats, alerts, and command acknowledgements always flow, so your fleet stays visible. You get an in-app warning at 80% and again when the limit is reached.
500 Internal server error

Error response body:

json

{
  "reason": "Message type 'unknown' is not supported. Allowed types: heartbeat, publish, state_new, state_update, client_cmd_resp."
}