EN

CRM & Software anbinden

Verbinde meiti mit deinem System – an einem Nachmittag.

meiti nimmt Anrufe an, beantwortet WhatsApp, Chats und E-Mails – für dein Team. Mit drei Bausteinen wird daraus ein Teil deines CRM, ERP oder Shops: Dein System gibt meiti Kontext, meiti schreibt Ergebnisse zurück, und dein Team springt mit einem Klick hin und her.

  • Der Assistent begrüßt Stammkunden mit Namen und kennt ihren Stand.
  • Jede Gesprächsnotiz landet automatisch in deiner Kundenakte.
  • Dein Team öffnet den Vorgang mit einem Klick aus dem CRM – oder ruft direkt an.

Das erledigst du in der App – den Rest macht deine IT

  1. API-Schlüssel: Einstellungen → Einrichtung & Betrieb → Verbundene Systeme → API-Zugang → Schlüssel erstellen.
  2. CRM-Webhook: Einstellungen → Einrichtung & Betrieb → Verbundene Systeme → CRM & Fachsoftware → Webhook – URL und Token trägt deine IT ein.
  3. Gib Schlüssel und Token sicher weiter – nicht per normaler E-Mail, sondern z. B. über einen Passwort-Manager.

Voraussetzung: Deine Software kann Webhooks empfangen oder eigene Links/Buttons anzeigen – und jemand kann ein kleines Programm dafür schreiben.

Die API ist im Beta-Stadium: Sie läuft stabil, Details können sich vor der finalen Version noch ändern – jede Änderung steht im Changelog.

Fragen oder unsicher, ob deine Software passt? Schreib uns per WhatsApp oder ruf an: +49 89 41434078 – wir sprechen auch direkt mit deiner IT.

So greift eine gute meiti-Integration ineinander

Dein System bleibt das führende System für Kunden, Akten und Rechnungen. meiti ist die Stelle, an der Gespräche passieren. Eine gute Integration schließt den Kreis in vier Schritten:

  • Gespräch
  • Webhook von meiti
  • Antwort deines Systems
  • API-Call
Kunde Telefon · WhatsApp · Chat
meiti Assistent & Postfach
Dein System CRM · ERP · Kalender
  1. Anruf von +49 170 1234567 Ingrid Weber ruft an. meiti nimmt ab.
  2. POST · IncomingCallLookup CRM-Webhook: meiti fragt dein System, wer anruft – noch während es klingelt.
  3. 200 · firstName, crmAiInfo Dein System antwortet in unter 2 Sekunden mit Namen und einem kurzen Hinweis.
  4. „Hallo Frau Weber! Geht es um Ihre Heizung?“ Der Assistent kennt die Kundin und führt das Gespräch.
  5. POST · FinishedCall Nach dem Gespräch kommt die Zusammenfassung zu deinem System.
  6. 200 · crmProjectId: "AZ-2024/117" Dein System legt den Vorgang an und gibt seine Nummer zurück.
  7. POST /v1/appointments API: Dein Kalender plant den Termin, meiti lädt die Kundin ein.
  8. Termin Di., 9:00 – passt das? Die Einladung kommt per WhatsApp, mit Button zum Bestätigen.
  9. ✓ Bestätigt Frau Weber tippt auf „Bestätigen“.
  10. appointment.confirmed Signierter API-Webhook: Dein Kalender erfährt es sofort.
Ein Anruf, von der ersten Sekunde bis zum bestätigten Termin – und überall, wo dein System dabei ist.
  1. 1

    Kontext rein

    Ein Anruf oder Chat beginnt. meiti schickt IncomingCallLookup bzw. NewConversation, dein System antwortet mit Namen und einem kurzen Hinweis in crmAiInfo.

  2. 2

    meiti führt das Gespräch

    Der Assistent kennt den Kunden, beantwortet Fragen, nimmt Anliegen auf, leitet bei Bedarf an dein Team weiter.

  3. 3

    Ergebnis zurück

    Nach dem Gespräch kommt FinishedCall bzw. ConversationPaused mit Zusammenfassung. Dein System legt den Vorgang an und gibt seine Nummer als crmProjectId zurück.

  4. 4

    Weiterarbeiten – in beide Richtungen

    Dein Team öffnet den Vorgang per Deep Link mit deiner eigenen Nummer. Dein System schickt per API Nachrichten und Termine – sie landen im selben Gespräch.

IDs einmal verknüpfen

Gib meiti deine Nummern (crmContactId, crmProjectId) und merk dir meitis (meitiContactId, meitiProjectId). Danach findet jede Seite die andere.

Kurz und schnell antworten

crmAiInfo liest der Assistent live – zwei, drei Sätze reichen. Antworte innerhalb von 2 Sekunden direkt; Längeres per Callback.

Wiederholungen aushalten

Webhooks können doppelt kommen, Netzwerke brechen ab. Verarbeite idempotent und sende mit Idempotency-Key.

Einwilligung respektieren

Sag meiti mit purpose, was eine Nachricht ist. Werbung geht nur mit Marketing-Einwilligung raus.

Nur sagen, was jeder hören darf

Rufnummern lassen sich fälschen. Gib in crmAiInfo nichts, was der Assistent einem Fremden nicht sagen dürfte – etwa Beträge oder Gesundheitsdaten.

CRM-Webhook oder API-Webhooks? Beide stehen mit Vergleich, Feldern und Signatur auf der Seite Webhooks.

Was in meiti passiert

Wer bekommt was – Schritt für Schritt

Wähl einen Ablauf und klick dich durch: Links siehst du, was dein System sendet oder bekommt, in der Mitte, was dein Team in der meiti App sieht, rechts, was beim Kunden ankommt.

Die Texte zeigen, was ein Postfach mit eigener WhatsApp-Nummer sendet (Sie-Form). Über die gemeinsame meiti-Nummer kommen Vorlagen in Du-Form unter dem Absender „meiti“.

Dein System
Team in der meiti App
Kunde

Alle Abläufe als Text

Anruf kommt rein

