Build a voice for your OpenClaw bot.
Give your bot (an OpenClaw bot, a script, anything) the power to make real phone calls in a personality you define. Create an account, tell Workforce Wave who your bot is, add a card, and it starts calling.
API base used throughout: https://wfw-admin.vercel.app
1. The one thing to understand: your bot defines the voice agent.
Workforce Wave gives every account a voice agent. Your bot supplies that agent's identity by sending a personality and a greeting. The agent's name, its job, its tone, and its rules all live inside those two fields. Changing the bot is just sending new text.
If you run an OpenClaw bot, you literally hand your bot's own persona and task to the voice agent and it becomes the caller. Fill in the identity block below, and the guide turns it into the exact request that defines your bot.
2. Edit this - your bot's identity.
Fill these in. Everything else in the guide reads from them. You can change them anytime by repeating Step 4.
BOT_NAME = Nova
BOT_ROLE = an AI scheduling assistant for Acme Plumbing
BOT_TASK = Call customers to confirm tomorrow's appointments and reschedule if needed.
GREETING = Hi, this is Nova from Acme Plumbing. I'm calling to confirm your appointment.
VOICE_ID = (leave blank for the default voice, or paste an ElevenLabs voice id)
TONE_RULES = Be warm, brief, and natural. One or two short sentences per turn. Never invent facts; if unsure, say you will have someone follow up.These assemble into the agent's personality (its system prompt) and firstMessage (the opening line it speaks). You can re-send them anytime to rebrand the bot on the fly.
3. Two kinds of access.
Setup calls use your account session. Everything after that uses your bot's scoped key.
| Phase | What you call | Auth |
|---|---|---|
| One-time setup | signup, login, initialize | Your account session (login cookie) |
| Everything after | define identity, test, add card, place calls | Your bot's scoped key in the X-Bot-Key header |
Your scoped key controls only your agent. Keep it secret. You can revoke and re-mint it anytime.
4. Step by step.
Create your account
curl -X POST https://wfw-admin.vercel.app/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"name":"My Voice Bot","email":"you@example.com","phoneNumber":"+18435551234","password":"a-strong-password"}'Password must be at least 8 characters. Phone in E.164 format (+1...). This call does not log you in.
Log in and get a session
NextAuth uses a CSRF token. Fetch it first, then post credentials. Store the cookie jar for Step 3.
JAR=cookies.txt
CSRF=$(curl -s -c $JAR https://wfw-admin.vercel.app/api/auth/csrf \
| python -c "import sys,json;print(json.load(sys.stdin)['csrfToken'])")
curl -s -c $JAR -b $JAR -X POST https://wfw-admin.vercel.app/api/auth/callback/credentials \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "csrfToken=$CSRF" \
--data-urlencode "email=you@example.com" \
--data-urlencode "password=a-strong-password" \
--data-urlencode "json=true"Keep cookies.txt for the next two calls (initialize and task list).
Initialize your bot and get your key
curl -s -b cookies.txt -X POST https://wfw-admin.vercel.app/api/bots/initialize \
-H "Content-Type: application/json" -d '{}'Response (save the apiKey - it is shown once):
{ "success": true, "apiKey": "wfwk_xxxxxxxx", "agentId": "agent_xxxx", "clientId": 123 }The apiKey is your X-Bot-Key. Store it in a secret env var (e.g. WFW_BOT_KEY). All remaining calls use it.
Define your bot's identity
This is the heart of it. Send who your bot is. Using the identity block from Section 2, assemble the personality and greeting:
curl -s -X PATCH https://wfw-admin.vercel.app/api/bots/agent \
-H "X-Bot-Key: wfwk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"personality": "You are Nova, an AI scheduling assistant for Acme Plumbing. Your job: Call customers to confirm tomorrow\'s appointments and reschedule if needed. Be warm, brief, and natural. One or two short sentences per turn. Never invent facts; if unsure, say you will have someone follow up.",
"firstMessage": "Hi, this is Nova from Acme Plumbing. I\'m calling to confirm your appointment.",
"voiceId": ""
}'personality= "You are{BOT_NAME}, {BOT_ROLE}. Your job: {BOT_TASK} {TONE_RULES}" (max 8000 chars)
firstMessage = {GREETING} (max 1000 chars)
voiceId = {VOICE_ID} (omit or leave blank for the default voice)
Send at least one of the three fields. Re-send anytime to rename or repurpose the bot. The name is whatever you put in the personality and greeting.
Test it for free - no call, no charge
curl -s -X POST https://wfw-admin.vercel.app/api/bots/agent/simulate \
-H "X-Bot-Key: wfwk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"userInput":"Hi, who is this?"}'Returns a text transcript so you can verify your bot's character before paying for a live call. No call is placed. No charge.
Add a card (required before any real call)
curl -s -X POST https://wfw-admin.vercel.app/api/bots/payment-method/setup \
-H "X-Bot-Key: wfwk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"lineType":"outbound_shared"}'Returns { "url": "https://checkout.stripe.com/..." }. Open it, enter your card, and save. The instant the card saves, calling unlocks.
| lineType | Monthly fee | Inbound |
|---|---|---|
| outbound_shared | No monthly fee | Outbound only |
| dedicated | $7/month | Yes - your own number |
Both line types bill per minute. No card means test-only. If the Stripe page shows "Save my information for faster checkout," uncheck it so it does not ask for a phone number.
Place a call
curl -s -X POST https://wfw-admin.vercel.app/api/bots/calls \
-H "X-Bot-Key: wfwk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"phone_number":"+18435559876"}'Your bot calls that number and speaks as the identity you defined. Returns a taskId. Calls are queued and dialed within a minute.
See what happened
# List recent tasks using your session cookie
curl -s -b cookies.txt "https://wfw-admin.vercel.app/api/bots/tasks?limit=10"
# Or read a single task by the ID returned from the calls endpoint
# curl -s -b cookies.txt "https://wfw-admin.vercel.app/api/bots/tasks/{taskId}"5. Change the bot anytime.
Renaming or repurposing is just Step 4 again with new text. The change is live immediately for the next call.
# Become a friendly survey caller named "Sam"
curl -s -X PATCH https://wfw-admin.vercel.app/api/bots/agent \
-H "X-Bot-Key: wfwk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"personality": "You are Sam, a cheerful survey caller for Riverside Gym. Your job: ask members three quick questions about their experience. Keep it under a minute.",
"firstMessage": "Hi, this is Sam from Riverside Gym with a 30-second survey, is now okay?"
}'6. Driving it from an OpenClaw bot.
Your OpenClaw bot already has a personality and a task. Hand those straight to Workforce Wave so the phone agent IS your bot. Minimal Python wrapper:
#!/usr/bin/env python3
"""wfwbot - let an OpenClaw bot run a Workforce Wave voice line.
Subcommands:
define push this bot's name/personality/task to its WFW voice agent
call <number> place a call
simulate "<text>" free text test
Env: WFW_BOT_KEY (from /api/bots/initialize). Provisioning (signup/login/initialize)
is done once; see the steps above.
"""
import os, sys, json, urllib.request
BASE = "https://wfw-admin.vercel.app"
KEY = os.environ["WFW_BOT_KEY"]
# Your bot's identity. An OpenClaw bot can pass its OWN system prompt as PERSONALITY.
BOT_NAME = os.environ.get("BOT_NAME", "Nova")
BOT_ROLE = os.environ.get("BOT_ROLE", "an AI assistant for Workforce Wave")
BOT_TASK = os.environ.get("BOT_TASK", "Help the caller and take a message.")
GREETING = os.environ.get("BOT_GREETING", f"Hi, this is {BOT_NAME}. How can I help?")
VOICE_ID = os.environ.get("BOT_VOICE_ID", "")
RULES = "Be warm, brief, and natural. One or two short sentences per turn. Never invent facts."
def post(path, body, patch=False):
req = urllib.request.Request(BASE + path, data=json.dumps(body).encode(),
headers={"X-Bot-Key": KEY, "Content-Type": "application/json"},
method="PATCH" if patch else "POST")
return json.load(urllib.request.urlopen(req))
def define():
personality = f"You are {BOT_NAME}, {BOT_ROLE}. Your job: {BOT_TASK} {RULES}"
body = {"personality": personality, "firstMessage": GREETING}
if VOICE_ID:
body["voiceId"] = VOICE_ID
print(json.dumps(post("/api/bots/agent", body, patch=True), indent=2))
def call(number):
print(json.dumps(post("/api/bots/calls", {"phone_number": number}), indent=2))
def simulate(text):
print(json.dumps(post("/api/bots/agent/simulate", {"userInput": text}), indent=2))
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "define"
if cmd == "define": define()
elif cmd == "call": call(sys.argv[2])
elif cmd == "simulate": simulate(sys.argv[2])Usage:
export WFW_BOT_KEY=wfwk_xxxxxxxx
export BOT_NAME="Nova" BOT_ROLE="a receptionist for Acme" BOT_TASK="Book appointments."
python wfwbot.py define
python wfwbot.py simulate "who is this?"
python wfwbot.py call +18435559876To make an OpenClaw skill out of it, register wfwbot.py as a skill command and let the model call define (with its own persona/task) and call <number>.
7. Editable fields reference.
Every field you can change on the fly, where it goes, and what it controls.
| Field | Endpoint | What it controls | Limit |
|---|---|---|---|
| personality | PATCH /api/bots/agent | The bot's name, role, task, tone, and rules (its system prompt) | 8000 chars |
| firstMessage | PATCH /api/bots/agent | The exact opening line it speaks on every outbound call | 1000 chars |
| voiceId | PATCH /api/bots/agent | ElevenLabs voice ID (omit or leave blank for the default voice) | string |
| lineType | POST /api/bots/payment-method/setup | outbound_shared ($0/mo) or dedicated ($7/mo, adds inbound) | enum |
| phone_number | POST /api/bots/calls | Who to call. Must be E.164 format (e.g. +18435559876) | string |
The bot's name is set by the words you put in personality and firstMessage(for example "You are Nova..."). Renaming is just editing those two fields.
8. Good to know.
Auth
Setup uses your login session cookie. Everything else uses the X-Bot-Key header. Never share the key.
Card gate
Real outbound calls return 402 Payment Required until a card is on file. Free text simulation always works with no card.
Pricing
Per-minute usage billed monthly to your card. The dedicated line adds $7/month. You only pay for calls you place.
Phone format
Always E.164. Example: +18435551234. No spaces, no parentheses, country code required.
Revoke a key
DELETE /api/bots/keys/{id} with your session. Mint a new key by calling /api/bots/initialize again.
Personality changes are instant
No redeploy, no wait. The updated identity is live for the very next call your bot places.