Developer Documentation API v1

Build with Dhuveli

Integrate live vessel tracking into your own apps — a simple HTTP API for reading fleet data, managing share links and receiving event webhooks, plus an embeddable live map.

REST · JSON Bearer-token auth 60 req / min Event webhooks Embeddable map

Authentication

The API uses bearer tokens. Generate one in Dhuveli under Settings → API Access. The token is shown once — copy it then. Every request is scoped to your company; you only ever see your own vessels.

Send the token in the Authorization header on every request:

Authorization: Bearer YOUR_TOKEN_HERE
Accept: application/json
Keep tokens secret — call the API from your server, not from browser JavaScript, so the token isn't exposed. A token carries only the abilities you grant it, and can be revoked at any time.

Abilities

Choose a token's abilities when you create it under Settings → API Access:

AbilityGrants
readRead vessels, tracks and share links. Always granted.
manage-linksCreate, update and delete share links.
manage-webhooksCreate, update and delete webhook endpoints.

A request that needs an ability the token lacks returns 403 Forbidden.

Base URL

https://dhuveli.com/api/v1

Rate limit

60 requests per minute per token. Over the limit returns 429 Too Many Requests. For a live map, poll no faster than every ~10 seconds.

Endpoints

GET/vessels

All your active vessels, each with its latest position.

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     https://dhuveli.com/api/v1/vessels

Response

{
  "data": [
    {
      "id": 12,
      "name": "Ocean Queen",
      "type": "Dhoani",
      "capacity": 40,
      "state": { "key": "moving", "label": "Moving" },
      "position": {
        "lat": 3.947643,
        "lng": 73.486898,
        "fixTime": "2026-06-17T16:34:18+00:00",
        "speed": { "knots": 9.2, "kmh": 17.0 },
        "course": { "degrees": 163, "cardinal": "SSE" },
        "power": { "volts": 12.2, "on": true, "hasData": true }
      }
    }
  ]
}

position is null and state.key is "unknown" when a vessel has no recorded fixes.

GET/vessels/{id}/track

The most recent activity for one vessel, newest first: up to the last 30 positions and the last 10 events (each with its time). No query parameters.

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     "https://dhuveli.com/api/v1/vessels/12/track"

Response

{
  "vessel": { "id": 12, "name": "Ocean Queen" },
  "positions": [
    {
      "lat": 3.947643,
      "lng": 73.486898,
      "speed": { "knots": 9.2, "kmh": 17.0 },
      "course": { "degrees": 163, "cardinal": "SSE" },
      "fixTime": "2026-06-17T16:34:18+00:00"
    }
  ],
  "events": [
    {
      "type": "deviceMoving",
      "time": "2026-06-17T16:30:02+00:00",
      "attributes": null
    }
  ]
}

Share links

Control which vessels appear on a public tracking link (/l/<code>). Reading links works with any token; creating a temporary link or changing a link's vessels requires a token created with the “Allow managing share links” option.

GET/links

Your share links and the vessels currently on each.

{
  "data": [
    {
      "code": "sunset-ferry",
      "url": "https://dhuveli.com/l/sunset-ferry",
      "active": true,
      "expires_at": null,
      "vessels": [ { "id": 12, "name": "Ocean Queen" } ]
    }
  ]
}

POST/links

Create a temporary share link. An expiry is required — links created through the API always expire. Requires the manage-links ability.

Body fieldDescription
vessel_idsArray of vessel ids to show on the link (must belong to you and have a tracker).
expires_atRequired ISO 8601 datetime in the future — when the link stops working.
curl -X POST \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"vessel_ids": [12, 15], "expires_at": "2026-06-18T17:00:00Z"}' \
     https://dhuveli.com/api/v1/links

Response (201 Created)

{
  "code": "a1b2c3d4",
  "url": "https://dhuveli.com/l/a1b2c3d4",
  "active": true,
  "expires_at": "2026-06-18T17:00:00+00:00",
  "vessels": [
    { "id": 12, "name": "Ocean Queen" },
    { "id": 15, "name": "Reef Runner" }
  ]
}

