Lokkate API

Read your fleet from your own systems, and get its events pushed to you as they happen.

Keys are made in Developers, in the sidebar once you sign in as the account owner. No account yet? Ask us for a trial key on our demo fleet.

Base URL
https://lokkate.com/api/v1
Try it
curl https://lokkate.com/api/v1/vehicles -H "Authorization: Bearer YOUR_KEY"

The API is for businesses that want Lokkate data in their own software: a dispatch tool, an ERP, a customer portal or a spreadsheet. With it you can:

  • Read your fleet: vehicles and where they are, their position history, trips, events, and the readings of cargo sensors.
  • Get events pushed: a webhook calls your server when a trip ends, a vehicle leaves a geofence, a load gets too warm, and more.

The API reads data. It cannot change anything in your account yet. Everything here comes from one OpenAPI 3.1 file, which you can load into Postman, Insomnia or a code generator.

Quick start

  1. Open Developers

    Sign in to lokkate.com on a computer as the account owner and open Developers in the sidebar.

  2. Create a key

    Under Keys, create a key. It starts with lk_ and is shown once, so store it somewhere safe, like a password manager or your server's secrets.

  3. Call the API

    Request
    curl "https://lokkate.com/api/v1/vehicles" \
      -H "Authorization: Bearer $LOKKATE_KEY"

    You get your vehicles back as JSON. Every list has the records in data and paging facts in meta. Every response also has an X-Request-Id header; keep it in your logs.

    Response, shortened
    {
      "data": [
        {
          "id": 1042,
          "plate": "GT 4415-22",
          "online": true,
          "last_position": { "latitude": 5.6698, "longitude": -0.0166, "speed_kmh": 46, "state": "moving" }
        }
      ],
      "meta": { "page": 1, "pages": 1, "count": 1, "next_page": null }
    }

No vehicles yet? Ask us for a trial key on our demo fleet: three cars driving around Accra, so you can build against live data straight away. Each company gets its own key. Ask on WhatsApp.

Authentication

Send your key in the Authorization header on every request:

Header
Authorization: Bearer lk_...
  • A key reads everything on your account that you can see in Lokkate. Keep it on your server, never in a web page or a mobile app. The API does not answer browsers (no CORS).
  • You can have several keys at once. To rotate, create a new key, move your systems to it, then revoke the old one.
  • We only store a fingerprint of each key, so we cannot show it to you again. If you lose one, revoke it and create another.
  • HTTPS only. A missing, wrong or revoked key is 401 invalid_api_key.

Errors

Every error has the same shape. Branch on code; message is for people and may change.

400 Bad Request
{
  "error": {
    "code": "invalid_parameter",
    "message": "limit must be from 1 to 200; colour is not allowed",
    "details": [
      { "param": "limit", "message": "must be from 1 to 200" },
      { "param": "colour", "message": "is not allowed" }
    ]
  }
}

Only invalid_parameter has details, one entry per problem, so you can fix them all in one go. Unknown parameters are refused rather than ignored, so a typo never quietly returns everything.

StatusCodeWhen
400invalid_parameterA parameter is unknown, malformed or out of range. Read details.
401invalid_api_keyThe key is missing, wrong or revoked.
402account_inactiveTracking is paused on the account. Contact Lokkate.
404not_foundNo such record on this account, or no such endpoint.
429rate_limitedToo many requests this minute. Wait Retry-After seconds.
500internal_errorOur fault. Quote X-Request-Id.

Rate limits

Each key can make 120 requests a minute. The minute is a clock minute, not a rolling one. Every response tells you where you stand:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute: 120.
X-RateLimit-RemainingRequests left this minute.
X-RateLimit-ResetWhen this minute ends, in Unix seconds.
Retry-AfterOn a 429 only: seconds to wait before trying again.

Over the limit is 429 rate_limited. Wait Retry-After seconds and try again. Too many bad keys from one IP address in a minute are also answered with 429 until the minute ends. For live positions, use the positions.batch webhook instead of polling.

Paging

Lists come back in one of three ways.

Pages

Vehicles, trips, events and sensors take page (from 1) and limit (1 to 200, default 50). meta has page, pages, count and next_page, which is null on the last page. Asking for a page past the end is 400.

Syncing events with after_id

Send after_id to GET /events to read every event exactly once, oldest first. meta then has next_after_id and has_more. See the guide below.

Time windows

