• Virtual Marketer CRM Logo
    Virtual Marketer CRMDokumentation
  • Einführung
Erste Schritte
  • Quickstart
  • Kernkonzepte
KI verbinden
  • Custom Connector
  • CLI & Editoren
  • Ratenlimits
Integrationen
  • MCP
  • Webhooks
  • Leads von der Website
  • OpenAPI 3.1.0
  • N8N
  • WhatsApp-API
App-Leitfaden
  • Dashboard
  • Posteingang
  • Datensätze
  • Profil
  • Unternehmen
  • Lead-Versand
  • API-Keys
  • Filter-Syntax
Lizenzen & Sicherheit
  • Architektur & Sicherheit
  • Zurück
  1. Einführung
  2. WhatsApp-API

WhatsApp API

WhatsApp-Nachrichten aus den eigenen Systemen senden und lesen — Nummer verbinden, Account-Id finden, an eine Person oder eine Gruppe senden, sicher wiederholen, und ein vollständiges Google-Apps-Script-Beispiel.

Sobald eine WhatsApp-Nummer mit Ihrem Workspace verbunden ist, können Ihre eigenen Systeme darüber per REST senden: ein Formular, das eine Buchung bestätigt, ein Skript, das eine Bestellung nachfasst, ein Sheet, das eine Kundin erinnert. Diese Seite deckt den ganzen Weg ab — verbinden, die richtige Id finden, an eine Person oder eine Gruppe senden, sicher wiederholen und die geltenden Limits.

Eine Nummer verbinden

WhatsApp wird pro Person verbunden, unter Profil → Verbundene Konten: einen QR-Code mit dem Telefon scannen, dem die Nummer gehört. Jeder Nutzer verbindet seine eigene Nummer; eine verbundene Nummer wird nie zwischen Nutzern oder Workspaces geteilt, und ein erneutes Verbinden ändert nie, wem sie gehört.

Die eigene connectedAccountId finden

Jeder Aufruf unten braucht die Id des verbundenen WhatsApp-Kontos, von dem gesendet werden soll:

curl -H "x-api-key: $VM_CRM_API_KEY" \
     https://crm.virtual-marketer.de/api/v1/messaging/connected-accounts

Den Eintrag mit "provider": "whatsapp" suchen und dessen id lesen. Die Id merken — sie ändert sich nicht, solange die Nummer nicht getrennt und neu verbunden wird.

An eine Nummer senden

curl -X POST https://crm.virtual-marketer.de/api/v1/messaging/whatsapp/send \
     -H "x-api-key: $VM_CRM_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "connectedAccountId": "YOUR_CONNECTED_ACCOUNT_ID",
       "to": "+4915112345678",
       "text": "Ihre Bestellung ist unterwegs.",
       "idempotencyKey": "order-4821-shipped"
     }'

201 mit { "externalMessageId": "...", "sentAt": "...", "replayed": false, "outcome": "sent" }. to ist eine Telefonnummer in E.164-Form oder eine WhatsApp-Chat-Id aus dem Gruppen-Endpunkt unten. text ist optional, wenn mindestens eine Datei in attachments mitgeschickt wird (base64, filename + content_type + content, insgesamt 25 MB) — der Text wird dann zur Bildunterschrift der ersten Datei.

Die Nachricht landet im selben Moment im Posteingang, in dem sie versendet wird — im selben Thread, in dem auch eine Antwort der Kundin ankäme.

An eine Gruppe senden

Die Gruppen auflisten, in denen die verbundene Nummer Mitglied ist:

curl -H "x-api-key: $VM_CRM_API_KEY" \
     "https://crm.virtual-marketer.de/api/v1/messaging/whatsapp/groups?connectedAccountId=YOUR_CONNECTED_ACCOUNT_ID"

Liefert { "items": [{ "id": "...@g.us", "name": "...", "participantCount": 12 }, ...] }. Die id einer Gruppe als to im Sendeaufruf oben verwenden — das CRM legt keine Gruppen an, es sendet nur in solche, in denen die Nummer bereits Mitglied ist.

Sicher wiederholen: idempotencyKey

