Zum Inhalt springen

Anleitungen

Vermietungsquote aller Garagenparks täglich abrufen

Token mit Scope occupancy:read anlegen, dann täglich GET /v1/occupancy abrufen (alle Seiten).

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/occupancy?limit=200" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/occupancy?limit=200" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/occupancy?limit=200', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/occupancy?limit=200",
    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"])

Vermietungsquote in Excel (Power Query) — ohne Programmieren

Excel → Daten → Daten abrufen → Aus anderen Quellen → Leere Abfrage → Erweiterter Editor, folgenden Text einfügen und DEIN_TOKEN ersetzen. Danach „Schließen & laden“; mit „Alle aktualisieren“ holen Sie jederzeit den neuesten Stand.

Power Query (M)
let
    Token = "DEIN_TOKEN",
    Antwort = Json.Document(Web.Contents("https://api.manage2hero.com/v1/occupancy",
        [Query = [limit = "200"], Headers = [Authorization = "Bearer " & Token]])),
    Liste = Antwort[data][items],
    Tabelle = Table.FromRecords(Liste),
    Typen = Table.TransformColumnTypes(Tabelle, {
        {"units_total", Int64.Type}, {"units_rented", Int64.Type},
        {"occupancy_rate", Percentage.Type}})
in
    Typen
Das Token steht damit in der Excel-Datei. Geben Sie die Datei nicht weiter und verwenden Sie ein eigenes Token nur mit occupancy:read. Excel fragt beim ersten Abruf nach dem Datenschutz der Quelle — „Organisation“ wählen.

Verlauf der Vermietungsquote (Monate oder Tage)

Mit demselben Scope occupancy:read liefert GET /v1/occupancy/history die Entwicklung über die Zeit — ohne location_id für die ganze Organisation, mit location_id für einen Garagenpark. Gemessen wird stündlich; der Verlauf beginnt mit der Einführung des Endpunkts (history_starts_on).

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/occupancy/history?from=2026-01-01&to=2026-12-31&interval=month" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/occupancy/history?from=2026-01-01&to=2026-12-31&interval=month" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/occupancy/history?from=2026-01-01&to=2026-12-31&interval=month', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/occupancy/history?from=2026-01-01&to=2026-12-31&interval=month",
    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"])

Aufteilung nach Garagentyp für heute: GET /v1/occupancy?group_by=storage_type.

Umsatz je Monat in Excel oder Power BI

Token mit Scope bookings:read. GET /v1/revenue liefert dieselbe Zahl wie das Dashboard der Verwaltung; group_by=month ergibt eine Zeile je Monat (auch Monate ohne Umsatz).

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-12-31&group_by=month" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-12-31&group_by=month" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-12-31&group_by=month', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/revenue?from=2026-01-01&to=2026-12-31&group_by=month",
    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"])
Power Query (M)
let
    Token = "DEIN_TOKEN",
    Antwort = Json.Document(Web.Contents("https://api.manage2hero.com/v1/revenue",
        [Query = [from = "2026-01-01", to = "2026-12-31", group_by = "month"],
         Headers = [Authorization = "Bearer " & Token]])),
    Tabelle = Table.FromRecords(Antwort[data][groups]),
    Typen = Table.TransformColumnTypes(Tabelle, {{"revenue_net", Currency.Type}})
in
    Typen
Stornos wirken rückwirkend: Eine stornierte Buchung fällt aus ihrem ursprünglichen Monat heraus. Frühere Monate daher regelmäßig neu abrufen statt fortzuschreiben. Für die Einzelbuchungen (z. B. Abgleich mit der Finanzbuchhaltung) GET /v1/bookings verwenden.

Zahlungseingänge mit der Finanzbuchhaltung abgleichen

Token mit payments:read (und bookings:read für die Gegenbuchungen im Journal). GET /v1/payments nennt je Eingang, welchen Buchungen und Verträgen er zugeordnet ist, und was noch offen ist (unallocated_amount). Braucht Ihr System Name, IBAN und Verwendungszweck der Zahlenden, vergeben Sie zusätzlich payments:read_counterparty — nur dann enthält die Antwort diese Felder.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30&match_status=unmatched" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30&match_status=unmatched" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30&match_status=unmatched', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/payments?from=2026-09-01&to=2026-09-30&match_status=unmatched",
    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"])

Offene Posten und Mahnstand, z. B. für ein Inkassobüro

Token nur mit open_items:read und dunning:read anlegen (für Namen und Kontaktdaten zusätzlich tenants:read). GET /v1/dunning/leases?min_level=3 liefert alle Verträge, die mindestens die dritte Mahnstufe erreicht haben — mit offener Summe, Gebühren und den einzelnen Forderungen.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/dunning/leases?min_level=3&limit=100" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/dunning/leases?min_level=3&limit=100" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/dunning/leases?min_level=3&limit=100', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/dunning/leases?min_level=3&limit=100",
    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"])

Summen und Altersstruktur für die Liquiditätsplanung: GET /v1/open-items/summary?group_by=location. Die Einzelposten liefert GET /v1/open-items.

Offene Posten umfassen Forderungen aus Mietverträgen — wie die Forderungsübersicht der Verwaltung. Einzelrechnungen ohne Vertrag stehen nicht darin. Mahnungen lösen Sie weiterhin in der Verwaltung aus.

Aktuelle Mieterliste eines Garagenparks exportieren

Token mit tenants:read und locations:read. Erst die Id des Garagenparks über GET /v1/locations ermitteln, dann:

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants?limit=200" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants?limit=200" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants?limit=200', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/locations/6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b/tenants?limit=200",
    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"])