PUT/links/{code}/vessels

Replace which vessels are shown on a link. {code} is the link's slug (the part after /l/). Requires the manage-links ability.

curl -X PUT \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"vessel_ids": [12, 15]}' \
     https://dhuveli.com/api/v1/links/sunset-ferry/vessels

Response

{
  "code": "sunset-ferry",
  "url": "https://dhuveli.com/l/sunset-ferry",
  "active": true,
  "expires_at": "2026-07-01T00:00:00+00:00",
  "vessels": [
    { "id": 12, "name": "Ocean Queen" },
    { "id": 15, "name": "Reef Runner" }
  ]
}

Create the link itself (and its {code}) once in the dashboard under Shared Links; this endpoint then drives which vessels it shows.

PATCH/links/{code}

Update an existing link. Send any of the fields below (at least one required). Requires the manage-links ability, and you can only update your own links.

Body fieldDescription
expires_atNew ISO 8601 expiry, in the future (links stay temporary — you can extend but not remove the expiry).
activeBoolean — enable or disable the link without deleting it.
vessel_idsArray of vessel ids to show (must belong to you and have a tracker).
curl -X PATCH \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"expires_at": "2026-07-01T00:00:00Z", "active": false}' \
     https://dhuveli.com/api/v1/links/sunset-ferry

Response — the updated link (same shape as POST /links).

DELETE/links/{code}

Permanently delete one of your links — the public /l/{code} page stops working immediately. Requires the manage-links ability, and you can only delete your own links.

curl -X DELETE \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     https://dhuveli.com/api/v1/links/sunset-ferry

Response

{ "code": "sunset-ferry", "deleted": true }

Embedding the live map

You can embed a share link's live map directly in your own site with an <iframe>. For security, a link only loads in a frame on domains you've whitelisted under Settings → API Access → iFrame embedding — every other site is refused by the browser.

<iframe
    src="https://dhuveli.com/l/sunset-ferry"
    width="100%" height="500" style="border:0"
    allowfullscreen></iframe>
The iframe src must be https, and its domain must be on the link's company whitelist. A link with no vessels, or an expired one, shows a friendly status page inside the frame rather than failing.

Live data feed (optional)

Add ?t=iframe to the URL to turn on a postMessage feed: clicking a vessel no longer opens the in-map popup — instead the vessel's details are sent to your page, and the selected vessel keeps streaming updates on every refresh. This lets you render the data in your own UI.

<iframe src="https://dhuveli.com/l/sunset-ferry?t=iframe" …></iframe>

<script>
window.addEventListener("message", (event) => {
    // 1. Only trust messages from Dhuveli
    if (event.origin !== "https://dhuveli.com") return;
    if (event.data?.source !== "dhuveli") return;

    if (event.data.type === "vessel.selected") {
        // user tapped a vessel
        showVessel(event.data.vessel);
    } else if (event.data.type === "vessel.update") {
        // same vessel, fresh data (~every 10s)
        updateVessel(event.data.vessel);
    }
});
</script>

Message shape (both vessel.selected and vessel.update):

{
  "source": "dhuveli",
  "type": "vessel.selected",
  "version": 1,
  "timestamp": "2026-06-17T11:34:20.512Z",
  "vessel": {
    "name": "Ocean Queen",
    "descriptor": "Dhoani",
    "status": "online",
    "state": { "key": "moving", "label": "Moving" },
    "position": { "lat": 3.947643, "lng": 73.486898, "fixTime": "…", "fixAgeLabel": "Just now" },
    "speed": { "knots": 9.2, "kmh": 17.0 },
    "course": { "degrees": 163, "cardinal": "SSE" },
    "power": { "volts": 12.2, "on": true, "hasData": true },
    "track": { "distanceKm": 1.36, "fixCount": 30 }
  }
}

The vessel object uses the same fields as the API responses below (no internal device id). Always verify event.origin before trusting a message.

Field reference