Positions and sensor readings take a from and to window, oldest first by recorded_at. Times are ISO 8601 with a time zone, like 2026-10-11T09:00:00Z; a time without one is 400. Leave out to for now, and from for the default window before to.

  • Positions: 1 hour by default, at most 24 hours, up to 5,000 per call.
  • Sensor readings: 24 hours by default, at most 7 days, up to 10,000 per call.

When a window holds more than one call returns, meta.truncated is true. Call again with meta.next_from as from and the same to. next_from is inclusive, so rows at that second come back again: dedupe on recorded_at.

Link header

As a convenience, lists also send a Link header with the full URL of the next call, keeping every other parameter you sent. Pages link first, prev, next and last. No more to fetch means no Link header. meta stays the source of truth.

Growth

Within v1 we only add things: new fields, endpoints, event types and enum values. Ignore anything you do not know, and your integration keeps working. Every documented field is always present, and null when unknown. Times are UTC ending in Z, ids are integers, and units are in the field name, like speed_kmh.

Reference

Every endpoint is a GET on https://lokkate.com/api/v1 and needs a key.

GET /vehicles

List vehicles

Every vehicle you can see, oldest first by id, with its driver, last position and fitted sensors. Archived vehicles and vehicles with unpaid charges are left out.

Parameters

NameTypeDefaultDescription
pageinteger1The page to return. A page past the last one is 400.1 or more
limitinteger50Results per page.from 1 to 200
Example request
curl "https://lokkate.com/api/v1/vehicles" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": [
    {
      "id": 1042,
      "kind": "vehicle",
      "name": "Tema reefer",
      "plate": "GT 4415-22",
      "vin": "JTFSS22P900123456",
      "make": "Toyota",
      "model": "Dyna",
      "year": 2019,
      "status": "active",
      "online": true,
      "driver": {
        "id": 311,
        "name": "Ama Owusu"
      },
      "last_position": {
        "recorded_at": "2026-10-11T09:41:12Z",
        "latitude": 5.6698,
        "longitude": -0.0166,
        "speed_kmh": 46,
        "course": 92.0,
        "state": "moving"
      },
      "sensors": [
        {
          "id": 77,
          "name": "Rear freezer",
          "vehicle_id": 1042,
          "status": "reporting",
          "target_range": {
            "min_celsius": -20.0,
            "max_celsius": -15.0
          },
          "last_reading_at": "2026-10-11T09:40:58Z",
          "last_temperature_celsius": -17.4
        }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "pages": 1,
    "count": 1,
    "next_page": null
  }
}
GET /vehicles/{id}

Get a vehicle

One vehicle, in the same shape as the list. A vehicle you cannot see is 404.

Parameters

NameTypeDefaultDescription
idrequiredin the pathintegerThe vehicle id.
Example request
curl "https://lokkate.com/api/v1/vehicles/1042" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": {
    "id": 1042,
    "kind": "vehicle",
    "name": "Tema reefer",
    "plate": "GT 4415-22",
    "vin": "JTFSS22P900123456",
    "make": "Toyota",
    "model": "Dyna",
    "year": 2019,
    "status": "active",
    "online": false,
    "driver": null,
    "last_position": {
      "recorded_at": "2026-10-11T07:12:40Z",
      "latitude": 5.6365,
      "longitude": -0.0167,
      "speed_kmh": 0,
      "course": 0.0,
      "state": "parked"
    },
    "sensors": []
  }
}
GET /vehicles/{vehicle_id}/positions

List a vehicle's positions

The positions a vehicle reported in a time window, oldest first. The window is 1 hour before to by default and at most 24 hours long. One call returns up to 5,000 positions; when there are more, meta.truncated is true and you call again with meta.next_from as from and the same to. next_from is inclusive, so dedupe on recorded_at.

Parameters

NameTypeDefaultDescription
vehicle_idrequiredin the pathintegerThe vehicle id.
fromtimeStart of the window, inclusive. ISO 8601 with a time zone. Defaults to the default window before to.ISO 8601 with a time zone
totimeEnd of the window, inclusive. ISO 8601 with a time zone. Defaults to now.ISO 8601 with a time zone
Example request
curl "https://lokkate.com/api/v1/vehicles/1042/positions" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": [
    {
      "recorded_at": "2026-10-11T08:00:04Z",
      "latitude": 5.6037,
      "longitude": -0.187,
      "speed_kmh": 0,
      "course": 0.0,
      "state": "idling",
      "ignition": true
    },
    {
      "recorded_at": "2026-10-11T08:00:34Z",
      "latitude": 5.6051,
      "longitude": -0.1841,
      "speed_kmh": 34,
      "course": 61.0,
      "state": "moving",
      "ignition": true
    }
  ],
  "meta": {
    "from": "2026-10-11T08:00:00Z",
    "to": "2026-10-11T09:00:00Z",
    "truncated": false,
    "next_from": null
  }
}
GET /trips