Python: als CSV speichern
import csv, os, requests

TOKEN = os.environ["M2H_API_TOKEN"]
STANDORT = "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b"
zeilen, cursor = [], None
while True:
    params = {"limit": 200, **({"cursor": cursor} if cursor else {})}
    body = requests.get(f"https://api.manage2hero.com/v1/locations/{STANDORT}/tenants",
                        headers={"Authorization": f"Bearer {TOKEN}"}, params=params, timeout=30).json()
    if not body["success"]:
        raise RuntimeError(body["error"]["code"])
    zeilen += body["data"]["items"]
    cursor = body["data"]["next_cursor"]
    if not cursor:
        break

with open("mieter.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f, delimiter=";")
    w.writerow(["Einheit", "Vertrag", "Status", "Kundennummer", "Vorname", "Nachname", "Firma", "E-Mail", "Telefon"])
    for z in zeilen:
        t = z["tenant"]
        w.writerow([z["unit"]["number"], z["lease"]["contract_number"], z["lease"]["status"],
                    t["customer_number"], t["first_name"], t["last_name"], t["company"], t["email"], t["phone"]])

Bestandsmieter importieren

Der Import über die API ist derselbe Import wie unter Administration → Datenimport— nur mit JSON statt Datei. Ablauf:

1. Token mit tenants:import (und locations:read, um die Id des Garagenparks zu finden).
2. POST /v1/tenant-imports mit genau einem Garagenpark und 1–500 Datensätzen. Jeder Datensatz wird geprüft wie eine Zeile der Import-Datei; die Antwort nennt je Datensatz Fehler und Hinweise (z. B. „E-Mail gibt es schon bei einem Mieter“).
3. In der Verwaltung erscheint der Import im Import-Assistenten. Dort lassen sich fehlerhafte Datensätze korrigieren. Übernehmen können Sie dort oder per API (POST /v1/tenant-imports/{id}/commit, siehe unten).
4. Mit GET /v1/tenant-imports/{id} sehen Sie den Stand; nach der Übernahme stehen dort tenant_id und lease_id.

Felder eines Datensatzes

Dieselben Feldnamen wie in der Muster-Datei des Datei-Imports. Text als String, Beträge und Zahlen als JSON-Zahl (89.5), Ja/Nein als true/false, Datum als JJJJ-MM-TT. Pflicht sind Anrede, Vorname, Nachname, Geburtsdatum, E-Mail, Straße, Hausnummer, PLZ und Ort (bei Anrede „Firma“ zusätzlich der Firmenname). Die Vertragsnummer hat die Form Kürzel Einheit-lfd. Nr., z. B. HH01 42-1; über sie ordnet der Import-Assistent die Garage automatisch zu.

FeldTypPflicht
salutationText: Herr, Frau, Firma oder Diversja
first_nameTextja
last_nameTextja
company_nameText
date_of_birthDatum JJJJ-MM-TTja
emailText (E-Mail)ja
streetTextja
house_numberTextja
postal_codeTextja
cityTextja
countryText
phoneText
mobileText
contract_numberText (Kürzel Einheit-Nr.)
lease_startDatum JJJJ-MM-TT
rent_netZahl
depositZahl
deposit_paidtrue/false
move_in_dateDatum JJJJ-MM-TT
has_electricity_230vtrue/false
has_electricity_400vtrue/false
electricity_meter_feeZahl
electricity_advance_paymentZahl
electricity_price_kwh_netZahl
notice_period_monthsganze Zahl
minimum_lease_monthsganze Zahl
meter_readingZahl
meter_reading_dateDatum JJJJ-MM-TT
Bankdaten (iban, bic, account_holder) können über die API nicht übergeben werden — eine Anfrage damit wird abgelehnt (field_not_allowed). Bankverbindungen tragen Sie in der Verwaltung ein.
Für Importe gelten eigene Grenzen (Vorgabe: 50 offene Importe je Organisation, 20 neue je Token und Stunde) — siehe Grenzen.

Wiederholen ohne doppelten Import

Jede Anfrage braucht den Kopf Idempotency-Key — einen eigenen Schlüssel je Vorgang, z. B. eine UUID. Schicken Sie nach einem Abbruch (Timeout, Netzwerkfehler) dieselbe Anfrage mit demselben Schlüssel erneut, bekommen Sie denselben Import zurück (Status 200 statt 201) — es entsteht kein zweiter. Derselbe Schlüssel mit anderem Inhalt wird abgelehnt (idempotency_conflict).

curl (Bash: Linux, macOS, Git Bash)
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
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 Datensatz
Python (requests)
import 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"])

Stand abfragen

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" -Headers $kopf
JavaScript (Node.js 18+)
const 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 weiterverarbeiten
Python (requests)
import 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"])

Import abbrechen

Einen Import, den Sie nicht mehr brauchen (falsche Zuordnung, Testlauf), geben Sie mit DELETE /v1/tenant-imports/{id} frei. Das löscht den Import samt Datensätzen und gibt seinen Platz in der Grenze der offenen Importe sofort zurück — sonst bliebe er bis zur Aufbewahrungsfrist (30 bis 90 Tage) belegt. Es genügt der Scope tenants:import; der Ersteller des Tokens braucht in der Verwaltung zusätzlich Datenimport mit Vollzugriff — wie beim Löschen eines Imports dort (sonst 403 insufficient_permissions). Die Antwort lautet { "session_id": "…", "deleted": true }.

Abbrechen lässt sich nur ein Import, der über die API angelegt wurde. Unbekannte, fremde oder in der Verwaltung hochgeladene Importe und ein zweiter Aufruf liefern 404 not_found. Ist mindestens ein Datensatz schon übernommen, wird nichts gelöscht (409 session_has_transferred_records): Mieter und Verträge verweisen auf den Import. Wer den Import gerade im Import-Assistenten bearbeitet, verliert ihn — stimmen Sie sich bei gemeinsam genutzten Importen ab. Das Abbrechen eines Imports, der jünger als eine Stunde ist, zählt gegen die Stundengrenze neuer Importe je Token (429 import_rate_limited). Insgesamt sind je Token und Stunde also höchstens doppelt so viele neue Importe möglich wie die Grenze: einmal die Grenze beim Anlegen, einmal beim Abbrechen und erneuten Anlegen.

curl (Bash: Linux, macOS, Git Bash)
curl -s -X DELETE "https://api.manage2hero.com/v1/tenant-imports/01923f4e-7a2b-7c3d-8e4f-5a6b7c8d9e0f" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$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 $kopf
JavaScript (Node.js 18+)
const 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 weiterverarbeiten
Python (requests)
import 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"])

Dokumente abrufen und herunterladen

Mit documents:read listen Sie die Mieter- und Standortdokumente Ihrer Organisation: GET /v1/documents (Filter tenant_id, lease_id, location_id, category und der Zeitfilter nach Erstellungsdatum). Die Antwort nennt nur Angaben zur Datei — Name, Größe, Art, Datum und die Zuordnung — nie den Speicherort. Ausgeschlossen sind vertrauliche Arten wie Ausweise und Bonitätsunterlagen sowie archivierte Dokumente. Der Abgleich nach Änderungen (/v1/documents/changes) meldet auch, wenn eine Schlussabrechnung storniert wird (settlement_cancelled).

Die Datei selbst holen Sie mit POST /v1/documents/{id}/download-link (zusätzlich documents:download). Die Antwort enthält einen Link, der 5 Minuten gültig ist — rufen Sie die Datei sofort ab und speichern Sie den Link nicht. Jede Erzeugung zählt als schreibender Aufruf (20 statt 120 je Minute) und steht im Zugriffsprotokoll. Gibt es zum Eintrag keine Datei mehr, antwortet die API mit 404 file_not_available.

E-Mails an Mieter abrufen

GET /v1/emails (Scope emails:read) zeigt die System-E-Mails, die Ihre Organisation an Mieter versendet hat — Verträge, Abrechnungen, Rechnungen, Mahnungen, Kündigungen, Nachträge, Termine, Protokolle, Entscheidungen und Schadensmeldungen (38 freigegebene Vorlagen, dieselben wie im E-Mail-Verlauf des Mieterportals). Filter: tenant_id, lease_id, location_id (alle Mieter des Standorts), template_key, status (sent, delivered, opened) und der Zeitfilter nach Versandzeitpunkt. Eine Mail hat Betreff, einen Auszug von höchstens 200 Zeichen, Status und Zeitpunkt. Den Volltext als reinen Text (body_text) liefert die API nur mit dem Zusatz-Scope emails:read_body; behandeln Sie ihn beim Anzeigen als Text, nicht als HTML.

Nie enthalten: die Empfängeradresse, Kennungen des Versanddienstes, Fehlertexte, Mails mit Zugangsdaten oder Einmal-Links, Nachrichten an Interessenten und die E-Mails zum API-Zugang selbst (die der Plattform-Betreiber verwaltet). Der Mieterbezug entsteht beim Versand; ältere Mails ohne Bezug erscheinen nicht.

Zugriffsprotokoll lesen

GET /v1/usage (Scope usage:read) zeigt die Abrufe aller Tokens Ihrer Organisation, neueste zuerst: Token, Methode, Pfad (ohne Query-String), Status, Zeitpunkt und request_id. Filter: token_id, status (ein Code wie 404 oder eine Klasse wie 4xx), path_prefix und der Zeitfilter. IP-Adressen, User-Agent und Inhalte der Anfragen erscheinen nie. Abrufe werden 90 Tage aufbewahrt. Bei abgelehnten Anmeldungen bestimmt der Aufrufer den Pfad selbst — zeigen Sie ihn deshalb nie ungeprüft als HTML an.

Übernehmen per API

Mit einem Token mit tenants:import_commit übernehmen Sie einen API-Import ohne den Import-Assistenten: je Datensatz entstehen Mieter, laufender Vertrag mit Kostensatz, Anfangs-Zählerstand und die monatliche Dauerbuchung ab dem nächsten Monatsersten — rückwirkend wird nichts gebucht. Der Ersteller des Tokens braucht in der Verwaltung Datenimport (Bearbeiten) und Vermietungsprozess (Bearbeiten); er steht als Bearbeiter an den angelegten Daten.

Datensätze ohne Garage werden wie im Assistenten über die Vertragsnummer (HH01 42-1 → Einheit 42) zugeordnet. Mit rows übernehmen Sie nur bestimmte Zeilen, ohne rows alle. on_duplicate regelt Datensätze, deren E-Mail schon einem Mieter gehört: reject (Vorgabe) übernimmt sie nicht, reuse hängt den Vertrag an genau diesen Mieter, create_new legt einen neuen Mieter an.

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" \
  --data '{"on_duplicate": "reject"}'

Die Antwort nennt je Zeile created (mit tenant_id, lease_id), skipped, failed (mit Code, z. B. not_assigned, unit_occupied) oder already_transferred. Ein wiederholter Aufruf — etwa nach einem Timeout — ist unschädlich: bereits übernommene Zeilen werden nicht erneut angelegt und melden already_transferred mit tenant_id, lease_id und contract_number. Meldet eine Zeile record_changed, wurde sie währenddessen in der Verwaltung geändert; rufen Sie die Übernahme einfach erneut auf. Eine Übernahme ist endgültig. Der Body ist höchstens 64 KB groß (413 payload_too_large).

Bei on_duplicate: reject meldet die Antwort skipped mit duplicate_tenant — im Import (GET /v1/tenant-imports/{id}) bleibt die Zeile aber mit Status pending und error_code: tenant_decision_required stehen. Sie wird nicht als übernommen geführt; entscheiden können Sie sie im Import-Assistenten oder mit einem erneuten Aufruf mit reuse bzw. create_new.

Verträge lesen

GET /v1/contracts und GET /v1/contracts/{id} (Scope contracts:read) liefern Verträge mit Status, Laufzeit, Kaution, Unterschriftsstand, Kündigungen und Nachträgen (Strom, Wasser). Der Ersteller des Tokens braucht Vermietungsprozess und Mieter, jeweils mindestens Lesen. Filter: tenant_id, location_id (Verträge an Garagen des Standorts) und status. Mit ?format=csv oder xlsx bekommen Sie die Liste als Datei.

Der Zeitfilter bezieht sich auf die letzte Änderung (updated_at); ohne Angabe gelten die letzten 30 Tage. Für alle Verträge verwenden Sie period=all, für einen Abgleich seit einem Zeitpunkt from=<Zeitpunkt>. updated_at ist das Neueste aus dem Vertrag, seinen Kündigungen, Nachträgen und Unterschriften.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/contracts?period=last_7_days&status=termination_received" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contracts?period=last_7_days&status=termination_received" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/contracts?period=last_7_days&status=termination_received', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/contracts?period=last_7_days&status=termination_received",
    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"])
Änderungsabgleich: Für Verträge gibt es einen Änderungsstrom: GET /v1/contracts/changes/cursor (Einstieg, vor dem Vollabruf), GET /v1/contracts/changes (geänderte Kennungen seit dem Cursor) und GET /v1/contracts/by-ids (aktueller Stand, höchstens 100 Kennungen). Er meldet auch, was ein Zeitfilter nie zeigt: das Zurücksetzen einer Gegenzeichnung, das Löschen einer Kündigung oder eines Nachtrags, gelöschte und zum Löschen vorgemerkte Verträge (die Kennung fehlt dann in by-ids). Ablauf und Fehler (410 cursor_expired nach 90 Tagen) wie bei den übrigen Ressourcen, siehe „Änderungsabgleich“. Das unterschriebene PDF ist über die API nicht abrufbar; Vertragstext, Hashes, Bankdaten und Freitexte der Kündigungen erscheinen nie. Zum Löschen vorgemerkte Verträge fehlen in der Liste.

Einheiten und Garagen lesen

GET /v1/units und GET /v1/units/{id} (Scope units:read; der Ersteller braucht Garagen/Einheiten und Standorte, jeweils Lesen) zeigen die einzelnen Stellplätze mit Status, Maßen, Ausstattung, aktuellem Vertrag, Mieter und Grundmiete. Filter: location_id, storage_type_id und status (available, reserved, rented, blocked).

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/units?status=available&limit=200" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/units?status=available&limit=200" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/units?status=available&limit=200', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/units?status=available&limit=200",
    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"])

Abweichung bei der Liste: Ohne Zeitfilter ist die Liste vollständig (Stammdaten); ein Zeitfilter bezieht sich auf die letzte Änderung der Einheit selbst. Eine abgelaufene, noch nicht freigegebene Reservierung erscheint bereits als available. Der Sperrgrund wird nie ausgegeben.

Der aktuelle Vertrag ist der Vertrag, der heute läuft (Europe/Berlin): Status von „eingezogen“ bis „Abrechnung“, Mietbeginn erreicht. Künftige, beendete und zum Löschen markierte Verträge erscheinen nie. current_rent ist die Grundmiete ohne Nebenkosten, Strom, Wasser und Kaution (net je Monat, vat_rate als Bruch, gross auf Cent gerundet). Es ist null, wenn kein Vertrag läuft oder der Vertrag heute keinen gültigen Kostensatz hat. Vom Mieter sehen Sie nur Kennung und Anzeigename; Details gibt es über GET /v1/tenants/{id}.

Rufen Sie für die aktuelle Belegung die Liste regelmäßig vollständig ab. Ein neuer Vertrag oder ein Mieterwechsel ist kein verlässlicher Änderungsanlass für updated_at der Einheit.

Nebenkosten-Abrechnungen und Zähler

Lesen mit utility:read: GET /v1/utility/statements (Strom- und Wasserabrechnungen je Mieter und Vertrag), GET /v1/meters (Zähler) und GET /v1/meters/{id}/readings (Zählerstände). Der Ersteller braucht Zählerstände, Strom und Wasser, jeweils mindestens Lesen. Alle drei Listen gibt es auch als CSV oder Excel.

Abrechnungen

Filter: year (Jahr, in dem der Abrechnungszeitraum endet), status (finalized, sent, cancelled, je Position), tenant_id, lease_id, location_id und updated_since. Der Zeitfilter bezieht sich auf den Erstellungszeitpunkt; mit year, lease_id oder updated_since ohne Zeitangabe gilt period=all, sonst die letzten 30 Tage. Beträge sind die gespeicherten Werte der Verwaltung; die API rechnet nichts neu. Entwürfe erscheinen nie.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/utility/statements?year=2025" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/utility/statements?year=2025" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/utility/statements?year=2025', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/utility/statements?year=2025",
    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"])
Reihenfolge: Abrechnungen und Zähler kommen nach Änderungszeitpunkt (updated_at, dann id) aufsteigend; der Cursor trägt genau dieses Paar. Eine Zeile, die sich beim Blättern ändert, rückt ans Ende und wird noch geliefert. Die Zählerstände bleiben „neueste Ablesung zuerst“. Zähler haben keinen Zeitfilter (Stammdaten).

Zählerstand eintragen

POST /v1/meters/{id}/readings (Scope utility:write zusätzlich zu utility:read; der Ersteller braucht Bearbeiten) trägt eine Zwischenablesung ein. Pflicht sind der Kopf Idempotency-Key sowie reading_date (Tag, höchstens ein Tag in der Zukunft) und meter_value (Zahl ab 0). Am Hauptstromzähler ist außerdem obis_code Pflicht (1.8.0, 1.8.1, 1.8.2 Netzbezug; 2.8.0, 2.8.1, 2.8.2 Einspeisung), an allen anderen Zählern ist er nicht erlaubt. Den Vertrag bestimmt die API wie die Verwaltung aus dem Ablesedatum, Ableser ist der Ersteller des Tokens. Ein- und Auszugsprotokolle, Korrekturen und Fotos gehen nicht über die API; deaktivierte Zähler nehmen keine neuen Stände an.

curl (Bash: Linux, macOS, Git Bash)
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}'
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 weiterverarbeiten
Python (requests)
import 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"])

