Keeping occupancy fresh
goodcharge shows a station's status as a dot, and that dot turns red after 15 minutes without any data from the station — the same as a review refusal or a rejected request. An operator's own app is the only thing that can prevent that: goodcharge has no other way to tell a quiet station from a stopped one.
There are two calls for this, and they cover two different situations.
When a point changes: updatePoints
Call updatePoints whenever a charging point's own state changes — a car plugs in, unplugs, or the point goes out of service. updatePoints changes only the points you list; everything else about the station stays as it was from the last publish.
Each point is identified by its position, which is its index in the charging_points array from publish, plus one — the first point declared is position: 1, the second position: 2, and so on.
// The second charging point just became occupied:
await station.updatePoints([{ position: 2, status: 'occupied' }]);
When nothing changes: startHeartbeat
Between real events, nothing calls updatePoints, and goodcharge would eventually flag the station as silent. startHeartbeat covers that gap: it calls heartbeat() on an interval — every 5 minutes by default — for as long as your process runs, and it returns a function you call to stop it.
const stop = station.startHeartbeat({ onError: console.error });
// Later, when you shut down:
stop();
startHeartbeat never throws once it has started, and the interval it schedules never rejects: a beat that fails is reported to onError if you gave one, instead of crashing anything. If your onError itself throws, that throw is ignored too — a broken handler cannot bring down your process. The only way startHeartbeat throws is synchronously, at the call itself, if intervalMs is out of range.
Retries happen underneath you
Both calls run over HTTP, and goodcharge enforces one request per second per key. A call that hits that limit gets a 429, which the SDK retries automatically — you do not need to slow down your own calls or catch that case yourself. The same automatic retry applies to a 5xx response, a timeout, or a network error. See Handling errors for what is and is not retried this way.
A complete example
This is a minimal occupancy loop: publish the station once, react to plug events as they come in, keep a heartbeat running in between, and stop everything cleanly on shutdown. It only uses publish, updatePoints, and startHeartbeat with the stop function it returns — see Publish your first station for what publish itself needs.
import { GoodchargeStation } from '@goodcharge/sdk';
const station = new GoodchargeStation({ apiKey: process.env.GOODCHARGE_API_KEY! });
await station.publish({
metadata: {
gps_coordinates: { latitude: 45.764, longitude: 4.835 },
address: '1 place Bellecour',
city: 'Lyon',
postal_code: '69002',
country: 'FR',
private: { level: 'public' },
},
charging_points: [
{ plug_type: 'type_2', power: 22, status: 'free' },
{ plug_type: 'type_2', power: 22, status: 'free' },
],
pricing: { connection_fee: 0.5, price_per_kilowatt_hour: 0.35, currency: 'EUR' },
});
// Keep the station alive between real events.
const stopHeartbeat = station.startHeartbeat({
onError: (error) => console.error('heartbeat failed:', error),
});
// Called by your own hardware or app logic whenever a point's state changes.
async function onPointStateChanged(position: number, occupied: boolean) {
await station.updatePoints([{ position, status: occupied ? 'occupied' : 'free' }]);
}
// Example: point 1 gets occupied, then freed up again a moment later.
await onPointStateChanged(1, true);
await onPointStateChanged(1, false);
// Clean shutdown: stop the heartbeat so nothing keeps the process alive.
function shutdown() {
stopHeartbeat();
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
Because calls on one GoodchargeStation are serialised, onPointStateChanged can be called from several places in your code at once without any locking on your side — each call waits its turn.