Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ricardomb-tech/surqo/llms.txt

Use this file to discover all available pages before exploring further.

Each Surqo ESP32 node goes through a four-stage lifecycle before it begins contributing sensor data: it registers itself on boot, becomes discoverable in the UI, gets claimed by a user to a specific farm, and then routes all future readings to that farm automatically — whether they arrive over MQTT or via the HTTP fallback. No manual device configuration is needed beyond connecting the node to WiFi and powering it on.

Device Pairing Flow

1

ESP32 boots and registers

The node calls POST /api/v1/devices/register with its MAC address and firmware version. If no record exists for that MAC a new device is created; if it already exists, last_seen is updated.
2

Claim code is derived

The backend derives a claim_code from the last 6 characters of the normalized MAC address (e.g. MAC a4:cf:12:a3:f2:c1 → claim code A3F2C1). This code is returned to the node and printed to the serial monitor.
3

Device appears as discoverable

For 20 minutes after the last register call the device appears in GET /api/v1/devices/discoverable. The frontend polls this endpoint to enable zero-code auto-discovery: power on the node and it appears automatically in the dashboard.
4

User claims the device

The user calls POST /api/v1/devices/claim with the claim_code and a farm_id. All future MQTT readings from that MAC are routed to the paired farm.

Endpoints

POST /api/v1/devices/register

Public endpoint — called by the ESP32 on every boot. Rate limit: 30 requests/minute per IP. Creates a device record if the MAC is new. If the device already exists, only last_seen and (optionally) firmware_version are updated.
device_mac
string
required
Raw MAC address in any standard format (colons, hyphens, or plain hex). The backend normalizes it to lowercase plain hex (e.g. a4cf12a3f2c1). Maximum 32 characters. Returns 400 if the normalized result is shorter than 6 characters.
firmware_version
string
Firmware version string (e.g. "2.0.0"). Maximum 20 characters.
Response — 200 OK
{"claim_code": "A3F2C1", "is_claimed": false}
claim_code
string
Uppercase 6-character code the user enters to pair this node to a farm.
is_claimed
boolean
true if the device is already paired to a farm.

GET /api/v1/devices/

Auth required. Returns the list of devices paired to the authenticated user’s account. Response — 200 OKlist[DeviceResponse]

GET /api/v1/devices/discoverable

Auth required. Returns unpaired devices (farm_id is null) that have been seen within the last 20 minutes, ordered by last_seen descending. This endpoint powers the auto-discovery UX: when a user powers on a new node it appears in the list automatically — no code entry required until the claim step. Response — 200 OKlist[DeviceResponse]

POST /api/v1/devices/claim

Auth required. Pairs a device to one of the user’s farms. Rate limit: 20 requests/minute per IP.
claim_code
string
required
Uppercase claim code displayed on the ESP32 serial monitor or shown in the app. Between 4 and 12 characters. The backend strips whitespace and uppercases automatically.
farm_id
string (UUID)
required
UUID of the farm to pair the device to. The user must own this farm.
name
string
Optional human-readable nickname for the node (e.g. "Nodo Norte"). Maximum 200 characters. Defaults to "Nodo {claim_code}" if not provided.
Response — 201 CreatedDeviceResponse Error responses
StatusCondition
403farm_id not found or not owned by the user
404No device found with this claim code — verify the node is powered on
409Device already claimed by a different user account

PATCH /api/v1/devices/{device_id}

Auth required. Rename a paired device.
device_id
string (UUID)
required
UUID of the device record. Returns 404 if not found or not owned by the user.
name
string
required
New display name for the device. Maximum 200 characters.
Response — 200 OK — Updated DeviceResponse

DELETE /api/v1/devices/{device_id}

Auth required. Unlinks the device from the user’s account and farm. After this call the device has no user_id or farm_id and will re-appear in the discoverable list the next time it boots and calls /register.
device_id
string (UUID)
required
UUID of the device to unpair. Returns 404 if not found or not owned by the user.
Response — 204 No Content

POST /api/v1/devices/{device_mac}/reading

Public endpoint — ESP32 HTTP fallback. Rate limit: 120 requests/minute per IP. Called by the ESP32 when MQTT is unavailable. The backend resolves which farm to write data to using the device’s MAC. If the device is not yet claimed the reading is discarded and the claim code is returned so the user can pair it.
device_mac
string
required
Raw MAC address of the device. Normalized the same way as in /register.
The request body is the same sensor reading JSON used by the MQTT consumer. Include firmware_version to keep the device record updated. Response — 202 Accepted
{"status": "ok"}
Or, if the device is not yet paired:
{"status": "unclaimed", "claim_code": "A3F2C1"}

Device Object

{
  "id": "uuid-device",
  "device_mac": "a4cf12a3f2c1",
  "claim_code": "A3F2C1",
  "name": "Nodo Norte",
  "farm_id": "uuid-finca",
  "firmware_version": "2.0.0",
  "last_seen": "2026-06-28T14:35:00Z",
  "created_at": "2026-06-01T00:00:00Z"
}
The discovery window is 20 minutes — deliberately longer than the ESP32’s default deep sleep cycle of 15 minutes, so a node that woke up once and went back to sleep is still visible in the UI when the user opens the dashboard.

Build docs developers (and LLMs) love