Antwort 201 mit der angelegten Ablesung (source: "api"). Eine Wiederholung mit gleichem Schlüssel und gleichem Inhalt liefert 200 mit derselben Ablesung (nichts wird doppelt), anderer Inhalt 409 idempotency_conflict. Der Schlüssel gilt je Token.

Plausibilitätsprüfung wie in der Verwaltung: Ein Stand kleiner als der Vorwert oder größer als ein späterer Stand (chronological_order) und ein Datum in einem bereits abgerechneten Zeitraum (in_billed_period) ergeben 400 validation_error; die Codes stehen in error.details.plausibility. Eine Warnung (unrealistic_consumption) ergibt ebenfalls 400, mit error.details.plausibility_warnings; bestätigen Sie sie mit force_warnings: true (mit demselben oder einem neuen Schlüssel). Wird die Ablesung später in der Verwaltung gelöscht, legt ein Retry mit demselben Schlüssel sie neu an. Ändert sich der Zähler gleichzeitig, prüft die API selbst bis zu dreimal erneut; danach kommt 503 service_unavailable und Sie senden mit demselben Schlüssel erneut.

DATEV-Export anfordern und herunterladen

Mit datev:export stoßen Sie den Buchungsstapel eines Standorts und Monats an. Für den Scope selbst braucht der Ersteller Finanzbuchhaltung (Lesen); der DATEV-Export in der Verwaltung prüft dasselbe Finanzrecht. Das Token anlegen darf nur, wer in der Verwaltung API-Zugang mit Vollzugriff hat (gilt für jedes Token). Es gibt keinen getrennten Zusatz-Scope für den Download; weil die Datei Buchungsdaten enthält, gehört das Token in sichere Hände. Der Export schreibt nichts in die Buchhaltung; festgehalten wird nur der Verlauf.

  1. POST /v1/datev/exports mit Idempotency-Key und location_id, year (2020–2099), month (1–12), optional format (datev als Standard in Windows-1252 oder csv in UTF-8 mit BOM). Der Export läuft sofort; die Antwort 201 zeigt status = ready, row_count und file_available.
  2. Bei status = failed nennt failure_code den Grund (z. B. fehlende Berater- oder Mandantennummer am Standort, Buchung ohne sicheren Steuerschlüssel, leerer Stapel). Fordern Sie nach der Korrektur mit einem neuen Schlüssel erneut an.
  3. POST /v1/datev/exports/{id}/download-link liefert einen Link, der 5 Minuten gültig ist (nie länger, als die Datei noch aufbewahrt wird); expires_in_seconds nennt die tatsächliche Dauer. Laden Sie die Datei sofort und beachten Sie den Zeichensatz in encoding.
