← Zurück zum Blog

11.05.2026 · TUTORIAL · IMMOBILIEN-TECH · 12 MIN

ImmoScout24 API anbinden: Node.js-Tutorial mit OAuth 1.0a

Die ImmoScout24-REST-API ist mächtig — und gleichzeitig der API-Zugang, an dem die meisten Entwicklerinnen und Entwickler die ersten zwei Tage verlieren. Das liegt nicht an der API selbst, sondern an einem Detail: ImmoScout24 setzt bis heute auf OAuth 1.0a. Dieses Tutorial zeigt Schritt für Schritt, wie man die Anbindung sauber mit Node.js baut — vom Zugang über die Signatur, das CRUD eines Inserats bis zum Foto-Upload und zur Statusabfrage.

Inhalt
  1. Was kann die ImmoScout24-API?
  2. Zugang beantragen: Welche Rolle brauchst du?
  3. OAuth 1.0a in Node.js (Live-Code)
  4. Inserate lesen, anlegen, aktualisieren
  5. Bilder und Anhänge hochladen
  6. Status- und Veröffentlichungs-Updates
  7. Typische Stolpersteine
  8. Wann sich die direkte Anbindung lohnt

Was kann die ImmoScout24-API?

Die Plattform stellt zwei API-Welten bereit. Im Tutorial geht es um die ältere, aber wichtigere Importexport-API (auch Restapi genannt), über die Makler-Software, CRMs und eigene Anwendungen Inserate auf IS24 publizieren. Der Endpoint hat das Basis-Schema https://rest.immobilienscout24.de/restapi/api/offer/v1.0 und exponiert Ressourcen wie /realestate, /realestate/{id}/attachment und /publish.

Daneben existiert die Search-API für Lese-Zugriffe auf das öffentliche Inventar — relevant für Vergleichs-, Bewertungs- oder Analyse-Tools. Beide laufen über denselben OAuth-1.0a-Mechanismus, unterscheiden sich aber in Berechtigungsmodell und Rate-Limit.

Die wichtigsten Use-Cases, für die wir die Anbindung bauen:

  • CRM-Sync — Listings aus Propstack, OnOffice, FlowFact oder einem eigenen System automatisch zu IS24 spiegeln.
  • Bulk-Onboarding — initialer Import eines Bestands beim Wechsel zwischen CRMs, idempotent mit eigenen Referenzen (externalId).
  • Veröffentlichungs-Workflows — Inserate je nach Lagerstatus, Kampagnen-Logik oder Zahlung freischalten und wieder zurückziehen.
  • Reporting — Performance-Daten (Aufrufe, Anfragen) zurück ins eigene CRM holen.

Zugang beantragen: Welche Rolle brauchst du?

Bevor eine einzige Zeile Code läuft, brauchst du vier Geheimnisse: consumerKey, consumerSecret, accessToken und tokenSecret. Das Consumer-Paar bekommst du nach Antrag im ImmoScout24 Partner Portal; das Token-Paar erzeugst du anschließend pro Kunde im Rahmen eines klassischen OAuth-1.0a-Drei-Schritt-Flows (request token → user authorization → access token).

Praxis-Hinweis: Für die meisten B2B-Anbindungen reicht der Rollen-Typ „Customer-Side Application". Wenn du eine Multi-Tenant-SaaS für viele Maklerbüros betreibst, brauchst du zusätzlich die „Partner-Side"-Freischaltung — damit kannst du im Namen verschiedener Kunden-Accounts publishen, ohne dass jeder Kunde sein eigenes Consumer-Pair sieht.

Faustregel: Wenn dein Projekt mehr als zwei Maklerbüros bedient, lohnt der Aufwand für den Partner-Status spätestens ab Monat 2. Vorher kommst du mit einzelnen Customer-Tokens schneller voran.

OAuth 1.0a in Node.js — Live-Code

OAuth 1.0a verlangt eine kryptographische Signatur pro Request. Wir signieren mit HMAC-SHA1 über einen normalisierten Parameter-String. Das Paket oauth-1.0a erspart einem die meisten Fallstricke; in Kombination mit node-fetch oder dem nativen fetch ab Node 18 entsteht ein schlanker Client:

// is24-client.js — minimaler IS24-Client
import OAuth from 'oauth-1.0a';
import crypto from 'node:crypto';

const oauth = OAuth({
    consumer: {
        key: process.env.IS24_CONSUMER_KEY,
        secret: process.env.IS24_CONSUMER_SECRET,
    },
    signature_method: 'HMAC-SHA1',
    hash_function(base, key) {
        return crypto.createHmac('sha1', key).update(base).digest('base64');
    },
});