CRM-Webhook: Dein System sagt meiti, wer anruft – und bekommt danach die Zusammenfassung zurück.

  1. Sofort – Frau Weber ruft an. Das Team bekommt einen Push. Ruft sie zu einem offenen Vorgang erneut an, steht „Eingehender Anruf“ im Verlauf.
    • Team: Anruf · +49 170 1234567
    • Kunde: 📞 ruft Heizung Weber an …
  2. Sofort, max. 2 Sekunden – meiti fragt dein System: Wer ist das?. meiti wartet bis zu 2 Sekunden auf deine Antwort, bevor der Assistent begrüßt. Was in dieser Zeit mit 200 zurückkommt, kennt er sofort.
    • Event an dich: POST /meiti/webhook · IncomingCallLookup
    • Antwort: 200 OK
    • Team: Webhook (IncomingCallLookup) wird gesendet...
    • Team: Webhook (IncomingCallLookup) erfolgreich. Daten wurden aktualisiert.
  3. Während des Gesprächs – Der Assistent übernimmt. Mit dem Kontext, den dein System geliefert hat. Wer im Team Live-Mitschrift-Pushes eingeschaltet hat, liest jede Äußerung des Anrufers mit.
    • Team: „Ich wollte wegen der Heizungswartung fragen …“
    • Kunde erhält: Guten Tag, hier ist Heizung Weber. Wie kann ich Ihnen helfen?
    • Kunde schreibt: Ich wollte wegen der Heizungswartung fragen …
  4. Nach dem Gespräch, ca. 1 Minute später – Zusammenfassung zurück in dein System. Mit Aufnahme kommt FinishedCall erst nach der Transkription. Antwortest du mit crmProjectId, trägt der Vorgang ab jetzt deine Aktennummer.
    • Event an dich: POST /meiti/webhook · FinishedCall
    • Antwort: 200 OK
    • Team: Webhook (FinishedCall) erfolgreich. Daten wurden aktualisiert.
  5. Jederzeit danach – Aus dem CRM direkt in den Vorgang. Ein Klick auf den Link in deinem CRM öffnet den Vorgang in der meiti App – auch in einem anderen Postfach (nach Rückfrage).
    • Anfrage: https://m.meiti.ai/open/project?crmId=AZ-2024%2F117
    • Team: Vorgang „Heizungswartung“ geöffnet

Termin zur Bestätigung

