Build a WhatsApp agent
with OpenClaw + TimelinesAI
Turn WhatsApp into a customer channel your OpenClaw agent operates on: auto-reply to incoming messages, send transactional and event-triggered notifications, sync with your CRM, share the inbox with teammates. TimelinesAI runs the WhatsApp gateway. Your agent handles the reasoning.
What your agent can do with WhatsApp
With a TimelinesAI skill installed, your OpenClaw agent can operate WhatsApp as a real customer channel. The long version of this list is in the sections below — every capability has the specific API calls it needs.
Answer incoming customer messages automatically — 24/7 autoresponder, after-hours responder, or full AI chatbot that escalates to a human when stuck.
Send transactional and event-triggered messages — order confirmations, shipping updates, appointment reminders, payment receipts, and notifications triggered by events in other tools (HubSpot, Stripe, Calendly). Only to customers who expect the message.
Qualify leads through multi-turn conversations — ask a sequence of questions, store answers, tag the chat as qualified or not.
Sync WhatsApp activity with your CRM — look up incoming numbers in HubSpot/Pipedrive, update deal stages, push notes back.
Summarize and score conversations — "what did ACME ask about last week", "score this chat 1–10 on intent".
Share the inbox with teammates — your agent drafts replies as private notes, humans send them; or your agent sends and humans watch.
Handle media — photos of receipts, PDFs, voice notes — processed by OpenClaw, replied to automatically.
Setup
Four things to wire together. After this, your skill just makes API calls.
Connect your WhatsApp number
Sign in at app.timelines.ai, scan the QR code with the phone that holds your business number. TimelinesAI runs the gateway from here on.
Get an API token
Integrations → Public Api → Copy. Save as TIMELINES_AI_API_KEY. One token covers the whole workspace.
Install a skill from the companion repo
4 ready-made skills + webhook receiver + docs at InitechSoftware/openclaw-whatsapp-skills. Clone, symlink into your OpenClaw skills/, done.
Register a webhook
Point message:received:new at a public HTTPS URL. TimelinesAI will push incoming messages to it. Your receiver invokes OpenClaw.
Smoke-test the token
Before you build anything, confirm auth works:
curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
https://app.timelines.ai/integrations/api/whatsapp_accountsYou should see a JSON list of your connected numbers, each with an id (JID), phone, status, and account_name. If not, see Tips below.
Register the webhook once
curl -sS -X POST \
-H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event_type":"message:received:new","url":"https://your-app.example.com/webhook","enabled":true}' \
https://app.timelines.ai/integrations/api/webhooksMinimal webhook receiver (Node / Vercel)
~30 lines. Accepts the webhook POST, hands incoming messages off to OpenClaw. Return 2xx within 5 seconds or TimelinesAI retries.
// api/webhook.js
export default async function handler(req, res) {
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 });
}
// Ack fast, then hand off to the agent.
res.status(200).json({ ok: true });
await fetch(process.env.OPENCLAW_HOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: data.chat_id,
text: data.text,
sender_phone: data.sender_phone,
}),
});
}Personal numbers vs WhatsApp Business API
Before you build outbound flows, understand which WhatsApp channel your use case belongs to. Picking wrong gets numbers banned.
Safe with the skills in this guide (personal number)
- •Inbound conversations. Customer messages you first, you reply. No risk.
- •Transactional sends. Order confirmations, shipping updates, delivery notifications, payment receipts, appointment reminders — any message a customer expects because they just did something with your business.
- •Event-triggered notifications from other tools. HubSpot deal → demo confirmation, Stripe failed payment → recovery note, Calendly booking → pre-meeting reminder. The customer opted in when they used the upstream tool.
- •Replying inside WhatsApp's 24-hour customer service window. Once a customer messages you, you have 24 hours to reply freely. The autoresponder, FAQ handler, and lead qualifier skills operate inside this window.
NOT safe on personal numbers
- •Cold outreach to lists you bought or scraped — WhatsApp bans numbers for this within hours.
- •Marketing broadcasts to customers who didn't specifically opt in.
- •Promotional campaigns, sales offers, seasonal pushes, product launches.
- •Anything resembling a marketing blast. If you're asking “what throughput can I get”, you're on the wrong channel.
How to tell which channel you need
- 1.Did the customer message you first, or are you replying inside an active 24-hour session? → Personal number, this guide.
- 2.Is the customer about to receive something they explicitly expect (order, appointment, payment, delivery)? → Personal number, transactional send.
- 3.Triggered by a customer-opted-in event in your CRM or billing tool? → Personal number, event-triggered send.
- 4.Broadcast, cold outreach, or promotional campaign? → Business API, use the TimelinesAI dashboard.
- 5.Not sure? → Don't send. Treat it as promotional and route it to the Business API path.
Every outbound capability below assumes you've passed this test. If you're not sure, re-read this section before shipping. TimelinesAI cannot protect you from the WhatsApp-layer ban — the ban is enforced upstream, at WhatsApp's infrastructure, not at the gateway.
Things to know
A few details that are easy to miss from the reference and will each cost you an hour if you don't know them.
GET /chats works. GET /chats/ returns the TimelinesAI 404 page — which looks like a network problem and isn't. Every URL in this guide is written without a trailing slash on purpose.curl --data-binary @file.json instead of inline -d "...".https://app.timelines.ai/integrations/api. Some older blog posts reference a different subdomain with an X-API-KEY header — that's outdated. Use Bearer auth on app.timelines.ai/integrations/api.POST /messages returns a message_uid — that's a receipt, not a delivery confirmation. Use GET /messages/{uid}/status_history to check actual delivery.API reference
Every endpoint you need to build every capability below. Base URL https://app.timelines.ai/integrations/api. Auth Authorization: Bearer $TIMELINES_AI_API_KEY.
Reading
| Method | Path | What it returns |
|---|---|---|
| GET | /whatsapp_accounts | Your connected WhatsApp numbers, each with JID, phone, status, account name. |
| GET | /chats | Chat list. Supports ?phone=... and ?label=... filters. Each chat has a whatsapp_account_id field holding the JID of the number that owns it. |
| GET | /chats/{id} | One chat's full detail. |
| GET | /chats/{id}/messages | Message history with ?limit=N. Includes from_me, sender_phone, text, timestamp, message_type (whatsapp vs note). |
| GET | /chats/{id}/labels | Labels on the chat. |
| GET | /messages/{uid}/status_history | Sent / Delivered / Read timeline for an outbound message. |
| GET | /messages/{uid}/reactions | Reactions on a message. |
| GET | /files | Files you uploaded via the API. |
| GET | /webhooks | Your registered webhook subscriptions. |
Writing
| Method | Path | What it does |
|---|---|---|
| POST | /messages | Send to a phone number. Body: {"phone":"+...","text":"..."}. Returns {"message_uid":"..."}. |
| POST | /chats/{id}/messages | Send into an existing chat. Body: {"text":"..."}. Sender is whichever WhatsApp number owns the chat. |
| POST | /chats/{id}/notes | Attach a private note to a chat. Not sent to WhatsApp, visible only inside TimelinesAI. Used for agent state and draft-review workflows. |
| POST | /chats/{id}/labels | Add a label to a chat. Use for stage tracking, routing, stop-reply flags. |
| PATCH | /chats/{id} | Update chat metadata — assignee, read state. |
| PUT | /messages/{uid}/reactions | Set a reaction emoji on a message. |
| POST | /files | Upload a file by URL. TimelinesAI fetches and hosts it. |
| POST | /files_upload | Multipart file upload. |
| POST | /webhooks | Register a webhook subscription. |
| PUT | /webhooks/{id} | Update or enable/disable a subscription. |
| DELETE | /webhooks/{id} | Remove a subscription. |
Incoming
Incoming messages — what your agent can handle
Every time a customer messages your WhatsApp number, TimelinesAI fires a message:received:new event to your webhook with the chat id, text, sender phone, and attachments. Your skill reads the event, decides what to do, and replies with POST /chats/{chat_id}/messages. Everything below is a variation on that loop.
Auto-reply to every incoming message
“Answer questions about shipping, returns, and opening hours. For anything else, draft a reply and tag the chat for review.”
“Between 22:00 and 08:00 reply automatically. During business hours just flag incoming chats for me.”
“Handle my WhatsApp replies while I'm in this meeting.”
skill receives the webhook payload, composes a reply, calls POST /chats/{id}/messages with {"text":"..."}. One API call per turn, no state.
FAQ handler with escalation to a human
“Answer questions about shipping, returns, and opening hours. For anything else, tag the chat needs-human and stop replying until I clear the tag.”
before replying, check GET /chats/{id}/labels. If the chat has needs-human, exit without sending. If the incoming text matches an FAQ topic, reply. Otherwise POST /chats/{id}/labels with the escalation tag and exit silently. Your team's inbox filters on the label.
Route conversations to the right person
“For each incoming chat, figure out if it's sales, support, or billing and tag it. Assign sales chats to alex@ours and billing to jamie@ours.”
classify intent from the text, POST /chats/{id}/labels with intent/sales or similar, then PATCH /chats/{id} with {"responsible_email":"..."} to hand the chat off in the TimelinesAI inbox.
Qualify leads through a question sequence
“For any new chat from our Facebook ad campaign, ask about their use case, team size, and timeline. Store answers as notes on the chat. Tag it qualified if team size is 5 or more.”
labels track which question you're currently on (discovery/q1, q2, q3), notes hold the answers. Each turn: read the current stage label, parse incoming text as the answer, write it via POST /chats/{id}/notes, advance the label, ask the next question. No external database — see State persistence below.
Understand photos, PDFs, and receipts
“When a customer sends a photo of a receipt, extract the amount and vendor and add them as a note.”
“If someone sends a PDF, classify it as invoice / contract / ID and tag the chat accordingly.”
webhook payloads include an attachment URL. Download it in the handler (it expires quickly), process with OpenClaw's vision or document tools, write extracted data back via POST /chats/{id}/notes.
Transcribe voice notes and reply
“Transcribe inbound voice notes. Reply in text if it's short, or back as a voice note if the answer is long.”
webhook delivers a URL to the voice file. Download, transcribe, compose a reply. Text replies via POST /chats/{id}/messages; voice replies via POST /chats/{id}/voice_message (multipart .ogg or .mp3).
Match the customer's language
“If the customer writes in Spanish, reply in Spanish. Switch languages mid-conversation if they do.”
pure OpenClaw-side reasoning on the incoming text. TimelinesAI just carries the reply.
React to messages without sending a full reply
“React with 👀 to every incoming message so customers know I've seen it, then take my time composing the real reply.”
PUT /messages/{message_uid}/reactions with the emoji. No message credit consumed — reactions are lightweight.
Outbound
Outbound — messages your agent sends
These are messages your agent starts, not replies. Triggered by a customer action your other tools detect (new order, failed payment, demo scheduled), or by a direct human instruction about a specific person. All of these assume the customer either messaged you first or explicitly expects a transactional message — if neither is true, it belongs on WhatsApp Business API, not here.
Send a transactional message by name or to a new recipient
“Message John that his invoice is ready.”
“Text the plumber our new office address so he can deliver the parts.”
“Send the signed contract to the client who just wired the deposit.”
for an existing chat, look it up via GET /chats?name=John (or your CRM) and call POST /chats/{id}/messages. For a new recipient, POST /messages with {"phone":"+...","text":"..."}. The whatsapp-send skill in the companion repo handles both modes with UTF-8-safe serialization.
$ 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"}}Scheduled follow-ups inside an active conversation
“Every Monday at 9am, check chats tagged to-follow-up that had customer activity in the last 24 hours but no reply from us, and send a gentle check-in.”
OpenClaw's scheduling skill fires on the cron, pulls the audience with GET /chats?label=to-follow-up, filters to chats where the last customer message was less than 24 hours ago, and sends. The 24-hour filter is critical — outside that window, a follow-up becomes a re-engagement touch and you're back to Business API territory.
Trigger messages from events in your other tools
“When a HubSpot deal hits 'demo scheduled', send a WhatsApp confirmation with the meeting link.”
“When Stripe reports a failed payment, send a polite recovery message with a link to update the card.”
“When a Calendly booking is created, send a pre-meeting reminder the morning of.”
your existing tool (HubSpot, Stripe, Calendly, Pipedrive) fires its own webhook into your agent. The agent classifies the event and calls POST /messages or POST /chats/{id}/messages. This is the highest-value pattern in the guide — WhatsApp becomes a delivery channel for any workflow you already have, and the sends are transactional by nature because they're downstream of a customer action in the upstream tool.
Send files and documents on request
“Generate the quote PDF and send it to the customer who just asked for pricing.”
“Email the contract as a PDF and also drop it in the WhatsApp chat for the client.”
two steps. Upload the file via POST /files (by URL) or POST /files_upload (multipart). Then reference it in POST /chats/{id}/messages. The customer asked for the document — this is a reply to their request, not cold outreach.
Check whether a message was actually delivered
“Did John actually receive the invoice message I sent this morning?”
every send returns a message_uid. Later, GET /messages/{uid}/status_history returns the Sent / Delivered / Read timeline. Delivery is usually within a second or two on an active number. The whatsapp-delivery-check skill in the companion repo wraps this.
$ 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 and analytics
TimelinesAI's read endpoints give your agent enough data to answer analytical questions and sync state with your CRM in natural language.
Response time reporting
“What was our average first-reply time on WhatsApp this week?”
“Who on my team is slowest to reply?”
GET /chats + GET /chats/{id}/messages + GET /messages/{uid}/status_history. Aggregate client-side.
Unanswered message detection
“How many messages did we receive yesterday? How many are still unanswered?”
“Show me every chat with an incoming message in the last 24 hours and no reply.”
GET /chats?read=false for unread chats, then filter messages by from_me=false.
Summarize conversations on demand
“Summarize the entire conversation with ACME Corp. What are their pain points?”
“Give me a one-paragraph brief on every chat I haven't replied to yet.”
fetch GET /chats/{id}/messages, let OpenClaw summarize. Paginate for long threads.
Enrich your CRM with WhatsApp activity
“For every new chat this week, look up the number in HubSpot. If it's a contact, tag the chat with their deal stage. If not, create the contact.”
combine TimelinesAI reads with your CRM's API. Write results back to the chat via POST /chats/{id}/labels and POST /chats/{id}/notes.
Score leads from conversation content
“Score every chat tagged inbound-lead from 1 to 10 on fit and urgency. Write the score as a note.”
LLM reasoning over GET /chats/{id}/messages, result written via POST /chats/{id}/notes.
Operations
Scale and handoff
Patterns for human-in-the-loop workflows, multi-agent routing, and conversation memory.
Draft replies for human review instead of sending
“For every new incoming message, draft a reply and save it as a note. Don't send — I'll review and send them myself.”
POST /chats/{id}/notes with the draft text instead of /messages. The note appears in the same chat view your teammates already use.
Hand off to a human when the agent is stuck
“If the conversation goes more than 5 turns without resolution, or the customer asks for a human, tag escalate and stop replying until I clear the tag.”
count turns with GET /chats/{id}/messages, check stop-reply labels with GET /chats/{id}/labels before every send. If escalation triggers, POST /chats/{id}/labels with escalate and exit.
Run multiple specialized agents on one inbox
“Sales AI handles pricing questions, support AI handles product questions. Route by intent; if both are unsure, escalate to me.”
two OpenClaw agents, two skills, one workspace. An intent-classification skill runs first, tags the chat, and the specialist skills check the label before replying.
Remember past conversations with the same customer
“Last week you mentioned you were traveling — how did that go?”
OpenClaw's own memory plus GET /chats/{id}/messages for the full WhatsApp history. Chat history survives across invocations because it lives on TimelinesAI.
State persistence
OpenClaw skills don't keep state in memory across invocations. WhatsApp conversations are multi-turn. The fix is to store state on the chat itself:
- •Labels hold discrete stage —
discovery/q1,qualified,escalate. Add withPOST /chats/{id}/labels, read withGET /chats/{id}/labels. - •Notes hold structured data — team_size=8, draft replies, lead scores. Add with
POST /chats/{id}/notes. Read by iteratingGET /chats/{id}/messagesand filteringmessage_type == "note".
The upside: crash safety, visibility to human teammates, clean handoff — a human can clear a label to rewind the flow, or add escalate to take over. The trade-off: every state transition is an HTTP call. For customer-facing flows this is fine.
Sending from the right number
If your workspace has more than one WhatsApp number connected, your skill has to make sure it sends from the intended one. Every chat has a whatsapp_account_id field holding the full JID (like PHONE@s.whatsapp.net) of the number that owns it. When you POST /chats/{id}/messages, the sender is always that JID — you don't pick it, the chat does.
The pattern:
- 1Hard-code the allowed sender JID in each skill’s environment (e.g. ALLOWED_SENDER_JID).
- 2Before sending, GET /chats/{id} and compare whatsapp_account_id to your allowed JID.
- 3If they don’t match, skip the send — surface to a human or drop the event.
Two extra HTTP calls per turn, zero chance of sending from the wrong persona. For single-number workspaces this doesn't apply.
Example: one full round trip
A concrete five-step loop showing what the API looks like in practice — how your agent sends an outbound, confirms delivery, processes the customer's reply, and sends a follow-up. Phone numbers, chat IDs, and message UIDs below are placeholders — substitute your own.
Placeholders used throughout this example:
Your business number → +1 555 0100 (JID: 15550100@s.whatsapp.net) Your customer's number → +1 555 0200 API token → $TIMELINES_AI_API_KEY
Confirm your token works and list your connected numbers
$ 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"}
]}}You should see a list of your connected numbers. If the status is anything other than active, fix that in the TimelinesAI dashboard before continuing.
Send an outbound message
Write the payload to a file with UTF-8 encoding, then curl it. This is the pattern for every outbound.
$ 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"}}The response is a receipt, not a delivery confirmation. Hold on to the message_uid for the next step.
Check delivery status
$ 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 is typically within a second on an active number. The Read status appears later, when the recipient actually opens the chat.
The customer replies, your webhook fires
When the customer responds, TimelinesAI posts to your registered webhook URL:
{
"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"
}
}Your receiver acks with a 200 immediately, then hands the payload to your OpenClaw skill.
Reply back from the same chat
$ 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"}}Because you're sending into an existing chat via /chats/{id}/messages, the sender is automatically the WhatsApp number that owns the chat — you don't pick it, the chat record does. This is the whole reason you don't accidentally send from the wrong number in multi-number workspaces.
Limits and caveats
POST /messages returns a receipt, not a delivery confirmation. Use /status_history.Building blocks
Companion skill bundle on GitHub →4 working skills, a Vercel webhook receiver, compliance docs, and a full mirror of this guide. MIT licensed.
TimelinesAI API Docs · OpenClaw Skills Docs · v0.1.0 release
Capability guide · 2026 · Canonical URL timelines.ai/guide/openclaw-whatsapp-skills