List trips

Trips newest first. A trip starts when the engine goes on and is completed when it goes off. A trip changes after it is created, so there is no after_id: sync completed trips from the trip.completed webhook, or by from and to windows on started_at.

Parameters

NameTypeDefaultDescription
pageinteger1The page to return. A page past the last one is 400.1 or more
limitinteger50Results per page.from 1 to 200
vehicle_idintegerOnly this vehicle. A vehicle you cannot see is 404.
statusstringOnly trips in this state.one of: active, completed
fromtimeOnly trips that started at or after this time. ISO 8601 with a time zone.ISO 8601 with a time zone
totimeOnly trips that started at or before this time. ISO 8601 with a time zone. Must be after from.ISO 8601 with a time zone
Example request
curl "https://lokkate.com/api/v1/trips" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": [
    {
      "id": 9120,
      "vehicle_id": 1042,
      "status": "completed",
      "started_at": "2026-10-11T06:58:10Z",
      "ended_at": "2026-10-11T07:42:55Z",
      "duration_seconds": 2685,
      "distance_km": 28.4,
      "max_speed_kmh": 78,
      "moving_seconds": 2110,
      "idle_seconds": 575,
      "stops": 3,
      "start": {
        "latitude": 5.6037,
        "longitude": -0.187
      },
      "end": {
        "latitude": 5.6698,
        "longitude": -0.0166
      },
      "driver": {
        "id": 311,
        "name": "Ama Owusu"
      }
    }
  ],
  "meta": {
    "page": 1,
    "pages": 4,
    "count": 172,
    "next_page": 2
  }
}
GET /trips/{id}

Get a trip

One trip. An active trip has ended_at, duration_seconds and distance_km set to null.

Parameters

NameTypeDefaultDescription
idrequiredin the pathintegerThe trip id.
Example request
curl "https://lokkate.com/api/v1/trips/9120" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": {
    "id": 9131,
    "vehicle_id": 1042,
    "status": "active",
    "started_at": "2026-10-11T09:20:03Z",
    "ended_at": null,
    "duration_seconds": null,
    "distance_km": null,
    "max_speed_kmh": 52,
    "moving_seconds": 1084,
    "idle_seconds": 160,
    "stops": 1,
    "start": {
      "latitude": 5.6365,
      "longitude": -0.0167
    },
    "end": null,
    "driver": {
      "id": 311,
      "name": "Ama Owusu"
    }
  }
}
GET /events

List events

Events newest first, in pages. To sync, send after_id instead: results are then oldest first by id, page is ignored and meta has next_after_id and has_more. Events appear in a sync about 5 seconds after we record them. A device that was offline can send old events late, so sync with after_id, never with time windows. Engine events are only listed for keys Lokkate staff have turned them on for.

Parameters

NameTypeDefaultDescription
pageinteger1The page to return. A page past the last one is 400.1 or more
limitinteger50Results per page.from 1 to 200
vehicle_idintegerOnly this vehicle. A vehicle you cannot see is 404.
after_idintegerSync mode. Only events with a larger id, oldest first. Send 0 to start from the beginning.
typelist of typesOnly these event types, comma separated, like geofence.exited,geofence.entered.19 types, listed under Webhooks
fromtimeOnly events that occurred at or after this time. ISO 8601 with a time zone.ISO 8601 with a time zone
totimeOnly events that occurred at or before this time. ISO 8601 with a time zone. Must be after from.ISO 8601 with a time zone
Example request
curl "https://lokkate.com/api/v1/events" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response: browsing by page
200 OK
{
  "data": [
    {
      "id": 55210,
      "type": "geofence.exited",
      "occurred_at": "2026-10-11T09:02:17Z",
      "vehicle_id": 1042,
      "driver": {
        "id": 311,
        "name": "Ama Owusu"
      },
      "location": {
        "latitude": 5.6412,
        "longitude": -0.0102
      },
      "data": {
        "geofence_id": 18,
        "geofence_name": "Tema Harbour",
        "speed_kmh": 22,
        "reason_text": "Tema Harbour"
      }
    }
  ],
  "meta": {
    "page": 1,
    "pages": 12,
    "count": 583,
    "next_page": 2
  }
}
Example response: syncing with after_id
200 OK
{
  "data": [
    {
      "id": 55211,
      "type": "sensor.out_of_range",
      "occurred_at": "2026-10-11T09:05:40Z",
      "vehicle_id": 1042,
      "driver": null,
      "location": null,
      "data": {
        "sensor_id": 77,
        "sensor_label": "Rear freezer",
        "celsius": -12.6,
        "target_min": -20.0,
        "target_max": -15.0,
        "reason_text": "Rear freezer at -12.6 °C, target -20 °C to -15 °C"
      }
    }
  ],
  "meta": {
    "next_after_id": 55211,
    "has_more": false
  }
}
GET /sensors