curl (Bash: Linux, macOS, Git Bash)
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"}'
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 weiterverarbeiten
Python (requests)
import 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"])
curl (Bash: Linux, macOS, Git Bash)
curl -s -X POST "https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link" \
  -H "Authorization: Bearer $M2H_API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
PowerShell (Windows)
$kopf = @{
  Authorization     = "Bearer $env:M2H_API_TOKEN"
  "Idempotency-Key" = [guid]::NewGuid().ToString()
}
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/datev/exports/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link" -Headers $kopf
JavaScript (Node.js 18+)
import { 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-2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f-download-link-${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/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link', {
  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 weiterverarbeiten
Python (requests)
import 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-2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f-download-link-{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/2f6c1d3e-8a4b-4c5d-9e7f-0a1b2c3d4e5f/download-link",
    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"])

Den Stand lesen Sie mit GET /v1/datev/exports/{id}; GET /v1/datev/exports zeigt den Verlauf der per API angeforderten Exporte (Zeitfilter nach Anlage, status, location_id). Exporte aus der Verwaltung erscheinen dort nicht.

Dateilaufzeit: Die Datei liegt 24 Stunden zum Abruf bereit (file_expires_at), danach ist file_available falsch und der Link ergibt 404 file_not_available; der Verlaufseintrag bleibt. Ist der Export noch nicht fertig, antwortet der Link mit 409 export_not_ready. Jeder Link-Aufruf erzeugt einen neuen Link, zählt als schreibender Aufruf (20 statt 120 je Minute) und sollte nie gespeichert oder weitergegeben werden. Stundenlimit: höchstens 30 neue Exporte je Organisation und Stunde (429 rate_limited, Retry-After: 3600); Wiederholungen mit demselben Schlüssel zählen nicht. Ein Auftrag, der länger als 15 Minuten offen bleibt, gilt als failed mit timeout. Der Export zeigt den aktuellen Stand des Stapels; ein Storno nach dem Export erzeugt keine Korrekturbuchung im bereits exportierten Stapel.

Handwerker und Aufträge lesen

Mit contractors:read lesen Sie GET /v1/contractors (Handwerksbetriebe) und GET /v1/contractor-orders (Reparatur-Aufträge je Schadensposition). Der Ersteller braucht Artikel und Handwerker, Garagen/Einheiten und Vermietungsprozess, jeweils mindestens Lesen; fehlt eines, lehnt schon die Token-Erstellung ab. Beide Listen gibt es auch als CSV oder Excel.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/contractor-orders?updated_since=2026-10-01T00:00:00Z&status=sent" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/contractor-orders?updated_since=2026-10-01T00:00:00Z&status=sent" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/contractor-orders?updated_since=2026-10-01T00:00:00Z&status=sent', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/contractor-orders?updated_since=2026-10-01T00:00:00Z&status=sent",
    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"])

Die Betriebe haben keinen Zeitfilter (kleine Stammdatenliste, vollständig abrufen), nur active. categories sind die Kategorien der Artikel, die der Betrieb ausführt. Bei den Aufträgen gilt der Zeitfilter für den Anlagezeitpunkt (ohne Angabe 30 Tage); Filter: contractor_id, status (not_sent, sent, failed), location_id, unit_id. Für einen täglichen Abgleich verwenden Sie updated_since allein: Dann gilt kein Anlage-Zeitraum, und ein vor Monaten angelegter, heute versendeter Auftrag erscheint.

updated_since meldet keine gelöschten Aufträge. Rufen Sie deshalb zusätzlich regelmäßig, etwa wöchentlich, mit period=all vollständig ab und entfernen Sie Kennungen, die nicht mehr erscheinen. Nie enthalten sind Adressen, Telefon, E-Mail, Steuer- und Bankdaten der Betriebe sowie Empfängeradresse, Fehlertexte und Anhänge der Aufträge. amount_net ist leer (null) bei Abrechnung nach Aufwand oder ohne Preisangabe; sonst ist es Menge mal Einzelpreis, auf 2 Stellen gerundet, bei einem Einzelpreis von 0 also 0.

Schäden lesen, anlegen und den Status setzen

GET /v1/damages und GET /v1/damages/{id} (Scope damages:read) zeigen die Schadensakten:kind unit (an einer Einheit) oder location (am Gelände). Meldungen von Mietern und Hausmeistern erscheinen erst nach der Annahme durch die Verwaltung. Der Zeitfilter gilt für den Meldezeitpunkt (ohne Angabe 30 Tage). Filter: location_id, unit_id, lease_id, tenant_id, status (WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN, KEINE_REPARATUR), kind, reporter_type und updated_since (mit Zeitzone). Eine unbekannte Kennung als Filter ergibt eine leere Liste.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/damages?status=WEITERE_KLAERUNG" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/damages?status=WEITERE_KLAERUNG" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/damages?status=WEITERE_KLAERUNG', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/damages?status=WEITERE_KLAERUNG",
    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"])

Änderungsabgleich: GET /v1/damages/changes/cursor (Einstieg, vor dem Vollabruf), GET /v1/damages/changes (geänderte Kennungen seit dem Cursor) und GET /v1/damages/by-ids (aktueller Stand, höchstens 100 Kennungen, Listenform ohne documents) melden neue und geänderte Akten, geänderte oder gelöschte Dokumente und Meldungen sowie gelöschte Akten (die Kennung fehlt dann in by-ids). Die Kennungen der Dokumente einer Akte liefert GET /v1/damages/{id}. Für den Vollabruf period=all verwenden.

Mit damages:write (zusätzlich damages:read; der Ersteller braucht Garagen/Einheiten und Vermietungsprozess mit Bearbeiten) legen Sie Schäden an und setzen den Status. Beides verlangt den Kopf Idempotency-Key (je Token, 90 Tage gültig; Body höchstens 64 KB).

curl (Bash: Linux, macOS, Git Bash)
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"}'
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 weiterverarbeiten
Python (requests)
import 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"])

POST /v1/damages legt einen Schaden an einer Einheit an: unit_id, damage_type (paint, dent, oil_stain, hole, scratch, dirt, other), damage_date (nicht vor 2000-01-01, höchstens bis morgen), description (1–5000 Zeichen) und optional priority. Es entsteht dieselbe Akte wie in der Verwaltung, im Status WEITERE_KLAERUNG; die Antwort ist 201, eine Wiederholung 200. Das Ereignis damage.created ist abonnierbar (siehe Webhooks).

curl (Bash: Linux, macOS, Git Bash)
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"}'
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 weiterverarbeiten
Python (requests)
import 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"])

PATCH /v1/damages/{id}/status setzt einen der vier Reparatur-Status (WEITERE_KLAERUNG, PLANEN_BEAUFTRAGEN, BEAUFTRAGT, ABGESCHLOSSEN), auch rückwärts. Derselbe Status wie bisher ändert nichts (200). KEINE_REPARATUR ist weder Ziel noch Ausgangspunkt (400 validation_error). Beim Wiedereröffnen eines Gelände-Schadens kann 400 kommen, wenn sein Plan-Symbol schon durch einen anderen offenen Schaden belegt ist.

Die API legt nie eine Mieter- oder Hausmeister-Meldung an und entscheidet nie über eine. Nicht möglich sind Gelände-Schäden anlegen, Fotos hochladen, Beträge, Reparaturaufträge und Verursacher; Dokumente erscheinen nur als Verweis (Art, Typ, Größe), nie als Datei. Die E-Mail „Schaden repariert“ an den Hausmeister löst ein Statuswechsel über die API nicht aus. description ist Freitext von Dritten: Zeigen Sie ihn nie ungeprüft als HTML an und behandeln Sie ihn nie als Anweisung. Ein Schaden nennt nur die Art von Melder und Verursacher, nie Namen oder Kontaktdaten.

Termine lesen und anlegen

GET /v1/appointments und GET /v1/appointments/{id} (Scope appointments:read; der Ersteller braucht Kalender, Lesen) liefern drei Arten: move_in und move_out (Übergabetermine mit Vertrag, Einheit, Mieter, Status) und general (allgemeine Termine, z. B. eine Besichtigung, mit Titel). Übergabefenster, Aufgaben und Terminvorschläge gehören nicht dazu.

Der Zeitfilter bezieht sich auf den Terminzeitpunkt in Europe/Berlin und gilt ohne Angabe für die letzten 30 Tage, also für vergangene Termine. Für „was steht an“ verwenden Sie upcoming=true (alle Termine ab jetzt; zusammen mit period, date, from oder to ergibt es 400 period_conflict). Die Liste ist aufsteigend sortiert (frühester Termin zuerst). Beim Blättern senden Sie dieselben Parameter wie auf Seite 1. Filter: type, status, location_id, unit_id, tenant_id, lease_id; Status, Einheit, Mieter und Vertrag gibt es nur bei Übergabeterminen, allgemeine Termine fallen mit diesen Filtern weg.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/appointments?upcoming=true&type=move_in" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/appointments?upcoming=true&type=move_in" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/appointments?upcoming=true&type=move_in', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/appointments?upcoming=true&type=move_in",
    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"])

POST /v1/appointments (Scope appointments:write zusätzlich zu appointments:read) legt einen Übergabetermin oder einen allgemeinen Termin an, mit Idempotency-Key (Body höchstens 16 KB, Antwort 201). Der Ersteller braucht Kalender mit Bearbeiten, für Übergabetermine zusätzlich Vermietungsprozess mit Bearbeiten. Ein Übergabetermin braucht type, lease_id, date und time; ein allgemeiner Termin type, title und date (optional time, location_id, duration_minutes).

curl (Bash: Linux, macOS, Git Bash)
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"}'
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 weiterverarbeiten
Python (requests)
import 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"])
  • Uhrzeit ist Ortszeit Europe/Berlin. Eine Uhrzeit, die es bei der Zeitumstellung nicht gibt (die Lücke im Frühjahr), wird abgelehnt (400 validation_error, time: invalid_value).
  • Datum: nicht in der Vergangenheit. Für heute zählt nur eine Uhrzeit, die noch nicht vorbei ist; ein allgemeiner Termin heute ohne Uhrzeit zählt ab Tagesbeginn und wird abgelehnt. Höchstens 3 Jahre voraus. Verstoß: 400 mit date: invalid_value.
  • Kein Umplanen: Gibt es für Vertrag und Art schon einen aktiven Übergabetermin, ergibt das 409 appointment_conflict, auch bei zwei gleichzeitigen Anfragen. Die API legt nur an, sie plant nicht um, sagt nicht ab und löscht nicht.
  • Hausmeister: Der Standard-Hausmeister des Standorts wird eingetragen. Ist keiner hinterlegt, kommt 400 (lease_id: invalid_value). Der Hausmeister erscheint nie in der Antwort.
  • Wie in der Verwaltung rückt der Vertrag bei Einzug auf „Einzugsprotokoll ausstehend“ und bei Auszug auf „Auszugsprotokoll ausstehend“.
  • Keine E-Mails und keine Benachrichtigungen: Anders als die Verwaltung benachrichtigt die API weder Hausmeister noch Mieter. Der Termin erscheint im Kalender der Verwaltung und des Hausmeisters.

Interessenten und Warteliste

Mit prospects:read (Vermietungsprozess, Lesen) lesen Sie GET /v1/prospects, GET /v1/prospects/{id} und GET /v1/waitlist. Die Antworten enthalten keine Kontaktdaten: nur Kennung, Status, Herkunft, Zeitpunkte, Name oder Firma, Nutzungsart und die Wünsche (Standort, Garagentyp, Mietbeginn, Strom, Wasser); nie E-Mail, Telefon, Anschrift, Geburtsdatum, Bank- oder Ausweisdaten. Die Warteliste nennt nur Kennungen. Einen Zusatz-Scope für Kontaktdaten gibt es bewusst nicht.

Anfragen: Der Zeitfilter gilt für den Anfragezeitpunkt (ohne Angabe 30 Tage); Filter location_id, storage_type_id, status, source (public_form, tenant_portal, api) und updated_since. Einen Filter unit_id gibt es nicht, denn Anfragen nennen Garagentypen. Die Warteliste zeigt je Eintrag den position (Platz am Standort, 1 = vorn, nur bei waiting und notified); auch hier gilt ohne Angabe der Zeitfilter der letzten 30 Tage, ältere offene Einträge erscheinen nur mit period=all.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/waitlist?period=all&status=waiting" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/waitlist?period=all&status=waiting" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/waitlist?period=all&status=waiting', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/waitlist?period=all&status=waiting",
    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"])

POST /v1/prospects (Scope prospects:write zusätzlich zu prospects:read; der Ersteller braucht Vermietungsprozess mit Bearbeiten) liefert eine Mietanfrage ein, etwa von Ihrer eigenen Webseite. Pflicht sind der Kopf Idempotency-Key und privacy_consent: true; die Pflichtfelder und Prüfungen sind dieselben wie im öffentlichen Formular (Anrede, Name, Geburtsdatum, Anschrift, Telefon oder Mobil, E-Mail, Nutzung, Herkunft, Mietbeginn und 1–5 Standorte mit Wünschen; Strom- und Wasseroption sind optional).

curl (Bash: Linux, macOS, Git Bash)
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}'
PowerShell (Windows)
$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 $body
JavaScript (Node.js 18+)
import { 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 weiterverarbeiten
Python (requests)
import 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"])
  • Limits: höchstens 30 neue Anfragen je Token und Stunde und 5 je Token und E-Mail-Adresse in 24 Stunden; sonst 429 rate_limited mit Retry-After (3600 bzw. 86400). Gezählt werden nur die über dieses Token eingelieferten Anfragen. Dazu gilt das schreibende Kontingent (20 je Minute). Body höchstens 50 KB.
  • Keine Bestätigungs-Mail: Die API versendet keine E-Mail an die anfragende Person. Die Adresse gilt als nicht bestätigt (email_verified: false), source ist api. Bestätigung und Einwilligungshinweis sind Ihre Aufgabe.
  • Nicht angenommen: iban, bic, account_holder, credit_check_consent (400 field_not_allowed) sowie Status, Herkunft und Bestätigung.
  • Gehört die E-Mail-Adresse schon einer anderen Person, wird die Anfrage trotzdem angelegt und in der Verwaltung intern markiert; die Antwort verrät davon nichts. Eine Wiederholung mit demselben Schlüssel liefert 200 mit derselben Anfrage.

Zeitleiste eines Mieters

GET /v1/tenants/{id}/timeline (Scope timeline:read) zeigt die Ereignisse eines Mieters aus E-Mail-Verlauf, Mahnungen, Zahlungen, Dokumenten und Vertrag in einer Liste, neueste zuerst. Der Ersteller des Tokens braucht für den Scope das Recht Mieter (Lesen).

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). Der Ersteller braucht dafür auch die Rechte des jeweiligen Scopes. timeline:read allein liefert 200 mit leerer Liste und sources: [], keinen Fehler. Das Feld sources nennt die Quellen, die für Ihr Token enthalten sind. Die Zusatz-Scopes emails:read_body, payments:read_counterparty und documents:download gelten hier nicht: Die Zeitleiste enthält nur Kurzangaben (Zeitpunkt, Typ, Objekt-Kennung, Bezeichnung, Status), keine Volltexte, Beträge, Gegenparteien, Dateipfade oder Download-Links. Details holen Sie über object_id aus dem Einzel-Endpunkt; ein Ereignis erscheint nur, wenn der Einzel-Endpunkt es für dasselbe Token ebenfalls zeigt.
curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline?period=last_3_months" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$kopf = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline?period=last_3_months" -Headers $kopf
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline?period=last_3_months', {
  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 weiterverarbeiten
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/tenants/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/timeline?period=last_3_months",
    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"])
  • Ereignisse und Zeitpunkt (occurred_at): email_sent (Versand), dunning_notice_issued (Ausstellungsdatum, Beginn des Tages in Europe/Berlin), payment_received (Buchungsdatum, Beginn des Tages in Europe/Berlin), document_added (Eingang in die Mieterakte), contract_created, contract_signed (jüngste Unterschrift des Mieters), contract_countersigned (jüngste Gegenzeichnung, erst wenn alle unterschriebenen Dokumente gegengezeichnet sind), termination_filed und supplementary_agreement_filed (Eingang einer Kündigung bzw. eines Nachtrags; je Vertrag die jüngsten 50 Kündigungen und 100 Nachträge, wie in /v1/contracts). Mietbeginn und Mietende sind keine Ereignisse.
  • Zahlungen erscheinen nur, wenn sie dem Mieter über eine gespeicherte Zuordnung zu seinem Vertrag zugeordnet sind, nicht über Name oder IBAN. Eine auf mehrere Mieter verteilte Zahlung steht in jeder beteiligten Zeitleiste.
  • Felder: id (<Typ>:<Zeilen-Id>, eindeutig, zum Entdoppeln), source, type, object_id (bei Kündigung und Nachtrag der Vertrag), title (E-Mail-Betreff, Dateiname, Vertragsnummer; sonst null) und status (Mailstatus, Mahnstufe level_<n>, Zuordnungsstatus der Zahlung, Dokumentkategorie, Kündigungs- oder Nachtragsstatus; sonst null).
  • Zeitfilter auf occurred_at wie bei den übrigen Listen; ohne Angabe gelten die letzten 30 Tage, für die gesamte Geschichte period=all. Weitere Parameter gibt es nicht, auch keine CSV-/Excel-Ausgabe.
  • Blättern: Die Folgeseiten sind kein Schnappschuss. Ändert sich zwischen zwei Abrufen ein berechneter Zeitpunkt (etwa durch eine neue Unterschrift), kann ein Ereignis fehlen oder doppelt erscheinen; entdoppeln Sie über id und rufen Sie bei Bedarf neu ab.
  • Unbekannter oder fremder Mieter: 404 not_found.