Anrufer erkennen und dem Assistenten Kontext geben
„Frau Weber, schön, dass Sie anrufen – wie kann ich helfen?“ Bei jedem Anruf schickt meiti IncomingCallLookup mit der Telefonnummer. Findet dein System den Kunden, antwortest du direkt mit Name, Kundennummer und einem Hinweis für den Assistenten. Derselbe Handler beantwortet auch NewConversation (Chat) und nimmt nach dem Gespräch die Zusammenfassung entgegen – siehe nächstes Rezept.
// Node + Express. crm.* stands for your own CRM code.
import express from "express";
const app = express();
app.post("/meiti/webhook", express.json(), async (req, res) => {
if (req.get("Authorization") !== `Bearer ${process.env.MEITI_WEBHOOK_TOKEN}`) return res.sendStatus(401);
const { eventType, contactData, projectData } = req.body;
// 1) Before the conversation: tell meiti who this is.
if (eventType === "IncomingCallLookup" || eventType === "NewConversation") {
const customer = await crm.findByPhone(contactData.phoneNumber);
if (!customer) return res.sendStatus(204); // unknown: nothing to add
return res.json({
requestContactUpdate: true,
contactData: {
crmContactId: String(customer.id), // ids are strings
firstName: customer.firstName,
lastName: customer.lastName,
crmAiInfo: `Kundin seit ${customer.since}. Bevorzugt Termine vormittags.`,
},
});
}
// 2) After the conversation: file the summary, hand back your case number.
if (eventType === "FinishedCall" || eventType === "ConversationPaused") {
const caseId = await crm.upsertCase({
customerId: contactData.crmContactId, // null when the caller was unknown
meitiProjectId: projectData.meitiProjectId,
summary: projectData.currentSummary, // running summary of the whole project
});
return res.json({ requestProjectUpdate: true, projectData: { crmProjectId: String(caseId) } });
}
res.sendStatus(204); // other or newer events: nothing to do
});
app.listen(3000);
# Python + Flask. crm.* stands for your own CRM code.
import os
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/meiti/webhook")
def meiti_webhook():
if request.headers.get("Authorization") != f"Bearer {os.environ['MEITI_WEBHOOK_TOKEN']}":
abort(401)
event = request.get_json()
contact, project = event["contactData"], event["projectData"]
# 1) Before the conversation: tell meiti who this is.
if event["eventType"] in ("IncomingCallLookup", "NewConversation"):
customer = crm.find_by_phone(contact["phoneNumber"])
if customer is None:
return "", 204 # unknown: nothing to add
return {
"requestContactUpdate": True,
"contactData": {
"crmContactId": str(customer.id), # ids are strings
"firstName": customer.first_name,
"lastName": customer.last_name,
"crmAiInfo": f"Kundin seit {customer.since}. Bevorzugt Termine vormittags.",
},
}
# 2) After the conversation: file the summary, hand back your case number.
if event["eventType"] in ("FinishedCall", "ConversationPaused"):
case_id = crm.upsert_case(
customer_id=contact.get("crmContactId"), # None when the caller was unknown
meiti_project_id=project["meitiProjectId"],
summary=project["currentSummary"], # running summary of the whole project
)
return {"requestProjectUpdate": True, "projectData": {"crmProjectId": str(case_id)}}
return "", 204 # other or newer events: nothing to do
// net/http. crm stands for your own CRM code.
package main
import (
"encoding/json"
"net/http"
"os"
)
type meitiEvent struct {
EventType string `json:"eventType"`
CallbackURL string `json:"callbackUrl"`
ContactData struct {
PhoneNumber string `json:"phoneNumber"`
CrmContactID *string `json:"crmContactId"` // nil when the caller was unknown
} `json:"contactData"`
ProjectData struct {
MeitiProjectID int `json:"meitiProjectId"`
CurrentSummary *string `json:"currentSummary"` // running summary of the whole project, can be empty
} `json:"projectData"`
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func meitiWebhook(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer "+os.Getenv("MEITI_WEBHOOK_TOKEN") {
w.WriteHeader(http.StatusUnauthorized)
return
}
var e meitiEvent
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
switch e.EventType {
// 1) Before the conversation: tell meiti who this is.
case "IncomingCallLookup", "NewConversation":
customer, ok := crm.FindByPhone(e.ContactData.PhoneNumber)
if !ok {
w.WriteHeader(http.StatusNoContent) // unknown: nothing to add
return
}
writeJSON(w, map[string]any{
"requestContactUpdate": true,
"contactData": map[string]any{
"crmContactId": customer.ID, // ids are strings
"firstName": customer.FirstName,
"lastName": customer.LastName,
"crmAiInfo": "Kundin seit " + customer.Since + ". Bevorzugt Termine vormittags.",
},
})
// 2) After the conversation: file the summary, hand back your case number.
case "FinishedCall", "ConversationPaused":
caseID := crm.UpsertCase(e.ContactData.CrmContactID, e.ProjectData.MeitiProjectID, e.ProjectData.CurrentSummary)
writeJSON(w, map[string]any{"requestProjectUpdate": true, "projectData": map[string]any{"crmProjectId": caseID}})
default:
w.WriteHeader(http.StatusNoContent) // other or newer events: nothing to do
}
}
func main() {
http.HandleFunc("POST /meiti/webhook", meitiWebhook)
http.ListenAndServe(":3000", nil)
}
// ASP.NET Core minimal API (Program.cs). crm stands for your own CRM service.
using System.Text.Json;
var app = WebApplication.CreateBuilder(args).Build();
app.MapPost("/meiti/webhook", async (HttpRequest request) =>
{
if (request.Headers.Authorization != $"Bearer {Environment.GetEnvironmentVariable("MEITI_WEBHOOK_TOKEN")}")
return Results.Unauthorized();
using var doc = await JsonDocument.ParseAsync(request.Body);
var evt = doc.RootElement;
var type = evt.GetProperty("eventType").GetString();
var contact = evt.GetProperty("contactData");
var project = evt.GetProperty("projectData");
// 1) Before the conversation: tell meiti who this is.
if (type is "IncomingCallLookup" or "NewConversation")
{
var customer = await crm.FindByPhoneAsync(contact.GetProperty("phoneNumber").GetString());
if (customer is null) return Results.NoContent(); // unknown: nothing to add
return Results.Ok(new
{
requestContactUpdate = true,
contactData = new
{
crmContactId = customer.Id.ToString(), // ids are strings
firstName = customer.FirstName,
lastName = customer.LastName,
crmAiInfo = $"Kundin seit {customer.Since}. Bevorzugt Termine vormittags.",
},
});
}
// 2) After the conversation: file the summary, hand back your case number.
if (type is "FinishedCall" or "ConversationPaused")
{
var caseId = await crm.UpsertCaseAsync(
customerId: contact.GetProperty("crmContactId").GetString(), // null when the caller was unknown
meitiProjectId: project.GetProperty("meitiProjectId").GetInt32(),
summary: project.GetProperty("currentSummary").GetString()); // running summary of the whole project
return Results.Ok(new { requestProjectUpdate = true, projectData = new { crmProjectId = caseId.ToString() } });
}
return Results.NoContent(); // other or newer events: nothing to do
});
app.Run();
<?php
// meiti-webhook.php. $crm stands for your own CRM code.
// Apache/FPM often strip Authorization; check the fallbacks too.
$auth = $_SERVER["HTTP_AUTHORIZATION"]
?? $_SERVER["REDIRECT_HTTP_AUTHORIZATION"]
?? (function_exists("getallheaders") ? (getallheaders()["Authorization"] ?? "") : "");
if ($auth !== "Bearer " . getenv("MEITI_WEBHOOK_TOKEN")) {
http_response_code(401);
exit;
}
$event = json_decode(file_get_contents("php://input"), true);
$contact = $event["contactData"];
$project = $event["projectData"];
header("Content-Type: application/json");
// 1) Before the conversation: tell meiti who this is.
if (in_array($event["eventType"], ["IncomingCallLookup", "NewConversation"], true)) {
$customer = $crm->findByPhone($contact["phoneNumber"]);
if ($customer === null) { http_response_code(204); exit; } // unknown: nothing to add
echo json_encode([
"requestContactUpdate" => true,
"contactData" => [
"crmContactId" => (string)$customer["id"], // ids are strings
"firstName" => $customer["first_name"],
"lastName" => $customer["last_name"],
"crmAiInfo" => "Kundin seit {$customer['since']}. Bevorzugt Termine vormittags.",
],
]);
exit;
}
// 2) After the conversation: file the summary, hand back your case number.
if (in_array($event["eventType"], ["FinishedCall", "ConversationPaused"], true)) {
// crmContactId is null when the caller was unknown; currentSummary covers the whole project.
$caseId = $crm->upsertCase($contact["crmContactId"] ?? null, $project["meitiProjectId"], $project["currentSummary"]);
echo json_encode(["requestProjectUpdate" => true, "projectData" => ["crmProjectId" => (string)$caseId]]);
exit;
}
http_response_code(204); // other or newer events: nothing to do
So sieht ein eingehendes Event aus
{
"eventType": "IncomingCallLookup",
"timestampUtc": "2026-10-08T09:14:03.512Z",
"callbackUrl": "https://api.meiti.ai/api/webhook-callback/5f0c2e…",
"contactData": {
"meitiContactId": 18342,
"phoneNumber": "+491701234567",
"crmContactId": null,
"language": "de",
"firstName": null,
"lastName": null,
"crmAiInfo": null,
"postalAddresses": []
},
"projectData": {
"meitiProjectId": 67890,
"crmProjectId": null,
"projectName": "Anruf von +49 170 1234567",
"currentSummary": null,
"inquirySummary": null,
"crmAiInfo": null
}
}
Gekürzt: Kontakt- und Vorgangsdaten enthalten außerdem Adresse, eigene Felder und Zusammenfassungen. Unbekannte Anrufer haben noch keine crmContactId.
Akte existiert schon? Gib beim IncomingCallLookup zusätzlich requestProjectUpdate: true mit deiner crmProjectId zurück – dann trägt meitis Vorgang sofort deine Aktennummer. Mehrere meiti-Vorgänge können dieselbe Nummer tragen; ein Deep Link zeigt dann eine Auswahl. Mit projectData.crmAiInfo bekommt der Assistent auch einen Hinweis zur Akte selbst.