const token = {
    key: process.env.IS24_ACCESS_TOKEN,
    secret: process.env.IS24_TOKEN_SECRET,
};

const BASE = 'https://rest.immobilienscout24.de/restapi/api/offer/v1.0';

export async function is24Request(path, { method = 'GET', body, contentType = 'application/json' } = {}) {
    const url = `${BASE}${path}`;
    const requestData = { url, method };

    const authHeader = oauth.toHeader(oauth.authorize(requestData, token));

    const res = await fetch(url, {
        method,
        headers: {
            ...authHeader,
            'Accept': 'application/json',
            ...(body ? { 'Content-Type': contentType } : {}),
        },
        body: body && contentType === 'application/json' ? JSON.stringify(body) : body,
    });

    if (!res.ok) {
        const text = await res.text();
        throw new Error(`IS24 ${res.status}: ${text}`);
    }
    return res.status === 204 ? null : res.json();
}

Das Skelett ist bewusst klein gehalten — kein Retry, kein Throttling. Beides bauen wir später als Wrapper drumherum. Wichtig ist nur: Die Signatur muss genau die finale URL abdecken, inklusive Query-Parameter. Wer hier mit URL-Builder-Bibliotheken jongliert, riskiert 401 Invalid signature-Fehler, weil die Encoding-Reihenfolge nicht stimmt.

Inserate lesen, anlegen, aktualisieren

Im IS24-Datenmodell gibt es eine Vielzahl von Realestate-Typen — Wohnung kaufen, Haus mieten, Anlageobjekt, Gastronomie usw. Jeder Typ hat ein eigenes Schema mit Pflicht- und Kann-Feldern. Wir bauen ein einfaches Beispiel mit einer Eigentumswohnung zum Kauf (ApartmentBuy):

// create-listing.js
import { is24Request } from './is24-client.js';

const apartment = {
    'realEstate.apartmentBuy': {
        title: 'Lichtdurchflutete 3-Zimmer-Wohnung in Eppendorf',
        externalId: 'EXT-2026-1042',
        address: {
            street: 'Kümmellstraße',
            houseNumber: '12',
            postcode: '20249',
            city: 'Hamburg',
            internationalCountryRegion: { country: 'GERMANY', region: 'Hamburg' },
        },
        numberOfRooms: 3,
        livingSpace: 78.5,
        baseRent: null,
        price: { value: 645000.0, currency: 'EUR', marketingType: 'PURCHASE', priceIntervalType: 'ONE_TIME_CHARGE' },
        condition: 'WELL_KEPT',
        constructionYear: 1928,
        energyCertificate: {
            energyCertificateAvailability: 'AVAILABLE',
            energyConsumptionContainsWarmWater: false,
            buildingEnergyRatingType: 'ENEV_2014',
            energyPerformanceCertificate: true,
            thermalCharacteristic: 89.0,
            energyEfficiencyClass: 'C',
        },
        descriptionNote: 'Saniert 2019, eigener Balkon, kein Provisionsteilung.',
        showAddress: true,
    },
};

const created = await is24Request('/user/me/realestate', { method: 'POST', body: apartment });
console.log('Created realestate ID:', created['realEstate.apartmentBuy'].id);

Drei Dinge, die in der Doku gerne untergehen:

  • externalId ist dein Anker für Idempotenz. Setze ihn auf einen stabilen Wert aus deinem CRM (z. B. Propstack-Property-ID), damit du beim erneuten Lauf das passende Realestate findest, ohne IS24-IDs lokal zu pflegen.
  • Das Wurzel-Objekt ist genau ein Property mit dem Realestate-Typ als Key. Schiebst du mehrere Typen oder schreibst du realEstate statt realEstate.apartmentBuy, antwortet IS24 mit 415 Unsupported Media Type.
  • Update läuft per PUT /user/me/realestate/ext-{externalId} — das ist der Trick, mit dem du ohne IS24-ID arbeiten kannst, solange dein externalId eindeutig ist.

Bilder und Anhänge hochladen

Bilder gehen nicht im JSON-Payload, sondern als separater Multipart-Request gegen /realestate/{id}/attachment. Reihenfolge: Erst das Realestate-Objekt anlegen, dann pro Bild ein Multipart-POST. Beachte den titlePicture-Flag — nur eines pro Listing.

// upload-image.js
import fs from 'node:fs';
import FormData from 'form-data';