List sensors

Every sensor fitted to a vehicle you can see, oldest first by id, with its target range and latest temperature.

Parameters

NameTypeDefaultDescription
pageinteger1The page to return. A page past the last one is 400.1 or more
limitinteger50Results per page.from 1 to 200
Example request
curl "https://lokkate.com/api/v1/sensors" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": [
    {
      "id": 77,
      "name": "Rear freezer",
      "vehicle_id": 1042,
      "status": "out_of_range",
      "target_range": {
        "min_celsius": -20.0,
        "max_celsius": -15.0
      },
      "last_reading_at": "2026-10-11T09:05:40Z",
      "last_temperature_celsius": -12.6
    },
    {
      "id": 78,
      "name": "Sensor 2",
      "vehicle_id": 1042,
      "status": "never_reported",
      "target_range": null,
      "last_reading_at": null,
      "last_temperature_celsius": null
    }
  ],
  "meta": {
    "page": 1,
    "pages": 1,
    "count": 2,
    "next_page": null
  }
}
GET /sensors/{sensor_id}/readings

List a sensor's readings

The readings a sensor reported in a time window, oldest first. The window is 24 hours before to by default and at most 7 days long. One call returns up to 10,000 readings; when there are more, meta.truncated is true and you call again with meta.next_from as from and the same to. next_from is inclusive, so dedupe on recorded_at.

Parameters

NameTypeDefaultDescription
sensor_idrequiredin the pathintegerThe sensor id.
fromtimeStart of the window, inclusive. ISO 8601 with a time zone. Defaults to the default window before to.ISO 8601 with a time zone
totimeEnd of the window, inclusive. ISO 8601 with a time zone. Defaults to now.ISO 8601 with a time zone
Example request
curl "https://lokkate.com/api/v1/sensors/77/readings" \
  -H "Authorization: Bearer $LOKKATE_KEY"
Example response
200 OK
{
  "data": [
    {
      "recorded_at": "2026-10-11T07:00:12Z",
      "temperature_celsius": -18.2,
      "humidity_percent": 41,
      "magnet_detected": false,
      "battery_millivolts": 3010
    },
    {
      "recorded_at": "2026-10-11T07:00:42Z",
      "temperature_celsius": -18.1,
      "humidity_percent": 41,
      "magnet_detected": true,
      "battery_millivolts": 3010
    }
  ],
  "meta": {
    "from": "2026-10-10T09:00:00Z",
    "to": "2026-10-11T09:00:00Z",
    "truncated": true,
    "next_from": "2026-10-11T07:00:42Z"
  }
}

Webhooks

A webhook sends events to your server as they happen, so you do not have to poll. Each account has one webhook. Set it up in Developers, under Webhook: give an HTTPS address, choose the event types you want (or all of them), and copy the signing secret. Send test sends a test.ping so you can check your receiver.

What we send

A POST with a JSON body and these headers:

HeaderMeaning
Lokkate-Event-IdThe payload id.
Lokkate-Event-TypeThe payload type, like trip.completed.
Lokkate-Signaturet=<unix seconds>,v1=<hex signature>. Check it before you trust the body.

The body is always { id, type, created_at, data }. For events, data.event is the event in the same shape as GET /events, and data.trip is the trip summary on trip.completed and null otherwise.

