Saw someone ask about this in the Vapi community, so I put together a working version.
A business wants to call a customer, play a 15-20 second announcement, then hang up automatically.
Just: "Hey, you have 100 unused points. Thank you, goodbye."
CRM/webhook trigger
↓
outbound call
↓
capture live controlUrl
↓
send say command
↓
auto-end after playback
Vapi gives every live call its own controlUrl.
So instead of hardcoding a websocket/control endpoint, the workflow grabs the controlUrl from the call response and uses it for that specific call.
That makes the flow reusable with dynamic CRM data:
curl 'https://api.vapi.ai/call' \
-H 'authorization: Bearer YOUR_API_KEY' \
-H 'content-type: application/json' \
--data-raw '{
"assistantId": "YOUR_ASSISTANT_ID",
"customer": {
"number": "+12345678913"
},
"phoneNumberId": "YOUR_PHONE_NUMBER_ID"
}'{
"id": "7420f27a-30fd-4f49-a995-5549ae7cc00d",
"assistantId": "5b0a4a08-133c-4146-9315-0984f8c6be80",
"phoneNumberId": "42b4b25d-031e-4786-857f-63b346c9580f",
"type": "outboundPhoneCall",
"status": "queued",
"monitor": {
"listenUrl": "wss://.../transport",
"controlUrl": "https://.../control"
}
}Use monitor.controlUrl from the response for step 2.
curl -X POST 'CONTROL_URL_FROM_PREVIOUS_RESPONSE' \
-H 'content-type: application/json' \
--data-raw '{
"type": "say",
"content": "Hello, you currently have 100 unused points within our supermarket rewards program. Please redeem them soon. Thank you, goodbye.",
"endCallAfterSpoken": true
}'No conversational flow required.
Sometimes the call is not fully connected immediately after creation. Adding a small delay improves reliability.
2–5 second delay before sending the control request.
Click to copy, then import into n8n.
{
"name": "Vapi Outbound Announcement Flow",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "vapi-announcement",
"options": {}
},
"id": "da34565f-8461-4144-ac91-2b802a285831",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
496,
-32
],
"webhookId": "vapi-announcement"
},
{
"parameters": {
"method": "POST",
"url": "https://api.vapi.ai/call",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "authorization",
"value": "Bearer YOUR_VAPI_API_KEY"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"assistantId\":\"YOUR_ASSISTANT_ID\",\"customer\":{\"number\":\"{{$json.phone}}\"},\"phoneNumberId\":\"YOUR_PHONE_NUMBER_ID\"}",
"options": {}
},
"id": "e93dcbe6-1a6e-40f7-b258-dcc752549997",
"name": "Create Call",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
720,
-32
]
},
{
"parameters": {
"amount": 3,
"unit": "seconds"
},
"id": "b45f7188-a7f2-4982-a2c9-59e23f7abace",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
944,
-32
]
},
{
"parameters": {
"values": {
"string": [
{
"name": "callId",
"value": "={{$node[\"Create Call\"].json[\"id\"]}}"
},
{
"name": "name",
"value": "={{$node[\"Webhook\"].json[\"name\"]}}"
},
{
"name": "controlUrl",
"value": "={{$node[\"Create Call\"].json[\"monitor\"][\"controlUrl\"]}}"
}
],
"number": [
{
"name": "points",
"value": "={{$node[\"Webhook\"].json[\"points\"]}}"
}
]
},
"options": {}
},
"id": "abea84c7-f8af-487a-992f-affec169bd06",
"name": "Prepare Data",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [
1168,
-32
]
},
{
"parameters": {
"method": "POST",
"url": "={{$json.controlUrl}}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"type\":\"say\",\"content\":\"Hello {{$json.name}}, you currently have {{$json.points}} unused points within our supermarket rewards program. Please redeem them soon. Thank you, goodbye.\",\"endCallAfterSpoken\":true}",
"options": {}
},
"id": "c2efcc52-8984-4d71-bb65-d86397eccd20",
"name": "Send Announcement",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
1392,
-32
]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Create Call",
"type": "main",
"index": 0
}
]
]
},
"Create Call": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Prepare Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare Data": {
"main": [
[
{
"node": "Send Announcement",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"id": "3WV8r1xfkKNb7aLx"
}This is much cleaner than building a full conversational assistant when you only need one-way announcements, reminders, or notifications.
Built after watching multiple people accidentally turn a 20-second supermarket announcement into a distributed AI orchestration platform. An industry tradition.