Version: 2.0
Base URL (production): https://terradetect.onrender.com
Base URL (local dev): http://localhost:8080
All API endpoints are prefixed with /api/v1/. The single exception is the
ESP32 data ingestion endpoint which keeps its legacy path /api/esp32 to
avoid reflashing all deployed devices.
All requests and responses use application/json unless noted otherwise.
All protected endpoints require a JWT access token in the Authorization
header:
Authorization: Bearer <access_token>
Tokens are issued by the login endpoint and expire after 15 minutes. Use the refresh endpoint to get a new access token without re-authenticating.
Every endpoint that fails returns this shape. No endpoint deviates from it.
{
"error": {
"code": "DEVICE_NOT_FOUND",
"message": "No device with that ID exists."
}
}| Code | HTTP Status | Meaning |
|---|---|---|
UNAUTHORIZED |
401 | Missing or invalid JWT |
FORBIDDEN |
403 | Valid JWT but insufficient permission |
NOT_FOUND |
404 | Resource does not exist |
VALIDATION_ERROR |
422 | Request body failed validation |
INVALID_CREDENTIALS |
401 | Wrong username/password/device_id |
DEVICE_NOT_REGISTERED |
403 | Device ID exists but is unregistered |
DEVICE_ALREADY_REGISTERED |
409 | Device ID already claimed by a user |
INVALID_API_KEY |
401 | ESP32 API key does not match device |
RATE_LIMITED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Unexpected server error |
| Endpoint group | Limit |
|---|---|
POST /api/v1/auth/login |
5 requests / minute / IP |
POST /api/v1/auth/register |
3 requests / minute / IP |
POST /api/esp32 |
2 requests / second / device_id |
| All other endpoints | 60 requests / minute / user |
Rate limited responses return HTTP 429 with a Retry-After header
(seconds until the limit resets).
POST /api/v1/auth/register
Creates a new user account. The device_id field is optional. If a
device_id is provided it must already exist in the database with
registered: false (pre-provisioned by an admin). When a valid
device_id is provided the device will be claimed (set to
registered: true) and its api_key will be returned once. If no
device_id is provided the account is created without associating a
device; the user or an admin may claim a device later.
Request (with device)
{
"username": "gagan",
"password": "min8chars",
"device_id": "ABC123"
}Request (without device)
{
"username": "gagan",
"password": "min8chars"
}| Field | Type | Rules |
|---|---|---|
username |
string | 3β32 chars, alphanumeric + underscores |
password |
string | min 8 chars |
device_id |
string | optional; if provided must be exactly 6 chars and unregistered |
Response 201 (device claimed)
{
"username": "gagan",
"device_id": "ABC123",
"api_key": "95b09474fa652e53...4d4f19f2"
}Response 201 (no device provided)
{
"username": "gagan",
"device_id": ""
}
β οΈ When present,api_keyis returned only once (on successful registration that claims a device). The client must store it securely β it cannot be retrieved again and must be reset by an admin if lost.
POST /api/v1/auth/login
Authenticates a user and returns JWT tokens.
Request
{
"username": "gagan",
"password": "min8chars",
"device_id": "ABC123"
}Response 200
{
"access_token": "eyJhbGci....",
"refresh_token": "eyJhbGci....",
"expires_in": 900,
"token_type": "Bearer",
"user": {
"username": "gagan",
"device_id": "ABC123"
}
}| Field | Description |
|---|---|
access_token |
Short-lived JWT β 15 minutes. Send in Authorization header. |
refresh_token |
Long-lived JWT β 30 days. Store in secure storage only. |
expires_in |
Seconds until access token expires (always 900). |
POST /api/v1/auth/refresh
Issues a new access token using a valid refresh token. Does not require
the Authorization header β uses the refresh token directly.
Request
{
"refresh_token": "eyJhbGci...."
}Response 200
{
"access_token": "eyJhbGci....",
"expires_in": 900,
"token_type": "Bearer"
}If the refresh token is expired or invalid, returns 401 UNAUTHORIZED.
The client should redirect to login.
POST /api/v1/auth/logout
π Requires JWT
Invalidates the refresh token server-side (added to a denylist in MongoDB). The access token will still work until it naturally expires (max 15 min), which is acceptable.
Request β empty body {}
Response 200
{
"message": "Logged out successfully."
}POST /api/v1/device/check
Used by the ESP32 on boot to verify its device ID is registered in the
system before attempting data uploads. No auth required β the API key
check on /api/esp32 is the actual security gate.
Request
{
"device_id": "ABC123"
}Response 200
{
"registered": true
}registered: false means the device ID exists but hasn't been claimed
by a user yet. 404 NOT_FOUND means the device ID doesn't exist at all.
POST /api/esp32
β οΈ This endpoint intentionally keeps its legacy path to avoid reflashing deployed ESP32 devices.
Receives sensor readings from an ESP32. Authenticated via per-device
API key in the x-api-key header β not JWT.
Headers
x-api-key: 95b09474fa652e53...4d4f19f2
Content-Type: application/json
Request
{
"device_id": "ABC123",
"temperature": 28.45,
"ph": 6.72,
"humidity": 63.1,
"ec": 1.34,
"N": 48.2,
"P": 31.5,
"K": 39.8,
"moisture": 57.3
}| Field | Type | Unit | Required |
|---|---|---|---|
device_id |
string | β | β |
temperature |
float | Β°C | β |
ph |
float | 0β14 | β |
humidity |
float | % | β |
ec |
float | ΞΌS/cm | β (default 0) |
N |
float | kg/ha | β (default 0) |
P |
float | kg/ha | β (default 0) |
K |
float | kg/ha | β (default 0) |
moisture |
float | % | β (default 40) |
Response 200
{
"status": "success",
"message": "Sensor data received.",
"timestamp": "2025-07-12T14:32:01Z"
}GET /api/v1/sensor/latest
π Requires JWT
Returns the most recent sensor document for the authenticated user's device.
Response 200
{
"data": {
"temperature": 28.45,
"ph": 6.72,
"humidity": 63.1,
"ec": 1.34,
"N": 48.2,
"P": 31.5,
"K": 39.8,
"moisture": 57.3
},
"timestamp": "2025-07-12T14:32:01Z",
"source": "esp32"
}404 NOT_FOUND if no data has been received for this device yet.
GET /api/v1/sensor/history
π Requires JWT
Returns paginated historical sensor readings for the authenticated user's device, sorted newest-first.
Query Parameters
| Param | Type | Default | Max |
|---|---|---|---|
page |
int | 1 | β |
per_page |
int | 10 | 100 |
Example: GET /api/v1/sensor/history?page=2&per_page=20
Response 200
{
"history": [
{
"temperature": 28.45,
"ph": 6.72,
"humidity": 63.1,
"ec": 1.34,
"N": 48.2,
"P": 31.5,
"K": 39.8,
"moisture": 57.3,
"timestamp": "2025-07-12T14:32:01Z"
}
],
"pagination": {
"total": 142,
"page": 2,
"per_page": 20,
"total_pages": 8
}
}All prediction endpoints are protected by JWT. They accept either manual input (user types values) or sensor data (values pulled from the latest ESP32 reading).
POST /api/v1/predict/crop
π Requires JWT
Returns the best-matching crop and a ranked suitability list for the given soil conditions.
Request β manual input
{
"source": "manual",
"N": 48.0,
"P": 31.0,
"K": 40.0,
"temperature": 28.5,
"humidity": 63.0,
"ph": 6.7,
"rainfall": 120.0
}Request β from sensor
{
"source": "sensor",
"rainfall": 120.0
}When source is "sensor", the server fetches the latest reading for
the authenticated user's device and uses it. rainfall is always manual
since the ESP32 does not measure it.
| Field | Type | Unit | Required (manual) |
|---|---|---|---|
N |
float | kg/ha | β |
P |
float | kg/ha | β |
K |
float | kg/ha | β |
temperature |
float | Β°C | β |
humidity |
float | % | β |
ph |
float | 0β14 | β |
rainfall |
float | mm | β (both modes) |
Response 200
{
"recommended_crop": "rice",
"confidence": 91.4,
"model_prediction": "rice",
"model_confidence": 88.2
}| Field | Description |
|---|---|
recommended_crop |
Best crop by suitability score calculation |
confidence |
Suitability score (0β100) |
model_prediction |
Raw ML model output |
model_confidence |
ML model's probability (0β100) |
POST /api/v1/predict/suitability
π Requires JWT
Analyzes how well current soil conditions match a specific crop's ideal parameters. Returns a score and per-parameter adjustment table.
Request
{
"source": "manual",
"crop_name": "wheat",
"N": 48.0,
"P": 31.0,
"K": 40.0,
"temperature": 28.5,
"humidity": 63.0,
"ph": 6.7,
"rainfall": 120.0
}Response 200
{
"crop": "wheat",
"suitability_score": 74.3,
"recommendations": [
"Nitrogen (N) is too low (Current: 48.0, Ideal: 60.0). Increase by 12.0."
],
"table": [
{
"parameter": "Nitrogen (N)",
"recommended": 60.0,
"observed": 48.0,
"status": "low",
"remarks": "Too low. Increase by 12.0. Apply nitrogen-rich fertilizers like urea or ammonium sulfate."
},
{
"parameter": "pH",
"recommended": 6.5,
"observed": 6.7,
"status": "optimal",
"remarks": "Optimal"
}
]
}status is one of: "optimal", "low", "high".
POST /api/v1/predict/fertilizer
π Requires JWT
Recommends a fertilizer based on soil conditions and target crop.
Request
{
"source": "manual",
"crop_name": "wheat",
"soil_type": "Black",
"N": 48.0,
"P": 31.0,
"K": 40.0,
"temperature": 28.5,
"humidity": 63.0,
"ph": 6.7,
"rainfall": 120.0,
"moisture": 57.0
}soil_type must be one of: "Black", "Clayey", "Loamy", "Red", "Sandy".
Response 200
{
"fertilizer": "Urea",
"composition": "46-0-0",
"deficiencies": {
"N": 2.0,
"P": 9.0,
"K": 0.0
},
"rationale": "Recommended based on soil and crop requirements.",
"application": "Apply in split doses - half at planting and half during vegetative growth. For cereals, incorporate into soil before planting.",
"nitrogen_advice": "Add 2.0 kg/ha of nitrogen using Urea or similar.",
"phosphorus_advice": "Add 9.0 kg/ha of phosphorus using DAP or similar."
}GET /api/v1/weather
π Requires JWT
Proxies a WeatherAPI.com request server-side. The API key never reaches the client. Returns only the fields the app needs.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
lat |
float | β | Latitude |
lon |
float | β | Longitude |
Example: GET /api/v1/weather?lat=23.18&lon=75.77
Response 200
{
"temperature": 34.2,
"humidity": 58.0,
"rainfall_mm": 0.0,
"condition": "Partly cloudy",
"location": "Ujjain, Madhya Pradesh"
}rainfall_mm is today's precipitation total. Returns 0.0 if no rain.
This section documents what the firmware must send and how it must
authenticate. It is the implementation contract for sketches/.
1. Load device_id from EEPROM
2. Connect WiFi via WiFiManager
3. POST /api/v1/device/check β confirm registered: true
4. If not registered β blink error, reset WiFiManager, restart
5. Begin sensor loop
POST /api/esp32
Headers:
x-api-key: <device api key>
Content-Type: application/json
Body: sensor payload (see Ingest ESP32 Data above)
The firmware must pin the Let's Encrypt ISRG Root X1 certificate.
client.setInsecure() must not be used in production builds.
The root CA PEM should be stored in sketches/secrets.h (gitignored).
A template file sketches/secrets.h.template is committed with
placeholder values.
For Go backend implementation β these are the exact collection shapes.
{
"_id": "ObjectId",
"username": "gagan",
"password_hash": "pbkdf2:sha256:...",
"device_id": "ABC123",
"created_at": "ISODate"
}{
"_id": "ObjectId",
"device_id": "ABC123",
"api_key": "95b09474fa652e53...4d4f19f2",
"registered": true,
"created_at": "ISODate"
}{
"_id": "ObjectId",
"device_id": "ABC123",
"temperature": 28.45,
"ph": 6.72,
"humidity": 63.1,
"ec": 1.34,
"N": 48.2,
"P": 31.5,
"K": 39.8,
"moisture": 57.3,
"timestamp": "ISODate"
}Index required: { device_id: 1, timestamp: -1 } β used by both
/sensor/latest (limit 1) and /sensor/history (paginated).
{
"_id": "ObjectId",
"token_hash": "sha256 of the refresh token",
"expires_at": "ISODate"
}TTL index on expires_at so MongoDB auto-purges expired entries.
| Version | Change |
|---|---|
| 2.0 | JWT auth replacing Flask sessions; /api/v1/ prefix; weather proxy; refresh tokens; standardized errors |
| 1.0 | Original Python Flask implementation |