For developers

Connect your fleet to your system.

A REST API and webhooks to get positions, trips and alerts into your rental, logistics or accounting system.

API v1 is live.

Version 1 is stable: we only add fields and endpoints. Breaking changes go to /api/v2. Alerts, geofences, drivers, commands and rentals endpoints are coming next.

The basics

A simple, stable and documented API.

Authentication

Bearer tokens with abilities

Owners and admins create tokens in Panel > Integrations. The token is shown once. Every request sends it as Authorization: Bearer. A token stops working when it is revoked, expires, or its owner loses the matching permission.

  • vehicles:readList and read vehicles
  • vehicles:writeEdit name, plate, group and speed limit
  • positions:readLive state of one or all vehicles
  • history:readPosition history and GPX routes
  • reports:readTrips, stops and summaries
  • webhooks:manageCreate, list and delete webhooks
  • alerts:readAlerts (endpoints coming soon)
  • geofences:writeGeofences (endpoints coming soon)
  • commands:writeDevice commands (endpoints coming soon)
Example: list vehicles
curl "https://gps.appalbania.com/api/v1/vehicles?limit=2" \
  -H "Authorization: Bearer $GPS_TOKEN" \
  -H "Accept: application/json"
{
  "data": [
    {
      "id": 42,
      "name": "Iveco Daily",
      "plate": "AA 123 BB",
      "type": "van",
      "speed_limit_kmh": 90,
      "group": { "id": 3, "name": "Tirana" },
      "device": { "id": 17, "unique_id": "356307042441013", "model": "Teltonika FMB920", "status": "active" },
      "archived": false,
      "updated_at": "2026-10-06T09:41:12Z"
    }
  ],
  "next_cursor": "eyJ2ZWhpY2xlcy5pZCI6NDIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
  "prev_cursor": null,
  "has_more": true,
  "limit": 2
}

Endpoints

Base URL https://gps.appalbania.com/api/v1. Times in ISO-8601 UTC. History ranges: at most 7 days for positions, 31 days for reports.

Method Path Ability Description
GET /me any token The token, its owner and organisation
GET /usage any token Rate limit, requests, vehicles and webhooks counters
GET /vehicles vehicles:read List vehicles (?q, ?group_id, ?archived, ?cursor, ?limit)
GET /vehicles/{id} vehicles:read One vehicle
PATCH /vehicles/{id} vehicles:write Change name, plate, group_id, speed_limit_kmh
GET /states positions:read Live state of all vehicles
GET /vehicles/{id}/state positions:read Live state: lat, lng, speed_kmh, course, ignition, status
GET /vehicles/{id}/positions history:read Positions between ?from and ?to
GET /vehicles/{id}/route.gpx history:read The same track as a GPX file
GET /vehicles/{id}/trips reports:read Trips between ?from and ?to
GET /vehicles/{id}/stops reports:read Stops between ?from and ?to
GET /vehicles/{id}/summary reports:read Distance, speeds, engine hours for a period
GET /webhooks webhooks:manage List webhooks
POST /webhooks webhooks:manage Create a webhook (returns the secret once)
DELETE /webhooks/{id} webhooks:manage Delete a webhook
GET /webhooks/{id}/deliveries webhooks:manage Delivery log, newest first
GET /devices/catalog none Supported device models and capabilities (no token)

Live state

One status per vehicle

status is moving above 5 km/h, parked with ignition off, idle otherwise, and offline when the last fix is older than 10 minutes. You only ever see data from the moment a device was assigned to your organisation.

Example: GET /vehicles/42/state
{
  "data": {
    "vehicle_id": 42,
    "status": "moving",
    "lat": 41.3275,
    "lng": 19.8187,
    "speed_kmh": 54.2,
    "course": 128,
    "altitude": 112.0,
    "ignition": true,
    "fix_time": "2026-10-06T09:41:12Z",
    "device_time": "2026-10-06T09:41:12Z"
  }
}

Errors, pages and limits

Predictable answers

Errors are application/problem+json. Another organisation’s ids answer 404, never 403.

  • 401Missing, wrong, expired or revoked token
  • 403The token lacks the ability (see "ability"), or the IP is not allowed
  • 404Not found, or not yours
  • 422Invalid fields (see "errors"), or a range that is too long
  • 423Organisation is read-only (suspended): only GET works
  • 429Rate limit hit: wait Retry-After seconds
  • 503Tracking data temporarily unavailable: retry shortly