export async function uploadAttachment(realestateId, filePath, { title, titlePicture = false } = {}) {
    const form = new FormData();
    const metadata = {
        attachment: { '@xsi.type': 'common:Picture', title, titlePicture },
    };
    form.append('metadata', JSON.stringify(metadata), { contentType: 'application/json' });
    form.append('attachment', fs.createReadStream(filePath));

    const path = `/user/me/realestate/${realestateId}/attachment`;
    return is24Request(path, {
        method: 'POST',
        body: form,
        contentType: `multipart/form-data; boundary=${form.getBoundary()}`,
    });
}

IS24 akzeptiert JPEG, PNG und PDF — Bilder werden serverseitig auf maximal 1.500 × 1.000 px verkleinert. Wer das Hochladen parallelisiert, läuft schnell ins Rate-Limit; sequenziell oder mit einem kleinen Konkurrenz-Pool (max. drei gleichzeitig) ist die sichere Variante.

Status- und Veröffentlichungs-Updates

Ein Inserat existiert in zwei Welten: als Realestate-Objekt und als Veröffentlichung. Das Anlegen eines Realestate alleine ist noch nicht live. Du musst es separat publishen — und zwar pro Channel:

// publish.js
const publishRequest = {
    'publish.publishObject': {
        realEstate: { '@id': realestateId },
        publishChannel: { '@id': 10000 /* IS24-Hauptkanal */ },
    },
};

await is24Request('/user/me/publish', { method: 'POST', body: publishRequest });

Für das De-Listing schickst du einen DELETE-Request gegen denselben Endpoint mit der publishObject-ID. Wichtig: Inserate, die noch eine aktive Vermarktungslaufzeit haben, kannst du nicht hart löschen — nur depublishen. Dauerhafte Löschung erfolgt frühestens nach Ablauf der Laufzeit.

Statusabfragen laufen über GET /publish?realestate={id}. Die Plattform bietet keine Realtime-Webhooks für Veröffentlichungsstatus, daher pollen wir alle paar Minuten. Bei großen Beständen lohnt eine zentrale Job-Queue (BullMQ, AWS SQS), die Updates priorisiert.

Typische Stolpersteine — und ihre Lösung

Aus über drei Jahren Praxis mit IS24-Anbindungen die Klassiker, die jedem Team mindestens einmal passieren:

1. 401 Invalid signature trotz korrekter Keys

Fast immer ein Encoding-Problem in der Signatur-Basis-String. Prüfe, ob du den Body bei POST aus der Signatur ausschließt (richtig) und nur Query-Parameter sowie OAuth-Parameter einbeziehst.

2. 415 Unsupported Media Type beim Realestate-POST

Du hast den Realestate-Typ-Key falsch gesetzt oder im JSON-Wurzel-Element fehlt der Namensraum-Prefix. Validiere das Payload-Schema gegen die offizielle XSD — IS24 ist hier penibel.

3. Idempotente Updates schlagen fehl

Häufige Ursache: externalId mit Sonderzeichen oder Whitespace. Verwende nur [A-Za-z0-9_-], maximal 60 Zeichen. Wir hashen die CRM-ID zur Sicherheit nochmal vor dem Senden.

4. Bilder-Upload bricht ab

Achte auf den korrekten boundary-String in der Content-Type-Header-Zeile. Bibliotheken wie form-data setzen den, manche HTTP-Clients überschreiben den Header bei nachträglichem Setzen.

5. Rate-Limit-Hits unter Last

IS24 limitiert pro Consumer auf etwa 60 Requests pro Minute. Implementiere ein Token-Bucket-Throttle in deinem Client; sonst bekommst du intermittierende 503-Responses, die wie zufällige Fehler aussehen.

Wann sich die direkte Anbindung lohnt

Wenn du weniger als ein paar Dutzend Inserate verwaltest und ein CRM nutzt, das die Schnittstelle bereits eingebaut hat (Propstack, OnOffice, FlowFact, Justimmo), nimmst du das CRM. Eigene Anbindung lohnt sich, wenn:

  • du mehrere Maklerbüros bedienst und einen einheitlichen Workflow brauchst,
  • dein Inseratsfluss eigene Geschäftslogik trägt (z. B. Zahlungs-Gate, automatische Reaktivierung, Multi-Portal-Spiegelung),
  • du Reporting-Daten in eine BI-Pipeline ziehen willst,
  • du eine SaaS für Makler baust und IS24 als einer von mehreren Veröffentlichungskanälen dient.

Bei DevNest bauen wir solche Anbindungen seit Jahren — als Stand-Alone-Bridge zwischen CRM und Portal oder als Teil einer kompletten Propstack-Integration. Wenn du an einer ähnlichen Lösung arbeitest, schreib uns: Projekt starten.

Weitere Themen: ImmoScout-Portal-Anbindung · Propstack-Entwicklung · API-Entwicklung Hamburg · Backend-API-Entwicklung: REST vs. GraphQL