Back to portfolio
Voice AI / Automation

Vapi + n8n: Outbound Announcement + Auto Hangup

Vapin8nWebhooksCRMVoice AIOutbound CallsAutomationAPI Integration

Saw someone ask about this in the Vapi community, so I put together a working version.

The use case

A business wants to call a customer, play a 15-20 second announcement, then hang up automatically.

No conversation.No input.No agent loop.

Just: "Hey, you have 100 unused points. Thank you, goodbye."

How it works

CRM/webhook trigger

↓

outbound call

↓

capture live controlUrl

↓

send say command

↓

auto-end after playback

The main detail

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:

  • → customer name
  • → points balance
  • → appointment time
  • → invoice amount
  • → renewal date
  • → whatever the use case needs

Use cases

Appointment reminders
Billing alerts
Loyalty reminders
Renewal notifications
Lead reactivation
Delivery alerts
Pharmacy pickup
Insurance notices

Flow overview

01Create outbound call
02Get monitor.controlUrl from response
03Send "say" control message
04endCallAfterSpoken: true
05Call automatically ends

Step 1 — Create the outbound call

BASH
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"
  }'

Example response

JSON
{
  "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.

Step 2 — Send the announcement

BASH
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
  }'

What happens

  • Play the announcement
  • Automatically hang up afterward
  • Never wait for customer input

No conversational flow required.

Production tips

Add a small delay before control

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.

n8n workflow JSON

Click to copy, then import into n8n.

View on Gist
JSON
{
  "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"
}

Why this approach

This is much cleaner than building a full conversational assistant when you only need one-way announcements, reminders, or notifications.

Lower latency
Lower cost
No transcription needed
No interruption handling
CRM-friendly
Automation-friendly

Works well with

n8nMake.comZapierGoHighLevelHubSpotAirtableCustom CRMs

Useful docs

Built after watching multiple people accidentally turn a 20-second supermarket announcement into a distributed AI orchestration platform. An industry tradition.

Back to portfolio