Skip to content
Support the creatorSupport

Receiving uploads on your server

Long Ping posts to your own endpoint. There is no hosted backend and no fixed schema on your side: you get plain JSON over HTTP(S), and you answer with a status code (and, if you like, new settings for the device).

With the defaults (batch: true, no template) every request looks like this:

POST /locations HTTP/1.1
Content-Type: application/json
Accept: application/json
Authorization: Bearer <access token>
{
"locations": [
{
"uuid": "6f1c2a1e-8b9e-4c1a-9a43-0d6c1d1a7f10",
"timestamp": "2026-10-26T12:00:00.123Z",
"event": "interval",
"is_moving": true,
"coords": { "latitude": 43.85, "longitude": 18.41, "accuracy": 4.8, "speed": 12.3, "heading": 181, "altitude": 520.4 },
"battery": { "level": 0.82, "is_charging": false },
"mock": false
}
]
}
  • Up to maxBatchSize (100) records per request, oldest first.
  • method can be PUT instead of POST. Your http.headers are sent too; a stored access token adds Authorization: Bearer ….
  • Every field is described in Location record.

Change the body shape with rootProperty, params, extras and locationTemplate, and see what your server gets. Pick a reply to see what the device does with it.

Mode

http.batch: true · up to maxBatchSize records per request

Tokens become raw JSON values. Strings go in without quotes, so write"{{timestamp}}" yourself.

Your server receives

1 request · 2 records

Pretty-printed here; the device sends compact JSON.

You reply

The body rules, exactly:

Mode Body
batch: true { ...params, [rootProperty ?? 'locations']: [r1, r2, …] }
batch: false { ...params, [rootProperty ?? 'location']: r }, one request per record
batch: false, rootProperty: '.' { ...params, ...r }: the record’s fields at the body root

Each r is the record JSON, or your rendered locationTemplate. '.' only applies to single mode; with batching on it falls back to locations.

Every record has a uuid generated on the device when the fix is recorded. Retries can resend records you already stored: for example, your server saved the batch but the reply was lost to a timeout. Make uuid a unique key and ignore duplicates (ON CONFLICT DO NOTHING, INSERT OR IGNORE, insertOrIgnore). All three examples below do this.

Order by timestamp, not by arrival. Each device sends its queue oldest first, but a backlog that waited out a dead zone arrives after newer data from other devices, and a fix taken with getCurrentPosition({ persist: true }) (event current) can fall between interval fixes.

You answer The device
any 2xx Deletes the sent records. Reads long_ping from a JSON body, if present.
401 or 403 Refreshes the token if it can and retries once; otherwise pauses uploads. Tracking continues.
413 Retries straight away with half the batch.
408, 429, 5xx, or no answer Keeps the records and backs off; Retry-After (seconds) wins.
any other 4xx Counts a failed attempt for each sent record; drops a record after 3 attempts.

Answer quickly and do heavy work (geocoding, ETA updates) after replying or in a job queue. The device waits up to http.timeoutSec (30 s) per request, and only one upload runs at a time.

The full table, with backoff timings, is in the HTTP contract. To change device settings from your reply, see Remote config & remote stop.

Each example accepts the default batch format, checks a bearer token, stores records with uuid as the primary key, ignores duplicates and answers 2xx. They expect this config on the device:

await LongPing.configure({
http: {
url: 'https://api.example.com/locations',
params: { device_id: 'van-07' }, // arrives at the body root
},
});
await LongPing.setAuthorization({ accessToken: token });
server.mjs
// npm install express (node:sqlite is built into Node 22.13+)
import express from 'express';
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('locations.db');
db.exec(`
CREATE TABLE IF NOT EXISTS locations (
uuid TEXT PRIMARY KEY, -- retries may resend a record
device_id TEXT NOT NULL,
recorded_at TEXT NOT NULL, -- fix time, UTC
event TEXT NOT NULL,
is_moving INTEGER NOT NULL,
lat REAL NOT NULL,
lng REAL NOT NULL,
accuracy REAL,
speed REAL,
battery REAL,
record TEXT NOT NULL -- the full record as received
)`);
const insert = db.prepare(`
INSERT OR IGNORE INTO locations
(uuid, device_id, recorded_at, event, is_moving, lat, lng, accuracy, speed, battery, record)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const app = express();
app.use(express.json({ limit: '2mb' }));
app.post('/locations', (req, res) => {
if (req.get('authorization') !== `Bearer ${process.env.API_TOKEN}`) {
return res.status(401).json({ error: 'unauthorized' }); // device refreshes or pauses uploads
}
const deviceId = String(req.body.device_id ?? 'unknown');
const records = Array.isArray(req.body.locations) ? req.body.locations : [];
db.exec('BEGIN');
try {
for (const r of records) {
if (!r?.uuid || !r.coords) continue; // log it; don't fail the whole batch
insert.run(
r.uuid, deviceId, r.timestamp, r.event, r.is_moving ? 1 : 0,
r.coords.latitude, r.coords.longitude, r.coords.accuracy, r.coords.speed,
r.battery?.level ?? null, JSON.stringify(r),
);
}
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
console.error(err);
return res.status(500).json({ error: 'storage' }); // device keeps the records and retries
}
res.json({}); // any 2xx = delivered
});
app.listen(3000, () => console.log('listening on :3000'));

If your API already has a format, set http.locationTemplate instead of adapting the server:

http: {
url: 'https://api.example.com/v2/positions',
rootProperty: 'positions',
locationTemplate: '{"id":"{{uuid}}","lat":{{latitude}},"lng":{{longitude}},"at":"{{timestamp}}"}',
}

Keep {{uuid}} in the template if you want to deduplicate. Tokens become raw values, so string tokens need quotes in the template. configure() rejects a template that does not render valid JSON with INVALID_CONFIG.