FieldMeaning
idStable vessel identifier (use this to track a vessel across calls).
state.keymoving · idle · stopped · unknown
position.speedSpeed over ground, in knots and kmh.
position.courseHeading in degrees (0–359) and a 16-point cardinal label.
position.powerBattery/supply voltage. on is true above 6 V; hasData false if the device reports none.
fixTimeWhen the device recorded the fix (ISO 8601, UTC).

Webhooks

Receive vessel events at your own URL as they happen — the same events we post to Telegram. Add an endpoint under Settings → API Access → Event webhooks, or manage them with the API below (token needs the manage-webhooks ability). Each endpoint gets a signing secret, shown once, used to verify every delivery.

Events

TypeFired when
vessel.channel_crossedA vessel crosses a channel. Carries channel.name and a compass heading.
vessel.geofence_enterA vessel enters a named place or ETA zone (arrival). Carries geofence.name and geofence.type.
vessel.geofence_exitA vessel leaves a named place.
vessel.overspeedSpeed threshold exceeded. Carries speed_knots.
vessel.power_cutThe tracker lost external power.
vessel.power_restoredExternal power came back.
vessel.alarmAny other device alarm (e.g. SOS). Carries the raw alarm code.

Payload

Every delivery is a JSON POST. Vessels are identified by id only. The source block is our stamp. id is stable across retries — use it to dedupe.

{
  "id": "evt_01j9x8y7z6...",
  "type": "vessel.channel_crossed",
  "occurred_at": "2026-07-25T08:14:22+00:00",
  "data": {
    "vessel": { "id": 42, "name": "Ocean Queen" },
    "event": {
      "channel": { "name": "North Channel" },
      "heading": "NE"
    }
  },
  "source": { "provider": "Dhuveli", "api_version": "v1" }
}

Headers & signature verification

HeaderValue
X-Dhuveli-EventThe event type.
X-Dhuveli-DeliveryThe delivery/event id (matches id in the body).
X-Dhuveli-TimestampUnix seconds, part of the signed base.
X-Dhuveli-Signaturesha256=<hex> — HMAC-SHA256 of "{timestamp}.{rawBody}" with your endpoint secret.

Verify against the raw request body (before JSON parsing), using a constant-time compare:

// Node.js (Express, express.raw())
const crypto = require('crypto');

function verify(req, secret) {
  const ts   = req.get('X-Dhuveli-Timestamp');
  const sig  = req.get('X-Dhuveli-Signature');
  const base = ts + '.' + req.body;            // req.body is the raw Buffer/string
  const mine = 'sha256=' + crypto.createHmac('sha256', secret).update(base).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mine));
}

Delivery, retries & security

Respond 2xx to acknowledge. Non-2xx or a timeout is retried with exponential backoff (10s → 30s → 1m → 5m → 15m → 1h). After repeated consecutive failures an endpoint is automatically disabled — re-enable it in Settings. Callback URLs must be public https; URLs that resolve to private, loopback or link-local addresses are rejected. Redirects are not followed.

Managing endpoints via the API

GET/webhooks

List your endpoints (secrets are never returned). Requires manage-webhooks.

POST/webhooks

Register an endpoint. The signing secret is returned once. Omit event_types (or send an empty array) to receive every event.

curl -X POST \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"url": "https://your-app.com/webhooks/dhuveli", "description": "Production", "event_types": ["vessel.geofence_enter", "vessel.overspeed"]}' \
     https://dhuveli.com/api/v1/webhooks

Response (201 Created)

{
  "id": 3,
  "url": "https://your-app.com/webhooks/dhuveli",
  "description": "Production",
  "event_types": [ "vessel.geofence_enter", "vessel.overspeed" ],
  "active": true,
  "secret": "whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

PATCH/webhooks/{id}  ·  DELETE/webhooks/{id}

Update (url, description, event_types, active) or delete an endpoint. Setting active: true also clears the auto-disable.

POST/webhooks/{id}/test

Queue a webhook.test ping to the endpoint so you can confirm signature verification end-to-end.

Errors

StatusMeaning
401Missing or invalid token.
403Token lacks the required ability (e.g. manage-links).
404Vessel or link not found in your company.
422Invalid query parameters.
429Rate limit exceeded.