← Zurück zum Blog

12.05.2026 · TUTORIAL · IMMOBILIEN-TECH · 13 MIN

OnOffice REST-API anbinden: Node.js-Tutorial mit HMAC-Signatur

OnOffice ist neben Propstack und FlowFact das dritte große CRM im DACH-Maklermarkt. Wer Software an OnOffice anbindet, kämpft an einer anderen Front als bei IS24: nicht OAuth, sondern eine eigene HMAC-SHA256-Signatur pro Request, kombiniert mit einem konsolidierten Action-Endpoint, der alle Module bedient. Dieses Tutorial zeigt, wie man die Anbindung sauber baut — von der Auth-Signatur über Estate-CRUD bis zum Datei-Upload.

Inhalt
  1. Was kann die OnOffice-API?
  2. Zugang: Was du brauchst, bevor du startest
  3. HMAC-Signatur in Node.js
  4. Estates lesen, anlegen, aktualisieren
  5. Adressen-Modul und Verknüpfungen
  6. Datei-Upload (Exposé, Fotos, Grundrisse)
  7. Events und Webhooks
  8. Stolpersteine in der Praxis
  9. Wann lohnt sich eine eigene OnOffice-Integration?

Was kann die OnOffice-API?

OnOffice ist ein All-in-One-CRM mit eigener UI, Document-Builder, E-Mail-Modul, Kalender und Aufgaben-Verwaltung. Die REST-API deckt diese Module ab und stellt sie unter https://api.onoffice.de/api/latest/api.php bereit — ein einziger Endpoint, an den du JSON-POST-Requests mit unterschiedlichen actionid-Werten schickst (lesen, schreiben, löschen, etc.).

Die wichtigsten Module für Integrationen:

  • estate — Immobilien-Objekte mit allen Pflicht- und Marketing-Feldern.
  • address — Personen-Datensätze (Eigentümer, Interessenten, Mieter) inkl. DSGVO-Markierungen.
  • file — Anhänge (Bilder, Grundrisse, Exposés) als Base64-Payload.
  • relation — Verknüpfungen zwischen Estates und Adressen (z. B. „diese Person ist Eigentümer dieses Objekts").
  • task, calendar, agentslog — Workflow-Module für Makler-Tagesgeschäft.
  • fields — Schema-Discovery: welche Felder das CRM für den Account aktiviert hat.

Anders als bei Propstack gibt es keine echten Server-zu-Server-Webhooks — Änderungen erkennst du über das Action-Log oder durch regelmäßiges Diffing. Mehr dazu im Event-Abschnitt.

Zugang: Was du brauchst, bevor du startest

Du brauchst drei Werte, die OnOffice nach Antrag aushändigt:

  • API-Token — pro Kunde einmalig, identifiziert deinen Zugang.
  • API-Secret — geheim, wird für die HMAC-Signatur gebraucht.
  • API-User — der technische Benutzer, dessen Rechte du im OnOffice-Backend rollenbasiert konfigurierst.

Plane bei der Konfiguration im OnOffice-Backend genug Zeit ein: Welche Felder bekommt der API-User zu sehen, welche darf er schreiben, welche Gruppen gehören dazu? Falsch gesetzte Rechte führen zu Status 9-Fehlern, die einem die ersten Stunden Debugging kosten.

HMAC-Signatur in Node.js

OnOffice nutzt eine selbstgebaute Auth-Methode mit folgendem Schema pro Request: Du baust einen sortierten Parameter-Block, hashst zusätzlich den Payload (resourcetype+Parameter), zusammensetzt einen Auth-String mit Timestamp + Token + Resource + Resource-Hash und HMAC-SHA256-signierst das Ganze mit dem Secret. Das Ergebnis (Base64) wandert in den hmac-Parameter, plus eine eindeutige actionid und der frische timestamp.

// onoffice-client.js
import crypto from 'node:crypto';

const TOKEN = process.env.ONOFFICE_TOKEN;
const SECRET = process.env.ONOFFICE_SECRET;
const ENDPOINT = 'https://api.onoffice.de/api/latest/api.php';

function buildHmac(timestamp, resourceType, parameters) {
    // 1. Parameter sortiert serialisieren
    const sortedParams = Object.keys(parameters).sort()
        .map(k => `${k}=${JSON.stringify(parameters[k])}`)
        .join('&');

    const resourceHash = crypto.createHash('sha256')
        .update(`${resourceType}${sortedParams}`)
        .digest('hex');

    const message = [timestamp, TOKEN, resourceType, resourceHash].join('');
    return crypto.createHmac('sha256', SECRET).update(message).digest('base64');
}

export async function onofficeAction({ actionId, resourceType, parameters = {}, identifier = '' }) {
    const timestamp = Math.floor(Date.now() / 1000);
    const hmac = buildHmac(timestamp, resourceType, parameters);

    const body = {
        token: TOKEN,
        request: {
            actions: [{
                actionid: actionId,
                resourceid: '',
                identifier,
                resourcetype: resourceType,
                timestamp,
                hmac,
                hmac_version: '2',
                parameters,
            }],
        },
    };

    const res = await fetch(ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
    });

    const data = await res.json();
    const action = data.response.results[0];
    if (action.status.errorcode !== 0) {
        throw new Error(`OnOffice ${action.status.errorcode}: ${action.status.message}`);
    }
    return action.data.records;
}