Jeder Sendeaufruf akzeptiert ein optionales idempotencyKey (1–128 Zeichen, frei wählbar — eine Bestell-Id, eine Buchungs-Id, irgendetwas, das für genau diese eine Nachricht eindeutig ist). Ein Aufruf mit demselben Schlüssel liefert das ursprüngliche Ergebnis erneut zurück, statt ein zweites Mal zu senden — ein Retry nach einem Timeout, einer abgebrochenen Verbindung oder einem Skript, das zweimal läuft, kann so keine Nachricht verdoppeln. Ohne Schlüssel ist jeder Aufruf ein neuer Sendevorgang.

Ein Schlüssel, der gerade noch in Bearbeitung ist, antwortet mit 409 Conflict — kurz warten und mit demselben Schlüssel erneut versuchen, statt unter demselben Schlüssel eine andere Nachricht zu senden. Ein Replay (der Schlüssel ist bereits abgeschlossen) antwortet mit 200 statt 201, mit derselben JSON-Form, aber nur den Kennungen — nie dem Nachrichtentext — plus "replayed": true.

outcome und was sent_unconfirmed bedeutet

Jede Antwort trägt ein Feld outcome:

  • "sent" — der Normalfall: die Nachricht ist raus und das CRM hat sie erfasst.
  • "sent_unconfirmed" — die Nachricht wurde an WhatsApp zugestellt, aber das CRM konnte sie anschließend nicht erfassen (zum Beispiel ein Datenbank-Hänger). Der Aufruf antwortet beim ersten Mal mit 502 und genau dieser Aussage. Nicht erneut senden — stattdessen mit demselben idempotencyKey erneut aufrufen; dieser Retry findet den erfassten Versuch und liefert ihn als Replay zurück (200, "outcome": "sent_unconfirmed"), statt eine zweite Nachricht zu senden.
  • "unknown" — das CRM konnte nicht feststellen, ob die Nachricht WhatsApp erreicht hat, bevor der Sendevorgang selbst fehlschlug (ein Timeout, eine abgebrochene Verbindung, ein 5xx vom WhatsApp-Transport) — oder die ursprüngliche Anfrage wurde mitten im Ablauf abgebrochen (zum Beispiel ein Prozess-Neustart), und es ist genug Zeit vergangen, dass das CRM den Überblick verloren hat. Der erste Fall antwortet ebenfalls beim ersten Mal mit 502 und der Aussage, dass der Ausgang unbekannt ist. In beiden Fällen gilt: nicht erneut senden — mit demselben idempotencyKey erneut aufrufen; dieser Retry liefert den erfassten Ausgang "unknown" als Replay zurück (200), statt eine zweite Nachricht zu senden. Das ist der einzige Fall, bei dem man anhalten und von Hand nachsehen sollte — den Thread im Posteingang prüfen, bevor unter einem neuen Schlüssel erneut gesendet wird.

Ratenlimits

Senden ist pro Nutzer begrenzt: 30 Nachrichten pro Minute, 300 pro Stunde. Ein Aufruf über dem Limit antwortet mit 429 Too Many Requests und einem Retry-After-Header — die Sekunden, bis die älteste in diesem Fenster gezählte Nachricht aus dem Fenster fällt (mindestens 1 Sekunde, höchstens die Fensterlänge). Das Fenster abwarten und dann mit demselben idempotencyKey erneut versuchen — genau dafür ist der Schlüssel da, wenn ein Versuch am Limit gescheitert ist. Kommt ein großer Retry-After-Wert zurück (beim Stunden-Limit bis zu einer Stunde), nicht darauf warten: nach einer begrenzten Wartezeit abbrechen — das Beispiel unten deckelt bei 6 Minuten — und den Fehlschlag melden statt endlos zu warten, mit demselben idempotencyKey für den nächsten Versuch.

(Das ist das Limit für Nachrichten, die Sie selbst auslösen. Das Lesen und Synchronisieren eines verbundenen Kontos hat ein eigenes, separates Budget — siehe Ratenlimits.)

Keine Kaltakquise

Der WhatsApp-Zugang hier läuft über einen inoffiziellen Transport (WAHA), nicht über die WhatsApp Business Platform. Nur für Konversationen verwenden, die die empfangende Person bereits begonnen oder erkennbar eingewilligt hat — eine Buchungsbestätigung, eine Support-Nachfrage, ein Bestellstatus. Wer jemanden anschreibt, der noch nie geschrieben hat, riskiert eine Sperrung der Nummer durch WhatsApp, und unaufgeforderte kommerzielle Nachrichten sind unabhängig vom Transport nach §7 UWG in Deutschland unzulässig. Diesen Endpunkt nicht für Kaltakquise-Flows verwenden.

