Quick Start
Get from a board in your hand to its data in the dashboard in a few minutes, with nothing installed on your computer. No board yet? Send a message from the command line instead.
tendrl-dev-mcp lets your AI assistant flash, provision, and upload code to the board for you. The steps below are the same flow, done by hand in the browser.
Prerequisites#
- A Tendrl account (sign up at tendrl.com)
- A board: an ESP32 (including S2, S3, and C3), a Raspberry Pi Pico W, or an OpenMV camera, and a USB data cable
- Chrome, Edge, Opera, or Firefox 151+ on a desktop. The browser talks to the board over WebSerial, which Safari and iOS don't implement.
Step 1: Create an entity#
An entity is Contact's record of one device.
- Navigate to Entities in the sidebar
- Click Create Entity
- Fill in the details:
- Name: A unique identifier (e.g.,
bench-esp32) - Role:
DefaultEntityis fine for sending messages - Uses MQTT: turn this on. Boards talk to Contact over MQTT, and the entity's Device tab needs it.
When you create the entity, Contact automatically issues an API key bound to it.
Creating an entity from the dashboard.
Step 2: Copy the API key#
Contact shows the Connection Instructions dialog as soon as the entity is created. Reveal the API Key Secret with the eye icon, or use the copy button.
Connection Instructions, shown once when the entity is created.
The secret is shown once. If you lose it, the provisioning form in Step 4 can Rotate it for you.
Step 3: Connect the board#
- Open the entity and switch to its Device tab
- Plug the board into USB and click Connect Device
- Pick the board in the browser's port picker
The tab reads the board's platform, free memory, and any SDK already installed, and recommends an install tier. See Connecting a Board.
A brand-new ESP32 usually isn't running MicroPython yet. Flash it from the same tab: Flashing MicroPython takes about a minute, still in the browser. A Pico W takes MicroPython as a UF2 drag-and-drop, and OpenMV boards are flashed with the OpenMV IDE.
Step 4: Provision#
In the provisioning form:
- Paste the entity's API key from Step 2
- Enter your Wi-Fi SSID and password. Most boards are 2.4 GHz only. The password goes down the USB cable; Contact never stores it.
- On ESP32, keep Tendrl firmware, the low-RAM install. If the board needs it, provisioning reflashes and asks you to unplug and replug once.
- Click Provision
Provisioning installs the SDK, writes /config.json, writes a starter main.py if the board has none, and resets the board. Open the Terminal to watch it join Wi-Fi and connect. Full details: Provisioning a Device.
Step 5: Send data#
The starter main.py connects the client and idles. Make it publish something: open main.py in the Device tab's Files view (/flash/main.py on OpenMV), replace it with this, save with ⌘S / Ctrl-S, and click ▶ Run.
from tendrl import Client
client = Client(debug=True)
client.start()
while True:
gc.collect()
client.publish({"free_heap": gc.mem_free()}, tags=["sensor"])
time.sleep(10)
Swap gc.mem_free() for a real sensor reading when you have one wired up. tags is what routes a message to connectors and Strand workflows.
Step 6: See it in the dashboard#
- Navigate to Messages in the sidebar
- Your board's messages appear every ten seconds
- Click one to view its details
The entity also shows as online. If nothing arrives, Device troubleshooting covers the usual causes: Wi-Fi on a 5 GHz-only network, a wrong key, or a board out of memory.
No board? Send from the command line#
Do Steps 1 and 2 (the Uses MQTT switch doesn't matter here), then publish with the entity's key:
curl -X POST https://app.tendrl.com/api/entities/message \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"msg_type": "publish",
"data": {
"temperature": 23.5,
"humidity": 65
},
"context": {"tags": ["sensor"]}
}'
A success returns 200 with the stored message id:
{"code": 200, "content": "01J8XR4M2K9P7QW3FDNVBZ0HYT"}
msg_type is required (publish, heartbeat, state_new, or state_update). Then check Messages as in Step 6.
If it didn't work#
Match the reason in the response body against this table before anything else — the status code alone doesn't distinguish a wrong key from a wrong kind of key.
| Status | reason |
What it means |
|---|---|---|
| 401 | Access Denied |
The key is wrong or revoked. The Bearer prefix is optional; a bare token is accepted. |
| 401 | Only entities can write messages |
You used an account-level API key. Messages must be sent with the key issued to the entity — the one under its Connection Instructions, not one from Access Control → API Keys. |
| 403 | Access Denied (in a message field, not reason) |
The entity's role lacks entity:WriteMessages. DefaultEntity has it; a custom role may not. |
| 400 | Message type is required |
msg_type is missing from the body. |
| 400 | Invalid message format |
The JSON is malformed, or data isn't an object. |
| 413 | Message size exceeds the 5KB per message limit. |
Trim the payload; the cap is per message and applies on every plan. |
| 429 | Monthly data limit exceeded. Your <plan> plan includes … |
You've hit the plan's data cap for the billing period. The rest of the sentence names what the plans above include, with price. |
The most common first-run failure is the third row. Both key types are valid and both look identical, but only the entity's own key can publish on its behalf.
Next Steps#
- MicroPython SDK —
@tetherfor sensor loops, offline queueing, commands to the device - The Device Tab — terminal, file editor, camera, and remote deployments
- SDKs & Clients overview — Python, Go, JavaScript, and the nano-agent
- Add an Alert so you hear about it when a device stops reporting
- Build a Data Flow to route messages to Slack and other systems
- Set up Message Validation to ensure data quality
Tendrl