Bau einen WhatsApp-Agent
mit OpenClaw + TimelinesAI
Mach WhatsApp zu einem Kundenkanal, den dein OpenClaw-Agent bedient: eingehende Nachrichten automatisch beantworten, transaktionale und ereignisgesteuerte Benachrichtigungen senden, mit deinem CRM synchronisieren, den Posteingang mit dem Team teilen. TimelinesAI betreibt das WhatsApp-Gateway. Dein Agent kümmert sich um die Logik.
Auto-AntwortTransaktionale SendsCRM-SyncGeteilter Posteingang
Was dein Agent mit WhatsApp machen kann
Mit einem TimelinesAI-Skill installiert kann dein OpenClaw-Agent WhatsApp als echten Kundenkanal bedienen. Die lange Version dieser Liste steht in den Abschnitten unten — jede Fähigkeit hat die spezifischen API-Calls, die sie braucht.
Eingehende Kundennachrichten automatisch beantworten — 24/7-Autoresponder, Out-of-Office-Antwort oder ein vollwertiger AI-Chatbot, der bei Bedarf an einen Menschen übergibt.
Transaktionale und ereignisgesteuerte Nachrichten senden — Bestellbestätigungen, Versand-Updates, Terminerinnerungen, Zahlungsbestätigungen und Benachrichtigungen, ausgelöst von Events in anderen Tools (HubSpot, Stripe, Calendly). Nur an Kunden, die die Nachricht erwarten.
Leads in mehrteiligen Gesprächen qualifizieren — eine Reihe von Fragen stellen, Antworten speichern, den Chat als qualifiziert markieren oder nicht.
WhatsApp-Aktivität mit deinem CRM synchronisieren — eingehende Nummern in HubSpot/Pipedrive nachschlagen, Deal-Stufen aktualisieren, Notizen zurückschreiben.
Gespräche zusammenfassen und bewerten — „Was hat ACME letzte Woche gefragt?“, „Bewerte diesen Chat 1–10 nach Intent“.
Den Posteingang mit dem Team teilen — dein Agent entwirft Antworten als private Notizen, Menschen senden sie ab; oder der Agent sendet und Menschen schauen zu.
Mit Medien umgehen — Belege als Fotos, PDFs, Sprachnachrichten — von OpenClaw verarbeitet und automatisch beantwortet.
Wie du diesen Guide benutzt
Dieser Guide ist dual-mode. Du kannst ihn selbst lesen und das Setup manuell durchgehen — der Weg, den die Sections unten auslegen — oder du gibst das Ganze deinem OpenClaw-Agent (oder irgendeinem Agent, der eine URL lesen kann) und lässt ihn das Setup für dich machen. Beide Wege landen am selben Ort: vier WhatsApp-Skills installiert, Token und Webhook verdrahtet, dein Agent antwortet auf Kundennachrichten.
Wenn du das selbst liest
Scroll durch die Capability-Gruppen unten — Incoming, Outbound, CRM & Analytics, Operations — und entscheide, welche für deinen Use Case zählen. Dann folge dem Setup manuell; vier Schritte, etwa zehn Minuten. Jede Capability listet die genauen API-Calls, die sie macht, und jeder Code-Block in diesem Guide wurde vor der Veröffentlichung gegen die echte TimelinesAI-API ausgeführt. Wenn du stecken bleibst, sammelt Wichtig zu wissen jedes Gotcha, das dich sonst eine Stunde kostet.
Wenn dein Agent das Setup übernehmen soll
Paste diesen Prompt in OpenClaw (oder Claude Code, Cursor, Claude Desktop, irgendeinen Agent, der eine URL lesen kann). Er zeigt per URL auf diesen Guide, damit der Agent die live-Version liest, und führt dich dann durch Install, Token, Webhook, Smoke-Test und das Aktivieren deines ersten Skills — und fragt dich nur dann nach Input, wenn er wirklich was von dir braucht.
Read https://timelines.ai/guide/openclaw-whatsapp-skills end to end.
Then help me set up an OpenClaw + TimelinesAI WhatsApp agent:
1. Clone InitechSoftware/openclaw-whatsapp-skills into ~/.openclaw/workspace/
and symlink the four skills into ~/.openclaw/skills/.
2. Ask me for my TimelinesAI API token (Integrations → Public API → Copy
on app.timelines.ai) and write it to ~/.openclaw/workspace/.env.timelinesai
as TIMELINES_AI_API_KEY.
3. Run the smoke-test curl from Setup. If it fails, walk me through the
"If you see something else" failure block in the guide.
4. Help me deploy examples/vercel-webhook-receiver from the companion repo
and register the webhook with TimelinesAI.
5. Ask which of the four skills I want to enable first — whatsapp-autoresponder,
whatsapp-lead-qualifier, whatsapp-send, or whatsapp-delivery-check — and
walk me through its capability section plus any relevant gotchas from
Things-to-know.
6. Before enabling anything that sends outbound messages, show me Channel
choice so I pick the right WhatsApp channel (personal number vs Business
API) for my use case.Der Prompt hört absichtlich auf, bevor irgendwas Outbound anschaltet — er routet dich erst durch Channel choice, damit du den richtigen WhatsApp-Kanal (persönliche Nummer vs Business API) für das wählst, was du tatsächlich vorhast. Das Ban-Risiko ist echt und liegt upstream, nicht etwas, wovor dich das Gateway schützen kann.
Setup
Vier Dinge zum Verdrahten. Danach macht dein Skill nur noch API-Calls.
- 1
Verbinde deine WhatsApp-Nummer
Bei app.timelines.ai anmelden, den QR-Code mit dem Telefon scannen, das deine Geschäftsnummer hält. TimelinesAI betreibt das Gateway ab hier. Derselbe Ablauf wie das normale Personal-Number-Setup.
- 2
Hol dir ein API-Token
Integrationen → Public API → Kopieren. Speichere als TIMELINES_AI_API_KEY. Ein Token deckt den ganzen Workspace ab.
- 3
Installiere einen Skill aus dem Companion-Repo
Clone InitechSoftware/openclaw-whatsapp-skills nach ~/.openclaw/workspace/, dann symlinke jedes Skill-Verzeichnis nach ~/.openclaw/skills/. Vier-Zeilen-Quickstart im Companion-README.
- 4
Registriere einen Webhook
Zeig message:received:new auf eine öffentliche HTTPS-URL. TimelinesAI pusht eingehende Nachrichten dorthin. Dein Receiver ruft OpenClaw auf.
Token-Smoketest
Bevor du irgendwas baust, bestätige, dass die Auth funktioniert:
curl -sS -w "\nHTTP: %{http_code}\n" \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/whatsapp_accountsDu solltest sehen
{
"status": "ok",
"data": {
"whatsapp_accounts": [
{
"id": "<phone>@s.whatsapp.net",
"phone": "+<phone>",
"status": "connected",
"account_name": "<your label>"
}
]
}
}Wenn du etwas anderes siehst
- HTTP 401 mit dem reinen Text Unauthorized — das Token ist nicht angekommen. Prüf Schritt 2 nochmal, stell sicher dass der Bearer-Prefix dabei ist und dass das Token beim Einfügen nicht umgebrochen wurde. Die Antwort ist reiner Text, kein JSON — ein Pipe zu jq wird crashen.
- Eine bootstrap-gestylte HTML-„Page not found“-Seite mit HTTP 404 — du hast einen nachgestellten Slash oder einen Tippfehler im Pfad. Die API liefert HTML (keinen JSON-Fehler) für falsche Pfade — wenn deine Ausgabe mit <!DOCTYPE html> beginnt, entfern jeden trailing Slash und prüf den Pfad.
Webhook einmal registrieren
# Generate a random secret once. Add it as a query param so only
# TimelinesAI's registered URL can reach your receiver.
WEBHOOK_SECRET=$(openssl rand -hex 16)
WEBHOOK_URL="https://your-app.example.com/api/webhook?secret=${WEBHOOK_SECRET}"
curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"event_type\":\"message:received:new\",\"url\":\"${WEBHOOK_URL}\",\"enabled\":true}" \
https://app.timelines.ai/integrations/api/webhooksMinimaler Webhook-Receiver (Node / Vercel)
~40 Zeilen. Prüft den ?secret= Query-Parameter, wirft ausgehende Echos raus, acks 2xx innerhalb des 5-Sekunden-Retry-Fensters und reicht die Nachricht dann an deinen OpenClaw-Host weiter.
// api/webhook.js
export default async function handler(req, res) {
// Path-segment auth — TimelinesAI doesn't sign webhooks yet, so the
// ?secret=<token> you registered is the only thing stopping strangers
// from invoking this endpoint.
if (req.query.secret !== process.env.WEBHOOK_SECRET) {
return res.status(404).end();
}
if (req.method !== "POST") return res.status(405).end();
const { event_type, data } = req.body || {};
if (event_type !== "message:received:new") {
return res.status(200).json({ ignored: event_type });
}
// Drop your agent's own sends echoing back as "received".
// TimelinesAI also fires message:received:new for outbound messages
// synced from another WhatsApp client on the same number.
if (data.from_me === true) {
return res.status(200).json({ ignored: "from_me" });
}
// Ack FAST. TimelinesAI retries 3x with a 5-second timeout per attempt —
// if you take longer than 5s the same event hits you up to 3 times.
res.status(200).json({ ok: true });
// TL webhook payloads sometimes arrive flat (data.chat_id) and sometimes
// nested (data.chat.id) depending on event flavor and version. Destructure
// with fallback so both shapes work — see Things to know below.
const chatId = data.chat_id ?? data.chat?.id;
const whatsappAccountId =
data.whatsapp_account_id ?? data.chat?.whatsapp_account_id;
// Fire-and-forget handoff to wherever your OpenClaw host accepts
// inbound messages. For production durability replace this with a
// push to a queue (QStash, Inngest, SQS) your agent drains separately.
try {
await fetch(process.env.OPENCLAW_HOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: chatId,
whatsapp_account_id: whatsappAccountId,
text: data.text,
sender_phone: data.sender_phone,
message_uid: data.message_uid,
}),
});
} catch (err) {
console.error("[timelinesai-webhook] handoff failed:", err);
}
}OPENCLAW_HOOK_URL ist der Endpoint, den dein OpenClaw-Deployment für eingehende Nachrichten bereitstellt — hängt davon ab, wie du den Agent hostest. Für eine produktionsreife Referenz mit Durability-Notizen siehe examples/vercel-webhook-receiver im Companion-Repo.
Was in deinem OpenClaw-Workspace drin ist
Du hast einen TimelinesAI-Token verbunden und Skills aus dem Companion-Repo gesymlinkt. Hier ist, was du tatsächlich installiert hast, und welche Dateien du editierst, wenn du ändern willst, wie dein Agent denkt, was er weiß und wie er sich verhält. Nichts hier ist Magie — es ist alles einfaches Markdown und Env-Dateien in zwei Verzeichnissen.
Skills — ~/.openclaw/skills/<skill>/
Jeder Skill, den du gesymlinkt hast, ist ein Verzeichnis mit einer Pflichtdatei (SKILL.md) plus den Ressourcen, auf die diese Datei verweist. Wenn du ändern willst, wie dein FAQ-Handler klingt oder die Reihenfolge der Fragen des Lead-Qualifiers umsortieren, lebt die Änderung im SKILL.md dieses Skills. Kein Code zum Neukompilieren, kein Build-Step.
| Datei | Was sie steuert |
|---|---|
| SKILL.md | Der ganze Skill. YAML-Frontmatter oben (name, description, user-invocable und etwaige Dispatch-Flags) plus der Body mit Anweisungen/Prompt darunter. Hier editierst du, was der Skill tut, wann OpenClaw ihn wählt und wie er sich verhält. Jeder der vier WhatsApp-Skills im Companion-Repo ist ein einzelnes SKILL.md — kein Code zum Neukompilieren, einfach editieren und neu starten. |
| README.md | Menschliche Dokumentation für den Skill. OpenClaw liest diese Datei NICHT — sie ist für dich, dein Team und jeden, der den Skill auf GitHub ansieht. Optional; die Skills im Companion-Repo haben alle eine. |
| Scripts, resources, schemas | Alles, was im SKILL.md über {baseDir}/... referenziert wird — Helper-Skripte, Prompt-Templates, JSON-Schemas, Test-Fixtures. Von OpenClaw ignoriert, wenn dein SKILL.md nicht explizit darauf zeigt. Leg was der Skill braucht neben SKILL.md und referenzier es per Pfad. |
Für die vier WhatsApp-Skills — whatsapp-autoresponder, whatsapp-lead-qualifier, whatsapp-send, whatsapp-delivery-check — hat der Kopf jeder SKILL.md die Role und die erlaubten Tools; der Body hat den echten Prompt, mit dem dein Agent läuft. Lies eines davon end-to-end, bevor du editierst; die Muster wiederholen sich.
Workspace — ~/.openclaw/workspace/
Das Workspace-Verzeichnis ist OpenClaws Zuhause für alles, was kein Skill ist: wer dein Agent ist, was er über dich weiß, welche Tools er hat, wie er bootet. OpenClaws eigene Docs sagen „dieser Ordner ist Zuhause. Behandle ihn so“. Diese Dateien kommen mit dem Install — du füllst sie mit der Zeit aus:
| Datei | Was sie steuert |
|---|---|
| IDENTITY.md | Wer dein Agent ist — Name, Creature (wie er sich selbst konzeptualisiert), Vibe, Signatur-Emoji, Avatar. Füll das früh aus; die meisten anderen Dateien verweisen darauf. |
| SOUL.md | Kern-Betriebsprinzipien und Persönlichkeit. Wie der Agent sich verhalten soll, wenn niemand hinsieht — wann echt vs performativ, wie man Vertrauen verdient, wo Privatsphären-Grenzen liegen. OpenClaws Docs nennen es das Gewissen des Agents. |
| USER.md | Wem der Agent hilft (dir). Name, Pronomen, Zeitzone, Interessen, Projektkontext. Agents sind besser darin, einer spezifischen Person zu helfen als einem generischen „user“ — diese Datei ist wie sie sich erinnern. |
| TOOLS.md | Umgebungsspezifische Config, die du in keinem Skill haben willst — Gerätenamen, Host-Adressen, lokale Präferenzen. Lebt im Workspace, damit du sie editieren kannst, ohne etwas zu berühren, was du teilen könntest. |
| AGENTS.md | Das Workspace-README für Agents. Beschreibt, wie OpenClaw erwartet, dass dieser Workspace betrieben wird. Kommt normalerweise mit dem Install und wird selten editiert. |
| BOOT.md | Kurze, explizite Anweisungen, was OpenClaw beim Start tun soll. Leer per Default; füll es aus, wenn du deterministisches Boot-Verhalten über Neustarts hinweg willst. |
| BOOTSTRAP.md | First-Boot-Onboarding-Konversation. Führt einen frischen Workspace durch das Aufsetzen seiner Identität und weist dich dann an, die Ergebnisse in IDENTITY.md, SOUL.md, USER.md zu speichern. Lösch es, sobald du die anderen ausgefüllt hast. |
| HEARTBEAT.md | Periodische Task-Definitionen. Leere Datei bedeutet keine Heartbeats; füge hier Tasks hinzu, wenn du willst, dass der Agent etwas in Intervallen prüft (z.B. eine Queue alle fünf Minuten pollen). |
Env-Dateien pro Integration
Neben den kanonischen Dateien oben fügst du eine Env-Datei pro Integration hinzu, die du verbindest. Die sind NICHT Teil des OpenClaw-Installs — sie gehören dir und halten Secrets, die nie in ein öffentliches Repo committed werden sollten:
| Datei | Was sie steuert |
|---|---|
| .env.<integration> | Secrets und Config für eine Integration, eine Datei pro Integration. Für die WhatsApp-Arbeit hast du .env.timelinesai mit TIMELINES_AI_API_KEY und ALLOWED_SENDER_JID in Setup-Schritt 2 erstellt. Zieh dieses Muster nach, wenn du HubSpot, Stripe, Pipedrive oder irgendein anderes Tool hinzufügst — eine .env.<name> und source sie im Skill, der sie braucht. |
Personal Numbers vs WhatsApp Business API
Bevor du Outbound-Flows baust, versteh, zu welchem WhatsApp-Kanal dein Use Case gehört. Falsch wählen führt zu Sperren.
Sicher mit den Skills in diesem Guide (Personal Number)
- •Eingehende Gespräche. Der Kunde schreibt zuerst, du antwortest. Kein Risiko.
- •Transaktionale Sends. Bestellbestätigungen, Versand-Updates, Liefer-Benachrichtigungen, Zahlungsbestätigungen, Terminerinnerungen — jede Nachricht, die ein Kunde erwartet, weil er gerade etwas mit deinem Business gemacht hat.
- •Ereignisgesteuerte Benachrichtigungen aus anderen Tools. HubSpot-Deal → Demo-Bestätigung, Stripe-Zahlungsfehler → Recovery-Notiz, Calendly-Buchung → Pre-Meeting-Reminder. Der Kunde hat opted in, als er das Upstream-Tool benutzt hat.
- •Antworten innerhalb des 24-Stunden-Customer-Service-Fensters von WhatsApp. Sobald ein Kunde dir schreibt, hast du 24 Stunden Zeit, frei zu antworten. Die Autoresponder-, FAQ-Handler- und Lead-Qualifier-Skills arbeiten innerhalb dieses Fensters.
NICHT sicher auf Personal Numbers
- •Cold Outreach an Listen, die du gekauft oder gescraped hast — WhatsApp sperrt Nummern dafür innerhalb von Stunden.
- •Marketing-Broadcasts an Kunden, die nicht explizit opted in sind.
- •Werbekampagnen, Verkaufsangebote, saisonale Pushs, Produktlaunches.
- •Alles, was nach einem Marketing-Blast aussieht. Wenn du fragst „welchen Throughput kann ich rausholen“, bist du im falschen Kanal.
Wie du entscheidest, welchen Kanal du brauchst
- 1.Hat der Kunde dir zuerst geschrieben, oder antwortest du innerhalb einer aktiven 24-Stunden-Session? → Personal Number, dieser Guide.
- 2.Steht der Kunde kurz davor, etwas zu erhalten, das er explizit erwartet (Bestellung, Termin, Zahlung, Lieferung)? → Personal Number, transaktionaler Send.
- 3.Ausgelöst durch ein Customer-opt-in-Event in deinem CRM oder Billing-Tool? → Personal Number, ereignisgesteuerter Send.
- 4.Broadcast, Cold Outreach oder Werbekampagne? → Business API, nimm das TimelinesAI-Dashboard.
- 5.Nicht sicher? → Nicht senden. Behandle es als werblich und route es auf den Business-API-Pfad.
Jede Outbound-Fähigkeit unten geht davon aus, dass du diesen Test bestanden hast. Wenn du unsicher bist, lies diesen Abschnitt vor dem Shipping nochmal. TimelinesAI kann dich nicht vor der Sperre auf WhatsApp-Ebene schützen — die Sperre wird upstream durchgesetzt, in der WhatsApp-Infrastruktur, nicht im Gateway.
API-Referenz
Jeder Endpoint, den du brauchst, um jede Fähigkeit unten zu bauen. Base URL https://app.timelines.ai/integrations/api. Auth Authorization: Bearer $TIMELINES_AI_API_KEY.
Lesen
| Methode | Pfad | Was es zurückgibt |
|---|---|---|
| GET | /whatsapp_accounts | Deine verbundenen WhatsApp-Nummern, jede mit JID, Telefon, Status und Account-Name. |
| GET | /chats | Chat-Liste. Unterstützt Filter ?phone=... und ?label=.... Paginiere mit ?page=N (50 pro Seite, fix). Jeder Chat hat whatsapp_account_id mit der JID der besitzenden Nummer und chatgpt_autoresponse_enabled — siehe Wichtig zu wissen bevor du deinen eigenen Agent launchst. |
| GET | /chats/{id} | Vollständiges Detail eines Chats. |
| GET | /chats/{id}/messages | Nachrichtenverlauf, 50 pro Seite. Paginiere mit ?page=N und schau auf data.has_more_pages. Jede Nachricht trägt from_me, sender_phone, text, timestamp, message_type, status (Sent/Delivered/Read) und origin (Public API vs WhatsApp-App). |
| GET | /chats/{id}/labels | Labels des Chats. |
| GET | /messages/{uid}/status_history | Sent / Delivered / Read-Timeline einer ausgehenden Nachricht. |
| GET | /messages/{uid}/reactions | Reaktionen auf eine Nachricht. Liefert {data: {users: [{name, phone, reaction, current}], reactions: {<emoji>: count}, total: N}} — ein Objekt, kein flaches Array. users listet, wer reagiert hat (mit dem gewählten Emoji und einem current-Boolean, das deinen eigenen Workspace markiert); reactions ist ein Histogramm nach Emoji. Leerzustand: {users: [], reactions: {}, total: 0}. |
| GET | /files | Dateien, die du über die API hochgeladen hast. |
| GET | /webhooks | Deine registrierten Webhook-Subscriptions. |
Schreiben
| Methode | Pfad | Was es macht |
|---|---|---|
| POST | /messages | Sendet an eine Telefonnummer. Body: {"phone":"+...","text":"..."}. Liefert {"message_uid":"..."}. |
| POST | /chats/{id}/messages | Sendet in einen bestehenden Chat. Body: {"text":"..."}. Sender ist die WhatsApp-Nummer, der der Chat gehört. |
| POST | /chats/{id}/notes | Hängt eine private Notiz an einen Chat. Wird nicht an WhatsApp gesendet, nur innerhalb von TimelinesAI sichtbar. Wird für Agent-State und Review-Workflows verwendet. |
| POST | /chats/{id}/labels | Fügt einem Chat ein Label hinzu. Für Stage-Tracking, Routing, Stop-Reply-Flags. |
| PATCH | /chats/{id} | Aktualisiert Chat-Metadaten — Verantwortlicher (responsible_email), Lesestatus und chatgpt_autoresponse_enabled. Das letzte abschalten, bevor dein Agent zu antworten anfängt, sonst rennt TLs eingebauter Responder gegen deinen. |
| PATCH | /messages/{uid}/reactions | Setzt ein Reaktions-Emoji auf einer Nachricht. Body nimmt das reale Emoji-Zeichen, keinen Shortcode. |
| POST | /files_upload | Lädt eine Datei per multipart/form-data hoch (Feld: file). Gibt data.uid zurück, das du an chat/messages als file_uid weitergibst. Keine Upload-per-URL-Variante. |
| POST | /webhooks | Registriert eine Webhook-Subscription. |
| PUT | /webhooks/{id} | Aktualisiert oder aktiviert/deaktiviert eine Subscription. |
| DELETE | /webhooks/{id} | Entfernt eine Subscription. |
Wichtig zu wissen
Ein paar Details, die in der Referenz leicht zu übersehen sind und dich jeweils eine Stunde kosten, wenn du sie nicht kennst.
Wenn API-Calls fehlschlagen
Fünf Fehlermodi werden deinen Skill irgendwann treffen. Jeder sieht in der Antwort anders aus und jeder hat eine andere richtige Reaktion.
Ein kleines Shell-Muster, das auf jeden Status-Code verzweigt:
$ # Capture the HTTP status into a variable, then branch
$ http_code=$(curl -sS -o /tmp/resp.json -w "%{http_code}" \
-X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/messages)
$ case "$http_code" in
200|201) ;; # ok, carry on
400) echo "bad body: $(cat /tmp/resp.json)"; exit 1 ;;
401) echo "token expired - rotate"; exit 2 ;;
429) sleep 30; retry_once ;; # respect Retry-After
5*) sleep $((2 ** attempt)); retry_with_backoff ;;
esacSicher in Produktion betreiben
Zwei weitere Dinge, die keine Fehler sind, dir aber später einen schlechten Tag ersparen.
Gesprächskontext
Das Webhook liefert deinem Skill eine einzelne Nachricht — die gerade eingetroffene. Keine vorherigen Turns, kein Chat-Verlauf, kein laufendes Transkript. Für ein zustandsloses FAQ oder einen After-Hours-Responder reicht das völlig. Für einen Gesprächsagent, der sich daran erinnern muss, was der Kunde drei Turns zuvor gesagt hat, muss dein Skill den Kontext selbst holen. Drei Muster decken die gesamte Spannweite ab — vom billigsten möglichen Loop bis zu dem, der wirklich erinnert.
Zustandslos
Antworte nur auf die letzte Nachricht. Ein POST pro Turn, kein GET, kein Verlauf. Das ist der einfachste Loop und der Default in den fertigen Skills. Nutze ihn für FAQ-Bots, After-Hours-Responder und Klassifikation oder Routing — überall, wo die Antwort nur von der aktuellen Nachricht abhängt. Der Agent vergisst alles zwischen den Turns: wenn der Kunde ein Follow-up schreibt, das sich auf etwas Früheres bezieht, wird er es nicht verstehen.
Volles Kontextfenster
Hol die letzten 20 Nachrichten vor jeder Antwort und gib sie dem Modell als bisheriges Gespräch mit. Zwei API-Calls pro Turn statt einem, plus die zusätzlichen Prompt-Token bei jedem Call. Nutze das für Gesprächsagents, Multi-Turn-Qualifizierung und alles, wo der Agent den Faden behalten muss. Zwanzig jüngste Turns reichen meistens — breiter zu gehen hilft selten und verteuert den Prompt; schmaler zu gehen lässt den Agent vergessen, was der Kunde vor einer Minute gesagt hat.
Adaptiver Kontext
Hol den Kontext nur, wenn die eingehende Nachricht wie ein Follow-up aussieht. Eine billige Heuristik auf dem Text — beginnt mit einem Pronomen, bezieht sich auf „das“ oder „es“, kommt innerhalb von 30 Sekunden nach deiner vorherigen Antwort, kurze Einwort-Acknowledgments — entscheidet, ob du Verlauf holst oder zustandslos antwortest. Die meisten Turns bleiben billig; Follow-ups bekommen Gedächtnis. Fang mit dem vollen Fenster an und wechsle nur dann auf adaptiv, wenn du dein Kostenbudget kennst und echten Produktionstraffic hast, um die Heuristik zu tunen.
Das Fenster zu holen ist ein einzelnes GET. Die Antwort ist ein flaches Array von Nachrichten des Chats, die neueste zuletzt, gemischt aus eingehenden Kundennachrichten, deinen eigenen ausgehenden Antworten und allen Notizen, die dein Skill geschrieben hat:
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats/12345678/messages?limit=20"
{"status":"ok","data":[
{"message_uid":"INBOUND-UID-PLACEHOLDER",
"text":"hi, is the blue one still available?",
"sender_phone":"+15550200",
"message_type":"text","origin":"WhatsApp",
"created_at":"2026-04-14T10:22:03Z"},
{"message_uid":"OUTBOUND-UID-PLACEHOLDER",
"text":"Yes, one left. Want me to hold it for you?",
"sender_phone":"",
"message_type":"text","origin":"Public API",
"created_at":"2026-04-14T10:22:41Z"},
{"message_uid":"INBOUND-UID-PLACEHOLDER-2",
"text":"yes please, until tomorrow",
"sender_phone":"+15550200",
"message_type":"text","origin":"WhatsApp",
"created_at":"2026-04-14T10:23:08Z"}
]}Fallen, die in der Produktion beißen
State-Persistenz
OpenClaw-Skills halten zwischen Aufrufen keinen State im Speicher. WhatsApp-Gespräche sind multi-turn. Die Lösung ist, den State am Chat selbst zu speichern:
- •Labels halten die diskrete Stage — discovery/q1, qualified, escalate. Hinzufügen mit POST /chats/{id}/labels, lesen mit GET /chats/{id}/labels.
- •Notizen halten strukturierte Daten — team_size=8, Antwort-Entwürfe, Lead-Scores. Hinzufügen mit POST /chats/{id}/notes. Lesen durch Iterieren über GET /chats/{id}/messages und Filter auf message_type == "note".
Vorteil: Crash-Sicherheit, Sichtbarkeit für menschliche Teamkollegen, sauberer Handoff — ein Mensch kann ein Label löschen, um den Flow zurückzuspulen, oder escalate hinzufügen, um zu übernehmen. Der Trade-off: jede State-Transition ist ein HTTP-Call. Für kundenbezogene Flows ist das in Ordnung.
Aus der richtigen Nummer senden
Wenn dein Workspace mehr als eine WhatsApp-Nummer verbunden hat, muss dein Skill sicherstellen, dass er aus der gemeinten Nummer sendet. Jeder Chat hat ein Feld whatsapp_account_id mit der vollen JID (wie TELEFON@s.whatsapp.net) der besitzenden Nummer. Wenn du POST /chats/{id}/messages aufrufst, ist der Sender immer diese JID — du wählst sie nicht, der Chat tut es.
Das Muster:
- 1Hardcode die erlaubte Sender-JID im Environment jedes Skills (z. B. ALLOWED_SENDER_JID).
- 2Vor dem Senden GET /chats/{id} und vergleich whatsapp_account_id mit deiner erlaubten JID.
- 3Wenn sie nicht passen, überspring den Send — an einen Menschen eskalieren oder das Event verwerfen.
Zwei zusätzliche HTTP-Calls pro Turn, null Chance, aus dem falschen Persona zu senden. Für Single-Number-Workspaces gilt das nicht.
Eingehend
Eingehende Nachrichten — was dein Agent verarbeiten kann
Jedes Mal, wenn ein Kunde an deine WhatsApp-Nummer schreibt, feuert TimelinesAI ein message:received:new-Event an deinen Webhook mit Chat-ID, Text, Sender-Telefon und Anhängen. Dein Skill liest das Event, entscheidet, was zu tun ist, und antwortet mit POST /chats/{chat_id}/messages. Alles unten ist eine Variation dieser Schleife.
Auf jede eingehende Nachricht automatisch antworten
“Beantworte Fragen zu Versand, Rücksendungen und Öffnungszeiten. Für alles andere entwirf eine Antwort und markiere den Chat zur Überprüfung.”
“Zwischen 22:00 und 08:00 antworte automatisch. Während der Geschäftszeiten flagge mir nur die eingehenden Chats.”
“Übernimm meine WhatsApp-Antworten, während ich in diesem Meeting bin.”
der Skill empfängt das Webhook-Payload, formuliert eine Antwort und ruft POST /chats/{id}/messages mit {"text":"..."} auf. Standardmäßig zustandslos — ein API-Call pro Turn. Für Mehrfach-Turn-Gespräche, in denen der Agent sich an frühere Nachrichten erinnern muss, siehe Gesprächskontext unten.
FAQ-Handler mit Eskalation an einen Menschen
“Beantworte Fragen zu Versand, Rücksendungen und Öffnungszeiten. Für alles andere markiere den Chat needs-human und höre auf zu antworten, bis ich das Label lösche.”
vor dem Antworten GET /chats/{id}/labels prüfen. Hat der Chat needs-human, ohne Sendung verlassen. Passt der eingehende Text zu einem FAQ-Thema, antworten. Sonst POST /chats/{id}/labels mit dem Eskalations-Tag und stillschweigend verlassen. Der Posteingang deines Teams filtert nach dem Label.
Gespräche an die richtige Person routen
“Bestimme für jeden eingehenden Chat, ob es um Sales, Support oder Billing geht, und tagge ihn. Weise Sales-Chats an alex@ours zu und Billing an jamie@ours.”
Intent aus dem Text klassifizieren, POST /chats/{id}/labels mit intent/sales o. ä., dann PATCH /chats/{id} mit {"responsible_email":"..."}, um den Chat im TimelinesAI-Posteingang zu übergeben.
Leads durch eine Fragesequenz qualifizieren
“Für jeden neuen Chat aus unserer Facebook-Kampagne nach Use Case, Teamgröße und Timeline fragen. Antworten als Notizen am Chat speichern. Tagge qualified, wenn die Teamgröße 5 oder mehr ist.”
Labels markieren, bei welcher Frage du gerade bist (discovery/q1, q2, q3); Notizen speichern die Antworten. Pro Turn: aktuelles Stage-Label lesen, eingehenden Text als Antwort parsen, mit POST /chats/{id}/notes ablegen, Label fortschreiben, nächste Frage stellen. Keine externe Datenbank — siehe State-Persistenz unten.
$ # 1. Read current stage from the chat's labels. Labels nest under
$ # data.labels as a string array — see Response shapes in Things to know.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels
{"status":"ok","data":{"labels":["discovery/q1"]}}
$ # 2. Save the customer's answer as a structured note. Notes are stored
$ # as messages with message_type=note and aren't pushed to WhatsApp.
$ cat > /tmp/note.json <<'JSON'
{"text":"team_size=12"}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/note.json \
https://app.timelines.ai/integrations/api/chats/12345678/notes
{"status":"ok","data":{"message_uid":"NOTE-UID-PLACEHOLDER"}}
$ # 3. Advance the stage label. POST /labels REPLACES the full set, so
$ # read existing labels, swap q1 for q2, POST the combined list. The
$ # body key is "labels" (plural, array) — not "label" singular.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels \
| jq -c '.data.labels | map(if . == "discovery/q1" then "discovery/q2" else . end) | {labels: .}' \
> /tmp/labels.json
$ cat /tmp/labels.json
{"labels":["discovery/q2"]}
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/labels.json \
https://app.timelines.ai/integrations/api/chats/12345678/labels
{"status":"ok","data":{"labels":["discovery/q2"]}}Fotos, PDFs und Belege verstehen
“Wenn ein Kunde ein Foto eines Belegs schickt, extrahiere Betrag und Händler und füg sie als Notiz hinzu.”
“Wenn jemand ein PDF schickt, klassifiziere es als Rechnung / Vertrag / Ausweis und tagge den Chat entsprechend.”
Webhook-Payloads enthalten eine Anhang-URL. Lade sie inline im Handler herunter (sie läuft schnell ab), verarbeite mit den Vision- oder Dokument-Tools von OpenClaw und schreib die extrahierten Daten via POST /chats/{id}/notes zurück.
$ # The attachment URL in the webhook payload expires in ~15 minutes.
$ # Download it inline in the receiver, before queuing async work.
$ ATTACH_URL="https://files.timelines.ai/abc123/receipt.jpg?expires=..."
$ curl -sS "$ATTACH_URL" -o /tmp/receipt.jpg
$ # Process /tmp/receipt.jpg with OpenClaw vision tools, then write
$ # the extracted fields back as a structured note on the chat:
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary '{"text":"receipt: amount=$42.50 vendor=Cafe Madrid"}' \
https://app.timelines.ai/integrations/api/chats/12345678/notesSprachnachrichten transkribieren und antworten
“Transkribiere eingehende Sprachnachrichten. Antworte als Text — wenn du wirklich eine Sprachantwort brauchst, schick sie aus dem TimelinesAI-Dashboard.”
der Webhook liefert eine URL zur Sprachdatei. Herunterladen, transkribieren, Textantwort via POST /chats/{id}/messages formulieren. Sprachantworten sind heute ein Feature des TimelinesAI-Dashboards und stehen nicht in der aktuellen Public-API-Referenz — wenn dein Skill Sprachantworten programmatisch senden muss, kontaktier den TimelinesAI-Support, um zu prüfen, ob der Legacy-voice_message-Endpoint in deinem Workspace noch verfügbar ist.
Die Sprache des Kunden treffen
“Wenn der Kunde auf Spanisch schreibt, antworte auf Spanisch. Wenn er mitten im Gespräch die Sprache wechselt, wechsle mit.”
reines OpenClaw-seitiges Reasoning über den eingehenden Text. TimelinesAI trägt nur die Antwort.
Auf Nachrichten reagieren, ohne eine vollständige Antwort zu senden
“Reagiere mit 👀 auf jede eingehende Nachricht, damit Kunden wissen, dass ich sie gesehen habe, dann formuliere ich die echte Antwort in Ruhe.”
PATCH /messages/{uid}/reactions mit {"reaction":"👀"}. Das reaction-Feld muss das reale Emoji-Zeichen tragen — Shortcodes wie "eyes" oder ":eyes:" werden mit HTTP 400 "Reaction has invalid format" abgelehnt. Kein Nachrichten-Credit verbraucht — Reaktionen sind leichtgewichtig.
$ # Send the raw emoji character in the body. Save to a UTF-8 file and
$ # use --data-binary so shells can't downgrade it.
$ printf '{"reaction":"👀"}' > /tmp/react.json
$ curl -sS -X PATCH \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/react.json \
https://app.timelines.ai/integrations/api/messages/INBOUND-UID-PLACEHOLDER/reactions
{"status":"ok"}Ausgehend
Ausgehend — Nachrichten, die dein Agent startet
Nachrichten, die dein Agent initiiert, keine Antworten. Ausgelöst durch ein Event in deinen anderen Tools (eine neue Bestellung, eine fehlgeschlagene Zahlung, ein gebuchtes Meeting) oder eine direkte menschliche Anweisung zu einer bestimmten Person.
Vor jeder Fähigkeit in diesem Abschnitt: der Kunde hat den Thread entweder vor Kurzem geöffnet (innerhalb des 24-Stunden-Session-Fensters von WhatsApp) oder erwartet diese Nachricht explizit. Wenn keines davon stimmt, sende es nicht von einer Personal Number — das ist Business-API-Land.
Eine transaktionale Nachricht per Name oder an einen neuen Empfänger senden
“Sag John, dass seine Rechnung bereit ist.”
“Schick dem Klempner unsere neue Büroadresse, damit er die Teile liefern kann.”
“Schick den unterschriebenen Vertrag an den Kunden, der gerade die Anzahlung überwiesen hat.”
für einen bestehenden Chat per GET /chats?name=John (oder dein CRM) suchen und POST /chats/{id}/messages aufrufen. Für einen neuen Empfänger POST /messages mit {"phone":"+...","text":"..."}. Der whatsapp-send-Skill im Companion-Repo handhabt beide Modi mit UTF-8-sicherer Serialisierung.
$ cat > /tmp/send.json <<'JSON'
{"phone":"+15550200",
"text":"Hi - your order shipped. Tracking: ABC123."}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}Geplante Follow-ups innerhalb eines aktiven Gesprächs
“Jeden Montag um 9 Uhr Chats mit Tag to-follow-up prüfen, die in den letzten 24 Stunden Kundenaktivität hatten, aber keine Antwort von uns, und einen sanften Reminder schicken.”
plane es mit einem OpenClaw-Cronjob oder einer Standing Order (siehe OpenClaws Automation-Docs). Der Job holt das Publikum mit GET /chats?label=to-follow-up&read=false, liest last_message_timestamp jedes Chats und sendet nur, wenn der letzte Kunden-Turn weniger als 24 Stunden zurückliegt. Außerhalb dieses Fensters wird ein Follow-up zu Re-Engagement und du bist wieder im Business-API-Land — siehe Channel choice.
Nachrichten aus Events in deinen anderen Tools auslösen
“Wenn ein HubSpot-Deal auf 'demo scheduled' geht, sende eine WhatsApp-Bestätigung mit dem Meeting-Link.”
“Wenn Stripe eine fehlgeschlagene Zahlung meldet, sende eine höfliche Recovery-Nachricht mit einem Link zum Karten-Update.”
“Wenn eine Calendly-Buchung erstellt wird, sende am Morgen einen Pre-Meeting-Reminder.”
dein bestehendes Tool (HubSpot, Stripe, Calendly, Pipedrive) feuert seinen eigenen Webhook auf denselben Receiver, den du im Setup eingerichtet hast — füg einen Handler hinzu, der auf Event-Source oder Request-Pfad verzweigt, branche vom TimelinesAI-Handler ab und ruf POST /messages (neuer Empfänger) oder POST /chats/{id}/messages (in einen bestehenden Chat) auf. WhatsApp wird zum Auslieferungskanal für jeden Workflow, den du schon hast. Da die Sendungen einer Kundenaktion im Upstream-Tool folgen, sind sie von Natur aus transaktional — voll im Rahmen der Personal-Number-Regeln.
Dateien und Dokumente auf Anfrage senden
“Generiere das Angebot-PDF und schick es dem Kunden, der gerade nach Preisen gefragt hat.”
“Schick den Vertrag als PDF per E-Mail und leg ihn auch in den WhatsApp-Chat des Kunden.”
zwei Schritte. Datei-Bytes mit POST /files_upload als multipart/form-data hochladen (es gibt keinen Upload-per-URL-Endpoint — dein Agent muss die Datei zuerst herunterladen und dann POSTen). Die Antwort liefert eine uid, die du an POST /chats/{id}/messages im Feld file_uid weitergibst. Der Kunde hat das Dokument angefragt — das ist eine Antwort auf seine Bitte, kein Cold Outreach.
$ # Step 1 - upload the file bytes as multipart/form-data.
$ # The form field name is "file". The response gives you a uid.
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-F "file=@/path/to/quote.pdf" \
https://app.timelines.ai/integrations/api/files_upload
{"status":"ok","data":{
"uid":"FILE-UID-PLACEHOLDER",
"filename":"quote.pdf",
"size":128453,
"mimetype":"application/pdf",
"uploaded_by_email":"you@yourcompany.com",
"uploaded_at":"2026-04-14 10:22:03 +0000",
"temporary_download_url":"https://tl-prod-data.s3.amazonaws.com/..."
}}
$ # Step 2 - attach the uploaded file to a chat message using file_uid.
$ cat > /tmp/send.json <<'JSON'
{"text":"Quote attached for your review.",
"file_uid":"FILE-UID-PLACEHOLDER"}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/chats/12345678/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}Prüfen, ob eine Nachricht tatsächlich zugestellt wurde
“Hat John die Rechnung wirklich bekommen, die ich heute Morgen geschickt habe?”
jeder Send liefert eine message_uid. Später gibt GET /messages/{uid}/status_history die Sent / Delivered / Read-Timeline zurück. Die Zustellung erfolgt normalerweise innerhalb von ein bis zwei Sekunden auf einer aktiven Nummer. Der whatsapp-delivery-check-Skill im Companion-Repo kapselt das.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/messages/OUTBOUND-UID-PLACEHOLDER/status_history
{"status":"ok","data":[
{"status":"Sent", "timestamp":"2026-04-12 12:28:40 +0000"},
{"status":"Delivered","timestamp":"2026-04-12 12:28:41 +0000"}
]}CRM
CRM und Analyse
Lese-Endpoints geben deinem Agent genug Daten, um analytische Fragen zu beantworten und State mit deinem CRM in natürlicher Sprache zu synchronisieren.
Reaktionszeit-Reporting
“Wie war unsere durchschnittliche Erstantwortzeit auf WhatsApp diese Woche?”
“Wer in meinem Team ist beim Antworten am langsamsten?”
jüngste Chats holen, dann für jeden Chat die Nachrichtenzeitleiste holen, die erste ausgehende Nachricht nach jeder eingehenden finden und die Deltas client-seitig aggregieren. Zwei Endpoints, kein spezieller Analytics-Call.
$ # Pull the last page of a chat's messages (50 per page, fixed).
$ # Messages nest under data.messages — not a flat .data[] array.
$ # FILTER message_type=="whatsapp" to exclude notes (see Things to know
$ # below — notes live in the same timeline with from_me=false and would
$ # otherwise corrupt the response-time calculation).
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats/12345678/messages?page=1" \
| jq -r '.data.messages[] | select(.message_type=="whatsapp") | "\(.timestamp)\tfrom_me=\(.from_me)\t\(.text[0:40])"'
# 2026-04-12 12:28:40 +0300 from_me=false Hi - is the order shipped?
# 2026-04-12 12:30:15 +0300 from_me=true Yes - tracking ABC123, ETA 2-3
# 2026-04-12 14:02:11 +0300 from_me=false Got it, thanks!
# ...
$ # Compute first-reply latency from each consecutive (false -> true)
$ # pair, then average across all chats. Pure client-side aggregation.
$ # For full history walk ?page=2, ?page=3, ... until data.has_more_pages is false.Erkennung unbeantworteter Nachrichten
“Wie viele Nachrichten haben wir gestern erhalten? Wie viele sind noch unbeantwortet?”
“Zeig mir jeden Chat mit einer eingehenden Nachricht in den letzten 24 Stunden ohne Antwort.”
GET /chats?read=false für ungelesene Chats, dann auf .data.messages[] filtern wo message_type=="whatsapp" UND from_me==false — Notes tragen auch from_me=false und würden den Unbeantwortet-Count aufblähen. Siehe Wichtig zu wissen.
Gespräche on demand zusammenfassen
“Fasse das gesamte Gespräch mit ACME Corp zusammen. Was sind ihre Pain Points?”
“Gib mir ein einseitiges Briefing zu jedem Chat, den ich noch nicht beantwortet habe.”
GET /chats/{id}/messages holen, auf .data.messages[] mit message_type=="whatsapp" filtern (Notes würden sonst die früheren Kritzeleien deines eigenen Agents wie Kundenaussagen in die Zusammenfassung schleusen — siehe Wichtig zu wissen), dann OpenClaw zusammenfassen lassen. Mit ?page=N paginieren für lange Threads.
Dein CRM mit WhatsApp-Aktivität anreichern
“Für jeden neuen Chat dieser Woche die Nummer in HubSpot nachschlagen. Wenn es ein Kontakt ist, den Chat mit seinem Deal-Stage taggen. Wenn nicht, den Kontakt anlegen.”
TimelinesAI-Reads mit der API deines CRMs kombinieren. Ergebnisse via POST /chats/{id}/labels und POST /chats/{id}/notes zurückschreiben.
$ # 1. Pull recent chats with their phone numbers. /chats returns 50 per
$ # page under data.chats — paginate with ?page=N if you need more.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats?page=1" \
| jq -r '.data.chats[] | "\(.id)\t\(.phone)"'
# 12345678 +15550100
# 12345679 +15550200
# ...
$ # 2. For each phone, look up your CRM (HubSpot/Pipedrive/Close).
$ # Pseudo-code:
$ # contact = hubspot.search_by_phone(phone)
$ # stage = contact.deal_stage if contact else "unknown"
$ # 3. Add the deal stage label WITHOUT wiping existing labels.
$ # POST /labels is REPLACE semantics (full set), so GET current labels,
$ # append the new one, and POST the combined list.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels \
| jq -c '.data.labels + ["hubspot/qualified-lead"] | unique | {labels: .}' \
> /tmp/labels.json
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/labels.json \
https://app.timelines.ai/integrations/api/chats/12345678/labelsLeads aus dem Gesprächsinhalt scoren
“Score jeden Chat mit Tag inbound-lead von 1 bis 10 nach Fit und Dringlichkeit. Schreib den Score als Notiz.”
LLM-Reasoning über GET /chats/{id}/messages — vorher auf message_type=="whatsapp" filtern, sonst werden deine eigenen früheren Lead-Score-Notes als Kundenaussagen re-gelesen und der Score driftet bei jedem Durchlauf. Ergebnis via POST /chats/{id}/notes mit berechenbarem Prefix schreiben (z.B. "lead_score: fit=8 urgency=6"), damit dein nächster Lauf es finden und überschreiben kann.
Betrieb
Skalieren und Handoff
Muster für Human-in-the-Loop-Workflows, Multi-Agent-Routing und Conversation-Memory. Alle vier Fähigkeiten unten sind so entworfen, dass sie mit Menschen koexistieren, die dieselben Chats aus dem geteilten Posteingang von TimelinesAI bearbeiten.
Antworten für menschliche Überprüfung entwerfen statt zu senden
“Für jede neue eingehende Nachricht eine Antwort entwerfen und als Notiz speichern. Nicht senden — ich prüfe und sende selbst.”
POST /chats/{id}/notes mit dem Entwurfstext statt /messages. Die Notiz erscheint in derselben Chat-Ansicht, die dein Team bereits nutzt.
An einen Menschen übergeben, wenn der Agent feststeckt
“Wenn das Gespräch mehr als 5 Turns ohne Auflösung dauert, oder der Kunde nach einem Menschen fragt, tagge escalate und höre auf zu antworten, bis ich das Label entferne.”
Turns mit GET /chats/{id}/messages zählen, Stop-Reply-Labels mit GET /chats/{id}/labels vor jedem Send prüfen. Löst die Eskalation aus, POST /chats/{id}/labels mit escalate und beenden.
$ # 1. Count outbound 'whatsapp' messages (skip notes) in this chat.
$ # Messages nest under data.messages — not a flat .data[] array.
$ TURNS=$(curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
"https://app.timelines.ai/integrations/api/chats/12345678/messages?page=1" \
| jq '[.data.messages[] | select(.from_me==true and .message_type=="whatsapp")] | length')
$ echo $TURNS
8
$ # 2. Past the threshold? Append 'escalate' to the chat's labels and stop.
$ # POST /labels REPLACES the full set, so read existing first, combine,
$ # then POST — otherwise you wipe every other label on the chat.
$ if [ "$TURNS" -ge 5 ]; then
curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/chats/12345678/labels \
| jq -c '.data.labels + ["escalate"] | unique | {labels: .}' \
> /tmp/labels.json
curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/labels.json \
https://app.timelines.ai/integrations/api/chats/12345678/labels
fi
$ # 3. Before every future send, GET /chats/{id}/labels and bail
$ # if .data.labels contains 'escalate' or 'needs-human'.Mehrere spezialisierte Agents auf einem Posteingang laufen lassen
“Sales-AI übernimmt Preisfragen, Support-AI übernimmt Produktfragen. Nach Intent routen; wenn beide unsicher sind, an mich eskalieren.”
zwei Spezialisten-Skills in einem OpenClaw-Workspace (einer für Sales, einer für Support) plus ein dritter Intent-Klassifikations-Skill, der zuerst auf jede eingehende Nachricht läuft, den Chat mit dem gewählten Spezialisten taggt und rausgeht. Jeder Spezialist liest das Intent-Label vor dem Antworten und bricht ab, wenn es auf den anderen zeigt — so feuert exakt ein Skill pro Nachricht.
Sich an frühere Gespräche mit demselben Kunden erinnern
“Letzte Woche hast du erwähnt, dass du verreist warst — wie war’s?”
OpenClaws eigenes Memory plus GET /chats/{id}/messages für die volle WhatsApp-Historie. Der Verlauf überlebt zwischen Aufrufen, weil er auf TimelinesAI lebt.
Teste deinen Agent end-to-end mit einer zweiten WhatsApp-Nummer
Nach dem Setup kannst du die Capabilities hier gelistet sehen, aber du kannst deinen Agent nicht wirklich arbeiten sehen, bis ein echter Kunde ihn anschreibt. Das ist ein schlechter Loop zum Iterieren. Hier ein besserer: verbinde eine zweite WhatsApp-Nummer mit demselben TimelinesAI-Workspace, benutze sie als gescripteten Kunden und beobachte, wie dein echter Agent-Skill die Konversation end-to-end führt. Keine Mocks, keine fake Webhooks — es ist dein echter Agent, deine echte API, nur mit einer zweiten Nummer, die die Rolle des Menschen auf der anderen Seite spielt.
Das Muster
TimelinesAI lässt mehrere WhatsApp-Nummern in einem Workspace leben und routet ihre eingehenden Webhooks durch denselben Receiver. Der Trick ist das whatsapp_account_id-Feld auf jedem Webhook-Payload — es trägt den JID der Nummer, die die Nachricht empfangen hat. Lies es, schalte darauf, und dispatche entweder zum Persona-Schritt (wenn dein Agent dem Kunden gerade geantwortet hat) oder zum Agent-Schritt (wenn der Kunde deinem Agent gerade geschrieben hat). Jede Seite sendet über das normale POST /messages oder POST /chats/{id}/messages aus Sicht der anderen Seite.
Architektur
[+1 555 0100] [+1 555 0200]
persona agent under test
| |
| one TimelinesAI workspace |
| |
+-------> Public API <-------+
|
message:received:new
|
v
+--------------------------+
| Your test receiver |
| switch (accountJid) { |
| persona_jid -> next |
| agent_jid -> skill |
| } |
+--------------------------+Persona-Sender
Die Persona-Seite ist ein gescripteter Kunde: eine Liste von Zeilen, die sie als Nächstes sagt. Wenn es Zeit für den nächsten Turn ist, POST an /messages mit der Telefonnummer des Agents als Empfänger. TimelinesAI macht das Routing, weil beide Nummern in einem Workspace sind.
// persona-sender.js — send the next scripted customer turn from the
// persona WhatsApp number to the agent-under-test number. TimelinesAI
// routes the send because both numbers share one workspace.
const PERSONA_SCRIPT = [
"hi I saw your ad",
"we want to reduce customer churn",
"we are 25 people",
"we need to roll out in Q3",
];
let idx = 0;
export async function sendNextPersonaTurn() {
if (idx >= PERSONA_SCRIPT.length) return;
const text = PERSONA_SCRIPT[idx++];
await fetch("https://app.timelines.ai/integrations/api/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TIMELINES_AI_API_KEY}`,
"Content-Type": "application/json",
},
body: Buffer.from(JSON.stringify({
phone: process.env.AGENT_UNDER_TEST_PHONE,
text,
}), "utf-8"),
});
console.log(`[persona] sent: ${text}`);
}Receiver mit JID-Switch
Ein Receiver, ein Branch pro Seite. Alles andere, was du schon aus dem Setup kennst — die ?secret=-Auth, der from_me-Echo-Filter, der flache-vs-verschachtelte Payload-Fallback — gilt hier unverändert.
// api/webhook.js — route events by which number received them
export default async function handler(req, res) {
if (req.query.secret !== process.env.WEBHOOK_SECRET) return res.status(404).end();
const { event_type, data } = req.body || {};
if (event_type !== "message:received:new") return res.status(200).end();
if (data?.from_me === true) return res.status(200).end();
res.status(200).json({ ok: true });
const accountJid = data.whatsapp_account_id ?? data.chat?.whatsapp_account_id;
if (accountJid === process.env.AGENT_UNDER_TEST_JID) {
// Customer just messaged the agent — run your agent-under-test skill
await handleAgentTurn(data);
} else if (accountJid === process.env.PERSONA_JID) {
// Agent just replied to the persona — advance the scripted scenario
await advancePersona(data);
}
}Diese zwei Snippets sind die Kern-Idee. Der volle Harness — Persona-Skripte, Scenario-Loader, CLI-Kickoff, farbiger Split-Pane-Logger, Vercel-Config — lebt in examples/test-harness/ im Companion-Repo, mit einer eigenen README, die durch Deploy, Env Vars, Webhook-Registrierung und das erste Scenario end-to-end führt.
Zusehen, wie es passiert
Der Harness läuft autonom — du musst nicht jede Antwort freigeben. Deine Aufsicht passiert im TimelinesAI-Dashboard: öffne den Chat zwischen deiner Persona- und Agent-Nummer in einem anderen Browser-Tab, und jeder Turn erscheint live. Wenn der Agent etwas Dummes sagt, schick eine manuelle Antwort aus der Agent-Inbox, um die Konversation zu lenken. Wenn du Verhalten mitten im Run ändern musst, setz ein Label oder schreib eine Note auf den Chat — beides ist für den Agent-Skill beim nächsten Turn sichtbar. Stopp den Run, indem du den registrierten Webhook löschst oder die Vercel-Funktion abschaltest; das Scenario hält sofort an.
Beispiel: ein vollständiger Round Trip
Eine konkrete Fünf-Schritt-Schleife, die zeigt, wie die API in der Praxis aussieht — wie dein Agent einen Outbound sendet, die Zustellung bestätigt, die Antwort des Kunden verarbeitet und ein Follow-up sendet. Telefonnummern, Chat-IDs und message UIDs unten sind Platzhalter — ersetz sie durch deine eigenen.
Im gesamten Beispiel verwendete Platzhalter:
Your business number → +1 555 0100 (JID: 15550100@s.whatsapp.net) Your customer's number → +1 555 0200 API token → $TIMELINES_AI_API_KEY
Prüfe, dass dein Token funktioniert, und liste deine verbundenen Nummern auf
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/whatsapp_accounts
{"status":"ok","data":{"whatsapp_accounts":[
{"id":"15550100@s.whatsapp.net","phone":"+15550100",
"status":"active","account_name":"Your Business"}
]}}Du solltest eine Liste deiner verbundenen Nummern sehen. Wenn der Status nicht active ist, behebe das im TimelinesAI-Dashboard, bevor du weitermachst.
Sende eine ausgehende Nachricht
Schreib das Payload mit expliziter UTF-8-Codierung in eine Datei und gib es dann curl. Das ist das Muster für jeden Send.
$ cat > /tmp/send.json <<'JSON'
{"phone":"+15550200",
"text":"Hi - your order shipped. Tracking: ABC123."}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/send.json \
https://app.timelines.ai/integrations/api/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}Die Antwort ist eine Quittung, keine Zustellbestätigung. Heb die message_uid für den nächsten Schritt auf.
Prüfe den Zustellstatus
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
.../messages/OUTBOUND-UID-PLACEHOLDER/status_history
{"status":"ok","data":[
{"status":"Sent", "timestamp":"2026-04-12 12:28:40 +0000"},
{"status":"Delivered","timestamp":"2026-04-12 12:28:41 +0000"}
]}Sent → Delivered passiert auf einer aktiven Nummer typischerweise innerhalb einer Sekunde. Der Read-Status erscheint später, wenn der Empfänger den Chat tatsächlich öffnet.
Der Kunde antwortet, dein Webhook feuert
Wenn der Kunde antwortet, postet TimelinesAI an die Webhook-URL, die du registriert hast:
{
"event_type": "message:received:new",
"data": {
"chat_id": 12345678,
"message_uid": "INBOUND-UID-PLACEHOLDER",
"sender_phone": "+15550200",
"sender_name": "Customer",
"text": "Thanks! When will it arrive?",
"timestamp": "2026-04-12 12:29:20 +0000"
}
}Dein Receiver acked sofort mit 200 und reicht das Payload dann an deinen OpenClaw-Skill weiter.
Antworte aus demselben Chat zurück
$ cat > /tmp/reply.json <<'JSON'
{"text":"Estimated delivery is 2-3 business days. You'll get tracking updates to this chat."}
JSON
$ curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @/tmp/reply.json \
https://app.timelines.ai/integrations/api/chats/12345678/messages
{"status":"ok","data":{"message_uid":"REPLY-UID-PLACEHOLDER"}}Da du in einen bestehenden Chat über /chats/{id}/messages sendest, ist der Sender automatisch die WhatsApp-Nummer, der der Chat gehört — du wählst sie nicht, der Chat-Datensatz tut es. Genau deshalb sendest du in Multi-Nummer-Workspaces nicht versehentlich von der falschen Nummer.
Grenzen und Vorbehalte
Was dieser Guide NICHT abdeckt und warum.
Verwandte Guides
Wenn du das für einen echten Kundenkanal baust, decken diese drei Seiten angrenzendes Terrain ab, das du früher oder später berührst.
Mehrere WhatsApp-Nummern verbinden →
Mehrere Personal Numbers aus einem Workspace betreiben, mit Shared-Inbox-Routing und Per-Number-JID-Pinning.
HubSpot + WhatsApp-Integration →
Gespräche mit HubSpot-Deals synchronisieren — das nächste Off-the-shelf-Companion zu Fähigkeit #11 (ereignisgesteuerte Sends).
ChatGPT-Agents für WhatsApp →
Die Nicht-OpenClaw-Variante derselben Idee — ein gemanagter Autoresponder, den du im Dashboard konfigurierst, ohne Skill.
Bausteine
Companion-Skill-Bundle auf GitHub →4 funktionierende Skills, ein Vercel-Webhook-Receiver, Compliance-Docs und ein vollständiger Mirror dieses Guides. MIT-lizenziert.
TimelinesAI API-Docs · OpenClaw Skills Docs · v0.1.0-Release
Fähigkeits-Guide · 2026 · Kanonische URL timelines.ai/guide/openclaw-whatsapp-skills