API: POST /v1/appointments mit confirmed: false – meiti lädt ein, der Kunde bestätigt im Chat.

  1. Sofort – Dein System legt den Termin an. Ist das WhatsApp-Fenster zu, geht die Einladung als freigegebene Vorlage raus. Die Termin-Events gehen an deine Webhooks.
    • Anfrage: POST /v1/appointments
    • Antwort: 201 Created
    • Event an dich: appointment.created · AppointmentCreated
    • Team: Neuer Termin (#4K7Q) am 20.10.26 10:00
    • Kunde erhält: Sie haben eine Termineinladung 📆 (#4K7Q) von Heizung Weber erhalten! [Jetzt ansehen]
  2. Kunde tippt „Jetzt ansehen“ – Die Einladung mit allen Details. Bestätigt wird im Chat – über die Buttons.
    • Kunde: Kunde tippt „Jetzt ansehen“
    • Kunde erhält: Termineinladung (#4K7Q) mit Heizung Weber Heizungswartung 📅 Di. 20.10.26 10:00 - 11:00 [Annehmen ✅ | Ablehnen ❌]
  3. Kunde tippt „Annehmen“ – Bestätigt – alle erfahren es. Das Team bekommt einen Push, dein System appointment.confirmed bzw. AppointmentConfirmed. Lehnt der Kunde ab, fragt meiti noch einmal nach – erst danach kommt appointment.cancelled.
    • Event an dich: appointment.confirmed · AppointmentConfirmed
    • Team: Der Kunde hat den Termin (#4K7Q) bestätigt
    • Team: Ingrid Weber · hat den Termin bestätigt.
    • Kunde: Kunde tippt „Annehmen ✅“
    • Kunde erhält: Ihr Termin (#4K7Q) ist bestätigt ✅ Di. 20.10.26 10:00 - 11:00 [Termin verwalten 📆 | Termin speichern 📩]
  4. 24 Stunden vorher – Erinnerung an den Kunden. Erinnerungen gibt es mit eigener WhatsApp-Nummer, sobald beide Seiten bestätigt haben. Standard: 24 Stunden und 30 Minuten vorher, im Kalender einstellbar.
    • Event an dich: appointment.reminder_sent · AppointmentReminderSent
    • Kunde erhält: Erinnerung: Der Termin (#4K7Q) ist am Di. 20.10.26 10:00. Bis bald! [Termin verwalten]
  5. 30 Minuten vorher – Zweite Erinnerung.
    • Event an dich: appointment.reminder_sent · AppointmentReminderSent
    • Kunde erhält: Erinnerung: Der Termin (#4K7Q) ist am Di. 20.10.26 10:00. Bis bald! [Termin verwalten]

Termin direkt vereinbart

API: POST /v1/appointments mit confirmed: true – zum Beispiel, wenn der Termin am Telefon schon feststand.

  1. Sofort – Bestätigung statt Einladung. Der Termin ist sofort von beiden Seiten bestätigt. Deshalb kommt nur appointment.created – kein appointment.confirmed.
    • Anfrage: POST /v1/appointments
    • Antwort: 201 Created
    • Event an dich: appointment.created · AppointmentCreated
    • Team: Neuer Termin (#4K7Q) am 20.10.26 10:00
    • Kunde erhält: Ihr Termin (#4K7Q) ist bestätigt ✅ Di. 20.10.26 10:00 - 11:00 [Termin verwalten 📆 | Termin speichern 📩]
  2. Kunde tippt „Termin speichern“ – Ab in den Kalender. Der Kunde bekommt einen Link zur Terminseite und eine .ics-Datei.
    • Kunde: Kunde tippt „Termin speichern 📩“
    • Kunde erhält: Hier können Sie den Termin (#4K7Q) in Ihrem Kalender speichern. 📎 Termin.ics
  3. 24 Stunden vorher – Erinnerung. Wie oben: mit eigener WhatsApp-Nummer, Standard 24 Stunden und 30 Minuten vorher.
    • Event an dich: appointment.reminder_sent · AppointmentReminderSent
    • Kunde erhält: Erinnerung: Der Termin (#4K7Q) ist am Di. 20.10.26 10:00. Bis bald! [Termin verwalten]

Termin verschieben & absagen

API: PATCH und DELETE /v1/appointments/{id} – was der Kunde dabei bekommt.

  1. Sofort – Neuer Zeitpunkt. Ohne confirmed: true muss der Kunde neu bestätigen – seine Bestätigung wird zurückgesetzt. Mit confirmed: true bekommt er stattdessen „Ihr Termin wurde verschoben 📆 Bisher … Neu …“.
    • Anfrage: PATCH /v1/appointments/5120
    • Event an dich: appointment.rescheduled · AppointmentRescheduled
    • Team: Termin (#4K7Q) zum 22.10.26 09:00 verschoben
    • Kunde erhält: Achtung, wir mussten den 📆 Termin (#4K7Q) leider verlegen. Bitte buchen Sie ihn jetzt neu! [Jetzt ansehen]
  2. Später – Absage durch dein System. Gibst du message mit, folgt sie als eigene Nachricht direkt nach der Absage.
    • Anfrage: DELETE /v1/appointments/5120
    • Event an dich: appointment.cancelled · AppointmentCancelled
    • Team: Termin (#4K7Q) abgesagt
    • Kunde erhält: ACHTUNG, Ihr Termin (#4K7Q) wurde abgesagt.
    • Kunde erhält: Unser Techniker ist leider krank. Wir melden uns mit einem neuen Termin.

Nachricht senden

API: POST /v1/messages – hier mit geschlossenem WhatsApp-Fenster, dem häufigsten Fall bei der ersten Nachricht.

  1. Sofort – Dein System sendet. Die Nachricht steht im Postfach wie eine Antwort aus deinem Team (vom Ersteller des Schlüssels). Der Assistent übernimmt danach nicht automatisch. Der Kunde bekommt erst eine kurze Benachrichtigung.
    • Anfrage: POST /v1/messages
    • Antwort: 202 Accepted
    • Team: Hallo Frau Weber, Ihre Rechnung 2024-117 ist fertig.
    • Kunde erhält: Hallo Ingrid, Sie haben Updates von Heizung Weber bezüglich Ihrer Anfrage erhalten. Erhalten Sie jetzt Ihre Nachrichten! [Jetzt anschauen]
  2. Kunde tippt – Die eigentliche Nachricht kommt an. Mit einer freigegebenen WhatsApp-Vorlage im Request kommt der Inhalt sofort an – ohne diesen Zwischenschritt.
    • Kunde: Kunde tippt „Jetzt anschauen“
    • Kunde erhält: Hallo Frau Weber, Ihre Rechnung 2024-117 ist fertig.
  3. Wenn der Kunde antwortet – Die Antwort landet im Postfach. Dein Team antwortet in der App – oder dein System liest den Verlauf mit GET /v1/projects/{id}/messages.
    • Anfrage: GET /v1/projects/67890/messages
    • Team: Neue Antwort von Ingrid Weber
    • Kunde schreibt: Danke! Kann ich per Überweisung zahlen?

In 5 Minuten zum ersten API-Call

  1. API-Schlüssel erstellen

    In der meiti App: Einstellungen → Einrichtung & Betrieb → Verbundene Systeme → API-Zugang einen Schlüssel erstellen – die API ist für jedes Postfach freigeschaltet. Er gilt für genau ein Postfach und wird nur einmal angezeigt – leg ihn als MEITI_API_KEY in deine Umgebungsvariablen.

  2. Schlüssel prüfen

    GET /v1/me ist der einfachste Test: Er zeigt dir, zu welchem Postfach der Schlüssel gehört.

    curl https://api.meiti.ai/v1/me \
      -H "X-Api-Key: $MEITI_API_KEY"
    const res = await fetch("https://api.meiti.ai/v1/me", {
      headers: { "X-Api-Key": process.env.MEITI_API_KEY },
    });
    console.log(await res.json()); // { workspaceId, workspaceName, keyName }
    import os, requests
    
    res = requests.get(
        "https://api.meiti.ai/v1/me",
        headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
    )
    print(res.json())  # {'workspaceId': ..., 'workspaceName': ..., 'keyName': ...}
    // main.go — go run main.go
    package main
    
    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"os"
    )
    
    // call sends JSON to the meiti API and decodes the JSON answer. Extra args are header name/value pairs.
    func call(method, path string, in any, headers ...string) (map[string]any, int, error) {
    	var body io.Reader
    	if in != nil {
    		b, err := json.Marshal(in)
    		if err != nil {
    			return nil, 0, err
    		}
    		body = bytes.NewReader(b)
    	}
    	req, err := http.NewRequest(method, "https://api.meiti.ai"+path, body)
    	if err != nil {
    		return nil, 0, err
    	}
    	req.Header.Set("X-Api-Key", os.Getenv("MEITI_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	for i := 0; i+1 < len(headers); i += 2 {
    		req.Header.Set(headers[i], headers[i+1])
    	}
    	res, err := http.DefaultClient.Do(req)
    	if err != nil {
    		return nil, 0, err
    	}
    	defer res.Body.Close()
    	var out map[string]any
    	_ = json.NewDecoder(res.Body).Decode(&out)
    	return out, res.StatusCode, nil
    }
    
    func main() {
    	me, status, err := call("GET", "/v1/me", nil)
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(status, me) // 200 map[keyName:… workspaceId:… workspaceName:…]
    }
    using System.Net.Http.Json;
    using System.Text.Json;
    
    using var http = new HttpClient { BaseAddress = new Uri("https://api.meiti.ai/") };
    http.DefaultRequestHeaders.Add("X-Api-Key", Environment.GetEnvironmentVariable("MEITI_API_KEY"));
    
    var me = await http.GetFromJsonAsync<JsonElement>("v1/me");
    Console.WriteLine(me); // { "workspaceId": …, "workspaceName": …, "keyName": … }
    <?php
    $ch = curl_init("https://api.meiti.ai/v1/me");
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY")],
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $me = json_decode(curl_exec($ch), true);
    print_r($me); // workspaceId, workspaceName, keyName
  3. Dich selbst als Kontakt anlegen

    Nachrichten gehen an Kontakte des Postfachs. Leg dich mit deiner Handynummer an – 201 heißt neu, 200 heißt: gab es schon.

    curl https://api.meiti.ai/v1/contacts \
      -H "X-Api-Key: $MEITI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "phone": "+491701234567", "firstName": "Test", "lastName": "Ich" }'
    const res = await fetch("https://api.meiti.ai/v1/contacts", {
      method: "POST",
      headers: { "X-Api-Key": process.env.MEITI_API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ phone: "+491701234567", firstName: "Test", lastName: "Ich" }),
    });
    console.log(res.status, await res.json()); // 201 (new) or 200 (already there) { id, … }
    import os, requests
    
    res = requests.post(
        "https://api.meiti.ai/v1/contacts",
        headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
        json={"phone": "+491701234567", "firstName": "Test", "lastName": "Ich"},
    )
    print(res.status_code, res.json())  # 201 (new) or 200 (already there)
    // call() from step 2
    contact, status, err := call("POST", "/v1/contacts", map[string]any{
    	"phone": "+491701234567", "firstName": "Test", "lastName": "Ich",
    })
    fmt.Println(status, contact, err) // 201 (new) or 200 (already there)
    // http from step 2 (BaseAddress + X-Api-Key)
    var res = await http.PostAsJsonAsync("v1/contacts", new { phone = "+491701234567", firstName = "Test", lastName = "Ich" });
    Console.WriteLine($"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}"); // 201 (new) or 200 (already there)
    <?php
    $ch = curl_init("https://api.meiti.ai/v1/contacts");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY"), "Content-Type: application/json"],
        CURLOPT_POSTFIELDS => json_encode(["phone" => "+491701234567", "firstName" => "Test", "lastName" => "Ich"]),
    ]);
    $body = curl_exec($ch);
    echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . " " . $body; // 201 (new) or 200 (already there)
  4. Erste Nachricht senden

    curl https://api.meiti.ai/v1/messages \
      -H "X-Api-Key: $MEITI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "phone": "+491701234567", "text": "Hallo aus meiner ersten Integration!" }'
    const res = await fetch("https://api.meiti.ai/v1/messages", {
      method: "POST",
      headers: { "X-Api-Key": process.env.MEITI_API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ phone: "+491701234567", text: "Hallo aus meiner ersten Integration!" }),
    });
    console.log(res.status, await res.json()); // 202 { status, channel, waitingForReply, … }
    import os, requests
    
    res = requests.post(
        "https://api.meiti.ai/v1/messages",
        headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
        json={"phone": "+491701234567", "text": "Hallo aus meiner ersten Integration!"},
    )
    print(res.status_code, res.json())  # 202 {status, channel, waitingForReply, ...}
    // call() from step 2
    sent, status, err := call("POST", "/v1/messages", map[string]any{
    	"phone": "+491701234567",
    	"text":  "Hallo aus meiner ersten Integration!",
    })
    fmt.Println(status, sent, err) // 202 map[channel:… status:queued waitingForReply:…]
    // http from step 2 (BaseAddress + X-Api-Key)
    var res = await http.PostAsJsonAsync("v1/messages", new { phone = "+491701234567", text = "Hallo aus meiner ersten Integration!" });
    Console.WriteLine($"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}"); // 202 { status, channel, waitingForReply, … }
    <?php
    $ch = curl_init("https://api.meiti.ai/v1/messages");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY"), "Content-Type: application/json"],
        CURLOPT_POSTFIELDS => json_encode(["phone" => "+491701234567", "text" => "Hallo aus meiner ersten Integration!"]),
    ]);
    $body = curl_exec($ch);
    echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . " " . $body; // 202 { status, channel, waitingForReply, … }

    Die Antwort kommt sofort mit 202. Spannend ist waitingForReply:

    {
      "projectId": 67890,
      "contactId": 18342,
      "messageId": 991245,
      "status": "queued",
      "channel": "whatsapp",
      "senderId": "whatsapp_3",
      "sentAsTemplate": false,
      "waitingForReply": true,
      "consentOverridden": false
    }

    Warum steht da „waitingForReply: true“?

    WhatsApp erlaubt freie Texte nur, wenn der Kunde in den letzten 24 Stunden geschrieben hat. Ist dieses Fenster zu, schickt meiti automatisch eine kurze Benachrichtigung mit dem Button „Jetzt anschauen“. Tippt der Kunde darauf, öffnet sich das Fenster und deine Nachricht kommt sofort hinterher – nichts geht verloren.

    Soll der Inhalt direkt ankommen, gib eine freigegebene WhatsApp-Vorlage mit (whatsApp.template, Liste unter GET /v1/whatsapp/templates) – dann steht sentAsTemplate: true in der Antwort. Mit eigener WhatsApp-Nummer siehst du bei deinem ersten Test genau diese Benachrichtigung, sonst eine ähnliche von meiti.

Tipp: Zum Testen legst du dir ein eigenes Postfach oder eine eigene Organisation nur für Tests an und erstellst dort einen API-Schlüssel. Jeder Schlüssel gehört zu genau einem Postfach.

Rezepte

Typische Integrationen zum Nachbauen

In allen Beispielen stehen crm, customer, invoice, caseRecord und calendar für deinen eigenen Code.

Anrufer erkennen und Gesprächsnotizen ins CRM – die Rezepte für den CRM-Webhook, dazu lokales Testen mit einem Tunnel, stehen auf der Seite Webhooks.
API

Nachricht aus deinem System: „Ihre Rechnung ist fertig“

Dein System kennt den Moment, meiti den Kanal. Schick Telefonnummer, E-Mail oder contactId und den Text – meiti wählt WhatsApp, SMS, RCS oder E-Mail, öffnet bei Bedarf einen neuen Vorgang und zeigt die Nachricht im Postfach. Sie zählt wie eine Antwort aus deinem Team: Der Assistent übernimmt danach nicht automatisch, dein Team antwortet im Postfach.

curl https://api.meiti.ai/v1/messages \
  -H "X-Api-Key: $MEITI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-2024-117-ready" \
  -d '{
    "phone": "+491701234567",
    "text": "Hallo Frau Weber, Ihre Rechnung 2024-117 ist fertig. Fragen? Antworten Sie einfach hier.",
    "purpose": "service"
  }'
const res = await fetch("https://api.meiti.ai/v1/messages", {
  method: "POST",
  headers: {
    "X-Api-Key": process.env.MEITI_API_KEY,
    "Content-Type": "application/json",
    // Same key on a retry = the message is sent only once.
    "Idempotency-Key": `invoice-${invoice.number}-ready`,
  },
  body: JSON.stringify({
    phone: customer.phone,
    text: `Hallo ${customer.name}, Ihre Rechnung ${invoice.number} ist fertig. Fragen? Antworten Sie einfach hier.`,
    purpose: "service",
  }),
});
const sent = await res.json(); // 202: { projectId, contactId, messageId, status, channel }
import os, requests

res = requests.post(
    "https://api.meiti.ai/v1/messages",
    headers={
        "X-Api-Key": os.environ["MEITI_API_KEY"],
        # Same key on a retry = the message is sent only once.
        "Idempotency-Key": f"invoice-{invoice.number}-ready",
    },
    json={
        "phone": customer.phone,
        "text": f"Hallo {customer.name}, Ihre Rechnung {invoice.number} ist fertig. Fragen? Antworten Sie einfach hier.",
        "purpose": "service",
    },
)
sent = res.json()  # 202: projectId, contactId, messageId, status, channel
// call() from the quickstart; the last two args set the Idempotency-Key header.
sent, status, err := call("POST", "/v1/messages", map[string]any{
	"phone":   customer.Phone,
	"text":    fmt.Sprintf("Hallo %s, Ihre Rechnung %s ist fertig. Fragen? Antworten Sie einfach hier.", customer.Name, invoice.Number),
	"purpose": "service",
}, "Idempotency-Key", "invoice-"+invoice.Number+"-ready") // same key on a retry = sent only once
if err != nil {
	log.Fatal(err)
}
fmt.Println(status, sent["status"], sent["channel"]) // 202 queued whatsapp
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/messages")
{
    Content = JsonContent.Create(new
    {
        phone = customer.Phone,
        text = $"Hallo {customer.Name}, Ihre Rechnung {invoice.Number} ist fertig. Fragen? Antworten Sie einfach hier.",
        purpose = "service",
    }),
};
// Same key on a retry = the message is sent only once.
request.Headers.Add("Idempotency-Key", $"invoice-{invoice.Number}-ready");

var res = await http.SendAsync(request); // http from the quickstart (BaseAddress + X-Api-Key)
var sent = await res.Content.ReadFromJsonAsync<JsonElement>(); // 202: projectId, contactId, messageId, status, channel
<?php
$ch = curl_init("https://api.meiti.ai/v1/messages");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "X-Api-Key: " . getenv("MEITI_API_KEY"),
        "Content-Type: application/json",
        // Same key on a retry = the message is sent only once.
        "Idempotency-Key: invoice-{$invoice['number']}-ready",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "phone" => $customer["phone"],
        "text" => "Hallo {$customer['name']}, Ihre Rechnung {$invoice['number']} ist fertig. Fragen? Antworten Sie einfach hier.",
        "purpose" => "service",
    ]),
]);
$sent = json_decode(curl_exec($ch), true); // 202: projectId, contactId, messageId, status, channel
  • Idempotency-Key: dieselbe Anfrage zweimal = eine Nachricht. Gilt 24 Stunden.
  • purpose: none (Standard), service oder marketing – Marketing nur mit Einwilligung, sonst 403.
  • Kanal festlegen: channels: ["whatsapp", "sms"] in Prioritätsreihenfolge; Absender siehst du unter GET /v1/senders.
  • WhatsApp-Fenster geschlossen? Gib unter whatsApp eine freigegebene Vorlage mit (GET /v1/whatsapp/templates), sonst wartet der Text, bis der Kunde antwortet.

Kontakte, die es noch nicht gibt, legst du vorher an – POST /v1/contacts legt an oder aktualisiert anhand der Telefonnummer:

curl https://api.meiti.ai/v1/contacts \
  -H "X-Api-Key: $MEITI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+491701234567",
    "firstName": "Ingrid",
    "lastName": "Weber",
    "email": "ingrid.weber@example.com",
    "serviceConsent": true
  }'
# 201 = created, 200 = updated. Keep the returned "id" next to your own customer id.
const res = await fetch("https://api.meiti.ai/v1/contacts", {
  method: "POST",
  headers: { "X-Api-Key": process.env.MEITI_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    phone: "+491701234567",
    firstName: "Ingrid",
    lastName: "Weber",
    email: "ingrid.weber@example.com",
    serviceConsent: true,
  }),
});
const contact = await res.json(); // 201 created / 200 updated
await crm.customers.update(customerId, { meitiContactId: contact.id });
res = requests.post(
    "https://api.meiti.ai/v1/contacts",
    headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
    json={
        "phone": "+491701234567",
        "firstName": "Ingrid",
        "lastName": "Weber",
        "email": "ingrid.weber@example.com",
        "serviceConsent": True,
    },
)
contact = res.json()  # 201 created / 200 updated
crm.customers.update(customer_id, meiti_contact_id=contact["id"])
contact, status, err := call("POST", "/v1/contacts", map[string]any{
	"phone":          "+491701234567",
	"firstName":      "Ingrid",
	"lastName":       "Weber",
	"email":          "ingrid.weber@example.com",
	"serviceConsent": true,
})
if err != nil || status >= 300 {
	log.Fatal(status, err, contact)
}
// 201 created / 200 updated — keep the id next to your own customer id
crm.SaveMeitiContactID(customerID, int(contact["id"].(float64)))
var res = await http.PostAsJsonAsync("v1/contacts", new
{
    phone = "+491701234567",
    firstName = "Ingrid",
    lastName = "Weber",
    email = "ingrid.weber@example.com",
    serviceConsent = true,
});
var contact = await res.Content.ReadFromJsonAsync<JsonElement>(); // 201 created / 200 updated
customer.MeitiContactId = contact.GetProperty("id").GetInt32();
<?php
$ch = curl_init("https://api.meiti.ai/v1/contacts");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "phone" => "+491701234567",
        "firstName" => "Ingrid",
        "lastName" => "Weber",
        "email" => "ingrid.weber@example.com",
        "serviceConsent" => true,
    ]),
]);
$contact = json_decode(curl_exec($ch), true); // 201 created / 200 updated
$crm->updateCustomer($customerId, ["meiti_contact_id" => $contact["id"]]);
API

