TL;DR
This tutorial shows how to connect Twilio voice calls to your CRM through OpenClaw using a secure webhook, an IVR menu, and a small playbook that upserts a contact and posts a summary to Slack. The end result is a production ready pipeline with request validation, schema enforcement, and clear run logs that your team can operate. The primary outcome is faster intake and fewer manual steps through workflow automation that fits your current tools. You can complete the build in about an hour with the examples below.
What You Will Build
You will assemble a voice intake pipeline that answers a Twilio number, guides callers through a keypad menu, and forwards structured events to OpenClaw where a playbook validates, transforms, and delivers the data to your CRM. The same pattern works for HubSpot, Salesforce, or any system with an HTTP API. We will keep the scope tight so you can ship a reliable minimal viable pipeline first, then extend it as your team needs evolve.
Along the way, you will:
- Configure a Twilio phone number and IVR menu.
- Forward event payloads from Twilio to an authenticated ingestion endpoint.
- Validate requests and normalize data into a stable contract.
- Upsert a contact in your CRM with keypad responses and optional voicemail.
- Send a Slack notification with a short summary for operator visibility.
If you prefer to see the platform in action before building, skim the AI marketing automation features on the ButterGrow site to understand what ButterGrow does. The overview highlights how event ingestion, playbooks, and monitoring work together. See the entry here: AI marketing automation features.
Prerequisites
- A Twilio account with an incoming phone number and a project level auth token.
- A CRM with an API key. HubSpot and Salesforce are popular choices for this pattern.
- An OpenClaw workspace with permissions to create playbooks and environment secrets. Use the onboarding flow to get started in minutes.
- A Slack channel and an incoming webhook URL for notifications.
- Basic Node.js familiarity if you choose to deploy a small forwarding function.
Brand new to ButterGrow and OpenClaw. Start from the homepage to see how the hosted OpenClaw assistant fits into your stack: ButterGrow.
Architecture Overview
At a high level the call flow looks like this.
- A caller dials your Twilio number.
- Twilio answers with TwiML and prompts for keypad input.
- Twilio sends a webhook with call status, digits, and optional recording to your endpoint.
- Your endpoint validates the request signature and forwards a normalized JSON payload to OpenClaw.
- An OpenClaw playbook validates against a schema, enriches metadata, and upserts the lead into your CRM.
- The playbook posts a Slack message with the summary and a link to the CRM record.
Contracts First
Resist the urge to push raw webhook payloads directly into a CRM. Instead, define a small contract that all callers must satisfy. This improves reliability and makes future changes cheaper. We will use a versioned contract and a validation step to keep inputs clean and reduce operator noise.
Here is a minimal contract you can start with.
{
"schema_version": "1.0",
"call_sid": "CA1234567890abcdef",
"from_number": "+14155550100",
"to_number": "+14155559876",
"ivr_choice": "1",
"recording_url": "https://api.twilio.com/2010-04-01/Accounts/AC.../Recordings/RE...",
"call_started_at": "2026-08-21T10:20:30Z",
"campaign": "summer-ads-2026",
"source": "voice",
"notes": "caller pressed 1 then left voicemail"
}
Step 1Provision a Number and Basic Answer
Start by configuring an incoming phone number in the Twilio console. Set the Voice configuration to use a webhook or a TwiML Bin so that Twilio can answer and route calls to an IVR menu. The simplest option is a TwiML Bin that gathers a single digit and forwards the result to your forwarding endpoint.
Example TwiML for a basic menu.
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Gather numDigits="1" action="https://your-forwarder.example.com/twilio/voice" method="POST">
<Say voice="alice">Press 1 for sales. Press 2 for support. Press 3 to leave a message.</Say>
</Gather>
<Say voice="alice">We did not receive input. Goodbye.</Say>
<Hangup/>
</Response>
If you prefer a visual builder, Twilio Studio provides a canvas to wire prompts and branches without code. Both routes work for this pipeline and you can switch later without breaking the downstream contract if you keep the forwarded payload stable.
Step 2Build the IVR Flow in Studio
Design a minimal menu first, then expand. A simple flow that captures a single digit keeps your initial risk low and lets your team validate the operator experience before adding more complexity. This is the best entry point if you are wondering how to build a Twilio IVR pipeline without spending days on options you might not need.
Suggested nodes for a first pass.
- A Split Based On widget to branch on digits.
- A Record widget for voicemail when the caller chooses the message path.
- An HTTP Request widget that posts to your forwarding endpoint with the chosen digit and recording URL if it exists.
When POSTing from Studio, include the call SID, the caller number, the dialed number, a timestamp, and the menu selection. Keep field names stable for your contract mapper.
Example payload to send downstream.
{
"CallSid": "CA1234567890abcdef",
"From": "+14155550100",
"To": "+14155559876",
"Digits": "1",
"RecordingUrl": "https://api.twilio.com/2010-04-01/Accounts/AC.../Recordings/RE...",
"Timestamp": "2026-08-21T10:20:30Z"
}
Step 3Create a Forwarding Endpoint with Request Validation
You can deploy a tiny forwarding service in a serverless function, a container, or behind an API gateway. The service should validate Twilio signatures, map the incoming fields to your contract, and forward to OpenClaw. The example below uses Node.js and Express. It also demonstrates a constant time comparison for the signature.
// file: server/index.js
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.urlencoded({ extended: true })); // Twilio sends application/x-www-form-urlencoded by default
const TWILIO_AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
const OPENCLAW_INGEST_URL = process.env.OPENCLAW_INGEST_URL; // e.g. https://ingest.example.internal/twilio-voice
const OPENCLAW_TOKEN = process.env.OPENCLAW_TOKEN;
function computeSignature(url, params) {
const data = Object.keys(params)
.sort()
.reduce((acc, key) => acc + key + params[key], url);
return crypto.createHmac("sha1", TWILIO_AUTH_TOKEN).update(data).digest("base64");
}
function safeEqual(a, b) {
const buffA = Buffer.from(String(a));
const buffB = Buffer.from(String(b));
if (buffA.length !== buffB.length) return false;
return crypto.timingSafeEqual(buffA, buffB);
}
app.post("/twilio/voice", async (req, res) => {
try {
const url = `${process.env.PUBLIC_BASE_URL}${req.originalUrl}`;
const signature = req.get("X-Twilio-Signature");
const expected = computeSignature(url, req.body);
if (!safeEqual(signature || "", expected)) {
console.warn("invalid signature", { signature, expected });
return res.status(403).send("invalid signature");
}
const payload = {
schema_version: "1.0",
call_sid: req.body.CallSid,
from_number: req.body.From,
to_number: req.body.To,
ivr_choice: req.body.Digits || null,
recording_url: req.body.RecordingUrl || null,
call_started_at: req.body.Timestamp || new Date().toISOString(),
source: "voice",
notes: req.body.Digits ? `caller pressed ${req.body.Digits}` : "no digits"
};
const resp = await fetch(OPENCLAW_INGEST_URL, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${OPENCLAW_TOKEN}`
},
body: JSON.stringify(payload)
});
if (!resp.ok) {
const text = await resp.text();
console.error("forward failed", { status: resp.status, text });
return res.status(202).send("accepted but downstream rejected");
}
res.status(200).send("ok");
} catch (err) {
console.error("handler error", err);
res.status(500).send("server error");
}
});
app.listen(process.env.PORT || 3000, () => console.log("forwarder listening"));
Environment variables to set for the forwarder.
export TWILIO_AUTH_TOKEN=your_auth_token
export PUBLIC_BASE_URL=https://your-forwarder.example.com
export OPENCLAW_INGEST_URL=https://ingest.example.internal/twilio-voice
export OPENCLAW_TOKEN=your_openclaw_token
If you plan to deploy behind an API gateway, configure a secret header and allow only known source IP ranges. On the OpenClaw side, require a bearer token so only your forwarder can publish events.
Step 4Define and Enforce the Schema
OpenClaw playbooks can validate inputs against a JSON schema before any side effects. This keeps data clean and safe. Use a schema that covers the minimal fields and evolve the version as your IVR grows. The example below uses the popular draft 7 syntax.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Twilio Voice Intake",
"type": "object",
"required": ["schema_version", "call_sid", "from_number", "to_number", "call_started_at"],
"properties": {
"schema_version": { "type": "string" },
"call_sid": { "type": "string" },
"from_number": { "type": "string", "pattern": "^\\+?[0-9]{7,15}$" },
"to_number": { "type": "string", "pattern": "^\\+?[0-9]{7,15}$" },
"ivr_choice": { "type": ["string", "null"], "pattern": "^[0-9]$" },
"recording_url": { "type": ["string", "null"], "format": "uri" },
"call_started_at": { "type": "string", "format": "date-time" },
"campaign": { "type": ["string", "null"] },
"source": { "type": "string" },
"notes": { "type": ["string", "null"] }
},
"additionalProperties": false
}
If you are new to versioned contracts, our overview of OpenClaw Playbooks and versioning explains why small reusable templates pay off and how to keep migrations predictable.
Step 5Build the Playbook to Upsert in the CRM
Create a playbook with three main stages. Validate, transform, then deliver. Keep the first draft short so you can reach a clean end to end run before adding enrichments.
Example playbook in a YAML like format.
name: "twilio-voice-intake"
version: "1.0.0"
triggers:
- source: "ingest.twilio.voice"
match:
schema_version: "1.0"
steps:
- id: validate
uses: "schema.validate"
with:
schema_ref: "schemas/twilio-voice-intake.json"
- id: map
uses: "transform.object"
with:
mapping:
email: null
phone: "{{ from_number }}"
firstname: null
lastname: null
lifecycle_stage: "subscriber"
notes: "{{ notes }}"
campaign: "{{ campaign }}"
source: "{{ source }}"
- id: upsert_hubspot
uses: "http.request"
with:
url: "https://api.hubapi.com/crm/v3/objects/contacts"
method: POST
headers:
content-type: application/json
authorization: "Bearer {{ secrets.HUBSPOT_TOKEN }}"
body:
properties:
phone: "{{ steps.map.output.phone }}"
hs_lead_status: "NEW"
lifecyclestage: "subscriber"
notes_last_updated: "{{ now }}"
recent_notes: "{{ steps.map.output.notes }}"
- id: notify_slack
uses: "http.request"
with:
url: "{{ secrets.SLACK_WEBHOOK_URL }}"
method: POST
headers:
content-type: application/json
body:
text: "New voice lead from {{ from_number }} with choice {{ ivr_choice }}"
If you use Salesforce instead of HubSpot, swap the upsert step for an authenticated POST to the Contacts endpoint with the appropriate field names. Keep the transformation layer separate so you can reuse it across tools.
Reliability matters once you move past a proof of concept. Twilio may retry deliveries when your forwarder is slow or the network drops. Use idempotency keys derived from the call SID and event type so repeated deliveries do not create duplicates. We covered this pattern in detail in our idempotency, retries, and DLQs guide.
Step 6Test the Pipeline End to End
Set environment variables locally and run your forwarder. Then point your Twilio webhook to that URL and place a test call from your mobile phone. Press 1 to simulate a sales path or 3 to record a voicemail. Confirm that your Slack notification arrives and that the CRM shows a new or updated contact.
You can also simulate the webhook without dialing by posting a form encoded body to your forwarder. This is useful for automated tests in a CI environment where you might not have a real call.
curl -X POST https://your-forwarder.example.com/twilio/voice \
-H "X-Twilio-Signature: fake" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "CallSid=CA1234567890abcdef&From=%2B14155550100&To=%2B14155559876&Digits=1&Timestamp=2026-08-21T10%3A20%3A30Z"
The signature in the example is a placeholder. Your handler should reject it with a 403 when validation is enabled. For real tests, compute a signature in the same way your handler expects or use Twilio's helper libraries.
Step 7Secure and Operate the Pipeline
Security and operations are the difference between a spare time experiment and a dependable intake system. Adopt a few guardrails before you hand this over to the team.
- Verify the request signature for every inbound webhook. Treat failures as a security incident and track them with an alert.
- Restrict the forwarder to allow only known source networks and require a bearer token for the OpenClaw ingest endpoint.
- Store secrets in an environment vault with rotation policies so the team does not share tokens in chat or source control.
- Add a lightweight dashboard to track runs per hour, acceptance rate, and average processing latency. Small, descriptive metrics help teammates trust the pipeline.
If you want more examples of reusable templates and safer changes, review our overview of OpenClaw Playbooks and versioning. It outlines how to evolve contracts without breaking consumers and how to introduce approvals for higher risk changes.
Step 8Extend the Flow When the Team Is Ready
Once the minimal pipeline is stable, add small improvements.
- Enrich leads with reverse phone lookup or geo hints when allowed by your privacy policy.
- Add a second menu level for after hours routing and update your transformation mapping to include the schedule name.
- Attach the voicemail recording URL to the CRM record so reps can scan context quickly.
- Add a simple score field when the caller presses a high intent digit so your team can triage faster.
For additional context on reusable data movement patterns, browse OpenClaw Playbooks and versioning and the answers to common questions on the site.
Troubleshooting Guide
Here are some common issues and how to fix them.
- Calls are not hitting the forwarder. Check the TwiML or Studio flow action URL and confirm that the DNS and TLS configuration is correct. Use a request inspector to confirm that Twilio attempted delivery and note the HTTP status code.
- Signature validation fails in production but not locally. Confirm that the PUBLIC_BASE_URL matches the exact URL Twilio posts to, including query parameters, and that you are not performing any middleware mutations before computing the signature.
- Leads appear twice in the CRM. Confirm that your playbook uses idempotency keys and that you return status 200 when a duplicate arrives. Double check the retry policy.
- Slack notifications are missing. Inspect the downstream response and confirm that the webhook URL is correct and not rate limited. Add a small buffer with retry for the notification step if needed.
If your team wants a hosted path with built in monitoring, versioned playbooks, and a smooth onboarding, you can try ButterGrow and the hosted OpenClaw assistant. The product docs include a quickstart that shows how to set it up and how the onboarding flow works. Start with get started in minutes and scan the AI marketing automation features to see what the platform covers out of the box.
References
- Twilio request validation guidance: Official docs for computing and verifying webhook signatures.
- TwiML Voice reference: Vocabulary for building call flows and prompts.
- HubSpot Contacts API reference: Fields and endpoints for creating and updating contacts.
Frequently Asked Questions
How do I validate the X-Twilio-Signature header for inbound webhooks?+
Use Twilio's request validation with your auth token to compute an HMAC for the full URL and body, then compare it to the X-Twilio-Signature header using a constant time comparison. Reject requests that fail validation and log the reason for audit.
What JSON schema should I use to standardize call events before sending to my CRM?+
Define a minimal contract with call_sid, from_number, to_number, ivr_choice, recording_url, and timestamps. Keep optional fields like campaign and source for attribution. Version your schema and validate in the ingestion step so downstream services receive consistent data.
How can I prevent duplicate leads when Twilio retries webhooks?+
Use the call_sid and an event type as a deterministic idempotency key. Store a short lived key in a cache or database during processing. If a duplicate arrives, short circuit the pipeline and return 200 to stop further retries while preserving delivery guarantees.
What CRM should I start with for a simple proof of concept?+
HubSpot is a fast option due to its simple Contacts API. If you already use Salesforce, the same pattern applies with the REST API. Start with one field mapping and expand once the pipeline is stable and your team is comfortable with the new process.
Can I collect DTMF responses and a voicemail in the same flow?+
Yes. Capture keypad digits in the IVR menu first to qualify the caller, then branch to a Record step to capture voicemail. Send both the digits and recording URL in the payload to OpenClaw so the CRM entry contains the full context.
Where should I add alerts so the team knows about failed runs?+
Add a final step that posts a message to Slack only when the playbook status is not success. Include the call_sid, error class, and a link to the run in your observability tool so on call responders can triage quickly without logging into multiple systems.
Ready to try ButterGrow?
See how ButterGrow can supercharge your growth with a quick demo.
Book a Demo