Falle #1: Die HMAC-Version muss explizit als hmac_version: '2' gesetzt sein. Ohne das Flag rechnet OnOffice mit dem alten v1-Schema, das andere Hash-Reihenfolge nutzt — Folge: Status -10001 ohne hilfreiche Fehlermeldung.

Estates lesen, anlegen, aktualisieren

Estate-Records liest du mit der Action https://api.onoffice.de/api/stable/api.php/read (in unserer Helper-Funktion: actionId: 'urn:onoffice-de-ns:smart:2.5:smartml:action:read'). Du gibst die gewünschten Felder explizit an, um Antwort-Größe zu reduzieren:

// list-estates.js
import { onofficeAction } from './onoffice-client.js';

const estates = await onofficeAction({
    actionId: 'urn:onoffice-de-ns:smart:2.5:smartml:action:read',
    resourceType: 'estate',
    parameters: {
        data: ['Id', 'kaufpreis', 'plz', 'ort', 'objekttitel', 'objektnr_extern', 'vermarktungsart', 'objektart'],
        filter: {
            status: [{ op: '=', val: 1 }],   // Status 1 = aktiv
            vermarktungsart: [{ op: '=', val: 'kauf' }],
        },
        listlimit: 200,
        listoffset: 0,
        sortby: { erstellt_am: 'DESC' },
    },
});

console.log(`Gefunden: ${estates.length} aktive Verkaufs-Objekte`);

Schreiben mit create: Du übergibst die Felder direkt im parameters.data-Block. Eine eindeutige externe Referenz (objektnr_extern) als Idempotenz-Anker einbauen, damit du Doppelimporte vermeidest.

// create-estate.js
const created = await onofficeAction({
    actionId: 'urn:onoffice-de-ns:smart:2.5:smartml:action:create',
    resourceType: 'estate',
    parameters: {
        data: {
            objekttitel: 'Sanierte 3-Zi-Whg Eppendorf',
            objektnr_extern: 'EXT-2026-1042',
            vermarktungsart: 'kauf',
            objektart: 'wohnung',
            kaufpreis: 645000,
            anzahl_zimmer: 3,
            wohnflaeche: 78.5,
            plz: '20249',
            ort: 'Hamburg',
            objektzustand: 'gepflegt',
            energieausweistyp: 'verbrauch',
            energieverbrauchskennwert: 89,
            energieklasse: 'C',
            // ... weitere Pflichtfelder je nach Vermarktungsart
        },
    },
});

console.log('Created Estate ID:', created[0].id);

Adressen-Modul und Verknüpfungen

Eigentümer, Interessenten und Mieter liegen im separaten address-Modul. Eine Adresse mit einer Immobilie verknüpfst du über relation:

// link-owner.js
const owner = await onofficeAction({
    actionId: 'urn:onoffice-de-ns:smart:2.5:smartml:action:create',
    resourceType: 'address',
    parameters: {
        data: {
            Vorname: 'Anna',
            Name: 'Schmidt',
            Email: 'a.schmidt@example.com',
            Telefon1: '+49 40 123456',
            adresstyp: 1, // 1 = Privatperson
            artData: 'eigentuemer',
            dsgvostatus: 'erklaerung_unterschrieben',
        },
    },
});

await onofficeAction({
    actionId: 'urn:onoffice-de-ns:smart:2.5:smartml:action:create',
    resourceType: 'relation',
    parameters: {
        relationtype: 'urn:onoffice-de-ns:smart:2.5:relationtypes:estate:address:owner',
        parentid: estateId,
        childid: owner[0].id,
    },
});

Achtung beim Feld dsgvostatus: Wenn du Adressen ohne explizite Einwilligung einspielst, riskiert dein Kunde eine Abmahnung. Wir setzen den Status nur, wenn wir ein verifiziertes Opt-In aus dem Quellsystem haben — sonst bleibt das Feld leer und der Makler muss manuell freigeben.

Datei-Upload (Exposé, Fotos, Grundrisse)

Datei-Uploads gehen als Base64-kodierter Inhalt im data.Content-Feld der file-Action. Das macht große Bilder sehr unhandlich — eine Datei von 5 MB wird zu einer ~7-MB-JSON-Payload. Plane Timeouts großzügig (60 s+) und parallelisiere maximal 2-3 Uploads.

