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.
CRM-Webhook: Einstellungen → Einrichtung & Betrieb → Verbundene Systeme → CRM & Fachsoftware → Webhook – URL und Token trägt deine IT ein.
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
Deep Link
KundeTelefon · WhatsApp · Chat
meitiAssistent & Postfach
Dein SystemCRM · ERP · Kalender
Anruf von +49 170 1234567Ingrid Weber ruft an. meiti nimmt ab.
POST · IncomingCallLookupCRM-Webhook: meiti fragt dein System, wer anruft – noch während es klingelt.
200 · firstName, crmAiInfoDein System antwortet in unter 2 Sekunden mit Namen und einem kurzen Hinweis.
„Hallo Frau Weber! Geht es um Ihre Heizung?“Der Assistent kennt die Kundin und führt das Gespräch.
POST · FinishedCallNach dem Gespräch kommt die Zusammenfassung zu deinem System.
200 · crmProjectId: "AZ-2024/117"Dein System legt den Vorgang an und gibt seine Nummer zurück.
POST /v1/appointmentsAPI: Dein Kalender plant den Termin, meiti lädt die Kundin ein.
Termin Di., 9:00 – passt das?Die Einladung kommt per WhatsApp, mit Button zum Bestätigen.
✓ BestätigtFrau Weber tippt auf „Bestätigen“.
appointment.confirmedSignierter API-Webhook: Dein Kalender erfährt es sofort.
open/project?crmId=AZ-2024%2F117Deep Link: Dein Team klickt im CRM „In meiti öffnen“ und landet im Gespräch.
Ein Anruf, von der ersten Sekunde bis zum bestätigten Termin – und überall, wo dein System dabei ist.
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
meiti führt das Gespräch
Der Assistent kennt den Kunden, beantwortet Fragen, nimmt Anliegen auf, leitet bei Bedarf an dein Team weiter.
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
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.
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 …
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.
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 …
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.
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).
API: POST /v1/appointments mit confirmed: false – meiti lädt ein, der Kunde bestätigt im Chat.
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]
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 ❌]
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 📩]
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]
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.
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 📩]
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
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.
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]
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.
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]
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.
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
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.
Schlüssel prüfen
GET /v1/me ist der einfachste Test: Er zeigt dir, zu welchem Postfach der Schlüssel gehört.
// 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": … }
Hallo Test, Sie haben Updates von Heizung Weber bezüglich Ihrer Anfrage erhalten. Erhalten Sie jetzt Ihre Nachrichten!
Jetzt anschauen
Kunde tippt …
Hallo aus meiner ersten Integration!
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.
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();
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:
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
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.
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", …
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
Endpunkt
Wofür
GET /v1/me
Für welches Postfach der Schlüssel spricht.
POST /v1/messages
Eine Nachricht an einen Kontakt senden.
POST /v1/messages/log
Eine Nachricht eintragen, die außerhalb von meiti geschrieben wurde (eigenes Postfach, anderes Tool) – nichts wird gesendet.
Spä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.
Jeder Link funktioniert als https://m.meiti.ai/open/… (Mail, Browser, Smartphone – ohne App mit Download-Seite) und als meiti://open/… (öffnet die installierte App direkt, z. B. aus einer Windows-Anwendung).
Link
Öffnet
/open/project?id=67890
Den Vorgang mit meitis ID (projectData.meitiProjectId).
/open/project?crmId=AZ-2024%2F117
Die Vorgänge mit deiner Vorgangsnummer (crmProjectId).
/open/project?crmContactId=K-1042
Die offenen Vorgänge des Kunden mit deiner Kundennummer (crmContactId). Hat er keinen, startet meiti einen neuen.
/open/dial?number=%2B491701234567
Das Anruffenster mit dieser Nummer – gewählt wird erst nach dem Tippen.
meiti sucht in allen Postfächern der angemeldeten Person. Liegt der Vorgang in einem anderen, fragt die App vor dem Wechsel. Mehrere Treffer (z. B. dieselbe Aktennummer an mehreren Vorgängen): Die Person wählt aus einer Liste mit Postfach, offene zuerst, höchstens 20. Kein Treffer: Die App sagt es.
Am Desktop öffnet https://m.meiti.ai/… im Browser eine Seite mit „In der App öffnen“. Ist die meiti App unter Windows installiert, öffnet meiti://open/… sie direkt.
Nummern werden ohne Groß-/Kleinschreibung und ohne Leerzeichen, -, ., /, _ verglichen – aber immer als ganze Nummer.
Werte URL-kodieren, z. B. / als %2F und + als %2B.
Ein Link öffnet nur die App: Die Person muss angemeldet sein und sieht nur, worauf sie Zugriff hat. Kontakte legt ein Link nie an.
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.