Wiederholungen und Idempotenz
Welche Anfragen Sie nach einem Timeout gefahrlos wiederholen können und wie Sie doppelte Ereignisse vermeiden.
Wenn eine Anfrage in einen Timeout läuft oder die Verbindung abbricht, wissen Sie nicht, ob BatteryPass sie ausgeführt hat. Ob Sie sie einfach erneut senden können, hängt von der Operation ab.
Überblick
| Operationen | Methode | Gefahrlos wiederholbar | Warum |
|---|---|---|---|
| Alle Lesezugriffe, auch öffentliche Abrufe von Pässen | GET | Ja | Sie ändern nichts. |
updateCondition | PATCH | Ja, mit demselben measured_at | Eine Messung, die gleich alt oder älter als die gespeicherte ist, wird übersprungen und aufgeführt. |
createLifecycleEvent | POST | Nein | Jeder erfolgreiche Aufruf erfasst ein neues Ereignis. |
Eine mit 429 beantwortete Anfrage wurde nicht ausgeführt und lässt sich nach Retry-After immer wiederholen
(siehe Ratenlimits). Eine Antwort 4xx heißt, dass die Anfrage als Ganzes abgelehnt
wurde: Korrigieren Sie sie, statt sie zu wiederholen.
Lesezugriffe
Jedes GET der API können Sie beliebig oft wiederholen. Wiederholen Sie Netzwerkfehler und Antworten 5xx
mit exponentiellem Backoff.
Zustandsdaten
updateCondition führt Messwerte mit den Zustandsdaten der Einheit zusammen. Für jeden Wert vergleicht
BatteryPass sein measured_at mit dem des gespeicherten Werts:
- neuer als die gespeicherte Messung oder gleicher Zeitpunkt mit anderem Wert: gespeichert, aufgeführt in
accepted; - gleicher Zeitpunkt und gleicher Wert: übersprungen mit dem Grund
unchanged; - älter als die gespeicherte Messung: übersprungen mit dem Grund
older_measurement.
Eine wiederholte Anfrage mit demselben Body ändert also nichts. Senden Sie zum Beispiel:
{
"measured_at": "2026-10-24T02:00:00Z",
"values": [
{ "field_key": "condition.state_of_charge", "value": 87.5 },
{ "field_key": "condition.number_of_full_cycles", "value": 412 }
]
}Wurde der erste Versuch gespeichert, antwortet die Wiederholung:
{
"accepted": [],
"skipped": [
{ "field_key": "condition.state_of_charge", "reason": "unchanged" },
{ "field_key": "condition.number_of_full_cycles", "reason": "unchanged" }
],
"publication": { "outcome": "not_published" }
}Diese Anfrage erfasst kein neues Lebenszyklusereignis und veröffentlicht nichts: not_published heißt hier,
dass diese Anfrage nichts veröffentlicht hat; der Pass behält seine aktuelle Version.
Setzen Sie measured_at auf den Zeitpunkt der Messung, zum Beispiel den Zeitpunkt, zu dem Ihr
Batteriemanagementsystem den Wert erfasst hat, und behalten Sie ihn bei einer Wiederholung bei. Ein
measured_at aus der Uhrzeit beim Senden macht jede Wiederholung zu einer neuen Messung.
Lebenszyklusereignisse
createLifecycleEvent erkennt keine Duplikate: Jeder erfolgreiche Aufruf erfasst ein neues Ereignis, und die
veröffentlichte API kennt keinen Idempotenzschlüssel. Prüfen Sie nach einem Timeout, einem Verbindungsabbruch
oder einer Antwort 5xx, ob das Ereignis angekommen ist, bevor Sie es erneut senden:
- Lesen Sie
listLifecycleEventsfür die Einheit (neueste zuerst). - Suchen Sie ein Ereignis mit
source: "api"und Ihremtype,occurred_onunddescription. - Senden Sie das Ereignis nur erneut, wenn es fehlt.
Schreiben Sie eine eigene Referenz in description, etwa die Nummer des Serviceauftrags, damit sich das
Ereignis leicht wiederfinden lässt. BatteryPass speichert die Beschreibung ohne führende und nachfolgende
Leerzeichen.
type NewEvent = { type: string; occurred_on: string; description: string };
type LifecycleEvent = NewEvent & { id: string; source: string };
const base = `${process.env.BATTERYPASS_URL}/api/v1`;
const headers = {
Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`,
'Content-Type': 'application/json',
};
// Sucht ein Ereignis, das diese Integration schon erfasst hat (gleicher Typ, Tag und Beschreibung).
async function findEvent(batteryId: string, e: NewEvent): Promise<LifecycleEvent | undefined> {
let cursor: string | null = null;
do {
const query = new URLSearchParams({ limit: '200' });
if (cursor) query.set('cursor', cursor);
const res = await fetch(`${base}/batteries/${batteryId}/lifecycle-events?${query}`, { headers });
if (!res.ok) throw new Error(`listLifecycleEvents: ${res.status}`);
const page: { items: LifecycleEvent[]; next_cursor: string | null } = await res.json();
const hit = page.items.find(
(x) => x.source === 'api' && x.type === e.type && x.occurred_on === e.occurred_on && x.description === e.description,
);
if (hit) return hit;
// Neueste zuerst: Sobald eine Seite ältere Tage erreicht, ist das Ereignis nicht da.
if (page.items.some((x) => x.occurred_on < e.occurred_on)) return undefined;
cursor = page.next_cursor;
} while (cursor);
return undefined;
}
async function recordEvent(batteryId: string, e: NewEvent): Promise<LifecycleEvent> {
for (let attempt = 1; attempt <= 3; attempt++) {
let res: Response | undefined;
try {
res = await fetch(`${base}/batteries/${batteryId}/lifecycle-events`, {
method: 'POST',
headers,
body: JSON.stringify(e),
signal: AbortSignal.timeout(30_000),
});
} catch {
res = undefined; // Timeout oder Verbindungsfehler
}
if (res?.status === 201) return res.json();
if (res && res.status < 500) throw new Error(`createLifecycleEvent: ${res.status} ${await res.text()}`);
// Keine Antwort oder 5xx: Das Ereignis ist vielleicht trotzdem erfasst.
const existing = await findEvent(batteryId, e);
if (existing) return existing;
}
throw new Error('createLifecycleEvent: nach 3 Versuchen kein Erfolg');
}
const event = await recordEvent('0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12', {
type: 'inspection',
occurred_on: '2026-10-24',
description: 'Serviceauftrag SO-2026-0815: jährliche Inspektion',
});
console.log(event.id);Die Beispiele behandeln ein 429 wie jede andere Ablehnung; kombinieren Sie sie im Betrieb mit dem Backoff aus
Ratenlimits.
Der Header Idempotency-Key
BatteryPass kennt einen Header Idempotency-Key, aber nur bei Operationen des Portals wie dem Veröffentlichen
und Massenjobs. Keine der veröffentlichten Operationen akzeptiert ihn; senden Sie ihn also nicht an die API,
dort bewirkt er nichts.