Anfragen & Antworten
Adresse und Versionierung
Basisadresse: https://api.manage2hero.com. Alle Endpunkte beginnen mit /v1.
Versionierung, Änderungen und Auslaufen
Die Version steht im Pfad (/v1). Innerhalb von v1 ändern wir nur erweiternd. Brechende Änderungen gäbe es nur in einer neuen Version (/v2); die alte bliebe mindestens 12 Monate parallel verfügbar. Die Frist beginnt mit dem Datum des Eintrags im Änderungsprotokoll, der die neue Version ankündigt.
| Änderung | Innerhalb von v1 | Ankündigung |
|---|---|---|
| Neue Endpunkte, neue optionale Abfrageparameter | jederzeit möglich | Eintrag im Änderungsprotokoll |
| Neue Felder in Antworten und in Webhook-Nachrichten | jederzeit möglich | Eintrag im Änderungsprotokoll |
| Neue Werte in bestehenden Aufzählungen (Fehlercodes, Statuswerte, Ereignistypen, Abschaltgründe, Scopes, Garagentypen) | jederzeit möglich | Eintrag im Änderungsprotokoll |
| Texte (error.message, Beschreibungen) | können sich ändern | keine |
| Entfernen oder Umbenennen von Endpunkten, Feldern, Parametern, Fehlercodes oder Webhook-Feldern; geänderte Bedeutung oder Typ eines Felds; neue Pflichtparameter | nicht in v1 | nur mit neuer Version (/v2), 12 Monate parallel |
Was Ihr Programm tun sollte: unbekannte Felder ignorieren, unbekannte Werte in Aufzählungen (Status, Fehlercode, Ereignistyp) nicht zum Absturz führen lassen, sondern als „unbekannt“ behandeln, und sich nur auf error.code, nie auf error.message verlassen. In Webhook-Nachrichten bleiben id, type, created_at und data mit ihrer Bedeutung erhalten; die Signatur wird wie im Webhooks-Handbuch berechnet — werten Sie im Kopf X-M2H-Signature nur t und v1 aus und ignorieren Sie weitere Einträge.
Code-Generatoren: Aufzählungen (enum) in der openapi.json sind offene Wertemengen: Neue Status-, Fehler-, Ereignis- und Scope-Werte können innerhalb von v1 hinzukommen (siehe Tabelle oben). Planen Sie in generierten Clients und Prüfungen einen Auffangzweig für unbekannte Werte ein (kein harter Abbruch, keine erschöpfende Fallunterscheidung ohne Standardfall) und behandeln Sie sie als „unbekannt“.
Webhooks und neue Ereignistypen: Ein Webhook erhält nur die Ereignistypen, die er in events abonniert hat. Ein neuer Ereignistyp erreicht bestehende Webhooks daher nicht von selbst; er muss über PATCH /v1/webhooks/{id} dazugenommen werden. Die Liste der abonnierbaren Typen wächst dabei nur.
Auslaufende Funktionen: Innerhalb von v1 nehmen wir nichts weg. Sollte aus Sicherheitsgründen eine Funktion dennoch kurzfristig geändert werden müssen, steht das sofort im Änderungsprotokoll. Die Köpfe Deprecation und Sunset sendet die API derzeit nicht; führen wir sie ein, steht das im Änderungsprotokoll — verlassen Sie sich nicht darauf, sondern lesen Sie das Änderungsprotokoll.
Spezifikationsrevisionen: openapi.json trägt neben info.version (die API-Version, 1.0.0) das Feld info.x-revision — das Datum des jüngsten Änderungsprotokoll-Eintrags, der die Spezifikation betrifft (aktuell 2026-10-20). So erkennen generierte Clients und Prüfwerkzeuge, ob sich die Beschreibung geändert hat; die zugehörige Änderung steht unter diesem Datum im Änderungsprotokoll. Speichern Sie für reproduzierbare Builds dieopenapi.json samt Revision.
Hülle
{ "success": true, "data": { … }, "request_id": "req_…" }{ "success": false, "error": { "code": "…", "message": "…" }, "request_id": "req_…" }error.code ist stabil und für Programme gedacht; error.message ist ein erklärender Text, der sich ändern kann. Die request_id steht auch im Kopf X-Request-Id — bitte bei Rückfragen angeben.
Seitenweise Listen
Listen liefern data.items und data.next_cursor (einzige Ausnahme: GET /v1/webhooks ohne next_cursor, höchstens 10 Einträge — siehe Hinweis unten). Mit ?limit= (1–200, Vorgabe 50) steuern Sie die Seitengröße. Ist next_cursor nicht null, holen Sie die nächste Seite mit ?cursor=<next_cursor>. Den Cursor bitte unverändert zurückgeben.
GET /v1/webhooks liefert alle Webhooks der Organisation (höchstens 10) in einer Antwort; data enthält dort nur items, kein next_cursor.async function alle(pfad) {
const items = [];
let cursor = null;
do {
const url = new URL(pfad, 'https://api.manage2hero.com');
url.searchParams.set('limit', '200');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` } });
const body = await res.json();
if (!body.success) throw new Error(body.error.code);
items.push(...body.data.items);
cursor = body.data.next_cursor;
} while (cursor);
return items;
}Listenabruf oder Änderungsabgleich?
| Aspekt | Listenabruf (Cursor) | Änderungsabgleich (changes) |
|---|---|---|
| Eignet sich für | Abrufen und Auswerten eines Zeitraums, Exporte, einmaliges Befüllen | laufende Synchronisation: Neues, Geändertes und Entferntes seit dem letzten Abgleich |
| Verfügbar für | alle Listen | bookings, invoices, payments, dunning_notices, documents, emails, contracts, damages |
| Cursor bedeutet | Position in der Sortierung (und bei Listen mit Zeitfilter die beim ersten Abruf aufgelösten Zeitgrenzen) | Stand des Abgleichs; 90 Tage gültig (410 cursor_expired) |
Cursor und Filter: Geben Sie den Cursor unverändert zurück und senden Sie bei jeder Folgeseite dieselben Filter (Status, Standort, Zeitfilter usw.) wie bei der ersten. Mit anderen Filtern ist das Ergebnis nicht definiert. Ein Cursor gilt nur für den Endpunkt, von dem er stammt.
Keine Momentaufnahme: Seiten werden nacheinander gelesen. Ändert oder entsteht ein Datensatz, während Sie blättern, kann er auf einer späteren Seite erscheinen oder fehlen; wir garantieren keinen Schnappschuss über mehrere Seiten. Ordnen Sie Datensätze deshalb über ihre id zu (Doppelte überschreiben), und nutzen Sie für laufende Abgleiche den Änderungsabgleich statt wiederholter Vollabrufe.
Grenzen je Abruf (ressourcenspezifisch):
| Abruf | Seitengröße | Höchstspanne / -größe |
|---|---|---|
| Listen allgemein | 1–200 (Vorgabe 50); Datei (CSV/Excel): eine Seite, bis 200 Zeilen | — |
/v1/bookings | wie allgemein | höchstens 366 Kalendertage je Abruf (from und to zählen mit; to − from höchstens 365) (sonst 400 range_too_large) |
/v1/payments | wie allgemein | höchstens 366 Kalendertage je Abruf (from und to zählen mit; to − from höchstens 365) |
/v1/invoices | wie allgemein | höchstens 1.831 Kalendertage je Abruf (from und to zählen mit; to − from höchstens 1.830) |
/v1/dunning/notices | wie allgemein | höchstens 1.831 Kalendertage je Abruf (from und to zählen mit; to − from höchstens 1.830) |
…/changes | 1–500 (Vorgabe 100) | Änderungen 90 Tage aufbewahrt |
…/by-ids | bis zu 100 Kennungen in ids | — |
/v1/tenant-imports/{id} | Datensätze je Seite 1–500 (Vorgabe 100) | 1 MB je Anfrage (Übernahme 64 KB) |
Änderungen abgleichen
Für Buchungen, Rechnungen, Zahlungseingänge, Mahnungen, Dokumente, E-Mails, Verträge und Schäden müssen Sie nicht jedes Mal alles neu abrufen: (1) Einstieg holen mit GET /v1/…/changes/cursor, (2) einmal alles über die Liste abrufen, (3) danach regelmäßig GET /v1/…/changes?cursor=… aufrufen. Die Antwort nennt die Kennungen, die neu, geändert oder entfernt sind. Laden Sie jede über GET /v1/…/by-ids?ids=… nach: Was zurückkommt, ist der aktuelle Stand; fehlt eine Kennung dort, entfernen Sie den Datensatz bei sich. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Blättern Sie mit next_cursor; ist er null, merken Sie sich resume_cursor für den nächsten Abgleich. Es erscheinen nur abgeschlossene Änderungen — auch eine, die während Ihres Abrufs noch lief, kommt beim nächsten Mal sicher. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 cursor_expired (dann bei Schritt 1 neu beginnen). Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer) lösen kein Ereignis aus — ein gelegentlicher Vollabruf, etwa wöchentlich, gleicht sie ab.
Idempotenz: Schreibaufrufe sicher wiederholen
Schreibende Endpunkte, die etwas anlegen oder auslösen, verlangen den Kopf Idempotency-Key (1–255 sichtbare ASCII-Zeichen, am besten eine UUID je Vorgang; fehlt er: 400 idempotency_key_required, ungültige Zeichen: 400 validation_error). Damit lässt sich ein Aufruf nach einem Timeout oder Verbindungsabbruch gefahrlos wiederholen: Dieselbe Anfrage wirkt nur einmal. Gleicher Schlüssel und gleicher Inhalt liefert das Ergebnis des ersten Aufrufs (meist 200 statt 201); gleicher Schlüssel mit anderem Inhalt ergibt 409 idempotency_conflict — das ist die Standardregel. Eine einzige Ausnahme: POST /v1/webhooks/{id}/deliveries/retry-failed bindet den Schlüssel an die Filter der Anfrage; ein anderer Filter mit demselben Schlüssel ist dort ein neuer Vorgang und ergibt kein 409 (Tabelle „Je Endpunkt“).
Der Ablauf im Client
- Schlüssel erzeugen und speichern, bevor Sie senden — zusammen mit dem Vorgang (Datenbank, Warteschlange). Nur so kennen Sie ihn nach einem Absturz noch.
- Body einmal festlegen und für jede Wiederholung unverändert verwenden (gleiche Felder, gleiche Werte). Ein geänderter Inhalt mit altem Schlüssel ist ein Fehler (
409), kein neuer Vorgang. - Mit Timeout senden (z. B. 30 Sekunden). Läuft er ab oder bricht die Verbindung, ist der Ausgang unbekannt: Der Server kann den Vorgang ausgeführt haben.
- Wiederholen mit demselben Schlüssel und demselben Body bei Timeout, Verbindungsabbruch,
429(zuerstRetry-Afterabwarten),500und503— mit wachsender Wartezeit und einer Obergrenze an Versuchen — außerdem bei409 readings_locked(Zählerstände werden gerade bearbeitet; nach einigen Sekunden). Alle anderen Fehler (400,401,403,404,409,413,415,422) behebt eine Wiederholung nicht: Eingabe oder Zugang korrigieren; für eine geänderte Anfrage einen neuen Schlüssel verwenden. - Erst nach der Antwort als erledigt markieren und den Schlüssel danach nicht wiederverwenden: ein Schlüssel gehört zu genau einem Vorgang.
import { randomUUID } from 'node:crypto';
const BASIS = 'https://api.manage2hero.com';
// 1. Vorgang samt Schlüssel dauerhaft vormerken, BEVOR gesendet wird
// (speichere/alsErledigtMarkieren: Ihre Datenbank, Datei oder Warteschlange).
const vorgang = {
key: randomUUID(),
body: JSON.stringify({ url: 'https://erp.example.com/hooks/manage2hero', events: ['payment.received'] }),
};
await speichere(vorgang);
// 2. Senden. Bei Timeout, Verbindungsabbruch, 429, 500 und 503 mit DEMSELBEN Schlüssel
// und unverändertem Body wiederholen, mit wachsender Wartezeit.
async function sende(v) {
for (let versuch = 1; versuch <= 5; versuch++) {
let res = null;
try {
res = await fetch(BASIS + '/v1/webhooks', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.M2H_API_TOKEN,
'Content-Type': 'application/json',
'Idempotency-Key': v.key,
},
body: v.body,
signal: AbortSignal.timeout(30_000),
});
} catch {
// Timeout oder Abbruch: der Ausgang ist unbekannt. Genau dafür gibt es den Schlüssel.
}
// 200/201 = Erfolg; andere 4xx behebt eine Wiederholung nicht (Eingabe oder Zugang korrigieren).
if (res && res.status !== 429 && res.status < 500) return res;
const warten = Number(res?.headers.get('Retry-After')) || 2 ** versuch;
await new Promise((r) => setTimeout(r, warten * 1000));
}
throw new Error('Kein Ergebnis nach 5 Versuchen. Vorgang und Schlüssel behalten, später erneut senden.');
}
// 3. Auswerten; erst jetzt den Vorgang als erledigt markieren.
const antwort = await (await sende(vorgang)).json();
if (!antwort.success) throw new Error(antwort.error.code + ' (' + antwort.request_id + ')');
await alsErledigtMarkieren(vorgang.key, antwort.data);secret nur in der ersten erfolgreichen Antwort (201); eine Wiederholung antwortet 200 ohne Geheimnis. Ging die erste Antwort verloren, erzeugen Sie mit rotate-secret ein neues.Je Endpunkt
Die Bindung des Schlüssels ist nicht überall gleich. Verwenden Sie zum Wiederholen immer dasselbe Token und je Vorgang einen frischen Schlüssel; dann sind Sie unabhängig von den Unterschieden. Gilt ein Schlüssel nur für die Organisation, trifft ihn auch ein anderes Token derselben Organisation; gilt er für das Token, ist derselbe Schlüssel mit einem anderen Token ein neuer Vorgang.
| Endpunkt | Schlüssel gilt für | Aufbewahrung | Gleicher Schlüssel, anderes Token derselben Organisation |
|---|---|---|---|
POST /v1/webhooks | Token und Anlage | Solange der Webhook existiert; mit dem Löschen vergisst die API den Schlüssel | Neuer Vorgang |
POST /v1/webhooks/{id}/rotate-secret | Token und Vorgang „Rotation“ | 30 Tage | Neuer Vorgang |
POST /v1/webhooks/{id}/test | Token und Vorgang „Test“ | Solange die Testzustellung im Zustellverlauf steht (30 Tage); danach löst derselbe Schlüssel ein neues Testereignis aus | Neuer Vorgang |
POST /v1/sandbox/events | Token und Vorgang „Sandbox-Ereignis“: derselbe Schlüssel wählt dasselbe Musterobjekt und stellt nichts erneut ein | 7 Tage (Zustellungen der Sandbox) | Neuer Vorgang |
POST /v1/webhooks/{id}/deliveries/{delivery_id}/retry | Token und genau diese Zustellung | 30 Tage | Neuer Vorgang |
POST /v1/webhooks/{id}/deliveries/retry-failed | Token und die Filter der Anfrage; derselbe Schlüssel mit anderem Filter ist ein neuer Vorgang (kein 409) | 30 Tage | Neuer Vorgang |
POST /v1/tenant-imports | Organisation | Solange der Import existiert (abgeschlossene Importe werden nach 30 Tagen, offene nach 90 Tagen gelöscht, ein abgebrochener sofort) | Treffer: derselbe Vorgang |
POST /v1/tenant-imports/{id}/commit | kein Idempotency-Key nötig | Wiederholen ist unschädlich: bereits übernommene Datensätze melden already_transferred | wie bei jedem Token |
POST /v1/damages, PATCH /v1/damages/{id}/status | Token; beide Endpunkte teilen sich die Schlüssel | 90 Tage. Gehört der Schlüssel zu einem gelöschten Schaden, antwortet die API 404, legt aber nichts neu an | Neuer Vorgang |
POST /v1/appointments | Organisation | Solange der Termin existiert; ein gelöschter Termin wird bei Wiederholung neu angelegt | Treffer: derselbe Vorgang |
POST /v1/meters/{id}/readings | Token | Solange die Ablesung existiert (auch eine stornierte bleibt Treffer); die Wiederholung wird nicht erneut auf Plausibilität geprüft | Neuer Vorgang |
POST /v1/prospects | Token | Solange die Anfrage existiert | Neuer Vorgang |
POST /v1/datev/exports | Organisation | Solange der Export-Eintrag existiert (die Datei selbst ist nur 24 Stunden abrufbar) | Treffer: derselbe Vorgang |
Zeitfilter
Die neueren Listen haben einen gemeinsamen Zeitfilter: Dokumente, E-Mails, Zugriffsprotokoll, Verträge, Abrechnungen und Zählerstände, DATEV-Exporte, Handwerker-Aufträge, Schäden, Termine, Interessenten, Warteliste und der Zustellverlauf der Webhooks. Welchen Zeitpunkt der Filter meint (Erstellung, letzte Änderung, Termin), steht bei der jeweiligen Anleitung und in der Referenz. Verwenden Sie genau eine der drei Formen; mehrere zugleich ergeben 400 period_conflict.
| Angabe | Bedeutung |
|---|---|
period=today | Heute. Weiter: yesterday, this_month, last_month, last_7_days, last_30_days, last_3_months, last_12_months |
period=all | Kein Zeitfilter — seitenweise über den Cursor |
date=2026-10-04 | Genau ein Kalendertag |
from=…&to=… | Freier Zeitraum. Ein Datum bei to nennt den letzten Tag (einschließlich), ein Zeitpunkt (2026-10-04T08:00:00Z) die ausgeschlossene Obergrenze. from nach to ergibt 400 range_invalid |
Ohne Angabe gelten die letzten 30 Tage, nicht „alle“ — so entsteht kein versehentlich riesiger Abruf. Ausnahmen, die bei der jeweiligen Anleitung stehen: Einheiten, Zähler und Handwerksbetriebe (Stammdaten) haben keinen Zeitfilter, Termine lassen sich mit upcoming=true ab jetzt abrufen, und für Abrechnungen mit year, lease_id oder updated_since gilt ohne Zeitangabe period=all. Tage und Monate rechnen wir in der Zeitzone Europe/Berlin (mit Sommer- und Winterzeit). Ältere Listen (Buchungen, Rechnungen, Zahlungseingänge) behalten ihre bisherigen Parameter from und to als reine Kalendertage.
Als Datei abrufen (CSV, Excel)
Buchungen, Rechnungen, Zahlungseingänge, Mahnungen, offene Posten, E-Mails, Verträge, Abrechnungen, Zähler und Zählerstände, Handwerksbetriebe und -aufträge, Schäden, Termine, Interessenten und die Warteliste liefern auf Wunsch eine Datei statt JSON: ?format=csv oder ?format=xlsx. Bei CSV wählen Sie mit delimiter das Trennzeichen (; für deutsches Excel, Vorgabe, oder ,; auch als Wort semicolon und comma). Die Datei ist UTF-8 mit Kennzeichen für Umlaute. Fehler kommen immer als JSON.
curl -H "Authorization: Bearer $TOKEN" -o buchungen.csv "https://api.manage2hero.com/bookings?from=2026-09-01&to=2026-09-30&format=csv"Eine Datei enthält höchstens eine Seite (bis 200 Zeilen). Den Cursor der nächsten Seite liefert die Antwort im Kopf X-Next-Cursor; fehlt der Kopf, war es die letzte Seite. Texte, die mit =, +, - oder @ beginnen, bekommen ein vorangestelltes ', damit Excel sie nicht als Formel ausführt; Zahlen und Beträge bleiben echte Zahlen. Mehrere Werte einer Zeile (etwa die Zuordnungen einer Sammelzahlung) stehen als JSON-Text in einer Zelle. Excel lehnt Zellen über 32.767 Zeichen ab; die API meldet dann 400 unsupported_format mit dem Hinweis auf CSV oder JSON.
Formate
Zeitpunkte als ISO 8601 in UTC (2026-09-24T08:15:00.000Z), Kalenderdaten als JJJJ-MM-TT, Quoten als Dezimalbruch zwischen 0 und 1 (0.8667 = 86,67 %). Zeichensatz UTF-8.