Recognise callers and give the assistant context
“Ms Weber, thanks for calling — how can I help?” On every call meiti sends IncomingCallLookup with the phone number. If your system knows the customer, answer directly with name, customer number and a note for the assistant. The same handler also answers NewConversation (chat) and receives the summary afterwards — see the next recipe.
// 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
What an incoming event looks like
{
"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
}
}
Shortened: contact and project data also carry address, custom fields and summaries. Unknown callers have no crmContactId yet.
Case already exists? Also return requestProjectUpdate: true with your crmProjectId on IncomingCallLookup — meiti's project carries your case number right away. Several meiti projects can carry the same number; a deep link then shows a list. With projectData.crmAiInfo the assistant also gets a note about the case itself.