Overview
The SGT Systems REST API gives integrators a uniform, JSON-based interface to interact with managed IoT devices, telemetry streams, alarms, and reports across the platform. It is designed around standard HTTP semantics: predictable URLs, conventional verbs (GET, POST, PATCH, DELETE), and meaningful status codes. Every endpoint accepts and returns JSON encoded as UTF-8, and every response carries a unique request identifier for traceability.
The API is intended for three primary audiences:
- Customer integrators pulling telemetry, raising work orders, or syncing devices into their own dashboards.
- OEM partners embedding SGT Systems capabilities (alarming, reporting, edge orchestration) inside their own products.
- Internal services within SGT Systems — automation jobs, the mobile field-service app, and analytics pipelines.
Throughout this reference, examples assume the production base URL and a Bearer-token API key. Sandbox and on-premises deployments follow the identical surface; only the hostname differs.
Authentication
All requests must be authenticated. The API supports two authentication mechanisms:
- API keys — sent as a bearer token in the
Authorizationheader. Best for server-to-server integrations. - OAuth2 (authorization code + PKCE) — for end-user, interactive flows, primarily from partner web apps and the SGT mobile app.
Authorization: Bearer sgt_live_5h7k9w...truncated
X-SGT-Tenant: acme-industries
Content-Type: application/json
Accept: application/json
User-Agent: acme-erp-sync/1.4.2
The X-SGT-Tenant header is required when an API key has multi-tenant scope. For single-tenant keys it may be omitted; if present it must match the key's bound tenant or the request is rejected with 403 forbidden. See the Authentication & API Keys guide for details on issuing, rotating, and scoping keys.
OAuth2 clients exchange an authorization code for an access token at https://auth.sgtsystems.com/oauth2/token. Access tokens are JWTs valid for one hour; refresh tokens are valid for 30 days of inactivity. Both flow types use the same Authorization: Bearer header on subsequent API calls — the API does not distinguish between the two on a per-request basis.
Base URL & Versioning
The production base URL is:
https://api.sgtsystems.com/v1
The sandbox URL is structurally identical:
https://api.sandbox.sgtsystems.com/v1
Versioning is URL-based. Breaking changes will only be introduced under a new major version (/v2, /v3, ...). Within a major version, the platform may add new optional fields, new endpoints, and new event types — clients must be tolerant of unknown fields. Deprecations are announced at least 180 days in advance via the developer changelog and a Deprecation response header with an RFC 9745 sunset date.
What counts as a breaking change on the SGT API:
- Removing or renaming a field, endpoint, or scope.
- Tightening a previously-accepted input (e.g. shortening a max length).
- Changing the meaning or type of an existing field.
What is explicitly not a breaking change and may happen without a version bump:
- Adding optional response fields.
- Adding new endpoints.
- Adding new event types or webhook payload fields.
- Loosening input validation.
Common Response Formats
All successful responses return a JSON object with the following envelope:
{
"data": { ... resource or array ... },
"meta": {
"request_id": "req_01HW3K9F2X...",
"served_by": "api-prod-us-east-2-a4",
"took_ms": 37
}
}
List endpoints additionally include a pagination object inside meta. Errors follow the structure described under Error Codes. Every response, success or error, includes the X-Request-Id header mirrored from meta.request_id — quote this value in any support request and the SGT operations team can usually trace the full lifecycle of the call in seconds.
Resource identifiers in responses use a stable prefix scheme so they are self-describing in logs:
| Prefix | Resource | Example |
|---|---|---|
dev_ | Device | dev_42 |
site_ | Site / location | site_dhaka_wh1 |
alm_ | Alarm instance | alm_01HW3K9F2X |
evt_ | Event | evt_01HW3K9F2X |
rpt_ | Report run | rpt_01HW3K9F2X |
wh_ | Webhook endpoint | wh_01HW3K9F2X |
req_ | Request | req_01HW3K9F2X |
Pagination
List endpoints use cursor-based pagination. Cursors are opaque, URL-safe strings; do not parse them, do not store them long-term, and do not assume anything about their format — the platform reserves the right to change the encoding within a major version. Use the supplied next cursor verbatim.
GET /v1/devices?limit=100&cursor=eyJpZCI6Ijg5...
{
"data": [ { ... }, { ... } ],
"meta": {
"pagination": {
"limit": 100,
"next": "eyJpZCI6IjEwMC...",
"has_more": true
}
}
}
Maximum limit is 500. When has_more is false there are no more pages. The platform deliberately does not expose a total count — for large result sets, computing an exact total would be prohibitively expensive. If you need a count, use a counting endpoint such as GET /devices/count which returns an approximation accurate to within ~1 minute of staleness.
Rate Limiting
Rate limits are enforced per API key, with separate buckets for read and write traffic:
| Bucket | Limit | Window | Burst |
|---|---|---|---|
Read (GET, HEAD) | 600 | per minute | 120 |
Write (POST, PATCH, DELETE) | 120 | per minute | 30 |
| Telemetry ingest | 10,000 | per minute | 2,000 |
| Report runs | 30 | per hour | 5 |
Every response includes the following headers:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 572
X-RateLimit-Reset: 1716549600
When a limit is exceeded the API returns 429 Too Many Requests along with a Retry-After header indicating the number of seconds to wait. The bucket uses a leaky-bucket algorithm with the listed burst capacity — short spikes are absorbed, sustained over-rate traffic is rejected. Clients should implement exponential backoff with jitter, not a fixed-interval retry that will synchronise with other clients and prolong contention.
Error Codes
Error responses use standard HTTP status codes and a consistent JSON body:
{
"error": {
"code": "device_not_found",
"message": "No device with id 'dev_42' exists in tenant 'acme-industries'.",
"details": { "device_id": "dev_42" },
"request_id": "req_01HW3K9F2X..."
}
}
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | invalid_request | Malformed JSON or missing required fields. | No |
| 401 | unauthenticated | Missing or invalid credentials. | No |
| 403 | forbidden | Authenticated, but key lacks the required scope. | No |
| 404 | not_found | Resource does not exist or is not visible to the caller. | No |
| 409 | conflict | State conflict, e.g. duplicate slug or stale If-Match. | No (resolve first) |
| 422 | validation_failed | Semantic validation error; see details. | No |
| 429 | rate_limited | Rate limit exceeded. Honor Retry-After. | Yes, after delay |
| 500 | internal_error | Unexpected server error; safe to retry with backoff. | Yes, with backoff |
| 502 | bad_gateway | Upstream component returned an unexpected response. | Yes, with backoff |
| 503 | unavailable | Temporary outage; retry with exponential backoff. | Yes, with backoff |
| 504 | timeout | Upstream timeout; idempotent calls are safe to retry. | Yes, idempotent only |
Endpoints
Devices
Devices represent any field asset registered with the SGT platform — gateways, PLCs, energy meters, environmental sensors, and so on.
| Method | Path | Purpose |
|---|---|---|
GET | /devices | List devices, with filtering by site, type, and status. |
POST | /devices | Register a new device. |
GET | /devices/{id} | Fetch a single device. |
PATCH | /devices/{id} | Update mutable fields (name, tags, site). |
DELETE | /devices/{id} | Decommission a device. Soft-delete; data retained per retention policy. |
POST | /devices/{id}/commands | Issue an out-of-band command (reboot, refresh config). |
Filterable query parameters on GET /devices: site, type, status (online, offline, provisioning), tag, updated_since (ISO-8601 timestamp). Filters are AND-combined. updated_since is the canonical way to build an incremental sync loop without re-reading the entire fleet on every poll.
Telemetry
Time-series readings from devices. Telemetry is append-only — corrections happen by writing a new reading with the same ts and a flag indicating supersession.
| Method | Path | Purpose |
|---|---|---|
GET | /telemetry | Query readings by device, metric, and time range. |
POST | /telemetry/ingest | Bulk ingest readings (up to 1,000 per request). |
GET | /telemetry/aggregate | Server-side aggregation: avg, min, max, sum, p95, p99. |
GET | /telemetry/export | Generate a signed CSV/Parquet export for a long range. |
Time ranges are inclusive of from and exclusive of to. Maximum window per call is 31 days for raw readings, 366 days for aggregates. Larger windows return 400 invalid_request; use /telemetry/export instead.
Events & Alarms
| Method | Path | Purpose |
|---|---|---|
GET | /events | List events, filterable by type, severity, and acknowledgement status. |
POST | /events/{id}/ack | Acknowledge an alarm. |
POST | /events/{id}/clear | Manually clear an alarm. |
POST | /events/{id}/comment | Attach an operator note for the audit trail. |
Reports
| Method | Path | Purpose |
|---|---|---|
POST | /reports | Trigger an on-demand report run. |
GET | /reports/{id} | Get report status and signed download URL once complete. |
GET | /reports | List recent runs for the tenant. |
Reports are asynchronous. The initial POST returns 202 Accepted with a report ID; the response body indicates the expected completion time. Either poll GET /reports/{id} with a sensible backoff, or subscribe to the report.ready webhook event and wait for the push.
Webhook Subscriptions
| Method | Path | Purpose |
|---|---|---|
GET | /webhooks | List your registered webhook endpoints. |
POST | /webhooks | Register a new endpoint; returns a generated signing secret. |
PATCH | /webhooks/{id} | Update event subscriptions or rotate the secret. |
DELETE | /webhooks/{id} | Unregister an endpoint. |
POST | /webhooks/{id}/test | Send a signed test payload to verify your receiver. |
For payload details and signature verification see Webhooks & Event Streaming.
Code Examples
curl — list devices
curl -sS https://api.sgtsystems.com/v1/devices?limit=20 \
-H "Authorization: Bearer $SGT_API_KEY" \
-H "X-SGT-Tenant: acme-industries" \
-H "User-Agent: acme-erp-sync/1.4.2"
curl — ingest telemetry
curl -sS -X POST https://api.sgtsystems.com/v1/telemetry/ingest \
-H "Authorization: Bearer $SGT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"readings": [
{"device_id":"dev_42","metric":"power_kw","ts":"2026-05-24T10:00:00Z","value":12.7},
{"device_id":"dev_42","metric":"power_kw","ts":"2026-05-24T10:00:05Z","value":12.9}
]
}'
Python — query aggregated telemetry with retry
import os, time, random
import requests
from datetime import datetime, timedelta, timezone
API = "https://api.sgtsystems.com/v1"
headers = {
"Authorization": f"Bearer {os.environ['SGT_API_KEY']}",
"X-SGT-Tenant": "acme-industries",
"User-Agent": "acme-erp-sync/1.4.2",
}
def get_with_retry(url, params, max_attempts=5):
for attempt in range(max_attempts):
r = requests.get(url, headers=headers, params=params, timeout=30)
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", "5"))
time.sleep(wait + random.uniform(0, 1))
continue
if 500 <= r.status_code < 600:
time.sleep(min(60, 2 ** attempt) + random.uniform(0, 1))
continue
r.raise_for_status()
return r.json()
raise RuntimeError("exhausted retries")
end = datetime.now(timezone.utc)
start = end - timedelta(hours=24)
resp = get_with_retry(
f"{API}/telemetry/aggregate",
params={
"device_id": "dev_42",
"metric": "power_kw",
"from": start.isoformat(),
"to": end.isoformat(),
"interval": "15m",
"fn": "avg",
},
)
for point in resp["data"]:
print(point["ts"], point["value"])
Python — paginate through all devices
def iter_devices():
cursor = None
while True:
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
page = get_with_retry(f"{API}/devices", params=params)
for d in page["data"]:
yield d
pg = page["meta"]["pagination"]
if not pg["has_more"]:
break
cursor = pg["next"]
for device in iter_devices():
print(device["id"], device["name"], device["status"])
User-Agent identifying your integration (e.g. acme-erp-sync/1.4.2). It helps SGT Systems support staff quickly diagnose issues affecting a specific client and gives you a foothold for asking about the rollout of new API features.
Versioning of Resources
Mutable resources (devices, webhooks, alarm rules) carry an etag. To make a safe update, include the etag in an If-Match header. If the resource changed in the meantime, the server returns 409 conflict and you can re-read and retry. This prevents lost-update races between multiple concurrent integrators.
Need help?
Questions, edge cases, or feature requests? Reach the SGT Systems integrations team at support@sgtsystems.com or via the contact page. When opening a ticket, include the request_id from any failing response — it lets us trace the call end-to-end across our infrastructure.