Ein vollständiges Beispiel: Google Apps Script

Eine Bestellbestätigung aus einem Google Sheet, mit einer Retry-Schleife, die einen 429 abwartet und denselben idempotencyKey wiederverwendet, damit ein Retry nicht doppelt sendet:

const CRM_API_KEY = PropertiesService.getScriptProperties().getProperty("VM_CRM_API_KEY");
const CONNECTED_ACCOUNT_ID = "YOUR_CONNECTED_ACCOUNT_ID";

function sendWhatsAppConfirmation(orderId, phoneE164, messageText) {
  const url = "https://crm.virtual-marketer.de/api/v1/messaging/whatsapp/send";
  const payload = {
    connectedAccountId: CONNECTED_ACCOUNT_ID,
    to: phoneE164,
    text: messageText,
    idempotencyKey: "order-" + orderId,
  };
  const options = {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": CRM_API_KEY },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };

  const maxAttempts = 5;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = UrlFetchApp.fetch(url, options);
    const status = response.getResponseCode();

    if (status === 201 || status === 200) {
      const body = JSON.parse(response.getContentText());

      if (body.outcome === "unknown") {
        // Das CRM konnte nicht mehr feststellen, ob wirklich gesendet wurde. Nicht automatisch
        // erneut senden — protokollieren und den Thread von Hand prüfen, bevor erneut gesendet wird.
        Logger.log("WhatsApp-Sendestatus unbekannt für Bestellung " + orderId + " — Thread von Hand prüfen.");
        return body;
      }

      return body;
    }

    if (status === 409) {
      // Ein anderer Versuch mit demselben idempotencyKey läuft noch — warten und mit
      // DEMSELBEN Schlüssel erneut versuchen, nicht mit einem neuen, das würde genau
      // die Doppelversendung auslösen, die der Schlüssel verhindern soll.
      const retryAfter = Number(response.getHeaders()["Retry-After"] || 5);
      Utilities.sleep(Math.min(retryAfter, 60) * 1000);
      continue;
    }

    if (status === 429) {
      const retryAfter = Number(response.getHeaders()["Retry-After"] || 5);
      const maxAutoWaitSeconds = 360;

      if (retryAfter > maxAutoWaitSeconds) {
        throw new Error(
          "WhatsApp-Versand für Bestellung " + orderId + " ist " + retryAfter +
            "s ratenlimitiert, länger als dieses Skript automatisch wartet. Später mit DEMSELBEN idempotencyKey erneut versuchen.",
        );
      }

      Utilities.sleep(retryAfter * 1000);
      continue;
    }

    if (status === 502) {
      // sent_unconfirmed: WhatsApp hat die Nachricht bereits, das CRM konnte sie nur nicht
      // erfassen. Mit DEMSELBEN Schlüssel erneut versuchen, um das erfasste Ergebnis zu
      // holen — nie erneut senden.
      Utilities.sleep(2000 * attempt);
      continue;
    }

    if (status >= 500) {
      Utilities.sleep(2000 * attempt);
      continue;
    }

    throw new Error("WhatsApp send failed (" + status + "): " + response.getContentText());
  }

  throw new Error("WhatsApp send did not succeed after " + maxAttempts + " attempts for order " + orderId);
}

function onOrderShipped() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const row = sheet.getActiveCell().getRow();
  const orderId = sheet.getRange(row, 1).getValue();
  const phone = sheet.getRange(row, 2).getValue();

  sendWhatsAppConfirmation(orderId, phone, "Ihre Bestellung " + orderId + " ist unterwegs.");
}

Den Schlüssel unter Project Settings → Script Properties ablegen, nie fest im Skript — dieselbe Regel wie für jedes andere serverseitige Secret.

Weiter

  • API-Schlüssel: erstellen, rotieren, widerrufen.
  • Ratenlimits: das separate Budget für Lesen und Synchronisieren eines Kontos.
  • OpenAPI: die vollständige REST-Referenz mit jedem Feld.
  • Webhooks: über eine eingehende Antwort benachrichtigt werden, statt zu pollen.
Eine Nummer verbinden
Die eigene connectedAccountId finden
An eine Nummer senden
An eine Gruppe senden
Sicher wiederholen: idempotencyKey
outcome und was sent_unconfirmed bedeutet
Ratenlimits
Keine Kaltakquise
Ein vollständiges Beispiel: Google Apps Script
Weiter