Build a Voice Agent in 90 Seconds with the WFW API
The best way to understand what the WFW API actually does is to use it. This post walks through the complete path from zero to a deployed, callable voice agent using nothing but curl and a business URL.
No SDK. No dashboard. No system prompt to write. Five API calls.
Prerequisites
- A WFW account (sign up at workforcewave.com — $50 free API credit, no credit card required)
- Service account credentials from Settings → API Keys in the dashboard
- A publicly-accessible business URL (we'll use a dental practice as the example)
curlandjq(optional, for pretty-printing JSON responses)
That's it. You don't need a Twilio account, an ElevenLabs account, or any voice infrastructure. WFW handles all of that.
Step 1: Get an Access Token
WFW uses OAuth 2.1 client credentials for bot authentication. Your service account ID and secret exchange for a short-lived access token.
curl -s -X POST https://api.workforcewave.com/v2/auth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "sa_your_service_account_id",
"client_secret": "sk_live_your_secret",
"grant_type": "client_credentials"
}' | jq .
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scopes": ["agents:write", "agents:read", "calls:read", "calls:write"]
}
The token is valid for 1 hour. Store it in a variable for the subsequent calls:
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Step 2: Create the Agent
This is the call that kicks off Workforce Wave provisioning. You're passing a business URL; Workforce Wave does everything else — crawls the site, extracts content, generates the system prompt, seeds the KB, configures ElevenLabs, provisions the phone number.
curl -s -X POST https://api.workforcewave.com/v2/agents \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: provision-ridgeline-dental-001" \
-d '{
"payload": {
"name": "Ridgeline Dental AI",
"platform": "elevenlabs",
"business_url": "https://ridgelinedental.com",
"template_id": "dental_receptionist"
}
}' | jq .
{
"operation_id": "op_rldnt_7c3a2f",
"status": "pending",
"estimated_seconds": 90,
"created_at": "2026-06-15T14:22:08Z"
}
The response comes back immediately — this is a 202 Accepted, not a 200 OK. The provisioning is happening asynchronously. Save your operation_id.
A few things worth noting about this request:
The Idempotency-Key header — if your network drops and you retry this request with the same key, you'll get the same operation_id back instead of creating a duplicate agent. Always include this for provisioning calls.
templateid: dentalreceptionist — this sets the vertical for Workforce Wave. The template determines which VIL (Vertical Intelligence Layer) gets injected, which compliance constraints apply (HIPAA in this case), and which tool integrations are enabled by default.
platform: elevenlabs — the voice synthesis platform. ElevenLabs is the current default.
Step 3: Poll for Completion
Workforce Wave takes 75–120 seconds. Poll the operation endpoint until status is active (or failed).
curl -s https://api.workforcewave.com/v2/operations/op_rldnt_7c3a2f \
-H "Authorization: Bearer $TOKEN" | jq .
While pending:
{
"operation_id": "op_rldnt_7c3a2f",
"status": "pending",
"stage": "kb_seeding",
"progress_percent": 62,
"estimated_seconds_remaining": 34
}
When complete:
{
"operation_id": "op_rldnt_7c3a2f",
"status": "active",
"agent_id": "agt_rldnt_a8b2c1",
"phone_number": "+18435552847",
"provisioning_time_seconds": 88,
"provisioning_notes": {
"scout_confidence": "high",
"pages_crawled": 23,
"kb_documents_created": 4,
"fallback_used": false
}
}
scoutconfidence: high means Workforce Wave found sufficient content. pagescrawled: 23 and kbdocumentscreated: 4 tell you the KB structure that was built. Save your agent_id.
If you'd rather not poll, subscribe to the agent.activated webhook before you POST. You'll get a push notification the moment the agent goes live.
Step 4: Make a Test Call
Your agent is live on +18435552847. You can call it from any phone right now. But you can also initiate a programmatic test call via the API:
curl -s -X POST https://api.workforcewave.com/v2/calls/initiate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-call-rldnt-001" \
-d '{
"agent_id": "agt_rldnt_a8b2c1",
"to": "+18435559876",
"context": {
"call_purpose": "test",
"test_scenario": "appointment_inquiry"
}
}' | jq .
{
"call_id": "call_rldnt_cc4d5e",
"status": "queued",
"estimated_start_seconds": 3,
"to": "+18435559876",
"agent_id": "agt_rldnt_a8b2c1"
}
The call will connect in seconds. You'll hear Ridgeline Dental's new voice agent introduce itself using the practice name and persona Workforce Wave extracted from the site.
Step 5: Get the Transcript
Once the call ends, the transcript is available within a few seconds:
curl -s https://api.workforcewave.com/v2/calls/call_rldnt_cc4d5e/transcript \
-H "Authorization: Bearer $TOKEN" | jq .
{
"call_id": "call_rldnt_cc4d5e",
"agent_id": "agt_rldnt_a8b2c1",
"duration_seconds": 47,
"turns": [
{
"speaker": "agent",
"text": "Thank you for calling Ridgeline Dental, this is Aria. How can I help you today?",
"timestamp_ms": 820
},
{
"speaker": "caller",
"text": "Hi, I'd like to schedule a new patient appointment.",
"timestamp_ms": 4210
},
{
"speaker": "agent",
"text": "Of course! I'd be happy to help you get that scheduled. Are you a new patient with us?",
"timestamp_ms": 5080
}
],
"extractions": {
"intent": "appointment_schedule",
"patient_type": "new_patient",
"appointment_booked": false,
"disposition": "inquiry_completed",
"escalation_triggered": false
}
}
The extractions block is structured data pulled from the conversation — intent, outcome, disposition, and any custom extraction fields configured for the agent. This is what downstream systems read when they want to know what happened on a call without parsing the transcript text.
The JavaScript Version
Here's the same five-step flow in Node.js — no SDK, just native fetch:
const BASE = 'https://api.workforcewave.com/v2';
// Step 1: Get token
const { access_token: token } = await fetch(`${BASE}/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.WFW_CLIENT_ID,
client_secret: process.env.WFW_CLIENT_SECRET,
grant_type: 'client_credentials'
})
}).then(r => r.json());
const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
// Step 2: Create agent
const { operation_id } = await fetch(`${BASE}/agents`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': 'provision-ridgeline-001' },
body: JSON.stringify({
payload: {
name: 'Ridgeline Dental AI',
platform: 'elevenlabs',
business_url: 'https://ridgelinedental.com',
template_id: 'dental_receptionist'
}
})
}).then(r => r.json());
// Step 3: Poll until active
let agent_id;
while (true) {
const op = await fetch(`${BASE}/operations/${operation_id}`, { headers }).then(r => r.json());
if (op.status === 'active') { agent_id = op.agent_id; break; }
if (op.status === 'failed') throw new Error(`Provisioning failed: ${op.error?.message}`);
await new Promise(resolve => setTimeout(resolve, 5000)); // poll every 5s
}
// Step 4: Initiate test call
const { call_id } = await fetch(`${BASE}/calls/initiate`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': 'test-call-001' },
body: JSON.stringify({ agent_id, to: process.env.TEST_PHONE_NUMBER })
}).then(r => r.json());
// Step 5: Get transcript (wait for call to end first)
// In production, subscribe to call.transcript_ready webhook instead of polling
const transcript = await fetch(`${BASE}/calls/${call_id}/transcript`, { headers })
.then(r => r.json());
console.log('Agent provisioned:', agent_id);
console.log('Call transcript:', transcript.turns.length, 'turns');
What You Didn't Have to Do
You did not:
- Write a system prompt
- Configure ElevenLabs
- Set up a Twilio account or phone number
- Handle webhook infrastructure
- Write HIPAA compliance logic
- Build an intent classification layer
- Configure call escalation rules
All of that happened inside Workforce Wave's provisioning pipeline. The API surface is the 20% that operators and builders interact with. The other 80% — the vertical intelligence, the compliance layer, the voice synthesis configuration, the KB structure — is handled by the platform.
That's the design principle: the API should expose enough for a developer to build anything, while the defaults should be good enough that you don't have to configure anything you don't want to.
Next in this series: Bot Auth vs. Human Auth: How WFW Handles Two Different Identities — the technical design behind WFW's two-surface authentication architecture.
Ready to put AI voice agents to work in your business?
Get a Live Demo — It's FreeContinue Reading
Related Articles
Rate Limiting and Idempotency: What Your Bot Needs to Know
The two most important API patterns for AI consumers of the WFW API — with concrete examples and a production-ready TypeScript client.
Workforce Wave AI: The Engine Behind Auto-Provisioning
What happens inside the 5-step Workforce Wave pipeline when a partner enters a business URL, why partners get an operationId instead of a 30-second wait, and how ww_operations powers the fleet dashboard progress bar.
The Bot Creation Matrix: Four Ways to Deploy AI, Now All Live on WFW
Dual-mode agent support just shipped, completing the Bot Creation Matrix. WFW is now the only platform where a bot can be the creator and the consumer — entirely human-free.