You connect a dispatch or billing system to Polygon. The first job is to pull a dealer account's units and devices into that system. You have a username, a password and a request collection from your account manager. Your first call returns HTTP 200 with an error inside, and the response does not say which check failed.
This post follows one dealer user at Harbor Telematics from sign-in to the first page of units and the first page of devices. Along the way it names each check the API runs, so you can read any error and fix it.
The web app and your integration call the same API. Every request is a POST to one URL. The JSON body names a service and a method in header and carries the method's input in data:
{
"header": {
"service": "unit-service",
"method": "getUnitListV2",
"version": "0.0.1",
"lang": "en",
"timezone": -330,
"token": "<user-token>",
"account": "<account-id>"
},
"data": { "page": 1, "page_size": 10 }
}
Each call passes these checks in order:
authorization HTTP header identifies your client application. The realm decides which methods you may call, and it can restrict calls to one IP address or origin.header.token from a sign-in.header.account, an active account the user belongs to.data against its request schema.The version value must match the server's API version, 0.0.1 today. The lang value must be a language the server supports. The timezone value is an offset in minutes in the browser's getTimezoneOffset() convention, so UTC+05:30 is -330.
A unit is the vehicle your team sees on the map; a device is the hardware inside it. The two lists below are separate calls. See how devices and units relate.
Ask support@getgps.eu or your account manager for the API collection and a realm key for your integration. Import the collection into Postman. It has five folders: Authentication, Account Management, Fleet Units, Devices and Consumers.
Set these collection variables before the first call:
| Variable | Value |
|---|---|
base_url |
Your API address, for example https://api.example.com/api |
api_key |
The realm key you received |
username, password |
The sign-in of a dedicated integration user |
session_id |
A UUID you generate once and keep |
The test scripts fill user_token, bearer_token, account_id, device_id and model_id for you. Then point the collection's Authorization tab at the realm key:
Type: API Key
Key: authorization
Value: bearer {{api_key}}
Add to: Header
The server strips the word bearer before the lookup, so a bare key works too. Keep the realm key and password as secret environment values, and never share an export that contains them.
Run "Sign In". The signin method requires session_id (1 to 40 characters), so add it if your copy of the request lacks it:
{
"header": { "service": "auth-service", "method": "signin", "version": "0.0.1", "lang": "en", "timezone": -330 },
"data": {
"username": "{{username}}",
"password": "{{password}}",
"session_id": "{{session_id}}",
"remember": true
}
}
A trimmed response:
{
"header": { "status": "OK", "correlationId": "<correlation-id>" },
"data": {
"user_token": "<user-token>",
"bearer_token": "<bearer-token>",
"active_account_count": 1,
"accounts": [
{ "account_id": "<dealer-account-id>", "account_name": "Harbor Telematics", "type": "DEALER", "status": "ACTIVE" }
]
}
}
The test script stores both tokens and copies the first active account into account_id. The two tokens do different jobs:
user_token goes in header.token. It expires after 20 minutes without a call, and every authenticated call restarts those 20 minutes.bearer_token comes back only when remember is true, and it lasts two months. "Refresh Session Token" trades it for a new user_token on the same session.Trade-off: a refresh keeps you on one session, while every sign-in with a new session_id opens another one. A user can hold five sessions at once, web app sessions included.
With Harbor Telematics in account_id, run "List Fleet Units". Add an explicit sort:
{
"header": { "service": "unit-service", "method": "getUnitListV2", "version": "0.0.1", "lang": "en", "timezone": -330, "token": "{{user_token}}", "account": "{{account_id}}" },
"data": {
"page": 1,
"page_size": 10,
"sorts": [{ "field": "name", "type": "asc" }],
"filters": []
}
}
{
"header": { "status": "OK", "correlationId": "<correlation-id>" },
"data": {
"pagination": { "total_records": 2, "total_pages": 1, "page": 1, "page_size": 10 },
"list": [
{
"id": "<unit-id>",
"name": "Truck 12",
"status": "ACTIVE",
"motion_state": "parked",
"owner_account": { "id": "<dealer-account-id>", "name": "Harbor Telematics" },
"devices": [{ "id": "<device-id>", "name": "NW-0012" }]
}
]
}
}
To narrow the list, add { "field": "name", "operator": "ilike", "value": "Truck" } to filters. To read a sub-account such as Northwind Logistics, keep your own account in the header and send its id in data.account_ids. The method rejects any id outside your hierarchy.
Next, run "List Devices":
{
"header": { "service": "device-service", "method": "deviceList", "version": "0.0.1", "lang": "en", "timezone": -330, "token": "{{user_token}}", "account": "{{account_id}}" },
"data": { "page": 1, "page_size": 10, "filter": "" }
}
{
"header": { "status": "OK", "correlationId": "<correlation-id>" },
"data": {
"success": true,
"count": "2",
"devices": [
{
"id": "<device-id>",
"device_name": "NW-0012",
"identifier": "<imei>",
"status": "ACTIVE",
"model_id": "<model-id>",
"owner_account_id": "<dealer-account-id>",
"allocated_to": "Northwind Logistics",
"labels": []
}
]
}
}
The two lists page differently. The units list returns a pagination block, and it caps page_size at 500. The devices list returns only count and caps page_size at 200. It always sorts by device name. Its filter is one string, matched against name, IMEI, model, firmware version, status and SIM number.
Both lists return more fields than their published schemas describe. Build your mapping from a real response.
Every failure comes back as HTTP 200 with header.status set to ERROR:
{
"header": { "status": "ERROR", "correlationId": "<correlation-id>" },
"error": {
"code": "POLY_ERR_ACCESSRIGHT",
"title": "Action Prohibited",
"message": "You are not allowed to perform this action. ...",
"category": "POLY_VALIDATION_ERROR"
}
}
Branch on error.code, not on the message, because the message follows header.lang.
| Code | Check that failed | Fix |
|---|---|---|
POLY_ERR_ACCESSDINED |
Realm key missing, unknown, or not granted this method | Check api_key; ask support to grant the method |
POLY_ERR_IP_DENIED, POLY_ERR_HOST_DENIED |
Realm restricted to another IP or origin | Call from the allowed address |
POLY_ERR_ACCESSTOKEN |
Token missing, expired, or issued under another realm | Refresh, or sign in again |
POLY_ERR_ACCOUNT_MISSING |
No header.account |
Set account_id |
POLY_ERR_ACCOUNT_ACCESS_ERROR |
User is not an active member of that account | Use an id from the sign-in accounts list |
POLY_ERR_ACCESSRIGHT |
Role lacks a required permission | Edit the role |
POLY_ERR_DATA_VALIDATION |
data fails the schema |
The message names the field |
POLY_ERR_VERSION |
Wrong version |
Send 0.0.1 |
The role check requires every permission a method lists. The units list needs read access on both Units and Devices; the devices list needs read access on Devices. Role edits apply on the next call.
POLY_ERR_DATA_VALIDATION and a message that names session_id. The collection's body omits the field. Add session_id with a stable UUID.user_token for that session is still alive, so keep using it. Refresh only after it expires.bearer_token in the sign-in response. Your realm honours remember, and you sent false. Sign in again with true if you need to refresh later.session_id, refresh instead of signing in again, and call signout (empty data) when the job ends.sorts the units query has no order. Always send a sort.count arrives as a string. Convert it before you divide by page_size.page, page_size and filterAccount. It returns the whole tree below your account in one response. Filter by name with searchData instead.length of undefined. A few older list methods read filters without checking it. Send filters: [] wherever a method accepts it.