Pagination, limits, retries

  • Lists answer {"data": [...], "next_cursor": "…", "has_more": true}. Pass next_cursor as ?cursor until it is null.
  • X-RateLimit-Limit and X-RateLimit-Remaining on every answer; the limit is per organisation, shared by all its tokens.
  • Send an Idempotency-Key header on POST: the same key and body within 24 hours returns the first answer (Idempotent-Replayed: true).
Example: missing ability
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json

{
  "type": "https://gps.appalbania.com/en/developers#error-forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "This token does not have the history:read ability.",
  "instance": "/api/v1/vehicles/42/positions",
  "ability": "history:read"
}
curl -X POST "https://gps.appalbania.com/api/v1/webhooks" \
  -H "Authorization: Bearer $GPS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-erp-setup" \
  -d '{"url":"https://erp.example.com/gps","events":["alert.created","trip.ended"]}'

Webhooks

Events come to you

Instead of polling, register an HTTPS URL. We POST {id, type, created_at, tenant_id, data} and expect a 2xx answer within 10 seconds.

  • Header X-GPS-Signature: t=<unix>,v1=<hex HMAC-SHA256 of t + "." + raw body> with your secret
  • Reject timestamps older than 5 minutes; use the event id to ignore duplicates
  • Failed deliveries are retried with exponential backoff for up to 24 hours
  • After 20 failures in a row the webhook is disabled and the owner is emailed
  • Private, local and link-local addresses are refused

Events

  • alert.createdNew alert
  • ignition.onIgnition on
  • ignition.offIgnition off
  • trip.startedTrip started
  • trip.endedTrip ended
  • geofence.enterEntered a geofence
  • geofence.exitLeft a geofence
  • vehicle.status_changedStatus changed
  • device.onlineDevice online
  • device.offlineDevice offline
  • command.resultCommand result
  • position.updatedNew position (opt-in, at most every 30 s per vehicle)
Verifying the signature: PHP
// Header: X-GPS-Signature: t=1791279672,v1=5f2b…
[$t, $v1] = sscanf($_SERVER['HTTP_X_GPS_SIGNATURE'] ?? '', 't=%d,v1=%s');
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $t.'.'.$body, getenv('GPS_WEBHOOK_SECRET'));

if (! $v1 || ! hash_equals($expected, $v1) || abs(time() - $t) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true); // $event['type'], $event['data']
http_response_code(204);
Verifying the signature: Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post('/gps/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = Object.fromEntries((req.get('X-GPS-Signature') || '')
    .split(',').map((pair) => pair.split('=')));
  const t = Number(parts.t);
  const expected = crypto.createHmac('sha256', process.env.GPS_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest('hex');
  const valid = typeof parts.v1 === 'string' && parts.v1.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
    && Math.abs(Date.now() / 1000 - t) <= 300;

  if (!valid) return res.sendStatus(401);
  const event = JSON.parse(req.body); // event.type, event.data
  res.sendStatus(204);
});
Verifying the signature: cURL + OpenSSL
# body.json = the raw request body, t and v1 from X-GPS-Signature
t=1791279672
expected=$(printf '%s.%s' "$t" "$(cat body.json)" \
  | openssl dgst -sha256 -hmac "$GPS_WEBHOOK_SECRET" | sed 's/^.* //')
[ "$expected" = "$v1" ] && echo valid || echo invalid

# Replay a captured delivery against your endpoint
curl -X POST https://erp.example.com/gps \
  -H "Content-Type: application/json" \
  -H "X-GPS-Signature: t=$t,v1=$expected" \
  --data-binary @body.json

Recipe for rental systems

How to connect the platform to your rent-a-car software today.

  1. 1

    Map the cars

    GET /v1/vehicles?q=<plate or VIN> once and store the vehicle id in your system.

  2. 2

    Follow the rental

    Subscribe to ignition.on, trip.ended and alert.created, or poll GET /v1/states for the live position.

  3. 3

    Close with the numbers

    At return, GET /v1/vehicles/{id}/summary?from=<pickup>&to=<return> gives the distance and odometer.

Need another endpoint?

Tell us which system you use and what data you need.