trip.completed
{
  "id": 55204,
  "type": "trip.completed",
  "created_at": "2026-10-11T07:42:56Z",
  "data": {
    "event": {
      "id": 55204,
      "type": "trip.completed",
      "occurred_at": "2026-10-11T07:42:55Z",
      "vehicle_id": 1042,
      "driver": {
        "id": 311,
        "name": "Ama Owusu"
      },
      "location": {
        "latitude": 5.6698,
        "longitude": -0.0166
      },
      "data": {
        "trip_id": 9120
      }
    },
    "trip": {
      "id": 9120,
      "vehicle_id": 1042,
      "status": "completed",
      "started_at": "2026-10-11T06:58:10Z",
      "ended_at": "2026-10-11T07:42:55Z",
      "duration_seconds": 2685,
      "distance_km": 28.4,
      "max_speed_kmh": 78,
      "moving_seconds": 2110,
      "idle_seconds": 575,
      "stops": 3,
      "start": {
        "latitude": 5.6037,
        "longitude": -0.187
      },
      "end": {
        "latitude": 5.6698,
        "longitude": -0.0166
      },
      "driver": {
        "id": 311,
        "name": "Ama Owusu"
      }
    }
  }
}

Verifying the signature

v1 is the HMAC-SHA256 of the timestamp, a dot and the raw body (t.body), in hex, keyed with your whole signing secret, whsec_ included. Use the raw bytes you received, before any JSON parsing. Reject the request when the signature does not match or t is more than 5 minutes from your clock.

require "openssl"

def valid_signature?(header, raw_body, secret)
  parts = header.to_s.split(",").filter_map { |pair| pair.split("=", 2) if pair.include?("=") }.to_h
  timestamp = parts["t"].to_i
  return false if (Time.now.to_i - timestamp).abs > 300

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
  OpenSSL.secure_compare(expected, parts["v1"].to_s)
end

# Rails: valid_signature?(request.headers["Lokkate-Signature"], request.raw_post, ENV["LOKKATE_WEBHOOK_SECRET"])

Answering, retries and pausing

  • Answer with any 2xx within 10 seconds. The 10 seconds cover the whole exchange, so do the work later, after you answer. We do not follow redirects.
  • Anything else is retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours.
  • While your server is failing, new events wait too, instead of arriving all at once.
  • A webhook that has failed for 24 hours straight is turned off and the account owner gets an email. Turn it back on in Developers once your server is fixed.
  • Every delivery and every attempt, with the status and the start of your answer, is listed on the webhook's page in Developers.

Duplicates and order

Delivery is at least once and in no fixed order. Dedupe on type and id together, and use occurred_at, not arrival order, to know what happened first. The only exception is test.ping, whose id is always 0.

Live positions

positions.batch pushes positions so you do not have to poll. It is off by default and is not part of "all events": turn on Live positions on your webhook. Every 30 seconds you get one POST with every new position of your vehicles since the last one, oldest first, up to 2,000 at a time. Nothing is sent when nothing is new, and nothing from before you turned it on is sent. The payload id is the last position id in the batch.

positions.batch
{
  "id": 8812045,
  "type": "positions.batch",
  "created_at": "2026-10-11T09:00:35Z",
  "data": {
    "from": "2026-10-11T09:00:04Z",
    "to": "2026-10-11T09:00:28Z",
    "count": 2,
    "positions": [
      {
        "vehicle_id": 1042,
        "recorded_at": "2026-10-11T09:00:04Z",
        "latitude": 5.6698,
        "longitude": -0.0166,
        "speed_kmh": 46,
        "course": 92.0,
        "state": "moving",
        "ignition": true
      },
      {
        "vehicle_id": 1043,
        "recorded_at": "2026-10-11T09:00:28Z",
        "latitude": 5.556,
        "longitude": -0.1969,
        "speed_kmh": 0,
        "course": 0.0,
        "state": "parked",
        "ignition": false
      }
    ]
  }
}

Event types

These are the values of type, in webhooks and in GET /events. New types may be added: ignore the ones you do not know. Engine events are only sent when Lokkate staff turn them on for you.