Mails aus deinem eigenen Postfach und Notizen in meiti

Nicht jede Nachricht läuft über meiti: Mails aus deinem Gmail- oder Outlook-Postfach, WhatsApps aus einem anderen Tool. Mit POST /v1/messages/log trägst du sie in den Vorgang des Kontakts ein – zum Beispiel aus einem n8n-Workflow bei jeder neuen Mail. meiti sendet dabei nichts und antwortet nicht darauf; der Assistent liest sie aber als Teil des Gesprächs mit.

curl https://api.meiti.ai/v1/messages/log \
  -H "X-Api-Key: $MEITI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "erika.weber@example.com",
    "direction": "in",
    "channel": "email",
    "subject": "Angebot Badsanierung",
    "text": "Hallo, wann können Sie diese Woche vorbeikommen?",
    "occurredAt": "2026-10-09T08:12:00Z",
    "externalId": "<CAF7x9Q@mail.gmail.com>"
  }'
// e.g. from an n8n Gmail trigger: one call per new mail in your own mailbox.
const res = await fetch("https://api.meiti.ai/v1/messages/log", {
  method: "POST",
  headers: { "X-Api-Key": process.env.MEITI_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    email: mail.from,
    direction: "in",                // "out" for mails your team sent
    channel: "email",
    subject: mail.subject,
    text: mail.text,
    occurredAt: mail.date,
    externalId: mail.messageId,     // same id again = filed only once
  }),
});
const logged = await res.json(); // 201: { projectId, contactId, messageId, created: true, truncated }
import os, requests