// upload-file.js
import fs from 'node:fs/promises';

const buffer = await fs.readFile('./grundriss.pdf');
await onofficeAction({
    actionId: 'urn:onoffice-de-ns:smart:2.5:smartml:action:do',
    resourceType: 'uploadfile',
    parameters: {
        data: buffer.toString('base64'),
        module: 'estate',
        relatedRecordId: estateId,
        title: 'Grundriss EG',
        Art: 'Grundriss',
        file: 'grundriss.pdf',
    },
});

Reihenfolge der Bilder steuerst du mit dem Feld oposition in einem nachgelagerten Update — OnOffice nimmt den ersten Datei-Datensatz als Titelbild, wenn nichts anderes konfiguriert ist.

Events und Webhooks

OnOffice hat keine echten Push-Webhooks. Statt das anzuwarten, nutzen wir zwei Tricks für „Near-Realtime"-Sync:

  1. Action-Log-Polling: Das Modul agentslog protokolliert Änderungen pro User. Wir pollen es alle 30–60 Sekunden mit einem letzte_aenderung-Filter — das deckt 90 % der Use-Cases.
  2. Field-Diff: Bei strukturkritischen Migrationen halten wir einen schlanken Snapshot in einer eigenen Datenbank und vergleichen pro Polling-Tick. Aufwendiger, aber zuverlässiger für Pricing- und Adress-Änderungen, die teilweise nicht im Action-Log landen.

Wer eine Bridge nach extern (z. B. zu eigenen Portalen) bauen will, kann das Polling und die Adapter-Logik in einer separaten Worker-Architektur kapseln — den Aufbau haben wir im Detail in Propstack-Webhooks zu Portalen pushen beschrieben.

Stolpersteine in der Praxis

1. Felder, die im UI sichtbar sind, aber per API nicht

OnOffice-Lizenzen unterscheiden zwischen UI-aktivierten Feldern und API-freigeschalteten Feldern. Wenn ein Makler ein Custom-Feld eingerichtet hat, ist es nicht automatisch per API beschreibbar — das muss separat aktiviert werden. fields-Discovery vor dem ersten Import laufen lassen.

2. Multi-Lang-Felder

Bei mehrsprachigen Inseraten erwartet OnOffice die Sprachvariante als Suffix (z. B. objekttitel für DE, objekttitel_engl für EN). Felder ohne Suffix gehen ins Standard-Sprachfeld; falsches Suffix → silent ignore.

3. Status-Codes statt HTTP-Codes

Die API antwortet fast immer mit HTTP 200. Erfolg oder Fehler steht in response.results[].status.errorcode. Wer auf HTTP-Status prüft, sieht keine Fehler. Immer auf die Errorcodes prüfen.

4. Rate-Limit pro Kunde

OnOffice limitiert auf Kunden-Account-Ebene. Ein Token bekommt etwa 20 Requests pro Sekunde — bei größeren Beständen lohnt eine Job-Queue mit Throttle. Bei parallelen Sync-Jobs für mehrere Maklerbüros braucht jeder Mandant ein eigenes Token-Bucket.

5. Bulk-Read braucht Pagination

listlimit max. 500 pro Request. Bei großen Datenbeständen über listoffset paginieren — aber Vorsicht: zwischen zwei Seiten kann sich der Datenbestand ändern. Für deterministische Imports einen Snapshot-Mechanismus einbauen (z. B. Filter auf erstellt_am < SNAPSHOT_TIME).

Wann lohnt sich eine eigene OnOffice-Integration?

Wer als Maklerbüro nur Standard-Workflows abdecken will, kommt mit OnOffice-Bordmitteln (Portal-Anbindungen, Document-Builder, integriertes Marketing) weit. Eine eigene API-Integration lohnt sich, wenn:

  • du Daten aus mehreren Quellsystemen (Excel, Altbestand, anderes CRM) konsolidieren willst,
  • du eine Multi-Mandanten-SaaS für mehrere Maklerbüros baust,
  • du spezielle Reportings, BI-Pipelines oder externe Portale bedienst, die nicht über die Standard-Schnittstellen abgedeckt sind,
  • du eine Migration zu oder von OnOffice planst — typisch beim Wechsel von/zu Propstack oder FlowFact.

Wir bei DevNest bauen solche Integrationen für Maklerbüros aller Größen — von der Single-Office-Migration bis zur Bridge zwischen mehreren CRM-Mandanten. Fragen oder konkretes Projekt? Projekt anfragen.

Weitere Themen: ImmoScout24-API anbinden · Propstack-Webhooks zu Portalen pushen · Propstack-Entwicklung · CRM für Immobilien