TypeWhen it fires
trip.startedThe engine went on and a trip began.data: trip_id
trip.completedThe engine went off and the trip ended. In webhooks, data.trip carries the trip summary.data: trip_id
geofence.exitedA vehicle left a geofence it is assigned to.data: geofence_id geofence_name speed_kmh reason_text
geofence.enteredA vehicle came into a geofence it was outside of. outside_seconds says how long it was outside.data: geofence_id geofence_name speed_kmh outside_seconds reason_text
sensor.out_of_rangeA sensor read a temperature outside its target range. Sent on the first reading out of range, once per breach.data: sensor_id sensor_label celsius target_min target_max reason_text
sensor.back_in_rangeA sensor that was out of range is reading inside its target range again.data: sensor_id sensor_label celsius target_min target_max reason_text
sensor.offlineA sensor stopped reporting.data: sensor_id sensor_label
sensor.onlineA sensor that was offline is reporting again.data: sensor_id sensor_label
vehicle.offlineA vehicle's tracker stopped reporting.
vehicle.onlineA vehicle's tracker is reporting again. Says how long it was offline.data: offline_seconds
speeding.startedA vehicle went over a speed limit set for it.data: speed_limit_id speed_limit_name max_kmh speed_kmh peak_kmh reason_text
speeding.endedA vehicle is back under the speed limit. Says how long it was over and its top speed.data: speed_limit_id speed_limit_name max_kmh speed_kmh peak_kmh over_seconds reason_text
service.due_soonA vehicle's service is coming up.data: service_tracker_id reason_text
service.overdueA vehicle's service is overdue.data: service_tracker_id reason_text
driver.missingA pool vehicle moved with no driver checked in.
engine.immobilizedThe engine was cut remotely. Only sent when Lokkate staff turn engine events on.
engine.releasedThe engine was released remotely. Only sent when Lokkate staff turn engine events on.
engine.immobilize_failedA remote engine cut did not go through. Only sent when Lokkate staff turn engine events on.data: reason_text
engine.release_failedA remote engine release did not go through. Only sent when Lokkate staff turn engine events on.data: reason_text
positions.batchEvery new position of your visible vehicles, pushed every 30 seconds. Off by default; turn on Live positions.
test.pingThe test event sent from the Developers page. Its id is always 0; do not dedupe on it.

Guides

Syncing events with after_id

Use this when you want every event in your own database, or as a safety net behind your webhook.

  1. Start with after_id=0. You get the oldest events first.

  2. Save the events, then save meta.next_after_id.

  3. If meta.has_more is true, call again straight away with the new after_id. If it is false, wait a minute or so.

    Request
    curl "https://lokkate.com/api/v1/events?after_id=55210&limit=200" \
      -H "Authorization: Bearer $LOKKATE_KEY"

Events show up in a sync about 5 seconds after we record them, so the cursor never skips one. Do not sync with from and to: a tracker that was out of signal sends its backlog later, with an occurred_at in the past. Filters such as vehicle_id and type still apply while syncing. Trips change after they start, so sync them from the trip.completed webhook instead.

Tracking a delivery

Lokkate does not know about your orders or shipments, and does not need to. Your system knows which vehicle carries which delivery; Lokkate tells you where that vehicle went and what happened to the load. Here is one way to put it together.

  1. Draw the places. In Lokkate, draw a geofence around your depot and around each regular drop-off point (say Tema Harbour and a customer's warehouse in Spintex), and assign the delivery vehicles to them. Creating geofences through the API is coming soon.

  2. Listen. On your webhook, choose geofence.exited, geofence.entered, trip.started, trip.completed and, for cold loads, sensor.out_of_range.

  3. Dispatch. When your system sends delivery 4471 out on vehicle 1042, store the vehicle id with it.

  4. Departed. geofence.exited for the depot with vehicle_id 1042 means the delivery left. Keep its occurred_at. The trip.started just before it carries data.trip_id; link that trip to the delivery.

  5. Arrived. geofence.entered for the customer's geofence means it got there. geofence.exited for the same geofence means it left again, and the gap between the two is the time on site.

  6. The load. If a sensor.out_of_range arrives for vehicle 1042 in between, flag the delivery. For the full temperature record, read the window from departure to arrival:

    Request
    curl "https://lokkate.com/api/v1/sensors/77/readings?from=2026-10-11T06:58:10Z&to=2026-10-11T07:42:55Z" \
      -H "Authorization: Bearer $LOKKATE_KEY"
  7. The route. trip.completed carries the trip summary: distance, top speed, stops and where it ended. For the path itself, read the vehicle's positions for the same window (24 hours at most per call):

    Request
    curl "https://lokkate.com/api/v1/vehicles/1042/positions?from=2026-10-11T06:58:10Z&to=2026-10-11T07:42:55Z" \
      -H "Authorization: Bearer $LOKKATE_KEY"

If your server was down and missed a webhook, the same events are in GET /events: filter by vehicle_id and type=geofence.exited,geofence.entered to catch up.

Changelog

DateChange
11 October 2026Version 1 released: vehicles, positions, trips, events, sensors and readings, and webhooks for events and live positions.

Support

Something not working? Send us the X-Request-Id header from the response. Every call is logged, so we can find yours in seconds. You can see the same log yourself under Developers.

WhatsApp or call +233 55 771 1911, or email support@lokkate.com.