res = requests.post(
    "https://api.meiti.ai/v1/messages/log",
    headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
    json={
        "email": mail.sender,
        "direction": "in",             # "out" for mails your team sent
        "channel": "email",
        "subject": mail.subject,
        "text": mail.text,
        "occurredAt": mail.date.isoformat(),
        "externalId": mail.message_id,  # same id again = filed only once
    },
)
logged = res.json()  # 201 (200 if logged before): projectId, contactId, messageId, created, truncated
// call() from the quickstart.
logged, status, err := call("POST", "/v1/messages/log", map[string]any{
	"email":      mail.From,
	"direction":  "in", // "out" for mails your team sent
	"channel":    "email",
	"subject":    mail.Subject,
	"text":       mail.Text,
	"occurredAt": mail.Date.Format(time.RFC3339),
	"externalId": mail.MessageID, // same id again = filed only once
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(status, logged["projectId"], logged["created"]) // 201 67890 true
var res = await http.PostAsJsonAsync("v1/messages/log", new // http from the quickstart
{
    email = mail.From,
    direction = "in",            // "out" for mails your team sent
    channel = "email",
    subject = mail.Subject,
    text = mail.TextBody,
    occurredAt = mail.Date,
    externalId = mail.MessageId, // same id again = filed only once
});
var logged = await res.Content.ReadFromJsonAsync<JsonElement>(); // 201: projectId, contactId, messageId, created, truncated
<?php
$ch = curl_init("https://api.meiti.ai/v1/messages/log");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "email" => $mail["from"],
        "direction" => "in",              // "out" for mails your team sent
        "channel" => "email",
        "subject" => $mail["subject"],
        "text" => $mail["text"],
        "occurredAt" => $mail["date"],
        "externalId" => $mail["messageId"], // same id again = filed only once
    ]),
]);
$logged = json_decode(curl_exec($ch), true); // 201: projectId, contactId, messageId, created, truncated
  • direction: in = vom Kunden, out = von deinem Team. channel: email, whatsapp, sms oder rcs; subject nur bei E-Mail.
  • Ziel: projectId oder ein Kontakt (contactId, phone, email) – dann landet die Nachricht im aktuellen Vorgang des Kontakts. Der Kanal, über den meiti dem Kontakt antwortet, bleibt unverändert.
  • externalId (z. B. die Message-ID der Mail): dieselbe ID auf demselben Kanal noch mal = nichts Neues, Antwort 200 mit der ersten Nachricht.
  • Eingehende Nachrichten sind fürs Team ungelesen; eine Push-Benachrichtigung gibt es nur mit notify: true. Der Assistent bleibt, wie er ist – außer du schickst ai: "pause" (dein Team übernimmt) oder ai: "resume" (zurück an den Assistenten).

