Referenz
Maschinenlesbar als OpenAPI 3.1: /openapi.json — z. B. zum Erzeugen eines Clients oder zum Import in Postman/Insomnia.
Die Webhook-Aufrufe samt Abgleich als importfertige Postman-Sammlung: /openapi.json?format=postman.
GET/v1/locations
Alle Garagenparks Ihrer Organisation, alphabetisch nach Name. Mit `active=true` nur aktive Standorte.
Scope: locations:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
active | Query | true = nur aktive, false = nur inaktive Garagenparks bzw. Handwerksbetriebe bzw. Zähler |
curl -s "https://api.manage2hero.com/v1/locations" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/locations" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/locations', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/locations",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"code": "HH01",
"name": "Garagenpark Hamburg Nord",
"street": "Beispielweg",
"house_number": "12",
"postal_code": "22297",
"city": "Hamburg",
"country_code": "DE",
"active": true
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/occupancy
Belegungszahlen je Garagenpark. `occupancy_rate` = vermietete Einheiten ÷ alle Einheiten (0–1). Gekündigte, noch laufende Verträge zählen als vermietet; reservierte und gesperrte Einheiten stehen im Nenner. Die Rohzahlen sind immer dabei. Mit `group_by=storage_type` enthält jeder Garagenpark zusätzlich `storage_types`: dieselben Zahlen je Garagentyp.
Scope: occupancy:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
group_by | Query | Belegung: storage_type = zusätzlich je Garagentyp (storage_types). Umsatz: month (Vorgabe), category oder location. Offene Posten (Summen): location |
curl -s "https://api.manage2hero.com/v1/occupancy" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/occupancy" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/occupancy', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/occupancy",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"location_code": "HH01",
"location_name": "Garagenpark Hamburg Nord",
"as_of": "2026-09-24T08:15:00.000Z",
"units_total": 120,
"units_rented": 104,
"units_reserved": 3,
"units_blocked": 2,
"units_available": 11,
"occupancy_rate": 0.8667,
"occupancy_rate_definition": "rented_of_total"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/locations/{id}/occupancy
Wie `/v1/occupancy`, für genau einen Garagenpark (auch mit `group_by=storage_type`). Unbekannte oder fremde Id: 404.
Scope: occupancy:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
group_by | Query | Belegung: storage_type = zusätzlich je Garagentyp (storage_types). Umsatz: month (Vorgabe), category oder location. Offene Posten (Summen): location |
curl -s "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/occupancy" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/occupancy" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/occupancy', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/occupancy",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"location_code": "HH01",
"location_name": "Garagenpark Hamburg Nord",
"as_of": "2026-09-24T08:15:00.000Z",
"units_total": 120,
"units_rented": 104,
"units_reserved": 3,
"units_blocked": 2,
"units_available": 11,
"occupancy_rate": 0.8667,
"occupancy_rate_definition": "rented_of_total"
},
"request_id": "req_4n2Xc9…"
}GET/v1/locations/{id}/tenants
Eine Zeile je laufendem Vertrag, sortiert nach Einheitennummer. „Laufend“ umfasst die Kündigungs- und Auszugsphase (`lease.status = terminated`). Enthält nur Kontaktdaten — keine Postadresse, keine Bankdaten, kein Geburtsdatum.
Scope: tenants:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
curl -s "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"lease": {
"id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01-0042",
"status": "active",
"lease_start": "2023-04-01",
"lease_end": null
},
"unit": {
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"number": "42"
},
"tenant": {
"id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
"customer_number": "10042",
"salutation": "mr",
"first_name": "Max",
"last_name": "Mustermann",
"company": null,
"email": "max.mustermann@example.org",
"phone": "+49 40 1234567",
"mobile": null
}
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/tenant-imports
Legt einen Import für genau einen Garagenpark an (1–500 Datensätze, Body höchstens 1 MB). Die Datensätze werden wie beim Datei-Import geprüft; fehlerhafte werden mit Fehlercodes angelegt und lassen sich in der Verwaltung korrigieren. Zuordnen und Übernehmen erfolgt im Import-Assistenten der Verwaltung oder mit `POST /v1/tenant-imports/{id}/commit` (eigener Scope `tenants:import_commit`). Bankdaten (`iban`, `bic`, `account_holder`) sind nicht erlaubt. Gleicher `Idempotency-Key` mit gleichem Inhalt liefert denselben Import (200), mit anderem Inhalt 409. Eigene Grenzen je Organisation (Vorgabe 50 offene Importe, 20 neue je Token und Stunde; anhebbar über den Support): 422 `import_limit_reached` bzw. 429 `import_rate_limited`, Grenze und Stand in `error.details`. Feste Gesamtgrenze: 500 Importe je Organisation (Datei und API, auch abgeschlossene) → 422 `import_total_limit_reached`.
Scope: tenants:import
curl -s -X POST "https://api.manage2hero.com/v1/tenant-imports" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data @import.json$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = Get-Content -Raw -Encoding UTF8 import.json
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/tenant-imports" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-tenant-imports-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/tenant-imports', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"name": "Übernahme aus Altsystem",
"records": [
{
"salutation": "Herr",
"first_name": "Max",
"last_name": "Mustermann",
"date_of_birth": "1980-05-17",
"email": "max.mustermann@example.org",
"street": "Musterstraße",
"house_number": "1",
"postal_code": "22297",
"city": "Hamburg",
"phone": "+49 40 1234567",
"contract_number": "HH01 42-1",
"lease_start": "2023-04-01",
"rent_net": 89.5,
"deposit": 150,
"has_electricity_230v": true,
"electricity_advance_payment": 10,
"electricity_meter_fee": 2.5
}
]
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const { id, records } = body.data; // Import-Id und Ergebnis je Datensatzimport hashlib, json, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-tenant-imports-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/tenant-imports",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json=json.load(open("import.json", encoding="utf-8")),
timeout=60,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"]["id"], body["data"]["records"])Beispielantwort
{
"success": true,
"data": {
"id": "01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"name": "Übernahme aus Altsystem",
"status": "pending",
"record_count": 1,
"error_count": 0,
"transferred_count": 0,
"created_at": "2026-09-24T08:15:00.000Z",
"completed_at": null,
"records": [
{
"row": 1,
"status": "pending",
"errors": [],
"warnings": [],
"storage_unit_id": null,
"tenant_id": null,
"lease_id": null,
"transferred_at": null,
"error_code": null
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/tenant-imports/{id}
Status des Imports und je Datensatz Prüfergebnis (Codes), Zuordnung und nach der Übernahme `tenant_id`/`lease_id`. Keine Feldwerte. Nur Importe, die über die API angelegt wurden; sonst 404.
Scope: tenants:import
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
curl -s "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"name": "Übernahme aus Altsystem",
"status": "pending",
"record_count": 1,
"error_count": 0,
"transferred_count": 0,
"created_at": "2026-09-24T08:15:00.000Z",
"completed_at": null,
"records": [
{
"row": 1,
"status": "pending",
"errors": [],
"warnings": [],
"storage_unit_id": null,
"tenant_id": null,
"lease_id": null,
"transferred_at": null,
"error_code": null
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}DELETE/v1/tenant-imports/{id}
Bricht einen über die API angelegten Import ab und löscht ihn samt Datensätzen, damit fehlgeschlagene oder überflüssige Importe die Grenze der offenen Importe nicht belegen. In der Verwaltung angelegte Importe, unbekannte oder fremde Ids und bereits gelöschte Importe: 404. Ist mindestens ein Datensatz des Imports schon übernommen, wird nichts gelöscht: 409 `session_has_transferred_records` (Mieter und Verträge verweisen auf den Import). Das Löschen eines Imports, der jünger als eine Stunde ist, zählt gegen die Stundengrenze neuer Importe je Token: 429 `import_rate_limited`. Keine Query-Parameter und kein Body. Der Scope `tenants:import` genügt; zusätzlich braucht der Ersteller des Tokens in der Verwaltung das Recht Datenimport mit Vollzugriff, wie beim Löschen eines Imports dort (sonst 403 `insufficient_permissions`).
Scope: tenants:import
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X DELETE "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Method Delete -Uri "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.delete(
"https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"session_id": "01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
"deleted": true
},
"request_id": "req_4n2Xc9…"
}POST/v1/tenant-imports/{id}/commit
Übernimmt die Datensätze eines API-Imports wie der Import-Assistent: je Datensatz Mieter, laufender Vertrag mit Kostensatz, Anfangs-Zählerstand und Dauerbuchung ab dem nächsten Monatsersten (keine rückwirkenden Buchungen). Datensätze ohne Garage werden über die Vertragsnummer zugeordnet. `on_duplicate` regelt Datensätze, deren E-Mail schon einem Mieter gehört: `reject` (Vorgabe) übernimmt sie nicht (`skipped`, `duplicate_tenant`; im Import bleibt der Datensatz `pending` mit `error_code: tenant_decision_required` — die Antwort sagt `skipped`, der Status im Import ändert sich nicht), `reuse` hängt den Vertrag an diesen Mieter (nur bei genau einem Treffer), `create_new` legt einen neuen Mieter an. Ohne `rows` werden alle Datensätze übernommen. Wiederholen ist unschädlich: bereits übernommene Datensätze melden `already_transferred` (mit `tenant_id`, `lease_id` und `contract_number`). Body höchstens 64 KB (413 `payload_too_large`). Fehlerhafte Datensätze melden `failed` mit Code und lassen sich in der Verwaltung korrigieren; `record_changed` heißt, dass der Datensatz während der Übernahme in der Verwaltung geändert wurde — einfach erneut aufrufen. Endgültig — eine Übernahme lässt sich über die API nicht zurücknehmen.
Scope: tenants:import_commit
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X POST "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f/commit" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data @import.json$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = Get-Content -Raw -Encoding UTF8 import.json
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f/commit" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-tenant-imports-01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f-commit-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f/commit', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"on_duplicate": "reject"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const { id, records } = body.data; // Import-Id und Ergebnis je Datensatzimport hashlib, json, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-tenant-imports-01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f-commit-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f/commit",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json=json.load(open("import.json", encoding="utf-8")),
timeout=60,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"]["id"], body["data"]["records"])Beispielantwort
{
"success": true,
"data": {
"session_id": "01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f",
"on_duplicate": "reject",
"created": 1,
"already_transferred": 0,
"skipped": 1,
"failed": 1,
"records": [
{
"row": 1,
"outcome": "created",
"code": null,
"tenant_id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01 42-1",
"warnings": []
},
{
"row": 2,
"outcome": "skipped",
"code": "duplicate_tenant",
"tenant_id": null,
"lease_id": null,
"contract_number": null,
"warnings": []
},
{
"row": 3,
"outcome": "failed",
"code": "unit_occupied",
"tenant_id": null,
"lease_id": null,
"contract_number": null,
"warnings": []
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/occupancy/history
Belegung je Tag (`interval=day`, höchstens 366 Kalendertage einschließlich `from` und `to`) oder je Monat (`interval=month`, Vorgabe, höchstens 1.831 Kalendertage einschließlich `from` und `to`). Ohne `location_id` die Summe aller Garagenparks Ihrer Organisation — die Quote entsteht aus den summierten Einheiten. Gemessen wird einmal je Stunde; der Tageswert ist der letzte Messwert des Tages (deutsche Zeit), der Monatswert der letzte vorhandene Tageswert des Monats. `partial: true` kennzeichnet den laufenden Tag bzw. Monat. Die Messung begann mit der Einführung dieses Endpunkts (`history_starts_on`); frühere Zeiträume sind leer, es wird nichts zurückgerechnet. Tage ohne Messung (Ausfall) werden nicht aufgefüllt, sondern in `missing_days` genannt. Wird ein Garagenpark gelöscht, verschwindet auch sein Verlauf. Zu lange Zeiträume: 400 `range_too_large`.
Scope: occupancy:read
| Parameter | Ort | Beschreibung |
|---|---|---|
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
interval | Query | day = je Tag (höchstens 366 Kalendertage einschließlich from und to), month = je Monat (Vorgabe, höchstens 1.831 Kalendertage einschließlich from und to) |
curl -s "https://api.manage2hero.com/v1/occupancy/history?from=2026-07-01&to=2026-09-30" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/occupancy/history?from=2026-07-01&to=2026-09-30" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/occupancy/history?from=2026-07-01&to=2026-09-30', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/occupancy/history?from=2026-07-01&to=2026-09-30",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"location_id": null,
"from": "2026-07-01",
"to": "2026-09-30",
"interval": "month",
"history_starts_on": "2026-07-14",
"occupancy_rate_definition": "rented_of_total",
"points": [
{
"period": "2026-07",
"date": "2026-07-31",
"units_total": 240,
"units_rented": 205,
"units_reserved": 6,
"units_blocked": 4,
"units_available": 25,
"occupancy_rate": 0.8542,
"partial": false
},
{
"period": "2026-08",
"date": "2026-08-31",
"units_total": 240,
"units_rented": 209,
"units_reserved": 5,
"units_blocked": 4,
"units_available": 22,
"occupancy_rate": 0.8708,
"partial": false
},
{
"period": "2026-09",
"date": "2026-09-28",
"units_total": 240,
"units_rented": 211,
"units_reserved": 3,
"units_blocked": 4,
"units_available": 22,
"occupancy_rate": 0.8792,
"partial": true
}
],
"missing_days": [
"2026-08-17"
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/bookings
Alle Buchungen Ihrer Organisation mit Buchungsdatum im Zeitraum `from`–`to` (höchstens 366 Kalendertage einschließlich `from` und `to`, sonst 400 `range_too_large`), sortiert nach Buchungsdatum. Mit Kontonummern (SKR03) für Soll und Haben, Beträgen netto/Steuer/brutto (`vat_rate` als Anteil, 0.19 = 19 %) und Belegnummer. `category`: rent, deposit, electricity, water, damage, dunning_fee, revenue_other, overpayment — unbekannte neue Belegarten erscheinen als `other`. Gegenbuchungen von Zahlungen (`is_counter_posting`) sind enthalten; mit `include_counter_postings=false` ausblenden. Buchungen ohne Garage haben `location_id = null` und erscheinen nur ohne Standortfilter. Stornierte Buchungen stehen mit `status = cancelled` im Journal. `is_payment_planning_target=true` kennzeichnet `due_date` ausschließlich als unverbindliches Zahlungsziel, keine Mahnfälligkeit; dies gilt auch für CSV, Excel und den Abruf per Kennung.
Scope: bookings:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
lease_id | Query | Nur dieser Vertrag |
category | Query | Buchungen: Belegart (rent, deposit, electricity, water, damage, dunning_fee, revenue_other, overpayment). Dokumente: Hauptordner des Dokuments |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
include_counter_postings | Query | false = Gegenbuchungen von Zahlungen ausblenden (Vorgabe true) |
curl -s "https://api.manage2hero.com/v1/bookings?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/bookings?from=2026-09-01&to=2026-09-30" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/bookings?from=2026-09-01&to=2026-09-30', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/bookings?from=2026-09-01&to=2026-09-30",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"booking_date": "2026-09-01",
"due_date": "2026-09-03",
"is_payment_planning_target": false,
"document_number": "RE-2026-00412",
"category": "rent",
"booking_text": "Miete 09/2026 Garage 42",
"debit_account": "10042",
"credit_account": "8400",
"amount_net": 84.03,
"vat_rate": 0.19,
"vat_amount": 15.97,
"amount_gross": 100,
"status": "open",
"is_counter_posting": false,
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"unit_id": "3a4b5c6d-7e8f-4a0b-8c1d-2e3f4a5b6c7d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/revenue
Nettoumsatz im Zeitraum `from`–`to` (höchstens 1.831 Kalendertage einschließlich `from` und `to`), gruppiert mit `group_by=month` (Vorgabe, jeder Monat auch mit 0), `category` oder `location`. Dieselbe Zahl wie im Dashboard der Verwaltung: Haben minus Soll auf den Erlöskonten, ohne Gegenbuchungen und ohne stornierte Buchungen; Gutschriften mindern den Umsatz. Zeitbezug ist das Buchungsdatum. **Stornos wirken rückwirkend:** Eine stornierte Buchung fällt aus ihrem ursprünglichen Monat heraus — abgerufene Werte früherer Monate können sich deshalb später ändern. Buchungen ohne Garage stehen bei `group_by=location` unter `key = null`.
Scope: bookings:read
| Parameter | Ort | Beschreibung |
|---|---|---|
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
group_by | Query | Belegung: storage_type = zusätzlich je Garagentyp (storage_types). Umsatz: month (Vorgabe), category oder location. Offene Posten (Summen): location |
curl -s "https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-03-31" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-03-31" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-03-31', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-03-31",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"from": "2026-01-01",
"to": "2026-03-31",
"location_id": null,
"group_by": "month",
"currency": "EUR",
"revenue_net_total": 30512.4,
"groups": [
{
"key": "2026-01",
"name": null,
"revenue_net": 10150.2
},
{
"key": "2026-02",
"name": null,
"revenue_net": 10080
},
{
"key": "2026-03",
"name": null,
"revenue_net": 10282.2
}
],
"definition": "revenue_accounts_credit_minus_debit_excl_cancelled"
},
"request_id": "req_4n2Xc9…"
}GET/v1/tenants/{id}
Kontaktdaten eines Mieters Ihrer Organisation — auch ohne laufenden Vertrag (`has_current_lease = false`), etwa für offene Forderungen ehemaliger Mieter. Keine Postadresse, keine Bankdaten, kein Geburtsdatum. Unbekannte oder fremde Id: 404.
Scope: tenants:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"customer_number": "10042",
"salutation": "mr",
"first_name": "Max",
"last_name": "Muster",
"company": null,
"email": "max@example.org",
"phone": "040 1234567",
"mobile": null,
"has_current_lease": false
},
"request_id": "req_4n2Xc9…"
}GET/v1/invoices
Rechnungen Ihrer Organisation mit Rechnungsdatum im Zeitraum `from`–`to` (höchstens 1.831 Kalendertage einschließlich `from` und `to`), sortiert nach Datum. Entwürfe erscheinen nie. `status`: open, partially_paid, paid, cancelled, dunning. Stornos: `is_cancellation` kennzeichnet eine Stornorechnung, `cancels_invoice_id` und `cancelled_by_invoice_id` verknüpfen Original und Storno. `amount_paid` enthält nur an der Rechnung erfasste Zahlungen — Bankzuordnungen stehen in `/v1/payments`, der tatsächliche Restbetrag einer Mietforderung in `/v1/open-items`. Keine Anschrift und keine E-Mail des Kunden. `is_payment_planning_target=true` kennzeichnet `due_date` ausschließlich als unverbindliches Zahlungsziel, keine Mahnfälligkeit; dies gilt auch für Detail, CSV, Excel und den Abruf per Kennung.
Scope: invoices:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
lease_id | Query | Nur dieser Vertrag |
tenant_id | Query | Nur dieser Mieter |
curl -s "https://api.manage2hero.com/v1/invoices?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/invoices?from=2026-09-01&to=2026-09-30" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/invoices?from=2026-09-01&to=2026-09-30', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/invoices?from=2026-09-01&to=2026-09-30",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
"invoice_number": "RE-2026-00412",
"kind": "recurring_rent",
"status": "partially_paid",
"invoice_date": "2026-09-01",
"service_date": "2026-09-01",
"due_date": "2026-09-03",
"is_payment_planning_target": false,
"subject": "Miete September 2026",
"total_net": 84.03,
"total_vat": 15.97,
"total_gross": 100,
"amount_paid": 40,
"paid_at": null,
"is_cancellation": false,
"cancels_invoice_id": null,
"cancelled_by_invoice_id": null,
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"customer_number": "10042",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"unit_id": "3a4b5c6d-7e8f-4a0b-8c1d-2e3f4a5b6c7d"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/invoices/{id}
Wie ein Eintrag aus `/v1/invoices`, dazu die Positionen. Unbekannte oder fremde Id und Entwürfe: 404.
Scope: invoices:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/invoices/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/invoices/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/invoices/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/invoices/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
"invoice_number": "RE-2026-00412",
"kind": "recurring_rent",
"status": "partially_paid",
"invoice_date": "2026-09-01",
"service_date": "2026-09-01",
"due_date": "2026-09-03",
"is_payment_planning_target": false,
"subject": "Miete September 2026",
"total_net": 84.03,
"total_vat": 15.97,
"total_gross": 100,
"amount_paid": 40,
"paid_at": null,
"is_cancellation": false,
"cancels_invoice_id": null,
"cancelled_by_invoice_id": null,
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"customer_number": "10042",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"unit_id": "3a4b5c6d-7e8f-4a0b-8c1d-2e3f4a5b6c7d",
"items": [
{
"position": 1,
"description": "Miete Garage 42, September 2026",
"quantity": 1,
"unit": "Monat",
"unit_price_net": 84.03,
"discount_percent": 0,
"vat_rate": 0.19,
"line_net": 84.03,
"line_vat": 15.97,
"line_gross": 100
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/payments
Eingegangene Bankumsätze (Betrag > 0) mit Buchungsdatum im Zeitraum `from`–`to` (höchstens 366 Kalendertage einschließlich `from` und `to`), sortiert nach Datum, mit ihrer Zuordnung: `allocations` nennt je Buchung den Vertrag und den Betrag — eine Zahlung kann auf mehrere Verträge verteilt sein. `overpayment_amount` = bei der Zuordnung als Überzahlung auf den Vertrag verbuchter Rest, `unallocated_amount` = weder zugeordnet noch als Überzahlung verbucht. Nur Bankumsätze: Manuell an einer Rechnung erfasste Zahlungen stehen in `/v1/invoices` (`amount_paid`). **Bankdaten nur mit Zusatz-Scope:** Gegenkonto (`counterparty`: Name, IBAN, BIC, Bank), Verwendungszweck, Mandatsreferenz und Gläubiger-Id erscheinen nur, wenn das Token zusätzlich `payments:read_counterparty` hat — sonst fehlen diese Felder ganz. Bei Umsätzen aus der Bankanbindung sind `bic`, `bank_name` und `creditor_id` häufig `null`, weil die Bank sie nicht liefert.
Scope: payments:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
match_status | Query | Zuordnung: unmatched, partial, matched, ignored |
curl -s "https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19",
"booking_date": "2026-09-02",
"value_date": "2026-09-02",
"amount": 100,
"currency": "EUR",
"match_status": "matched",
"allocated_amount": 100,
"overpayment_amount": 0,
"unallocated_amount": 0,
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"is_potential_duplicate": false,
"allocations": [
{
"booking_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"amount": 100
}
]
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/open-items
Offene Forderungen mit Restbetrag (`amount_open`), sortiert nach Fälligkeit. Enthalten sind vertragsgebundene Forderungen (Soll auf einem Debitorkonto) — dieselbe Menge wie die Forderungsübersicht der Verwaltung. Gutschriften, Gegenbuchungen und Überzahlungen zählen nicht; Teilzahlungen (Bankzuordnung und an der Rechnung erfasste Zahlung) sind abgezogen; was eine Schlussabrechnung nach Auszug auf das Schlusskonto übertragen oder mit Guthaben gedeckt hat, zählt nicht mehr (der Rest steht als eigener Posten auf dem Schlusskonto). **Einzelrechnungen ohne Vertrag fehlen** — sie stehen mit Status und bezahltem Betrag in `/v1/invoices`. Filter: `location_id`, `lease_id`, `tenant_id` (Kontaktdaten über `GET /v1/tenants/{id}`), `overdue_only=true`. `days_overdue` zählt nach deutschem Datum.
Scope: open_items:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
lease_id | Query | Nur dieser Vertrag |
tenant_id | Query | Nur dieser Mieter |
overdue_only | Query | true = nur überfällige Forderungen |
curl -s "https://api.manage2hero.com/v1/open-items" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/open-items" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/open-items', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/open-items",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"document_number": "RE-2026-00388",
"category": "rent",
"booking_text": "Miete 08/2026 Garage 42",
"due_date": "2026-08-03",
"days_overdue": 56,
"amount_gross": 100,
"amount_paid": 40,
"amount_open": 60,
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01-0042",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"customer_number": "10042",
"unit_id": "3a4b5c6d-7e8f-4a0b-8c1d-2e3f4a5b6c7d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"dunning_level": 1,
"excluded_from_dunning": false
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/open-items/summary
Anzahl und Summe der offenen Forderungen, davon überfällig, und die Altersklassen `not_due`, `1_30`, `31_60`, `61_90`, `over_90` (Tage nach Fälligkeit). Mit `group_by=location` zusätzlich je Garagenpark. Enthalten sind vertragsgebundene Forderungen (Soll auf einem Debitorkonto) — dieselbe Menge wie die Forderungsübersicht der Verwaltung. Gutschriften, Gegenbuchungen und Überzahlungen zählen nicht; Teilzahlungen (Bankzuordnung und an der Rechnung erfasste Zahlung) sind abgezogen; was eine Schlussabrechnung nach Auszug auf das Schlusskonto übertragen oder mit Guthaben gedeckt hat, zählt nicht mehr (der Rest steht als eigener Posten auf dem Schlusskonto). **Einzelrechnungen ohne Vertrag fehlen** — sie stehen mit Status und bezahltem Betrag in `/v1/invoices`.
Scope: open_items:read
| Parameter | Ort | Beschreibung |
|---|---|---|
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
group_by | Query | Belegung: storage_type = zusätzlich je Garagentyp (storage_types). Umsatz: month (Vorgabe), category oder location. Offene Posten (Summen): location |
curl -s "https://api.manage2hero.com/v1/open-items/summary" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/open-items/summary" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/open-items/summary', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/open-items/summary",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"as_of": "2026-09-28",
"location_id": null,
"count": 37,
"amount_open": 4210.5,
"overdue_count": 29,
"overdue_amount": 3580.5,
"aging": [
{
"bucket": "not_due",
"count": 8,
"amount": 630
},
{
"bucket": "1_30",
"count": 14,
"amount": 1420
},
{
"bucket": "31_60",
"count": 9,
"amount": 1210.5
},
{
"bucket": "61_90",
"count": 4,
"amount": 650
},
{
"bucket": "over_90",
"count": 2,
"amount": 300
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/dunning/leases
Alle Verträge mit mindestens einer überfälligen Forderung: offene Summe, offene Mahngebühren, höchste bereits gemahnte Stufe (`dunning_level`), die Stufe, die eine Mahnung heute setzen würde (`next_level`, null = derzeit keine Mahnung möglich, z. B. weil die Zahlungsfrist der letzten Mahnung noch läuft) und ob eine Kündigung empfohlen ist — berechnet wie in der Forderungsübersicht der Verwaltung mit den dort eingestellten Mahnstufen. Sortiert nach `lease_id`. Mit `min_level` nur Verträge ab dieser gemahnten Stufe; eine Seite kann dann weniger Einträge als `limit` haben. Mahnungen werden über die API nicht ausgelöst.
Scope: dunning:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
min_level | Query | Nur Verträge ab dieser bereits gemahnten Stufe (0–5) |
curl -s "https://api.manage2hero.com/v1/dunning/leases" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/leases" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/dunning/leases', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/dunning/leases",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"as_of": "2026-09-28",
"items": [
{
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01-0042",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"total_open": 160,
"total_open_fees": 5,
"oldest_due_date": "2026-07-03",
"max_days_overdue": 87,
"dunning_level": 2,
"next_level": 3,
"termination_recommended": false,
"items": [
{
"open_item_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"document_number": "RE-2026-00301",
"due_date": "2026-07-03",
"days_overdue": 87,
"amount_open": 100,
"dunning_level": 2,
"next_level": 3,
"excluded_from_dunning": false
}
]
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/dunning/notices
Verbindliche Mahnungen mit Ausstellungsdatum im Zeitraum `from`–`to` (höchstens 1.831 Kalendertage einschließlich `from` und `to`), sortiert nach Datum: Stufe, Zahlungsfrist, Summen, Herkunft (`manual`/`automatic`) und Versandzeitpunkt (`sent_at`, null = noch nicht versendet). Ohne Empfängeradresse und ohne PDF.
Scope: dunning:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
lease_id | Query | Nur dieser Vertrag |
level | Query | Nur Mahnungen dieser Stufe (1–5) |
curl -s "https://api.manage2hero.com/v1/dunning/notices?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/notices?from=2026-09-01&to=2026-09-30" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/dunning/notices?from=2026-09-01&to=2026-09-30', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/dunning/notices?from=2026-09-01&to=2026-09-30",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01-0042",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"level": 2,
"issued_on": "2026-09-10",
"payment_deadline": "2026-09-17",
"total_open": 160,
"total_fees": 5,
"total_due": 165,
"source": "automatic",
"sent_at": "2026-09-10T06:02:11.000Z"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/dunning/notices/{id}
Wie ein Eintrag aus `/v1/dunning/notices`, dazu die gemahnten Forderungen mit Stufe, offenem Betrag zum Zeitpunkt der Mahnung und Gebühr. Unbekannte oder fremde Id: 404.
Scope: dunning:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/dunning/notices/7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/notices/7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/dunning/notices/7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/dunning/notices/7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01-0042",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"level": 2,
"issued_on": "2026-09-10",
"payment_deadline": "2026-09-17",
"total_open": 160,
"total_fees": 5,
"total_due": 165,
"source": "automatic",
"sent_at": "2026-09-10T06:02:11.000Z",
"items": [
{
"open_item_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"document_number": "RE-2026-00301",
"due_date": "2026-07-03",
"level": 2,
"amount_open": 100,
"fee": 5
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/bookings/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Buchungen. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/bookings/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: bookings:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/bookings/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/bookings/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/bookings/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/bookings/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/bookings/changes
Kennungen der Buchungen, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/bookings/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: bookings:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/bookings/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/bookings/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/bookings/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/bookings/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/bookings/by-ids
Aktueller Stand von 1 bis 100 Buchungen (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: bookings:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/bookings/by-ids?ids=5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/bookings/by-ids?ids=5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/bookings/by-ids?ids=5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/bookings/by-ids?ids=5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"booking_date": "2026-09-01",
"due_date": "2026-09-03",
"is_payment_planning_target": false,
"document_number": "RE-2026-00412",
"category": "rent",
"booking_text": "Miete 09/2026 Garage 42",
"debit_account": "10042",
"credit_account": "8400",
"amount_net": 84.03,
"vat_rate": 0.19,
"vat_amount": 15.97,
"amount_gross": 100,
"status": "open",
"is_counter_posting": false,
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"unit_id": "3a4b5c6d-7e8f-4a0b-8c1d-2e3f4a5b6c7d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b"
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/invoices/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Rechnungen. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/invoices/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: invoices:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/invoices/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/invoices/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/invoices/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/invoices/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/invoices/changes
Kennungen der Rechnungen, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/invoices/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: invoices:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/invoices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/invoices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/invoices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/invoices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/invoices/by-ids
Aktueller Stand von 1 bis 100 Rechnungen (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: invoices:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/invoices/by-ids?ids=2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/invoices/by-ids?ids=2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/invoices/by-ids?ids=2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/invoices/by-ids?ids=2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
"invoice_number": "RE-2026-00412",
"kind": "recurring_rent",
"status": "partially_paid",
"invoice_date": "2026-09-01",
"service_date": "2026-09-01",
"due_date": "2026-09-03",
"is_payment_planning_target": false,
"subject": "Miete September 2026",
"total_net": 84.03,
"total_vat": 15.97,
"total_gross": 100,
"amount_paid": 40,
"paid_at": null,
"is_cancellation": false,
"cancels_invoice_id": null,
"cancelled_by_invoice_id": null,
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"customer_number": "10042",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"unit_id": "3a4b5c6d-7e8f-4a0b-8c1d-2e3f4a5b6c7d"
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/payments/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Zahlungseingänge. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/payments/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: payments:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/payments/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/payments/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/payments/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/payments/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/payments/changes
Kennungen der Zahlungseingänge, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/payments/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: payments:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/payments/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/payments/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/payments/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/payments/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/payments/by-ids
Aktueller Stand von 1 bis 100 Zahlungseingänge (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: payments:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/payments/by-ids?ids=8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/payments/by-ids?ids=8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/payments/by-ids?ids=8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/payments/by-ids?ids=8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "8f7e6d5c-4b3a-4291-8a7b-6c5d4e3f2a19",
"booking_date": "2026-09-02",
"value_date": "2026-09-02",
"amount": 100,
"currency": "EUR",
"match_status": "matched",
"allocated_amount": 100,
"overpayment_amount": 0,
"unallocated_amount": 0,
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"is_potential_duplicate": false,
"allocations": [
{
"booking_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"amount": 100
}
]
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/dunning/notices/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Mahnungen. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/dunning/notices/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: dunning:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/dunning/notices/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/notices/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/dunning/notices/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/dunning/notices/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/dunning/notices/changes
Kennungen der Mahnungen, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/dunning/notices/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: dunning:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/dunning/notices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/notices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/dunning/notices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/dunning/notices/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/dunning/notices/by-ids
Aktueller Stand von 1 bis 100 Mahnungen (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: dunning:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/dunning/notices/by-ids?ids=7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/notices/by-ids?ids=7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/dunning/notices/by-ids?ids=7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/dunning/notices/by-ids?ids=7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "HH01-0042",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"level": 2,
"issued_on": "2026-09-10",
"payment_deadline": "2026-09-17",
"total_open": 160,
"total_fees": 5,
"total_due": 165,
"source": "automatic",
"sent_at": "2026-09-10T06:02:11.000Z"
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/documents/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Dokumente. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/documents/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: documents:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/documents/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/documents/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/documents/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/documents/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/documents/changes
Kennungen der Dokumente, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/documents/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: documents:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/documents/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/documents/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/documents/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/documents/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/documents/by-ids
Aktueller Stand von 1 bis 100 Dokumente (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: documents:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/documents/by-ids?ids=7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/documents/by-ids?ids=7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/documents/by-ids?ids=7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/documents/by-ids?ids=7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"source": "tenant",
"category": "contract_data",
"folder": null,
"file_name": "Mietvertrag HH01-0042.pdf",
"file_size": 184320,
"mime_type": "application/pdf",
"created_at": "2026-09-18T09:41:12.000Z",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"file_available": true,
"settlement_cancelled": false
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/emails/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der E-Mails. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/emails/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: emails:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/emails/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/emails/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/emails/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/emails/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/emails/changes
Kennungen der E-Mails, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/emails/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: emails:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/emails/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/emails/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/emails/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/emails/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/emails/by-ids
Aktueller Stand von 1 bis 100 E-Mails (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: emails:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/emails/by-ids?ids=7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/emails/by-ids?ids=7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/emails/by-ids?ids=7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/emails/by-ids?ids=7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47",
"template_key": "dunning_notice",
"subject": "Zahlungserinnerung zu Ihrem Garagenstellplatz",
"body_preview": "Guten Tag Herr Muster, Zahlungserinnerung zur Miete September 2026.",
"status": "delivered",
"created_at": "2026-10-02T07:15:42.318204+00:00",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b"
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/contracts/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Verträge. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/contracts/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: contracts:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/contracts/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contracts/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contracts/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contracts/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/contracts/changes
Kennungen der Verträge, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/contracts/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: contracts:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/contracts/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contracts/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contracts/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contracts/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/contracts/by-ids
Aktueller Stand von 1 bis 100 Verträge (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`).
Scope: contracts:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/contracts/by-ids?ids=0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contracts/by-ids?ids=0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contracts/by-ids?ids=0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contracts/by-ids?ids=0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "V-2026-0042",
"status": "termination_received",
"lease_start": "2025-04-01",
"lease_end": "2026-12-31",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"unit_id": "3a7d1e55-2b9c-4f60-8d14-6c0e9b2a7f31",
"location_id": "5e2f8c10-7a3b-4d96-b1c8-0f4e6d2a9b73",
"deposit_amount": 450,
"notice_period_months": 3,
"minimum_lease_months": 12,
"signature": {
"tenant_signed": true,
"tenant_signed_at": "2025-03-12T09:41:07.512330+00:00",
"countersigned": true,
"countersigned_at": "2025-03-13T14:02:55.108421+00:00",
"signed_documents": 3
},
"terminations": [
{
"id": "c41e7f2a-98b3-4d05-a6e1-2b7c3d9f0e18",
"status": "confirmed",
"type": "ordinary",
"terminated_by": "tenant",
"termination_date": "2026-09-15",
"requested_end_date": "2026-12-31",
"end_date": "2026-12-31",
"decided_at": "2026-09-16T08:20:11.004512+00:00",
"created_at": "2026-09-15T18:03:27.771902+00:00"
}
],
"supplementary_agreements": [
{
"id": "e8a05b31-6c2d-4f79-9d83-1a4b7c0e5f26",
"resource_type": "electricity",
"agreement_type": "activation",
"status": "completed",
"requested_date": "2025-05-01",
"effective_date": "2025-05-06",
"created_at": "2025-04-28T10:15:44.201733+00:00"
}
],
"created_at": "2025-03-10T12:00:00.000000+00:00",
"updated_at": "2026-09-16T08:20:11.004512+00:00"
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/damages/changes/cursor
Liefert den Einstieg für den Änderungsabgleich der Schäden. **Vor** einem Vollabruf holen: Alles, was sich danach ändert, liefert `/v1/damages/changes` ab diesem Cursor — auch Änderungen, die während des Vollabrufs noch nicht abgeschlossen waren. Der Cursor gilt nur für diese Ressource: ein Cursor einer anderen Ressource wird mit 400 `invalid_cursor` abgewiesen.
Scope: damages:read
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/damages/changes/cursor" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/damages/changes/cursor" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/damages/changes/cursor', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/damages/changes/cursor",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/damages/changes
Kennungen der Schäden, die sich seit dem Cursor geändert haben — neu, geändert, entfernt oder nicht mehr sichtbar. Jede Kennung per `/v1/damages/by-ids` nachladen: Was dort zurückkommt, ist der aktuelle Stand; eine Kennung, die dort fehlt, lokal entfernen. Die Reihenfolge der Einträge ist nicht die Reihenfolge der Änderungen. Es erscheinen nur abgeschlossene Änderungen, nichts geht verloren. Mit `next_cursor` weiterblättern; ist er `null`, den `resume_cursor` für den nächsten Abgleich merken. Dieselbe Kennung kann mehrfach vorkommen. Änderungen verknüpfter Stammdaten (z. B. Vertragsnummer, Standort einer Garage) lösen kein Ereignis aus; ein gelegentlicher Vollabruf (z. B. wöchentlich) gleicht sie ab. Änderungen werden 90 Tage aufbewahrt; ein älterer Cursor liefert 410 `cursor_expired` — dann neu einsteigen.
Scope: damages:read
| Parameter | Ort | Beschreibung |
|---|---|---|
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
curl -s "https://api.manage2hero.com/v1/damages/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/damages/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/damages/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/damages/changes?cursor=eyJsbyI6IjEyMzQ1NiJ9",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
"changed_at": "2026-09-29T21:14:03.512Z"
}
],
"next_cursor": null,
"resume_cursor": "eyJsbyI6IjEyMzQ1NiJ9"
},
"request_id": "req_4n2Xc9…"
}GET/v1/damages/by-ids
Aktueller Stand von 1 bis 100 Schäden (`ids`, durch Komma getrennt), mit denselben Feldern wie die Liste — ohne Zeitraum und ohne Filter. Unbekannte, fremde oder nicht mehr sichtbare Kennungen fehlen in der Antwort. Mehr als 100 Kennungen ergeben 400 `validation_error` (`details.ids = ["too_many_ids"]`), doppelte Kennungen ebenso (`["duplicate_ids"]`). Die Antwort hat die Listenform (ohne das Feld `documents`); die Kennungen der Dokumente eines Schadens liefert `GET /v1/damages/{id}`.
Scope: damages:read
| Parameter | Ort | Beschreibung |
|---|---|---|
ids | Query | Kennungen, durch Komma getrennt (1–100), z. B. aus …/changes |
curl -s "https://api.manage2hero.com/v1/damages/by-ids?ids=2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/damages/by-ids?ids=2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/damages/by-ids?ids=2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/damages/by-ids?ids=2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
"case_number": "VDF-12-003",
"kind": "unit",
"status": "WEITERE_KLAERUNG",
"process_type": null,
"damage_type": "dent",
"priority": "normal",
"description": "Delle im Rolltor, links unten.",
"damage_date": "2026-10-02",
"reported_at": "2026-10-03T07:41:12.482915+00:00",
"updated_at": null,
"reporter_type": "administration",
"causer_type": null,
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"unit_number": "12",
"location_id": "8e2d4c61-7b05-4a93-9f1c-6d3e5a7b9c20",
"lease_id": null,
"tenant_id": null,
"document_count": 0
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/emails
System-E-Mails, die Ihre Organisation an Mieter versendet hat (Verträge, Abrechnungen, Rechnungen, Mahnungen, Kündigungen, Nachträge, Termine, Protokolle, Entscheidungen, Schadensmeldungen), neueste zuerst. **Zeitfilter** auf den Versandzeitpunkt: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage**; alles gibt es nur mit `period=all` (seitenweise über `next_cursor`). Tage gelten in der Zeitzone Europe/Berlin. Filter: `tenant_id`, `lease_id`, `location_id` (alle Mieter mit einem Vertrag an einer Garage des Standorts; Mails ohne Vertragsbezug erscheinen nur ohne Standortfilter), `template_key`, `status` (`sent`, `delivered`, `opened`). Der Token sieht die ganze Organisation. **Was nie erscheint:** Mails ohne Mieterbezug (Interessenten, Warteliste, Anfragen), Mails mit Zugangsdaten oder Einmal-Links (Passwort- und Zugangsdaten-Mails, Codes, Einladungen, Angebote), nicht gelistete Vorlagen, fehlgeschlagene oder unzustellbare Mails, Testversand (`[Test]` im Betreff) — und nie die Empfängeradresse, die Anbieter-Kennung oder Fehlertexte. `body_preview` ist ein Klartext-Auszug von höchstens 200 Zeichen. **Volltext nur mit Zusatz-Scope:** `body_text` (reiner Text; Tags, Skripte und Stile entfernt, Absätze erhalten, Links ohne Adresse) erscheint nur, wenn das Token zusätzlich `emails:read_body` hat — sonst fehlt das Feld ganz. `body_text` ist Text, kein HTML: beim Anzeigen in einer Webseite wie jeden Text maskieren.
Scope: emails:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
tenant_id | Query | Nur dieser Mieter |
lease_id | Query | Nur dieser Vertrag |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
template_key | Query | Nur E-Mails dieser Vorlage, z. B. nk_abrechnung |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
curl -s "https://api.manage2hero.com/v1/emails" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/emails" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/emails', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/emails",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47",
"template_key": "dunning_notice",
"subject": "Zahlungserinnerung zu Ihrem Garagenstellplatz",
"body_preview": "Guten Tag Herr Muster, Zahlungserinnerung zur Miete September 2026.",
"status": "delivered",
"created_at": "2026-10-02T07:15:42.318204+00:00",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/emails/{id}
Wie ein Eintrag aus `/v1/emails`; mit dem Zusatz-Scope `emails:read_body` zusätzlich `body_text`. Unbekannte oder fremde Id sowie jede E-Mail, die `/v1/emails` nie ausgibt (Interessent, nicht gelistete Vorlage, fehlgeschlagener Versand, Test): 404 `not_found`.
Scope: emails:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/emails/7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/emails/7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/emails/7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/emails/7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "7d3f9a52-1c84-4e6b-b2a0-5f8e9c1d3a47",
"template_key": "dunning_notice",
"subject": "Zahlungserinnerung zu Ihrem Garagenstellplatz",
"body_preview": "Guten Tag Herr Muster, Zahlungserinnerung zur Miete September 2026.",
"status": "delivered",
"created_at": "2026-10-02T07:15:42.318204+00:00",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"body_text": "Guten Tag Herr Muster,\n\nZahlungserinnerung zur Miete September 2026, offener Betrag 100,00 EUR.\n\nFreundliche Grüße\nGaragenverwaltung"
},
"request_id": "req_4n2Xc9…"
}GET/v1/usage
Die externen Abrufe Ihrer Organisation (alle Tokens), neueste zuerst: Token (`token_id`, `token_name`, `token_public_id` — nie das Geheimnis), `method`, `path` (ohne Query-String), `status_code`, Zeitpunkt `created_at` und `request_id`. Aufrufe der Verwaltungsoberfläche erscheinen nicht. **Nie enthalten:** IP-Adressen, User-Agent und Anfrageinhalte. Zeitfilter wie bei allen Listen: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder -Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); ohne Angabe die letzten 30 Tage; mehrere Formen → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. Weitere Filter: `token_id` (ein fremdes oder unbekanntes Token ergibt eine leere Liste), `status` (genauer Code wie `404` oder Klasse `4xx`/`5xx`) und `path_prefix` (z. B. `/v1/invoices`). Mit `next_cursor` weiterblättern. **Aufbewahrung 90 Tage:** ältere Abrufe sind nicht mehr abrufbar. Der Aufruf von `/v1/usage` erscheint selbst im Protokoll. **Abgewiesene Aufrufe** (401), die sich keinem Token zuordnen lassen — fehlender Kopf, anderes Authorization-Schema, ungültiges Format, unbekannte Token-Kennung —, stehen nicht im Protokoll; abgelehnte Anmeldungen mit bekannter Kennung (falsches Geheimnis, widerrufen, abgelaufen) erscheinen nur gedrosselt (höchstens 20 Zeilen je Token und Minute).
Scope: usage:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
token_id | Query | Nur Abrufe dieses API-Tokens |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
path_prefix | Query | Nur Abrufe, deren Pfad so beginnt, z. B. /v1/invoices |
curl -s "https://api.manage2hero.com/v1/usage" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/usage" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/usage', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/usage",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a",
"created_at": "2026-10-05T08:14:22.481Z",
"token_id": "00000000-aaaa-aaaa-aaaa-000000000042",
"token_name": "Buchhaltung Export",
"token_public_id": "xxxxxxxxxxxx",
"method": "GET",
"path": "/v1/invoices",
"status_code": 200,
"request_id": "req_7f3a9c1e2b4d"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/documents
Dokumente Ihrer Organisation — Mieterdokumente (Mieterakte) und Standortdateien — mit **Erstellungsdatum** im Zeitraum, neueste zuerst. Zeitfilter: genau eine Form aus `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (`to` ausgeschlossen); ohne Angabe gelten die letzten 30 Tage (Tagesgrenzen in Europe/Berlin), mehrere Formen liefern 400 `period_conflict`, ein umgekehrter Zeitraum 400 `range_invalid`. Filter: `tenant_id`, `lease_id`, `location_id`, `category`. `tenant_id`, `lease_id` und `category` betreffen nur Mieterdokumente (dann erscheinen keine Standortdateien); `location_id` liefert die Standortdateien und die Mieterdokumente von Verträgen dieses Standorts. Mit `next_cursor` weiterblättern. **Sichtbar** sind Dokumente der sechs Hauptordner der Mieterakte, nicht archiviert und ohne Altkategorien (`category`: damages, communication, contract_data, handover, invoices, other); andere Arten (etwa Ausweise oder Bonitätsunterlagen) erscheinen nie. Das Final-PDF einer stornierten Schlussabrechnung bleibt sichtbar und trägt `settlement_cancelled = true`. Übergabeprotokolle, die nur als Protokoll hinterlegt sind, und Dokumente anderer Bereiche (z. B. Interessenten) gehören nicht dazu. Es gibt nur Metadaten: **nie** Speicherpfad, Bucket oder Datei-Adresse. `file_available = false` heißt, dass zur Zeile keine gültig hinterlegte Datei existiert. Die Datei selbst gibt es nur über `POST /v1/documents/{id}/download-link`. Das Token sieht die ganze Organisation (keine Standortgrenzen).
Scope: documents:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
tenant_id | Query | Nur dieser Mieter |
lease_id | Query | Nur dieser Vertrag |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
category | Query | Buchungen: Belegart (rent, deposit, electricity, water, damage, dunning_fee, revenue_other, overpayment). Dokumente: Hauptordner des Dokuments |
curl -s "https://api.manage2hero.com/v1/documents" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/documents" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/documents', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/documents",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"source": "tenant",
"category": "contract_data",
"folder": null,
"file_name": "Mietvertrag HH01-0042.pdf",
"file_size": 184320,
"mime_type": "application/pdf",
"created_at": "2026-09-18T09:41:12.000Z",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"file_available": true,
"settlement_cancelled": false
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/documents/{id}
Wie ein Eintrag aus `/v1/documents`. Unbekannte, fremde oder archivierte Id sowie nicht freigegebene Kategorien: 404 `not_found`.
Scope: documents:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"source": "tenant",
"category": "contract_data",
"folder": null,
"file_name": "Mietvertrag HH01-0042.pdf",
"file_size": 184320,
"mime_type": "application/pdf",
"created_at": "2026-09-18T09:41:12.000Z",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"file_available": true,
"settlement_cancelled": false
},
"request_id": "req_4n2Xc9…"
}POST/v1/documents/{id}/download-link
Erzeugt einen **Link, der 5 Minuten gültig ist** (`expires_at`), und liefert ihn in `url`. Den Link nicht speichern, nicht weitergeben und nicht protokollieren: Wer ihn hat, kann die Datei laden. Für eine spätere Anfrage einen neuen Link erzeugen — jeder Aufruf erzeugt einen neuen und zählt als schreibender Aufruf (engeres Kontingent je Token und Minute). Braucht den Zusatz-Scope `documents:download` **und** `documents:read` am Token; der Scope ist als sensibel markiert. Kein Body nötig. Unbekannte, fremde oder nicht freigegebene Dokumente: 404 `not_found`; ein Dokument ohne gültig hinterlegte Datei: 404 `file_not_available`.
Scope: documents:download
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f/download-link" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f/download-link" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f/download-link', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/documents/7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f/download-link",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"document_id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"url": "https://<storage-host>/storage/v1/object/sign/…?token=…",
"expires_at": "2026-10-05T12:05:00.000Z",
"expires_in_seconds": 300,
"file_name": "Mietvertrag HH01-0042.pdf",
"mime_type": "application/pdf"
},
"request_id": "req_4n2Xc9…"
}GET/v1/contractors
Die Handwerksbetriebe (ausführende Firmen) Ihrer Organisation, neueste zuerst: `id`, `name`, `active`, `contact_name` (nur der Name des Ansprechpartners) und `categories` (Kategorien der Artikel, die der Betrieb ausführt; leer, wenn keine zugeordnet ist). Filter: `active` (`true`/`false`). Es gibt bewusst keinen Änderungsfilter: `categories` ergibt sich live aus den Artikeln des Betriebs, `updated_at` meldet nur Änderungen am Firmeneintrag selbst — rufen Sie die (kleine) Liste vollständig ab (so fallen auch gelöschte Betriebe auf). Mit `next_cursor` weiterblättern, auf Wunsch als Datei mit `?format=csv` oder `?format=xlsx`. Der Token sieht die ganze Organisation. **Nie enthalten:** Anschrift, Telefon, E-Mail, Fax, Webseite, Steuernummer, USt-Id, Bankverbindung und Notizen. Voraussetzung beim Ersteller des Tokens: die Rechte Artikel, Garagen/Einheiten und Vermietungsprozess, jeweils mindestens Lesen (die Aufträge zeigen Schadensdaten).
Scope: contractors:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
active | Query | true = nur aktive, false = nur inaktive Garagenparks bzw. Handwerksbetriebe bzw. Zähler |
curl -s "https://api.manage2hero.com/v1/contractors" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contractors" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contractors', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contractors",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "3b1f6c2e-8a47-4d9b-9c15-e2a07f4d6b38",
"name": "Malerbetrieb Muster GmbH",
"active": true,
"contact_name": "Erika Beispiel",
"categories": [
"Malerarbeiten",
"Bodenbeläge"
],
"created_at": "2026-09-12T08:21:05.482113+00:00",
"updated_at": "2026-10-02T09:40:17.100245+00:00"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/contractor-orders
Die Reparatur-Aufträge Ihrer Organisation an Handwerker, neueste zuerst — ein Auftrag je Schadensposition. Je Auftrag: `status` (`not_sent` = noch nicht oder nicht mehr versendet, `sent` = versendet bzw. von Hand als versendet markiert, `failed` = Versand fehlgeschlagen), `order_kind` (`time_and_material` = nach Aufwand, `quote_request` = Kostenvoranschlag angefragt), Zeitpunkte `created_at` (Anlage), `sent_at` (Versand, das Auftragsdatum) und `updated_at` (letzte Änderung von Auftrag oder Position), der Betrieb (`contractor_id` aus `/v1/contractors`, bei einer frei erfassten externen Firma leer; `company_name`), der Bezug (`damage_id`, `unit_id`, `location_id` — nur Kennungen; bei Schäden aus Übergabeprotokollen ist `damage_id` die Kennung des Protokollschadens, bei Schäden an Parkflächen ist `unit_id` leer) und die Position (`item_name`, `quantity`, `unit`, `unit_price` netto, `vat_rate`, `is_time_and_material`, `amount_net` = Menge × Einzelpreis, auf 2 Stellen gerundet; leer bei Abrechnung nach Aufwand oder ohne Preis). **Zeitfilter** auf den Anlagezeitpunkt: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage**; alles gibt es nur mit `period=all`. Tage gelten in der Zeitzone Europe/Berlin. Zusätzlich `updated_since` für Änderungen (Auftrag, Position und, solange der Name nicht aus dem Versand stammt, der Betrieb; einschließlich; `YYYY-MM-DD` = Beginn des Tages in Europe/Berlin oder ein ISO-Zeitpunkt). **Allein angegeben, gilt kein Anlage-Zeitraum** (auch nicht die 30 Tage) — so erscheint ein vor Monaten angelegter, heute versendeter Auftrag; zusammen mit `period`, `date` oder `from`/`to` gilt beides (Schnittmenge). **Gelöschte Aufträge** meldet `updated_since` nicht (die Verwaltung kann eine Schadensposition samt Auftrag löschen): gleichen Sie den Bestand regelmäßig mit einem vollständigen Abruf (`period=all`, seitenweise) ab und entfernen Sie Kennungen, die nicht mehr erscheinen. Weitere Filter: `contractor_id`, `status`, `location_id`, `unit_id`; eine fremde oder unbekannte Id → 404 `not_found`. Mit `next_cursor` weiterblättern, auf Wunsch als Datei mit `?format=csv` oder `?format=xlsx`. **Nie enthalten:** Empfängeradresse, Fehlertexte, Versandprotokolle, Anhänge, Begründungen zu Preisänderungen und Nutzerkennungen.
Scope: contractors:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
updated_since | Query | Nur Datensätze, die seit diesem Zeitpunkt geändert wurden (ISO 8601 mit Zone, z. B. 2026-10-01T08:00:00Z). Zähler und Abrechnungen: auch als Datum möglich (erstellt oder geändert seit). Schäden: nur seit diesem Zeitpunkt angelegt oder geändert (kein lückenloser Änderungsstrom: Fenster überlappen lassen). Handwerker-Aufträge: einschließlich dieses Zeitpunkts, auch als YYYY-MM-DD (Beginn des Tages) möglich; allein angegeben, gilt dort kein Anlage-Zeitraum |
contractor_id | Query | Nur Aufträge an diesen Handwerksbetrieb (Id aus GET /v1/contractors) |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
unit_id | Query | Nur diese Einheit (Garage; bei Zählern: Id aus den Einheiten; bei Schäden: Id aus unit.id von GET /v1/locations/{id}/tenants) |
curl -s "https://api.manage2hero.com/v1/contractor-orders" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contractor-orders" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contractor-orders', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contractor-orders",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "8e2d4a61-5b73-4c0f-a9d8-1f6e3b7c2a45",
"status": "sent",
"order_kind": "time_and_material",
"created_at": "2026-10-02T07:15:42.318204+00:00",
"updated_at": "2026-10-02T07:16:03.774512+00:00",
"sent_at": "2026-10-02T07:16:03.774512+00:00",
"sent_manually": false,
"contractor_id": "3b1f6c2e-8a47-4d9b-9c15-e2a07f4d6b38",
"company_name": "Malerbetrieb Muster GmbH",
"damage_id": "c4a9e8d2-1b36-4f75-8e0a-9d2c5b7f1a63",
"unit_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"location_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"item_name": "Wandfläche streichen",
"quantity": 12.5,
"unit": "m²",
"unit_price": 8.4,
"vat_rate": 19,
"is_time_and_material": false,
"amount_net": 105
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/units
Alle Einheiten (Garagen, Stellplätze) Ihrer Organisation mit Standort, Garagentyp, Maßen, Ausstattung, **Status**, aktuellem **Mieter als Kurzangabe** (Kennung und Anzeigename), **Grundmiete** und **Zählern**, zuletzt geänderte zuerst. Filter: `location_id`, `storage_type_id`, `status` (`available`, `reserved`, `rented`, `blocked`). **Zeitfilter** auf die letzte Änderung der Einheit (`updated_at`): genau eine der Formen `period`, `date` oder `from`/`to` (wie bei den übrigen Listen); **ohne Angabe gibt es keinen Zeitfilter** — die Liste ist vollständig, seitenweise über `next_cursor`. Mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. Unbekannter oder fremder Standort bzw. Garagentyp → 404. **Status:** `reserved` gilt bei einer Reservierung nur, solange eine aktive, nicht abgelaufene Reservierung besteht; eine abgelaufene, noch nicht freigegebene Reservierung erscheint bereits als `available`. Manuell oder durch einen Vertragsentwurf reservierte Einheiten bleiben `reserved`. **Aktueller Vertrag** (`lease_id`, `tenant`, `current_rent`) ist der laufende Vertrag am heutigen Tag (inklusive Kündigungs- und Auszugsphase, ohne künftige Verträge); `current_rent` ist die Grundmiete laut heute gültigem Kostensatz des Vertrags (netto, Steuersatz als Bruch, brutto auf Cent gerundet; ohne Nebenkosten und Kaution) und fehlt (`null`) ohne Vertrag. `tenant` enthält nie Adresse, E-Mail oder Telefon. `meters` sind die aktiven Zähler der Einheit. Maße und Fläche stammen von der Einheit, sonst vom Garagentyp. `updated_at` ändert sich nur, wenn die Einheit selbst geändert wird — nicht bei neuem Vertrag oder Mieterwechsel; für aktuelle Belegung die Liste regelmäßig vollständig abrufen. Der Token sieht die ganze Organisation.
Scope: units:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
storage_type_id | Query | Interessenten-Anfragen: nur Anfragen mit Wunsch für diesen Garagentyp (Id eines Garagentyps). Einheiten: nur Einheiten dieses Garagentyps (Id aus dem Feld storage_type_id einer Einheit) |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
curl -s "https://api.manage2hero.com/v1/units" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/units" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/units', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/units",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "5b1e7c3a-2d49-4f60-9a8b-0c7d6e5f4a31",
"number": "A-14",
"location_id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
"location_name": "Garagenpark Nord",
"storage_type_id": "8e7d6c5b-4a39-4281-9706-f5e4d3c2b1a0",
"storage_type_name": "Einzelgarage",
"area": 18.5,
"width": 3,
"depth": 6,
"height": 2.4,
"has_water": false,
"is_insulated": true,
"has_electricity": true,
"has_high_voltage": false,
"status": "rented",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant": {
"id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"display_name": "Erika Muster"
},
"current_rent": {
"net": 85,
"vat_rate": 0.19,
"gross": 101.15
},
"meters": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"type": "electricity",
"meter_number": "EZ-004711"
}
],
"created_at": "2025-03-01T08:00:00.123456+00:00",
"updated_at": "2026-09-28T14:21:07.654321+00:00"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/units/{id}
Wie ein Eintrag aus `/v1/units`. Unbekannte oder fremde Id: 404 `not_found`.
Scope: units:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/units/5b1e7c3a-2d49-4f60-9a8b-0c7d6e5f4a31" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/units/5b1e7c3a-2d49-4f60-9a8b-0c7d6e5f4a31" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/units/5b1e7c3a-2d49-4f60-9a8b-0c7d6e5f4a31', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/units/5b1e7c3a-2d49-4f60-9a8b-0c7d6e5f4a31",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "5b1e7c3a-2d49-4f60-9a8b-0c7d6e5f4a31",
"number": "A-14",
"location_id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
"location_name": "Garagenpark Nord",
"storage_type_id": "8e7d6c5b-4a39-4281-9706-f5e4d3c2b1a0",
"storage_type_name": "Einzelgarage",
"area": 18.5,
"width": 3,
"depth": 6,
"height": 2.4,
"has_water": false,
"is_insulated": true,
"has_electricity": true,
"has_high_voltage": false,
"status": "rented",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant": {
"id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"display_name": "Erika Muster"
},
"current_rent": {
"net": 85,
"vat_rate": 0.19,
"gross": 101.15
},
"meters": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"type": "electricity",
"meter_number": "EZ-004711"
}
],
"created_at": "2025-03-01T08:00:00.123456+00:00",
"updated_at": "2026-09-28T14:21:07.654321+00:00"
},
"request_id": "req_4n2Xc9…"
}GET/v1/contracts
Mietverträge Ihrer Organisation mit Laufzeit, Status, Kaution, Kündigungen, Nachträgen (Strom/Wasser) und Unterschriftsstand, zuletzt geänderte zuerst. **Zeitfilter** auf die letzte Änderung (`updated_at`: der Vertrag selbst, eine Kündigung, ein Nachtrag oder eine Unterschrift/Gegenzeichnung): genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage** — für alle Verträge `period=all` (seitenweise über `next_cursor`); für einen Abgleich seit einem Zeitpunkt `from=<Zeitpunkt>`. **Grenze des Abgleichs:** `updated_at` erfasst neue und geänderte Verträge, Kündigungen, Nachträge und Unterschriften, nicht aber das Zurücksetzen einer Gegenzeichnung, das Löschen einer Kündigung oder eines Nachtrags und gelöschte Verträge — für einen lückenlosen Abgleich nutzen Sie den Änderungsstrom (`GET /v1/contracts/changes`, siehe `changes/cursor` und `by-ids`). Tage gelten in der Zeitzone Europe/Berlin. Filter: `tenant_id`, `location_id` (Verträge an Garagen des Standorts), `status` (Vertragsstatus). Unbekannter Mieter oder Standort der eigenen Organisation → 404. Der Token sieht die ganze Organisation. **Unterschrift:** `signature.tenant_signed`/`tenant_signed_at` (jüngste Unterschrift des Mieters), `countersigned`/`countersigned_at` (alle unterschriebenen Dokumente von der Verwaltung gegengezeichnet), `signed_documents` (Hauptvertrag und Anlagen). Das unterschriebene PDF ist nicht Teil dieses Endpunkts. **Nachträge:** `effective_date` ist der geplante (`planned`) bzw. ausgeführte Tag (`completed`, `confirmed`); Anfragen, Ablehnungen und Stornos haben keins. **Was nie erscheint:** der Vertragstext und -Snapshot, Hashes, IP und Gerät der Unterschrift, Dateipfade, Freitexte und Notizen der Kündigung, Bank- und Ausweisdaten. Verträge, die zum Löschen vorgemerkt sind, erscheinen nicht. Ausgabe auch als `?format=csv` oder `?format=xlsx` (Kündigungen und Nachträge als JSON-Text in einer Zelle).
Scope: contracts:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
tenant_id | Query | Nur dieser Mieter |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
curl -s "https://api.manage2hero.com/v1/contracts" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contracts" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contracts', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contracts",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "V-2026-0042",
"status": "termination_received",
"lease_start": "2025-04-01",
"lease_end": "2026-12-31",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"unit_id": "3a7d1e55-2b9c-4f60-8d14-6c0e9b2a7f31",
"location_id": "5e2f8c10-7a3b-4d96-b1c8-0f4e6d2a9b73",
"deposit_amount": 450,
"notice_period_months": 3,
"minimum_lease_months": 12,
"signature": {
"tenant_signed": true,
"tenant_signed_at": "2025-03-12T09:41:07.512330+00:00",
"countersigned": true,
"countersigned_at": "2025-03-13T14:02:55.108421+00:00",
"signed_documents": 3
},
"terminations": [
{
"id": "c41e7f2a-98b3-4d05-a6e1-2b7c3d9f0e18",
"status": "confirmed",
"type": "ordinary",
"terminated_by": "tenant",
"termination_date": "2026-09-15",
"requested_end_date": "2026-12-31",
"end_date": "2026-12-31",
"decided_at": "2026-09-16T08:20:11.004512+00:00",
"created_at": "2026-09-15T18:03:27.771902+00:00"
}
],
"supplementary_agreements": [
{
"id": "e8a05b31-6c2d-4f79-9d83-1a4b7c0e5f26",
"resource_type": "electricity",
"agreement_type": "activation",
"status": "completed",
"requested_date": "2025-05-01",
"effective_date": "2025-05-06",
"created_at": "2025-04-28T10:15:44.201733+00:00"
}
],
"created_at": "2025-03-10T12:00:00.000000+00:00",
"updated_at": "2026-09-16T08:20:11.004512+00:00"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/contracts/{id}
Wie ein Eintrag aus `/v1/contracts`. Unbekannte oder fremde Id sowie ein zum Löschen vorgemerkter Vertrag: 404 `not_found`.
Scope: contracts:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/contracts/0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contracts/0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/contracts/0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/contracts/0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"contract_number": "V-2026-0042",
"status": "termination_received",
"lease_start": "2025-04-01",
"lease_end": "2026-12-31",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"unit_id": "3a7d1e55-2b9c-4f60-8d14-6c0e9b2a7f31",
"location_id": "5e2f8c10-7a3b-4d96-b1c8-0f4e6d2a9b73",
"deposit_amount": 450,
"notice_period_months": 3,
"minimum_lease_months": 12,
"signature": {
"tenant_signed": true,
"tenant_signed_at": "2025-03-12T09:41:07.512330+00:00",
"countersigned": true,
"countersigned_at": "2025-03-13T14:02:55.108421+00:00",
"signed_documents": 3
},
"terminations": [
{
"id": "c41e7f2a-98b3-4d05-a6e1-2b7c3d9f0e18",
"status": "confirmed",
"type": "ordinary",
"terminated_by": "tenant",
"termination_date": "2026-09-15",
"requested_end_date": "2026-12-31",
"end_date": "2026-12-31",
"decided_at": "2026-09-16T08:20:11.004512+00:00",
"created_at": "2026-09-15T18:03:27.771902+00:00"
}
],
"supplementary_agreements": [
{
"id": "e8a05b31-6c2d-4f79-9d83-1a4b7c0e5f26",
"resource_type": "electricity",
"agreement_type": "activation",
"status": "completed",
"requested_date": "2025-05-01",
"effective_date": "2025-05-06",
"created_at": "2025-04-28T10:15:44.201733+00:00"
}
],
"created_at": "2025-03-10T12:00:00.000000+00:00",
"updated_at": "2026-09-16T08:20:11.004512+00:00"
},
"request_id": "req_4n2Xc9…"
}GET/v1/tenants/{id}/timeline
Ereignisse zu EINEM Mieter aus E-Mail-Verlauf, Mahnungen, Zahlungen, Dokumenten und Vertrag in einer Liste, neueste zuerst. **Datenschutz:** Eine Quelle erscheint nur, wenn das Token ZUSÄTZLICH ihren Lese-Scope hat — `emails:read` (E-Mails), `dunning:read` (Mahnungen), `payments:read` (Zahlungen), `documents:read` (Dokumente), `contracts:read` (Vertrag). `timeline:read` allein liefert eine leere Liste (200, `sources: []`), keinen Fehler. Die Zusatz-Scopes der Einzel-Endpunkte (`emails:read_body`, `payments:read_counterparty`, `documents:download`) gelten hier nicht: Die Zeitleiste enthält nur Kurzangaben — nie Volltext, Gegenpartei, Betrag, Dateipfad oder Download-Link; Details stehen im jeweiligen Einzel-Endpunkt (über `object_id`). `sources` nennt die Quellen, die für Ihr Token enthalten sind. Jedes Ereignis erscheint nur, wenn der Einzel-Endpunkt es für dasselbe Token auch zeigt. **Ereignisse und Zeitpunkt (`occurred_at`):** `email_sent` (Versand der E-Mail), `dunning_notice_issued` (Ausstellungsdatum der Mahnung, Beginn des Tages in Europe/Berlin), `payment_received` (Buchungsdatum des Zahlungseingangs, Beginn des Tages in Europe/Berlin; nur Zahlungen, die dem Mieter über eine gespeicherte Zuordnung zu seinem Vertrag zugeordnet sind — nie über Name oder IBAN erraten), `document_added` (Eingang des Dokuments in die Mieterakte), `contract_created` (Anlage des Vertrags), `contract_signed` (jüngste Unterschrift des Mieters), `contract_countersigned` (jüngste Gegenzeichnung, erst wenn alle unterschriebenen Dokumente gegengezeichnet sind), `termination_filed` (Eingang einer Kündigung, `status` = Kündigungsstatus), `supplementary_agreement_filed` (Eingang eines Nachtrags, `status` = Nachtragsstatus; je Vertrag die jüngsten 50 Kündigungen und 100 Nachträge wie in `/v1/contracts`). Mietbeginn und -ende sind keine Ereignisse. **Felder:** `id` (eindeutig, `<Typ>:<Id der Zeile>` — zum Entdoppeln), `source`, `type`, `object_id` (Id des Einzelobjekts; bei Kündigung und Nachtrag der Vertrag), `title` (E-Mail-Betreff, Dateiname, Vertragsnummer; sonst `null`), `status` (E-Mail-Status, Mahnstufe `level_<n>`, Zuordnungsstatus der Zahlung, Dokumentkategorie, Kündigungs-/Nachtragsstatus; sonst `null`). **Zeitfilter** auf `occurred_at`: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage** — für die gesamte Geschichte `period=all` (seitenweise über `next_cursor`; der Cursor merkt sich die Zeitgrenzen der ersten Seite). Tage gelten in der Zeitzone Europe/Berlin. **Blättern:** Die Folgeseite ist kein Schnappschuss; ändert sich zwischen zwei Abrufen ein berechneter Zeitpunkt (z. B. eine neue Unterschrift verschiebt `contract_signed`), kann ein Ereignis fehlen oder doppelt erscheinen — am `id` entdoppeln, bei Bedarf neu abrufen. Unbekannter oder fremder Mieter: 404. Der Token sieht die ganze Organisation. Keine Ausgabe als CSV/Excel.
Scope: timeline:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
curl -s "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "termination_filed:c41e7f2a-98b3-4d05-a6e1-2b7c3d9f0e18",
"occurred_at": "2026-09-15T18:03:27.771902+00:00",
"source": "contract",
"type": "termination_filed",
"object_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"title": "V-2026-0042",
"status": "requested"
},
{
"id": "payment_received:7d1f3c52-0a4b-4e68-9b21-5c8d7e6f4a30",
"occurred_at": "2026-09-02T22:00:00.000000+00:00",
"source": "payment",
"type": "payment_received",
"object_id": "7d1f3c52-0a4b-4e68-9b21-5c8d7e6f4a30",
"title": null,
"status": "matched"
},
{
"id": "email_sent:2f6a9d14-83c5-4b07-a1e2-9d3c8b7a6f51",
"occurred_at": "2026-09-02T07:15:09.482113+00:00",
"source": "email",
"type": "email_sent",
"object_id": "2f6a9d14-83c5-4b07-a1e2-9d3c8b7a6f51",
"title": "Ihre Zahlungserinnerung",
"status": "delivered"
}
],
"next_cursor": null,
"sources": [
"email",
"dunning",
"payment",
"document",
"contract"
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/datev/exports
Die über die API angeforderten DATEV-Exporte Ihrer Organisation mit **Anlagezeitpunkt** im Zeitraum, neueste zuerst: `status` (`requested` = läuft, `ready` = fertig, `failed` = fehlgeschlagen mit `failure_code`), Standort, Jahr und Monat des Buchungsstapels, Format, Zahl der Buchungssätze, Größe und ob die Datei noch abrufbar ist. Zeitfilter: genau eine Form aus `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); ohne Angabe gelten die letzten 30 Tage (Tagesgrenzen in Europe/Berlin), mehrere Formen liefern 400 `period_conflict`, ein umgekehrter Zeitraum 400 `range_invalid`. Filter: `status` und `location_id`. Mit `next_cursor` weiterblättern. Es erscheinen nur Exporte, die über die API angefordert wurden — keine aus der Verwaltung. Nie enthalten: Dateiname, Speicherort, Buchungsdaten.
Scope: datev:export
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
curl -s "https://api.manage2hero.com/v1/datev/exports" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/datev/exports" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/datev/exports', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/datev/exports",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f",
"status": "ready",
"format": "datev",
"encoding": "windows-1252",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"year": 2026,
"month": 9,
"row_count": 184,
"failure_code": null,
"file_available": true,
"file_size": 23871,
"file_expires_at": "2026-10-11T08:14:22.000Z",
"created_at": "2026-10-10T08:14:20.481516Z",
"completed_at": "2026-10-10T08:14:22.000Z"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/datev/exports
Exportiert den **Buchungsstapel** eines Standorts für einen Monat (`location_id`, `year`, `month`) — mit demselben Exportlauf wie der DATEV-Export in der Verwaltung (Finanzen), also mit denselben Steuerschlüsseln, derselben Festschreibungs-Kennzeichnung und denselben Prüfungen. `format`: `datev` (Standard, DATEV-Buchungsstapel im EXTF-Format, Zeichensatz Windows-1252, braucht Berater- und Mandantennummer am Standort) oder `csv` (einfaches CSV, UTF-8 mit BOM). Der Export läuft **synchron**: Die Antwort (201) nennt den abgeschlossenen Export. `status = ready`: die Datei liegt **24 Stunden** zum Abruf bereit (`POST /v1/datev/exports/{id}/download-link`). `status = failed`: `failure_code` nennt den Grund (`datev_stammdaten_unvollstaendig`, `datev_bu_key_invalid`, `no_items_in_batch`, `export_changed_retry`, `storage_failed`, `internal_error`); Einzelheiten zu abgelehnten Buchungen stehen nur in der Verwaltung. Nach der Korrektur mit einem neuen `Idempotency-Key` erneut anfordern. Der Export **schreibt nichts in die Buchhaltung** (nur der Verlauf wird festgehalten) und liefert den aktuellen Stand des Stapels. Der Kopf `Idempotency-Key` ist Pflicht: gleicher Schlüssel mit gleichem Inhalt liefert denselben Export (200), mit anderem Inhalt 409 `idempotency_conflict`. Für den Standort und Monat gibt es keinen Buchungsstapel, oder der Standort ist unbekannt oder fremd: 404 `not_found`. Höchstens 30 neue Exporte je Organisation und Stunde (429 `rate_limited`, `Retry-After`). Der Aufruf zählt als schreibender Aufruf (engeres Kontingent je Token und Minute). Das Token sieht die ganze Organisation (keine Standortgrenzen); der Export enthält die Buchungsdaten des Stapels — das Token deshalb nur an Systeme geben, die sie sehen dürfen.
Scope: datev:export
curl -s -X POST "https://api.manage2hero.com/v1/datev/exports" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"location_id":"6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b","year":2026,"month":9,"format":"datev"}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"year": 2026,
"month": 9,
"format": "datev"
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/datev/exports" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-datev-exports-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/datev/exports', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"year": 2026,
"month": 9,
"format": "datev"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-datev-exports-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/datev/exports",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"year": 2026,
"month": 9,
"format": "datev"
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f",
"status": "ready",
"format": "datev",
"encoding": "windows-1252",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"year": 2026,
"month": 9,
"row_count": 184,
"failure_code": null,
"file_available": true,
"file_size": 23871,
"file_expires_at": "2026-10-11T08:14:22.000Z",
"created_at": "2026-10-10T08:14:20.481516Z",
"completed_at": "2026-10-10T08:14:22.000Z"
},
"request_id": "req_4n2Xc9…"
}GET/v1/datev/exports/{id}
Wie ein Eintrag aus `/v1/datev/exports`: `status`, Eckdaten und `file_available` (die Datei ist fertig, nicht abgelaufen und abrufbar). Der Export läuft synchron; `requested` sieht nur, wer ihn während des Laufs abfragt — bleibt ein Auftrag länger als 15 Minuten offen, gilt er als `failed` mit `failure_code = timeout`. Die Datei selbst gibt es nur über `POST /v1/datev/exports/{id}/download-link`. Unbekannte oder fremde Id: 404 `not_found`.
Scope: datev:export
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f",
"status": "ready",
"format": "datev",
"encoding": "windows-1252",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"year": 2026,
"month": 9,
"row_count": 184,
"failure_code": null,
"file_available": true,
"file_size": 23871,
"file_expires_at": "2026-10-11T08:14:22.000Z",
"created_at": "2026-10-10T08:14:20.481516Z",
"completed_at": "2026-10-10T08:14:22.000Z"
},
"request_id": "req_4n2Xc9…"
}POST/v1/datev/exports/{id}/download-link
Erzeugt einen **Link, der höchstens 5 Minuten gültig ist** (`expires_at`; nie über das Ende der Aufbewahrung der Datei hinaus, `expires_in_seconds` nennt die Dauer), und liefert ihn in `url`. Den Link nicht speichern, nicht weitergeben und nicht protokollieren: Wer ihn hat, kann die Datei laden — sie enthält die Buchungsdaten des Stapels. `encoding` nennt den Zeichensatz der Datei (`windows-1252` bei `datev`, `utf-8` bei `csv`). Für eine spätere Anfrage einen neuen Link erzeugen — jeder Aufruf erzeugt einen neuen und zählt als schreibender Aufruf (engeres Kontingent je Token und Minute). Kein Body nötig. Die Datei liegt 24 Stunden nach dem Export bereit. Der Export ist noch nicht fertig: 409 `export_not_ready`. Der Export ist fehlgeschlagen, die Datei abgelaufen oder entfernt: 404 `file_not_available`. Unbekannte oder fremde Id: 404 `not_found`.
Scope: datev:export
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"export_id": "2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f",
"url": "https://<storage-host>/storage/v1/object/sign/…?token=…",
"expires_at": "2026-10-10T08:19:22.000Z",
"expires_in_seconds": 300,
"file_name": "datev_2026-09_2f6c1d3e.csv",
"mime_type": "text/csv",
"encoding": "windows-1252"
},
"request_id": "req_4n2Xc9…"
}GET/v1/prospects
Mietanfragen von Interessenten (aus dem öffentlichen Formular, dem Mieterportal oder per API), neueste zuerst. **Zeitfilter** auf den Anfragezeitpunkt: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage**; alles gibt es nur mit `period=all` (seitenweise über `next_cursor`). Tage gelten in der Zeitzone Europe/Berlin. Filter: `location_id` (Anfragen mit Wunsch für diesen Standort), `storage_type_id` (Garagentyp), `status` (`new`, `open_bank_details`, `bank_details_received`, `contract_ready`, `contract_signing`, `contract_submitted`, `waitlisted`, `converted`, `rejected`, `expired`, `withdrawn`), `source` (`public_form`, `tenant_portal`, `api`) und `updated_since` (ISO-Zeitpunkt; nur Anfragen, die seitdem geändert wurden — kein Änderungsstrom, gelöschte Anfragen erscheinen nicht). Der Token sieht die ganze Organisation. **Datenschutz:** Ausgegeben werden nur Kennung, Status, Herkunft, Zeitpunkte, Name/Firma, Nutzungsart, Mehrfachanmietung, `email_verified` und die gewünschten Standorte und Garagentypen (mit Mietbeginn und Ausstattung) — **nie** E-Mail-Adresse, Telefon, Anschrift, Geburtsdatum, Bank-, Bonitäts- oder Ausweisdaten, Freitexte oder Notizen. Diese Angaben sehen nur Mitarbeitende in der Verwaltung. Auf Wunsch als CSV/Excel (`format`); die Wünsche stehen dann als JSON-Text in einer Zelle.
Scope: prospects:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
storage_type_id | Query | Interessenten-Anfragen: nur Anfragen mit Wunsch für diesen Garagentyp (Id eines Garagentyps). Einheiten: nur Einheiten dieses Garagentyps (Id aus dem Feld storage_type_id einer Einheit) |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
source | Query | Herkunft der Anfrage: public_form (öffentliches Formular), tenant_portal (Mieterportal) oder api |
updated_since | Query | Nur Datensätze, die seit diesem Zeitpunkt geändert wurden (ISO 8601 mit Zone, z. B. 2026-10-01T08:00:00Z). Zähler und Abrechnungen: auch als Datum möglich (erstellt oder geändert seit). Schäden: nur seit diesem Zeitpunkt angelegt oder geändert (kein lückenloser Änderungsstrom: Fenster überlappen lassen). Handwerker-Aufträge: einschließlich dieses Zeitpunkts, auch als YYYY-MM-DD (Beginn des Tages) möglich; allein angegeben, gilt dort kein Anlage-Zeitraum |
curl -s "https://api.manage2hero.com/v1/prospects" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/prospects" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/prospects', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/prospects",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33",
"status": "new",
"source": "api",
"created_at": "2026-10-09T08:12:31.402117+00:00",
"updated_at": "2026-10-09T08:12:31.402117+00:00",
"first_name": "Max",
"last_name": "Muster",
"company": null,
"usage_type": "private",
"multiple_rental": false,
"unit_count": 1,
"email_verified": false,
"locations": [
{
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"priority": 1,
"lease_start_date": "2026-11-01",
"unit_types": [
{
"storage_type_id": "a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60",
"priority": 1,
"power_option": "normalstrom",
"water_option": false,
"status": "new"
}
]
}
]
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/prospects/{id}
Wie ein Eintrag aus `/v1/prospects` (gleiche Felder, kein Kontaktdaten-Zusatz). Unbekannte oder fremde Id: 404 `not_found`.
Scope: prospects:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/prospects/4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/prospects/4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/prospects/4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/prospects/4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33",
"status": "new",
"source": "api",
"created_at": "2026-10-09T08:12:31.402117+00:00",
"updated_at": "2026-10-09T08:12:31.402117+00:00",
"first_name": "Max",
"last_name": "Muster",
"company": null,
"usage_type": "private",
"multiple_rental": false,
"unit_count": 1,
"email_verified": false,
"locations": [
{
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"priority": 1,
"lease_start_date": "2026-11-01",
"unit_types": [
{
"storage_type_id": "a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60",
"priority": 1,
"power_option": "normalstrom",
"water_option": false,
"status": "new"
}
]
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/waitlist
Wartelisten-Einträge (Interessenten und Bestandsmieter, die auf eine freie Garage warten). **Zeitfilter** auf den Eintragszeitpunkt: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage**; alles gibt es nur mit `period=all` (seitenweise über `next_cursor`). Tage gelten in der Zeitzone Europe/Berlin. Achtung: Wegen der Vorgabe „letzte 30 Tage“ erscheinen ältere offene Einträge nur mit `period=all`. Filter: `location_id`, `status` (`waiting`, `notified`, `assigned`, `cancelled`). `position` ist der Platz in der Warteliste des Standorts (1 = vorn, wie in der Verwaltung) und nur für offene Einträge (`waiting`, `notified`) gesetzt, sonst `null`. `storage_type_ids` nennt die gewünschten Garagentypen (leer = jeder Typ). `prospect_id` verweist auf `GET /v1/prospects/{id}`, `tenant_id` auf einen Bestandsmieter. **Keine Namen und keine Kontaktdaten.** Auf Wunsch als CSV/Excel (`format`).
Scope: prospects:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
curl -s "https://api.manage2hero.com/v1/waitlist" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/waitlist" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/waitlist', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/waitlist",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "c91d3a7e-2b45-4e68-a0f3-7b1c5d9e2a48",
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"status": "waiting",
"position": 3,
"created_at": "2026-09-21T14:03:55.118204+00:00",
"prospect_id": "4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33",
"tenant_id": null,
"storage_type_ids": [
"a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60"
]
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/prospects
Legt eine Mietanfrage an, als hätte die Person das öffentliche Anfrageformular ausgefüllt: **dieselben Feldprüfungen** (Pflichtfelder, Längen, Wertelisten, Telefon oder Mobil, Geburtsdatum und Mietbeginn als echte Kalendertage, Mietbeginn nicht in der Vergangenheit, 1–5 Standorte mit je höchstens 20 Garagentypen, jeder Standort/Garagentyp muss zur Organisation gehören und dort angeboten werden). **`privacy_consent` muss `true` sein** — die Datenschutz-Einwilligung der anfragenden Person; ohne sie 400. `Idempotency-Key` ist Pflicht: derselbe Schlüssel mit demselben Inhalt liefert dieselbe Anfrage (200, auch wenn der Mietbeginn inzwischen verstrichen ist), mit anderem Inhalt 409 `idempotency_conflict`. Bankdaten (`iban`, `bic`, `account_holder`) und `credit_check_consent` werden nicht angenommen (400 `field_not_allowed`) — die erfasst die Verwaltung später. **Besonderheiten gegenüber dem Formular:** Herkunft `source: "api"`, die E-Mail-Adresse gilt als **nicht bestätigt** (`email_verified: false`), die API sendet **keine E-Mail** an die anfragende Person (Bestätigung und Einwilligungshinweise sind Sache des Integrators), und es gibt keine automatische Verknüpfung mit einem Mieterportal-Konto. Gehört die E-Mail-Adresse schon einer anderen Person (anderer Name/Geburtsdatum), wird die Anfrage trotzdem angelegt und in der Verwaltung mit einer wichtigen internen Notiz markiert; die Antwort verrät davon nichts (kein Auskunftsloch). **Missbrauchsgrenzen:** höchstens 30 neue Anfragen je Token und Stunde und 5 je E-Mail-Adresse und Token in 24 Stunden (gezählt werden nur eigene API-Einlieferungen) → 429 `rate_limited` mit `Retry-After`. Body höchstens 50 KB. Es erscheinen keine Einmal-Links oder Zugangsdaten in der Antwort; die Antwort hat dieselbe Form wie `GET /v1/prospects/{id}`. Erfordert zusätzlich das Recht Vermietungsprozess mit Bearbeiten beim Ersteller des Tokens.
Scope: prospects:write
curl -s -X POST "https://api.manage2hero.com/v1/prospects" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"salutation":"Herr","first_name":"Max","last_name":"Muster","date_of_birth":"1985-06-15","street":"Musterstraße","house_number":"12","postal_code":"20095","city":"Hamburg","country":"Deutschland","mobile":"+49 170 1234567","email":"max.muster@example.com","usage_type":"private","usage_description":"Oldtimer-Einstellplatz","referral_source":"internet","lease_start_date":"2026-11-01","locations":[{"location_id":"5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05","priority":1,"unit_types":[{"storage_type_id":"a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60","priority":1,"power_option":"normalstrom"}]}],"privacy_consent":true}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"salutation": "Herr",
"first_name": "Max",
"last_name": "Muster",
"date_of_birth": "1985-06-15",
"street": "Musterstraße",
"house_number": "12",
"postal_code": "20095",
"city": "Hamburg",
"country": "Deutschland",
"mobile": "+49 170 1234567",
"email": "max.muster@example.com",
"usage_type": "private",
"usage_description": "Oldtimer-Einstellplatz",
"referral_source": "internet",
"lease_start_date": "2026-11-01",
"locations": [
{
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"priority": 1,
"unit_types": [
{
"storage_type_id": "a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60",
"priority": 1,
"power_option": "normalstrom"
}
]
}
],
"privacy_consent": true
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/prospects" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-prospects-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/prospects', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"salutation": "Herr",
"first_name": "Max",
"last_name": "Muster",
"date_of_birth": "1985-06-15",
"street": "Musterstraße",
"house_number": "12",
"postal_code": "20095",
"city": "Hamburg",
"country": "Deutschland",
"mobile": "+49 170 1234567",
"email": "max.muster@example.com",
"usage_type": "private",
"usage_description": "Oldtimer-Einstellplatz",
"referral_source": "internet",
"lease_start_date": "2026-11-01",
"locations": [
{
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"priority": 1,
"unit_types": [
{
"storage_type_id": "a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60",
"priority": 1,
"power_option": "normalstrom"
}
]
}
],
"privacy_consent": true
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-prospects-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/prospects",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"salutation": "Herr",
"first_name": "Max",
"last_name": "Muster",
"date_of_birth": "1985-06-15",
"street": "Musterstraße",
"house_number": "12",
"postal_code": "20095",
"city": "Hamburg",
"country": "Deutschland",
"mobile": "+49 170 1234567",
"email": "max.muster@example.com",
"usage_type": "private",
"usage_description": "Oldtimer-Einstellplatz",
"referral_source": "internet",
"lease_start_date": "2026-11-01",
"locations": [
{
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"priority": 1,
"unit_types": [
{
"storage_type_id": "a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60",
"priority": 1,
"power_option": "normalstrom"
}
]
}
],
"privacy_consent": True
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "4b7c2e19-8a3d-4f60-9c15-2d8e6a1b7f33",
"status": "new",
"source": "api",
"created_at": "2026-10-09T08:12:31.402117+00:00",
"updated_at": "2026-10-09T08:12:31.402117+00:00",
"first_name": "Max",
"last_name": "Muster",
"company": null,
"usage_type": "private",
"multiple_rental": false,
"unit_count": 1,
"email_verified": false,
"locations": [
{
"location_id": "5f1e9b0c-3a7d-4c82-b6e4-9d2a8c7f1e05",
"priority": 1,
"lease_start_date": "2026-11-01",
"unit_types": [
{
"storage_type_id": "a3c8d4e2-6b19-4f7a-8e05-1c9b2d3f4a60",
"priority": 1,
"power_option": "normalstrom",
"water_option": false,
"status": "new"
}
]
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/appointments
Übergabetermine (Einzug `move_in`, Auszug `move_out`) und allgemeine Termine (`general`, z. B. Besichtigungen) Ihrer Organisation, **zeitlich aufsteigend** (frühester zuerst, bei gleichem Zeitpunkt nach `id`). `date` und `time` gelten in Europe/Berlin; `starts_at` ist daraus der Zeitpunkt (UTC), ohne Uhrzeit der Tagesbeginn. **Zeitfilter** auf den Terminzeitpunkt: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen zugleich → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. **Ohne Angabe gelten die letzten 30 Tage** (vergangene Termine!); für kommende Termine `upcoming=true` (alle ab jetzt, nicht zusammen mit den Zeitfiltern) oder `from=<heute>` verwenden. Filter: `type` (`move_in`, `move_out`, `general`), `status` (`planned`, `confirmed`, `rescheduled`, `completed`, `cancelled`; nur Übergabetermine — allgemeine Termine haben keinen Status und fallen mit diesem Filter weg), `location_id`, `unit_id`, `tenant_id`, `lease_id` (Einheit, Mieter und Vertrag gibt es nur bei Übergabeterminen). Eine fremde oder unbekannte Kennung in einem Filter → 404 `not_found`. Der Token sieht die ganze Organisation. **Was nie erscheint:** der Hausmeister, die Zuständigen allgemeiner Termine, interne Notizen und Änderungsgründe, Übergabefenster, Aufgaben und Zusatzvereinbarungen (keine Termine dieser Schnittstelle) sowie Übergabetermine ohne Datum.
Scope: appointments:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
upcoming | Query | Termine: true = nur Termine ab jetzt (ersetzt den Zeitfilter; nicht zusammen mit period, date, from oder to) |
type | Query | Termine: move_in (Einzug), move_out (Auszug) oder general (allgemeiner Termin). Zähler: Zählerart electricity (Strom) oder water (Wasser) |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
unit_id | Query | Nur diese Einheit (Garage; bei Zählern: Id aus den Einheiten; bei Schäden: Id aus unit.id von GET /v1/locations/{id}/tenants) |
tenant_id | Query | Nur dieser Mieter |
lease_id | Query | Nur dieser Vertrag |
curl -s "https://api.manage2hero.com/v1/appointments" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/appointments" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/appointments', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/appointments",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07",
"type": "move_in",
"status": "planned",
"date": "2026-10-15",
"time": "10:30",
"starts_at": "2026-10-15T08:30:00+00:00",
"duration_minutes": null,
"title": null,
"description": "Schlüsselübergabe am Tor",
"location_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"unit_id": "8a1c4e7b-3d52-4b96-8f0a-2c5e9d1b7a34",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"confirmed_by_tenant": false,
"created_at": "2026-10-10T09:12:44.318204+00:00",
"updated_at": null
},
{
"id": "6f2a9c3d-1b84-4e75-9d06-4a8b7c1e5f92",
"type": "general",
"status": null,
"date": "2026-10-20",
"time": "14:00",
"starts_at": "2026-10-20T12:00:00+00:00",
"duration_minutes": 30,
"title": "Besichtigung Garage 12",
"description": "Interessent trifft sich am Haupteingang",
"location_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"unit_id": null,
"lease_id": null,
"tenant_id": null,
"confirmed_by_tenant": null,
"created_at": "2026-10-10T09:15:02.101933+00:00",
"updated_at": null
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/appointments/{id}
Wie ein Eintrag aus `/v1/appointments`. Unbekannte oder fremde Id sowie jeder Termin, den `/v1/appointments` nie ausgibt (Übergabetermin ohne Datum): 404 `not_found`.
Scope: appointments:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/appointments/3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/appointments/3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/appointments/3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/appointments/3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07",
"type": "move_in",
"status": "planned",
"date": "2026-10-15",
"time": "10:30",
"starts_at": "2026-10-15T08:30:00+00:00",
"duration_minutes": null,
"title": null,
"description": "Schlüsselübergabe am Tor",
"location_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"unit_id": "8a1c4e7b-3d52-4b96-8f0a-2c5e9d1b7a34",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"confirmed_by_tenant": false,
"created_at": "2026-10-10T09:12:44.318204+00:00",
"updated_at": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/appointments
Legt einen **Übergabetermin** (`type` = `move_in` oder `move_out`, mit `lease_id`, `date`, `time`) oder einen **allgemeinen Termin** (`type` = `general`, mit `title`, `date`; optional `time`, `location_id`, `description`, `duration_minutes`) an — nach den Regeln der Terminverwaltung (eine Transaktion). Antwort: der angelegte Termin (201). **Übergabetermin:** Vertrag der eigenen Organisation (sonst 404 `not_found`); der Hausmeister ist der Standard-Hausmeister des Standorts (ist keiner hinterlegt → 400 mit `lease_id: invalid_value`); `confirmed_by_tenant` (Vorgabe `false`) legt den Status `confirmed` statt `planned` fest. Für Vertrag und Art darf es **keinen aktiven Termin** geben (die Verwaltung würde umplanen, die API nie): sonst 409 `appointment_conflict`. Wie dort rückt der Vertragsstatus von „Zahlung eingegangen“ auf „Einzugsprotokoll ausstehend“ bzw. von „Kündigung eingegangen“ auf „Auszugsprotokoll ausstehend“. **Datum:** nicht in der Vergangenheit (heute nur mit einer Uhrzeit, die noch nicht vorbei ist; heute ohne Uhrzeit gilt ab Tagesbeginn und wird abgelehnt), höchstens 3 Jahre voraus (Europe/Berlin), sonst 400 mit `date: invalid_value`; eine Wiederholung mit demselben Schlüssel bleibt davon unberührt. **Es werden keine E-Mails und keine Benachrichtigungen versendet** — weder an den Hausmeister noch an den Mieter (die Verwaltung versendet beim Setzen eines Übergabetermins beides). Pflicht-Kopf `Idempotency-Key`: gleicher Schlüssel mit gleichem Inhalt liefert denselben Termin (200), mit anderem Inhalt 409 `idempotency_conflict`. Die Wiederholungserkennung hängt am angelegten Termin: Wird er in der Verwaltung gelöscht, bevor die Wiederholung eintrifft, legt dieselbe Anfrage einen neuen Termin an (wie beim Mieterimport). Felder, die nicht zur Art passen, sind `unknown_field`. Der Scope `appointments:write` braucht zusätzlich `appointments:read`; Übergabetermine verlangen vom Ersteller des Tokens das Recht Vermietungsprozess (Bearbeiten), allgemeine Termine das Recht Kalender (Bearbeiten). Body höchstens 16 KB.
Scope: appointments:write
curl -s -X POST "https://api.manage2hero.com/v1/appointments" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"type":"move_in","lease_id":"0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b","date":"2026-10-15","time":"10:30","description":"Schlüsselübergabe am Tor"}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"type": "move_in",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"date": "2026-10-15",
"time": "10:30",
"description": "Schlüsselübergabe am Tor"
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/appointments" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-appointments-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/appointments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"type": "move_in",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"date": "2026-10-15",
"time": "10:30",
"description": "Schlüsselübergabe am Tor"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-appointments-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/appointments",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"type": "move_in",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"date": "2026-10-15",
"time": "10:30",
"description": "Schlüsselübergabe am Tor"
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3b9d1e6a-7c24-4f58-a1b3-8e2f6c4d9a07",
"type": "move_in",
"status": "planned",
"date": "2026-10-15",
"time": "10:30",
"starts_at": "2026-10-15T08:30:00+00:00",
"duration_minutes": null,
"title": null,
"description": "Schlüsselübergabe am Tor",
"location_id": "5e0b7a3c-2f1d-4c6b-9a8e-7d6c5b4a3f21",
"unit_id": "8a1c4e7b-3d52-4b96-8f0a-2c5e9d1b7a34",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"confirmed_by_tenant": false,
"created_at": "2026-10-10T09:12:44.318204+00:00",
"updated_at": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/damages
Die Schadensakten Ihrer Organisation, neueste zuerst. Ein Schaden ist eine **Fallakte** der Verwaltung: an einer Einheit (`kind = unit`) oder am Gelände/Park eines Standorts (`kind = location`, mit `process_type`). Meldungen von Mietern oder Hausmeistern, die noch auf die Entscheidung der Verwaltung warten, erscheinen erst nach der Annahme; Übergabeprotokoll-Schäden ohne Fallakte gehören nicht dazu. **Zeitfilter** auf den Meldezeitpunkt (`reported_at`): genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder -Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); **ohne Angabe gelten die letzten 30 Tage** (alles nur mit `period=all`, mit `next_cursor` seitenweise); mehrere Formen → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. Tagesgrenzen in Europe/Berlin. Filter: `location_id` (Gelände-Schäden und Schäden an Einheiten des Standorts), `unit_id`, `lease_id` und `tenant_id` (Vertrag bzw. Mieter der Mieter-Meldung; nie der Verursacher), `status` (`WEITERE_KLAERUNG`, `PLANEN_BEAUFTRAGEN`, `BEAUFTRAGT`, `ABGESCHLOSSEN`, `KEINE_REPARATUR`), `kind` (`unit`, `location`), `reporter_type` (`tenant`, `caretaker`, `administration`) und `updated_since` (ISO-Zeitpunkt mit Zeitzone, z. B. `2026-10-01T10:00:00Z`; Akte angelegt oder geändert seit dann, auch durch ein neues oder geändertes Dokument — das Löschen eines Dokuments zählt nicht; **kein lückenloser Änderungsstrom** — für einen lückenlosen Abgleich nutzen Sie `GET /v1/damages/changes`). Auf Wunsch als CSV oder Excel (`format=csv|xlsx`). **Datenschutz:** nur die Art des Melders und des Verursachers, nie Namen oder Kontaktdaten; keine Versicherungs-, Kosten- und Artikeldaten. **`description` ist Freitext und unsicher** (kann beliebige Zeichen und Angaben Dritter enthalten) — nie als HTML ausgeben. Das Token sieht die ganze Organisation (keine Standortgrenzen).
Scope: damages:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
unit_id | Query | Nur diese Einheit (Garage; bei Zählern: Id aus den Einheiten; bei Schäden: Id aus unit.id von GET /v1/locations/{id}/tenants) |
lease_id | Query | Nur dieser Vertrag |
tenant_id | Query | Nur dieser Mieter |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
kind | Query | Schäden: unit = an einer Einheit, location = am Gelände (Park) eines Standorts |
reporter_type | Query | Schäden: Melder-Art tenant (Mieter), caretaker (Hausmeister) oder administration (Verwaltung) |
updated_since | Query | Nur Datensätze, die seit diesem Zeitpunkt geändert wurden (ISO 8601 mit Zone, z. B. 2026-10-01T08:00:00Z). Zähler und Abrechnungen: auch als Datum möglich (erstellt oder geändert seit). Schäden: nur seit diesem Zeitpunkt angelegt oder geändert (kein lückenloser Änderungsstrom: Fenster überlappen lassen). Handwerker-Aufträge: einschließlich dieses Zeitpunkts, auch als YYYY-MM-DD (Beginn des Tages) möglich; allein angegeben, gilt dort kein Anlage-Zeitraum |
curl -s "https://api.manage2hero.com/v1/damages" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/damages" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/damages', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/damages",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
"case_number": "VDF-12-003",
"kind": "unit",
"status": "WEITERE_KLAERUNG",
"process_type": null,
"damage_type": "dent",
"priority": "normal",
"description": "Delle im Rolltor, links unten.",
"damage_date": "2026-10-02",
"reported_at": "2026-10-03T07:41:12.482915+00:00",
"updated_at": null,
"reporter_type": "administration",
"causer_type": null,
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"unit_number": "12",
"location_id": "8e2d4c61-7b05-4a93-9f1c-6d3e5a7b9c20",
"lease_id": null,
"tenant_id": null,
"document_count": 0
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/damages/{id}
Wie ein Eintrag aus `/v1/damages`, zusätzlich `documents`: Verweise auf die Dokumente der Schadenakte (Fotos, Belege) — nur `id`, `file_type`, `mime_type`, `file_size` und `created_at`, höchstens 200, **nie Dateiname, Speicherpfad oder Datei-Adresse**; die Dateien selbst sind über die API nicht abrufbar (`/v1/documents` umfasst Mieter- und Standortdokumente, nicht die Dokumente der Schadenakte). Unbekannte oder fremde Id sowie Übergabeprotokoll-Schäden ohne Fallakte: 404.
Scope: damages:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
"case_number": "VDF-12-003",
"kind": "unit",
"status": "WEITERE_KLAERUNG",
"process_type": null,
"damage_type": "dent",
"priority": "normal",
"description": "Delle im Rolltor, links unten.",
"damage_date": "2026-10-02",
"reported_at": "2026-10-03T07:41:12.482915+00:00",
"updated_at": "2026-10-03T07:45:02.118+00:00",
"reporter_type": "administration",
"causer_type": null,
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"unit_number": "12",
"location_id": "8e2d4c61-7b05-4a93-9f1c-6d3e5a7b9c20",
"lease_id": null,
"tenant_id": null,
"document_count": 1,
"documents": [
{
"id": "c4a8e1f2-6b30-4d79-9e15-2a7f0b3d8c64",
"file_type": "foto",
"mime_type": "image/jpeg",
"file_size": 284113,
"created_at": "2026-10-03T07:45:02.118+00:00"
}
]
},
"request_id": "req_4n2Xc9…"
}POST/v1/damages
Legt eine Fallakte an einer Einheit an (`unit_id`, `damage_type`: `paint`, `dent`, `oil_stain`, `hole`, `scratch`, `dirt`, `other`; `damage_date` nicht vor 2000-01-01 und höchstens bis morgen; `description` 1–5000 Zeichen; optional `priority`). Es entsteht dieselbe Akte wie bei „Neuen Schaden erfassen“ in der Verwaltung: Status **`WEITERE_KLAERUNG`** (offen), Melder `administration`, die Vorgangsnummer vergibt die Verwaltung. Die API legt **nie** Meldungen von Mietern oder Hausmeistern an und umgeht keinen Genehmigungsweg; Gelände-/Parkschäden, Fotos, Beträge, Artikel, Rechnungen und Verursacher lassen sich nicht über die API erfassen. Eine unbekannte oder fremde `unit_id` → 404. Schreibender Aufruf (eigenes, engeres Kontingent). **`Idempotency-Key` ist Pflicht** (1–255 sichtbare ASCII-Zeichen, z. B. eine UUID je Vorgang, 90 Tage gültig je Token): gleicher Schlüssel mit gleichem Inhalt → derselbe Erfolg ohne erneute Wirkung (200), gleicher Schlüssel mit anderem Inhalt → 409 `idempotency_conflict`. Antwort 201 mit dem angelegten Schaden (Wiederholung: 200).
Scope: damages:write
curl -s -X POST "https://api.manage2hero.com/v1/damages" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"unit_id":"5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79","damage_type":"dent","damage_date":"2026-10-02","description":"Delle im Rolltor, links unten.","priority":"normal"}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"damage_type": "dent",
"damage_date": "2026-10-02",
"description": "Delle im Rolltor, links unten.",
"priority": "normal"
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/damages" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-damages-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/damages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"damage_type": "dent",
"damage_date": "2026-10-02",
"description": "Delle im Rolltor, links unten.",
"priority": "normal"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-damages-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/damages",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"damage_type": "dent",
"damage_date": "2026-10-02",
"description": "Delle im Rolltor, links unten.",
"priority": "normal"
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
"case_number": "VDF-12-003",
"kind": "unit",
"status": "WEITERE_KLAERUNG",
"process_type": null,
"damage_type": "dent",
"priority": "normal",
"description": "Delle im Rolltor, links unten.",
"damage_date": "2026-10-02",
"reported_at": "2026-10-03T07:41:12.482915+00:00",
"updated_at": null,
"reporter_type": "administration",
"causer_type": null,
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"unit_number": "12",
"location_id": "8e2d4c61-7b05-4a93-9f1c-6d3e5a7b9c20",
"lease_id": null,
"tenant_id": null,
"document_count": 0,
"documents": []
},
"request_id": "req_4n2Xc9…"
}PATCH/v1/damages/{id}/status
Setzt den Reparatur-Status einer Fallakte auf einen der vier Werte der Stufe 12 der Verwaltung: `WEITERE_KLAERUNG`, `PLANEN_BEAUFTRAGEN`, `BEAUFTRAGT`, `ABGESCHLOSSEN` (auch zurück, z. B. Wiedereröffnen). **`KEINE_REPARATUR` ist weder Ziel noch Ausgangspunkt** (400 `validation_error`): der Wert hängt an der Abrechnungsentscheidung der Verwaltung. Entspricht der Wunsch dem aktuellen Status, ändert sich nichts (200). Genehmigungs- und Abrechnungswege werden nicht berührt. **Hinweis:** Die E-Mail „Schaden repariert“ an den meldenden Hausmeister, die die Verwaltung beim Abschluss versendet, löst die API nicht aus. Unbekannte oder fremde Id sowie Übergabeprotokoll-Schäden ohne Fallakte: 404. Schreibender Aufruf (eigenes, engeres Kontingent). **`Idempotency-Key` ist Pflicht** (1–255 sichtbare ASCII-Zeichen, z. B. eine UUID je Vorgang, 90 Tage gültig je Token): gleicher Schlüssel mit gleichem Inhalt → derselbe Erfolg ohne erneute Wirkung (200), gleicher Schlüssel mit anderem Inhalt → 409 `idempotency_conflict`.
Scope: damages:write
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X PATCH "https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312/status" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"status":"PLANEN_BEAUFTRAGEN"}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"status": "PLANEN_BEAUFTRAGEN"
}
'@
Invoke-RestMethod -Method Patch -Uri "https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312/status" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-patch-v1-damages-2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312-status-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312/status', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"status": "PLANEN_BEAUFTRAGEN"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-patch-v1-damages-2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312-status-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.patch(
"https://api.manage2hero.com/v1/damages/2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312/status",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"status": "PLANEN_BEAUFTRAGEN"
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "2f6c1b84-3a9d-4e57-8c10-b7d5e0a9c312",
"case_number": "VDF-12-003",
"kind": "unit",
"status": "PLANEN_BEAUFTRAGEN",
"process_type": null,
"damage_type": "dent",
"priority": "normal",
"description": "Delle im Rolltor, links unten.",
"damage_date": "2026-10-02",
"reported_at": "2026-10-03T07:41:12.482915+00:00",
"updated_at": "2026-10-03T09:12:44.51+00:00",
"reporter_type": "administration",
"causer_type": null,
"unit_id": "5b1e7a30-9c42-4d6f-a8e1-0c3f2d4b6a79",
"unit_number": "12",
"location_id": "8e2d4c61-7b05-4a93-9f1c-6d3e5a7b9c20",
"lease_id": null,
"tenant_id": null,
"document_count": 1,
"documents": [
{
"id": "c4a8e1f2-6b30-4d79-9e15-2a7f0b3d8c64",
"file_type": "foto",
"mime_type": "image/jpeg",
"file_size": 284113,
"created_at": "2026-10-03T07:45:02.118+00:00"
}
]
},
"request_id": "req_4n2Xc9…"
}GET/v1/utility/statements
Die Strom- und Wasserabrechnungen Ihrer Organisation je Mieter und Vertrag: Abrechnungszeitraum, Zählerstände, Verbrauch und die **gespeicherten** Beträge (Arbeitspreis, Zählergebühr, Abwasser, Kosten netto/brutto, Umsatzsteuer, Vorauszahlungen, Saldo). Die API rechnet nichts neu und addiert keine Beträge; Kosten und Saldo stehen so, wie die Verwaltung sie festgeschrieben hat. Entwürfe erscheinen nie. `status` gilt **je Position**: `finalized` (festgeschrieben, noch nicht versendet), `sent` (diese Position wurde per E-Mail versendet) oder `cancelled` (storniert, auch eine einzelne stornierte Position). **Reihenfolge: nach Änderungszeitpunkt aufsteigend** (`updated_at`, dann `id`; zuletzt Geänderte am Ende) — so geht beim Blättern keine Zeile verloren, die sich währenddessen ändert; wer nur Neues sucht, nutzt `updated_since`. Zeitfilter wie bei allen Listen: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder -Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. Der Zeitfilter bezieht sich auf den Erstellungszeitpunkt der Abrechnung; ohne Zeitangabe die letzten 30 Tage — **mit `year`, `lease_id` oder `updated_since` ohne Zeitangabe `all`** (so verpasst ein Änderungsabgleich keine Abrechnung, die erst heute storniert oder versendet wurde). `year` ist das Jahr, in dem der Abrechnungszeitraum endet. `location_id` ist der Standort des Laufs, bei Läufen für alle Standorte der Standort der Vertragseinheit (auch im Filter). Weitere Filter: `status`, `tenant_id`, `lease_id`, `location_id` (eine fremde oder unbekannte Id → 404 `not_found`) und `updated_since` (ISO-Datum oder -Zeitpunkt: Abrechnungen, die seitdem erstellt oder geändert wurden). Mit `next_cursor` weiterblättern; auf Wunsch als CSV oder Excel.
Scope: utility:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
year | Query | Abrechnungsjahr = Jahr, in dem der Abrechnungszeitraum endet; ohne Zeitangabe dann alle Abrechnungen dieses Jahres |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
tenant_id | Query | Nur dieser Mieter |
lease_id | Query | Nur dieser Vertrag |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
updated_since | Query | Nur Datensätze, die seit diesem Zeitpunkt geändert wurden (ISO 8601 mit Zone, z. B. 2026-10-01T08:00:00Z). Zähler und Abrechnungen: auch als Datum möglich (erstellt oder geändert seit). Schäden: nur seit diesem Zeitpunkt angelegt oder geändert (kein lückenloser Änderungsstrom: Fenster überlappen lassen). Handwerker-Aufträge: einschließlich dieses Zeitpunkts, auch als YYYY-MM-DD (Beginn des Tages) möglich; allein angegeben, gilt dort kein Anlage-Zeitraum |
curl -s "https://api.manage2hero.com/v1/utility/statements" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/utility/statements" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/utility/statements', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/utility/statements",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "3f1e2d4c-5b6a-4789-9abc-0d1e2f3a4b5c",
"settlement_id": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
"status": "sent",
"resource_type": "electricity",
"is_moveout_settlement": false,
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"tenant_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"meter_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"period_from": "2025-01-01",
"period_to": "2025-12-31",
"reading_start": 1204.5,
"reading_end": 1873.2,
"consumption": 668.7,
"work_amount_net": 200.61,
"meter_fee_net": 12,
"wastewater_amount_net": null,
"costs_net": 212.61,
"vat_amount": 40.4,
"costs_gross": 253.01,
"advance_payments_total": 240,
"balance_net": 10.91,
"balance_gross": 13.01,
"invoice_number": "NK-2025-00042",
"sent_at": "2026-03-14T09:12:00.000Z",
"cancelled_at": null,
"created_at": "2026-03-10T14:03:21.000Z",
"updated_at": "2026-03-14T09:12:00.000Z"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}GET/v1/meters
Die Strom- und Wasserzähler Ihrer Organisation: Zählernummer (`meter_number`), Art (`type`: `electricity`, `water`), Spannung (nur Strom: 230 oder 400), Status (`active`), Einheit (`unit_id`) und Standort (`location_id`). `is_main_meter: true` ist der Hauptzähler (Hausanschluss) eines Standorts, `false` ein Unterzähler einer Einheit. Reihenfolge: nach Änderungszeitpunkt aufsteigend (`updated_at`, dann `id`). Filter: `location_id`, `unit_id` (eine fremde oder unbekannte Id → 404 `not_found`), `type`, `active` und `updated_since` (ISO-Datum oder -Zeitpunkt). Kein Zeitfilter. Mit `next_cursor` weiterblättern; auf Wunsch als CSV oder Excel. Die Stände eines Zählers liefert `GET /v1/meters/{id}/readings`.
Scope: utility:read
| Parameter | Ort | Beschreibung |
|---|---|---|
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
location_id | Query | Nur dieser Garagenpark (Id aus GET /v1/locations); ohne = ganze Organisation |
unit_id | Query | Nur diese Einheit (Garage; bei Zählern: Id aus den Einheiten; bei Schäden: Id aus unit.id von GET /v1/locations/{id}/tenants) |
type | Query | Termine: move_in (Einzug), move_out (Auszug) oder general (allgemeiner Termin). Zähler: Zählerart electricity (Strom) oder water (Wasser) |
active | Query | true = nur aktive, false = nur inaktive Garagenparks bzw. Handwerksbetriebe bzw. Zähler |
updated_since | Query | Nur Datensätze, die seit diesem Zeitpunkt geändert wurden (ISO 8601 mit Zone, z. B. 2026-10-01T08:00:00Z). Zähler und Abrechnungen: auch als Datum möglich (erstellt oder geändert seit). Schäden: nur seit diesem Zeitpunkt angelegt oder geändert (kein lückenloser Änderungsstrom: Fenster überlappen lassen). Handwerker-Aufträge: einschließlich dieses Zeitpunkts, auch als YYYY-MM-DD (Beginn des Tages) möglich; allein angegeben, gilt dort kein Anlage-Zeitraum |
curl -s "https://api.manage2hero.com/v1/meters" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/meters" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/meters', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/meters",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"meter_number": "EHZ-4711-0815",
"type": "electricity",
"voltage": 230,
"active": true,
"is_main_meter": false,
"unit_id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"installed_at": "2023-05-02T00:00:00.000Z",
"created_at": "2023-05-02T08:00:00.000Z",
"updated_at": "2023-05-02T08:00:00.000Z"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/meters
Legt an einer **Einheit** einen Zähler zusammen mit seiner **Erstablesung** an (`protocol_type` `new_installation`, Quelle `api`). Gedacht für die einmalige Übernahme von Zählernummern aus einem Altsystem. Der Token braucht zusätzlich `utility:read`. Body: `unit_id` (Id der Einheit, aus `GET /v1/units`), `type` (`electricity` oder `water`), `voltage` (bei Strom Pflicht: `230` oder `400`; bei Wasser nicht erlaubt), `meter_number` (Zählernummer, 1–100 Zeichen), `reading_date` (Tag `YYYY-MM-DD`, höchstens ein Tag in der Zukunft; zugleich das Einbaudatum) und `meter_value` (Zahl ≥ 0, Stand der Erstablesung). Die **Erstablesung ist Pflicht**. Hauptzähler eines Standorts sowie Zählwerke (OBIS) legt die API nicht an. **Voraussetzung:** Die Einheit muss in der Verwaltung für die Art eingerichtet sein (Strom 230 V, Strom 400 V bzw. Wasser); sonst 422 `unit_not_configured` — die API ändert keine Stammdaten der Einheit. Hängt an der Einheit für diese Art schon ein aktiver Zähler, kommt 409 `meter_exists` (vorhandene Zähler: `GET /v1/meters?unit_id=…`). **`Idempotency-Key` ist Pflicht** (1–255 Zeichen): gleicher Schlüssel mit gleichem Inhalt liefert denselben Zähler (200), mit anderem Inhalt 409 `idempotency_conflict`. Die Zählernummer muss nicht eindeutig sein; Dubletten prüft der Aufrufer vorab. Unbekannte oder fremde Einheit: 404 `not_found`. Antwort: der Zähler wie in `GET /v1/meters` plus `reading_id`. Schreibende Aufrufe haben ein engeres Kontingent. Zusätzlich braucht der Ersteller des Tokens in der Verwaltung die Rechte zum Bearbeiten von Zählerständen, Strom- und Wasserabrechnung.
Scope: utility:write
curl -s -X POST "https://api.manage2hero.com/v1/meters" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"unit_id":"2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b","type":"electricity","voltage":230,"meter_number":"EHZ-4711-0815","reading_date":"2026-10-01","meter_value":1873.2}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"unit_id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
"type": "electricity",
"voltage": 230,
"meter_number": "EHZ-4711-0815",
"reading_date": "2026-10-01",
"meter_value": 1873.2
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/meters" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-meters-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/meters', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"unit_id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
"type": "electricity",
"voltage": 230,
"meter_number": "EHZ-4711-0815",
"reading_date": "2026-10-01",
"meter_value": 1873.2
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-meters-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/meters",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"unit_id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
"type": "electricity",
"voltage": 230,
"meter_number": "EHZ-4711-0815",
"reading_date": "2026-10-01",
"meter_value": 1873.2
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"meter_number": "EHZ-4711-0815",
"type": "electricity",
"voltage": 230,
"active": true,
"is_main_meter": false,
"unit_id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
"location_id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
"installed_at": "2023-05-02T00:00:00.000Z",
"created_at": "2023-05-02T08:00:00.000Z",
"updated_at": "2023-05-02T08:00:00.000Z",
"reading_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
},
"request_id": "req_4n2Xc9…"
}GET/v1/meters/{id}/readings
Die Zählerstände eines Zählers: Ablesedatum (`reading_date`), Wert (`meter_value`), Zählwerk (`obis_code`, nur am Hauptstromzähler), Art der Ablesung (`protocol_type`, z. B. `interim_reading`, `per_move_in_protocol`), Quelle (`source`: `api` = über die API eingetragen, `app` = Verwaltung, Hausmeister oder Protokoll), Vertrag (`lease_id`) und `is_billed` (bereits abgerechnet). Neueste zuerst. Zeitfilter wie bei allen Listen: genau eine der Formen `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` (ein Tag) oder `from`/`to` (ISO-Datum oder -Zeitpunkt; ein Datum bei `to` nennt den letzten Tag einschließlich, ein Zeitpunkt die ausgeschlossene Obergrenze); mehrere Formen → 400 `period_conflict`, `from` nach `to` → 400 `range_invalid`. Der Zeitfilter bezieht sich auf das **Ablesedatum**; ohne Angabe die letzten 30 Tage. Stornierte Ablesungen erscheinen nur mit `include_cancelled=true`. Fotos und Ableser werden nicht ausgegeben. Unbekannter oder fremder Zähler: 404 `not_found`. Mit `next_cursor` weiterblättern; auf Wunsch als CSV oder Excel.
Scope: utility:read
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
include_cancelled | Query | true = auch stornierte Zählerstände (Vorgabe false) |
curl -s "https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"meter_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"reading_date": "2026-10-01T00:00:00.000Z",
"meter_value": 1873.2,
"obis_code": null,
"protocol_type": "interim_reading",
"source": "api",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"is_cancelled": false,
"is_billed": false,
"created_at": "2026-10-02T07:45:10.000Z"
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/meters/{id}/readings
Trägt einen Zählerstand als **Zwischenablesung** (`interim_reading`) ein, Quelle `api`. Ein-/Auszugsprotokolle und Korrekturen bleiben der Verwaltung vorbehalten. Der Token braucht zusätzlich `utility:read`. Body: `reading_date` (Tag `YYYY-MM-DD`, höchstens ein Tag in der Zukunft), `meter_value` (Zahl ≥ 0), `obis_code` (Pflicht am Hauptstromzähler, z. B. `1.8.0` Netzbezug; sonst nicht erlaubt) und optional `force_warnings`. Der Vertrag wird wie in der Verwaltung aus dem Ablesedatum bestimmt. **Plausibilitätsprüfung der Verwaltung:** Fehler (z. B. Stand kleiner als der Vorwert, `chronological_order`; Datum in einem bereits abgerechneten Zeitraum, `in_billed_period`) ergeben 400 `validation_error` mit den Codes in `error.details.plausibility`; Warnungen (`unrealistic_consumption`, …) ergeben 400 mit `error.details.plausibility_warnings` und werden nur mit `force_warnings: true` übernommen. **`Idempotency-Key` ist Pflicht** (1–255 Zeichen): gleicher Schlüssel mit gleichem Inhalt liefert dieselbe Ablesung (200), mit anderem Inhalt 409 `idempotency_conflict`. Unbekannter oder fremder Zähler: 404 `not_found`; ein deaktivierter Zähler nimmt keine neuen Stände an (400 `validation_error`). **Grenze der Idempotenz:** Sie gilt, solange die Ablesung existiert — wird sie in der Verwaltung gelöscht, legt ein erneutes Senden mit demselben `Idempotency-Key` sie neu an. Wird derselbe Zähler gleichzeitig geändert (z. B. in der Verwaltung), prüft die API erneut; gelingt das dreimal nicht, kommt 503 `service_unavailable` — dann mit demselben `Idempotency-Key` erneut senden. Schreibende Aufrufe haben ein engeres Kontingent. Zusätzlich braucht der Ersteller des Tokens in der Verwaltung die Rechte zum Bearbeiten von Zählerständen, Strom- und Wasserabrechnung.
Scope: utility:write
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X POST "https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"reading_date":"2026-10-01","meter_value":1873.2}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"reading_date": "2026-10-01",
"meter_value": 1873.2
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-meters-5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a-readings-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"reading_date": "2026-10-01",
"meter_value": 1873.2
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-meters-5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a-readings-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/meters/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/readings",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"reading_date": "2026-10-01",
"meter_value": 1873.2
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"meter_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"reading_date": "2026-10-01T00:00:00.000Z",
"meter_value": 1873.2,
"obis_code": null,
"protocol_type": "interim_reading",
"source": "api",
"lease_id": "0b6c2c1a-8e5f-4c2d-9a1b-3c4d5e6f7a8b",
"is_cancelled": false,
"is_billed": false,
"created_at": "2026-10-02T07:45:10.000Z"
},
"request_id": "req_4n2Xc9…"
}GET/v1/webhooks
Alle Webhook-Endpunkte Ihrer Organisation (höchstens 10), älteste zuerst. **Keine Seitenwechsel:** `data` enthält nur `items`, kein `next_cursor` (anders als die übrigen Listen). Ein Webhook meldet Ereignisse per signiertem POST an eine https-Adresse Ihres Systems. **Nie** das Signaturgeheimnis: Es erscheint nur einmal bei der Anlage und bei der Rotation. `active = false` mit `disabled_reason` zeigt einen abgeschalteten Webhook (`manual`, `delivery_failed`, `token_revoked`, `token_expired`, `creator_lost_access`, `api_disabled`). Zähler: `pending_deliveries` (wartende Zustellungen, Rückstau), `paused_deliveries` (pausierte Zustellungen; bei einem aktiven Webhook heißt ein Wert über 0: die Wiederaufnahme läuft noch), `enqueue_failures` (nicht eingestellte Ereignisse, die noch nachgestellt werden oder verloren sind) und `recording_gap` (`null` oder `{ from, to }`: ab wann während der Abschaltung Ereignisse nicht mehr aufgezeichnet werden konnten; `to` ist `null`, solange die Lücke offen ist — der Änderungsabgleich mit `updated_since = from` schließt sie). Abonnierbare Ereignisse:`payment.received`, `dunning.sent`, `contract.signed`, `invoice.created`, `document.created`, `email.sent`, `damage.created` (neue Fallakte, auch über die API angelegt) sowie `contract.terminated` (Kündigung bestätigt), `invoice.cancelled` (Rechnung storniert) und `damage.updated` (Fallakte geändert). Neue Typen gelten nur für Webhooks, die sie ausdrücklich in `events` aufführen. Der Scope verlangt vom Token-Ersteller Vollzugriff auf die API-Zugänge; endet das Token oder verliert der Ersteller das Recht, werden seine Webhooks abgeschaltet.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|
curl -s "https://api.manage2hero.com/v1/webhooks" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/webhooks" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/webhooks', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
"url": "https://erp.example.com/hooks/manage2hero",
"description": "ERP Buchhaltung",
"events": [
"payment.received",
"invoice.created"
],
"active": true,
"disabled_reason": null,
"disabled_at": null,
"created_at": "2026-10-10T08:15:30.412907+00:00",
"updated_at": "2026-10-10T08:15:30.412907+00:00",
"secret_rotated_at": null,
"previous_secret_valid_until": null,
"last_success_at": null,
"last_failure_at": null,
"enqueue_failures": 0,
"pending_deliveries": 0,
"paused_deliveries": 0,
"recording_gap": null
}
]
},
"request_id": "req_4n2Xc9…"
}POST/v1/webhooks
Legt einen Webhook an: `url` (nur **https auf Port 443** — ein anderer Port wird abgelehnt —, nur öffentlich erreichbare Adressen, keine Zugangsdaten in der Adresse, keine Weiterleitungen), `events` (mindestens eines) und optional `description`. Die Adresse wird geprüft (Form und DNS) — sonst 400 `webhook_url_invalid`; mehr als 10 Webhooks je Organisation: 409 `webhook_limit_reached`. Die Antwort 201 (Neuanlage) enthält **einmalig** das Signaturgeheimnis `secret` (`whsec_…`); bitte sofort sicher ablegen, es ist danach nicht mehr lesbar (nur rotieren). `Idempotency-Key` ist Pflicht: gleicher Schlüssel (mit demselben Token) + gleicher Inhalt liefert, wenn die erste Anlage abgeschlossen war, denselben Webhook (200) **ohne** Geheimnis; brach sie vorher ab, entsteht der Webhook jetzt (201 mit Geheimnis). Anderer Inhalt: 409 `idempotency_conflict`. Zugestellt wird vom Server im Minutentakt, nie im Aufruf; Aufbau der Nachrichten, Signatur und Wiederholungen: Webhooks-Handbuch.
Scope: webhooks:manage
curl -s -X POST "https://api.manage2hero.com/v1/webhooks" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"url":"https://erp.example.com/hooks/manage2hero","events":["payment.received","invoice.created"],"description":"ERP Buchhaltung"}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = '{"url":"https://erp.example.com/hooks/manage2hero","events":["payment.received","invoice.created"],"description":"ERP Buchhaltung"}'
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/webhooks" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-webhooks-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"url": "https://erp.example.com/hooks/manage2hero",
"events": [
"payment.received",
"invoice.created"
],
"description": "ERP Buchhaltung"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-webhooks-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={"url":"https://erp.example.com/hooks/manage2hero","events":["payment.received","invoice.created"],"description":"ERP Buchhaltung"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
"url": "https://erp.example.com/hooks/manage2hero",
"description": "ERP Buchhaltung",
"events": [
"payment.received",
"invoice.created"
],
"active": true,
"disabled_reason": null,
"disabled_at": null,
"created_at": "2026-10-10T08:15:30.412907+00:00",
"updated_at": "2026-10-10T08:15:30.412907+00:00",
"secret_rotated_at": null,
"previous_secret_valid_until": null,
"last_success_at": null,
"last_failure_at": null,
"enqueue_failures": 0,
"pending_deliveries": 0,
"paused_deliveries": 0,
"recording_gap": null,
"secret": "whsec_Q2xhdWRlRXhhbXBsZVNlY3JldE5vdFJlYWwwMDAwMDA"
},
"request_id": "req_4n2Xc9…"
}GET/v1/webhooks/{id}
Ein Webhook-Endpunkt ohne Geheimnis. Unbekannte oder fremde Id: 404 `not_found`.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
"url": "https://erp.example.com/hooks/manage2hero",
"description": "ERP Buchhaltung",
"events": [
"payment.received",
"invoice.created"
],
"active": true,
"disabled_reason": null,
"disabled_at": null,
"created_at": "2026-10-10T08:15:30.412907+00:00",
"updated_at": "2026-10-10T08:15:30.412907+00:00",
"secret_rotated_at": null,
"previous_secret_valid_until": null,
"last_success_at": null,
"last_failure_at": null,
"enqueue_failures": 0,
"pending_deliveries": 0,
"paused_deliveries": 0,
"recording_gap": null
},
"request_id": "req_4n2Xc9…"
}PATCH/v1/webhooks/{id}
Ändert `url`, `events`, `description` (`null` löscht sie) oder `active` — mindestens ein Feld. Die neue Adresse wird wie bei der Anlage geprüft (`webhook_url_invalid`). `active: false` schaltet ab (`disabled_reason = manual`) und **pausiert** offene Zustellungen (`paused`, nichts wird verworfen); solange die API freigeschaltet ist, werden neue Ereignisse weiter aufgezeichnet (bis 10.000 je Webhook, 29 Tage). `active: true` macht das aufrufende Token zum Erzeuger-Token (der Webhook hängt dann an dessen Gültigkeit) — auch bei einem aktiven Webhook; bei einem abgeschalteten Webhook schaltet es ihn außerdem wieder ein, und der Zustelldienst nimmt die pausierten Zustellungen von selbst wieder auf (je Minute höchstens 60 je Webhook, älteste zuerst, gleiche Ereignis-Id). `paused_deliveries` (nur zusammen mit `active: true`, sonst 400 `validation_error`): `resume` (Standard) oder `discard` — verwirft alle pausierten Zustellungen (`abandoned`), z. B. wenn Sie per Änderungsabgleich aufholen. Unbekannte oder fremde Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X PATCH "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"events":["payment.received"],"active":true}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = '{"events":["payment.received"],"active":true}'
Invoke-RestMethod -Method Patch -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-patch-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"events": [
"payment.received"
],
"active": true
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-patch-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.patch(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={"events":["payment.received"],"active":True},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
"url": "https://erp.example.com/hooks/manage2hero",
"description": "ERP Buchhaltung",
"events": [
"payment.received"
],
"active": true,
"disabled_reason": null,
"disabled_at": null,
"created_at": "2026-10-10T08:15:30.412907+00:00",
"updated_at": "2026-10-10T09:30:00.000000+00:00",
"secret_rotated_at": null,
"previous_secret_valid_until": null,
"last_success_at": null,
"last_failure_at": null,
"enqueue_failures": 0,
"pending_deliveries": 0,
"paused_deliveries": 0,
"recording_gap": null
},
"request_id": "req_4n2Xc9…"
}DELETE/v1/webhooks/{id}
Löscht den Webhook samt Zustellverlauf; offene Zustellungen entfallen. Keine Query-Parameter, kein Body. Unbekannte, fremde oder bereits gelöschte Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X DELETE "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Method Delete -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.delete(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
"deleted": true
},
"request_id": "req_4n2Xc9…"
}POST/v1/webhooks/{id}/rotate-secret
Erzeugt ein neues Signaturgeheimnis und zeigt es **einmalig** an (`secret`). Das bisherige gilt **24 Stunden** weiter: in dieser Zeit steht im Kopf `X-M2H-Signature` neben der neuen auch die alte Signatur (zwei `v1=`-Einträge, `previous_secret_valid_until` nennt das Ende), damit Sie ohne Ausfall umstellen können. Eine zweite Rotation innerhalb dieser 24 Stunden beendet die Übergangszeit des ersten Geheimnisses. `Idempotency-Key` ist Pflicht; eine Wiederholung eines früheren Schlüssels (auch nach weiteren Rotationen) rotiert innerhalb von **30 Tagen** nicht noch einmal und zeigt das Geheimnis nicht erneut; danach gilt derselbe Schlüssel als neuer Vorgang. Unbekannte oder fremde Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X POST "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/rotate-secret" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/rotate-secret" -Headers $kopfimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-rotate-secret-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/rotate-secret', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-rotate-secret-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/rotate-secret",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"id": "3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80",
"url": "https://erp.example.com/hooks/manage2hero",
"description": "ERP Buchhaltung",
"events": [
"payment.received",
"invoice.created"
],
"active": true,
"disabled_reason": null,
"disabled_at": null,
"created_at": "2026-10-10T08:15:30.412907+00:00",
"updated_at": "2026-10-10T08:15:30.412907+00:00",
"secret_rotated_at": "2026-10-10T10:00:00.000000+00:00",
"previous_secret_valid_until": "2026-10-11T10:00:00.000000+00:00",
"last_success_at": null,
"last_failure_at": null,
"enqueue_failures": 0,
"pending_deliveries": 0,
"paused_deliveries": 0,
"recording_gap": null,
"secret": "whsec_Q2xhdWRlUm90YXRlZFNlY3JldE5vdFJlYWwwMDAwMDA"
},
"request_id": "req_4n2Xc9…"
}POST/v1/webhooks/{id}/test
Stellt ein Testereignis `webhook.test` für genau diesen Webhook ein (gleiche Signatur und Kopfzeilen wie jedes Ereignis, `data` enthält `webhook_id`). Gesendet wird vom Server im Minutentakt — das Ergebnis steht danach in `GET /v1/webhooks/{id}/deliveries`. Ein Versuch, ein Fehlschlag schaltet den Webhook **nicht** ab. `Idempotency-Key` ist Pflicht (gleicher Schlüssel mit demselben Token = dieselbe Testzustellung, auch nach Abschluss, solange sie im Zustellverlauf steht — dieser hält 30 Tage; danach löst derselbe Schlüssel ein neues Testereignis aus). Höchstens 3 Testereignisse warten gleichzeitig je Webhook: 429 `rate_limited`. Ein abgeschalteter Webhook: 400 `validation_error`. Unbekannte oder fremde Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X POST "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/test" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/test" -Headers $kopfimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-test-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-test-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/test",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"delivery": {
"id": "b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13",
"event_id": "0e7f5a31-9c2b-4d68-8a14-6f3e2d1c0b97",
"event_type": "webhook.test",
"status": "pending",
"attempts": 0,
"replay_count": 0,
"created_at": "2026-10-10T09:02:11.208154+00:00",
"last_attempt_at": null,
"next_attempt_at": "2026-10-10T09:02:11.208154+00:00",
"delivered_at": null,
"response_status": null,
"error": null
}
},
"request_id": "req_4n2Xc9…"
}GET/v1/webhooks/{id}/deliveries
Zustellungen dieses Webhooks, neueste zuerst, mit Statuscode des Empfängers und einem festen Fehlercode (`timeout`, `connection_failed`, `tls_error`, `blocked_target`, `redirect_not_followed`, `http_status`) — **nie** der Antwortkörper des Empfängers und nie die Nutzlast. `status`: `pending` (wartet auf den nächsten Versuch), `succeeded`, `failed` (sechs Versuche erfolglos — der Webhook wurde abgeschaltet), `paused` (angehalten, weil der Webhook abgeschaltet ist oder war; wird nach dem Wiedereinschalten von selbst wieder aufgenommen, nach 29 Tagen nicht mehr), `abandoned` (verworfen: `paused_deliveries: discard`, ein Testereignis bei Abschaltung oder älter als 29 Tage beim Wiederaufnehmen); `failed` und `abandoned` lassen sich per Replay erneut einstellen (`POST …/deliveries/{delivery_id}/retry`, `POST …/deliveries/retry-failed`), `replay_count` zählt das. Zeitfilter auf den Zeitpunkt des Ereignisses: genau eine Form aus `period` (`today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_3_months`, `last_12_months`, `all`), `date` oder `from`/`to`; ohne Angabe gelten die letzten 30 Tage. Zustellungen werden 30 Tage aufbewahrt. Filter: `status`, `event_type`. Mit `next_cursor` weiterblättern. Unbekannte oder fremde Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
limit | Query | Einträge je Seite, 1–200 (Vorgabe 50; beim Import: Datensätze je Seite, 1–500, Vorgabe 100; beim Änderungsabgleich 1–500, Vorgabe 100) |
cursor | Query | next_cursor der vorigen Seite; beim Änderungsabgleich Pflicht: Einstieg aus …/changes/cursor, next_cursor oder resume_cursor |
period | Query | Fester Zeitraum: today, yesterday, this_month, last_month, last_7_days, last_30_days (Vorgabe), last_3_months, last_12_months oder all. Nicht zusammen mit date, from oder to |
date | Query | Genau ein Tag, YYYY-MM-DD. Nicht zusammen mit period, from oder to |
from | Query | Erster Tag des Zeitraums, YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich, z. B. 2026-10-01T08:00:00Z |
to | Query | Letzter Tag des Zeitraums (einschließlich), YYYY-MM-DD. Bei Listen mit period zusätzlich als Zeitpunkt möglich; dann ist er die ausgeschlossene Obergrenze |
status | Query | Buchungen: open, partially_paid, paid, cancelled. E-Mails: sent, delivered, opened. Handwerker-Aufträge: not_sent, sent, failed. Zugriffsprotokoll: HTTP-Status (z. B. 404) oder Klasse (4xx, 5xx). Einheiten: available, reserved, rented, blocked. Schäden: WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR. Abrechnungen: finalized, sent, cancelled. Webhook-Zustellungen: pending, succeeded, failed, abandoned. Termine (nur Übergabetermine): planned, confirmed, rescheduled, completed, cancelled. Interessenten: new, open_bank_details, bank_details_received, contract_ready, contract_signing, contract_submitted, waitlisted, converted, rejected, expired, withdrawn. Warteliste: waiting, notified, assigned, cancelled |
event_type | Query | Nur Zustellungen dieses Ereignisses, z. B. payment.received oder webhook.test |
curl -s "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries" \
-H "Authorization: Bearer $M2H_API_TOKEN"$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries" -Headers $kopfconst res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries', {
headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport os, requests
res = requests.get(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries",
headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"items": [
{
"id": "b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13",
"event_id": "0e7f5a31-9c2b-4d68-8a14-6f3e2d1c0b97",
"event_type": "payment.received",
"status": "succeeded",
"attempts": 1,
"replay_count": 0,
"created_at": "2026-10-10T09:02:11.208154+00:00",
"last_attempt_at": "2026-10-10T09:02:40.000000+00:00",
"next_attempt_at": null,
"delivered_at": "2026-10-10T09:02:40.512300+00:00",
"response_status": 200,
"error": null
}
],
"next_cursor": null
},
"request_id": "req_4n2Xc9…"
}POST/v1/webhooks/{id}/deliveries/{delivery_id}/retry
Stellt **eine** fehlgeschlagene (`failed`) oder verworfene (`abandoned`) Zustellung erneut ein: Sie wird wieder `pending`, ihre Versuche zählen neu (`attempts` ab 0, `replay_count` zählt die Wiederherstellungen) und sie wird im nächsten Minutentakt gesendet — mit **derselben** Ereignis-Id (`X-M2H-Event-Id`) und demselben Körper, aber neuem Zeitstempel und neuer Signatur. Der Webhook muss aktiv und sendefähig sein (sonst 400 `validation_error`: zuerst `PATCH {"active": true}`). Nicht möglich für wartende (`pending`) und zugestellte (`succeeded`) Zustellungen, für Testereignisse (`webhook.test`; dafür `POST /v1/webhooks/{id}/test`) und für Zustellungen, die älter als 29 Tage sind: 409 `delivery_not_replayable` (`error.details.reason`: `not_failed`, `test_event`, `expired`, `paused` — pausierte Zustellungen nimmt der Zustelldienst nach dem Wiedereinschalten von selbst wieder auf). Höchstens 1000 wiederhergestellte Zustellungen je Webhook und Stunde: 429 `rate_limited` mit `Retry-After`. `Idempotency-Key` ist Pflicht: derselbe Schlüssel für dieselbe Zustellung stößt nichts erneut an (`replayed: false`, aktueller Stand). Scheitert die Zustellung erneut sechsmal, wird der Webhook wie sonst abgeschaltet. Unbekannte oder fremde Webhook- oder Zustellungs-Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
delivery_id | Pfad | Id (UUID) |
curl -s -X POST "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13/retry" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13/retry" -Headers $kopfimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-deliveries-b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13-retry-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13/retry', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-deliveries-b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13-retry-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13/retry",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"delivery": {
"id": "b8d2e4f6-1a3c-4e5d-9f70-8c6b4a2e0d13",
"event_id": "0e7f5a31-9c2b-4d68-8a14-6f3e2d1c0b97",
"event_type": "payment.received",
"status": "pending",
"attempts": 0,
"replay_count": 1,
"created_at": "2026-10-10T09:02:11.208154+00:00",
"last_attempt_at": "2026-10-10T09:02:40.000000+00:00",
"next_attempt_at": "2026-10-12T09:30:00.000000+00:00",
"delivered_at": null,
"response_status": 503,
"error": "http_status"
},
"replayed": true
},
"request_id": "req_4n2Xc9…"
}POST/v1/webhooks/{id}/deliveries/retry-failed
Sammelaufruf nach dem Wiedereinschalten: stellt fehlgeschlagene (`failed`) und verworfene (`abandoned`) Zustellungen dieses Webhooks erneut ein (Wirkung je Zustellung wie bei `POST …/deliveries/{delivery_id}/retry`) — **älteste zuerst, höchstens 100 je Aufruf**. Der Body ist optional (leer oder `{}` = alle der letzten 29 Tage) und höchstens 4 KB groß (413 `payload_too_large`); Filter: Zeitraum wie im Zustellverlauf (genau eine Form aus `period`, `date`, `from`/`to`), `status` (`failed` oder `abandoned`), `event_type` (nicht `webhook.test`) und `limit` (1–100). Antwort: `replayed` (Anzahl) und `remaining` (weitere passende Zustellungen) — bei `remaining > 0` den Aufruf mit **neuem** `Idempotency-Key` wiederholen. Der Webhook muss aktiv und sendefähig sein (sonst 400 `validation_error`); höchstens 1000 wiederhergestellte Zustellungen je Webhook und Stunde (429 `rate_limited` mit `Retry-After`; ohne Passendes antwortet der Aufruf mit `replayed: 0`). `Idempotency-Key` ist Pflicht: dieselbe Anfrage mit demselben Schlüssel stößt nichts erneut an und meldet `replayed` des ersten Aufrufs (`remaining` wird neu gezählt). Unbekannte oder fremde Id: 404.
Scope: webhooks:manage
| Parameter | Ort | Beschreibung |
|---|---|---|
id | Pfad | Id (UUID) |
curl -s -X POST "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/retry-failed" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"status":"abandoned","period":"last_7_days","limit":100}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = '{"status":"abandoned","period":"last_7_days","limit":100}'
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/retry-failed" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-deliveries-retry-failed-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/retry-failed', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"status": "abandoned",
"period": "last_7_days",
"limit": 100
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-webhooks-3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80-deliveries-retry-failed-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/deliveries/retry-failed",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={"status":"abandoned","period":"last_7_days","limit":100},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"replayed": 42,
"remaining": 0
},
"request_id": "req_4n2Xc9…"
}POST/v1/sandbox/events
Nur mit einem **Sandbox-Token** (`m2h_test_…`; mit einem echten Token gibt es den Endpunkt nicht: 404). Stellt ein Ereignis des Typs `type` (`payment.received`, `dunning.sent`, `contract.signed`, `invoice.created`, `document.created`, `email.sent`, `damage.created`, `contract.terminated`, `invoice.cancelled`, `damage.updated`) für ein **Musterobjekt** der Sandbox ein - ohne echte Buchung, Mail oder Zahlung. Empfänger sind die **aktiven** Webhooks der Sandbox, die den Typ abonnieren (höchstens 3 Webhooks je Sandbox); Antwort: `event_id`, `object_id` (das Musterobjekt, Details per GET mit Ihrem Token), `recipients` (Webhooks, an die dieser Vorgang eingestellt wurde) und `queued` (eingestellte Zustellungen) - beide stammen aus den Zustellungen des Vorgangs und ändern sich bei einer Wiederholung mit demselben Schlüssel nicht, auch wenn Sie Webhooks inzwischen abgeschaltet oder umgestellt haben - sowie `delivery`. Gesendet wird vom Server im Minutentakt, **mit Signatur, Wiederholungen, Selbstabschaltung und Replay wie bei echten Webhooks**, die Nachricht trägt `livemode: false` (und den Kopf `X-M2H-Livemode: false`); der Verlauf steht in `GET /v1/webhooks/{id}/deliveries`. `delivery: not_in_this_environment` heißt: Entwicklung und Staging stellen nichts zu (nur die Produktion sendet) - die Zustellungen bleiben dort wartend. Zugestellt wird nur bei aktivem API-Zugang (sonst 403 `sandbox_unavailable` bei jedem Aufruf). `Idempotency-Key` ist Pflicht: derselbe Schlüssel wählt dasselbe Musterobjekt und stellt nichts erneut ein; die `event_id` bleibt dabei etwa sieben Tage stabil (so lange gibt es die Zustellungen), danach beginnt ein neuer Vorgang mit neuer `event_id`. Quote: höchstens 100 wartende und 500 in 24 Stunden eingestellte simulierte Zustellungen je Sandbox (422 `sandbox_limit_reached`); Zustellungen der Sandbox werden nach 7 Tagen gelöscht.
Scope: webhooks:manage
curl -s -X POST "https://api.manage2hero.com/v1/sandbox/events" \
-H "Authorization: Bearer $M2H_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{"type":"payment.received"}'$kopf = @{
Authorization = "Bearer $env:M2H_API_TOKEN"
"Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = @'
{
"type": "payment.received"
}
'@
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/sandbox/events" -Headers $kopf -ContentType "application/json; charset=utf-8" -Body $bodyimport { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
// Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
// Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
const VORGANG = 'auftrag-2026/1001'; // EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
const KEY_FILE = `idempotency-key-post-v1-sandbox-events-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
writeFileSync(tmp, neu, { flag: 'wx' }); // erst vollständig in eine temporäre Datei schreiben
try {
linkSync(tmp, KEY_FILE); // veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
} catch (e) {
if (e.code !== 'EEXIST') throw e; // vorhandener Schlüssel bleibt unverändert
} finally {
unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();
const res = await fetch('https://api.manage2hero.com/v1/sandbox/events', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
'Content-Type': 'application/json',
// Bei jeder Wiederholung (z. B. nach einem Timeout) diesen gespeicherten Schlüssel erneut senden.
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
"type": "payment.received"
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const daten = body.data; // hier weiterverarbeitenimport hashlib, os, uuid, requests
from pathlib import Path
# Schlüssel EINMAL erzeugen und VOR dem Senden dauerhaft speichern (hier: Datei, eine je Vorgang). Nach einem
# Neustart wird derselbe Schlüssel gelesen. Für einen neuen, anderen Vorgang einen neuen Dateinamen wählen. Datei erst nach der endgültigen Antwort löschen.
VORGANG = "auftrag-2026/1001" # EIGENE Kennung je Vorgang setzen (z. B. Auftrags-Id); nie für zwei Vorgänge wiederverwenden
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16] # als Dateiname gehasht: beliebige Zeichen (/, \ …) in der Kennung sind unkritisch
KEY_FILE = Path(f"idempotency-key-post-v1-sandbox-events-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}") # eindeutiger Name je Versuch, nie eine vorhandene Datei überschreiben
with tmp.open("x") as f:
f.write(neu) # erst vollständig in eine temporäre Datei schreiben
try:
os.link(tmp, KEY_FILE) # veröffentlicht sie atomar, ohne eine vorhandene zu überschreiben
except FileExistsError:
pass # vorhandener Schlüssel bleibt unverändert
finally:
tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()
res = requests.post(
"https://api.manage2hero.com/v1/sandbox/events",
headers={
"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
"Idempotency-Key": idempotency_key,
},
json={
"type": "payment.received"
},
timeout=30,
)
body = res.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])Beispielantwort
{
"success": true,
"data": {
"event_id": "0e7f5a31-9c2b-4d68-8a14-6f3e2d1c0b97",
"event_type": "payment.received",
"object_id": "7c1d9e2a-3b4f-4a65-8d70-1e2f3a4b5c6d",
"recipients": 1,
"queued": 1,
"delivery": "enabled"
},
"request_id": "req_4n2Xc9…"
}