Was nur dein Team wissen soll, legst du als Notiz ab. Notizen gehen nie an den Kunden, und der Assistent liest sie nicht:

curl https://api.meiti.ai/v1/notes \
  -H "X-Api-Key: $MEITI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-note-4711" \
  -d '{
    "phone": "+491701234567",
    "title": "Aus dem CRM",
    "text": "Kundin möchte nur vormittags angerufen werden. Offene Rechnung 2024-117."
  }'
const res = await fetch("https://api.meiti.ai/v1/notes", {
  method: "POST",
  headers: {
    "X-Api-Key": process.env.MEITI_API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": `crm-note-${note.id}`, // same key on a retry = one note
  },
  body: JSON.stringify({ phone: customer.phone, title: "Aus dem CRM", text: note.text }),
});
const created = await res.json(); // 201: { projectId, contactId, noteId }
import os, requests

res = requests.post(
    "https://api.meiti.ai/v1/notes",
    headers={
        "X-Api-Key": os.environ["MEITI_API_KEY"],
        "Idempotency-Key": f"crm-note-{note.id}",  # same key on a retry = one note
    },
    json={"phone": customer.phone, "title": "Aus dem CRM", "text": note.text},
)
created = res.json()  # 201: projectId, contactId, noteId
// call() from the quickstart; the last two args set the Idempotency-Key header.
created, status, err := call("POST", "/v1/notes", map[string]any{
	"phone": customer.Phone,
	"title": "Aus dem CRM",
	"text":  note.Text,
}, "Idempotency-Key", "crm-note-"+note.ID) // same key on a retry = one note
if err != nil {
	log.Fatal(err)
}
fmt.Println(status, created["noteId"]) // 201 5531
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/notes")
{
    Content = JsonContent.Create(new { phone = customer.Phone, title = "Aus dem CRM", text = note.Text }),
};
request.Headers.Add("Idempotency-Key", $"crm-note-{note.Id}"); // same key on a retry = one note

var res = await http.SendAsync(request); // http from the quickstart
var created = await res.Content.ReadFromJsonAsync<JsonElement>(); // 201: projectId, contactId, noteId
<?php
$ch = curl_init("https://api.meiti.ai/v1/notes");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "X-Api-Key: " . getenv("MEITI_API_KEY"),
        "Content-Type: application/json",
        "Idempotency-Key: crm-note-{$note['id']}", // same key on a retry = one note
    ],
    CURLOPT_POSTFIELDS => json_encode(["phone" => $customer["phone"], "title" => "Aus dem CRM", "text" => $note["text"]]),
]);
$created = json_decode(curl_exec($ch), true); // 201: projectId, contactId, noteId
API + Webhooks

Termine aus deinem Kalender bestätigen lassen

Dein Kalender plant, meiti kümmert sich um den Kunden: Mit confirmed: false lädt meiti ein und holt die Bestätigung ein, mit true ist der Termin direkt vereinbart. Erinnerungen laufen automatisch. Deine eigene ID in externalId kommt bei jedem Abruf und Webhook zurück.

curl https://api.meiti.ai/v1/appointments \
  -H "X-Api-Key: $MEITI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+491701234567",
    "start": "2026-10-20T08:00:00Z",
    "durationMinutes": 60,
    "title": "Heizungswartung",
    "kind": "in_person",
    "confirmed": false,
    "externalId": "CAL-8812",
    "metadata": { "technician": "Jonas" }
  }'
const res = await fetch("https://api.meiti.ai/v1/appointments", {
  method: "POST",
  headers: { "X-Api-Key": process.env.MEITI_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    phone: "+491701234567",
    start: "2026-10-20T08:00:00Z",
    durationMinutes: 60,
    title: "Heizungswartung",
    confirmed: false,          // meiti asks the customer to confirm
    externalId: calendarEvent.id, // comes back on every read and webhook
    metadata: { technician: "Jonas" },
  }),
});
const appointment = await res.json(); // 201: { id, status: "proposed", startLocal, … }
res = requests.post(
    "https://api.meiti.ai/v1/appointments",
    headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
    json={
        "phone": "+491701234567",
        "start": "2026-10-20T08:00:00Z",
        "durationMinutes": 60,
        "title": "Heizungswartung",
        "confirmed": False,            # meiti asks the customer to confirm
        "externalId": calendar_event.id,  # comes back on every read and webhook
        "metadata": {"technician": "Jonas"},
    },
)
appointment = res.json()  # 201: id, status "proposed", startLocal, ...
appointment, status, err := call("POST", "/v1/appointments", map[string]any{
	"phone":           "+491701234567",
	"start":           "2026-10-20T08:00:00Z",
	"durationMinutes": 60,
	"title":           "Heizungswartung",
	"kind":            "in_person",
	"confirmed":       false,                      // meiti asks the customer to confirm
	"externalId":      calendarEvent.ID,           // comes back on every read and webhook
	"metadata":        map[string]string{"technician": "Jonas"},
})
if err != nil || status != 201 {
	log.Fatal(status, err, appointment)
}
fmt.Println(appointment["id"], appointment["status"]) // … proposed
var res = await http.PostAsJsonAsync("v1/appointments", new
{
    phone = "+491701234567",
    start = new DateTime(2026, 10, 20, 8, 0, 0, DateTimeKind.Utc),
    durationMinutes = 60,
    title = "Heizungswartung",
    confirmed = false,              // meiti asks the customer to confirm
    externalId = calendarEvent.Id,  // comes back on every read and webhook
    metadata = new Dictionary<string, string> { ["technician"] = "Jonas" },
});
var appointment = await res.Content.ReadFromJsonAsync<JsonElement>(); // 201: id, status "proposed", …
<?php
$ch = curl_init("https://api.meiti.ai/v1/appointments");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "phone" => "+491701234567",
        "start" => "2026-10-20T08:00:00Z",
        "durationMinutes" => 60,
        "title" => "Heizungswartung",
        "confirmed" => false,            // meiti asks the customer to confirm
        "externalId" => $calendarEvent["id"],
        "metadata" => ["technician" => "Jonas"],
    ]),
]);
$appointment = json_decode(curl_exec($ch), true); // 201: id, status "proposed", …

Damit dein Kalender erfährt, was der Kunde tut, abonnierst du die Termin-Events:

curl https://api.meiti.ai/v1/webhooks \
  -H "X-Api-Key: $MEITI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://crm.example.com/meiti/events",
    "events": ["appointment.confirmed", "appointment.cancelled", "appointment.rescheduled"],
    "description": "Kalender-Sync"
  }'
# The response contains "secret" exactly once — store it, it signs every delivery.
const res = await fetch("https://api.meiti.ai/v1/webhooks", {
  method: "POST",
  headers: { "X-Api-Key": process.env.MEITI_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://crm.example.com/meiti/events",
    events: ["appointment.confirmed", "appointment.cancelled", "appointment.rescheduled"],
    description: "Kalender-Sync",
  }),
});
const { subscription, secret } = await res.json(); // secret is shown exactly once
res = requests.post(
    "https://api.meiti.ai/v1/webhooks",
    headers={"X-Api-Key": os.environ["MEITI_API_KEY"]},
    json={
        "url": "https://crm.example.com/meiti/events",
        "events": ["appointment.confirmed", "appointment.cancelled", "appointment.rescheduled"],
        "description": "Kalender-Sync",
    },
)
secret = res.json()["secret"]  # shown exactly once
created, status, err := call("POST", "/v1/webhooks", map[string]any{
	"url":         "https://crm.example.com/meiti/events",
	"events":      []string{"appointment.confirmed", "appointment.cancelled", "appointment.rescheduled"},
	"description": "Kalender-Sync",
})
if err != nil || status != 201 {
	log.Fatal(status, err, created)
}
secret := created["secret"].(string) // shown exactly once — store it
saveSecret(secret)
var res = await http.PostAsJsonAsync("v1/webhooks", new
{
    url = "https://crm.example.com/meiti/events",
    events = new[] { "appointment.confirmed", "appointment.cancelled", "appointment.rescheduled" },
    description = "Kalender-Sync",
});
var secret = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("secret").GetString(); // shown exactly once
<?php
$ch = curl_init("https://api.meiti.ai/v1/webhooks");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("MEITI_API_KEY"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "url" => "https://crm.example.com/meiti/events",
        "events" => ["appointment.confirmed", "appointment.cancelled", "appointment.rescheduled"],
        "description" => "Kalender-Sync",
    ]),
]);
$secret = json_decode(curl_exec($ch), true)["secret"]; // shown exactly once

Jede Zustellung ist signiert. Prüf die Signatur, bevor du etwas änderst – Code und Ereignisse stehen unter API-Webhooks.

Deep Links

„In meiti öffnen“ und „Mit meiti anrufen“ im CRM

Ein Link, deine eigene Nummer, kein API-Schlüssel: meiti sucht den Vorgang in allen Postfächern der angemeldeten Person, wechselt bei Bedarf das Postfach und öffnet ihn. Hat ein Kunde keinen offenen Vorgang, startet meiti einen neuen. Der Anruf-Link öffnet das Anruffenster – gewählt wird erst nach dem Tippen.

// https works everywhere; "meiti://open" opens the installed app directly (e.g. from a desktop CRM).
const base = "https://m.meiti.ai/open";

const openCase     = `${base}/project?crmId=${encodeURIComponent(caseRecord.number)}`;
const openCustomer = `${base}/project?crmContactId=${encodeURIComponent(customer.number)}`;
const openById     = `${base}/project?id=${meitiProjectId}`; // projectData.meitiProjectId from the webhook
const call         = `${base}/dial?number=${encodeURIComponent(customer.phone)}`;
from urllib.parse import quote

# https works everywhere; "meiti://open" opens the installed app directly (e.g. from a desktop CRM).
BASE = "https://m.meiti.ai/open"

open_case = f"{BASE}/project?crmId={quote(case.number, safe='')}"
open_customer = f"{BASE}/project?crmContactId={quote(customer.number, safe='')}"
open_by_id = f"{BASE}/project?id={meiti_project_id}"  # projectData.meitiProjectId from the webhook
call = f"{BASE}/dial?number={quote(customer.phone, safe='')}"
// imports: fmt, net/url, strings
// https works everywhere; "meiti://open" opens the installed app directly (e.g. from a desktop CRM).
const base = "https://m.meiti.ai/open"

// QueryEscape encodes & = / +, but turns spaces into "+"; meiti expects %20.
func esc(v string) string { return strings.ReplaceAll(url.QueryEscape(v), "+", "%20") }

func meitiLinks(caseNumber, customerNumber, phone string, meitiProjectID int) []string {
	return []string{
		base + "/project?crmId=" + esc(caseNumber),
		base + "/project?crmContactId=" + esc(customerNumber),
		fmt.Sprintf("%s/project?id=%d", base, meitiProjectID), // projectData.meitiProjectId from the webhook
		base + "/dial?number=" + esc(phone),
	}
}
using System.Diagnostics;

// Web CRM: https works everywhere. Windows desktop app with meiti installed: use "meiti://open".
var scheme = "https://m.meiti.ai/open"; // or "meiti://open"

var openCase     = $"{scheme}/project?crmId={Uri.EscapeDataString(caseRecord.Number)}";
var openCustomer = $"{scheme}/project?crmContactId={Uri.EscapeDataString(customer.Number)}";
var openById     = $"{scheme}/project?id={meitiProjectId}"; // projectData.meitiProjectId from the webhook
var call         = $"{scheme}/dial?number={Uri.EscapeDataString(customer.Phone)}";

Process.Start(new ProcessStartInfo(openCase) { UseShellExecute = true }); // hands the link to the OS
<?php
// https works everywhere; "meiti://open" opens the installed app directly (e.g. from a desktop CRM).
$base = "https://m.meiti.ai/open";

$openCase     = "$base/project?crmId=" . rawurlencode($case["number"]);
$openCustomer = "$base/project?crmContactId=" . rawurlencode($customer["number"]);
$openById     = "$base/project?id=" . (int)$meitiProjectId; // projectData.meitiProjectId from the webhook
$call         = "$base/dial?number=" . rawurlencode($customer["phone"]);
<!-- In your CRM's case view -->
<a href="https://m.meiti.ai/open/project?crmId=AZ-2024%2F117">In meiti öffnen</a>

<!-- Next to a phone number -->
<a href="https://m.meiti.ai/open/dial?number=%2B491701234567">Mit meiti anrufen</a>

Mit KI bauen

Lass Claude Code, Cursor oder Codex die Integration schreiben

Diese Seite ist für Menschen und für Agenten gemacht. Gib deinem Coding-Agenten den Prompt unten – er liest die llms.txt und die OpenAPI-Spezifikation und baut die drei Bausteine in deinem Stack. Oder kopier die ganze Seite als Markdown oben im Kopf.

Build a meiti integration for my system.

Read first:
- Full text:       https://meiti.app/docs/developers/llms-full.txt
- Developer guide: https://meiti.app/docs/developers
- OpenAPI spec:   https://api.meiti.ai/v1/openapi.json

Goal:
1. A CRM webhook endpoint that answers IncomingCallLookup/NewConversation with the caller's name,
   customer number (crmContactId) and a short crmAiInfo, and files FinishedCall/ConversationPaused
   summaries as cases, answering with crmProjectId.
2. Send messages from my system with POST /v1/messages (Idempotency-Key, purpose "service").
3. "Open in meiti" and "Call with meiti" links next to my cases (https://m.meiti.ai/open/...).

Rules: check the bearer token; ids are strings; answer within 2 seconds or use the callbackUrl;
dedupe CRM events on callbackUrl and API webhook events on X-Meiti-Delivery-Id; accept eventType as
string or legacy number; create a contact (POST /v1/contacts) before sending to it; read MEITI_API_KEY
and MEITI_WEBHOOK_TOKEN from env. Write tests that replay the sample payloads, and tell me how to test
live with the Manual event and POST /v1/webhooks/{id}/test.
My stack: <your language/framework here>
  • llms.txt: kurzer Überblick mit den Fakten, die Agenten am leichtesten falsch machen. llms-full.txt: dazu jedes Beispiel in jeder Sprache und jede Payload – ohne HTML.
  • openapi.json: jedes Feld, jeder Fehlercode – maschinenlesbar, auch für Postman, Insomnia oder n8n (Import).

API v1 Beta

Basis-URLhttps://api.meiti.ai/v1/
AuthentifizierungX-Api-Key: meiti_sk_… oder Authorization: Bearer meiti_sk_…
FehlerRFC 9457 application/problem+json: stabiler Code in title, Satz in detail
Limits60 Anfragen pro Minute und Schlüssel; 429 mit Retry-After
EndpunktWofür
GET /v1/meFür welches Postfach der Schlüssel spricht.
POST /v1/messagesEine Nachricht an einen Kontakt senden.
POST /v1/messages/logEine Nachricht eintragen, die außerhalb von meiti geschrieben wurde (eigenes Postfach, anderes Tool) – nichts wird gesendet.
POST /v1/notesInterne Notiz an einem Vorgang – nur fürs Team.
GET /v1/sendersAktive Absender (WhatsApp-Nummer, Postfächer, SMS).
GET /v1/whatsapp/templatesFreigegebene WhatsApp-Vorlagen.
POST /v1/contactsKontakt anlegen oder aktualisieren (per Telefonnummer).
GET /v1/contacts?phone=|email=, GET /v1/contacts/{id}Kontakt nachschlagen.
POST /v1/appointmentsTermin anlegen – vereinbart oder zur Bestätigung.
GET /v1/appointments?updatedSince=Änderungen abholen (aufsteigend nach updatedAt).
PATCH / DELETE /v1/appointments/{id}Verschieben, bestätigen, absagen.
GET /v1/projects, …/{id}/messagesVorgänge (Gespräche mit einem Kontakt) und Verlauf lesen; die id ist dieselbe wie meitiProjectId im CRM-Webhook.
GET|POST|DELETE /v1/webhooksWebhook-Abos verwalten, testen mit …/{id}/test – siehe API-Webhooks.
GET /v1/agents, PATCH /v1/agents/{id}, …/{id}/secret, …/{id}/testAssistenten mit eigener KI verbinden – siehe Eigene KI (BYOAI).
POST /v1/projects/{id}/agent-repliesSpätere Antwort deiner eigenen KI in einem Gespräch.

Typen statt Raten: npx openapi-typescript https://api.meiti.ai/v1/openapi.json -o meiti.d.ts (TypeScript) oder oapi-codegen (Go) erzeugen sie aus der Spezifikation.

Alle Felder, Beispiele und Fehlercodes stehen in der OpenAPI-Referenz.

Interaktive API-Referenz öffnen

Checkliste vor dem Start

  • Schlüssel und Webhook-Secret liegen in Umgebungsvariablen, nicht im Code.
  • Dein Webhook prüft Bearer-Token bzw. Signatur und antwortet in unter 2 Sekunden.
  • Mit dem Ereignis Manual (Button in den Vorgangsdetails) bzw. POST /v1/webhooks/{id}/test getestet.
  • Doppelte Events und Wiederholungen ändern nichts doppelt.
  • Nachrichten tragen einen Idempotency-Key und das passende purpose.
  • IDs sind in beide Richtungen verknüpft – Deep Links öffnen den richtigen Vorgang.
  • BYOAI: Chat-Endpunkt und WebSocket prüfen die Signatur, antworten innerhalb von timeoutSeconds und verwerfen späte Antworten bei 409; „Verbindung testen“ ist grün.
  • Mit einem Test-Postfach und deiner eigenen Nummer durchgespielt, dann auf echte Kunden umgestellt.

Fragen zur Anbindung?

Wir helfen dir und deiner IT bei der Planung, schauen auf den ersten Entwurf und nehmen Wünsche für die API auf.